diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 00000000..276a6a74 --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 420fd699726f92bf93f894befb472e90 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/_downloads/07fcc19ba03226cd3d83d4e40ec44385/auto_examples_python.zip b/_downloads/07fcc19ba03226cd3d83d4e40ec44385/auto_examples_python.zip new file mode 100644 index 00000000..2237f1bf Binary files /dev/null and b/_downloads/07fcc19ba03226cd3d83d4e40ec44385/auto_examples_python.zip differ diff --git a/_downloads/09df217f95985497f45d69e2d4bdc5b1/plot_2_example_add_feature.py b/_downloads/09df217f95985497f45d69e2d4bdc5b1/plot_2_example_add_feature.py new file mode 100644 index 00000000..3c8112cf --- /dev/null +++ b/_downloads/09df217f95985497f45d69e2d4bdc5b1/plot_2_example_add_feature.py @@ -0,0 +1,87 @@ +""" +=================== +Adding New Features +=================== + +""" +# %% +import py_neuromodulation as nm +import numpy as np +from typing import Iterable + +# %% +# In this example we will demonstrate how a new feature can be added to the existing feature pipeline. +# This can be done by creating a new feature class that implements the protocol class :class:`~nm_features.NMFeature` +# and registering it with the :func:`~nm_features.AddCustomFeature` function. + + +# %% +# Let's create a new feature class called `ChannelMean` that calculates the mean signal for each channel. +# We can optinally make it inherit from :class:`~nm_features.NMFeature` but as long as it has an adequate constructor +# and a `calc_feature` method with the appropriate signatures it will work. +# The :func:`__init__` method should take the settings, channel names and sampling frequency as arguments. +# The `calc_feature` method should take the data and a dictionary of features as arguments and return the updated dictionary. +class ChannelMean: + def __init__( + self, settings: nm.NMSettings, ch_names: Iterable[str], sfreq: float + ) -> None: + # If required for feature calculation, store the settings, + # channel names and sampling frequency (optional) + self.settings = settings + self.ch_names = ch_names + self.sfreq = sfreq + + # Here you can add any additional initialization code + # For example, you could store parameters for the functions\ + # used in the calc_feature method + + self.feature_name = "channel_mean" + + def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict: + # Here you can add any feature calculation code + # This example simply calculates the mean signal for each channel + ch_means = np.mean(data, axis=1) + + # Store the calculated features in the features_compute dictionary + # Be careful to use a unique keyfor each channel and metric you compute + for ch_idx, ch in enumerate(self.ch_names): + features_compute[f"{self.feature_name}_{ch}"] = ch_means[ch_idx] + + # Return the updated features_compute dictionary to the stream + return features_compute + + +nm.add_custom_feature("channel_mean", ChannelMean) + +# %% +# Now we can instantiate settings and observe that the new feature has been added to the list of features +settings = nm.NMSettings() # Get default settings + +settings.features + +# %% +# Let's create some artificial data to demonstrate the feature calculation. +N_CHANNELS = 5 +N_SAMPLES = 10000 # 10 seconds of random data at 1000 Hz sampling frequency + +data = np.random.random([N_CHANNELS, N_SAMPLES]) +stream = nm.Stream( + sfreq=1000, + data=data, + settings = settings, + sampling_rate_features_hz=10, + verbose=False, +) + +feature_df = stream.run() +columns = [col for col in feature_df.columns if "channel_mean" in col] + +feature_df[columns] + + +# %% +# Remove feature so that it does not interfere with other examples +nm.remove_custom_feature("channel_mean") + + + diff --git a/_downloads/0dc588ba0560fad53b77fac59849f12c/plot_1_example_BIDS.ipynb b/_downloads/0dc588ba0560fad53b77fac59849f12c/plot_1_example_BIDS.ipynb new file mode 100644 index 00000000..895b62ae --- /dev/null +++ b/_downloads/0dc588ba0560fad53b77fac59849f12c/plot_1_example_BIDS.ipynb @@ -0,0 +1,261 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n# ECoG Movement decoding example\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This example notebook read openly accessible data from the publication\n*Electrocorticography is superior to subthalamic local field potentials\nfor movement decoding in Parkinson\u2019s disease*\n(`Merk et al. 2022 _`).\nThe dataset is available [here](https://doi.org/10.7910/DVN/IO2FLM).\n\nFor simplicity one example subject is automatically shipped within\nthis repo at the *py_neuromodulation/data* folder, stored in\n[iEEG BIDS](https://www.nature.com/articles/s41597-019-0105-7) format.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from sklearn import metrics, model_selection, linear_model\nimport matplotlib.pyplot as plt\n\nimport py_neuromodulation as nm\nfrom py_neuromodulation import (\n nm_analysis,\n nm_decode,\n nm_define_nmchannels,\n nm_IO,\n nm_plots,\n NMSettings,\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's read the example using [mne_bids](https://mne.tools/mne-bids/stable/index.html).\nThe resulting raw object is of type [mne.RawArray](https://mne.tools/stable/generated/mne.io.RawArray.html).\nWe can use the properties such as sampling frequency, channel names, channel types all from the mne array and create the *nm_channels* DataFrame:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "(\n RUN_NAME,\n PATH_RUN,\n PATH_BIDS,\n PATH_OUT,\n datatype,\n) = nm_IO.get_paths_example_data()\n\n(\n raw,\n data,\n sfreq,\n line_noise,\n coord_list,\n coord_names,\n) = nm_IO.read_BIDS_data(\n PATH_RUN=PATH_RUN\n)\n\nnm_channels = nm_define_nmchannels.set_channels(\n ch_names=raw.ch_names,\n ch_types=raw.get_channel_types(),\n reference=\"default\",\n bads=raw.info[\"bads\"],\n new_names=\"default\",\n used_types=(\"ecog\", \"dbs\", \"seeg\"),\n target_keywords=[\"MOV_RIGHT\"],\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This example contains the grip force movement traces, we'll use the *MOV_RIGHT* channel as a decoding target channel.\nLet's check some of the raw feature and time series traces:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "plt.figure(figsize=(12, 4), dpi=300)\nplt.subplot(121)\nplt.plot(raw.times, data[-1, :])\nplt.xlabel(\"Time [s]\")\nplt.ylabel(\"a.u.\")\nplt.title(\"Movement label\")\nplt.xlim(0, 20)\n\nplt.subplot(122)\nfor idx, ch_name in enumerate(nm_channels.query(\"used == 1\").name):\n plt.plot(raw.times, data[idx, :] + idx * 300, label=ch_name)\nplt.legend(bbox_to_anchor=(1, 0.5), loc=\"center left\")\nplt.title(\"ECoG + STN-LFP time series\")\nplt.xlabel(\"Time [s]\")\nplt.ylabel(\"Voltage a.u.\")\nplt.xlim(0, 20)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "settings = NMSettings.get_fast_compute()\n\nsettings.features.welch = True\nsettings.features.fft = True\nsettings.features.bursts = True\nsettings.features.sharpwave_analysis = True\nsettings.features.coherence = True\n\nsettings.coherence.channels = [(\"LFP_RIGHT_0-LFP_RIGHT_2\", \"ECOG_RIGHT_0-avgref\")] \n# TONI: this example was failing because the rereferenced channel have different names than originals\n# We need to handle ch_names being changed after reref with settings.coherence.channels validation\n\nsettings.coherence.frequency_bands = [\"high beta\", \"low gamma\"]\nsettings.sharpwave_analysis_settings.estimator[\"mean\"] = []\nsettings.sharpwave_analysis_settings.sharpwave_features.enable_all()\nfor sw_feature in settings.sharpwave_analysis_settings.sharpwave_features.list_all():\n settings.sharpwave_analysis_settings.estimator[\"mean\"].append(sw_feature)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "stream = nm.Stream(\n sfreq=sfreq,\n nm_channels=nm_channels,\n settings=settings,\n line_noise=line_noise,\n coord_list=coord_list,\n coord_names=coord_names,\n verbose=True,\n)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "features = stream.run(\n data=data,\n out_path_root=PATH_OUT,\n folder_name=RUN_NAME,\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Feature Analysis Movement\nThe obtained performances can now be read and visualized using the :class:`nm_analysis.Feature_Reader`.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "# initialize analyzer\nfeature_reader = nm_analysis.FeatureReader(\n feature_dir=PATH_OUT,\n feature_file=RUN_NAME,\n)\nfeature_reader.label_name = \"MOV_RIGHT\"\nfeature_reader.label = feature_reader.feature_arr[\"MOV_RIGHT\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "feature_reader.feature_arr.iloc[100:108, -6:]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "print(feature_reader.feature_arr.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "feature_reader._get_target_ch()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "feature_reader.plot_target_averaged_channel(\n ch=\"ECOG_RIGHT_0\",\n list_feature_keywords=None,\n epoch_len=4,\n threshold=0.5,\n ytick_labelsize=7,\n figsize_x=12,\n figsize_y=12,\n)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "feature_reader.plot_all_features(\n ytick_labelsize=6,\n clim_low=-2,\n clim_high=2,\n ch_used=\"ECOG_RIGHT_0\",\n time_limit_low_s=0,\n time_limit_high_s=20,\n normalize=True,\n save=True,\n)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "nm_plots.plot_corr_matrix(\n feature=feature_reader.feature_arr.filter(regex=\"ECOG_RIGHT_0\"),\n ch_name=\"ECOG_RIGHT_0-avgref\",\n feature_names=list(\n feature_reader.feature_arr.filter(regex=\"ECOG_RIGHT_0-avgref\").columns\n ),\n feature_file=feature_reader.feature_file,\n show_plot=True,\n figsize=(15, 15),\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Decoding\n\nThe main focus of the *py_neuromodulation* pipeline is feature estimation.\nNevertheless, the user can also use the pipeline for machine learning decoding.\nIt can be used for regression and classification problems and also dimensionality reduction such as PCA and CCA.\n\nHere, we show an example using the XGBOOST classifier. The used labels came from a continuous grip force movement target, named \"MOV_RIGHT\".\n\nFirst we initialize the :class:`~nm_decode.Decoder` class, which the specified *validation method*, here being a simple 3-fold cross validation,\nthe evaluation metric, used machine learning model, and the channels we want to evaluate performances for.\n\nThere are many more implemented methods, but we will here limit it to the ones presented.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "model = linear_model.LinearRegression()\n\nfeature_reader.decoder = nm_decode.Decoder(\n features=feature_reader.feature_arr,\n label=feature_reader.label,\n label_name=feature_reader.label_name,\n used_chs=feature_reader.used_chs,\n model=model,\n eval_method=metrics.r2_score,\n cv_method=model_selection.KFold(n_splits=3, shuffle=True),\n)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "performances = feature_reader.run_ML_model(\n estimate_channels=True,\n estimate_gridpoints=False,\n estimate_all_channels_combined=True,\n save_results=True,\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The performances are a dictionary that can be transformed into a DataFrame:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "df_per = feature_reader.get_dataframe_performances(performances)\n\ndf_per" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "ax = nm_plots.plot_df_subjects(\n df_per,\n x_col=\"sub\",\n y_col=\"performance_test\",\n hue=\"ch_type\",\n PATH_SAVE=PATH_OUT / RUN_NAME / (RUN_NAME + \"_decoding_performance.png\"),\n figsize_tuple=(8, 5),\n)\nax.set_ylabel(r\"$R^2$ Correlation\")\nax.set_xlabel(\"Subject 000\")\nax.set_title(\"Performance comparison Movement decoding\")\nplt.tight_layout()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/_downloads/3b4900a2b2818ff30362215b76f7d5eb/plot_1_example_BIDS.py b/_downloads/3b4900a2b2818ff30362215b76f7d5eb/plot_1_example_BIDS.py new file mode 100644 index 00000000..eb83505f --- /dev/null +++ b/_downloads/3b4900a2b2818ff30362215b76f7d5eb/plot_1_example_BIDS.py @@ -0,0 +1,236 @@ +""" +ECoG Movement decoding example +============================== + +""" + +# %% +# This example notebook read openly accessible data from the publication +# *Electrocorticography is superior to subthalamic local field potentials +# for movement decoding in Parkinson’s disease* +# (`Merk et al. 2022 _`). +# The dataset is available `here `_. +# +# For simplicity one example subject is automatically shipped within +# this repo at the *py_neuromodulation/data* folder, stored in +# `iEEG BIDS `_ format. + +# %% +from sklearn import metrics, model_selection, linear_model +import matplotlib.pyplot as plt + +import py_neuromodulation as nm +from py_neuromodulation import ( + nm_analysis, + nm_decode, + nm_define_nmchannels, + nm_IO, + nm_plots, + NMSettings, +) + +# %% +# Let's read the example using `mne_bids `_. +# The resulting raw object is of type `mne.RawArray `_. +# We can use the properties such as sampling frequency, channel names, channel types all from the mne array and create the *nm_channels* DataFrame: + +( + RUN_NAME, + PATH_RUN, + PATH_BIDS, + PATH_OUT, + datatype, +) = nm_IO.get_paths_example_data() + +( + raw, + data, + sfreq, + line_noise, + coord_list, + coord_names, +) = nm_IO.read_BIDS_data( + PATH_RUN=PATH_RUN +) + +nm_channels = nm_define_nmchannels.set_channels( + ch_names=raw.ch_names, + ch_types=raw.get_channel_types(), + reference="default", + bads=raw.info["bads"], + new_names="default", + used_types=("ecog", "dbs", "seeg"), + target_keywords=["MOV_RIGHT"], +) + + +# %% +# This example contains the grip force movement traces, we'll use the *MOV_RIGHT* channel as a decoding target channel. +# Let's check some of the raw feature and time series traces: + +plt.figure(figsize=(12, 4), dpi=300) +plt.subplot(121) +plt.plot(raw.times, data[-1, :]) +plt.xlabel("Time [s]") +plt.ylabel("a.u.") +plt.title("Movement label") +plt.xlim(0, 20) + +plt.subplot(122) +for idx, ch_name in enumerate(nm_channels.query("used == 1").name): + plt.plot(raw.times, data[idx, :] + idx * 300, label=ch_name) +plt.legend(bbox_to_anchor=(1, 0.5), loc="center left") +plt.title("ECoG + STN-LFP time series") +plt.xlabel("Time [s]") +plt.ylabel("Voltage a.u.") +plt.xlim(0, 20) + +# %% +settings = NMSettings.get_fast_compute() + +settings.features.welch = True +settings.features.fft = True +settings.features.bursts = True +settings.features.sharpwave_analysis = True +settings.features.coherence = True + +settings.coherence.channels = [("LFP_RIGHT_0-LFP_RIGHT_2", "ECOG_RIGHT_0-avgref")] +# TONI: this example was failing because the rereferenced channel have different names than originals +# We need to handle ch_names being changed after reref with settings.coherence.channels validation + +settings.coherence.frequency_bands = ["high beta", "low gamma"] +settings.sharpwave_analysis_settings.estimator["mean"] = [] +settings.sharpwave_analysis_settings.sharpwave_features.enable_all() +for sw_feature in settings.sharpwave_analysis_settings.sharpwave_features.list_all(): + settings.sharpwave_analysis_settings.estimator["mean"].append(sw_feature) + +# %% +stream = nm.Stream( + sfreq=sfreq, + nm_channels=nm_channels, + settings=settings, + line_noise=line_noise, + coord_list=coord_list, + coord_names=coord_names, + verbose=True, +) + +# %% +features = stream.run( + data=data, + out_path_root=PATH_OUT, + folder_name=RUN_NAME, +) + +# %% +# Feature Analysis Movement +# ------------------------- +# The obtained performances can now be read and visualized using the :class:`nm_analysis.Feature_Reader`. + +# initialize analyzer +feature_reader = nm_analysis.FeatureReader( + feature_dir=PATH_OUT, + feature_file=RUN_NAME, +) +feature_reader.label_name = "MOV_RIGHT" +feature_reader.label = feature_reader.feature_arr["MOV_RIGHT"] + +# %% +feature_reader.feature_arr.iloc[100:108, -6:] + +# %% +print(feature_reader.feature_arr.shape) + +# %% +feature_reader._get_target_ch() + +# %% +feature_reader.plot_target_averaged_channel( + ch="ECOG_RIGHT_0", + list_feature_keywords=None, + epoch_len=4, + threshold=0.5, + ytick_labelsize=7, + figsize_x=12, + figsize_y=12, +) + +# %% +feature_reader.plot_all_features( + ytick_labelsize=6, + clim_low=-2, + clim_high=2, + ch_used="ECOG_RIGHT_0", + time_limit_low_s=0, + time_limit_high_s=20, + normalize=True, + save=True, +) + +# %% +nm_plots.plot_corr_matrix( + feature=feature_reader.feature_arr.filter(regex="ECOG_RIGHT_0"), + ch_name="ECOG_RIGHT_0-avgref", + feature_names=list( + feature_reader.feature_arr.filter(regex="ECOG_RIGHT_0-avgref").columns + ), + feature_file=feature_reader.feature_file, + show_plot=True, + figsize=(15, 15), +) + +# %% +# Decoding +# -------- +# +# The main focus of the *py_neuromodulation* pipeline is feature estimation. +# Nevertheless, the user can also use the pipeline for machine learning decoding. +# It can be used for regression and classification problems and also dimensionality reduction such as PCA and CCA. +# +# Here, we show an example using the XGBOOST classifier. The used labels came from a continuous grip force movement target, named "MOV_RIGHT". +# +# First we initialize the :class:`~nm_decode.Decoder` class, which the specified *validation method*, here being a simple 3-fold cross validation, +# the evaluation metric, used machine learning model, and the channels we want to evaluate performances for. +# +# There are many more implemented methods, but we will here limit it to the ones presented. + +model = linear_model.LinearRegression() + +feature_reader.decoder = nm_decode.Decoder( + features=feature_reader.feature_arr, + label=feature_reader.label, + label_name=feature_reader.label_name, + used_chs=feature_reader.used_chs, + model=model, + eval_method=metrics.r2_score, + cv_method=model_selection.KFold(n_splits=3, shuffle=True), +) + +# %% +performances = feature_reader.run_ML_model( + estimate_channels=True, + estimate_gridpoints=False, + estimate_all_channels_combined=True, + save_results=True, +) + +# %% +# The performances are a dictionary that can be transformed into a DataFrame: + +df_per = feature_reader.get_dataframe_performances(performances) + +df_per + +# %% +ax = nm_plots.plot_df_subjects( + df_per, + x_col="sub", + y_col="performance_test", + hue="ch_type", + PATH_SAVE=PATH_OUT / RUN_NAME / (RUN_NAME + "_decoding_performance.png"), + figsize_tuple=(8, 5), +) +ax.set_ylabel(r"$R^2$ Correlation") +ax.set_xlabel("Subject 000") +ax.set_title("Performance comparison Movement decoding") +plt.tight_layout() diff --git a/_downloads/5471d61c0ef5854ecd111aca54bf2606/plot_6_real_time_demo.ipynb b/_downloads/5471d61c0ef5854ecd111aca54bf2606/plot_6_real_time_demo.ipynb new file mode 100644 index 00000000..6ecea3ef --- /dev/null +++ b/_downloads/5471d61c0ef5854ecd111aca54bf2606/plot_6_real_time_demo.ipynb @@ -0,0 +1,57 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n# Real-time feature estimation\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Implementation of individual nm_streams\n\n*py_neuromodulation* was optimized for computation of real-time data streams.\nThere are however center -and lab specific hardware acquisition systems. Therefore, each experiment requires modules to interact with hardware platforms\nwhich periodically acquire data.\n\nGiven the raw data, data can be analyzed using *py_neuromodulation*. Preprocessing methods, such as re-referencing and normalization,\nfeature computation and decoding can be performed then in real-time.\n\nFor online as well as as offline analysis, the :class:`~nm_stream_abc` class needs to be instantiated.\nHere the `nm_settings` and `nm_channels` are required to be defined.\nPreviously for the offline analysis, an offline :class:`~nm_generator` object was defined that periodically yielded data.\nFor online data, the :meth:`~nm_stream_abc.run` function therefore needs to be overwritten, which first acquires data and then calls\nthe :meth:`~nm_run_analysis.process` function.\n\nThe following illustrates in pseudo-code how such a stream could be initialized:\n\n```python\nfrom py_neuromodulation import nm_stream_abc\n\nclass MyStream(nm_stream_abc):\ndef __init__(self, settings, channels):\n super().__init__(settings, channels)\n\ndef run(self):\n features_ = []\n while True:\n data = self.acquire_data()\n features_.append(self.run_analysis.process(data))\n # potentially use machine learning model for decoding\n```\n## Computation time examples\n\nThe following example calculates for six channels, CAR re-referencing, z-score normalization and FFT features results the following computation time:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import py_neuromodulation as nm\nfrom py_neuromodulation import NMSettings\nimport numpy as np\nimport timeit\n\n\ndef get_fast_compute_settings():\n settings = NMSettings.get_fast_compute()\n \n settings.preprocessing = [\"re_referencing\", \"notch_filter\"]\n settings.features.fft = True\n settings.postprocessing.feature_normalization = True\n return settings\n\n\ndata = np.random.random([1, 1000])\n\nprint(\"FFT Features, CAR re-referencing, z-score normalization\")\nprint()\nprint(\"Computation time for single ECoG channel: \")\nstream = nm.Stream(\n sfreq=1000,\n data=data,\n sampling_rate_features_hz=10,\n verbose=False,\n settings=get_fast_compute_settings(),\n)\nprint(\n f\"{np.round(timeit.timeit(lambda: stream.data_processor.process(data), number=100)/100, 3)} s\"\n)\n\nprint(\"Computation time for 6 ECoG channels: \")\ndata = np.random.random([6, 1000])\nstream = nm.Stream(\n sfreq=1000,\n data=data,\n sampling_rate_features_hz=10,\n verbose=False,\n settings=get_fast_compute_settings(),\n)\nprint(\n f\"{np.round(timeit.timeit(lambda: stream.data_processor.process(data), number=100)/100, 3)} s\"\n)\n\nprint(\n \"\\nFFT Features & Temporal Waveform Shape & Hjorth & Bursts, CAR re-referencing, z-score normalization\"\n)\nprint(\"Computation time for single ECoG channel: \")\ndata = np.random.random([1, 1000])\nstream = nm.Stream(\n sfreq=1000, data=data, sampling_rate_features_hz=10, verbose=False\n)\nprint(\n f\"{np.round(timeit.timeit(lambda: stream.data_processor.process(data), number=10)/10, 3)} s\"\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Those results show that the computation time for a typical pipeline (FFT, re-referencing, notch-filtering, feature normalization)\nis well below 10 ms, which is fast enough for real-time analysis with feature sampling rates below 100 Hz.\nComputation of more complex features could still result in feature sampling rates of more than 30 Hz.\n\n## Real-time movement decoding using the TMSi-SAGA amplifier\n\nIn the following example, we will show how we setup a real-time movement decoding experiment using the TMSi-SAGA amplifier.\nFirst, we relied on different software modules for data streaming and visualization.\n[LabStreamingLayer](https://labstreaminglayer.org) allows for real-time data streaming and synchronization across multiple devices.\nWe used [timeflux](https://timeflux.io) for real-time data visualization of features, decoded output.\nFor raw data visualization we used [Brain Streaming Layer](https://fcbg-hnp-meeg.github.io/bsl/dev/index.html).\n\nThe code for real-time movement decoding is added in the GitHub branch [realtime_decoding](https://github.com/neuromodulation/py_neuromodulation/tree/realtime_decoding).\nHere we relied on the [TMSI SAGA Python interface](https://gitlab.com/tmsi/tmsi-python-interface).\n\n\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/_downloads/55eeef1295e7a37b937b020f7edd5a64/plot_4_example_gridPointProjection.ipynb b/_downloads/55eeef1295e7a37b937b020f7edd5a64/plot_4_example_gridPointProjection.ipynb new file mode 100644 index 00000000..4b2b35b9 --- /dev/null +++ b/_downloads/55eeef1295e7a37b937b020f7edd5a64/plot_4_example_gridPointProjection.ipynb @@ -0,0 +1,274 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n# Grid Point Projection\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In ECoG datasets the electrode locations are usually different. For this reason, we established a grid\nwith a set of points defined in a standardized MNI brain.\nData is then interpolated to this grid, such that they are common across patients, which allows across patient decoding use cases.\n\nIn this notebook, we will plot these grid points and see how the features extracted from our data can be projected into this grid space.\n\nIn order to do so, we'll read saved features that were computed in the ECoG movement notebook.\nPlease note that in order to do so, when running the feature estimation, the settings\n\n

Note

```python\nstream.settings['postprocessing']['project_cortex'] = True\nstream.settings['postprocessing']['project_subcortex'] = True\n```\n need to be set to `True` for a cortical and/or subcortical projection.

\n\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import numpy as np\nimport matplotlib.pyplot as plt\n\nimport py_neuromodulation as nm\nfrom py_neuromodulation import (\n nm_analysis,\n nm_plots,\n nm_IO,\n NMSettings,\n nm_define_nmchannels\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Read features from BIDS data\n\nWe first estimate features, with the `grid_point` projection settings enabled for cortex. \n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "RUN_NAME, PATH_RUN, PATH_BIDS, PATH_OUT, datatype = nm_IO.get_paths_example_data()\n\n(\n raw,\n data,\n sfreq,\n line_noise,\n coord_list,\n coord_names,\n) = nm_IO.read_BIDS_data(\n PATH_RUN=PATH_RUN\n)\n\nsettings = NMSettings.get_fast_compute()\n\nsettings.postprocessing.project_cortex = True\n\nnm_channels = nm_define_nmchannels.set_channels(\n ch_names=raw.ch_names,\n ch_types=raw.get_channel_types(),\n reference=\"default\",\n bads=raw.info[\"bads\"],\n new_names=\"default\",\n used_types=(\"ecog\", \"dbs\", \"seeg\"),\n target_keywords=[\"MOV_RIGHT_CLEAN\",\"MOV_LEFT_CLEAN\"]\n)\n\nstream = nm.Stream(\n sfreq=sfreq,\n nm_channels=nm_channels,\n settings=settings,\n line_noise=line_noise,\n coord_list=coord_list,\n coord_names=coord_names,\n verbose=True,\n)\n\nfeatures = stream.run(\n data=data[:, :int(sfreq*5)],\n out_path_root=PATH_OUT,\n folder_name=RUN_NAME,\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From nm_analysis.py, we use the :class:~`nm_analysis.FeatureReader` class to load the data.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "# init analyzer\nfeature_reader = nm_analysis.FeatureReader(\n feature_dir=PATH_OUT, feature_file=RUN_NAME\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To perform the grid projection, for all computed features we check for every grid point if there is any electrode channel within the spatial range ```max_dist_mm```, and weight \nthis electrode contact by the inverse distance and normalize across all electrode distances within the maximum distance range.\nThis gives us a projection matrix that we can apply to streamed data, to transform the feature-channel matrix *(n_features, n_channels)* into the grid point matrix *(n_features, n_gridpoints)*.\n\nTo save computation time, this projection matrix is precomputed before the real time run computation. \nThe cortical grid is stored in *py_neuromodulation/grid_cortex.tsv* and the electrodes coordinates are stored in *_space-mni_electrodes.tsv* in a BIDS dataset.\n\n

Note

One remark is that our cortical and subcortical grids are defined for the **left** hemisphere of the brain and, therefore, electrode contacts are mapped to the left hemisphere.

\n\nFrom the analyzer, the user can plot the cortical projection with the function below, display the grid points and ECoG electrodes are crosses.\nThe yellow grid points are the ones that are active for that specific ECoG electrode location. The inactive grid points are shown in purple.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "feature_reader.plot_cort_projection()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also plot only the ECoG electrodes or the grid points, with the help of the data saved in feature_reader.sidecar. BIDS sidecar files are json files where you store additional information, here it is used to save the ECoG strip positions and the grid coordinates, which are not part of the settings and nm_channels.csv. We can check what is stored in the file and then use the nmplotter.plot_cortex function:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "grid_plotter = nm_plots.NM_Plot(\n ecog_strip=np.array(feature_reader.sidecar[\"coords\"][\"cortex_right\"][\"positions\"]),\n grid_cortex=np.array(feature_reader.sidecar[\"grid_cortex\"]),\n # grid_subcortex=np.array(feature_reader.sidecar[\"grid_subcortex\"]),\n sess_right=feature_reader.sidecar[\"sess_right\"],\n proj_matrix_cortex=np.array(feature_reader.sidecar[\"proj_matrix_cortex\"])\n)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "grid_plotter.plot_cortex(\n grid_color=np.sum(np.array(feature_reader.sidecar[\"proj_matrix_cortex\"]),axis=1),\n lower_clim=0.,\n upper_clim=1.0,\n cbar_label=\"Used Grid Points\",\n title = \"ECoG electrodes projected onto cortical grid\"\n)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "feature_reader.sidecar[\"coords\"][\"cortex_right\"][\"positions\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "feature_reader.nmplotter.plot_cortex(\n ecog_strip=np.array(\n feature_reader.sidecar[\"coords\"][\"cortex_right\"][\"positions\"],\n ),\n lower_clim=0.,\n upper_clim=1.0,\n cbar_label=\"Used ECoG Electrodes\",\n title = \"Plot of ECoG electrodes\"\n)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "feature_reader.nmplotter.plot_cortex(\n np.array(\n feature_reader.sidecar[\"grid_cortex\"]\n ),\n lower_clim=0.,\n upper_clim=1.0,\n cbar_label=\"All Grid Points\",\n title = \"All grid points\"\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The Projection Matrix\nTo go from the feature-channel matrix *(n_features, n_channels)* to the grid point matrix *(n_features, n_gridpoints)*\nwe need a projection matrix that has the shape *(n_channels, n_gridpoints)*.\nIt maps the strengths of the signals in each ECoG channel to the correspondent ones in the cortical grid.\nIn the cell below we plot this matrix, that has the property that the column sum over channels for each grid point is either 1 or 0.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "plt.figure(figsize=(8,5))\nplt.imshow(np.array(feature_reader.sidecar['proj_matrix_cortex']), aspect = 'auto')\nplt.colorbar(label = \"Strength of ECoG signal in each grid point\")\nplt.xlabel(\"ECoG channels\")\nplt.ylabel(\"Grid points\")\nplt.title(\"Matrix mapping from ECoG to grid\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Feature Plot in the Grid: An Example of Post-processing\nFirst we take the dataframe with all the features in all time points.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "df = feature_reader.feature_arr" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "df.iloc[:5, :5]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then we filter for only 'avgref_fft_theta', which gives us the value for fft_theta in all 6 ECoG channels over all time points. Then we take only the 6th time point - as an arbitrary choice.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "fft_theta_oneTimePoint = np.asarray(df[df.columns[df.columns.str.contains(pat = 'avgref_fft_theta')]].iloc[5])\nfft_theta_oneTimePoint" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then the projection of the features into the grid is gonna be the color of the grid points in the *plot_cortex* function.\nThat is the matrix multiplication of the projection matrix of the cortex and 6 values for the *fft_theta* feature above.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "grid_fft_Theta = np.array(feature_reader.sidecar[\"proj_matrix_cortex\"]) @ fft_theta_oneTimePoint\n\nfeature_reader.nmplotter.plot_cortex(np.array(\n feature_reader.sidecar[\"grid_cortex\"]),grid_color = grid_fft_Theta, set_clim = True, lower_clim=min(grid_fft_Theta[grid_fft_Theta>0]), upper_clim=max(grid_fft_Theta), cbar_label=\"FFT Theta Projection to Grid\", title = \"FFT Theta Projection to Grid\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lower and upper boundaries for clim were chosen to be the max and min values of the projection of the features (minimum value excluding zero). This can be checked in the cell below:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "grid_fft_Theta" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the plot above we can see how the intensity of the fast fourier transform in the theta band varies for each grid point in the cortex, for one specific time point.\n\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/_downloads/6f1e7a639e0699d6164445b55e6c116d/auto_examples_jupyter.zip b/_downloads/6f1e7a639e0699d6164445b55e6c116d/auto_examples_jupyter.zip new file mode 100644 index 00000000..46f78e2c Binary files /dev/null and b/_downloads/6f1e7a639e0699d6164445b55e6c116d/auto_examples_jupyter.zip differ diff --git a/_downloads/771b4d23823f2687a90bca68d1570e41/plot_7_lsl_example.ipynb b/_downloads/771b4d23823f2687a90bca68d1570e41/plot_7_lsl_example.ipynb new file mode 100644 index 00000000..8f31b448 --- /dev/null +++ b/_downloads/771b4d23823f2687a90bca68d1570e41/plot_7_lsl_example.ipynb @@ -0,0 +1,162 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n# Lab Streaming Layer (LSL) Example\n\nThis toolbox implements the lsl ecosystem which can be utilized for offline use cases as well as live streamings\nIn this example the data introduced in the first demo is being analyzed\nin a similar manner, This time however integrating an lsl stream.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n\nfrom py_neuromodulation import (\n nm_mnelsl_generator,\n nm_IO,\n nm_define_nmchannels,\n nm_analysis,\n nm_stream_offline,\n NMSettings,\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let\u2019s get the example data from the provided BIDS dataset and create the nm_channels DataFrame.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "(\n RUN_NAME,\n PATH_RUN,\n PATH_BIDS,\n PATH_OUT,\n datatype,\n) = nm_IO.get_paths_example_data()\n\n(\n raw,\n data,\n sfreq,\n line_noise,\n coord_list,\n coord_names,\n) = nm_IO.read_BIDS_data(PATH_RUN=PATH_RUN)\n\nnm_channels = nm_define_nmchannels.set_channels(\n ch_names=raw.ch_names,\n ch_types=raw.get_channel_types(),\n reference=\"default\",\n bads=raw.info[\"bads\"],\n new_names=\"default\",\n used_types=(\"ecog\", \"dbs\", \"seeg\"),\n target_keywords=[\"MOV_RIGHT\"],\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Playing the Data\n\nNow we need our data to be represeted in the LSL stream.\nFor this example an mne_lsl.Player is utilized, which is playing our earlier\nrecorded data. However, you could make use of any LSL source (live or\noffline).\nIf you want to bind your own data source, make sure to specify the\nnecessary parameters (data type, type, name) accordingly.\nIf you are unsure about the parameters of your data source you can\nalways search for available lsl streams.\n\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "settings = NMSettings.get_fast_compute()\n\nplayer = nm_mnelsl_generator.LSLOfflinePlayer(\n raw=raw, stream_name=\"example_stream\"\n) # TODO: add different keyword\n\nplayer.start_player(chunk_size=30)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating the LSLStream object\n\nNext let\u2019s create a Stream analog to the First Demo\u2019s example However as\nwe run the stream, we will set the *lsl-stream* value to True and pass\nthe stream name we earlier declared when initializing the player object\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "settings.features.welch = False\nsettings.features.fft = True\nsettings.features.bursts = False\nsettings.features.sharpwave_analysis = False\nsettings.features.coherence = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "stream = nm_stream_offline.Stream(\n sfreq=sfreq,\n nm_channels=nm_channels,\n settings=settings,\n coord_list=coord_list,\n verbose=True,\n line_noise=line_noise,\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We then simply have to set the `stream_lsl` parameter to be `True` and specify the `stream_lsl_name`.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "features = stream.run(\n stream_lsl=True,\n plot_lsl=False,\n stream_lsl_name=\"example_stream\",\n out_path_root=PATH_OUT,\n folder_name=RUN_NAME,\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can then look at the computed features and check if the streamed data was processed correctly.\nThis can be verified by the time label:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "plt.plot(features.time, features.MOV_RIGHT)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Feature Analysis of Movement\nWe can now check the movement averaged features of an ECoG channel.\nNote that the path was here adapted to be documentation build compliant.\n%%\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "feature_reader = nm_analysis.FeatureReader(feature_dir=PATH_OUT, feature_file=RUN_NAME)\nfeature_reader.label_name = \"MOV_RIGHT\"\nfeature_reader.label = feature_reader.feature_arr[\"MOV_RIGHT\"]\n\nfeature_reader.plot_target_averaged_channel(\n ch=\"ECOG_RIGHT_0\",\n list_feature_keywords=None,\n epoch_len=4,\n threshold=0.5,\n ytick_labelsize=7,\n figsize_x=12,\n figsize_y=12,\n)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/_downloads/7e92dd2e6cc86b239d14cafad972ae4f/plot_3_example_sharpwave_analysis.py b/_downloads/7e92dd2e6cc86b239d14cafad972ae4f/plot_3_example_sharpwave_analysis.py new file mode 100644 index 00000000..5ccbee87 --- /dev/null +++ b/_downloads/7e92dd2e6cc86b239d14cafad972ae4f/plot_3_example_sharpwave_analysis.py @@ -0,0 +1,332 @@ +""" +Analyzing temporal features +=========================== + +""" + +# %% +# Time series data can be characterized using oscillatory components, but assumptions of sinusoidality are for real data rarely fulfilled. +# See *"Brain Oscillations and the Importance of Waveform Shape"* `Cole et al 2017 `_ for a great motivation. +# We implemented here temporal characteristics based on individual trough and peak relations, +# based on the :meth:~`scipy.signal.find_peaks` method. The function parameter *distance* can be specified in the *nm_settings.json*. +# Temporal features can be calculated twice for troughs and peaks. In the settings, this can be specified by setting *estimate* to true +# in *detect_troughs* and/or *detect_peaks*. A statistical measure (e.g. mean, max, median, var) can be defined as a resulting feature from the peak and +# trough estimates using the *apply_estimator_between_peaks_and_troughs* setting. +# +# In py_neuromodulation the following characteristics are implemented: +# +# .. note:: +# The nomenclature is written here for sharpwave troughs, but detection of peak characteristics can be computed in the same way. +# +# - prominence: +# :math:`V_{prominence} = |\frac{V_{peak-left} + V_{peak-right}}{2}| - V_{trough}` +# - sharpness: +# :math:`V_{sharpnesss} = \frac{(V_{trough} - V_{trough-5 ms}) + (V_{trough} - V_{trough+5 ms})}{2}` +# - rise and decay rise time +# - rise and decay steepness +# - width (between left and right peaks) +# - interval (between troughs) +# +# Additionally, different filter ranges can be parametrized using the *filter_ranges_hz* setting. +# Filtering is necessary to remove high frequent signal fluctuations, but limits also the true estimation of sharpness and prominence due to signal smoothing. + +from typing import cast +import seaborn as sb +from matplotlib import pyplot as plt +from scipy import signal +from scipy.signal import fftconvolve +import numpy as np + +import py_neuromodulation as nm +from py_neuromodulation import ( + nm_define_nmchannels, + nm_IO, + NMSettings, +) +from py_neuromodulation.nm_sharpwaves import SharpwaveAnalyzer + + +# %% +# We will first read the example ECoG data and plot the identified features on the filtered time series. + +RUN_NAME, PATH_RUN, PATH_BIDS, PATH_OUT, datatype = nm_IO.get_paths_example_data() + +( + raw, + data, + sfreq, + line_noise, + coord_list, + coord_names, +) = nm_IO.read_BIDS_data(PATH_RUN=PATH_RUN) + +# %% +settings = NMSettings.get_fast_compute() + +settings.features.fft = True +settings.features.bursts = False +settings.features.sharpwave_analysis = True +settings.features.coherence = False + +settings.sharpwave_analysis_settings.estimator["mean"] = [] +settings.sharpwave_analysis_settings.sharpwave_features.enable_all() +for sw_feature in settings.sharpwave_analysis_settings.sharpwave_features.list_all(): + settings.sharpwave_analysis_settings.estimator["mean"].append(sw_feature) + +nm_channels = nm_define_nmchannels.set_channels( + ch_names=raw.ch_names, + ch_types=raw.get_channel_types(), + reference="default", + bads=raw.info["bads"], + new_names="default", + used_types=("ecog", "dbs", "seeg"), + target_keywords=["MOV_RIGHT"], +) + +stream = nm.Stream( + sfreq=sfreq, + nm_channels=nm_channels, + settings=settings, + line_noise=line_noise, + coord_list=coord_list, + coord_names=coord_names, + verbose=False, +) +sw_analyzer = cast( + SharpwaveAnalyzer, stream.data_processor.features.get_feature("sharpwave_analysis") +) + +# %% +# The plotted example time series, visualized on a short time scale, shows the relation of identified peaks, troughs, and estimated features: +data_plt = data[5, 1000:4000] + +filtered_dat = fftconvolve(data_plt, sw_analyzer.list_filter[0][1], mode="same") + +troughs = signal.find_peaks(-filtered_dat, distance=10)[0] +peaks = signal.find_peaks(filtered_dat, distance=5)[0] + +sw_results = sw_analyzer.analyze_waveform(filtered_dat) + +WIDTH = BAR_WIDTH = 4 +BAR_OFFSET = 50 +OFFSET_TIME_SERIES = -100 +SCALE_TIMESERIES = 1 + +hue_colors = sb.color_palette("viridis_r", 6) + +plt.figure(figsize=(5, 3), dpi=300) +plt.plot( + OFFSET_TIME_SERIES + data_plt, + color="gray", + linewidth=0.5, + alpha=0.5, + label="original ECoG data", +) +plt.plot( + OFFSET_TIME_SERIES + filtered_dat * SCALE_TIMESERIES, + linewidth=0.5, + color="black", + label="[5-30]Hz filtered data", +) + +plt.plot( + peaks, + OFFSET_TIME_SERIES + filtered_dat[peaks] * SCALE_TIMESERIES, + "x", + label="peaks", + markersize=3, + color="darkgray", +) +plt.plot( + troughs, + OFFSET_TIME_SERIES + filtered_dat[troughs] * SCALE_TIMESERIES, + "x", + label="troughs", + markersize=3, + color="lightgray", +) + +plt.bar( + troughs + BAR_WIDTH, + np.array(sw_results["prominence"]) * 4, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[0], + label="Prominence", + alpha=0.5, +) +plt.bar( + troughs + BAR_WIDTH * 2, + -np.array(sw_results["sharpness"]) * 6, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[1], + label="Sharpness", + alpha=0.5, +) +plt.bar( + troughs + BAR_WIDTH * 3, + np.array(sw_results["interval"]) * 5, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[2], + label="Interval", + alpha=0.5, +) +plt.bar( + troughs + BAR_WIDTH * 4, + np.array(sw_results["rise_time"]) * 5, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[3], + label="Rise time", + alpha=0.5, +) + +plt.xticks( + np.arange(0, data_plt.shape[0], 200), + np.round(np.arange(0, int(data_plt.shape[0] / 1000), 0.2), 2), +) +plt.xlabel("Time [s]") +plt.title("Temporal waveform shape features") +plt.legend(loc="center left", bbox_to_anchor=(1, 0.5)) +plt.ylim(-550, 700) +plt.xlim(0, 200) +plt.ylabel("a.u.") +plt.tight_layout() + +# %% +# See in the following example a time series example, that is aligned to movement. With movement onset the prominence, sharpness, and interval features are reduced: + +plt.figure(figsize=(8, 5), dpi=300) +plt.plot( + OFFSET_TIME_SERIES + data_plt, + color="gray", + linewidth=0.5, + alpha=0.5, + label="original ECoG data", +) +plt.plot( + OFFSET_TIME_SERIES + filtered_dat * SCALE_TIMESERIES, + linewidth=0.5, + color="black", + label="[5-30]Hz filtered data", +) + +plt.plot( + peaks, + OFFSET_TIME_SERIES + filtered_dat[peaks] * SCALE_TIMESERIES, + "x", + label="peaks", + markersize=3, + color="darkgray", +) +plt.plot( + troughs, + OFFSET_TIME_SERIES + filtered_dat[troughs] * SCALE_TIMESERIES, + "x", + label="troughs", + markersize=3, + color="lightgray", +) + +plt.bar( + troughs + BAR_WIDTH, + np.array(sw_results["prominence"]) * 4, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[0], + label="Prominence", + alpha=0.5, +) +plt.bar( + troughs + BAR_WIDTH * 2, + -np.array(sw_results["sharpness"]) * 6, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[1], + label="Sharpness", + alpha=0.5, +) +plt.bar( + troughs + BAR_WIDTH * 3, + np.array(sw_results["interval"]) * 5, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[2], + label="Interval", + alpha=0.5, +) +plt.bar( + troughs + BAR_WIDTH * 4, + np.array(sw_results["rise_time"]) * 5, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[3], + label="Rise time", + alpha=0.5, +) + +plt.axvline(x=1500, label="Movement start", color="red") + +# plt.xticks(np.arange(0, 2000, 200), np.round(np.arange(0, 2, 0.2), 2)) +plt.xticks( + np.arange(0, data_plt.shape[0], 200), + np.round(np.arange(0, int(data_plt.shape[0] / 1000), 0.2), 2), +) +plt.xlabel("Time [s]") +plt.title("Temporal waveform shape features") +plt.legend(loc="center left", bbox_to_anchor=(1, 0.5)) +plt.ylim(-450, 400) +plt.ylabel("a.u.") +plt.tight_layout() + +# %% +# In the *sharpwave_analysis_settings* the *estimator* keyword further specifies which statistic is computed based on the individual +# features in one batch. The "global" setting *segment_length_features_ms* specifies the time duration for feature computation. +# Since there can be a different number of identified waveform shape features for different batches (i.e. different number of peaks/troughs), +# taking a statistical measure (e.g. the maximum or mean) will be necessary for feature comparison. + +# %% +# Example time series computation for movement decoding +# ----------------------------------------------------- +# We will now read the ECoG example/data and investigate if samples differ across movement states. Therefore we compute features and enable the default *sharpwave* features. + +settings = NMSettings.get_default().reset() + +settings.features.sharpwave_analysis = True +settings.sharpwave_analysis_settings.filter_ranges_hz = [[5, 80]] + +nm_channels["used"] = 0 # set only two ECoG channels for faster computation to true +nm_channels.loc[[3, 8], "used"] = 1 + +stream = nm.Stream( + sfreq=sfreq, + nm_channels=nm_channels, + settings=settings, + line_noise=line_noise, + coord_list=coord_list, + coord_names=coord_names, + verbose=True, +) + +df_features = stream.run(data=data[:, :30000]) + +# %% +# We can then plot two exemplary features, prominence and interval, and see that the movement amplitude can be clustered with those two features alone: + +plt.figure(figsize=(5, 3), dpi=300) +print(df_features.columns) +plt.scatter( + df_features["ECOG_RIGHT_0-avgref_Sharpwave_Max_prominence_range_5_80"], + df_features["ECOG_RIGHT_5-avgref_Sharpwave_Mean_interval_range_5_80"], + c=df_features.MOV_RIGHT, + alpha=0.8, + s=30, +) +cbar = plt.colorbar() +cbar.set_label("Movement amplitude") +plt.xlabel("Prominence a.u.") +plt.ylabel("Interval a.u.") +plt.title("Temporal features predict movement amplitude") +plt.tight_layout() diff --git a/_downloads/838f113dfae594f3aa381795bf4ef483/plot_7_lsl_example.py b/_downloads/838f113dfae594f3aa381795bf4ef483/plot_7_lsl_example.py new file mode 100644 index 00000000..dd0acf42 --- /dev/null +++ b/_downloads/838f113dfae594f3aa381795bf4ef483/plot_7_lsl_example.py @@ -0,0 +1,134 @@ +""" +Lab Streaming Layer (LSL) Example +================================= + +This toolbox implements the lsl ecosystem which can be utilized for offline use cases as well as live streamings +In this example the data introduced in the first demo is being analyzed +in a similar manner, This time however integrating an lsl stream. + +""" + +# %% +from matplotlib import pyplot as plt + +from py_neuromodulation import ( + nm_mnelsl_generator, + nm_IO, + nm_define_nmchannels, + nm_analysis, + nm_stream_offline, + NMSettings, +) + +# %% +# Let’s get the example data from the provided BIDS dataset and create the nm_channels DataFrame. + +( + RUN_NAME, + PATH_RUN, + PATH_BIDS, + PATH_OUT, + datatype, +) = nm_IO.get_paths_example_data() + +( + raw, + data, + sfreq, + line_noise, + coord_list, + coord_names, +) = nm_IO.read_BIDS_data(PATH_RUN=PATH_RUN) + +nm_channels = nm_define_nmchannels.set_channels( + ch_names=raw.ch_names, + ch_types=raw.get_channel_types(), + reference="default", + bads=raw.info["bads"], + new_names="default", + used_types=("ecog", "dbs", "seeg"), + target_keywords=["MOV_RIGHT"], +) + +# %% +# Playing the Data +# ---------------- +# +# Now we need our data to be represeted in the LSL stream. +# For this example an mne_lsl.Player is utilized, which is playing our earlier +# recorded data. However, you could make use of any LSL source (live or +# offline). +# If you want to bind your own data source, make sure to specify the +# necessary parameters (data type, type, name) accordingly. +# If you are unsure about the parameters of your data source you can +# always search for available lsl streams. +# + +settings = NMSettings.get_fast_compute() + +player = nm_mnelsl_generator.LSLOfflinePlayer( + raw=raw, stream_name="example_stream" +) # TODO: add different keyword + +player.start_player(chunk_size=30) +# %% +# Creating the LSLStream object +# ----------------------------- +# +# Next let’s create a Stream analog to the First Demo’s example However as +# we run the stream, we will set the *lsl-stream* value to True and pass +# the stream name we earlier declared when initializing the player object + +settings.features.welch = False +settings.features.fft = True +settings.features.bursts = False +settings.features.sharpwave_analysis = False +settings.features.coherence = False + +# %% +stream = nm_stream_offline.Stream( + sfreq=sfreq, + nm_channels=nm_channels, + settings=settings, + coord_list=coord_list, + verbose=True, + line_noise=line_noise, +) +# %% +# We then simply have to set the `stream_lsl` parameter to be `True` and specify the `stream_lsl_name`. + +features = stream.run( + stream_lsl=True, + plot_lsl=False, + stream_lsl_name="example_stream", + out_path_root=PATH_OUT, + folder_name=RUN_NAME, +) + +# %% +# We can then look at the computed features and check if the streamed data was processed correctly. +# This can be verified by the time label: + +plt.plot(features.time, features.MOV_RIGHT) + + +###################################################################### +# Feature Analysis of Movement +# ---------------------------- +# We can now check the movement averaged features of an ECoG channel. +# Note that the path was here adapted to be documentation build compliant. +# %% + +feature_reader = nm_analysis.FeatureReader(feature_dir=PATH_OUT, feature_file=RUN_NAME) +feature_reader.label_name = "MOV_RIGHT" +feature_reader.label = feature_reader.feature_arr["MOV_RIGHT"] + +feature_reader.plot_target_averaged_channel( + ch="ECOG_RIGHT_0", + list_feature_keywords=None, + epoch_len=4, + threshold=0.5, + ytick_labelsize=7, + figsize_x=12, + figsize_y=12, +) diff --git a/_downloads/90fbd1df4debeeaae3e73270a3b567f7/plot_8_cebra_example.py b/_downloads/90fbd1df4debeeaae3e73270a3b567f7/plot_8_cebra_example.py new file mode 100644 index 00000000..9b3d889c --- /dev/null +++ b/_downloads/90fbd1df4debeeaae3e73270a3b567f7/plot_8_cebra_example.py @@ -0,0 +1,29 @@ +""" +Cebra Decoding with no training Example +====================================== + +The following example show how to use the Cebra decoding without training. + +""" + +import os + +# load example_cebra_decoding.html +with open(os.path.join("..", "examples", "example_cebra_decoding.html"), "rt") as fh: + html_data = fh.read() + +tmp_dir = os.path.join("..", "docs", "source", "auto_examples") +if os.path.exists(tmp_dir): + # building the docs with sphinx-gallery + with open(os.path.join(tmp_dir, "out.html"), "wt") as fh: + fh.write(html_data) +# set example path for thumbnail +# sphinx_gallery_thumbnail_path = '_static/CEBRA_embedding.png' + + +# %% +# CEBRA example +# ------------- +# +# .. raw:: html +# :file: out.html diff --git a/_downloads/94441d1bd6655937a2f35b47ad608f16/plot_2_example_add_feature.ipynb b/_downloads/94441d1bd6655937a2f35b47ad608f16/plot_2_example_add_feature.ipynb new file mode 100644 index 00000000..e52b78c2 --- /dev/null +++ b/_downloads/94441d1bd6655937a2f35b47ad608f16/plot_2_example_add_feature.ipynb @@ -0,0 +1,122 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n# Adding New Features\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import py_neuromodulation as nm\nimport numpy as np\nfrom typing import Iterable" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this example we will demonstrate how a new feature can be added to the existing feature pipeline.\nThis can be done by creating a new feature class that implements the protocol class :class:`~nm_features.NMFeature`\nand registering it with the :func:`~nm_features.AddCustomFeature` function.\n\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's create a new feature class called `ChannelMean` that calculates the mean signal for each channel.\nWe can optinally make it inherit from :class:`~nm_features.NMFeature` but as long as it has an adequate constructor\nand a `calc_feature` method with the appropriate signatures it will work.\nThe :func:`__init__` method should take the settings, channel names and sampling frequency as arguments.\nThe `calc_feature` method should take the data and a dictionary of features as arguments and return the updated dictionary.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "class ChannelMean:\n def __init__(\n self, settings: nm.NMSettings, ch_names: Iterable[str], sfreq: float\n ) -> None:\n # If required for feature calculation, store the settings,\n # channel names and sampling frequency (optional)\n self.settings = settings\n self.ch_names = ch_names\n self.sfreq = sfreq\n\n # Here you can add any additional initialization code\n # For example, you could store parameters for the functions\\\n # used in the calc_feature method\n \n self.feature_name = \"channel_mean\"\n\n def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:\n # Here you can add any feature calculation code\n # This example simply calculates the mean signal for each channel\n ch_means = np.mean(data, axis=1)\n\n # Store the calculated features in the features_compute dictionary\n # Be careful to use a unique keyfor each channel and metric you compute\n for ch_idx, ch in enumerate(self.ch_names):\n features_compute[f\"{self.feature_name}_{ch}\"] = ch_means[ch_idx]\n\n # Return the updated features_compute dictionary to the stream\n return features_compute\n\n\nnm.add_custom_feature(\"channel_mean\", ChannelMean)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can instantiate settings and observe that the new feature has been added to the list of features\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "settings = nm.NMSettings() # Get default settings\n\nsettings.features" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's create some artificial data to demonstrate the feature calculation.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "N_CHANNELS = 5\nN_SAMPLES = 10000 # 10 seconds of random data at 1000 Hz sampling frequency\n\ndata = np.random.random([N_CHANNELS, N_SAMPLES]) \nstream = nm.Stream(\n sfreq=1000,\n data=data,\n settings = settings,\n sampling_rate_features_hz=10,\n verbose=False,\n)\n\nfeature_df = stream.run()\ncolumns = [col for col in feature_df.columns if \"channel_mean\" in col]\n\nfeature_df[columns]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Remove feature so that it does not interfere with other examples\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "nm.remove_custom_feature(\"channel_mean\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/_downloads/b796d2b92269e9c1ea40190688a4e2ff/plot_5_example_rmap_computing.ipynb b/_downloads/b796d2b92269e9c1ea40190688a4e2ff/plot_5_example_rmap_computing.ipynb new file mode 100644 index 00000000..b09685a3 --- /dev/null +++ b/_downloads/b796d2b92269e9c1ea40190688a4e2ff/plot_5_example_rmap_computing.ipynb @@ -0,0 +1,39 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n# R-Map computation\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Across patient decoding using R-Map optimal connectivity\n\nECoG electrode placement is commonly very heterogeneous across patients and cohorts.\nTo still facilitate approaches that are able to perform decoding applications without patient individual training,\ntwo across-patient decoding approaches were previously investigated for movement decoding:\n\n\n* grid-point decoding\n* optimal connectivity channel decoding\n\n\nFirst, the grid-point decoding approach relies on definition of a cortical or subcortical grid.\nData from individual grid points is then interpolated onto those common grid points.\nThe approach was also explained in the :doc:`plot_4_example_gridPointProjection` notebook.\n\n\"R-Map\n\nThe R-Map decoding approach relies on the other hand on computation of whole brain connectivity. The electrode MNI space locations need to be known,\nthen the following steps can be performed for decoding without patient individual training:\n\n#. Using the [wjn_toolbox](https://github.com/neuromodulation/wjn_toolbox) *wjn_specrical_roi* function, the MNI coordinates can be transformed into NIFTI (.nii) files, containing the electrode contact region of interest (ROI):\n\n```python\nwjn_spherical_roi(roiname, mni, 4)\n```\n#. For the given *ROI.nii* files, the LeadDBS [LeadMapper](https://netstim.gitbook.io/leaddbs/connectomics/lead-mapper) tool can be used for functional or structural connectivity estimation.\n\n#. The py_neuromodulation :class:`~nm_RMAP.py` module can then compute the R-Map given the contact-individual connectivity fingerprints:\n\n```python\nnm_RMAP.calculate_RMap_numba(fingerprints, performances)\n```\n#. The fingerprints from test-set patients can then be correlated with the calculated R-Map:\n\n```python\nnm_RMAP.get_corr_numba(fp, fp_test)\n```\n#. The channel with highest correlation can then be selected for decoding without individual training. :class:`~nm_RMAP.py` contain already leave one channel and leave one patient out cross validation functions:\n\n```python\nnm_RMAP.leave_one_sub_out_cv(l_fps_names, l_fps_dat, l_per, sub_list)\n```\n#. The obtained R-Map correlations can then be estimated statistically and plotted against true correlates:\n\n```python\nnm_RMAP.plot_performance_prediction_correlation(per_left_out, per_predict, out_path_save)\n```\nsphinx_gallery_thumbnail_path = '_static/RMAP_figure.png'\n\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/_downloads/b872ac0317da0212ff0a63086651d8af/plot_8_cebra_example.ipynb b/_downloads/b872ac0317da0212ff0a63086651d8af/plot_8_cebra_example.ipynb new file mode 100644 index 00000000..bfb74e8c --- /dev/null +++ b/_downloads/b872ac0317da0212ff0a63086651d8af/plot_8_cebra_example.ipynb @@ -0,0 +1,50 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n# Cebra Decoding with no training Example\n\nThe following example show how to use the Cebra decoding without training.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import os\n\n# load example_cebra_decoding.html\nwith open(os.path.join(\"..\", \"examples\", \"example_cebra_decoding.html\"), \"rt\") as fh:\n html_data = fh.read()\n\ntmp_dir = os.path.join(\"..\", \"docs\", \"source\", \"auto_examples\")\nif os.path.exists(tmp_dir):\n # building the docs with sphinx-gallery\n with open(os.path.join(tmp_dir, \"out.html\"), \"wt\") as fh:\n fh.write(html_data)\n# set example path for thumbnail\n# sphinx_gallery_thumbnail_path = '_static/CEBRA_embedding.png'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CEBRA example\n\n.. raw:: html\n :file: out.html\n\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/_downloads/c2db0bf2b334d541b00662b991682256/plot_6_real_time_demo.py b/_downloads/c2db0bf2b334d541b00662b991682256/plot_6_real_time_demo.py new file mode 100644 index 00000000..8f251ee8 --- /dev/null +++ b/_downloads/c2db0bf2b334d541b00662b991682256/plot_6_real_time_demo.py @@ -0,0 +1,121 @@ +""" +Real-time feature estimation +============================ + +""" + +# %% +# Implementation of individual nm_streams +# --------------------------------------- +# +# *py_neuromodulation* was optimized for computation of real-time data streams. +# There are however center -and lab specific hardware acquisition systems. Therefore, each experiment requires modules to interact with hardware platforms +# which periodically acquire data. +# +# Given the raw data, data can be analyzed using *py_neuromodulation*. Preprocessing methods, such as re-referencing and normalization, +# feature computation and decoding can be performed then in real-time. +# +# For online as well as as offline analysis, the :class:`~nm_stream_abc` class needs to be instantiated. +# Here the `nm_settings` and `nm_channels` are required to be defined. +# Previously for the offline analysis, an offline :class:`~nm_generator` object was defined that periodically yielded data. +# For online data, the :meth:`~nm_stream_abc.run` function therefore needs to be overwritten, which first acquires data and then calls +# the :meth:`~nm_run_analysis.process` function. +# +# The following illustrates in pseudo-code how such a stream could be initialized: +# +# .. code-block:: python +# +# from py_neuromodulation import nm_stream_abc +# +# class MyStream(nm_stream_abc): +# def __init__(self, settings, channels): +# super().__init__(settings, channels) +# +# def run(self): +# features_ = [] +# while True: +# data = self.acquire_data() +# features_.append(self.run_analysis.process(data)) +# # potentially use machine learning model for decoding +# +# +# Computation time examples +# ------------------------- +# +# The following example calculates for six channels, CAR re-referencing, z-score normalization and FFT features results the following computation time: + +# %% +import py_neuromodulation as nm +from py_neuromodulation import NMSettings +import numpy as np +import timeit + + +def get_fast_compute_settings(): + settings = NMSettings.get_fast_compute() + + settings.preprocessing = ["re_referencing", "notch_filter"] + settings.features.fft = True + settings.postprocessing.feature_normalization = True + return settings + + +data = np.random.random([1, 1000]) + +print("FFT Features, CAR re-referencing, z-score normalization") +print() +print("Computation time for single ECoG channel: ") +stream = nm.Stream( + sfreq=1000, + data=data, + sampling_rate_features_hz=10, + verbose=False, + settings=get_fast_compute_settings(), +) +print( + f"{np.round(timeit.timeit(lambda: stream.data_processor.process(data), number=100)/100, 3)} s" +) + +print("Computation time for 6 ECoG channels: ") +data = np.random.random([6, 1000]) +stream = nm.Stream( + sfreq=1000, + data=data, + sampling_rate_features_hz=10, + verbose=False, + settings=get_fast_compute_settings(), +) +print( + f"{np.round(timeit.timeit(lambda: stream.data_processor.process(data), number=100)/100, 3)} s" +) + +print( + "\nFFT Features & Temporal Waveform Shape & Hjorth & Bursts, CAR re-referencing, z-score normalization" +) +print("Computation time for single ECoG channel: ") +data = np.random.random([1, 1000]) +stream = nm.Stream( + sfreq=1000, data=data, sampling_rate_features_hz=10, verbose=False +) +print( + f"{np.round(timeit.timeit(lambda: stream.data_processor.process(data), number=10)/10, 3)} s" +) + + +# %% +# Those results show that the computation time for a typical pipeline (FFT, re-referencing, notch-filtering, feature normalization) +# is well below 10 ms, which is fast enough for real-time analysis with feature sampling rates below 100 Hz. +# Computation of more complex features could still result in feature sampling rates of more than 30 Hz. +# +# Real-time movement decoding using the TMSi-SAGA amplifier +# --------------------------------------------------------- +# +# In the following example, we will show how we setup a real-time movement decoding experiment using the TMSi-SAGA amplifier. +# First, we relied on different software modules for data streaming and visualization. +# `LabStreamingLayer `_ allows for real-time data streaming and synchronization across multiple devices. +# We used `timeflux `_ for real-time data visualization of features, decoded output. +# For raw data visualization we used `Brain Streaming Layer `_. +# +# The code for real-time movement decoding is added in the GitHub branch `realtime_decoding `_. +# Here we relied on the `TMSI SAGA Python interface `_. +# diff --git a/_downloads/ce3914826f782cbd1ea8fd024eaf0ac3/plot_5_example_rmap_computing.py b/_downloads/ce3914826f782cbd1ea8fd024eaf0ac3/plot_5_example_rmap_computing.py new file mode 100644 index 00000000..c1c04629 --- /dev/null +++ b/_downloads/ce3914826f782cbd1ea8fd024eaf0ac3/plot_5_example_rmap_computing.py @@ -0,0 +1,63 @@ +""" +R-Map computation +================= + +""" + +# %% +# Across patient decoding using R-Map optimal connectivity +# -------------------------------------------------------- +# +# ECoG electrode placement is commonly very heterogeneous across patients and cohorts. +# To still facilitate approaches that are able to perform decoding applications without patient individual training, +# two across-patient decoding approaches were previously investigated for movement decoding: +# +# +# * grid-point decoding +# * optimal connectivity channel decoding +# +# +# First, the grid-point decoding approach relies on definition of a cortical or subcortical grid. +# Data from individual grid points is then interpolated onto those common grid points. +# The approach was also explained in the :doc:`plot_4_example_gridPointProjection` notebook. +# +# .. image:: ../_static/RMAP_figure.png +# :alt: R-Map and grid point approach for decoding without patient-individual training +# +# The R-Map decoding approach relies on the other hand on computation of whole brain connectivity. The electrode MNI space locations need to be known, +# then the following steps can be performed for decoding without patient individual training: +# +# #. Using the `wjn_toolbox `_ *wjn_specrical_roi* function, the MNI coordinates can be transformed into NIFTI (.nii) files, containing the electrode contact region of interest (ROI): +# +# .. code-block:: python +# +# wjn_spherical_roi(roiname, mni, 4) +# +# #. For the given *ROI.nii* files, the LeadDBS `LeadMapper `_ tool can be used for functional or structural connectivity estimation. +# +# #. The py_neuromodulation :class:`~nm_RMAP.py` module can then compute the R-Map given the contact-individual connectivity fingerprints: +# +# .. code-block:: python +# +# nm_RMAP.calculate_RMap_numba(fingerprints, performances) +# +# #. The fingerprints from test-set patients can then be correlated with the calculated R-Map: +# +# .. code-block:: python +# +# nm_RMAP.get_corr_numba(fp, fp_test) +# +# #. The channel with highest correlation can then be selected for decoding without individual training. :class:`~nm_RMAP.py` contain already leave one channel and leave one patient out cross validation functions: +# +# .. code-block:: python +# +# nm_RMAP.leave_one_sub_out_cv(l_fps_names, l_fps_dat, l_per, sub_list) +# +# #. The obtained R-Map correlations can then be estimated statistically and plotted against true correlates: +# +# .. code-block:: python +# +# nm_RMAP.plot_performance_prediction_correlation(per_left_out, per_predict, out_path_save) +# +# +# sphinx_gallery_thumbnail_path = '_static/RMAP_figure.png' diff --git a/_downloads/da36848a41e6a3235d91fb7cfb6d59b4/plot_0_first_demo.py b/_downloads/da36848a41e6a3235d91fb7cfb6d59b4/plot_0_first_demo.py new file mode 100644 index 00000000..4cec9288 --- /dev/null +++ b/_downloads/da36848a41e6a3235d91fb7cfb6d59b4/plot_0_first_demo.py @@ -0,0 +1,189 @@ +""" +First Demo +========== + +This Demo will showcase the feature estimation and +exemplar analysis using simulated data. +""" + +import numpy as np +from matplotlib import pyplot as plt + +import py_neuromodulation as nm + +from py_neuromodulation import nm_analysis, nm_define_nmchannels, nm_plots, NMSettings + +# %% +# Data Simulation +# --------------- +# We will now generate some exemplar data with 10 second duration for 6 channels with a sample rate of 1 kHz. + + +def generate_random_walk(NUM_CHANNELS, TIME_DATA_SAMPLES): + # from https://towardsdatascience.com/random-walks-with-python-8420981bc4bc + dims = NUM_CHANNELS + step_n = TIME_DATA_SAMPLES - 1 + step_set = [-1, 0, 1] + origin = (np.random.random([1, dims]) - 0.5) * 1 # Simulate steps in 1D + step_shape = (step_n, dims) + steps = np.random.choice(a=step_set, size=step_shape) + path = np.concatenate([origin, steps]).cumsum(0) + return path.T + + +NUM_CHANNELS = 6 +sfreq = 1000 +TIME_DATA_SAMPLES = 10 * sfreq +data = generate_random_walk(NUM_CHANNELS, TIME_DATA_SAMPLES) +time = np.arange(0, TIME_DATA_SAMPLES / sfreq, 1 / sfreq) + +plt.figure(figsize=(8, 4), dpi=100) +for ch_idx in range(data.shape[0]): + plt.plot(time, data[ch_idx, :]) +plt.xlabel("Time [s]") +plt.ylabel("Amplitude") +plt.title("Example random walk data") + +# %% +# Now let’s define the necessary setup files we will be using for data +# preprocessing and feature estimation. Py_neuromodualtion is based on two +# parametrization files: the *nm_channels.tsv* and the *nm_setting.json*. +# +# nm_channels +# ~~~~~~~~~~~ +# +# The *nm_channel* dataframe. This dataframe contains the columns +# +# +-----------------------------------+-----------------------------------+ +# | Column name | Description | +# +===================================+===================================+ +# | **name** | name of the channel | +# +-----------------------------------+-----------------------------------+ +# | **rereference** | different channel name for | +# | | bipolar re-referencing, or | +# | | average for common average | +# | | re-referencing | +# +-----------------------------------+-----------------------------------+ +# | **used** | 0 or 1, channel selection | +# +-----------------------------------+-----------------------------------+ +# | **target** | 0 or 1, for some decoding | +# | | applications we can define target | +# | | channels, e.g. EMG channels | +# +-----------------------------------+-----------------------------------+ +# | **type** | channel type according to the | +# | | `mne-python`_ toolbox | +# | | | +# | | | +# | | | +# | | | +# | | e.g. ecog, eeg, ecg, emg, dbs, | +# | | seeg etc. | +# +-----------------------------------+-----------------------------------+ +# | **status** | good or bad, used for channel | +# | | quality indication | +# +-----------------------------------+-----------------------------------+ +# | **new_name** | this keyword can be specified to | +# | | indicate for example the used | +# | | rereferncing scheme | +# +-----------------------------------+-----------------------------------+ +# +# .. _mne-python: https://mne.tools/stable/auto_tutorials/raw/10_raw_overview.html#sphx-glr-auto-tutorials-raw-10-raw-overview-py +# +# The :class:`~nm_stream_abc` can either be created as a *.tsv* text file, or as a pandas +# DataFrame. There are some helper functions that let you create the +# nm_channels without much effort: + +nm_channels = nm_define_nmchannels.get_default_channels_from_data( + data, car_rereferencing=True +) + +nm_channels + +# %% +# Using this function default channel names and a common average re-referencing scheme is specified. +# Alternatively the *nm_define_nmchannels.set_channels* function can be used to pass each column values. +# +# nm_settings +# ----------- +# Next, we will initialize the nm_settings dictionary and use the default settings, reset them, and enable a subset of features: + +settings = NMSettings.get_fast_compute() + + +# %% +# The setting itself is a .json file which contains the parametrization for preprocessing, feature estimation, postprocessing and +# definition with which sampling rate features are being calculated. +# In this example `sampling_rate_features_hz` is specified to be 10 Hz, so every 100ms a new set of features is calculated. +# +# For many features the `segment_length_features_ms` specifies the time dimension of the raw signal being used for feature calculation. Here it is specified to be 1000 ms. +# +# We will now enable the features: +# +# * fft +# * bursts +# * sharpwave +# +# and stay with the default preprcessing methods: +# +# * notch_filter +# * re_referencing +# +# and use *z-score* postprocessing normalization. + +settings.features.fooof = True +settings.features.fft = True +settings.features.bursts = True +settings.features.sharpwave_analysis = True + +# %% +# We are now ready to go to instantiate the *Stream* and call the *run* method for feature estimation: + +stream = nm.Stream( + settings=settings, + nm_channels=nm_channels, + verbose=True, + sfreq=sfreq, + line_noise=50, +) + +features = stream.run(data) + +# %% +# Feature Analysis +# ---------------- +# +# There is a lot of output, which we could omit by verbose being False, but let's have a look what was being computed. +# We will therefore use the :class:`~nm_analysis` class to showcase some functions. For multi-run -or subject analysis we will pass here the feature_file "sub" as default directory: + +analyzer = nm_analysis.FeatureReader( + feature_dir=stream.PATH_OUT, feature_file=stream.PATH_OUT_folder_name +) + +# %% +# Let's have a look at the resulting "feature_arr" DataFrame: + +analyzer.feature_arr.iloc[:10, :7] + +# %% +# Seems like a lot of features were calculated. The `time` column tells us about each row time index. +# For the 6 specified channels, it is each 31 features. +# We can now use some in-built plotting functions for visualization. +# +# .. note:: +# +# Due to the nature of simulated data, some of the features have constant values, which are not displayed through the image normalization. +# +# + +analyzer.plot_all_features(ch_used="ch1") + +# %% +nm_plots.plot_corr_matrix( + figsize=(25, 25), + show_plot=True, + feature=analyzer.feature_arr, +) + +# %% +# The upper correlation matrix shows the correlation of every feature of every channel to every other. +# This notebook demonstrated a first demo how features can quickly be generated. For further feature modalities and decoding applications check out the next notebooks. diff --git a/_downloads/eaa4305c75b19a1e2eea941f742a6331/plot_4_example_gridPointProjection.py b/_downloads/eaa4305c75b19a1e2eea941f742a6331/plot_4_example_gridPointProjection.py new file mode 100644 index 00000000..cae885a4 --- /dev/null +++ b/_downloads/eaa4305c75b19a1e2eea941f742a6331/plot_4_example_gridPointProjection.py @@ -0,0 +1,208 @@ +""" +Grid Point Projection +===================== + +""" + +# %% +# In ECoG datasets the electrode locations are usually different. For this reason, we established a grid +# with a set of points defined in a standardized MNI brain. +# Data is then interpolated to this grid, such that they are common across patients, which allows across patient decoding use cases. +# +# In this notebook, we will plot these grid points and see how the features extracted from our data can be projected into this grid space. +# +# In order to do so, we'll read saved features that were computed in the ECoG movement notebook. +# Please note that in order to do so, when running the feature estimation, the settings +# +# .. note:: +# +# .. code-block:: python +# +# stream.settings['postprocessing']['project_cortex'] = True +# stream.settings['postprocessing']['project_subcortex'] = True +# +# need to be set to `True` for a cortical and/or subcortical projection. +# + +# %% +import numpy as np +import matplotlib.pyplot as plt + +import py_neuromodulation as nm +from py_neuromodulation import ( + nm_analysis, + nm_plots, + nm_IO, + NMSettings, + nm_define_nmchannels +) + + +# %% +# Read features from BIDS data +# ---------------------------- +# +# We first estimate features, with the `grid_point` projection settings enabled for cortex. + + +# %% +RUN_NAME, PATH_RUN, PATH_BIDS, PATH_OUT, datatype = nm_IO.get_paths_example_data() + +( + raw, + data, + sfreq, + line_noise, + coord_list, + coord_names, +) = nm_IO.read_BIDS_data( + PATH_RUN=PATH_RUN +) + +settings = NMSettings.get_fast_compute() + +settings.postprocessing.project_cortex = True + +nm_channels = nm_define_nmchannels.set_channels( + ch_names=raw.ch_names, + ch_types=raw.get_channel_types(), + reference="default", + bads=raw.info["bads"], + new_names="default", + used_types=("ecog", "dbs", "seeg"), + target_keywords=["MOV_RIGHT_CLEAN","MOV_LEFT_CLEAN"] +) + +stream = nm.Stream( + sfreq=sfreq, + nm_channels=nm_channels, + settings=settings, + line_noise=line_noise, + coord_list=coord_list, + coord_names=coord_names, + verbose=True, +) + +features = stream.run( + data=data[:, :int(sfreq*5)], + out_path_root=PATH_OUT, + folder_name=RUN_NAME, +) + +# %% +# From nm_analysis.py, we use the :class:~`nm_analysis.FeatureReader` class to load the data. + +# init analyzer +feature_reader = nm_analysis.FeatureReader( + feature_dir=PATH_OUT, feature_file=RUN_NAME +) + +# %% +# To perform the grid projection, for all computed features we check for every grid point if there is any electrode channel within the spatial range ```max_dist_mm```, and weight +# this electrode contact by the inverse distance and normalize across all electrode distances within the maximum distance range. +# This gives us a projection matrix that we can apply to streamed data, to transform the feature-channel matrix *(n_features, n_channels)* into the grid point matrix *(n_features, n_gridpoints)*. +# +# To save computation time, this projection matrix is precomputed before the real time run computation. +# The cortical grid is stored in *py_neuromodulation/grid_cortex.tsv* and the electrodes coordinates are stored in *_space-mni_electrodes.tsv* in a BIDS dataset. +# +# .. note:: +# +# One remark is that our cortical and subcortical grids are defined for the **left** hemisphere of the brain and, therefore, electrode contacts are mapped to the left hemisphere. +# +# From the analyzer, the user can plot the cortical projection with the function below, display the grid points and ECoG electrodes are crosses. +# The yellow grid points are the ones that are active for that specific ECoG electrode location. The inactive grid points are shown in purple. + +feature_reader.plot_cort_projection() + +# %% +# We can also plot only the ECoG electrodes or the grid points, with the help of the data saved in feature_reader.sidecar. BIDS sidecar files are json files where you store additional information, here it is used to save the ECoG strip positions and the grid coordinates, which are not part of the settings and nm_channels.csv. We can check what is stored in the file and then use the nmplotter.plot_cortex function: + +grid_plotter = nm_plots.NM_Plot( + ecog_strip=np.array(feature_reader.sidecar["coords"]["cortex_right"]["positions"]), + grid_cortex=np.array(feature_reader.sidecar["grid_cortex"]), + # grid_subcortex=np.array(feature_reader.sidecar["grid_subcortex"]), + sess_right=feature_reader.sidecar["sess_right"], + proj_matrix_cortex=np.array(feature_reader.sidecar["proj_matrix_cortex"]) +) + +# %% +grid_plotter.plot_cortex( + grid_color=np.sum(np.array(feature_reader.sidecar["proj_matrix_cortex"]),axis=1), + lower_clim=0., + upper_clim=1.0, + cbar_label="Used Grid Points", + title = "ECoG electrodes projected onto cortical grid" +) + +# %% +feature_reader.sidecar["coords"]["cortex_right"]["positions"] + +# %% +feature_reader.nmplotter.plot_cortex( + ecog_strip=np.array( + feature_reader.sidecar["coords"]["cortex_right"]["positions"], + ), + lower_clim=0., + upper_clim=1.0, + cbar_label="Used ECoG Electrodes", + title = "Plot of ECoG electrodes" +) + +# %% +feature_reader.nmplotter.plot_cortex( + np.array( + feature_reader.sidecar["grid_cortex"] + ), + lower_clim=0., + upper_clim=1.0, + cbar_label="All Grid Points", + title = "All grid points" +) + +# %% +# The Projection Matrix +# --------------------- +# To go from the feature-channel matrix *(n_features, n_channels)* to the grid point matrix *(n_features, n_gridpoints)* +# we need a projection matrix that has the shape *(n_channels, n_gridpoints)*. +# It maps the strengths of the signals in each ECoG channel to the correspondent ones in the cortical grid. +# In the cell below we plot this matrix, that has the property that the column sum over channels for each grid point is either 1 or 0. + +plt.figure(figsize=(8,5)) +plt.imshow(np.array(feature_reader.sidecar['proj_matrix_cortex']), aspect = 'auto') +plt.colorbar(label = "Strength of ECoG signal in each grid point") +plt.xlabel("ECoG channels") +plt.ylabel("Grid points") +plt.title("Matrix mapping from ECoG to grid") + +# %% +# Feature Plot in the Grid: An Example of Post-processing +# ------------------------------------------------------- +# First we take the dataframe with all the features in all time points. + +df = feature_reader.feature_arr + +# %% +df.iloc[:5, :5] + +# %% +# Then we filter for only 'avgref_fft_theta', which gives us the value for fft_theta in all 6 ECoG channels over all time points. Then we take only the 6th time point - as an arbitrary choice. + +fft_theta_oneTimePoint = np.asarray(df[df.columns[df.columns.str.contains(pat = 'avgref_fft_theta')]].iloc[5]) +fft_theta_oneTimePoint + +# %% +# Then the projection of the features into the grid is gonna be the color of the grid points in the *plot_cortex* function. +# That is the matrix multiplication of the projection matrix of the cortex and 6 values for the *fft_theta* feature above. + +grid_fft_Theta = np.array(feature_reader.sidecar["proj_matrix_cortex"]) @ fft_theta_oneTimePoint + +feature_reader.nmplotter.plot_cortex(np.array( + feature_reader.sidecar["grid_cortex"]),grid_color = grid_fft_Theta, set_clim = True, lower_clim=min(grid_fft_Theta[grid_fft_Theta>0]), upper_clim=max(grid_fft_Theta), cbar_label="FFT Theta Projection to Grid", title = "FFT Theta Projection to Grid") + +# %% +# Lower and upper boundaries for clim were chosen to be the max and min values of the projection of the features (minimum value excluding zero). This can be checked in the cell below: + +grid_fft_Theta + +# %% +# In the plot above we can see how the intensity of the fast fourier transform in the theta band varies for each grid point in the cortex, for one specific time point. diff --git a/_downloads/efd2bd0f438531ec5d957e08ce0d6909/plot_3_example_sharpwave_analysis.ipynb b/_downloads/efd2bd0f438531ec5d957e08ce0d6909/plot_3_example_sharpwave_analysis.ipynb new file mode 100644 index 00000000..9a98b19a --- /dev/null +++ b/_downloads/efd2bd0f438531ec5d957e08ce0d6909/plot_3_example_sharpwave_analysis.ipynb @@ -0,0 +1,158 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n# Analyzing temporal features\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Time series data can be characterized using oscillatory components, but assumptions of sinusoidality are for real data rarely fulfilled.\nSee *\"Brain Oscillations and the Importance of Waveform Shape\"* [Cole et al 2017](https://doi.org/10.1016/j.tics.2016.12.008) for a great motivation.\nWe implemented here temporal characteristics based on individual trough and peak relations,\nbased on the :meth:~`scipy.signal.find_peaks` method. The function parameter *distance* can be specified in the *nm_settings.json*.\nTemporal features can be calculated twice for troughs and peaks. In the settings, this can be specified by setting *estimate* to true\nin *detect_troughs* and/or *detect_peaks*. A statistical measure (e.g. mean, max, median, var) can be defined as a resulting feature from the peak and\ntrough estimates using the *apply_estimator_between_peaks_and_troughs* setting.\n\nIn py_neuromodulation the following characteristics are implemented:\n\n

Note

The nomenclature is written here for sharpwave troughs, but detection of peak characteristics can be computed in the same way.

\n\n- prominence:\n $V_{prominence} = |\\frac{V_{peak-left} + V_{peak-right}}{2}| - V_{trough}$\n- sharpness:\n $V_{sharpnesss} = \\frac{(V_{trough} - V_{trough-5 ms}) + (V_{trough} - V_{trough+5 ms})}{2}$\n- rise and decay rise time\n- rise and decay steepness\n- width (between left and right peaks)\n- interval (between troughs)\n\nAdditionally, different filter ranges can be parametrized using the *filter_ranges_hz* setting.\nFiltering is necessary to remove high frequent signal fluctuations, but limits also the true estimation of sharpness and prominence due to signal smoothing.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from typing import cast\nimport seaborn as sb\nfrom matplotlib import pyplot as plt\nfrom scipy import signal\nfrom scipy.signal import fftconvolve\nimport numpy as np\n\nimport py_neuromodulation as nm\nfrom py_neuromodulation import (\n nm_define_nmchannels,\n nm_IO,\n NMSettings,\n)\nfrom py_neuromodulation.nm_sharpwaves import SharpwaveAnalyzer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will first read the example ECoG data and plot the identified features on the filtered time series.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "RUN_NAME, PATH_RUN, PATH_BIDS, PATH_OUT, datatype = nm_IO.get_paths_example_data()\n\n(\n raw,\n data,\n sfreq,\n line_noise,\n coord_list,\n coord_names,\n) = nm_IO.read_BIDS_data(PATH_RUN=PATH_RUN)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "settings = NMSettings.get_fast_compute()\n\nsettings.features.fft = True\nsettings.features.bursts = False\nsettings.features.sharpwave_analysis = True\nsettings.features.coherence = False\n\nsettings.sharpwave_analysis_settings.estimator[\"mean\"] = []\nsettings.sharpwave_analysis_settings.sharpwave_features.enable_all()\nfor sw_feature in settings.sharpwave_analysis_settings.sharpwave_features.list_all():\n settings.sharpwave_analysis_settings.estimator[\"mean\"].append(sw_feature)\n\nnm_channels = nm_define_nmchannels.set_channels(\n ch_names=raw.ch_names,\n ch_types=raw.get_channel_types(),\n reference=\"default\",\n bads=raw.info[\"bads\"],\n new_names=\"default\",\n used_types=(\"ecog\", \"dbs\", \"seeg\"),\n target_keywords=[\"MOV_RIGHT\"],\n)\n\nstream = nm.Stream(\n sfreq=sfreq,\n nm_channels=nm_channels,\n settings=settings,\n line_noise=line_noise,\n coord_list=coord_list,\n coord_names=coord_names,\n verbose=False,\n)\nsw_analyzer = cast(\n SharpwaveAnalyzer, stream.data_processor.features.get_feature(\"sharpwave_analysis\")\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The plotted example time series, visualized on a short time scale, shows the relation of identified peaks, troughs, and estimated features:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "data_plt = data[5, 1000:4000]\n\nfiltered_dat = fftconvolve(data_plt, sw_analyzer.list_filter[0][1], mode=\"same\")\n\ntroughs = signal.find_peaks(-filtered_dat, distance=10)[0]\npeaks = signal.find_peaks(filtered_dat, distance=5)[0]\n\nsw_results = sw_analyzer.analyze_waveform(filtered_dat)\n\nWIDTH = BAR_WIDTH = 4\nBAR_OFFSET = 50\nOFFSET_TIME_SERIES = -100\nSCALE_TIMESERIES = 1\n\nhue_colors = sb.color_palette(\"viridis_r\", 6)\n\nplt.figure(figsize=(5, 3), dpi=300)\nplt.plot(\n OFFSET_TIME_SERIES + data_plt,\n color=\"gray\",\n linewidth=0.5,\n alpha=0.5,\n label=\"original ECoG data\",\n)\nplt.plot(\n OFFSET_TIME_SERIES + filtered_dat * SCALE_TIMESERIES,\n linewidth=0.5,\n color=\"black\",\n label=\"[5-30]Hz filtered data\",\n)\n\nplt.plot(\n peaks,\n OFFSET_TIME_SERIES + filtered_dat[peaks] * SCALE_TIMESERIES,\n \"x\",\n label=\"peaks\",\n markersize=3,\n color=\"darkgray\",\n)\nplt.plot(\n troughs,\n OFFSET_TIME_SERIES + filtered_dat[troughs] * SCALE_TIMESERIES,\n \"x\",\n label=\"troughs\",\n markersize=3,\n color=\"lightgray\",\n)\n\nplt.bar(\n troughs + BAR_WIDTH,\n np.array(sw_results[\"prominence\"]) * 4,\n bottom=BAR_OFFSET,\n width=WIDTH,\n color=hue_colors[0],\n label=\"Prominence\",\n alpha=0.5,\n)\nplt.bar(\n troughs + BAR_WIDTH * 2,\n -np.array(sw_results[\"sharpness\"]) * 6,\n bottom=BAR_OFFSET,\n width=WIDTH,\n color=hue_colors[1],\n label=\"Sharpness\",\n alpha=0.5,\n)\nplt.bar(\n troughs + BAR_WIDTH * 3,\n np.array(sw_results[\"interval\"]) * 5,\n bottom=BAR_OFFSET,\n width=WIDTH,\n color=hue_colors[2],\n label=\"Interval\",\n alpha=0.5,\n)\nplt.bar(\n troughs + BAR_WIDTH * 4,\n np.array(sw_results[\"rise_time\"]) * 5,\n bottom=BAR_OFFSET,\n width=WIDTH,\n color=hue_colors[3],\n label=\"Rise time\",\n alpha=0.5,\n)\n\nplt.xticks(\n np.arange(0, data_plt.shape[0], 200),\n np.round(np.arange(0, int(data_plt.shape[0] / 1000), 0.2), 2),\n)\nplt.xlabel(\"Time [s]\")\nplt.title(\"Temporal waveform shape features\")\nplt.legend(loc=\"center left\", bbox_to_anchor=(1, 0.5))\nplt.ylim(-550, 700)\nplt.xlim(0, 200)\nplt.ylabel(\"a.u.\")\nplt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "See in the following example a time series example, that is aligned to movement. With movement onset the prominence, sharpness, and interval features are reduced:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "plt.figure(figsize=(8, 5), dpi=300)\nplt.plot(\n OFFSET_TIME_SERIES + data_plt,\n color=\"gray\",\n linewidth=0.5,\n alpha=0.5,\n label=\"original ECoG data\",\n)\nplt.plot(\n OFFSET_TIME_SERIES + filtered_dat * SCALE_TIMESERIES,\n linewidth=0.5,\n color=\"black\",\n label=\"[5-30]Hz filtered data\",\n)\n\nplt.plot(\n peaks,\n OFFSET_TIME_SERIES + filtered_dat[peaks] * SCALE_TIMESERIES,\n \"x\",\n label=\"peaks\",\n markersize=3,\n color=\"darkgray\",\n)\nplt.plot(\n troughs,\n OFFSET_TIME_SERIES + filtered_dat[troughs] * SCALE_TIMESERIES,\n \"x\",\n label=\"troughs\",\n markersize=3,\n color=\"lightgray\",\n)\n\nplt.bar(\n troughs + BAR_WIDTH,\n np.array(sw_results[\"prominence\"]) * 4,\n bottom=BAR_OFFSET,\n width=WIDTH,\n color=hue_colors[0],\n label=\"Prominence\",\n alpha=0.5,\n)\nplt.bar(\n troughs + BAR_WIDTH * 2,\n -np.array(sw_results[\"sharpness\"]) * 6,\n bottom=BAR_OFFSET,\n width=WIDTH,\n color=hue_colors[1],\n label=\"Sharpness\",\n alpha=0.5,\n)\nplt.bar(\n troughs + BAR_WIDTH * 3,\n np.array(sw_results[\"interval\"]) * 5,\n bottom=BAR_OFFSET,\n width=WIDTH,\n color=hue_colors[2],\n label=\"Interval\",\n alpha=0.5,\n)\nplt.bar(\n troughs + BAR_WIDTH * 4,\n np.array(sw_results[\"rise_time\"]) * 5,\n bottom=BAR_OFFSET,\n width=WIDTH,\n color=hue_colors[3],\n label=\"Rise time\",\n alpha=0.5,\n)\n\nplt.axvline(x=1500, label=\"Movement start\", color=\"red\")\n\n# plt.xticks(np.arange(0, 2000, 200), np.round(np.arange(0, 2, 0.2), 2))\nplt.xticks(\n np.arange(0, data_plt.shape[0], 200),\n np.round(np.arange(0, int(data_plt.shape[0] / 1000), 0.2), 2),\n)\nplt.xlabel(\"Time [s]\")\nplt.title(\"Temporal waveform shape features\")\nplt.legend(loc=\"center left\", bbox_to_anchor=(1, 0.5))\nplt.ylim(-450, 400)\nplt.ylabel(\"a.u.\")\nplt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the *sharpwave_analysis_settings* the *estimator* keyword further specifies which statistic is computed based on the individual\nfeatures in one batch. The \"global\" setting *segment_length_features_ms* specifies the time duration for feature computation.\nSince there can be a different number of identified waveform shape features for different batches (i.e. different number of peaks/troughs),\ntaking a statistical measure (e.g. the maximum or mean) will be necessary for feature comparison.\n\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example time series computation for movement decoding\nWe will now read the ECoG example/data and investigate if samples differ across movement states. Therefore we compute features and enable the default *sharpwave* features.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "settings = NMSettings.get_default().reset()\n\nsettings.features.sharpwave_analysis = True\nsettings.sharpwave_analysis_settings.filter_ranges_hz = [[5, 80]]\n\nnm_channels[\"used\"] = 0 # set only two ECoG channels for faster computation to true\nnm_channels.loc[[3, 8], \"used\"] = 1\n\nstream = nm.Stream(\n sfreq=sfreq,\n nm_channels=nm_channels,\n settings=settings,\n line_noise=line_noise,\n coord_list=coord_list,\n coord_names=coord_names,\n verbose=True,\n)\n\ndf_features = stream.run(data=data[:, :30000])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can then plot two exemplary features, prominence and interval, and see that the movement amplitude can be clustered with those two features alone:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "plt.figure(figsize=(5, 3), dpi=300)\nprint(df_features.columns)\nplt.scatter(\n df_features[\"ECOG_RIGHT_0-avgref_Sharpwave_Max_prominence_range_5_80\"],\n df_features[\"ECOG_RIGHT_5-avgref_Sharpwave_Mean_interval_range_5_80\"],\n c=df_features.MOV_RIGHT,\n alpha=0.8,\n s=30,\n)\ncbar = plt.colorbar()\ncbar.set_label(\"Movement amplitude\")\nplt.xlabel(\"Prominence a.u.\")\nplt.ylabel(\"Interval a.u.\")\nplt.title(\"Temporal features predict movement amplitude\")\nplt.tight_layout()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/_downloads/f6a010924997ddb4081d17921d191f88/plot_0_first_demo.ipynb b/_downloads/f6a010924997ddb4081d17921d191f88/plot_0_first_demo.ipynb new file mode 100644 index 00000000..6c735ff8 --- /dev/null +++ b/_downloads/f6a010924997ddb4081d17921d191f88/plot_0_first_demo.ipynb @@ -0,0 +1,205 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n# First Demo\n\nThis Demo will showcase the feature estimation and\nexemplar analysis using simulated data.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import numpy as np\nfrom matplotlib import pyplot as plt\n\nimport py_neuromodulation as nm\n\nfrom py_neuromodulation import nm_analysis, nm_define_nmchannels, nm_plots, NMSettings" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Data Simulation\nWe will now generate some exemplar data with 10 second duration for 6 channels with a sample rate of 1 kHz.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "def generate_random_walk(NUM_CHANNELS, TIME_DATA_SAMPLES):\n # from https://towardsdatascience.com/random-walks-with-python-8420981bc4bc\n dims = NUM_CHANNELS\n step_n = TIME_DATA_SAMPLES - 1\n step_set = [-1, 0, 1]\n origin = (np.random.random([1, dims]) - 0.5) * 1 # Simulate steps in 1D\n step_shape = (step_n, dims)\n steps = np.random.choice(a=step_set, size=step_shape)\n path = np.concatenate([origin, steps]).cumsum(0)\n return path.T\n\n\nNUM_CHANNELS = 6\nsfreq = 1000\nTIME_DATA_SAMPLES = 10 * sfreq\ndata = generate_random_walk(NUM_CHANNELS, TIME_DATA_SAMPLES)\ntime = np.arange(0, TIME_DATA_SAMPLES / sfreq, 1 / sfreq)\n\nplt.figure(figsize=(8, 4), dpi=100)\nfor ch_idx in range(data.shape[0]):\n plt.plot(time, data[ch_idx, :])\nplt.xlabel(\"Time [s]\")\nplt.ylabel(\"Amplitude\")\nplt.title(\"Example random walk data\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let\u2019s define the necessary setup files we will be using for data\npreprocessing and feature estimation. Py_neuromodualtion is based on two\nparametrization files: the *nm_channels.tsv* and the *nm_setting.json*.\n\n### nm_channels\n\nThe *nm_channel* dataframe. This dataframe contains the columns\n\n+-----------------------------------+-----------------------------------+\n| Column name | Description |\n+===================================+===================================+\n| **name** | name of the channel |\n+-----------------------------------+-----------------------------------+\n| **rereference** | different channel name for |\n| | bipolar re-referencing, or |\n| | average for common average |\n| | re-referencing |\n+-----------------------------------+-----------------------------------+\n| **used** | 0 or 1, channel selection |\n+-----------------------------------+-----------------------------------+\n| **target** | 0 or 1, for some decoding |\n| | applications we can define target |\n| | channels, e.g.\u00a0EMG channels |\n+-----------------------------------+-----------------------------------+\n| **type** | channel type according to the |\n| | `mne-python`_ toolbox |\n| | |\n| | |\n| | |\n| | |\n| | e.g.\u00a0ecog, eeg, ecg, emg, dbs, |\n| | seeg etc. |\n+-----------------------------------+-----------------------------------+\n| **status** | good or bad, used for channel |\n| | quality indication |\n+-----------------------------------+-----------------------------------+\n| **new_name** | this keyword can be specified to |\n| | indicate for example the used |\n| | rereferncing scheme |\n+-----------------------------------+-----------------------------------+\n\n\nThe :class:`~nm_stream_abc` can either be created as a *.tsv* text file, or as a pandas\nDataFrame. There are some helper functions that let you create the\nnm_channels without much effort:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "nm_channels = nm_define_nmchannels.get_default_channels_from_data(\n data, car_rereferencing=True\n)\n\nnm_channels" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using this function default channel names and a common average re-referencing scheme is specified.\nAlternatively the *nm_define_nmchannels.set_channels* function can be used to pass each column values.\n\n## nm_settings\nNext, we will initialize the nm_settings dictionary and use the default settings, reset them, and enable a subset of features:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "settings = NMSettings.get_fast_compute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The setting itself is a .json file which contains the parametrization for preprocessing, feature estimation, postprocessing and\ndefinition with which sampling rate features are being calculated.\nIn this example `sampling_rate_features_hz` is specified to be 10 Hz, so every 100ms a new set of features is calculated.\n\nFor many features the `segment_length_features_ms` specifies the time dimension of the raw signal being used for feature calculation. Here it is specified to be 1000 ms.\n\nWe will now enable the features:\n\n* fft\n* bursts\n* sharpwave\n\nand stay with the default preprcessing methods:\n\n* notch_filter\n* re_referencing\n\nand use *z-score* postprocessing normalization.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "settings.features.fooof = True\nsettings.features.fft = True\nsettings.features.bursts = True\nsettings.features.sharpwave_analysis = True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We are now ready to go to instantiate the *Stream* and call the *run* method for feature estimation:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "stream = nm.Stream(\n settings=settings,\n nm_channels=nm_channels,\n verbose=True,\n sfreq=sfreq,\n line_noise=50,\n)\n\nfeatures = stream.run(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Feature Analysis\n\nThere is a lot of output, which we could omit by verbose being False, but let's have a look what was being computed.\nWe will therefore use the :class:`~nm_analysis` class to showcase some functions. For multi-run -or subject analysis we will pass here the feature_file \"sub\" as default directory:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "analyzer = nm_analysis.FeatureReader(\n feature_dir=stream.PATH_OUT, feature_file=stream.PATH_OUT_folder_name\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's have a look at the resulting \"feature_arr\" DataFrame:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "analyzer.feature_arr.iloc[:10, :7]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Seems like a lot of features were calculated. The `time` column tells us about each row time index.\nFor the 6 specified channels, it is each 31 features.\nWe can now use some in-built plotting functions for visualization.\n\n

Note

Due to the nature of simulated data, some of the features have constant values, which are not displayed through the image normalization.

\n\n\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "analyzer.plot_all_features(ch_used=\"ch1\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "nm_plots.plot_corr_matrix(\n figsize=(25, 25),\n show_plot=True,\n feature=analyzer.feature_arr,\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The upper correlation matrix shows the correlation of every feature of every channel to every other.\nThis notebook demonstrated a first demo how features can quickly be generated. For further feature modalities and decoding applications check out the next notebooks.\n\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/_images/RMAP_figure.png b/_images/RMAP_figure.png new file mode 100644 index 00000000..97c798aa Binary files /dev/null and b/_images/RMAP_figure.png differ diff --git a/_images/sphx_glr_plot_0_first_demo_001.png b/_images/sphx_glr_plot_0_first_demo_001.png new file mode 100644 index 00000000..ba6d0cb7 Binary files /dev/null and b/_images/sphx_glr_plot_0_first_demo_001.png differ diff --git a/_images/sphx_glr_plot_0_first_demo_002.png b/_images/sphx_glr_plot_0_first_demo_002.png new file mode 100644 index 00000000..9e21c85d Binary files /dev/null and b/_images/sphx_glr_plot_0_first_demo_002.png differ diff --git a/_images/sphx_glr_plot_0_first_demo_003.png b/_images/sphx_glr_plot_0_first_demo_003.png new file mode 100644 index 00000000..fd6d17e3 Binary files /dev/null and b/_images/sphx_glr_plot_0_first_demo_003.png differ diff --git a/_images/sphx_glr_plot_0_first_demo_thumb.png b/_images/sphx_glr_plot_0_first_demo_thumb.png new file mode 100644 index 00000000..f0087613 Binary files /dev/null and b/_images/sphx_glr_plot_0_first_demo_thumb.png differ diff --git a/_images/sphx_glr_plot_1_example_BIDS_001.png b/_images/sphx_glr_plot_1_example_BIDS_001.png new file mode 100644 index 00000000..1d37396b Binary files /dev/null and b/_images/sphx_glr_plot_1_example_BIDS_001.png differ diff --git a/_images/sphx_glr_plot_1_example_BIDS_002.png b/_images/sphx_glr_plot_1_example_BIDS_002.png new file mode 100644 index 00000000..2e64cc5a Binary files /dev/null and b/_images/sphx_glr_plot_1_example_BIDS_002.png differ diff --git a/_images/sphx_glr_plot_1_example_BIDS_003.png b/_images/sphx_glr_plot_1_example_BIDS_003.png new file mode 100644 index 00000000..721b4c99 Binary files /dev/null and b/_images/sphx_glr_plot_1_example_BIDS_003.png differ diff --git a/_images/sphx_glr_plot_1_example_BIDS_004.png b/_images/sphx_glr_plot_1_example_BIDS_004.png new file mode 100644 index 00000000..9a2c53ca Binary files /dev/null and b/_images/sphx_glr_plot_1_example_BIDS_004.png differ diff --git a/_images/sphx_glr_plot_1_example_BIDS_005.png b/_images/sphx_glr_plot_1_example_BIDS_005.png new file mode 100644 index 00000000..32e3941b Binary files /dev/null and b/_images/sphx_glr_plot_1_example_BIDS_005.png differ diff --git a/_images/sphx_glr_plot_1_example_BIDS_thumb.png b/_images/sphx_glr_plot_1_example_BIDS_thumb.png new file mode 100644 index 00000000..094e4df6 Binary files /dev/null and b/_images/sphx_glr_plot_1_example_BIDS_thumb.png differ diff --git a/_images/sphx_glr_plot_2_example_add_feature_thumb.png b/_images/sphx_glr_plot_2_example_add_feature_thumb.png new file mode 100644 index 00000000..8a5fed58 Binary files /dev/null and b/_images/sphx_glr_plot_2_example_add_feature_thumb.png differ diff --git a/_images/sphx_glr_plot_3_example_sharpwave_analysis_001.png b/_images/sphx_glr_plot_3_example_sharpwave_analysis_001.png new file mode 100644 index 00000000..4a7d97e9 Binary files /dev/null and b/_images/sphx_glr_plot_3_example_sharpwave_analysis_001.png differ diff --git a/_images/sphx_glr_plot_3_example_sharpwave_analysis_002.png b/_images/sphx_glr_plot_3_example_sharpwave_analysis_002.png new file mode 100644 index 00000000..dfdcf841 Binary files /dev/null and b/_images/sphx_glr_plot_3_example_sharpwave_analysis_002.png differ diff --git a/_images/sphx_glr_plot_3_example_sharpwave_analysis_003.png b/_images/sphx_glr_plot_3_example_sharpwave_analysis_003.png new file mode 100644 index 00000000..66443a7c Binary files /dev/null and b/_images/sphx_glr_plot_3_example_sharpwave_analysis_003.png differ diff --git a/_images/sphx_glr_plot_3_example_sharpwave_analysis_thumb.png b/_images/sphx_glr_plot_3_example_sharpwave_analysis_thumb.png new file mode 100644 index 00000000..6ce75139 Binary files /dev/null and b/_images/sphx_glr_plot_3_example_sharpwave_analysis_thumb.png differ diff --git a/_images/sphx_glr_plot_4_example_gridPointProjection_001.png b/_images/sphx_glr_plot_4_example_gridPointProjection_001.png new file mode 100644 index 00000000..64012024 Binary files /dev/null and b/_images/sphx_glr_plot_4_example_gridPointProjection_001.png differ diff --git a/_images/sphx_glr_plot_4_example_gridPointProjection_002.png b/_images/sphx_glr_plot_4_example_gridPointProjection_002.png new file mode 100644 index 00000000..7c0b468b Binary files /dev/null and b/_images/sphx_glr_plot_4_example_gridPointProjection_002.png differ diff --git a/_images/sphx_glr_plot_4_example_gridPointProjection_003.png b/_images/sphx_glr_plot_4_example_gridPointProjection_003.png new file mode 100644 index 00000000..60afc80b Binary files /dev/null and b/_images/sphx_glr_plot_4_example_gridPointProjection_003.png differ diff --git a/_images/sphx_glr_plot_4_example_gridPointProjection_004.png b/_images/sphx_glr_plot_4_example_gridPointProjection_004.png new file mode 100644 index 00000000..de9a0d75 Binary files /dev/null and b/_images/sphx_glr_plot_4_example_gridPointProjection_004.png differ diff --git a/_images/sphx_glr_plot_4_example_gridPointProjection_005.png b/_images/sphx_glr_plot_4_example_gridPointProjection_005.png new file mode 100644 index 00000000..ef649bbc Binary files /dev/null and b/_images/sphx_glr_plot_4_example_gridPointProjection_005.png differ diff --git a/_images/sphx_glr_plot_4_example_gridPointProjection_006.png b/_images/sphx_glr_plot_4_example_gridPointProjection_006.png new file mode 100644 index 00000000..5b731f1d Binary files /dev/null and b/_images/sphx_glr_plot_4_example_gridPointProjection_006.png differ diff --git a/_images/sphx_glr_plot_4_example_gridPointProjection_thumb.png b/_images/sphx_glr_plot_4_example_gridPointProjection_thumb.png new file mode 100644 index 00000000..14101d8c Binary files /dev/null and b/_images/sphx_glr_plot_4_example_gridPointProjection_thumb.png differ diff --git a/_images/sphx_glr_plot_5_example_rmap_computing_thumb.png b/_images/sphx_glr_plot_5_example_rmap_computing_thumb.png new file mode 100644 index 00000000..94e361cc Binary files /dev/null and b/_images/sphx_glr_plot_5_example_rmap_computing_thumb.png differ diff --git a/_images/sphx_glr_plot_6_real_time_demo_thumb.png b/_images/sphx_glr_plot_6_real_time_demo_thumb.png new file mode 100644 index 00000000..8a5fed58 Binary files /dev/null and b/_images/sphx_glr_plot_6_real_time_demo_thumb.png differ diff --git a/_images/sphx_glr_plot_7_lsl_example_001.png b/_images/sphx_glr_plot_7_lsl_example_001.png new file mode 100644 index 00000000..b4d142c0 Binary files /dev/null and b/_images/sphx_glr_plot_7_lsl_example_001.png differ diff --git a/_images/sphx_glr_plot_7_lsl_example_002.png b/_images/sphx_glr_plot_7_lsl_example_002.png new file mode 100644 index 00000000..3774fc0c Binary files /dev/null and b/_images/sphx_glr_plot_7_lsl_example_002.png differ diff --git a/_images/sphx_glr_plot_7_lsl_example_thumb.png b/_images/sphx_glr_plot_7_lsl_example_thumb.png new file mode 100644 index 00000000..2589381e Binary files /dev/null and b/_images/sphx_glr_plot_7_lsl_example_thumb.png differ diff --git a/_images/sphx_glr_plot_8_cebra_example_thumb.png b/_images/sphx_glr_plot_8_cebra_example_thumb.png new file mode 100644 index 00000000..0055d36c Binary files /dev/null and b/_images/sphx_glr_plot_8_cebra_example_thumb.png differ diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 00000000..ee1685c7 --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,483 @@ + + + + + + + + + + Overview: module code — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + + + + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_IO.html b/_modules/nm_IO.html new file mode 100644 index 00000000..daaf582a --- /dev/null +++ b/_modules/nm_IO.html @@ -0,0 +1,888 @@ + + + + + + + + + + nm_IO — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_IO

+import json
+from pathlib import PurePath, Path
+from typing import TYPE_CHECKING
+
+import numpy as np
+import pandas as pd
+
+from py_neuromodulation.nm_types import _PathLike
+from py_neuromodulation import logger, PYNM_DIR
+
+if TYPE_CHECKING:
+    from py_neuromodulation.nm_settings import NMSettings
+    from mne_bids import BIDSPath
+    from mne import io as mne_io
+
+
+
+[docs] +def load_nm_channels( + nm_channels: pd.DataFrame | _PathLike, +) -> "pd.DataFrame": + """Read nm_channels from path or specify via BIDS arguments. + Nexessary parameters are then + ch_names (list), + ch_types (list), + bads (list) + used_types (list) + target_keywords (list) + reference Union[list, str] + """ + + if isinstance(nm_channels, pd.DataFrame): + nm_ch_return = nm_channels + elif nm_channels: + if not Path(nm_channels).is_file(): + raise ValueError( + "PATH_NM_CHANNELS is not a valid file. Got: " f"{nm_channels}" + ) + nm_ch_return = pd.read_csv(nm_channels) + + return nm_ch_return
+ + + +
+[docs] +def read_BIDS_data( + PATH_RUN: "_PathLike | BIDSPath", + line_noise: int = 50, +) -> tuple["mne_io.Raw", np.ndarray, float, int, list | None, list | None]: + """Given a run path and bids data path, read the respective data + + Parameters + ---------- + PATH_RUN : string + + Returns + ------- + raw_arr : mne.io.RawArray + raw_arr_data : np.ndarray + fs : int + line_noise : int + """ + + from mne_bids import read_raw_bids, get_bids_path_from_fname + + bids_path = get_bids_path_from_fname(PATH_RUN) + + raw_arr = read_raw_bids(bids_path) + coord_list, coord_names = get_coord_list(raw_arr) + if raw_arr.info["line_freq"] is not None: + line_noise = int(raw_arr.info["line_freq"]) + else: + logger.info( + f"Line noise is not available in the data, using value of {line_noise} Hz." + ) + return ( + raw_arr, + raw_arr.get_data(), + raw_arr.info["sfreq"], + line_noise, + coord_list, + coord_names, + )
+ + +
+[docs] +def read_mne_data( + PATH_RUN: "_PathLike | BIDSPath", + line_noise: int = 50, +): + """_summary_ + + Parameters + ---------- + PATH_RUN : _PathLike | BIDSPath + Path to mne.io.read_raw supported types https://mne.tools/stable/generated/mne.io.read_raw.html + line_noise : int, optional + line noise, by default 50 + + Returns + ------- + raw : mne.io.Raw + sfreq : float + ch_names : list[str] + ch_type : list[str] + bads : list[str] + """ + + from mne import io as mne_io + + raw_arr = mne_io.read_raw(PATH_RUN) + sfreq = raw_arr.info["sfreq"] + ch_names = raw_arr.info["ch_names"] + ch_types = raw_arr.get_channel_types() + logger.info( + f"Channel data is read using mne.io.read_raw function. Channel types might not be correct" + f" and set to 'eeg' by default" + ) + bads = raw_arr.info["bads"] + + if raw_arr.info["line_freq"] is not None: + line_noise = int(raw_arr.info["line_freq"]) + else: + logger.info( + f"Line noise is not available in the data, using value of {line_noise} Hz." + ) + + return raw_arr.get_data(), sfreq, ch_names, ch_types, bads
+ + +def get_coord_list( + raw: "mne_io.BaseRaw", +) -> tuple[list, list] | tuple[None, None]: + montage = raw.get_montage() + if montage is not None: + coord_list = np.array( + list(dict(montage.get_positions()["ch_pos"]).values()) + ).tolist() + coord_names = np.array( + list(dict(montage.get_positions()["ch_pos"]).keys()) + ).tolist() + else: + coord_list = None + coord_names = None + + return coord_list, coord_names + + +def read_grid(PATH_GRIDS: _PathLike | None, grid_str: str) -> pd.DataFrame: + if PATH_GRIDS is None: + grid = pd.read_csv(PYNM_DIR / ("grid_" + grid_str.lower() + ".tsv"), sep="\t") + else: + grid = pd.read_csv( + PurePath(PATH_GRIDS, "grid_" + grid_str.lower() + ".tsv"), sep="\t" + ) + return grid + + +def get_annotations(PATH_ANNOTATIONS: str, PATH_RUN: str, raw_arr: "mne_io.RawArray"): + filepath = PurePath(PATH_ANNOTATIONS, PurePath(PATH_RUN).name[:-5] + ".txt") + from mne import read_annotations + + try: + annot = read_annotations(filepath) + raw_arr.set_annotations(annot) + + # annotations starting with "BAD" are omitted with reject_by_annotations 'omit' param + annot_data = raw_arr.get_data(reject_by_annotation="omit") + except FileNotFoundError: + logger.critical(f"Annotations file could not be found: {filepath}") + + return annot, annot_data, raw_arr + + +
+[docs] +def read_plot_modules( + PATH_PLOT: _PathLike = PYNM_DIR / "plots", +): + """Read required .mat files for plotting + + Parameters + ---------- + PATH_PLOT : regexp, optional + path to plotting files, by default + """ + + faces = loadmat(PurePath(PATH_PLOT, "faces.mat")) + vertices = loadmat(PurePath(PATH_PLOT, "Vertices.mat")) + grid = loadmat(PurePath(PATH_PLOT, "grid.mat"))["grid"] + stn_surf = loadmat(PurePath(PATH_PLOT, "STN_surf.mat")) + x_ver = stn_surf["vertices"][::2, 0] + y_ver = stn_surf["vertices"][::2, 1] + x_ecog = vertices["Vertices"][::1, 0] + y_ecog = vertices["Vertices"][::1, 1] + z_ecog = vertices["Vertices"][::1, 2] + x_stn = stn_surf["vertices"][::1, 0] + y_stn = stn_surf["vertices"][::1, 1] + z_stn = stn_surf["vertices"][::1, 2] + + return ( + faces, + vertices, + grid, + stn_surf, + x_ver, + y_ver, + x_ecog, + y_ecog, + z_ecog, + x_stn, + y_stn, + z_stn, + )
+ + + +
+[docs] +def save_features_and_settings( + df_features, + run_analysis, + folder_name, + out_path, + settings: 'NMSettings', + nm_channels, + coords, + fs, + line_noise, +) -> None: + """save settings.json, nm_channels.csv and features.csv + + Parameters + ---------- + df_ : pd.Dataframe + feature dataframe + run_analysis_ : run_analysis.py object + This includes all (optionally projected) run_analysis estimated data + inluding added the resampled labels in features_arr + folder_name : string + output path + settings_wrapper : settings.py object + """ + + # create out folder if doesn't exist + if not Path(out_path, folder_name).exists(): + logger.info(f"Creating output folder: {folder_name}") + Path(out_path, folder_name).mkdir(parents=True) + + dict_sidecar = {"fs": fs, "coords": coords, "line_noise": line_noise} + + save_sidecar(dict_sidecar, out_path, folder_name) + save_features(df_features, out_path, folder_name) + settings.save(out_path, folder_name) + save_nm_channels(nm_channels, out_path, folder_name)
+ + + +
+[docs] +def write_csv(df, path_out): + """ + Function to save Pandas dataframes to disk as CSV using + PyArrow (almost 10x faster than Pandas) + Difference with pandas.df.to_csv() is that it does not + write an index column by default + """ + from pyarrow import csv, Table + + csv.write_csv(Table.from_pandas(df), path_out)
+ + +def save_nm_channels( + nmchannels: pd.DataFrame, + path_out: _PathLike, + folder_name: str = "", +) -> None: + if folder_name: + path_out = PurePath(path_out, folder_name, folder_name + "_nm_channels.csv") + write_csv(nmchannels, path_out) + logger.info(f"nm_channels.csv saved to {path_out}") + + +def save_features( + df_features: pd.DataFrame, + path_out: _PathLike, + folder_name: str = "", +) -> None: + if folder_name: + path_out = PurePath(path_out, folder_name, folder_name + "_FEATURES.csv") + write_csv(df_features, path_out) + logger.info(f"FEATURES.csv saved to {str(path_out)}") + + +def save_sidecar(sidecar: dict, path_out: _PathLike, folder_name: str = "") -> None: + save_general_dict(sidecar, path_out, "_SIDECAR.json", folder_name) + + +def save_general_dict( + dict_: dict, + path_out: _PathLike, + str_add: str = "", + folder_name: str = "", +) -> None: + if folder_name: + path_out = PurePath(path_out, folder_name, folder_name + str_add) + + with open(path_out, "w") as f: + json.dump( + dict_, + f, + default=default_json_convert, + indent=4, + separators=(",", ": "), + ) + logger.info(f"{str_add} saved to {path_out}") + + +def default_json_convert(obj) -> list | float: + if isinstance(obj, np.ndarray): + return obj.tolist() + if isinstance(obj, pd.DataFrame): + return obj.to_numpy().tolist() + if isinstance(obj, np.integer): + return int(obj) + if isinstance(obj, np.floating): + return float(obj) + raise TypeError("Not serializable") + + +def read_sidecar(PATH: _PathLike) -> dict: + with open(PurePath(str(PATH) + "_SIDECAR.json")) as f: + return json.load(f) + + +def read_features(PATH: _PathLike) -> pd.DataFrame: + return pd.read_csv(str(PATH) + "_FEATURES.csv", engine="pyarrow") + + +def read_nm_channels(PATH: _PathLike) -> pd.DataFrame: + return pd.read_csv(str(PATH) + "_nm_channels.csv") + + +def get_run_list_indir(PATH: _PathLike) -> list: + from os import walk + + f_files = [] + # for dirpath, _, files in Path(PATH).walk(): # Only works in python >=3.12 + for dirpath, _, files in walk(PATH): + for x in files: + if "FEATURES" in x: + f_files.append(PurePath(dirpath).name) + return f_files + + +
+[docs] +def loadmat(filename) -> dict: + """ + this function should be called instead of direct spio.loadmat + as it cures the problem of not properly recovering python dictionaries + from mat files. It calls the function check keys to cure all entries + which are still mat-objects + """ + from scipy.io import loadmat as sio_loadmat + + data = sio_loadmat(filename, struct_as_record=False, squeeze_me=True) + return _check_keys(data)
+ + + +
+[docs] +def get_paths_example_data(): + """ + This function should provide RUN_NAME, PATH_RUN, PATH_BIDS, PATH_OUT and datatype for the example + dataset used in most examples. + """ + + sub = "testsub" + ses = "EphysMedOff" + task = "gripforce" + run = 0 + datatype = "ieeg" + + # Define run name and access paths in the BIDS format. + RUN_NAME = f"sub-{sub}_ses-{ses}_task-{task}_run-{run}" + + PATH_BIDS = PYNM_DIR / "data" + + PATH_RUN = PYNM_DIR / "data" / f"sub-{sub}" / f"ses-{ses}" / datatype / RUN_NAME + + # Provide a path for the output data. + PATH_OUT = PATH_BIDS / "derivatives" + + return RUN_NAME, PATH_RUN, PATH_BIDS, PATH_OUT, datatype
+ + + +def _check_keys(dict): + """ + checks if entries in dictionary are mat-objects. If yes + todict is called to change them to nested dictionaries + """ + from scipy.io.matlab import mat_struct + + for key in dict: + if isinstance(dict[key], mat_struct): + dict[key] = _todict(dict[key]) + return dict + + +def _todict(matobj) -> dict: + """ + A recursive function which constructs from matobjects nested dictionaries + """ + from scipy.io.matlab import mat_struct + + dict = {} + for strg in matobj._fieldnames: + elem = matobj.__dict__[strg] + if isinstance(elem, mat_struct): + dict[strg] = _todict(elem) + else: + dict[strg] = elem + return dict +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_bursts.html b/_modules/nm_bursts.html new file mode 100644 index 00000000..bbf38d4c --- /dev/null +++ b/_modules/nm_bursts.html @@ -0,0 +1,765 @@ + + + + + + + + + + nm_bursts — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_bursts

+import numpy as np
+from numpy.lib._function_base_impl import _quantile as np_quantile  # type:ignore
+from collections.abc import Sequence
+from itertools import product
+
+from pydantic import Field
+from py_neuromodulation.nm_types import BoolSelector, NMBaseModel
+from py_neuromodulation.nm_features import NMFeature
+
+from typing import TYPE_CHECKING, Callable
+
+if TYPE_CHECKING:
+    from py_neuromodulation.nm_settings import NMSettings
+
+
+LARGE_NUM = 2**24
+
+
+
+[docs] +def get_label_pos(burst_labels, valid_labels): + """_summary_ + + Args: + burst_labels (_type_): _description_ + valid_labels (_type_): _description_ + + Returns: + _type_: _description_ + """ + max_label = np.max(burst_labels, axis=2).flatten() + min_label = np.min( + burst_labels, axis=2, initial=LARGE_NUM, where=burst_labels != 0 + ).flatten() + label_positions = np.zeros_like(valid_labels) + N = len(valid_labels) + pos = 0 + i = 0 + while i < N: + if valid_labels[i] >= min_label[pos] and valid_labels[i] <= max_label[pos]: + label_positions[i] = pos + i += 1 + else: + pos += 1 + return label_positions
+ + + +
+[docs] +class BurstFeatures(BoolSelector): + duration: bool = True + amplitude: bool = True + burst_rate_per_s: bool = True + in_burst: bool = True
+ + + +
+[docs] +class BurstSettings(NMBaseModel): + threshold: float = Field(default=75, ge=0, le=100) + time_duration_s: float = Field(default=30, ge=0) + frequency_bands: list[str] = ["low beta", "high beta", "low gamma"] + burst_features: BurstFeatures = BurstFeatures()
+ + + +
+[docs] +class Burst(NMFeature): + def __init__( + self, settings: "NMSettings", ch_names: Sequence[str], sfreq: float + ) -> None: + # Test settings + for fband_burst in settings.burst_settings.frequency_bands: + assert ( + fband_burst in list(settings.frequency_ranges_hz.keys()) + ), f"bursting {fband_burst} needs to be defined in settings['frequency_ranges_hz']" + + from py_neuromodulation.nm_filter import MNEFilter + + self.settings = settings.burst_settings + self.sfreq = sfreq + self.ch_names = ch_names + self.segment_length_features_s = settings.segment_length_features_ms / 1000 + self.samples_overlap = int( + self.sfreq + * self.segment_length_features_s + / settings.sampling_rate_features_hz + ) + + self.fband_names = settings.burst_settings.frequency_bands + + f_ranges: list[tuple[float, float]] = [ + ( + settings.frequency_ranges_hz[fband_name][0], + settings.frequency_ranges_hz[fband_name][1], + ) + for fband_name in self.fband_names + ] + + self.bandpass_filter = MNEFilter( + f_ranges=f_ranges, + sfreq=self.sfreq, + filter_length=self.sfreq - 1, + verbose=False, + ) + self.filter_data = self.bandpass_filter.filter_data + + self.num_max_samples_ring_buffer = int( + self.sfreq * self.settings.time_duration_s + ) + + self.n_channels = len(self.ch_names) + self.n_fbands = len(self.fband_names) + + # Create circular buffer array for previous time_duration_s + self.data_buffer = np.empty( + (self.n_channels, self.n_fbands, 0), dtype=np.float64 + ) + + self.used_features = self.settings.burst_features.get_enabled() + + self.feature_combinations = list( + product( + enumerate(self.ch_names), + enumerate(self.fband_names), + self.settings.burst_features.get_enabled(), + ) + ) + + # Variables to store results + self.burst_duration_mean: np.ndarray + self.burst_duration_max: np.ndarray + self.burst_amplitude_max: np.ndarray + self.burst_amplitude_mean: np.ndarray + self.burst_rate_per_s: np.ndarray + self.end_in_burst: np.ndarray + + self.STORE_FEAT_DICT: dict[str, Callable] = { + "duration": self.store_duration, + "amplitude": self.store_amplitude, + "burst_rate_per_s": self.store_burst_rate, + "in_burst": self.store_in_burst, + } + + self.batch = 0 + + # Structure matrix for np.ndimage.label + # pixels are connected only to adjacent neighbors along the last axis + self.label_structure_matrix = np.zeros((3, 3, 3)) + self.label_structure_matrix[1, 1, :] = 1 + +
+[docs] + def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict: + from scipy.signal import hilbert + from scipy.ndimage import label, sum_labels as label_sum, mean as label_mean + + filtered_data = np.abs(np.array(hilbert(self.filter_data(data)))) + + # Update buffer array + batch_size = ( + filtered_data.shape[-1] if self.batch == 0 else self.samples_overlap + ) + self.batch += 1 + self.data_buffer = np.concatenate( + ( + self.data_buffer, + filtered_data[:, :, -batch_size:], + ), + axis=2, + )[:, :, -self.num_max_samples_ring_buffer :] + + # Burst threshold is calculated with the percentile defined in the settings + # Call low-level numpy function directly, extra checks not needed + burst_thr = np_quantile(self.data_buffer, self.settings.threshold / 100)[ + :, :, None + ] # Add back the extra dimension + + # Get burst locations as a boolean array, True where data is above threshold (i.e. a burst) + bursts = filtered_data >= burst_thr + + # Use np.diff to find the places where bursts start and end + # Prepend False at the beginning ensures that data never starts on a burst + # Floor division to ignore last burst if series ends in a burst (true burst length unknown) + num_bursts = ( + np.sum(np.diff(bursts, axis=2, prepend=False), axis=2) // 2 + ).astype(np.float64) # np.astype added to avoid casting error in np.divide + + # Label each burst with a unique id, limiting connectivity to last axis (see scipy.ndimage.label docs for details) + burst_labels = label(bursts, self.label_structure_matrix)[0] # type: ignore # wrong return type in scipy + + # Remove labels of bursts that are at the end of the dataset, and 0 + labels_at_end = np.concatenate((np.unique(burst_labels[:, :, -1]), (0,))) + valid_labels = np.unique(burst_labels) + valid_labels = valid_labels[ + ~np.isin(valid_labels, labels_at_end, assume_unique=True) + ] + + # Find (channel, band) coordinates for each valid label and get an array that maps each valid label to its channel/band + # Channel band coordinate is flattened to a 1D array of length (n_channels x n_fbands) + label_positions = get_label_pos(burst_labels, valid_labels) + + # Now we're ready to calculate features + + + if "duration" in self.used_features or "burst_rate_per_s" in self.used_features: + # Handle division by zero using np.divide. Where num_bursts is 0, the result is 0 + self.burst_duration_mean = ( + np.divide( + np.sum(bursts, axis=2), + num_bursts, + out=np.zeros_like(num_bursts), + where=num_bursts != 0, + ) + / self.sfreq + ) + + if "duration" in self.used_features: + # First get burst length for each valid burst + burst_lengths = label_sum(bursts, burst_labels, index=valid_labels) / self.sfreq + + # Now the max needs to be calculated per channel/band + # For that, loop over channels/bands, get the corresponding burst lengths, and get the max + # Give parameter initial=0 so that when there are no bursts, the max is 0 + # TODO: it might be interesting to write a C function for this + duration_max_flat = np.zeros(self.n_channels * self.n_fbands) + for idx in range(self.n_channels * self.n_fbands): + duration_max_flat[idx] = np.max( + burst_lengths[label_positions == idx], initial=0 + ) + + self.burst_duration_max = duration_max_flat.reshape( + (self.n_channels, self.n_fbands) + ) + + if "amplitude" in self.used_features: + # Max amplitude is just the max of the filtered data where there is a burst + self.burst_amplitude_max = (filtered_data * bursts).max(axis=2) + + # The mean is actually a mean of means, so we need the mean for each individual burst + label_means = label_mean(filtered_data, burst_labels, index=valid_labels) + # Now, loop over channels/bands, get the corresponding burst means, and calculate the mean of means + # TODO: it might be interesting to write a C function for this + amplitude_mean_flat = np.zeros(self.n_channels * self.n_fbands) + for idx in range(self.n_channels * self.n_fbands): + mask = label_positions == idx + amplitude_mean_flat[idx] = ( + np.mean(label_means[mask]) if np.any(mask) else 0 + ) + + self.burst_amplitude_mean = amplitude_mean_flat.reshape( + (self.n_channels, self.n_fbands) + ) + + if "burst_rate_per_s" in self.used_features: + self.burst_rate_per_s = ( + self.burst_duration_mean / self.segment_length_features_s + ) + + if "in_burst" in self.used_features: + self.end_in_burst = bursts[:, :, -1] # End in burst + + # Create dictionary of features which is the correct return format + for (ch_i, ch), (fb_i, fb), feat in self.feature_combinations: + self.STORE_FEAT_DICT[feat](features_compute, ch_i, ch, fb_i, fb) + + return features_compute
+ + + def store_duration( + self, features_compute: dict, ch_i: int, ch: str, fb_i: int, fb: str + ): + features_compute[f"{ch}_bursts_{fb}_duration_mean"] = self.burst_duration_mean[ + ch_i, fb_i + ] + + features_compute[f"{ch}_bursts_{fb}_duration_max"] = self.burst_duration_max[ + ch_i, fb_i + ] + + def store_amplitude( + self, features_compute: dict, ch_i: int, ch: str, fb_i: int, fb: str + ): + features_compute[f"{ch}_bursts_{fb}_amplitude_mean"] = ( + self.burst_amplitude_mean[ch_i, fb_i] + ) + features_compute[f"{ch}_bursts_{fb}_amplitude_max"] = self.burst_amplitude_max[ + ch_i, fb_i + ] + + def store_burst_rate( + self, features_compute: dict, ch_i: int, ch: str, fb_i: int, fb: str + ): + features_compute[f"{ch}_bursts_{fb}_burst_rate_per_s"] = self.burst_rate_per_s[ + ch_i, fb_i + ] + + def store_in_burst( + self, features_compute: dict, ch_i: int, ch: str, fb_i: int, fb: str + ): + features_compute[f"{ch}_bursts_{fb}_in_burst"] = self.end_in_burst[ch_i, fb_i]
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_coherence.html b/_modules/nm_coherence.html new file mode 100644 index 00000000..99b08d30 --- /dev/null +++ b/_modules/nm_coherence.html @@ -0,0 +1,702 @@ + + + + + + + + + + nm_coherence — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_coherence

+import numpy as np
+from collections.abc import Iterable
+
+from py_neuromodulation.nm_types import FrequencyRange, NMBaseModel, Field
+from typing import TYPE_CHECKING
+
+from py_neuromodulation.nm_features import NMFeature
+from py_neuromodulation.nm_types import BoolSelector
+from py_neuromodulation import logger
+
+if TYPE_CHECKING:
+    from py_neuromodulation.nm_settings import NMSettings
+
+
+
+[docs] +class CoherenceMethods(BoolSelector): + coh: bool = True + icoh: bool = True
+ + + +
+[docs] +class CoherenceFeatures(BoolSelector): + mean_fband: bool = True + max_fband: bool = True + max_allfbands: bool = True
+ + + +
+[docs] +class CoherenceSettings(NMBaseModel): + features: CoherenceFeatures = CoherenceFeatures() + method: CoherenceMethods = CoherenceMethods() + channels: list[tuple[str, str]] = [("STN_RIGHT_0", "ECOG_RIGHT_0")] + frequency_bands: list[str] = Field(default=["high beta"], min_length=1)
+ + + +class CoherenceObject: + def __init__( + self, + sfreq: float, + window: str, + fbands: list[FrequencyRange], + fband_names: list[str], + ch_1_name: str, + ch_2_name: str, + ch_1_idx: int, + ch_2_idx: int, + coh: bool, + icoh: bool, + features_coh: CoherenceFeatures, + ) -> None: + self.sfreq = sfreq + self.window = window + self.fbands = fbands + self.fband_names = fband_names + self.ch_1 = ch_1_name + self.ch_2 = ch_2_name + self.ch_1_idx = ch_1_idx + self.ch_2_idx = ch_2_idx + self.coh = coh + self.icoh = icoh + self.features_coh = features_coh + + self.Pxx = None + self.Pyy = None + self.Pxy = None + self.f = None + self.coh_val = None + self.icoh_val = None + + def get_coh(self, features_compute, x, y): + from scipy.signal import welch, csd + + self.f, self.Pxx = welch(x, self.sfreq, self.window, nperseg=128) + self.Pyy = welch(y, self.sfreq, self.window, nperseg=128)[1] + self.Pxy = csd(x, y, self.sfreq, self.window, nperseg=128)[1] + + if self.coh: + self.coh_val = np.abs(self.Pxy**2) / (self.Pxx * self.Pyy) + if self.icoh: + self.icoh_val = np.array(self.Pxy / (self.Pxx * self.Pyy)).imag + + for coh_idx, coh_type in enumerate([self.coh, self.icoh]): + if coh_type: + if coh_idx == 0: + coh_val = self.coh_val + coh_name = "coh" + else: + coh_val = self.icoh_val + coh_name = "icoh" + + for idx, fband in enumerate(self.fbands): + if self.features_coh.mean_fband: + feature_calc = np.mean( + coh_val[np.bitwise_and(self.f > fband[0], self.f < fband[1])] + ) + feature_name = "_".join( + [ + coh_name, + self.ch_1, + "to", + self.ch_2, + "mean_fband", + self.fband_names[idx], + ] + ) + features_compute[feature_name] = feature_calc + if self.features_coh.max_fband: + feature_calc = np.max( + coh_val[np.bitwise_and(self.f > fband[0], self.f < fband[1])] + ) + feature_name = "_".join( + [ + coh_name, + self.ch_1, + "to", + self.ch_2, + "max_fband", + self.fband_names[idx], + ] + ) + features_compute[feature_name] = feature_calc + if self.features_coh.max_allfbands: + feature_calc = self.f[np.argmax(coh_val)] + feature_name = "_".join( + [ + coh_name, + self.ch_1, + "to", + self.ch_2, + "max_allfbands", + self.fband_names[idx], + ] + ) + features_compute[feature_name] = feature_calc + return features_compute + + +
+[docs] +class NMCoherence(NMFeature): + def __init__( + self, settings: "NMSettings", ch_names: list[str], sfreq: float + ) -> None: + self.settings = settings.coherence + self.frequency_ranges_hz = settings.frequency_ranges_hz + self.sfreq = sfreq + self.ch_names = ch_names + self.coherence_objects: Iterable[CoherenceObject] = [] + + self.test_settings(settings, ch_names, sfreq) + + for idx_coh in range(len(self.settings.channels)): + fband_names = self.settings.frequency_bands + fband_specs = [] + for band_name in fband_names: + fband_specs.append(self.frequency_ranges_hz[band_name]) + + ch_1_name = self.settings.channels[idx_coh][0] + ch_1_name_reref = [ch for ch in self.ch_names if ch.startswith(ch_1_name)][ + 0 + ] + ch_1_idx = self.ch_names.index(ch_1_name_reref) + + ch_2_name = self.settings.channels[idx_coh][1] + ch_2_name_reref = [ch for ch in self.ch_names if ch.startswith(ch_2_name)][ + 0 + ] + ch_2_idx = self.ch_names.index(ch_2_name_reref) + + self.coherence_objects.append( + CoherenceObject( + sfreq, + "hann", + fband_specs, + fband_names, + ch_1_name, + ch_2_name, + ch_1_idx, + ch_2_idx, + self.settings.method.coh, + self.settings.method.icoh, + self.settings.features, + ) + ) + + @staticmethod + def test_settings( + settings: "NMSettings", + ch_names: Iterable[str], + sfreq: float, + ): + flat_channels = [ + ch for ch_pair in settings.coherence.channels for ch in ch_pair + ] + assert all(ch_coh in ch_names for ch_coh in flat_channels), ( + f"coherence selected channels don't match the ones in nm_channels. \n" + f"ch_names: {ch_names} \n settings.coherence.channels: {settings.coherence.channels}" + ) + + assert all( + f_band_coh in settings.frequency_ranges_hz + for f_band_coh in settings.coherence.frequency_bands + ), ( + "coherence selected frequency bands don't match the ones" + "specified in s['frequency_ranges_hz']" + f"coherence frequency bands: {settings.coherence.frequency_bands}" + f"specified frequency_ranges_hz: {settings.frequency_ranges_hz}" + ) + + assert all( + settings.frequency_ranges_hz[fb][0] < sfreq / 2 + and settings.frequency_ranges_hz[fb][1] < sfreq / 2 + for fb in settings.coherence.frequency_bands + ), ( + "the coherence frequency band ranges need to be smaller than the Nyquist frequency" + f"got sfreq = {sfreq} and fband ranges {settings.coherence.frequency_bands}" + ) + + if not settings.coherence.method.get_enabled(): + logger.warn( + "feature coherence enabled, but no coherence['method'] selected" + ) + +
+[docs] + def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict: + for coh_obj in self.coherence_objects: + features_compute = coh_obj.get_coh( + features_compute, + data[coh_obj.ch_1_idx, :], + data[coh_obj.ch_2_idx, :], + ) + + return features_compute
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_decode.html b/_modules/nm_decode.html new file mode 100644 index 00000000..5d43866d --- /dev/null +++ b/_modules/nm_decode.html @@ -0,0 +1,1417 @@ + + + + + + + + + + nm_decode — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_decode

+from sklearn import model_selection
+from sklearn.linear_model import LinearRegression
+from sklearn.base import clone
+from sklearn.metrics import r2_score
+
+import pandas as pd
+import numpy as np
+from copy import deepcopy
+from pathlib import PurePath
+import pickle
+
+from py_neuromodulation import logger
+
+from typing import Callable
+
+
+class CV_res:
+    def __init__(
+        self,
+        get_movement_detection_rate: bool = False,
+        RUN_BAY_OPT: bool = False,
+        mrmr_select: bool = False,
+        model_save: bool = False,
+    ) -> None:
+        self.score_train: list = []
+        self.score_test: list = []
+        self.y_test: list = []
+        self.y_train: list = []
+        self.y_test_pr: list = []
+        self.y_train_pr: list = []
+        self.X_test: list = []
+        self.X_train: list = []
+        self.coef: list = []
+
+        if get_movement_detection_rate:
+            self.mov_detection_rates_test: list = []
+            self.tprate_test: list = []
+            self.fprate_test: list = []
+            self.mov_detection_rates_train: list = []
+            self.tprate_train: list = []
+            self.fprate_train: list = []
+        if RUN_BAY_OPT:
+            self.best_bay_opt_params: list = []
+        if mrmr_select:
+            self.mrmr_select: list = []
+        if model_save:
+            self.model_save: list = []
+
+
+
+[docs] +class Decoder: +
+[docs] + class ClassMissingException(Exception): + def __init__( + self, + message="Only one class present.", + ) -> None: + self.message = message + super().__init__(self.message) + + def __str__(self): + return self.message
+ + + def __init__( + self, + features: "pd.DataFrame| None " = None, + label: np.ndarray | None = None, + label_name: str | None = None, + used_chs: list[str] = [], + model = LinearRegression(), + eval_method: Callable = r2_score, + cv_method = model_selection.KFold(n_splits=3, shuffle=False), + use_nested_cv: bool = False, + threshold_score = True, + mov_detection_threshold: float = 0.5, + TRAIN_VAL_SPLIT: bool = False, + RUN_BAY_OPT: bool = False, + STACK_FEATURES_N_SAMPLES: bool = False, + time_stack_n_samples: int = 5, + save_coef: bool = False, + get_movement_detection_rate: bool = False, + min_consequent_count: int = 3, + bay_opt_param_space: list = [], + VERBOSE: bool = False, + sfreq: int | None = None, + undersampling: bool = False, + oversampling: bool = False, + mrmr_select: bool = False, + pca: bool = False, + cca: bool = False, + model_save: bool = False, + ) -> None: + """Initialize here a feature file for processing + Read settings.json nm_channels.csv and features.csv + Read target label + + Parameters + ---------- + model : machine learning model + model that utilizes fit and predict functions + eval_method : sklearn metrics + evaluation scoring method, will default to r2_score if not passed + cv_method : sklearm model_selection method + threshold_score : boolean + if True set lower threshold at zero (useful for r2), + mov_detection_threshold : float + if get_movement_detection_rate is True, find given minimum 'threshold' respective + consecutive movement blocks, by default 0.5 + TRAIN_VAL_SPLIT (boolean): + if true split data into additinal validation, and run class weighted CV + save_coef (boolean): + if true, save model._coef trained coefficients + get_movement_detection_rate (boolean): + save detection rate and tpr / fpr as well + min_consequent_count (int): + if get_movement_detection_rate is True, find given 'min_consequent_count' respective + consecutive movement blocks with minimum size of 'min_consequent_count' + """ + + self.model = model + self.eval_method = eval_method + self.cv_method = cv_method + self.use_nested_cv = use_nested_cv + self.threshold_score = threshold_score + self.mov_detection_threshold = mov_detection_threshold + self.TRAIN_VAL_SPLIT = TRAIN_VAL_SPLIT + self.RUN_BAY_OPT = RUN_BAY_OPT + self.save_coef = save_coef + self.sfreq = sfreq + self.get_movement_detection_rate = get_movement_detection_rate + self.min_consequent_count = min_consequent_count + self.STACK_FEATURES_N_SAMPLES = STACK_FEATURES_N_SAMPLES + self.time_stack_n_samples = time_stack_n_samples + self.bay_opt_param_space = bay_opt_param_space + self.VERBOSE = VERBOSE + self.undersampling = undersampling + self.oversampling = oversampling + self.mrmr_select = mrmr_select + self.used_chs = used_chs + self.label = label + self.label_name = label_name + self.cca = cca + self.pca = pca + self.model_save = model_save + + self.set_data(features) + + self.ch_ind_data = {} + self.grid_point_ind_data = {} + self.active_gridpoints = [] + self.feature_names = [] + self.ch_ind_results = {} + self.gridpoint_ind_results = {} + self.all_ch_results = {} + self.columns_names_single_ch = None + + if undersampling: + from imblearn.under_sampling import RandomUnderSampler + + self.rus = RandomUnderSampler(random_state=0) + + if oversampling: + from imblearn.over_sampling import RandomOverSampler + + self.ros = RandomOverSampler(random_state=0) + + def set_data(self, features): + if features is not None: + self.features = features + self.feature_names = [ + col + for col in self.features.columns + if not (("time" in col) or (self.label_name in col)) + ] + self.data = np.nan_to_num(np.array(self.features[self.feature_names])) + +
+[docs] + def set_data_ind_channels(self): + """specified channel individual data""" + self.ch_ind_data = {} + for ch in self.used_chs: + self.ch_ind_data[ch] = np.nan_to_num( + np.array( + self.features[ + [col for col in self.features.columns if col.startswith(ch)] + ] + ) + )
+ + +
+[docs] + def set_CV_results(self, attr_name, contact_point=None): + """set CV results in respectie nm_decode attributes + The reference is first stored in obj_set, and the used lateron + + Parameters + ---------- + attr_name : string + is either all_ch_results, ch_ind_results, gridpoint_ind_results + contact_point : object, optional + usually an int specifying the grid_point or string, specifying the used channel, + by default None + """ + if contact_point is not None: + getattr(self, attr_name)[contact_point] = {} + obj_set = getattr(self, attr_name)[contact_point] + else: + obj_set = getattr(self, attr_name) + + def set_scores(cv_res: CV_res, set_inner_CV_res: bool = False): + """ + This function renames the CV_res keys for InnerCV + """ + + def set_score(key_: str, val): + if set_inner_CV_res: + key_ = "InnerCV_" + key_ + obj_set[key_] = val + + set_score("score_train", cv_res.score_train) + set_score("score_test", cv_res.score_test) + set_score("y_test", cv_res.y_test) + set_score("y_train", cv_res.y_train) + set_score("y_test_pr", cv_res.y_test_pr) + set_score("y_train_pr", cv_res.y_train_pr) + set_score("X_train", cv_res.X_train) + set_score("X_test", cv_res.X_test) + + if self.save_coef: + set_score("coef", cv_res.coef) + if self.get_movement_detection_rate: + set_score("mov_detection_rates_test", cv_res.mov_detection_rates_test) + set_score( + "mov_detection_rates_train", + cv_res.mov_detection_rates_train, + ) + set_score("fprate_test", cv_res.fprate_test) + set_score("fprate_train", cv_res.fprate_train) + set_score("tprate_test", cv_res.tprate_test) + set_score("tprate_train", cv_res.tprate_train) + + if self.RUN_BAY_OPT: + set_score("best_bay_opt_params", cv_res.best_bay_opt_params) + + if self.mrmr_select: + set_score("mrmr_select", cv_res.mrmr_select) + if self.model_save: + set_score("model_save", cv_res.model_save) + return obj_set + + obj_set = set_scores(self.cv_res) + + if self.use_nested_cv: + obj_set = set_scores(self.cv_res_inner, set_inner_CV_res=True)
+ + +
+[docs] + def run_CV_caller(self, feature_contacts: str = "ind_channels"): + """Wrapper that call for all channels / grid points / combined channels the CV function + + Parameters + ---------- + feature_contacts : str, optional + "grid_points", "ind_channels" or "all_channels_combined" , by default "ind_channels" + """ + valid_feature_contacts = [ + "ind_channels", + "all_channels_combined", + "grid_points", + ] + if feature_contacts not in valid_feature_contacts: + raise ValueError(f"{feature_contacts} not in {valid_feature_contacts}") + + if feature_contacts == "grid_points": + for grid_point in self.active_gridpoints: + self.run_CV(self.grid_point_ind_data[grid_point], self.label) + self.set_CV_results("gridpoint_ind_results", contact_point=grid_point) + return self.gridpoint_ind_results + + if feature_contacts == "ind_channels": + for ch in self.used_chs: + self.ch_name_tested = ch + self.run_CV(self.ch_ind_data[ch], self.label) + self.set_CV_results("ch_ind_results", contact_point=ch) + return self.ch_ind_results + + if feature_contacts == "all_channels_combined": + dat_combined = np.array(self.data) + self.run_CV(dat_combined, self.label) + self.set_CV_results("all_ch_results", contact_point=None) + return self.all_ch_results
+ + +
+[docs] + def set_data_grid_points(self, cortex_only=False, subcortex_only=False): + """Read the run_analysis + Projected data has the shape (samples, grid points, features) + """ + + # activate_gridpoints stores cortex + subcortex data + self.active_gridpoints = np.unique( + [ + i.split("_")[0] + "_" + i.split("_")[1] + for i in self.features.columns + if "grid" in i + ] + ) + + if cortex_only: + self.active_gridpoints = [ + i for i in self.active_gridpoints if i.startswith("gridcortex") + ] + + if subcortex_only: + self.active_gridpoints = [ + i for i in self.active_gridpoints if i.startswith("gridsubcortex") + ] + + self.feature_names = [ + i[len(self.active_gridpoints[0] + "_") :] + for i in self.features.columns + if self.active_gridpoints[0] + "_" in i + ] + + self.grid_point_ind_data = {} + + self.grid_point_ind_data = { + grid_point: np.nan_to_num( + self.features[ + [i for i in self.features.columns if grid_point + "_" in i] + ] + ) + for grid_point in self.active_gridpoints + }
+ + +
+[docs] + def get_movement_grouped_array( + self, prediction, threshold=0.5, min_consequent_count=5 + ): + """Return given a 1D numpy array, an array of same size with grouped consective blocks + + Parameters + ---------- + prediction : np.ndarray + numpy array of either predictions or labels, that is going to be grouped + threshold : float, optional + threshold to be applied to 'prediction', by default 0.5 + min_consequent_count : int, optional + minimum required consective samples higher than 'threshold', by default 5 + + Returns + ------- + labeled_array : np.ndarray + grouped vector with incrementing number for movement blocks + labels_count : int + count of individual movement blocks + """ + + from scipy.ndimage import label as label_ndimage + from scipy.ndimage import binary_dilation, binary_erosion + + mask = prediction > threshold + structure = [True] * min_consequent_count # used for erosion and dilation + eroded = binary_erosion(mask, structure) + dilated = binary_dilation(eroded, structure) + labeled_array, labels_count = label_ndimage(dilated) + return labeled_array, labels_count
+ + +
+[docs] + def calc_movement_detection_rate( + self, y_label, prediction, threshold=0.5, min_consequent_count=3 + ): + """Given a label and prediction, return the movement detection rate on the basis of + movements classified in blocks of 'min_consequent_count'. + + Parameters + ---------- + y_label : [type] + [description] + prediction : [type] + [description] + threshold : float, optional + threshold to be applied to 'prediction', by default 0.5 + min_consequent_count : int, optional + minimum required consective samples higher than 'threshold', by default 3 + + Returns + ------- + mov_detection_rate : float + movement detection rate, where at least 'min_consequent_count' samples where high in prediction + fpr : np.ndarray + sklearn.metrics false positive rate np.ndarray + tpr : np.ndarray + sklearn.metrics true positive rate np.ndarray + """ + from sklearn.metrics import confusion_matrix + + pred_grouped, _ = self.get_movement_grouped_array( + prediction, threshold, min_consequent_count + ) + y_grouped, labels_count = self.get_movement_grouped_array( + y_label, threshold, min_consequent_count + ) + + hit_rate = np.zeros(labels_count) + pred_group_bin = np.array(pred_grouped > 0) + + for label_number in range(1, labels_count + 1): # labeling starts from 1 + hit_rate[label_number - 1] = np.sum( + pred_group_bin[np.where(y_grouped == label_number)[0]] + ) + + try: + mov_detection_rate = np.where(hit_rate > 0)[0].shape[0] / labels_count + except ZeroDivisionError: + logger.warning("no movements in label") + return 0, 0, 0 + + # calculating TPR and FPR: https://stackoverflow.com/a/40324184/5060208 + CM = confusion_matrix(y_label, prediction) + + TN = CM[0][0] + FN = CM[1][0] + TP = CM[1][1] + FP = CM[0][1] + fpr = FP / (FP + TN) + tpr = TP / (TP + FN) + + return mov_detection_rate, fpr, tpr
+ + + def init_cv_res(self) -> None: + return CV_res( + get_movement_detection_rate=self.get_movement_detection_rate, + RUN_BAY_OPT=self.RUN_BAY_OPT, + mrmr_select=self.mrmr_select, + model_save=self.model_save, + ) + + # @staticmethod + # @jit(nopython=True) +
+[docs] + def append_previous_n_samples(X: np.ndarray, y: np.ndarray, n: int = 5): + """ + stack feature vector for n samples + """ + TIME_DIM = X.shape[0] - n + FEATURE_DIM = int(n * X.shape[1]) + time_arr = np.empty((TIME_DIM, FEATURE_DIM)) + for time_idx, time_ in enumerate(np.arange(n, X.shape[0])): + for time_point in range(n): + time_arr[ + time_idx, + time_point * X.shape[1] : (time_point + 1) * X.shape[1], + ] = X[time_ - time_point, :] + return time_arr, y[n:]
+ + + @staticmethod + def append_samples_val(X_train, y_train, X_val, y_val, n): + X_train, y_train = Decoder.append_previous_n_samples(X_train, y_train, n=n) + X_val, y_val = Decoder.append_previous_n_samples(X_val, y_val, n=n) + return X_train, y_train, X_val, y_val + + def fit_model(self, model, X_train, y_train): + if self.TRAIN_VAL_SPLIT: + X_train, X_val, y_train, y_val = model_selection.train_test_split( + X_train, y_train, train_size=0.7, shuffle=False + ) + + if y_train.sum() == 0 or y_val.sum(0) == 0: + raise Decoder.ClassMissingException + + # if type(model) is xgboost.sklearn.XGBClassifier: + # classes_weights = class_weight.compute_sample_weight( + # class_weight="balanced", y=y_train + # ) + # model.set_params(eval_metric="logloss") + # model.fit( + # X_train, + # y_train, + # eval_set=[(X_val, y_val)], + # early_stopping_rounds=7, + # sample_weight=classes_weights, + # verbose=self.VERBOSE, + # ) + # elif type(model) is xgboost.sklearn.XGBRegressor: + # # might be necessary to adapt for other classifiers + # + # def evalerror(preds, dtrain): + # labels = dtrain.get_label() + # # return a pair metric_name, result. The metric name must not contain a + # # colon (:) or a space since preds are margin(before logistic + # # transformation, cutoff at 0) + # + # r2 = metrics.r2_score(labels, preds) + # + # if r2 < 0: + # r2 = 0 + # + # return "r2", -r2 + # + # model.set_params(eval_metric=evalerror) + # model.fit( + # X_train, + # y_train, + # eval_set=[(X_val, y_val)], + # early_stopping_rounds=10, + # verbose=self.VERBOSE, + # ) + # else: + # model.fit(X_train, y_train, eval_set=[(X_val, y_val)]) + else: + # check for LDA; and apply rebalancing + if self.oversampling: + X_train, y_train = self.ros.fit_resample(X_train, y_train) + if self.undersampling: + X_train, y_train = self.rus.fit_resample(X_train, y_train) + + # if type(model) is xgboost.sklearn.XGBClassifier: + # model.set_params(eval_metric="logloss") + # model.fit(X_train, y_train) + # else: + model.fit(X_train, y_train) + + return model + + def eval_model( + self, + model_train, + X_train, + X_test, + y_train, + y_test, + cv_res: CV_res, + save_data=True, + save_probabilities=False, + ) -> CV_res: + if self.save_coef: + cv_res.coef.append(model_train.coef_) + + y_test_pr = model_train.predict(X_test) + y_train_pr = model_train.predict(X_train) + + sc_te = self.eval_method(y_test, y_test_pr) + sc_tr = self.eval_method(y_train, y_train_pr) + + if self.threshold_score: + if sc_tr < 0: + sc_tr = 0 + if sc_te < 0: + sc_te = 0 + + if self.get_movement_detection_rate: + self._set_movement_detection_rates( + y_test, y_test_pr, y_train, y_train_pr, cv_res + ) + + cv_res.score_train.append(sc_tr) + cv_res.score_test.append(sc_te) + if save_data: + cv_res.X_train.append(X_train) + cv_res.X_test.append(X_test) + if self.model_save: + cv_res.model_save.append(deepcopy(model_train)) # clone won't copy params + cv_res.y_train.append(y_train) + cv_res.y_test.append(y_test) + + if not save_probabilities: + cv_res.y_train_pr.append(y_train_pr) + cv_res.y_test_pr.append(y_test_pr) + else: + cv_res.y_train_pr.append(model_train.predict_proba(X_train)) + cv_res.y_test_pr.append(model_train.predict_proba(X_test)) + return cv_res + + def _set_movement_detection_rates( + self, + y_test: np.ndarray, + y_test_pr: np.ndarray, + y_train: np.ndarray, + y_train_pr: np.ndarray, + cv_res: CV_res, + ) -> CV_res: + mov_detection_rate, fpr, tpr = self.calc_movement_detection_rate( + y_test, + y_test_pr, + self.mov_detection_threshold, + self.min_consequent_count, + ) + + cv_res.mov_detection_rates_test.append(mov_detection_rate) + cv_res.tprate_test.append(tpr) + cv_res.fprate_test.append(fpr) + + mov_detection_rate, fpr, tpr = self.calc_movement_detection_rate( + y_train, + y_train_pr, + self.mov_detection_threshold, + self.min_consequent_count, + ) + + cv_res.mov_detection_rates_train.append(mov_detection_rate) + cv_res.tprate_train.append(tpr) + cv_res.fprate_train.append(fpr) + + return cv_res + + def wrapper_model_train( + self, + X_train, + y_train, + X_test=None, + y_test=None, + cv_res: CV_res | None = None, + return_fitted_model_only: bool = False, + save_data=True, + ): + if cv_res is None: + cv_res = CV_res( + get_movement_detection_rate=self.get_movement_detection_rate, + RUN_BAY_OPT=self.RUN_BAY_OPT, + mrmr_select=self.mrmr_select, + model_save=self.model_save, + ) + + model_train = clone(self.model) + if self.STACK_FEATURES_N_SAMPLES: + if X_test is not None: + X_train, y_train, X_test, y_test = Decoder.append_samples_val( + X_train, + y_train, + X_test, + y_test, + n=self.time_stack_n_samples, + ) + else: + X_train, y_train = Decoder.append_previous_n_samples( + X_train, y_train, n=self.time_stack_n_samples + ) + + if y_train.sum() == 0 or ( + y_test is not None and y_test.sum() == 0 + ): # only one class present + raise Decoder.ClassMissingException + + if self.RUN_BAY_OPT: + model_train = self.bay_opt_wrapper(model_train, X_train, y_train) + + if self.mrmr_select: + from mrmr import mrmr_classif + + if len(self.feature_names) > X_train.shape[1]: + # analyze induvidual ch + columns_names = [ + col + for col in self.feature_names + if col.startswith(self.ch_name_tested) + ] + if self.columns_names_single_ch is None: + self.columns_names_single_ch = [ + f[len(self.ch_name_tested) + 1 :] for f in columns_names + ] + else: + # analyze all_ch_combined + columns_names = self.feature_names + X_train = pd.DataFrame(X_train, columns=columns_names) + X_test = pd.DataFrame(X_test, columns=columns_names) + + y_train = pd.Series(y_train) + selected_features = mrmr_classif(X=X_train, y=y_train, K=20, n_jobs=60) + + X_train = X_train[selected_features] + X_test = X_test[selected_features] + + if self.pca: + from sklearn.decomposition import PCA + + pca = PCA(n_components=10) + pca.fit(X_train) + X_train = pca.transform(X_train) + X_test = pca.transform(X_test) + + if self.cca: + from sklearn.cross_decomposition import CCA + + cca = CCA(n_components=10) + cca.fit(X_train, y_train) + X_train = cca.transform(X_train) + X_test = cca.transform(X_test) + + if self.STACK_FEATURES_N_SAMPLES: + if return_fitted_model_only: + X_train, y_train = self.append_previous_n_samples( + X_train, y_train, self.time_stack_n_samples + ) + else: + X_train, y_train, X_test, y_test = self.append_samples_val( + X_train, y_train, X_test, y_test, self.time_stack_n_samples + ) + + # fit model + model_train = self.fit_model(model_train, X_train, y_train) + + if return_fitted_model_only: + return model_train + + cv_res = self.eval_model( + model_train, X_train, X_test, y_train, y_test, cv_res, save_data + ) + + if self.mrmr_select: + cv_res.mrmr_select.append(selected_features) + + return cv_res + +
+[docs] + def run_CV(self, data, label): + """Evaluate model performance on the specified cross validation. + If no data and label is specified, use whole feature class attributes. + + Parameters + ---------- + data (np.ndarray): + data to train and test with shape samples, features + label (np.ndarray): + label to train and test with shape samples, features + """ + + def split_data(data): + if self.cv_method == "NonShuffledTrainTestSplit": + # set outer 10s set to train index + # test index is thus in the middle starting at random number + N_samples = data.shape[0] + test_area_points = (N_samples - self.sfreq * 10) - (self.sfreq * 10) + test_points = int(N_samples * 0.3) + + if test_area_points > test_points: + start_index = np.random.randint( + int(self.sfreq * 10), + N_samples - self.sfreq * 10 - test_points, + ) + test_index = np.arange(start_index, start_index + test_points) + train_index = np.concatenate( + ( + np.arange(0, start_index), + np.arange(start_index + test_points, N_samples), + ), + axis=0, + ).flatten() + yield train_index, test_index + else: + cv_single_tr_te_split = model_selection.check_cv( + cv=[ + model_selection.train_test_split( + np.arange(data.shape[0]), + test_size=0.3, + shuffle=False, + ) + ] + ) + for ( + train_index, + test_index, + ) in cv_single_tr_te_split.split(): + yield train_index, test_index + else: + for train_index, test_index in self.cv_method.split(data): + yield train_index, test_index + + cv_res = self.init_cv_res() + + if self.use_nested_cv: + cv_res_inner = self.init_cv_res() + + for train_index, test_index in split_data(data): + X_train, y_train = data[train_index, :], label[train_index] + X_test, y_test = data[test_index], label[test_index] + try: + cv_res = self.wrapper_model_train( + X_train, y_train, X_test, y_test, cv_res + ) + except Decoder.ClassMissingException: + continue + + if self.use_nested_cv: + data_inner = data[train_index] + label_inner = label[train_index] + for train_index_inner, test_index_inner in split_data(data_inner): + X_train_inner = data_inner[train_index_inner, :] + y_train_inner = label_inner[train_index_inner] + X_test_inner = data_inner[test_index_inner] + y_test_inner = label_inner[test_index_inner] + try: + cv_res_inner = self.wrapper_model_train( + X_train_inner, + y_train_inner, + X_test_inner, + y_test_inner, + cv_res_inner, + ) + except Decoder.ClassMissingException: + continue + + self.cv_res = cv_res + if self.use_nested_cv: + self.cv_res_inner = cv_res_inner
+ + +
+[docs] + def bay_opt_wrapper(self, model_train, X_train, y_train): + """Run bayesian optimization and test best params to model_train + Save best params into self.best_bay_opt_params + """ + + ( + X_train_bo, + X_test_bo, + y_train_bo, + y_test_bo, + ) = model_selection.train_test_split( + X_train, y_train, train_size=0.7, shuffle=False + ) + + if y_train_bo.sum() == 0 or y_test_bo.sum() == 0: + logger.critical("could not start Bay. Opt. with no labels > 0") + raise Decoder.ClassMissingException + + params_bo = self.run_Bay_Opt( + X_train_bo, y_train_bo, X_test_bo, y_test_bo, rounds=10 + ) + + # set bay. opt. obtained best params to model + params_bo_dict = {} + for i in range(len(params_bo)): + setattr(model_train, self.bay_opt_param_space[i].name, params_bo[i]) + params_bo_dict[self.bay_opt_param_space[i].name] = params_bo[i] + + self.best_bay_opt_params.append(params_bo_dict) + + return model_train
+ + +
+[docs] + def run_Bay_Opt( + self, + X_train, + y_train, + X_test, + y_test, + rounds=30, + base_estimator="GP", + acq_func="EI", + acq_optimizer="sampling", + initial_point_generator="lhs", + ): + """Run skopt bayesian optimization + skopt.Optimizer: + https://scikit-optimize.github.io/stable/modules/generated/skopt.Optimizer.html#skopt.Optimizer + + example: + https://scikit-optimize.github.io/stable/auto_examples/ask-and-tell.html#sphx-glr-auto-examples-ask-and-tell-py + + Special attention needs to be made with the run_CV output, + some metrics are minimized (MAE), some are maximized (r^2) + + Parameters + ---------- + X_train: np.ndarray + y_train: np.ndarray + X_test: np.ndarray + y_test: np.ndarray + rounds : int, optional + optimizing rounds, by default 10 + base_estimator : str, optional + surrogate model, used as optimization function instead of cross validation, by default "GP" + acq_func : str, optional + function to minimize over the posterior distribution, by default "EI" + acq_optimizer : str, optional + method to minimize the acquisition function, by default "sampling" + initial_point_generator : str, optional + sets a initial point generator, by default "lhs" + + Returns + ------- + skopt result parameters + """ + + def get_f_val(model_bo): + try: + model_bo = self.fit_model(model_bo, X_train, y_train) + except Decoder.ClassMissingException: + pass + + return self.eval_method(y_test, model_bo.predict(X_test)) + + from skopt import Optimizer + + opt = Optimizer( + self.bay_opt_param_space, + base_estimator=base_estimator, + acq_func=acq_func, + acq_optimizer=acq_optimizer, + initial_point_generator=initial_point_generator, + ) + + for _ in range(rounds): + next_x = opt.ask() + # set model values + model_bo = clone(self.model) + for i in range(len(next_x)): + setattr(model_bo, self.bay_opt_param_space[i].name, next_x[i]) + f_val = get_f_val(model_bo) + res = opt.tell(next_x, f_val) + if self.VERBOSE: + logger.info(f_val) + + # res is here automatically appended by skopt + return res.x
+ + +
+[docs] + def save(self, feature_path: str, feature_file: str, str_save_add=None) -> None: + """Save decoder object to pickle""" + + # why is the decoder not saved to a .json? + + if str_save_add is None: + PATH_OUT = PurePath(feature_path, feature_file, feature_file + "_ML_RES.p") + else: + PATH_OUT = PurePath( + feature_path, + feature_file, + feature_file + "_" + str_save_add + "_ML_RES.p", + ) + + logger.info(f"model being saved to: {PATH_OUT}") + with open(PATH_OUT, "wb") as output: # Overwrites any existing file. + pickle.dump(self, output)
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_define_nmchannels.html b/_modules/nm_define_nmchannels.html new file mode 100644 index 00000000..98358efd --- /dev/null +++ b/_modules/nm_define_nmchannels.html @@ -0,0 +1,766 @@ + + + + + + + + + + nm_define_nmchannels — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_define_nmchannels

+"""Module for handling nm_channels."""
+
+from collections.abc import Iterable
+import pandas as pd
+import numpy as np
+
+
+_LFP_TYPES = ["seeg", "dbs", "lfp"]  # must be lower-case
+
+
+
+[docs] +def set_channels( + ch_names: list[str], + ch_types: list[str], + reference: list | str = "default", + bads: list[str] | None = None, + new_names: str | list[str] = "default", + ecog_only: bool = False, + used_types: Iterable[str] | None = ("ecog", "dbs", "seeg"), + target_keywords: Iterable[str] | None = ("mov", "squared", "label"), +) -> pd.DataFrame: + """Return dataframe with channel-specific settings in nm_channels format. + + Return an nm_channels dataframe with the columns: "name", "rereference", + "used", "target", "type", "status", "new_name"]. "name" is set to ch_names, + "rereference" can be specified individually. "used" is set to 1 for all + channel types specified in `used_types`, else to 0. "target" is set to 1 + for all channels containing any of the `target_keywords`, else to 0. + + Possible channel types: + https://github.com/mne-tools/mne-python/blob/6ae3b22033c745cce5cd5de9b92da54c13c36484/doc/_includes/channel_types.rst + + Arguments + --------- + ch_names : list + list of channel names. + ch_types : list + list of channel types. Should optimally be of the types: "ECOG", + "DBS" or "SEEG". + reference : str | list of str | None, default: 'default' + re-referencing scheme. Default is "default". This sets ECOG channel + references to "average" and creates a bipolar referencing scheme + for LFP/DBS/SEEG channels, where each channel is referenced to + the adjacent lower channel, split by left and right hemisphere. + For this, the channel names must contain the substring "_L_" and/or + "_R_" (lower or upper case). CAVE: Adjacent channels will be + determined using the sort() function. + bads : str | list of str, default: None + channels that should be marked as bad and not be used for + average re-referencing etc. + new_names : list of str | None, default: 'default' + new channel names that should be used when writing out the + features and results. Useful when applying re-referencing. Set to + 'None' if no renaming should be performed. 'default' will infer + channel renaming from re-referencing information. If a list is + given, it should be in the same order as 'ch_names'. + ECOG_ONLY : boolean, default: False + if True, set only 'ecog' channel type to used + used_types : iterable of str | None, default : ("ecog", "dbs", "seeg") + data channel types to be used. Set to `None` to use no channel + types. + target_keywords : iterable of str | None, default : ("ecog", "dbs", "seeg") + keywords for target channels + + Returns + ------- + df: DataFrame in nm_channels format + """ + if not (len(ch_names) == len(ch_types)): + raise ValueError( + "Number of `ch_names` and `ch_types` must match." + f"Got: {len(ch_names)} `ch_names` and {len(ch_types)} `ch_types`." + ) + + df = pd.DataFrame( + data=None, + columns=[ + "name", + "rereference", + "used", + "target", + "type", + "status", + "new_name", + ], + ) + df["name"] = ch_names + + if used_types: + if isinstance(used_types, str): + used_types = [ + used_types + ] # Even if the user passes only ("ecog"), the if statement bellow will work + used_list = [] + for ch_type in ch_types: + if any(use_type.lower() == ch_type.lower() for use_type in used_types): + used_list.append(1) + else: + used_list.append(0) + df["used"] = used_list + else: + df["used"] = 0 + + if target_keywords: + if isinstance(target_keywords, str): + target_keywords = [target_keywords] + targets = [] + for ch_name in ch_names: + if any(kw.lower() in ch_name.lower() for kw in target_keywords): + targets.append(1) + else: + targets.append(0) + df["target"] = targets + else: + df["target"] = 0 + + # note: BIDS types are in caps, mne.io.RawArray types lower case + # so that 'type' will be in lower case here + df["type"] = ch_types + + if ecog_only: + df.loc[(df["type"] == "seeg") | (df["type"] == "dbs"), "used"] = 0 + + if isinstance(reference, str): + if reference.lower() == "default": + df = _get_default_references(df=df, ch_names=ch_names, ch_types=ch_types) + else: + raise ValueError( + "`reference` must be either `default`, `None` or " + "an iterable of new reference channel names. " + f"Got: {reference}." + ) + + elif isinstance(reference, list): + if len(ch_names) != len(reference): + raise ValueError( + "Number of `ch_names` and `reference` must match." + f"Got: {len(ch_names)} `ch_names` and {len(reference)}" + " `references`." + ) + df["rereference"] = reference + elif not reference: + df.loc[:, "rereference"] = "None" + else: + raise ValueError( + "`reference` must be either `default`, None or " + "an iterable of new reference channel names. " + f"Got: {reference}." + ) + + if bads: + if isinstance(bads, str): + bads = [bads] + df["status"] = ["bad" if ch in bads else "good" for ch in ch_names] + df.loc[df["status"] == "bad", "used"] = ( + 0 # setting bad channels to not being used + ) + else: + df["status"] = "good" + + if not new_names: + df["new_name"] = ch_names + elif isinstance(new_names, str): + if new_names.lower() != "default": + raise ValueError( + "`new_names` must be either `default`, None or " + "an iterable of new channel names. Got: " + f"{new_names}." + ) + new_names = [] + for name, ref in zip(df["name"], df["rereference"]): + if ref == "None": + new_names.append(name) + elif isinstance(ref, float): + if np.isnan(ref): + new_names.append(name) + elif ref == "average": + new_names.append(name + "-avgref") + else: + new_names.append(name + "-" + ref) + df["new_name"] = new_names + elif hasattr(new_names, "__iter__"): + if len(new_names) != len(ch_names): + raise ValueError( + "Number of `ch_names` and `new_names` must match." + f" Got: {len(ch_names)} `ch_names` and {len(new_names)}" + " `new_names`." + ) + else: + df["new_name"] = ch_names + else: + raise ValueError( + "`new_names` must be either `default`, None or" + f" an iterable of new channel names. Got: {new_names}." + ) + return df
+ + + +def _get_default_references( + df: pd.DataFrame, ch_names: list[str], ch_types: list[str] +) -> pd.DataFrame: + """Add references with default settings (ECOG CAR, LFP bipolar).""" + ecog_chs = [] + lfp_chs = [] + other_chs = [] + for ch_name, ch_type in zip(ch_names, ch_types): + if "ecog" in ch_type.lower() or "ecog" in ch_name.lower(): + ecog_chs.append(ch_name) + elif any( + lfp_type in ch_type.lower() or lfp_type in ch_name.lower() + for lfp_type in _LFP_TYPES + ): + lfp_chs.append(ch_name) + else: + other_chs.append(ch_name) + lfp_l = [ + lfp_ch + for lfp_ch in lfp_chs + if ("_l_" in lfp_ch.lower()) or ("_left_" in lfp_ch.lower()) + ] + lfp_l.sort() + lfp_r = [ + lfp_ch + for lfp_ch in lfp_chs + if ("_r_" in lfp_ch.lower()) or ("_right_" in lfp_ch.lower()) + ] + lfp_r.sort() + lfp_l_refs = [lfp_l[i - 1] if i > 0 else lfp_l[-1] for i, _ in enumerate(lfp_l)] + lfp_r_refs = [lfp_r[i - 1] if i > 0 else lfp_r[-1] for i, _ in enumerate(lfp_r)] + ref_idx = list(df.columns).index("rereference") + if len(ecog_chs) > 1: + for ecog_ch in ecog_chs: + df.iloc[df[df["name"] == ecog_ch].index[0], ref_idx] = "average" + if ( + len(lfp_l) > 1 + ): # if there is only a single channel, the channel would be subtracted from itself + for i, lfp in enumerate(lfp_l): + df.iloc[df[df["name"] == lfp].index[0], ref_idx] = lfp_l_refs[i] + if len(lfp_r) > 1: + for i, lfp in enumerate(lfp_r): + df.iloc[df[df["name"] == lfp].index[0], ref_idx] = lfp_r_refs[i] + for other_ch in other_chs: + df.iloc[df[df["name"] == other_ch].index[0], ref_idx] = "None" + + df = df.replace(np.nan, "None") + + return df + + +
+[docs] +def get_default_channels_from_data( + data: np.ndarray, + car_rereferencing: bool = True, +): + """Return default nm_channels dataframe with + ecog datatype, no bad channels, no targets, common average rereferencing + + Parameters + ---------- + data : np.ndarray + Data array in shape (n_channels, n_time) + car_rereferencing : bool, optional + use common average rereferencing, by default True + + Returns + ------- + pd.DataFrame + nm_channel dataframe containing columns: + - name + - rereference + - used + - target + - type + - status + - new_name + """ + + ch_name = [f"ch{idx}" for idx in range(data.shape[0])] + status = ["good" for _ in range(data.shape[0])] + type_nm = ["ecog" for _ in range(data.shape[0])] + + if car_rereferencing: + rereference = ["average" for _ in range(data.shape[0])] + new_name = [f"{ch}-avgref" for ch in ch_name] + else: + rereference = ["None" for _ in range(data.shape[0])] + new_name = ch_name + + new_name = [f"{ch}-avgref" for ch in ch_name] + target = np.array([0 for _ in range(data.shape[0])]) + used = np.array([1 for _ in range(data.shape[0])]) + + df = pd.DataFrame() + df["name"] = ch_name + df["rereference"] = rereference + df["used"] = used + df["target"] = target + df["type"] = type_nm + df["status"] = status + df["new_name"] = new_name + + return df
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_features.html b/_modules/nm_features.html new file mode 100644 index 00000000..98e2545a --- /dev/null +++ b/_modules/nm_features.html @@ -0,0 +1,638 @@ + + + + + + + + + + nm_features — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_features

+from typing import Protocol, Type, runtime_checkable, TYPE_CHECKING, TypeVar
+from collections.abc import Sequence
+
+if TYPE_CHECKING:
+    import numpy as np
+    from nm_settings import NMSettings
+
+from py_neuromodulation.nm_types import ImportDetails, get_class, FeatureName
+
+
+
+[docs] +@runtime_checkable +class NMFeature(Protocol): + def __init__( + self, settings: "NMSettings", ch_names: Sequence[str], sfreq: int | float + ) -> None: ... + +
+[docs] + def calc_feature(self, data: "np.ndarray", features_compute: dict) -> dict: + """ + Feature calculation method. Each method needs to loop through all channels + + Parameters + ---------- + data : 'np.ndarray' + (channels, time) + features_compute : dict + ch_names : Iterable[str] + + Returns + ------- + dict + """ + ...
+
+ + + +FEATURE_DICT: dict[FeatureName | str, ImportDetails] = { + "raw_hjorth": ImportDetails("nm_hjorth_raw", "Hjorth"), + "return_raw": ImportDetails("nm_hjorth_raw", "Raw"), + "bandpass_filter": ImportDetails("nm_oscillatory", "BandPower"), + "stft": ImportDetails("nm_oscillatory", "STFT"), + "fft": ImportDetails("nm_oscillatory", "FFT"), + "welch": ImportDetails("nm_oscillatory", "Welch"), + "sharpwave_analysis": ImportDetails("nm_sharpwaves", "SharpwaveAnalyzer"), + "fooof": ImportDetails("nm_fooof", "FooofAnalyzer"), + "nolds": ImportDetails("nm_nolds", "Nolds"), + "coherence": ImportDetails("nm_coherence", "NMCoherence"), + "bursts": ImportDetails("nm_bursts", "Burst"), + "linelength": ImportDetails("nm_linelength", "LineLength"), + "mne_connectivity": ImportDetails("nm_mne_connectivity", "MNEConnectivity"), + "bispectrum": ImportDetails("nm_bispectra", "Bispectra"), +} + + +
+[docs] +class FeatureProcessors: + """Class for calculating features.p""" + + def __init__( + self, settings: "NMSettings", ch_names: list[str], sfreq: float + ) -> None: + """_summary_ + + Parameters + ---------- + s : dict + _description_ + ch_names : list[str] + _description_ + sfreq : float + _description_ + + Raises + ------ + ValueError + _description_ + """ + from py_neuromodulation import user_features + + # Accept 'str' for custom features + self.features: dict[FeatureName | str, NMFeature] = { + feature_name: get_class(FEATURE_DICT[feature_name])( + settings, ch_names, sfreq + ) + for feature_name in settings.features.get_enabled() + } + + for feature_name, feature in user_features.items(): + self.features[feature_name] = feature(settings, ch_names, sfreq) + +
+[docs] + def register_new_feature(self, feature_name: str, feature: NMFeature) -> None: + """Register new feature. + + Parameters + ---------- + feature : nm_features_abc.Feature + New feature to add to feature list + """ + self.features[feature_name] = feature # type: ignore
+ + +
+[docs] + def estimate_features(self, data: "np.ndarray") -> dict: + """Calculate features, as defined in settings.json + Features are based on bandpower, raw Hjorth parameters and sharp wave + characteristics. + + Parameters + ---------- + data (np array) : (channels, time) + + Returns + ------- + dat (dict): naming convention : channel_method_feature_(f_band) + """ + + features_compute: dict = {} + + for feature in self.features.values(): + features_compute = feature.calc_feature( + data, + features_compute, + ) + + return features_compute
+ + + def get_feature(self, fname: FeatureName) -> NMFeature: + return self.features[fname]
+ + + +
+[docs] +def add_custom_feature(feature_name: str, new_feature: Type[NMFeature]): + """Add a custom feature to the dictionary of user-defined features. + The feature will be automatically enabled in the settings, + and computed when the Stream.run() method is called. + + Args: + feature_name (str): A name for the feature that will be used to + enable/disable the feature in settings and to store the feature + class instance in the DataProcessor + + new_feature (NMFeature): Class that implements the user-defined + feature. It should implement the NMSettings protocol (defined + in this file). + """ + from py_neuromodulation import user_features + from py_neuromodulation.nm_settings import NMSettings + + user_features[feature_name] = new_feature + NMSettings._add_feature(feature_name)
+ + + +
+[docs] +def remove_custom_feature(feature_name: str): + """Remove a custom feature from the dictionary of user-defined features. + + Args: + feature_name (str): Name of the feature to remove + """ + from py_neuromodulation import user_features + from py_neuromodulation.nm_settings import NMSettings + + user_features.pop(feature_name) + NMSettings._remove_feature(feature_name)
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_filter.html b/_modules/nm_filter.html new file mode 100644 index 00000000..0f2a93d5 --- /dev/null +++ b/_modules/nm_filter.html @@ -0,0 +1,679 @@ + + + + + + + + + + nm_filter — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_filter

+"""Module for filter functionality."""
+
+import numpy as np
+from typing import cast
+from collections.abc import Sequence
+
+from py_neuromodulation.nm_preprocessing import NMPreprocessor
+from py_neuromodulation import logger
+
+from mne.filter import create_filter
+
+
+
+[docs] +class MNEFilter: + """mne.filter wrapper + + This class stores for given frequency band ranges the filter + coefficients with length "filter_len". + The filters can then be used sequentially for band power estimation with + apply_filter(). + Note that this filter can be a bandpass, bandstop, lowpass, or highpass filter + depending on the frequency ranges given (see further details in mne.filter.create_filter). + + Parameters + ---------- + f_ranges : list[tuple[float | None, float | None]] + sfreq : float + Sampling frequency. + filter_length : str, optional + Filter length. Human readable (e.g. "1000ms", "1s"), by default "999ms" + l_trans_bandwidth : float | str, optional + Length of the lower transition band or "auto", by default 4 + h_trans_bandwidth : float | str, optional + Length of the higher transition band or "auto", by default 4 + verbose : bool | None, optional + Verbosity level, by default None + + Attributes + ---------- + filter_bank: np.ndarray shape (n,) + Factor to upsample by. + """ + + def __init__( + self, + f_ranges: Sequence[tuple[float | None, float | None]], + sfreq: float, + filter_length: str | float = "999ms", + l_trans_bandwidth: float | str = 4, + h_trans_bandwidth: float | str = 4, + verbose: bool | int | str | None = None, + ) -> None: + filter_bank = [] + # mne create_filter function only accepts str and int for filter_length + if isinstance(filter_length, float): + filter_length = int(filter_length) + + for f_range in f_ranges: + try: + filt = create_filter( + None, + sfreq, + l_freq=f_range[0], + h_freq=f_range[1], + fir_design="firwin", + l_trans_bandwidth=l_trans_bandwidth, # type: ignore + h_trans_bandwidth=h_trans_bandwidth, # type: ignore + filter_length=filter_length, # type: ignore + verbose=verbose, + ) + except ValueError: + filt = create_filter( + None, + sfreq, + l_freq=f_range[0], + h_freq=f_range[1], + fir_design="firwin", + verbose=verbose, + # filter_length=filter_length, + ) + filter_bank.append(filt) + self.filter_bank = np.vstack(filter_bank) + +
+[docs] + def filter_data(self, data: np.ndarray) -> np.ndarray: + """Apply previously calculated (bandpass) filters to data. + + Parameters + ---------- + data : np.ndarray (n_samples, ) or (n_channels, n_samples) + Data to be filtered + filter_bank : np.ndarray, shape (n_fbands, filter_len) + Output of calc_bandpass_filters. + + Returns + ------- + np.ndarray, shape (n_channels, n_fbands, n_samples) + Filtered data. + + """ + if data.ndim > 2: + raise ValueError( + f"Data must have one or two dimensions. Got:" + f" {data.ndim} dimensions." + ) + if data.ndim == 1: + data = np.expand_dims(data, axis=0) + + filtered = np.array( + [ + [np.convolve(filt, chan, mode="same") for filt in self.filter_bank] + for chan in data + ] + ) + + # ensure here that the output dimension matches the input dimension + if data.shape[1] != filtered.shape[-1]: + # select the middle part of the filtered data + middle_index = filtered.shape[-1] // 2 + filtered = filtered[ + :, + :, + middle_index - data.shape[1] // 2 : middle_index + data.shape[1] // 2, + ] + + return filtered
+
+ + + +
+[docs] +class NotchFilter(NMPreprocessor): + def __init__( + self, + sfreq: float, + line_noise: float | None = None, + freqs: np.ndarray | None = None, + notch_widths: int | np.ndarray | None = 3, + trans_bandwidth: float = 6.8, + ) -> None: + if line_noise is None and freqs is None: + raise ValueError( + "Either line_noise or freqs must be defined if notch_filter is" + "activated." + ) + + if freqs is None: + freqs = np.arange(line_noise, sfreq / 2, line_noise, dtype=int) + + if freqs.size > 0 and freqs[-1] >= sfreq / 2: + freqs = freqs[:-1] + + # Code is copied from filter.py notch_filter + if freqs.size == 0: + self.filter_bank = None + logger.warning( + "WARNING: notch_filter is activated but data is not being" + " filtered. This may be due to a low sampling frequency or" + " incorrect specifications. Make sure your settings are" + f" correct. Got: {sfreq = }, {line_noise = }, {freqs = }." + ) + return + + filter_length = int(sfreq - 1) + if notch_widths is None: + notch_widths = freqs / 200.0 + elif np.any(notch_widths < 0): + raise ValueError("notch_widths must be >= 0") + else: + notch_widths = np.atleast_1d(notch_widths) + if len(notch_widths) == 1: + notch_widths = notch_widths[0] * np.ones_like(freqs) + elif len(notch_widths) != len(freqs): + raise ValueError( + "notch_widths must be None, scalar, or the " "same length as freqs" + ) + notch_widths = cast(np.ndarray, notch_widths) # For MyPy only, no runtime cost + + # Speed this up by computing the fourier coefficients once + tb_half = trans_bandwidth / 2.0 + lows = [freq - nw / 2.0 - tb_half for freq, nw in zip(freqs, notch_widths)] + highs = [freq + nw / 2.0 + tb_half for freq, nw in zip(freqs, notch_widths)] + + self.filter_bank = create_filter( + data=None, + sfreq=sfreq, + l_freq=highs, + h_freq=lows, + filter_length=filter_length, # type: ignore + l_trans_bandwidth=tb_half, # type: ignore + h_trans_bandwidth=tb_half, # type: ignore + method="fir", + iir_params=None, + phase="zero", + fir_window="hamming", + fir_design="firwin", + verbose=False, + ) + + def process(self, data: np.ndarray) -> np.ndarray: + if self.filter_bank is None: + return data + + from mne.filter import _overlap_add_filter + + return _overlap_add_filter( + x=data, + h=self.filter_bank, + n_fft=None, + phase="zero", + picks=None, + n_jobs=1, + copy=True, + pad="reflect_limited", + )
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_fooof.html b/_modules/nm_fooof.html new file mode 100644 index 00000000..1adde010 --- /dev/null +++ b/_modules/nm_fooof.html @@ -0,0 +1,618 @@ + + + + + + + + + + nm_fooof — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_fooof

+from collections.abc import Iterable
+import numpy as np
+
+from typing import TYPE_CHECKING
+from py_neuromodulation.nm_types import NMBaseModel
+
+from py_neuromodulation.nm_features import NMFeature
+from py_neuromodulation.nm_types import BoolSelector, FrequencyRange
+from py_neuromodulation import logger
+
+if TYPE_CHECKING:
+    from py_neuromodulation.nm_settings import NMSettings
+
+
+
+[docs] +class FooofAperiodicSettings(BoolSelector): + exponent: bool = True + offset: bool = True + knee: bool = True
+ + + +
+[docs] +class FooofPeriodicSettings(BoolSelector): + center_frequency: bool = False + band_width: bool = False + height_over_ap: bool = False
+ + + +
+[docs] +class FooofSettings(NMBaseModel): + aperiodic: FooofAperiodicSettings = FooofAperiodicSettings() + periodic: FooofPeriodicSettings = FooofPeriodicSettings() + windowlength_ms: float = 800 + peak_width_limits: FrequencyRange = FrequencyRange(0.5, 12) + max_n_peaks: int = 3 + min_peak_height: float = 0 + peak_threshold: float = 2 + freq_range_hz: FrequencyRange = FrequencyRange(2, 40) + knee: bool = True
+ + + +
+[docs] +class FooofAnalyzer(NMFeature): + feat_name_map = { + "exponent": "exp", + "offset": "offset", + "knee": "knee_frequency", + "center_frequency": "cf", + "band_width": "bw", + "height_over_ap": "pw", + } + + def __init__( + self, settings: "NMSettings", ch_names: Iterable[str], sfreq: float + ) -> None: + self.settings = settings.fooof + self.sfreq = sfreq + self.ch_names = ch_names + + self.ap_mode = "knee" if self.settings.knee else "fixed" + + self.num_samples = int(self.settings.windowlength_ms * sfreq / 1000) + + self.f_vec = np.arange(0, int(self.num_samples / 2) + 1, 1) + + assert ( + settings.fooof.windowlength_ms <= settings.segment_length_features_ms + ), f"fooof windowlength_ms ({settings.fooof.windowlength_ms}) needs to be smaller equal than segment_length_features_ms ({settings.segment_length_features_ms})." + + assert ( + settings.fooof.freq_range_hz[0] < sfreq + and settings.fooof.freq_range_hz[1] < sfreq + ), f"fooof frequency range needs to be below sfreq, got {settings.fooof.freq_range_hz}" + + from fooof import FOOOFGroup + + self.fm = FOOOFGroup( + aperiodic_mode=self.ap_mode, + peak_width_limits=tuple(self.settings.peak_width_limits), + max_n_peaks=self.settings.max_n_peaks, + min_peak_height=self.settings.min_peak_height, + peak_threshold=self.settings.peak_threshold, + verbose=False, + ) + +
+[docs] + def calc_feature( + self, + data: np.ndarray, + features_compute: dict, + ) -> dict: + from scipy.fft import rfft + + spectra = np.abs(rfft(data[:, -self.num_samples :])) # type: ignore + + self.fm.fit(self.f_vec, spectra, self.settings.freq_range_hz) + + if not self.fm.has_model or self.fm.null_inds_ is None: + raise RuntimeError("FOOOF failed to fit model to data.") + + failed_fits: list[int] = self.fm.null_inds_ + + for ch_idx, ch_name in enumerate(self.ch_names): + FIT_PASSED = ch_idx not in failed_fits + exp = self.fm.get_params("aperiodic_params", "exponent")[ch_idx] + + for feat in self.settings.aperiodic.get_enabled(): + f_name = f"{ch_name}_fooof_a_{self.feat_name_map[feat]}" + + if not FIT_PASSED: + features_compute[f_name] = None + + elif feat == "knee" and exp == 0: + features_compute[f_name] = None + + else: + params = self.fm.get_params("aperiodic_params", feat)[ch_idx] + if feat == "knee": + # If knee parameter is negative, set knee frequency to 0 + if params < 0: + params = 0 + else: + params = params ** (1 / exp) + + features_compute[f_name] = np.nan_to_num(params) + + peaks_dict: dict[str, np.ndarray | None] = { + "bw": self.fm.get_params("peak_params", "BW") if FIT_PASSED else None, + "cf": self.fm.get_params("peak_params", "CF") if FIT_PASSED else None, + "pw": self.fm.get_params("peak_params", "PW") if FIT_PASSED else None, + } + + if type(peaks_dict["bw"]) is np.float64 or peaks_dict["bw"] is None: + peaks_dict["bw"] = [peaks_dict["bw"]] + peaks_dict["cf"] = [peaks_dict["cf"]] + peaks_dict["pw"] = [peaks_dict["pw"]] + + for peak_idx in range(self.settings.max_n_peaks): + for feat in self.settings.periodic.get_enabled(): + f_name = f"{ch_name}_fooof_p_{peak_idx}_{self.feat_name_map[feat]}" + + features_compute[f_name] = ( + peaks_dict[self.feat_name_map[feat]][peak_idx] + if peak_idx < len(peaks_dict[self.feat_name_map[feat]]) + else None + ) + + return features_compute
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_generator.html b/_modules/nm_generator.html new file mode 100644 index 00000000..0b5b842f --- /dev/null +++ b/_modules/nm_generator.html @@ -0,0 +1,507 @@ + + + + + + + + + + nm_generator — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_generator

+from collections.abc import Iterator
+from typing import TYPE_CHECKING
+import numpy as np
+
+if TYPE_CHECKING:
+    from py_neuromodulation.nm_settings import NMSettings
+
+
+
+[docs] +def raw_data_generator( + data: np.ndarray, + settings: "NMSettings", + sfreq: float, +) -> Iterator[tuple[np.ndarray, np.ndarray]]: + """ + This generator function mimics online data acquisition. + The data are iteratively sampled with sfreq_new. + Arguments + --------- + ieeg_raw (np array): shape (channels, time) + sfreq: int + sfreq_new: int + offset_time: float + Returns + ------- + np.array: 1D array of time stamps + np.array: new batch for run function of full segment length shape + """ + sfreq_new = settings.sampling_rate_features_hz + offset_time = settings.segment_length_features_ms + offset_start = offset_time / 1000 * sfreq + + ratio_samples_features = sfreq / sfreq_new + + ratio_counter = 0 + for cnt in range( + data.shape[1] + 1 + ): # shape + 1 guarantees that the last sample is also included + if (cnt - offset_start) >= ratio_samples_features * ratio_counter: + ratio_counter += 1 + + yield ( + np.arange(cnt - offset_start, cnt) / sfreq, + data[:, np.floor(cnt - offset_start).astype(int) : cnt], + )
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_hjorth_raw.html b/_modules/nm_hjorth_raw.html new file mode 100644 index 00000000..f8d6bed6 --- /dev/null +++ b/_modules/nm_hjorth_raw.html @@ -0,0 +1,514 @@ + + + + + + + + + + nm_hjorth_raw — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_hjorth_raw

+import numpy as np
+from collections.abc import Iterable
+
+from py_neuromodulation.nm_features import NMFeature
+from py_neuromodulation.nm_settings import NMSettings
+
+
+
+[docs] +class Hjorth(NMFeature): + def __init__(self, settings: NMSettings, ch_names: Iterable[str], sfreq: float) -> None: + self.ch_names = ch_names + +
+[docs] + def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict: + for ch_idx, ch_name in enumerate(self.ch_names): + features_compute["_".join([ch_name, "RawHjorth_Activity"])] = np.nan_to_num( + np.var(data[ch_idx, :]) + ) + deriv_variance = np.nan_to_num(np.var(np.diff(data[ch_idx, :]))) + mobility = np.nan_to_num(np.sqrt(deriv_variance / np.var(data[ch_idx, :]))) + features_compute["_".join([ch_name, "RawHjorth_Mobility"])] = mobility + + dat_deriv_2_var = np.nan_to_num(np.var(np.diff(np.diff(data[ch_idx, :])))) + deriv_mobility = np.nan_to_num(np.sqrt(dat_deriv_2_var / deriv_variance)) + features_compute["_".join([ch_name, "RawHjorth_Complexity"])] = ( + np.nan_to_num(deriv_mobility / mobility) + ) + + return features_compute
+
+ + + +
+[docs] +class Raw(NMFeature): + def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None: + self.ch_names = ch_names + +
+[docs] + def calc_feature( + self, + data: np.ndarray, + features_compute: dict, + ) -> dict: + for ch_idx, ch_name in enumerate(self.ch_names): + features_compute["_".join([ch_name, "raw"])] = data[ch_idx, -1] + + return features_compute
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_kalmanfilter.html b/_modules/nm_kalmanfilter.html new file mode 100644 index 00000000..b1127c3e --- /dev/null +++ b/_modules/nm_kalmanfilter.html @@ -0,0 +1,531 @@ + + + + + + + + + + nm_kalmanfilter — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_kalmanfilter

+from numpy import array, cov
+from py_neuromodulation.nm_types import NMBaseModel
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from py_neuromodulation.nm_settings import NMSettings
+
+
+
+[docs] +class KalmanSettings(NMBaseModel): + Tp: float = 0.1 + sigma_w: float = 0.7 + sigma_v: float = 1.0 + frequency_bands: list[str] = [ + "theta", + "alpha", + "low_beta", + "high_beta", + "low_gamma", + "high_gamma", + "HFA", + ] + + def validate_fbands(self, settings: "NMSettings") -> None: + assert all( + (item in settings.frequency_ranges_hz for item in self.frequency_bands) + ), ( + "Frequency bands for Kalman filter must also be specified in " + "bandpass_filter_settings." + )
+ + + +
+[docs] +def define_KF(Tp, sigma_w, sigma_v): + """Define Kalman filter according to white noise acceleration model. + See DOI: 10.1109/TBME.2009.2038990 for explanation + See https://filterpy.readthedocs.io/en/latest/kalman/KalmanFilter.html#r64ca38088676-2 for implementation details + + Parameters + ---------- + Tp : float + prediction interval + sigma_w : float + process noise + sigma_v : float + measurement noise + + Returns + ------- + filterpy.KalmanFilter + initialized KalmanFilter object + """ + from filterpy.kalman import KalmanFilter + + f = KalmanFilter(dim_x=2, dim_z=1) + f.x = array([0, 1]) # x here sensor signal and it's first derivative + f.F = array([[1, Tp], [0, 1]]) + f.H = array([[1, 0]]) + f.R = sigma_v + f.Q = array( + [ + [(sigma_w**2) * (Tp**3) / 3, (sigma_w**2) * (Tp**2) / 2], + [(sigma_w**2) * (Tp**2) / 2, (sigma_w**2) * Tp], + ] + ) + f.P = cov([[1, 0], [0, 1]]) + return f
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_linelength.html b/_modules/nm_linelength.html new file mode 100644 index 00000000..5ba1f421 --- /dev/null +++ b/_modules/nm_linelength.html @@ -0,0 +1,497 @@ + + + + + + + + + + nm_linelength — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_linelength

+import numpy as np
+from collections.abc import Iterable
+
+from py_neuromodulation.nm_features import NMFeature
+
+
+
+[docs] +class LineLength(NMFeature): + def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None: + self.settings = settings + self.ch_names = ch_names + + @staticmethod + def get_line_length(x: np.ndarray) -> np.floating: + return np.mean(np.abs(np.diff(x)) / (x.shape[0] - 1)) + + @staticmethod + def test_settings( + settings: dict, + ch_names: Iterable[str], + sfreq: float, + ): + # no settings to be checked + pass + +
+[docs] + def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict: + for ch_idx, ch_name in enumerate(self.ch_names): + features_compute["_".join([ch_name, "LineLength"])] = self.get_line_length( + data[ch_idx, :] + ) + + return features_compute
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_mne_connectivity.html b/_modules/nm_mne_connectivity.html new file mode 100644 index 00000000..4d0a812c --- /dev/null +++ b/_modules/nm_mne_connectivity.html @@ -0,0 +1,586 @@ + + + + + + + + + + nm_mne_connectivity — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_mne_connectivity

+from collections.abc import Iterable
+import numpy as np
+from typing import TYPE_CHECKING
+
+from py_neuromodulation.nm_features import NMFeature
+from py_neuromodulation.nm_types import NMBaseModel
+
+if TYPE_CHECKING:
+    from py_neuromodulation.nm_settings import NMSettings
+    from mne.io import RawArray
+    from mne import Epochs
+
+
+
+[docs] +class MNEConnectivitySettings(NMBaseModel): + method: str = "plv" + mode: str = "multitaper"
+ + + +
+[docs] +class MNEConnectivity(NMFeature): + def __init__( + self, + settings: "NMSettings", + ch_names: Iterable[str], + sfreq: float, + ) -> None: + self.ch_names = ch_names + self.settings = settings + self.mode = settings.mne_connectivity.mode + self.method = settings.mne_connectivity.method + self.sfreq = sfreq + + self.fbands = settings.frequency_ranges_hz + self.fband_ranges: list = [] + + @staticmethod + def get_epoched_data(raw: "RawArray", epoch_length: float = 1) -> "Epochs": + time_samples_s = raw.get_data().shape[1] / raw.info["sfreq"] + if epoch_length > time_samples_s: + raise ValueError( + f"the intended epoch length for mne connectivity: {epoch_length}s" + f" are longer than the passed data array {np.round(time_samples_s, 2)}s" + ) + + from mne import make_fixed_length_events, Epochs + + events = make_fixed_length_events(raw, duration=epoch_length, overlap=0) + event_id = {"rest": 1} + + epochs = Epochs( + raw, + events=events, + event_id=event_id, + tmin=0, + tmax=epoch_length, + baseline=None, + reject_by_annotation=True, + verbose=False, + ) + if epochs.events.shape[0] < 2: + raise Exception( + f"A minimum of 2 epochs is required for mne_connectivity," + f" got only {epochs.events.shape[0]}. Increase settings['segment_length_features_ms']" + ) + return epochs + + def estimate_connectivity(self, epochs: "Epochs"): + # n_jobs is here kept to 1, since setup of the multiprocessing Pool + # takes longer than most batch computing sizes + from mne_connectivity import spectral_connectivity_epochs + + spec_out = spectral_connectivity_epochs( + data=epochs, + sfreq=self.sfreq, + n_jobs=1, + method=self.method, + mode=self.mode, + indices=(np.array([0, 0, 1, 1]), np.array([2, 3, 2, 3])), + faverage=False, + block_size=1000, + verbose=False, + ) + return spec_out + +
+[docs] + def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict: + from mne.io import RawArray + from mne import create_info + + raw = RawArray( + data=data, + info=create_info(ch_names=self.ch_names, sfreq=self.sfreq), + verbose=False + ) + epochs = self.get_epoched_data(raw) + # there need to be minimum 2 of two epochs, otherwise mne_connectivity + # is not correctly initialized + + spec_out = self.estimate_connectivity(epochs) + + if not self.fband_ranges: + for fband_name, fband_range in self.fbands.items(): + self.fband_ranges.append( + np.where( + np.logical_and( + np.array(spec_out.freqs) > fband_range[0], + np.array(spec_out.freqs) < fband_range[1], + ) + )[0] + ) + + dat_conn = spec_out.get_data() + for conn in np.arange(dat_conn.shape[0]): + for fband_idx, fband in enumerate(self.fbands): + features_compute["_".join(["ch1", self.method, str(conn), fband])] = ( + np.mean(dat_conn[conn, self.fband_ranges[fband_idx]]) + ) + + return features_compute
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_nolds.html b/_modules/nm_nolds.html new file mode 100644 index 00000000..4f0c542b --- /dev/null +++ b/_modules/nm_nolds.html @@ -0,0 +1,565 @@ + + + + + + + + + + nm_nolds — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_nolds

+import numpy as np
+from collections.abc import Iterable
+
+from py_neuromodulation.nm_types import NMBaseModel
+from typing import TYPE_CHECKING
+
+from py_neuromodulation.nm_features import NMFeature
+from py_neuromodulation.nm_types import BoolSelector
+
+if TYPE_CHECKING:
+    from py_neuromodulation.nm_settings import NMSettings
+
+
+
+[docs] +class NoldsFeatures(BoolSelector): + sample_entropy: bool = False + correlation_dimension: bool = False + lyapunov_exponent: bool = True + hurst_exponent: bool = False + detrended_fluctuation_analysis: bool = False
+ + + +
+[docs] +class NoldsSettings(NMBaseModel): + raw: bool = True + frequency_bands: list[str] = ["low beta"] + features: NoldsFeatures = NoldsFeatures()
+ + + +
+[docs] +class Nolds(NMFeature): + def __init__( + self, settings: "NMSettings", ch_names: Iterable[str], sfreq: float + ) -> None: + self.settings = settings.nolds_features + self.ch_names = ch_names + + if len(self.settings.frequency_bands) > 0: + from py_neuromodulation.nm_oscillatory import BandPower + + self.bp_filter = BandPower(settings, ch_names, sfreq, use_kf=False) + + # Check if the selected frequency bands are defined in the global settings + for fb in settings.nolds_features.frequency_bands: + assert ( + fb in settings.frequency_ranges_hz + ), f"{fb} selected in nolds_features, but not defined in s['frequency_ranges_hz']" + +
+[docs] + def calc_feature( + self, + data: np.ndarray, + features_compute: dict, + ) -> dict: + data = np.nan_to_num(data) + if self.settings.raw: + features_compute = self.calc_nolds(data, features_compute) + if len(self.settings.frequency_bands) > 0: + data_filt = self.bp_filter.bandpass_filter.filter_data(data) + + for f_band_idx, f_band in enumerate(self.settings.frequency_bands): + # filter data now for a specific fband and pass to calc_nolds + features_compute = self.calc_nolds( + data_filt[:, f_band_idx, :], features_compute, f_band + ) # ch, bands, samples + return features_compute
+ + + def calc_nolds( + self, data: np.ndarray, features_compute: dict, data_str: str = "raw" + ) -> dict: + for ch_idx, ch_name in enumerate(self.ch_names): + for f_name in self.settings.features.get_enabled(): + features_compute[f"{ch_name}_nolds_{f_name}_{data_str}"] = ( + self.calc_nolds_feature(f_name, data[ch_idx, :]) + if data[ch_idx, :].sum() + else 0 + ) + + return features_compute + + @staticmethod + def calc_nolds_feature(f_name: str, dat: np.ndarray): + import nolds + + match f_name: + case "sample_entropy": + return nolds.sampen(dat) + case "correlation_dimension": + return nolds.corr_dim(dat, emb_dim=2) + case "lyapunov_exponent": + return nolds.lyap_r(dat) + case "hurst_exponent": + return nolds.hurst_rs(dat) + case "detrended_fluctuation_analysis": + return nolds.dfa(dat) + case _: + raise ValueError(f"Invalid nolds feature name: {f_name}")
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_normalization.html b/_modules/nm_normalization.html new file mode 100644 index 00000000..de039b1b --- /dev/null +++ b/_modules/nm_normalization.html @@ -0,0 +1,658 @@ + + + + + + + + + + nm_normalization — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_normalization

+"""Module for real-time data normalization."""
+
+import numpy as np
+from typing import TYPE_CHECKING, Callable, Literal, get_args
+
+from py_neuromodulation.nm_types import NMBaseModel, Field, NormMethod
+from py_neuromodulation.nm_preprocessing import NMPreprocessor
+
+if TYPE_CHECKING:
+    from py_neuromodulation.nm_settings import NMSettings
+
+NormalizerType = Literal["raw", "feature"]
+
+
+
+[docs] +class NormalizationSettings(NMBaseModel): + normalization_time_s: float = 30 + normalization_method: NormMethod = "zscore" + clip: float = Field(default=3, ge=0) + + @staticmethod + def list_normalization_methods() -> list[NormMethod]: + return list(get_args(NormMethod))
+ + + +
+[docs] +class Normalizer(NMPreprocessor): + def __init__( + self, + sfreq: float, + settings: "NMSettings", + type: NormalizerType, + ) -> None: + """Normalize raw data. + + normalize_samples : int + number of past samples considered for normalization + sample_add : int + number of samples to add to previous + method : str | default is 'mean' + data is normalized via subtraction of the 'mean' or 'median' and + subsequent division by the 'mean' or 'median'. For z-scoring enter + 'zscore'. + clip : float, optional + value at which to clip after normalization + """ + + self.type = type + self.settings: NormalizationSettings + + match self.type: + case "raw": + self.settings = settings.raw_normalization_settings.validate() + self.add_samples = int(sfreq / settings.sampling_rate_features_hz) + case "feature": + self.settings = settings.feature_normalization_settings.validate() + self.add_samples = 0 + + # For type = "feature" sfreq = sampling_rate_features_hz + self.num_samples_normalize = int(self.settings.normalization_time_s * sfreq) + + self.previous: np.ndarray = np.empty((0, 0)) # Default empty array + + self.method = self.settings.normalization_method + self.using_sklearn = self.method in ["quantile", "power", "robust", "minmax"] + + if self.using_sklearn: + import sklearn.preprocessing as skpp + + NORM_METHODS_SKLEARN: dict[NormMethod, Callable] = { + "quantile": lambda: skpp.QuantileTransformer(n_quantiles=300), + "robust": skpp.RobustScaler, + "minmax": skpp.MinMaxScaler, + "power": skpp.PowerTransformer, + } + + self.normalizer = norm_sklearn(NORM_METHODS_SKLEARN[self.method]()) + + else: + NORM_FUNCTIONS = { + "mean": norm_mean, + "median": norm_median, + "zscore": norm_zscore, + "zscore-median": norm_zscore_median, + } + self.normalizer = NORM_FUNCTIONS[self.method] + + def process(self, data: np.ndarray) -> np.ndarray: + # TODO: does feature normalization need to be transposed too? + if self.type == "raw": + data = data.T + + if self.previous.size == 0: # Check if empty + self.previous = data + return data if self.type == "raw" else data.T + + self.previous = np.vstack((self.previous, data[-self.add_samples :])) + + data = self.normalizer(data, self.previous) + + if self.settings.clip: + data = data.clip(min=-self.settings.clip, max=self.settings.clip) + + + self.previous = self.previous[-self.num_samples_normalize + 1 :] + + data = np.nan_to_num(data) + + return data if self.type == "raw" else data.T
+ + + +
+[docs] +class RawNormalizer(Normalizer): + def __init__(self, sfreq: float, settings: "NMSettings") -> None: + super().__init__(sfreq, settings, "raw")
+ + + +
+[docs] +class FeatureNormalizer(Normalizer): + def __init__(self, settings: "NMSettings") -> None: + super().__init__(settings.sampling_rate_features_hz, settings, "feature")
+ + + +""" Functions to check for NaN's before deciding which Numpy function to call """ + + +def nan_mean(data: np.ndarray, axis: int) -> np.ndarray: + return ( + np.nanmean(data, axis=axis) + if np.any(np.isnan(sum(data))) + else np.mean(data, axis=axis) + ) + + +def nan_std(data: np.ndarray, axis: int) -> np.ndarray: + return ( + np.nanstd(data, axis=axis) + if np.any(np.isnan(sum(data))) + else np.std(data, axis=axis) + ) + + +def nan_median(data: np.ndarray, axis: int) -> np.ndarray: + return ( + np.nanmedian(data, axis=axis) + if np.any(np.isnan(sum(data))) + else np.median(data, axis=axis) + ) + + +def norm_mean(current, previous): + mean = nan_mean(previous, axis=0) + return (current - mean) / mean + + +def norm_median(current, previous): + median = nan_median(previous, axis=0) + return (current - median) / median + + +def norm_zscore(current, previous): + std = nan_std(previous, axis=0) + std[std == 0] = 1 # same behavior as sklearn + return (current - nan_mean(previous, axis=0)) / std + + +def norm_zscore_median(current, previous): + std = nan_std(previous, axis=0) + std[std == 0] = 1 # same behavior as sklearn + return (current - nan_median(previous, axis=0)) / std + + +def norm_sklearn(sknormalizer): + # For the following methods we check for the shape of current + # when current is a 1D array, then it is the post-processing normalization, + # and we need to expand, and remove the extra dimension afterwards + # When current is a 2D array, then it is pre-processing normalization, and + # there's no need for expanding. + + def sk_normalizer(current, previous): + return ( + sknormalizer.fit(np.nan_to_num(previous)) + .transform( + # if post-processing: pad dimensions to 2 + np.reshape(current, (2 - len(current.shape)) * (1,) + current.shape) + ) + .squeeze() # if post-processing: remove extra dimension # type: ignore + ) + + return sk_normalizer +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_oscillatory.html b/_modules/nm_oscillatory.html new file mode 100644 index 00000000..d743d27a --- /dev/null +++ b/_modules/nm_oscillatory.html @@ -0,0 +1,885 @@ + + + + + + + + + + nm_oscillatory — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_oscillatory

+from collections.abc import Iterable
+import numpy as np
+from itertools import product
+
+from py_neuromodulation.nm_types import NMBaseModel
+from pydantic import field_validator
+from typing import TYPE_CHECKING
+
+from py_neuromodulation.nm_features import NMFeature
+from py_neuromodulation.nm_types import BoolSelector
+
+if TYPE_CHECKING:
+    from py_neuromodulation.nm_settings import NMSettings
+    from py_neuromodulation.nm_kalmanfilter import KalmanSettings
+
+
+class OscillatoryFeatures(BoolSelector):
+    mean: bool = True
+    median: bool = False
+    std: bool = False
+    max: bool = False
+
+
+class OscillatorySettings(NMBaseModel):
+    windowlength_ms: int = 1000
+    log_transform: bool = True
+    features: OscillatoryFeatures = OscillatoryFeatures(
+        mean=True, median=False, std=False, max=False
+    )
+    return_spectrum: bool = False
+
+
+ESTIMATOR_DICT = {
+    "mean": np.nanmean,
+    "median": np.nanmedian,
+    "std": np.nanstd,
+    "max": np.nanmax,
+}
+
+
+class OscillatoryFeature(NMFeature):
+    def __init__(
+        self, settings: "NMSettings", ch_names: Iterable[str], sfreq: int
+    ) -> None:
+        settings.validate()
+        self.settings: OscillatorySettings  # Assignment in subclass __init__
+        self.osc_feature_name: str  # Required for output
+
+        self.sfreq = int(sfreq)
+        self.ch_names = ch_names
+
+        self.frequency_ranges = settings.frequency_ranges_hz
+
+        # Test settings
+        assert self.settings.windowlength_ms <= settings.segment_length_features_ms, (
+            f"oscillatory feature windowlength_ms = ({self.settings.windowlength_ms})"
+            f"needs to be smaller than"
+            f"settings['segment_length_features_ms'] = {settings.segment_length_features_ms}",
+        )
+
+
+
+[docs] +class FFT(OscillatoryFeature): + def __init__( + self, + settings: "NMSettings", + ch_names: Iterable[str], + sfreq: int, + ) -> None: + from scipy.fft import rfftfreq + + self.osc_feature_name = "fft" + self.settings = settings.fft_settings + # super.__init__ needs osc_feature_name and settings + super().__init__(settings, ch_names, sfreq) + + window_ms = self.settings.windowlength_ms + + self.window_samples = int(-np.floor(window_ms / 1000 * sfreq)) + self.freqs = rfftfreq(-self.window_samples, 1 / np.floor(self.sfreq)) + + # Pre-calculate frequency ranges + self.idx_range = [ + ( + f_band, + np.where((self.freqs >= f_range[0]) & (self.freqs < f_range[1]))[0], + ) + for f_band, f_range in self.frequency_ranges.items() + ] + + self.estimators = [ + (est, ESTIMATOR_DICT[est]) for est in self.settings.features.get_enabled() + ] + +
+[docs] + def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict: + data = data[:, self.window_samples :] + + from scipy.fft import rfft + + Z = np.abs(rfft(data)) # type: ignore + + if self.settings.log_transform: + Z = np.log10(Z) + + for f_band_name, idx_range in self.idx_range: + # TODO Can we get rid of this for-loop? Hard to vectorize windows of different lengths... + Z_band = Z[:, idx_range] # Data for all channels + + for est_name, est_fun in self.estimators: + result = est_fun(Z_band, axis=1) + + for ch_idx, ch_name in enumerate(self.ch_names): + features_compute[ + f"{ch_name}_{self.osc_feature_name}_{f_band_name}_{est_name}" + ] = result[ch_idx] + + if self.settings.return_spectrum: + combinations = product(enumerate(self.ch_names), enumerate(self.freqs)) + for (ch_idx, ch_name), (idx, f) in combinations: + features_compute[f"{ch_name}_fft_psd_{int(f)}"] = Z[ch_idx][idx] + + return features_compute
+
+ + + +class Welch(OscillatoryFeature): + def __init__( + self, + settings: "NMSettings", + ch_names: Iterable[str], + sfreq: int, + ) -> None: + from scipy.fft import rfftfreq + + self.osc_feature_name = "welch" + self.settings = settings.welch_settings + # super.__init__ needs osc_feature_name and settings + super().__init__(settings, ch_names, sfreq) + + self.freqs = rfftfreq(self.sfreq, 1 / self.sfreq) + + self.idx_range = [ + ( + f_band, + np.where((self.freqs >= f_range[0]) & (self.freqs < f_range[1]))[0], + ) + for f_band, f_range in self.frequency_ranges.items() + ] + + self.estimators = [ + (est, ESTIMATOR_DICT[est]) for est in self.settings.features.get_enabled() + ] + + def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict: + from scipy.signal import welch + + _, Z = welch( + data, + fs=self.sfreq, + window="hann", + nperseg=self.sfreq, + noverlap=None, + ) + + if self.settings.log_transform: + Z = np.log10(Z) + + for f_band_name, idx_range in self.idx_range: + Z_band = Z[:, idx_range] + + for est_name, est_fun in self.estimators: + result = est_fun(Z_band, axis=1) + + for ch_idx, ch_name in enumerate(self.ch_names): + features_compute[ + f"{ch_name}_{self.osc_feature_name}_{f_band_name}_{est_name}" + ] = result[ch_idx] + + if self.settings.return_spectrum: + combinations = product(enumerate(self.ch_names), enumerate(self.freqs)) + for (ch_idx, ch_name), (idx, f) in combinations: + features_compute[f"{ch_name}_welch_psd_{str(f)}"] = Z[ch_idx][idx] + + return features_compute + + +
+[docs] +class STFT(OscillatoryFeature): + def __init__( + self, + settings: "NMSettings", + ch_names: Iterable[str], + sfreq: int, + ) -> None: + from scipy.fft import rfftfreq + + self.osc_feature_name = "stft" + self.settings = settings.stft_settings + # super.__init__ needs osc_feature_name and settings + super().__init__(settings, ch_names, sfreq) + + self.nperseg = self.settings.windowlength_ms + + self.freqs = rfftfreq(self.nperseg, 1 / self.sfreq) + + self.idx_range = [ + ( + f_band, + np.where((self.freqs >= f_range[0]) & (self.freqs <= f_range[1]))[0], + ) + for f_band, f_range in self.frequency_ranges.items() + ] + + self.estimators = [ + (est, ESTIMATOR_DICT[est]) for est in self.settings.features.get_enabled() + ] + +
+[docs] + def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict: + from scipy.signal import stft + + _, _, Zxx = stft( + data, + fs=self.sfreq, + window="hamming", + nperseg=self.nperseg, + boundary="even", + ) + + Z = np.abs(Zxx) + if self.settings.log_transform: + Z = np.log10(Z) + + for f_band_name, idx_range in self.idx_range: + Z_band = Z[:, idx_range, :] + + for est_name, est_fun in self.estimators: + result = est_fun(Z_band, axis=(1, 2)) + + for ch_idx, ch_name in enumerate(self.ch_names): + features_compute[ + f"{ch_name}_{self.osc_feature_name}_{f_band_name}_{est_name}" + ] = result[ch_idx] + + if self.settings.return_spectrum: + combinations = product(enumerate(self.ch_names), enumerate(self.freqs)) + for (ch_idx, ch_name), (idx, f) in combinations: + features_compute[f"{ch_name}_stft_psd_{str(f)}"] = Z[ch_idx].mean( + axis=1 + )[idx] + + return features_compute
+
+ + + +class BandpowerFeatures(BoolSelector): + activity: bool = True + mobility: bool = False + complexity: bool = False + + +################################### +######## BANDPOWER FEATURE ######## +################################### + + +class BandpassSettings(NMBaseModel): + segment_lengths_ms: dict[str, int] = { + "theta": 1000, + "alpha": 500, + "low beta": 333, + "high beta": 333, + "low gamma": 100, + "high gamma": 100, + "HFA": 100, + } + bandpower_features: BandpowerFeatures = BandpowerFeatures() + log_transform: bool = True + kalman_filter: bool = False + + @field_validator("bandpower_features") + @classmethod + def bandpower_features_validator(cls, bandpower_features: BandpowerFeatures): + assert ( + len(bandpower_features.get_enabled()) > 0 + ), "Set at least one bandpower_feature to True." + + return bandpower_features + + def validate_fbands(self, settings: "NMSettings") -> None: + for fband_name, seg_length_fband in self.segment_lengths_ms.items(): + assert seg_length_fband <= settings.segment_length_features_ms, ( + f"segment length {seg_length_fband} needs to be smaller than " + f" settings['segment_length_features_ms'] = {settings.segment_length_features_ms}" + ) + + for fband_name in settings.frequency_ranges_hz.keys(): + assert fband_name in self.segment_lengths_ms, ( + f"frequency range {fband_name} " + "needs to be defined in settings.bandpass_filter_settings.segment_lengths_ms]" + ) + + +
+[docs] +class BandPower(NMFeature): + def __init__( + self, + settings: "NMSettings", + ch_names: Iterable[str], + sfreq: float, + use_kf: bool | None = None, + ) -> None: + settings.validate() + + self.bp_settings: BandpassSettings = settings.bandpass_filter_settings + self.kalman_filter_settings: KalmanSettings = settings.kalman_filter_settings + self.sfreq = sfreq + self.ch_names = ch_names + self.KF_dict: dict = {} + + from py_neuromodulation.nm_filter import MNEFilter + + self.bandpass_filter = MNEFilter( + f_ranges=[ + tuple(frange) for frange in settings.frequency_ranges_hz.values() + ], + sfreq=self.sfreq, + filter_length=self.sfreq - 1, + verbose=False, + ) + + if use_kf or (use_kf is None and self.bp_settings.kalman_filter): + self.init_KF("bandpass_activity") + + seglengths = self.bp_settings.segment_lengths_ms + + self.feature_params = [] + for ch_idx, ch_name in enumerate(self.ch_names): + for f_band_idx, f_band in enumerate(settings.frequency_ranges_hz.keys()): + seglength_ms = seglengths[f_band] + seglen = int(np.floor(self.sfreq / 1000 * seglength_ms)) + for bp_feature in self.bp_settings.bandpower_features.get_enabled(): + feature_name = "_".join([ch_name, "bandpass", bp_feature, f_band]) + self.feature_params.append( + ( + ch_idx, + f_band_idx, + seglen, + bp_feature, + feature_name, + ) + ) + + def init_KF(self, feature: str) -> None: + from py_neuromodulation.nm_kalmanfilter import define_KF + + for f_band in self.kalman_filter_settings.frequency_bands: + for channel in self.ch_names: + self.KF_dict["_".join([channel, feature, f_band])] = define_KF( + self.kalman_filter_settings.Tp, + self.kalman_filter_settings.sigma_w, + self.kalman_filter_settings.sigma_v, + ) + + def update_KF(self, feature_calc: np.floating, KF_name: str) -> np.floating: + if KF_name in self.KF_dict: + self.KF_dict[KF_name].predict() + self.KF_dict[KF_name].update(feature_calc) + feature_calc = self.KF_dict[KF_name].x[0] + return feature_calc + +
+[docs] + def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict: + data = self.bandpass_filter.filter_data(data) + + for ( + ch_idx, + f_band_idx, + seglen, + bp_feature, + feature_name, + ) in self.feature_params: + features_compute[feature_name] = self.calc_bp_feature( + bp_feature, feature_name, data[ch_idx, f_band_idx, -seglen:] + ) + + return features_compute
+ + + def calc_bp_feature(self, bp_feature, feature_name, data): + match bp_feature: + case "activity": + feature_calc = np.var(data) + if self.bp_settings.log_transform: + feature_calc = np.log10(feature_calc) + if self.KF_dict: + feature_calc = self.update_KF(feature_calc, feature_name) + case "mobility": + feature_calc = np.sqrt(np.var(np.diff(data)) / np.var(data)) + case "complexity": + feature_calc = self.calc_complexity(data) + case _: + raise ValueError(f"Unknown bandpower feature: {bp_feature}") + + return np.nan_to_num(feature_calc) + + @staticmethod + def calc_complexity(data: np.ndarray) -> float: + dat_deriv = np.diff(data) + deriv_variance = np.var(dat_deriv) + mobility = np.sqrt(deriv_variance / np.var(data)) + dat_deriv_2_var = np.var(np.diff(dat_deriv)) + deriv_mobility = np.sqrt(dat_deriv_2_var / deriv_variance) + + return deriv_mobility / mobility
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_plots.html b/_modules/nm_plots.html new file mode 100644 index 00000000..bf546ca1 --- /dev/null +++ b/_modules/nm_plots.html @@ -0,0 +1,1051 @@ + + + + + + + + + + nm_plots — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_plots

+import numpy as np
+import pandas as pd
+from scipy.stats import zscore as scipy_zscore
+from matplotlib import pyplot as plt
+from matplotlib import gridspec
+import seaborn as sb
+from pathlib import PurePath
+
+from py_neuromodulation.nm_types import _PathLike
+from py_neuromodulation import logger
+
+
+def plot_df_subjects(
+    df,
+    x_col="sub",
+    y_col="performance_test",
+    hue=None,
+    title="channel specific performances",
+    PATH_SAVE: _PathLike = "",
+    figsize_tuple: tuple[float, float] = (5, 3),
+):
+    alpha_box = 0.4
+    plt.figure(figsize=figsize_tuple, dpi=300)
+    sb.boxplot(
+        x=x_col,
+        y=y_col,
+        hue=hue,
+        data=df,
+        palette="viridis",
+        showmeans=False,
+        boxprops=dict(alpha=alpha_box),
+        showcaps=True,
+        showbox=True,
+        showfliers=False,
+        notch=False,
+        whiskerprops={"linewidth": 2, "zorder": 10, "alpha": alpha_box},
+        capprops={"alpha": alpha_box},
+        medianprops=dict(linestyle="-", linewidth=5, color="gray", alpha=alpha_box),
+    )
+
+    ax = sb.stripplot(
+        x=x_col,
+        y=y_col,
+        hue=hue,
+        data=df,
+        palette="viridis",
+        dodge=True,
+        s=5,
+    )
+
+    if hue is not None:
+        n_hues = df[hue].nunique()
+
+        handles, labels = ax.get_legend_handles_labels()
+        plt.legend(
+            handles[0:n_hues],
+            labels[0:n_hues],
+            bbox_to_anchor=(1.05, 1),
+            loc=2,
+            title=hue,
+            borderaxespad=0.0,
+        )
+    plt.title(title)
+    plt.ylabel(y_col)
+    plt.xticks(rotation=90)
+    if PATH_SAVE:
+        plt.savefig(
+            PATH_SAVE,
+            bbox_inches="tight",
+        )
+    # plt.show()
+    return plt.gca()
+
+
+def plot_epoch(
+    X_epoch: np.ndarray,
+    y_epoch: np.ndarray,
+    feature_names: list,
+    z_score: bool | None = None,
+    epoch_len: int = 4,
+    sfreq: int = 10,
+    str_title: str = "",
+    str_label: str = "",
+    ytick_labelsize: float | None = None,
+):
+    if z_score is None:
+        X_epoch = scipy_zscore(
+            np.nan_to_num(np.nanmean(np.squeeze(X_epoch), axis=0)),
+            axis=0,
+            nan_policy="omit",
+        ).T
+    y_epoch = np.stack([np.array(y_epoch)])
+    plt.figure(figsize=(6, 6))
+    plt.subplot(211)
+    plt.imshow(X_epoch, aspect="auto")
+    plt.yticks(np.arange(0, len(feature_names), 1), feature_names, size=ytick_labelsize)
+    plt.xticks(
+        np.arange(0, X_epoch.shape[1], 1),
+        np.round(np.arange(-epoch_len / 2, epoch_len / 2, 1 / sfreq), 2),
+        rotation=90,
+    )
+    plt.gca().invert_yaxis()
+    plt.xlabel("Time [s]")
+    plt.title(str_title)
+
+    plt.subplot(212)
+    for i in range(y_epoch.shape[0]):
+        plt.plot(y_epoch[i, :], color="black", alpha=0.4)
+    plt.plot(
+        y_epoch.mean(axis=0),
+        color="black",
+        alpha=1,
+        linewidth=3.0,
+        label="mean target",
+    )
+    plt.legend()
+    plt.ylabel("Target")
+    plt.title(str_label)
+    plt.xticks(
+        np.arange(0, X_epoch.shape[1], 1),
+        np.round(np.arange(-epoch_len / 2, epoch_len / 2, 1 / sfreq), 2),
+        rotation=90,
+    )
+    plt.xlabel("Time [s]")
+    plt.tight_layout()
+
+
+def reg_plot(
+    x_col: str, y_col: str, data: pd.DataFrame, out_path_save: str | None = None
+):
+    
+    from py_neuromodulation.nm_stats import permutationTestSpearmansRho
+    
+    plt.figure(figsize=(4, 4), dpi=300)
+    rho, p = permutationTestSpearmansRho(
+        data[x_col],
+        data[y_col],
+        False,
+        "R^2",
+        5000,
+    )
+    sb.regplot(x=x_col, y=y_col, data=data)
+    plt.title(f"{y_col}~{x_col} p={np.round(p, 2)} rho={np.round(rho, 2)}")
+
+    if out_path_save is not None:
+        plt.savefig(
+            out_path_save,
+            bbox_inches="tight",
+        )
+
+
+
+[docs] +def plot_bar_performance_per_channel( + ch_names, + performances: dict, + PATH_OUT: _PathLike, + sub: str | None = None, + save_str: str = "ch_comp_bar_plt.png", + performance_metric: str = "Balanced Accuracy", +): + """ + performances dict is output of ml_decode + """ + plt.figure(figsize=(4, 3), dpi=300) + if sub is None: + sub = list(performances.keys())[0] + plt.bar( + np.arange(len(ch_names)), + [performances[sub][p]["performance_test"] for p in performances[sub]], + ) + plt.xticks(np.arange(len(ch_names)), ch_names, rotation=90) + plt.xlabel("channels") + plt.ylabel(performance_metric) + plt.savefig( + PurePath(PATH_OUT, save_str), + bbox_inches="tight", + ) + plt.close()
+ + + +def plot_corr_matrix( + feature: pd.DataFrame, + feature_file: _PathLike = "", + ch_name: str = "", + feature_names: list[str] = [], + show_plot=True, + OUT_PATH: _PathLike = "", + feature_name_plt="Features_corr_matr", + save_plot: bool = False, + save_plot_name: str = "", + figsize: tuple[float, float] = (7, 7), + title: str = "", + cbar_vmin: float = -1, + cbar_vmax: float = 1.0, +): + # cut out channel name for each column + if not ch_name: + feature_col_name = [ + i[len(ch_name) + 1 :] for i in feature_names if ch_name in i + ] + else: + feature_col_name = feature.columns + + plt.figure(figsize=figsize) + if ( + len(feature_names) > 0 + ): # Checking length to accomodate for tests passing a pandas Index + corr = feature[feature_names].corr() + else: + corr = feature.corr() + sb.heatmap( + corr, + xticklabels=feature_col_name, + yticklabels=feature_col_name, + vmin=cbar_vmin, + vmax=cbar_vmax, + cmap="viridis", + ) + if not title: + if ch_name: + plt.title("Correlation matrix features channel: " + str(ch_name)) + else: + plt.title("Correlation matrix") + else: + plt.title(title) + + # if len(feature_col_name) > 50: + # plt.xticks([]) + # plt.yticks([]) + + if save_plot: + plt_path = ( + PurePath(OUT_PATH, save_plot_name) + if save_plot_name + else get_plt_path( + OUT_PATH=OUT_PATH, + feature_file=feature_file, + ch_name=ch_name, + str_plt_type=feature_name_plt, + feature_name="_".join(feature_names), + ) + ) + + plt.savefig(plt_path, bbox_inches="tight") + logger.info(f"Correlation matrix figure saved to {plt_path}") + + if not show_plot: + plt.close() + + plt.tight_layout() + + return plt.gca() + + +def plot_feature_series_time(features) -> None: + plt.imshow(features.T, aspect="auto") + + +
+[docs] +def get_plt_path( + OUT_PATH: _PathLike = "", + feature_file: str = "", + ch_name: str = "", + str_plt_type: str = "", + feature_name: str = "", +) -> _PathLike: + """[summary] + + Parameters + ---------- + OUT_PATH : str, optional + folder of preprocessed runs, by default None + feature_file : str, optional + run_name, by default None + ch_name : str, optional + ch_name, by default None + str_plt_type : str, optional + type of plot, e.g. mov_avg_feature or corr_matr, by default None + feature_name : str, optional + e.g. bandpower, stft, sharpwave_prominence, by default None + """ + filename = ( + str_plt_type + + (("_ch_" + ch_name) if ch_name else "") + + (("_" + feature_name) if feature_name else "") + + ".png" + ) + + return PurePath(OUT_PATH, feature_file, filename)
+ + + +def plot_epochs_avg( + X_epoch: np.ndarray, + y_epoch: np.ndarray, + epoch_len: int, + sfreq: int, + feature_names: list[str] = [], + feature_str_add: str = "", + cut_ch_name_cols: bool = True, + ch_name: str = "", + label_name: str = "", + normalize_data: bool = True, + show_plot: bool = True, + save: bool = False, + OUT_PATH: _PathLike = "", + feature_file: str = "", + str_title: str = "Movement aligned features", + ytick_labelsize=None, + figsize_x: float = 8, + figsize_y: float = 8, +) -> None: + # cut channel name of for axis + "_" for more dense plot + if not feature_names: + if cut_ch_name_cols and None not in (ch_name, feature_names): + feature_names = [ + i[len(ch_name) + 1 :] for i in list(feature_names) if ch_name in i + ] + + if normalize_data: + X_epoch_mean = scipy_zscore( + np.nanmean(np.squeeze(X_epoch), axis=0), axis=0, nan_policy="omit" + ).T + else: + X_epoch_mean = np.nanmean(np.squeeze(X_epoch), axis=0).T + + if len(X_epoch_mean.shape) == 1: + X_epoch_mean = np.expand_dims(X_epoch_mean, axis=0) + + plt.figure(figsize=(figsize_x, figsize_y)) + gs = gridspec.GridSpec(2, 1, height_ratios=[2.5, 1]) + plt.subplot(gs[0]) + plt.imshow(X_epoch_mean, aspect="auto") + plt.yticks(np.arange(0, len(feature_names), 1), feature_names, size=ytick_labelsize) + plt.xticks( + np.arange(0, X_epoch.shape[1], int(X_epoch.shape[1] / 10)), + np.round(np.arange(-epoch_len / 2, epoch_len / 2, epoch_len / 10), 2), + rotation=90, + ) + plt.xlabel("Time [s]") + str_title = str_title + if ch_name: + str_title += f" channel: {ch_name}" + plt.title(str_title) + + plt.subplot(gs[1]) + for i in range(y_epoch.shape[0]): + plt.plot(y_epoch[i, :], color="black", alpha=0.4) + plt.plot( + y_epoch.mean(axis=0), + color="black", + alpha=1, + linewidth=3.0, + label="mean target", + ) + plt.legend() + plt.ylabel("Target") + plt.title(label_name) + plt.xticks( + np.arange(0, X_epoch.shape[1], int(X_epoch.shape[1] / 10)), + np.round(np.arange(-epoch_len / 2, epoch_len / 2, epoch_len / 10), 2), + rotation=90, + ) + plt.xlabel("Time [s]") + plt.tight_layout() + + if save: + plt_path = get_plt_path( + OUT_PATH, + feature_file, + ch_name, + str_plt_type="MOV_aligned_features", + feature_name=feature_str_add, + ) + plt.savefig(plt_path, bbox_inches="tight") + logger.info(f"Feature epoch average figure saved to: {str(plt_path)}") + if not show_plot: + plt.close() + + +def plot_grid_elec_3d( + cortex_grid: np.ndarray | None = None, + ecog_strip: np.ndarray | None = None, + grid_color: np.ndarray | None = None, + strip_color: np.ndarray | None = None, +): + ax = plt.axes(projection="3d") + + if cortex_grid is not None: + grid_color = np.ones(cortex_grid.shape[0]) if grid_color is None else grid_color + _ = ax.scatter3D( + cortex_grid[:, 0], + cortex_grid[:, 1], + cortex_grid[:, 2], + c=grid_color, + s=300, + alpha=0.8, + cmap="viridis", + ) + + if ecog_strip is not None: + strip_color = ( + np.ones(ecog_strip.shape[0]) if strip_color is None else strip_color + ) + _ = ax.scatter( + ecog_strip[:, 0], + ecog_strip[:, 1], + ecog_strip[:, 2], + c=strip_color, + s=500, # Bug? Third argument is s, what is this value? + alpha=0.8, + cmap="gray", + marker="o", + ) + + +def plot_all_features( + df: pd.DataFrame, + time_limit_low_s: float | None = None, + time_limit_high_s: float | None = None, + normalize: bool = True, + ytick_labelsize: int = 4, + clim_low: float | None = None, + clim_high: float | None = None, + save: bool = False, + title="all_feature_plt.pdf", + OUT_PATH: _PathLike = "", + feature_file: str = "", +): + if time_limit_high_s is not None: + df = df[df["time"] < time_limit_high_s * 1000] + if time_limit_low_s is not None: + df = df[df["time"] > time_limit_low_s * 1000] + + cols_plt = [c for c in df.columns if c != "time"] + if normalize: + data_plt = scipy_zscore(df[cols_plt], nan_policy="omit") + else: + data_plt = df[cols_plt] + + plt.figure() # figsize=(7, 5), dpi=300 + plt.imshow(data_plt.T, aspect="auto") + plt.xlabel("Time [s]") + plt.ylabel("Feature Names") + plt.yticks(np.arange(len(cols_plt)), cols_plt, size=ytick_labelsize) + + tick_num = np.arange(0, df.shape[0], int(df.shape[0] / 10)) + tick_labels = np.array(np.rint(df["time"].iloc[tick_num] / 1000), dtype=int) + plt.xticks(tick_num, tick_labels) + + plt.title(f"Feature Plot {feature_file}") + + if clim_low is not None: + plt.clim(vmin=clim_low) + if clim_high is not None: + plt.clim(vmax=clim_high) + + plt.colorbar() + plt.tight_layout() + + if save: + plt_path = PurePath(OUT_PATH, feature_file, title) + plt.savefig(plt_path, bbox_inches="tight") + + +class NM_Plot: + def __init__( + self, + ecog_strip: np.ndarray | None = None, + grid_cortex: np.ndarray | None = None, + grid_subcortex: np.ndarray | None = None, + sess_right: bool | None = False, + proj_matrix_cortex: np.ndarray | None = None, + ) -> None: + self.grid_cortex = grid_cortex + self.grid_subcortex = grid_subcortex + self.ecog_strip = ecog_strip + self.sess_right = sess_right + self.proj_matrix_cortex = proj_matrix_cortex + + from py_neuromodulation.nm_IO import read_plot_modules + + ( + self.faces, + self.vertices, + self.grid, + self.stn_surf, + self.x_ver, + self.y_ver, + self.x_ecog, + self.y_ecog, + self.z_ecog, + self.x_stn, + self.y_stn, + self.z_stn, + ) = read_plot_modules() + + def plot_grid_elec_3d(self) -> None: + plot_grid_elec_3d(np.array(self.grid_cortex), np.array(self.ecog_strip)) + + def plot_cortex( + self, + grid_cortex: np.ndarray | pd.DataFrame | None = None, + grid_color: np.ndarray | None = None, + ecog_strip: np.ndarray | None = None, + strip_color: np.ndarray | None = None, + sess_right: bool | None = None, + save: bool = False, + OUT_PATH: _PathLike = "", + feature_file: str = "", + feature_str_add: str = "", + show_plot: bool = True, + title: str = "Cortical grid", + set_clim: bool = True, + lower_clim: float = 0.5, + upper_clim: float = 0.7, + cbar_label: str = "Balanced Accuracy", + ): + """Plot MNI brain including selected MNI cortical projection grid + used strip ECoG electrodes + Colorcoded by grid_color + """ + + if grid_cortex is None: + if type(self.grid_cortex) is pd.DataFrame: + grid_cortex = np.array(self.grid_cortex) + else: + grid_cortex = self.grid_cortex + + if ecog_strip is None: + ecog_strip = self.ecog_strip + + if sess_right: + grid_cortex[0, :] = grid_cortex[0, :] * -1 # type: ignore # Handled above + + fig, axes = plt.subplots(1, 1, facecolor=(1, 1, 1), figsize=(14, 9)) + axes.scatter(self.x_ecog, self.y_ecog, c="gray", s=0.01) + axes.axes.set_aspect("equal", anchor="C") + + if grid_cortex is not None: + grid_color = ( + np.ones(grid_cortex.shape[0]) if grid_color is None else grid_color + ) + + pos_ecog = axes.scatter( + grid_cortex[:, 0], + grid_cortex[:, 1], + c=grid_color, + s=150, + alpha=0.8, + cmap="viridis", + label="grid points", + ) + if set_clim: + pos_ecog.set_clim(lower_clim, upper_clim) + if ecog_strip is not None: + strip_color = ( + np.ones(ecog_strip.shape[0]) if strip_color is None else strip_color + ) + + pos_ecog = axes.scatter( + ecog_strip[:, 0], + ecog_strip[:, 1], + c=strip_color, + s=400, + alpha=0.8, + cmap="viridis", + marker="x", + label="ecog electrode", + ) + plt.axis("off") + plt.legend() + plt.title(title) + if set_clim: + pos_ecog.set_clim(lower_clim, upper_clim) + cbar = fig.colorbar(pos_ecog) + cbar.set_label(cbar_label) + + if save: + plt_path = get_plt_path( + OUT_PATH, + feature_file, + str_plt_type="PLOT_CORTEX", + feature_name=feature_str_add, + ) + plt.savefig(plt_path, bbox_inches="tight") + logger.info(f"Feature epoch average figure saved to: {str(plt_path)}") + if not show_plot: + plt.close() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_projection.html b/_modules/nm_projection.html new file mode 100644 index 00000000..c629c3d5 --- /dev/null +++ b/_modules/nm_projection.html @@ -0,0 +1,848 @@ + + + + + + + + + + nm_projection — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_projection

+import numpy as np
+import pandas as pd
+from pydantic import Field
+from py_neuromodulation.nm_types import NMBaseModel
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from py_neuromodulation.nm_settings import NMSettings
+
+
+class ProjectionSettings(NMBaseModel):
+    max_dist_mm: float = Field(default=20.0, gt=0.0)
+
+
+
+[docs] +class Projection: + def __init__( + self, + settings: "NMSettings", + grid_cortex: pd.DataFrame, + grid_subcortex: pd.DataFrame, + coords: dict, + nm_channels: pd.DataFrame, + plot_projection: bool = False, + ) -> None: + self.grid_cortex = grid_cortex + self.grid_subcortex = grid_subcortex + self.coords = coords + self.nm_channels = nm_channels + self.project_cortex = settings.postprocessing.project_cortex + self.project_subcortex = settings.postprocessing.project_subcortex + self.max_dist_cortex = settings.project_cortex_settings.max_dist_mm + self.max_dist_subcortex = settings.project_subcortex_settings.max_dist_mm + self.ecog_channels: list # None case never handled, no need for default value + self.lfp_channels: list # None case never handled, no need for default value + + self.idx_chs_ecog: list = [] # feature series indexes for ecog channels + self.names_chs_ecog: list = [] # feature series name of ecog features + self.idx_chs_lfp: list = [] # feature series indexes for lfp channels + self.names_chs_lfp: list = [] # feature series name of lfp features + self.feature_names: list | None = None + self.initialized: bool = False + + self.remove_not_used_ch_from_coords() # remove beforehand non used channels from coords + + if len(self.coords["cortex_left"]["positions"]) == 0: + self.sess_right = True + self.ecog_strip = self.coords["cortex_right"]["positions"] + self.ecog_strip_names = self.coords["cortex_right"]["ch_names"] + elif len(self.coords["cortex_right"]["positions"]) == 0: + self.sess_right = False + self.ecog_strip = self.coords["cortex_left"]["positions"] + self.ecog_strip_names = self.coords["cortex_left"]["ch_names"] + + if self.sess_right and len(self.coords["subcortex_right"]["positions"]) > 0: + self.lfp_elec = self.coords["subcortex_right"]["positions"] + self.lfp_elec_names = self.coords["subcortex_right"]["ch_names"] + elif ( + self.sess_right is False + and len(self.coords["subcortex_left"]["positions"]) > 0 + ): + self.lfp_elec = self.coords["subcortex_left"]["positions"] + self.lfp_elec_names = self.coords["subcortex_left"]["ch_names"] + + self._initialize_channels() + + ( + self.proj_matrix_cortex, + self.proj_matrix_subcortex, + ) = self.calc_projection_matrix() + + if self.project_cortex: + self.active_cortex_gridpoints = np.nonzero( + self.proj_matrix_cortex.sum(axis=1) + )[0] + if self.project_subcortex: + self.active_subcortex_gridpoints = np.nonzero( + self.proj_matrix_subcortex.sum(axis=1) + )[0] + + if plot_projection: + from py_neuromodulation.nm_plots import NM_Plot + + nmplotter = NM_Plot( + ecog_strip=self.ecog_strip, + grid_cortex=self.grid_cortex.to_numpy(), + grid_subcortex=self.grid_subcortex.to_numpy(), + sess_right=self.sess_right, + proj_matrix_cortex=self.proj_matrix_cortex, + ) + nmplotter.plot_cortex() + + def remove_not_used_ch_from_coords(self): + ch_not_used = self.nm_channels.query('(used==0) or (status=="bad")').name + if len(ch_not_used) > 0: + for ch in ch_not_used: + for key_ in self.coords: + for idx, ch_coords in enumerate(self.coords[key_]["ch_names"]): + if ch.startswith(ch_coords): + # delete index + self.coords[key_]["positions"] = np.delete( + self.coords[key_]["positions"], idx, axis=0 + ) + self.coords[key_]["ch_names"].remove(ch) + +
+[docs] + def calc_proj_matrix( + self, + max_dist: float, + grid: np.ndarray, + coord_array: np.ndarray, + ) -> np.ndarray: + """Calculate projection matrix.""" + + channels = coord_array.shape[0] + distance_matrix = np.zeros([grid.shape[1], channels]) + + for project_point in range(grid.shape[1]): + for channel in range(coord_array.shape[0]): + distance_matrix[project_point, channel] = np.linalg.norm( + grid[:, project_point] - coord_array[channel, :] + ) + + proj_matrix = np.zeros(distance_matrix.shape) + for grid_point in range(distance_matrix.shape[0]): + used_channels = np.where(distance_matrix[grid_point, :] < max_dist)[0] + + rec_distances = distance_matrix[grid_point, used_channels] + sum_distances: float = np.sum(1 / rec_distances) + + for _, used_channel in enumerate(used_channels): + proj_matrix[grid_point, used_channel] = ( + 1 / distance_matrix[grid_point, used_channel] + ) / sum_distances + return proj_matrix
+ + +
+[docs] + def calc_projection_matrix(self): + """Calculates a projection matrix based on the used coordinate arrays + + Returns + ------- + proj_matrix_cortex (np.array) + cortical projection_matrix in shape [grid contacts, channel contact] defaults to None + proj_matrix_subcortex (np.array) + subcortical projection_matrix in shape [grid contacts, channel contact] defaults to None + """ + + proj_matrix_run = np.empty(2, dtype=object) + + if self.sess_right: + if self.project_cortex: + cortex_grid_right = np.copy(self.grid_cortex) + cortex_grid_right[:, 0] = cortex_grid_right[:, 0] * -1 + self.cortex_grid_right = np.array(cortex_grid_right.T) + else: + self.cortex_grid_right = None + + if self.project_subcortex: + subcortex_grid_right = np.copy(self.grid_subcortex) + subcortex_grid_right[:, 0] = subcortex_grid_right[:, 0] * -1 + self.subcortex_grid_right = np.array(subcortex_grid_right).T + else: + self.subcortex_grid_right = None + + grid_session = [self.cortex_grid_right, self.subcortex_grid_right] + + else: + if self.project_cortex: + self.cortex_grid_left = np.array(self.grid_cortex.T) + else: + self.cortex_grid_left = None + if self.project_subcortex: + self.subcortex_grid_left = np.array(self.grid_subcortex.T) + else: + self.subcortex_grid_left = None + + grid_session = [self.cortex_grid_left, self.subcortex_grid_left] + + coord_array = [ + self.ecog_strip if grid_session[0] is not None else None, + self.lfp_elec if grid_session[1] is not None else None, + ] + + for loc_, grid in enumerate(grid_session): + if loc_ == 0: # cortex + max_dist = self.max_dist_cortex + elif loc_ == 1: # subcortex + max_dist = self.max_dist_subcortex + + if grid_session[loc_] is not None: + proj_matrix_run[loc_] = self.calc_proj_matrix( + max_dist, grid, coord_array[loc_] + ) + + return proj_matrix_run[0], proj_matrix_run[1] # cortex, subcortex
+ + + def _initialize_channels(self) -> None: + """Initialize channel names via nm_channel new_name column""" + + if self.project_cortex: + self.ecog_channels = self.nm_channels.query( + '(type=="ecog") and (used == 1) and (status=="good")' + ).name.to_list() + + chs_ecog = self.ecog_channels.copy() + for ecog_channel in chs_ecog: + if ecog_channel not in self.ecog_strip_names: + self.ecog_channels.remove(ecog_channel) + # write ecog_channels to be new_name + self.ecog_channels = list( + self.nm_channels.query("name == @self.ecog_channels").new_name + ) + + if self.project_subcortex: + self.lfp_channels = self.nm_channels.query( + '(type=="lfp" or type=="seeg" or type=="dbs") \ + and (used == 1) and (status=="good")' + ).name.to_list() + # project only channels that are in the coords + # this also deletes channels of the other hemisphere + chs_lfp = self.lfp_channels.copy() + for lfp_channel in chs_lfp: + if lfp_channel not in self.lfp_elec_names: + self.lfp_channels.remove(lfp_channel) + # write lfp_channels to be new_name + self.lfp_channels = list( + self.nm_channels.query("name == @self.lfp_channels").new_name + ) + +
+[docs] + def init_projection_run(self, feature_dict: dict) -> None: + """Initialize indexes for respective channels in feature series computed by nm_features.py""" + # here it is assumed that only one hemisphere is recorded at a time! + if self.project_cortex: + for ecog_channel in self.ecog_channels: + self.idx_chs_ecog.append( + [ + ch_idx + for ch_idx, ch in enumerate(feature_dict.keys()) + if ch.startswith(ecog_channel) + ] + ) + self.names_chs_ecog.append( + [ + ch + for _, ch in enumerate(feature_dict.keys()) + if ch.startswith(ecog_channel) + ] + ) + if self.names_chs_ecog: + # get feature_names; given by ECoG sequency of features + self.feature_names = [ + feature_name[len(self.ecog_channels[0]) + 1 :] + for feature_name in self.names_chs_ecog[0] + ] + + if self.project_subcortex: + # for lfp_channels select here only the ones from the correct hemisphere! + for lfp_channel in self.lfp_channels: + self.idx_chs_lfp.append( + [ + ch_idx + for ch_idx, ch in enumerate(feature_dict.keys()) + if ch.startswith(lfp_channel) + ] + ) + self.names_chs_lfp.append( + [ + ch + for _, ch in enumerate(feature_dict.keys()) + if ch.startswith(lfp_channel) + ] + ) + if not self.feature_names and self.names_chs_lfp: + # get feature_names; given by LFP sequency of features + self.feature_names = [ + feature_name[len(self.lfp_channels[0]) + 1 :] + for feature_name in self.names_chs_lfp[0] + ] + + self.initialized = True
+ + +
+[docs] + def project_features(self, feature_dict: dict) -> None: + """Project data, given idx_chs_ecog/stn""" + + if not self.initialized: + self.init_projection_run(feature_dict) + + dat_cortex = ( + np.array( + [ + np.array([feature_dict[ch] for ch in ch_names]) + for ch_names in self.names_chs_ecog + ] + ) + if self.project_cortex + else None + ) + + dat_subcortex = ( + np.array( + [ + np.array([feature_dict[ch] for ch in ch_names]) + for ch_names in self.names_chs_lfp + ] + ) + if self.project_subcortex + else None + ) + + # project data + # get_projected_cortex_subcortex_data can return None + # but None is not handled and will throw error in the code below + + proj_cortex_array: np.ndarray + proj_subcortex_array: np.ndarray + (proj_cortex_array, proj_subcortex_array) = ( + self.get_projected_cortex_subcortex_data(dat_cortex, dat_subcortex) + ) # type: ignore # Ignore None return + + features_new: dict = {} + # proj_cortex_array has shape grid_points x feature_number + if self.project_cortex: + features_new = features_new | { + "gridcortex_" + + str(act_grid_point) + + "_" + + feature_name: proj_cortex_array[act_grid_point, feature_idx] + for feature_idx, feature_name in enumerate(self.feature_names) # type: ignore # Empty list handled above + for act_grid_point in self.active_cortex_gridpoints + } + if self.project_subcortex: + features_new = features_new | { + "gridsubcortex_" + + str(act_grid_point) + + "_" + + feature_name: proj_subcortex_array[act_grid_point, feature_idx] + for feature_idx, feature_name in enumerate(self.feature_names) # type: ignore # Empty list handled above + for act_grid_point in self.active_subcortex_gridpoints + } + + feature_dict.update(features_new)
+ + +
+[docs] + def get_projected_cortex_subcortex_data( + self, + dat_cortex: np.ndarray | None = None, + dat_subcortex: np.ndarray | None = None, + ) -> tuple[np.ndarray | None, np.ndarray | None]: + """Project cortical and subcortical data to predefined projection matrices + + Parameters + ---------- + dat_cortex : np.ndarray, optional + cortical features, by default None + dat_subcortex : np.ndarray, optional + subcortical features, by default None + + Returns + ------- + proj_cortex : np.ndarray + projected cortical features, by detault None + proj_subcortex : np.ndarray + projected subcortical features, by detault None + """ + proj_cortex = None + proj_subcortex = None + + if dat_cortex is not None: + proj_cortex = self.proj_matrix_cortex @ dat_cortex + if dat_subcortex is not None: + proj_subcortex = self.proj_matrix_subcortex @ dat_subcortex + + return proj_cortex, proj_subcortex
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_rereference.html b/_modules/nm_rereference.html new file mode 100644 index 00000000..1e9a6422 --- /dev/null +++ b/_modules/nm_rereference.html @@ -0,0 +1,563 @@ + + + + + + + + + + nm_rereference — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_rereference

+"""Re-referencing Module."""
+
+import numpy as np
+import pandas as pd
+
+from py_neuromodulation.nm_preprocessing import NMPreprocessor
+
+
+
+[docs] +class ReReferencer(NMPreprocessor): + def __init__( + self, + sfreq: float, + nm_channels: pd.DataFrame, + ) -> None: + """Initialize real-time rereference information. + + Parameters + ---------- + sfreq : float + Sampling frequency. Is not used, only kept for compatibility. + nm_channels : Pandas DataFrame + Dataframe containing information about rereferencing, as + specified in nm_channels.csv. + + + Raises: + ValueError: rereferencing using undefined channel + ValueError: rereferencing to same channel + """ + + self.ref_matrix: np.ndarray | None + + nm_channels = nm_channels[nm_channels["used"] == 1].reset_index(drop=True) + # (channels_used,) = np.where((nm_channels.used == 1)) + + ch_names = nm_channels["name"].tolist() + + # no re-referencing is being performed when there is a single channel present only + if nm_channels.shape[0] in (0, 1): + self.ref_matrix = None + return + + ch_types = nm_channels["type"] + refs = nm_channels["rereference"] + + type_map = {} + for ch_type in ch_types.unique(): + type_map[ch_type] = np.where( + (ch_types == ch_type) & (nm_channels["status"] == "good") + )[0] + + ref_matrix = np.zeros((len(nm_channels), len(nm_channels))) + for ind in range(len(nm_channels)): + ref_matrix[ind, ind] = 1 + # if ind not in channels_used: + # continue + ref = refs[ind] + if ref.lower() == "none" or pd.isnull(ref): + ref_idx = None + continue + if ref.lower() == "average": + ch_type = ch_types[ind] + ref_idx = type_map[ch_type][type_map[ch_type] != ind] + else: + ref_idx = [] + ref_channels = ref.split("&") + for ref_chan in ref_channels: + if ref_chan not in ch_names: + raise ValueError( + "One or more of the reference channels are not" + " part of the recording channels. First missing" + f" channel: {ref_chan}." + ) + if ref_chan == ch_names[ind]: + raise ValueError( + "You cannot rereference to the same channel." + f" Channel: {ref_chan}." + ) + ref_idx.append(ch_names.index(ref_chan)) + ref_matrix[ind, ref_idx] = -1 / len(ref_idx) + self.ref_matrix = ref_matrix + +
+[docs] + def process(self, data: np.ndarray) -> np.ndarray: + """Rereference data according to the initialized ReReferencer class. + + Args: + data (numpy ndarray) : + shape(n_channels, n_samples) - data to be rereferenced. + + Returns: + reref_data (numpy ndarray): + shape(n_channels, n_samples) - rereferenced data + """ + if self.ref_matrix is not None: + return self.ref_matrix @ data + else: + return data
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_resample.html b/_modules/nm_resample.html new file mode 100644 index 00000000..9507d63e --- /dev/null +++ b/_modules/nm_resample.html @@ -0,0 +1,525 @@ + + + + + + + + + + nm_resample — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_resample

+"""Module for resampling."""
+
+import numpy as np
+from py_neuromodulation.nm_types import NMBaseModel, Field
+
+
+from py_neuromodulation.nm_preprocessing import NMPreprocessor
+
+
+class ResamplerSettings(NMBaseModel):
+    resample_freq_hz: float = Field(default=1000, gt=0)
+
+
+
+[docs] +class Resampler(NMPreprocessor): + """Resample data. + + Parameters + ---------- + sfreq : float + Original sampling frequency. + + Attributes + ---------- + up: float + Factor to upsample by. + """ + + def __init__( + self, + sfreq: float, + resample_freq_hz: float, + ) -> None: + self.settings = ResamplerSettings(resample_freq_hz=resample_freq_hz) + + ratio = float(resample_freq_hz / sfreq) + if ratio == 1.0: + self.up = 0.0 + else: + self.up = ratio + +
+[docs] + def process(self, data: np.ndarray) -> np.ndarray: + """Resample raw data using mne.filter.resample. + + Parameters + ---------- + data : np.ndarray + Data to resample + + Returns + ------- + np.ndarray + Resampled data + """ + if not self.up: + return data + + from mne.filter import resample + + return resample(data.astype(np.float64), up=self.up, down=1.0)
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_run_analysis.html b/_modules/nm_run_analysis.html new file mode 100644 index 00000000..1b024252 --- /dev/null +++ b/_modules/nm_run_analysis.html @@ -0,0 +1,797 @@ + + + + + + + + + + nm_run_analysis — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_run_analysis

+"""This module contains the class to process a given batch of data."""
+
+from time import time
+import numpy as np
+import pandas as pd
+
+from py_neuromodulation import nm_IO, logger
+from py_neuromodulation.nm_types import _PathLike
+from py_neuromodulation.nm_features import FeatureProcessors
+from py_neuromodulation.nm_preprocessing import NMPreprocessors
+from py_neuromodulation.nm_projection import Projection
+from py_neuromodulation.nm_settings import NMSettings
+
+
+
+[docs] +class DataProcessor: + def __init__( + self, + sfreq: float, + settings: "NMSettings | _PathLike", + nm_channels: pd.DataFrame | _PathLike, + coord_names: list | None = None, + coord_list: list | None = None, + line_noise: float | None = None, + path_grids: _PathLike | None = None, + verbose: bool = True, + ) -> None: + """Initialize run class. + + Parameters + ---------- + features : features.py object + Feature_df object (needs to be initialized beforehand) + settings : dict + dictionary of settings such as "seglengths" or "frequencyranges" + reference : reference.py object + Rereference object (needs to be initialized beforehand), by default None + projection : projection.py object + projection object (needs to be initialized beforehand), by default None + resample : resample.py object + Resample object (needs to be initialized beforehand), by default None + notch_filter : nm_filter.NotchFilter, + Notch Filter object, needs to be instantiated beforehand + verbose : boolean + if True, log signal processed and computation time + """ + self.settings = NMSettings.load(settings) + self.nm_channels = self._load_nm_channels(nm_channels) + + self.sfreq_features: float = self.settings.sampling_rate_features_hz + self._sfreq_raw_orig: float = sfreq + self.sfreq_raw: float = sfreq // 1 + self.line_noise: float | None = line_noise + self.path_grids: _PathLike | None = path_grids + self.verbose: bool = verbose + + self.features_previous = None + + (self.ch_names_used, _, self.feature_idx, _) = self._get_ch_info() + + self.preprocessors = NMPreprocessors( + settings=self.settings, + nm_channels=self.nm_channels, + sfreq=self.sfreq_raw, + line_noise=self.line_noise, + ) + + if self.settings.postprocessing.feature_normalization: + from py_neuromodulation.nm_normalization import FeatureNormalizer + + self.feature_normalizer = FeatureNormalizer(self.settings) + + self.features = FeatureProcessors( + settings=self.settings, + ch_names=self.ch_names_used, + sfreq=self.sfreq_raw, + ) + + if coord_list is not None and coord_names is not None: + self.coords = self._set_coords( + coord_names=coord_names, coord_list=coord_list + ) + + self.projection = self._get_projection(self.settings, self.nm_channels) + + self.cnt_samples = 0 + + @staticmethod + def _add_coordinates(coord_names: list[str], coord_list: list) -> dict: + """Write cortical and subcortical coordinate information in joint dictionary + + Parameters + ---------- + coord_names : list[str] + list of coordinate names + coord_list : list + list of list of 3D coordinates + + Returns + ------- + dict with (sub)cortex_left and (sub)cortex_right ch_names and positions + """ + + def is_left_coord(val: float, coord_region: str) -> bool: + if coord_region.split("_")[1] == "left": + return val < 0 + return val > 0 + + coords: dict[str, dict[str, list | np.ndarray]] = {} + + for coord_region in [ + coord_loc + "_" + lat + for coord_loc in ["cortex", "subcortex"] + for lat in ["left", "right"] + ]: + coords[coord_region] = {} + + ch_type = "ECOG" if "cortex" == coord_region.split("_")[0] else "LFP" + + coords[coord_region]["ch_names"] = [ + coord_name + for coord_name, ch in zip(coord_names, coord_list) + if is_left_coord(ch[0], coord_region) and (ch_type in coord_name) + ] + + # multiply by 1000 to get m instead of mm + positions = [] + for coord, coord_name in zip(coord_list, coord_names): + if is_left_coord(coord[0], coord_region) and (ch_type in coord_name): + positions.append(coord) + coords[coord_region]["positions"] = ( + np.array(positions, dtype=np.float64) * 1000 + ) + + return coords + + def _get_ch_info( + self, + ) -> tuple[list[str], list[str], list[int], np.ndarray]: + """Get used feature and label info from nm_channels""" + nm_channels = self.nm_channels + ch_names_used = nm_channels[nm_channels["used"] == 1]["new_name"].tolist() + ch_types_used = nm_channels[nm_channels["used"] == 1]["type"].tolist() + + # used channels for feature estimation + feature_idx = np.where(nm_channels["used"] & ~nm_channels["target"])[0].tolist() + + # If multiple targets exist, select only the first + label_idx = np.where(nm_channels["target"] == 1)[0] + + return ch_names_used, ch_types_used, feature_idx, label_idx + + @staticmethod + def _get_grids( + settings: "NMSettings", + path_grids: _PathLike | None, + ) -> tuple[pd.DataFrame | None, pd.DataFrame | None]: + """Read settings specified grids + + Parameters + ---------- + settings : dict + path_grids : str + + Returns + ------- + Tuple + grid_cortex, grid_subcortex, + might be None if not specified in settings + """ + if settings.postprocessing.project_cortex: + grid_cortex = nm_IO.read_grid(path_grids, "cortex") + else: + grid_cortex = None + if settings.postprocessing.project_subcortex: + grid_subcortex = nm_IO.read_grid(path_grids, "subcortex") + else: + grid_subcortex = None + return grid_cortex, grid_subcortex + + def _get_projection( + self, settings: "NMSettings", nm_channels: pd.DataFrame + ) -> Projection | None: + """Return projection of used coordinated and grids""" + + if not any( + ( + settings.postprocessing.project_cortex, + settings.postprocessing.project_subcortex, + ) + ): + return None + + grid_cortex, grid_subcortex = self._get_grids(self.settings, self.path_grids) + projection = Projection( + settings=settings, + grid_cortex=grid_cortex, + grid_subcortex=grid_subcortex, + coords=self.coords, + nm_channels=nm_channels, + plot_projection=False, + ) + return projection + + @staticmethod + def _load_nm_channels( + nm_channels: pd.DataFrame | _PathLike, + ) -> pd.DataFrame: + if not isinstance(nm_channels, pd.DataFrame): + return nm_IO.load_nm_channels(nm_channels) + return nm_channels + + def _set_coords( + self, coord_names: list[str] | None, coord_list: list | None + ) -> dict: + if not any( + ( + self.settings.postprocessing.project_cortex, + self.settings.postprocessing.project_subcortex, + ) + ): + return {} + + if any((coord_list is None, coord_names is None)): + raise ValueError( + "No coordinates could be loaded. Please provide coord_list and" + f" coord_names. Got: {coord_list=}, {coord_names=}." + ) + + return self._add_coordinates( + coord_names=coord_names, + coord_list=coord_list, # type: ignore # None case handled above + ) + +
+[docs] + def process(self, data: np.ndarray) -> dict[str, float]: + """Given a new data batch, calculate and return features. + + Parameters + ---------- + data : np.ndarray + Current batch of raw data + + Returns + ------- + pandas Series + Features calculated from current data + """ + start_time = time() + + nan_channels = np.isnan(data).any(axis=1) + + data = np.nan_to_num(data)[self.feature_idx, :] + + data = self.preprocessors.process_data(data) + + # calculate features + features_dict = self.features.estimate_features(data) + + # normalize features + if self.settings.postprocessing.feature_normalization: + normed_features = self.feature_normalizer.process( + np.fromiter(features_dict.values(), dtype=np.float64) + ) + features_dict = { + key: normed_features[idx] + for idx, key in enumerate(features_dict.keys()) + } + + # project features to grid + if self.projection: + self.projection.project_features(features_dict) + + # check for all features, where the channel had a NaN, that the feature is also put to NaN + if nan_channels.sum() > 0: + # TONI: no need to do this if we store both old and new names for the channels + new_nan_channels = [] + for ch in list(np.array(self.ch_names_used)[nan_channels]): + for key in features_dict.keys(): + if ch in key: + new_nan_channels.append(key) + + for ch in new_nan_channels: + features_dict[ch] = np.nan + + if self.verbose: + logger.info("Last batch took: %.2f seconds", time() - start_time) + + return features_dict
+ + +
+[docs] + def save_sidecar( + self, + out_path_root: _PathLike, + folder_name: str, + additional_args: dict | None = None, + ) -> None: + """Save sidecar incuding fs, coords, sess_right to + out_path_root and subfolder 'folder_name'. + """ + sidecar: dict = { + "original_fs": self._sfreq_raw_orig, + "final_fs": self.sfreq_raw, + "sfreq": self.sfreq_features, + } + if self.projection: + sidecar["coords"] = self.projection.coords + if self.settings.postprocessing.project_cortex: + sidecar["grid_cortex"] = self.projection.grid_cortex + sidecar["proj_matrix_cortex"] = self.projection.proj_matrix_cortex + if self.settings.postprocessing.project_subcortex: + sidecar["grid_subcortex"] = self.projection.grid_subcortex + sidecar["proj_matrix_subcortex"] = self.projection.proj_matrix_subcortex + if additional_args is not None: + sidecar = sidecar | additional_args + + nm_IO.save_sidecar(sidecar, out_path_root, folder_name)
+ + + def save_settings(self, out_path_root: _PathLike, folder_name: str) -> None: + self.settings.save(out_path_root, folder_name) + + def save_nm_channels(self, out_path_root: _PathLike, folder_name: str) -> None: + nm_IO.save_nm_channels(self.nm_channels, out_path_root, folder_name) + + def save_features( + self, + out_path_root: _PathLike, + folder_name: str, + feature_arr: pd.DataFrame, + ) -> None: + nm_IO.save_features(feature_arr, out_path_root, folder_name)
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_settings.html b/_modules/nm_settings.html new file mode 100644 index 00000000..633dacf4 --- /dev/null +++ b/_modules/nm_settings.html @@ -0,0 +1,751 @@ + + + + + + + + + + nm_settings — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_settings

+"""Module for handling settings."""
+
+from pathlib import PurePath, Path
+from typing import ClassVar
+from pydantic import Field, model_validator
+
+from py_neuromodulation import PYNM_DIR, logger, user_features
+from py_neuromodulation.nm_types import (
+    BoolSelector,
+    FrequencyRange,
+    PreprocessorName,
+    _PathLike,
+)
+
+from py_neuromodulation.nm_types import NMBaseModel
+from py_neuromodulation.nm_filter_preprocessing import FilterSettings
+from py_neuromodulation.nm_kalmanfilter import KalmanSettings
+from py_neuromodulation.nm_projection import ProjectionSettings
+from py_neuromodulation.nm_bispectra import BispectraSettings
+from py_neuromodulation.nm_nolds import NoldsSettings
+from py_neuromodulation.nm_mne_connectivity import MNEConnectivitySettings
+from py_neuromodulation.nm_fooof import FooofSettings
+from py_neuromodulation.nm_coherence import CoherenceSettings
+from py_neuromodulation.nm_sharpwaves import SharpwaveSettings
+from py_neuromodulation.nm_oscillatory import OscillatorySettings, BandpassSettings
+from py_neuromodulation.nm_bursts import BurstSettings
+from py_neuromodulation.nm_normalization import NormMethod, NormalizationSettings
+from py_neuromodulation.nm_resample import ResamplerSettings
+
+
+
+[docs] +class FeatureSelection(BoolSelector): + raw_hjorth: bool = True + return_raw: bool = True + bandpass_filter: bool = False + stft: bool = False + fft: bool = True + welch: bool = True + sharpwave_analysis: bool = True + fooof: bool = False + nolds: bool = False + coherence: bool = False + bursts: bool = True + linelength: bool = True + mne_connectivity: bool = False + bispectrum: bool = False
+ + + +
+[docs] +class PostprocessingSettings(BoolSelector): + feature_normalization: bool = True + project_cortex: bool = False + project_subcortex: bool = False
+ + + +
+[docs] +class NMSettings(NMBaseModel): + # Class variable to store instances + _instances: ClassVar[list["NMSettings"]] = [] + + # General settings + sampling_rate_features_hz: float = Field(default=10, gt=0) + segment_length_features_ms: float = Field(default=1000, gt=0) + frequency_ranges_hz: dict[str, FrequencyRange] = { + "theta": FrequencyRange(4, 8), + "alpha": FrequencyRange(8, 12), + "low beta": FrequencyRange(13, 20), + "high beta": FrequencyRange(20, 35), + "low gamma": FrequencyRange(60, 80), + "high gamma": FrequencyRange(90, 200), + "HFA": FrequencyRange(200, 400), + } + + # Preproceessing settings + preprocessing: list[PreprocessorName] = [ + "raw_resampling", + "notch_filter", + "re_referencing", + ] + raw_resampling_settings: ResamplerSettings = ResamplerSettings() + preprocessing_filter: FilterSettings = FilterSettings() + raw_normalization_settings: NormalizationSettings = NormalizationSettings() + + # Postprocessing settings + postprocessing: PostprocessingSettings = PostprocessingSettings() + feature_normalization_settings: NormalizationSettings = NormalizationSettings() + project_cortex_settings: ProjectionSettings = ProjectionSettings(max_dist_mm=20) + project_subcortex_settings: ProjectionSettings = ProjectionSettings(max_dist_mm=5) + + # Feature settings + features: FeatureSelection = FeatureSelection() + + fft_settings: OscillatorySettings = OscillatorySettings() + welch_settings: OscillatorySettings = OscillatorySettings() + stft_settings: OscillatorySettings = OscillatorySettings() + bandpass_filter_settings: BandpassSettings = BandpassSettings() + kalman_filter_settings: KalmanSettings = KalmanSettings() + burst_settings: BurstSettings = BurstSettings() + sharpwave_analysis_settings: SharpwaveSettings = SharpwaveSettings() + mne_connectivity: MNEConnectivitySettings = MNEConnectivitySettings() + coherence: CoherenceSettings = CoherenceSettings() + fooof: FooofSettings = FooofSettings() + nolds_features: NoldsSettings = NoldsSettings() + bispectrum: BispectraSettings = BispectraSettings() + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + for feat_name in user_features.keys(): + setattr(self.features, feat_name, True) + + NMSettings._add_instance(self) + + @classmethod + def _add_instance(cls, instance: "NMSettings") -> None: + """Keep track of all instances created in class variable""" + cls._instances.append(instance) + + @classmethod + def _add_feature(cls, feature: str) -> None: + for instance in cls._instances: + setattr(instance.features, feature, True) + + @classmethod + def _remove_feature(cls, feature: str) -> None: + for instance in cls._instances: + delattr(instance.features, feature) + + @model_validator(mode="after") + def validate_settings(self): + if len(self.features.get_enabled()) == 0: + raise ValueError("At least one feature must be selected.") + + if self.features.bandpass_filter: + # Check BandPass settings frequency bands + self.bandpass_filter_settings.validate_fbands(self) + + # Check Kalman filter frequency bands + if self.bandpass_filter_settings.kalman_filter: + self.kalman_filter_settings.validate_fbands(self) + + for k, v in self.frequency_ranges_hz.items(): + if not isinstance(v, FrequencyRange): + self.frequency_ranges_hz[k] = FrequencyRange.create_from(v) + + return self + + def reset(self) -> "NMSettings": + self.features.disable_all() + self.preprocessing = [] + self.postprocessing.disable_all() + return self + + def set_fast_compute(self) -> "NMSettings": + self.reset() + self.features.fft = True + self.preprocessing = [ + "raw_resampling", + "notch_filter", + "re_referencing", + ] + self.postprocessing.feature_normalization = True + self.postprocessing.project_cortex = False + self.postprocessing.project_subcortex = False + + return self + + def enable_all_features(self): + self.features.enable_all() + return self + + @staticmethod + def get_fast_compute() -> "NMSettings": + return NMSettings.get_default().set_fast_compute() + + @classmethod + def load(cls, settings: "NMSettings | _PathLike | None") -> "NMSettings": + if isinstance(settings, cls): + return settings.validate() + if settings is None: + return cls.get_default() + return cls.from_file(str(settings)) + +
+[docs] + @staticmethod + def from_file(PATH: _PathLike) -> "NMSettings": + """Load settings from file or a directory. + + Args: + PATH (_PathLike): Path to settings file or to directory containing settings file, + or path to experiment including experiment prefix + (e.g. /path/to/exp/exp_prefix[_SETTINGS.json]) + + Raises: + ValueError: when file format is not supported. + + Returns: + NMSettings: PyNM settings object + """ + path = Path(PATH) + + # If directory is passed, look for settings file inside + if path.is_dir(): + for child in path.iterdir(): + if child.is_file() and child.suffix in [".json", ".yaml"]: + path = child + break + + # If prefix is passed, look for settings file matching prefix + if not path.is_dir() and not path.is_file(): + for child in path.parent.iterdir(): + ext = child.suffix.lower() + if ( + child.is_file() + and ext in [".json", ".yaml"] + and child.name == path.stem + "_SETTINGS" + ext + ): + path = child + break + + match path.suffix: + case ".json": + import json + + with open(path) as f: + model_dict = json.load(f) + case ".yaml": + import yaml + + with open(path) as f: + model_dict = yaml.safe_load(f) + case _: + raise ValueError("File format not supported.") + + return NMSettings(**model_dict)
+ + + @staticmethod + def get_default() -> "NMSettings": + return NMSettings.from_file(PYNM_DIR / "nm_settings.yaml") + + @staticmethod + def list_normalization_methods() -> list[NormMethod]: + return NormalizationSettings.list_normalization_methods() + + def save( + self, path_out: _PathLike, folder_name: str = "", format: str = "json" + ) -> None: + path_out = PurePath(path_out) + filename = f"SETTINGS.{format}" + + if folder_name: + path_out = path_out / folder_name + filename = f"{folder_name}_{filename}" + + path_out = path_out / filename + + with open(path_out, "w") as f: + match format: + case "json": + f.write(self.model_dump_json(indent=4)) + case "yaml": + import yaml + + yaml.dump(self.model_dump(), f, default_flow_style=False) + + logger.info(f"Settings saved to {path_out}")
+ + + +# For retrocompatibility with previous versions of PyNM +def get_default_settings() -> NMSettings: + return NMSettings.get_default() + + +def reset_settings(settings: NMSettings) -> NMSettings: + return settings.reset() + + +def set_settings_fast_compute() -> NMSettings: + return NMSettings.get_fast_compute() + + +def test_settings(settings: NMSettings) -> NMSettings: + return settings.validate() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_sharpwaves.html b/_modules/nm_sharpwaves.html new file mode 100644 index 00000000..f0c46945 --- /dev/null +++ b/_modules/nm_sharpwaves.html @@ -0,0 +1,889 @@ + + + + + + + + + + nm_sharpwaves — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_sharpwaves

+import numpy as np
+from collections.abc import Sequence
+from collections import defaultdict
+from itertools import product
+
+from py_neuromodulation.nm_types import NMBaseModel
+from pydantic import model_validator
+from typing import TYPE_CHECKING, Any, Callable
+from numpy._core._methods import _mean as np_mean
+
+from py_neuromodulation.nm_features import NMFeature
+from py_neuromodulation.nm_types import BoolSelector, FrequencyRange
+
+if TYPE_CHECKING:
+    from py_neuromodulation.nm_settings import NMSettings
+
+# Using low-level numpy mean function for performance, could do the same for the other estimators
+ESTIMATOR_DICT = {
+    "mean": np_mean,
+    "median": np.median,
+    "max": np.max,
+    "min": np.min,
+    "var": np.var,
+}
+
+
+class PeakDetectionSettings(NMBaseModel):
+    estimate: bool = True
+    distance_troughs_ms: float = 10
+    distance_peaks_ms: float = 5
+
+
+class SharpwaveFeatures(BoolSelector):
+    peak_left: bool = False
+    peak_right: bool = False
+    trough: bool = False
+    width: bool = False
+    prominence: bool = True
+    interval: bool = True
+    decay_time: bool = False
+    rise_time: bool = False
+    sharpness: bool = True
+    rise_steepness: bool = False
+    decay_steepness: bool = False
+    slope_ratio: bool = False
+
+
+class SharpwaveEstimators(NMBaseModel):
+    mean: list[str] = ["interval"]
+    median: list[str] = []
+    max: list[str] = ["prominence", "sharpness"]
+    min: list[str] = []
+    var: list[str] = []
+
+    def keys(self):
+        return ["mean", "median", "max", "min", "var"]
+
+    def values(self):
+        return [self.mean, self.median, self.max, self.min, self.var]
+
+
+class SharpwaveSettings(NMBaseModel):
+    sharpwave_features: SharpwaveFeatures = SharpwaveFeatures()
+    filter_ranges_hz: list[FrequencyRange] = [
+        FrequencyRange(5, 80),
+        FrequencyRange(5, 30),
+    ]
+    detect_troughs: PeakDetectionSettings = PeakDetectionSettings()
+    detect_peaks: PeakDetectionSettings = PeakDetectionSettings()
+    estimator: SharpwaveEstimators = SharpwaveEstimators()
+    apply_estimator_between_peaks_and_troughs: bool = True
+
+    def disable_all_features(self):
+        self.sharpwave_features.disable_all()
+        for est in self.estimator.keys():
+            self.estimator[est] = []
+
+    @model_validator(mode="after")
+    def test_settings(cls, settings):
+        # check if all features are also enabled via an estimator
+        estimator_list = [est for list_ in settings.estimator.values() for est in list_]
+
+        for used_feature in settings.sharpwave_features.get_enabled():
+            assert (
+                used_feature in estimator_list
+            ), f"Add estimator key for {used_feature}"
+
+        return settings
+
+
+
+[docs] +class SharpwaveAnalyzer(NMFeature): + def __init__( + self, settings: "NMSettings", ch_names: Sequence[str], sfreq: float + ) -> None: + self.sw_settings = settings.sharpwave_analysis_settings + self.sfreq = sfreq + self.ch_names = ch_names + self.list_filter: list[tuple[str, Any]] = [] + self.trough: list = [] + self.troughs_idx: list = [] + + settings.validate() + + # FrequencyRange's are already ensured to have high > low + # Test that the higher frequency is smaller than the sampling frequency + for filter_range in settings.sharpwave_analysis_settings.filter_ranges_hz: + assert filter_range[1] < sfreq, ( + "Filter range has to be smaller than sfreq, " + f"got sfreq {sfreq} and filter range {filter_range}" + ) + + for filter_range in settings.sharpwave_analysis_settings.filter_ranges_hz: + # Test settings + # TODO: handle None values + if filter_range[0] is None: + self.list_filter.append(("no_filter", None)) + else: + from mne.filter import create_filter + + self.list_filter.append( + ( + f"range_{filter_range[0]:.0f}_{filter_range[1]:.0f}", + create_filter( + None, + sfreq, + l_freq=filter_range[0], + h_freq=filter_range[1], + fir_design="firwin", + # l_trans_bandwidth=None, + # h_trans_bandwidth=None, + # filter_length=str(sfreq) + "ms", + verbose=False, + ), + ) + ) + + self.filter_names = [name for name, _ in self.list_filter] + self.filters = np.vstack([filter for _, filter in self.list_filter]) + self.filters = np.tile(self.filters[None, :, :], (len(self.ch_names), 1, 1)) + + self.used_features = self.sw_settings.sharpwave_features.get_enabled() + + # initializing estimator functions, respecitive for all sharpwave features + self.estimator_dict: dict[str, dict[str, Callable]] = { + feat: { + est: ESTIMATOR_DICT[est] + for est in self.sw_settings.estimator.keys() + if feat in self.sw_settings.estimator[est] + } + for feat_list in self.sw_settings.estimator.values() + for feat in feat_list + } + + estimator_combinations = [ + (feature_name, estimator_name, estimator) + for feature_name in self.used_features + for estimator_name, estimator in self.estimator_dict[feature_name].items() + ] + + filter_combinations = list( + product( + enumerate(self.ch_names), enumerate(self.filter_names), [False, True] + ) + ) + + self.estimator_key_map: dict[str, Callable] = {} + self.combinations = [] + for (ch_idx, ch_name), ( + filter_idx, + filter_name, + ), detect_troughs in filter_combinations: + for feature_name, estimator_name, estimator in estimator_combinations: + key_name = f"{ch_name}_Sharpwave_{estimator_name.title()}_{feature_name}_{filter_name}" + self.estimator_key_map[key_name] = estimator + self.combinations.append( + ( + (ch_idx, ch_name), + (filter_idx, filter_name), + detect_troughs, + estimator_combinations, + ) + ) + + # Check required feature computations according to settings + self.need_peak_left = ( + self.sw_settings.sharpwave_features.peak_left + or self.sw_settings.sharpwave_features.prominence + ) + self.need_peak_right = ( + self.sw_settings.sharpwave_features.peak_right + or self.sw_settings.sharpwave_features.prominence + ) + self.need_trough = ( + self.sw_settings.sharpwave_features.trough + or self.sw_settings.sharpwave_features.prominence + ) + + self.need_decay_steepness = ( + self.sw_settings.sharpwave_features.decay_steepness + or self.sw_settings.sharpwave_features.slope_ratio + ) + + self.need_rise_steepness = ( + self.sw_settings.sharpwave_features.rise_steepness + or self.sw_settings.sharpwave_features.slope_ratio + ) + + self.need_steepness = self.need_rise_steepness or self.need_decay_steepness + +
+[docs] + def calc_feature( + self, + data: np.ndarray, + features_compute: dict, + ) -> dict: + """Given a new data batch, the peaks, troughs and sharpwave features + are estimated. Importantly only new data is being analyzed here. In + steps of 1/settings["sampling_rate_features] analyzed and returned. + Pre-initialized filters are applied to each channel. + + Parameters + ---------- + data (np.ndarray): 2d data array with shape [num_channels, samples] + features_compute (dict): Features.py estimated features + + Returns + ------- + features_compute (dict): set features for Features.py object + """ + dict_ch_features: dict[str, dict[str, float]] = defaultdict(lambda: {}) + + from scipy.signal import fftconvolve + + data = np.tile(data[:, None, :], (1, len(self.list_filter), 1)) + data = fftconvolve(data, self.filters, axes=2, mode="same") + + self.filtered_data = ( + data # TONI: Expose filtered data for example 3, need a better way + ) + + for ( + (ch_idx, ch_name), + (filter_idx, filter_name), + detect_troughs, + estimator_combinations, + ) in self.combinations: + sub_data = data[ch_idx, filter_idx, :] + + key_name_pt = "Trough" if detect_troughs else "Peak" + + if (not detect_troughs and not self.sw_settings.detect_peaks.estimate) or ( + detect_troughs and not self.sw_settings.detect_troughs.estimate + ): + continue + + # the detect_troughs loop start with peaks, s.t. data does not need to be flipped + sub_data = -sub_data if detect_troughs else sub_data + # sub_data *= 1 - 2 * detect_troughs # branchless version + + waveform_results = self.analyze_waveform(sub_data) + + # for each feature take the respective fun. + for feature_name, estimator_name, estimator in estimator_combinations: + feature_data = waveform_results[feature_name] + key_name = f"{ch_name}_Sharpwave_{estimator_name.title()}_{feature_name}_{filter_name}" + + # zero check because no peaks can be also detected + feature_data = estimator(feature_data) if len(feature_data) != 0 else 0 + dict_ch_features[key_name][key_name_pt] = feature_data + + if self.sw_settings.apply_estimator_between_peaks_and_troughs: + # apply between 'Trough' and 'Peak' the respective function again + # save only the 'est_fun' (e.g. max) between them + + # the key_name stays, since the estimator function stays between peaks and troughs + for key_name, estimator in self.estimator_key_map.items(): + features_compute[key_name] = estimator( + [ + list(dict_ch_features[key_name].values())[0], + list(dict_ch_features[key_name].values())[1], + ] + ) + else: + # otherwise, save all write all "flattened" key value pairs in features_compute + for key, subdict in dict_ch_features.items(): + for key_sub, value_sub in subdict.items(): + features_compute[key + "_analyze_" + key_sub] = value_sub + + return features_compute
+ + +
+[docs] + def analyze_waveform(self, data) -> dict: + """Given the scipy.signal.find_peaks trough/peak distance + settings specified sharpwave features are estimated. + """ + + from scipy.signal import find_peaks + + # TODO: find peaks is actually not that big a performance hit, but the rest + # of this function is. Perhaps find_peaks can be put in a loop and the rest optimized somehow? + peak_idx: np.ndarray = find_peaks( + data, distance=self.sw_settings.detect_troughs.distance_peaks_ms + )[0] + trough_idx: np.ndarray = find_peaks( + -data, distance=self.sw_settings.detect_troughs.distance_troughs_ms + )[0] + + """ Find left and right peak indexes for each trough """ + peak_pointer = first_valid = last_valid = 0 + peak_idx_left_list: list[int] = [] + peak_idx_right_list: list[int] = [] + + for i in range(len(trough_idx)): + # Locate peak right of current trough + while ( + peak_pointer < peak_idx.size and peak_idx[peak_pointer] < trough_idx[i] + ): + peak_pointer += 1 + + if peak_pointer - 1 < 0: + # If trough has no peak to it's left, it's not valid + first_valid = i + 1 # Try with next one + continue + + if peak_pointer == peak_idx.size: + # If we went past the end of the peaks list, trough had no peak to its right + continue + + last_valid = i + peak_idx_left_list.append(peak_idx[peak_pointer - 1]) + peak_idx_right_list.append(peak_idx[peak_pointer]) + + # Remove non valid troughs and make array of left and right peaks for each trough + trough_idx = trough_idx[first_valid : last_valid + 1] + peak_idx_left = np.array(peak_idx_left_list, dtype=int) + peak_idx_right = np.array(peak_idx_right_list, dtype=int) + + """ Calculate features (vectorized) """ + results: dict = {} + + if self.need_peak_left: + results["peak_left"] = data[peak_idx_left] + + if self.need_peak_right: + results["peak_right"] = data[peak_idx_right] + + if self.need_trough: + results["trough"] = data[trough_idx] + + if self.sw_settings.sharpwave_features.interval: + results["interval"] = np.concatenate((np.zeros(1), np.diff(trough_idx))) * ( + 1000 / self.sfreq + ) + + if self.sw_settings.sharpwave_features.sharpness: + # sharpess is calculated on a +- 5 ms window + # valid troughs need 5 ms of margin on both sides + troughs_valid = trough_idx[ + np.logical_and( + trough_idx - int(5 * (1000 / self.sfreq)) > 0, + trough_idx + int(5 * (1000 / self.sfreq)) < data.shape[0], + ) + ] + trough_height = data[troughs_valid] + left_height = data[troughs_valid - int(5 * (1000 / self.sfreq))] + right_height = data[troughs_valid + int(5 * (1000 / self.sfreq))] + # results["sharpness"] = ((trough_height - left_height) + (trough_height - right_height)) / 2 + results["sharpness"] = trough_height - 0.5 * (left_height + right_height) + + if self.need_steepness: + # steepness is calculated as the first derivative + steepness: np.ndarray = np.concatenate((np.zeros(1), np.diff(data))) + + # Create an array with the rise and decay steepness for each trough + # 0th dimension for rise/decay, 1st for trough index, 2nd for timepoint + steepness_troughs = np.zeros((2, trough_idx.shape[0], steepness.shape[0])) + if self.need_rise_steepness or self.need_decay_steepness: + for i in range(len(trough_idx)): + steepness_troughs[ + 0, i, 0 : trough_idx[i] - peak_idx_left[i] + 1 + ] = steepness[peak_idx_left[i] : trough_idx[i] + 1] + steepness_troughs[ + 1, i, 0 : peak_idx_right[i] - trough_idx[i] + 1 + ] = steepness[trough_idx[i] : peak_idx_right[i] + 1] + + if self.need_rise_steepness: + # left peak -> trough + # + 1 due to python syntax, s.t. the last element is included + results["rise_steepness"] = np.max( + np.abs(steepness_troughs[0, :, :]), axis=1 + ) + + if self.need_decay_steepness: + # trough -> right peak + results["decay_steepness"] = np.max( + np.abs(steepness_troughs[1, :, :]), axis=1 + ) + + if self.sw_settings.sharpwave_features.slope_ratio: + results["slope_ratio"] = ( + results["rise_steepness"] - results["decay_steepness"] + ) + + if self.sw_settings.sharpwave_features.prominence: + results["prominence"] = np.abs( + (results["peak_right"] + results["peak_left"]) / 2 - results["trough"] + ) + + if self.sw_settings.sharpwave_features.decay_time: + results["decay_time"] = (peak_idx_left - trough_idx) * ( + 1000 / self.sfreq + ) # ms + + if self.sw_settings.sharpwave_features.rise_time: + results["rise_time"] = (peak_idx_right - trough_idx) * ( + 1000 / self.sfreq + ) # ms + + if self.sw_settings.sharpwave_features.width: + results["width"] = peak_idx_right - peak_idx_left # ms + + return results
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_stats.html b/_modules/nm_stats.html new file mode 100644 index 00000000..eeac2246 --- /dev/null +++ b/_modules/nm_stats.html @@ -0,0 +1,945 @@ + + + + + + + + + + nm_stats — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_stats

+import random
+import copy
+
+import matplotlib.pyplot as plt
+
+# from numba import njit
+import numpy as np
+import pandas as pd
+import scipy.stats as stats
+
+
+def fitlm(x, y):
+    import statsmodels.api as sm
+    return sm.OLS(y, sm.add_constant(x)).fit()
+
+
+def fitlm_kfold(x, y, kfold_splits=5):
+    from sklearn.linear_model import LinearRegression
+    from sklearn.model_selection import KFold
+
+    model = LinearRegression()
+    if isinstance(x, type(np.array([]))) or isinstance(x, type([])):
+        x = pd.DataFrame(x)
+    if isinstance(y, type(np.array([]))) or isinstance(y, type([])):
+        y = pd.DataFrame(y)
+    scores, coeffs = [], np.zeros(x.shape[1])
+    kfold = KFold(n_splits=kfold_splits, shuffle=True, random_state=42)
+    for i, (train, test) in enumerate(kfold.split(x, y)):
+        model.fit(x.iloc[train, :], y.iloc[train, :])
+        score = model.score(x.iloc[test, :], y.iloc[test, :])
+        # mdl = fitlm(np.squeeze(y.iloc[test,:].transpose()), np.squeeze(model.predict(x.iloc[test, :])))
+        scores.append(score)
+        coeffs = np.vstack((coeffs, model.coef_))
+    coeffs = list(np.delete(coeffs, 0))
+    return scores, coeffs, model, ["scores", "coeffs", "model"]
+
+
+def zscore(data):
+    return (data - data.mean()) / data.std()
+
+
+
+[docs] +def permutationTestSpearmansRho(x, y, plot_distr=True, x_unit=None, p=5000): + """ + Calculate permutation test for multiple repetitions of Spearmans Rho + https://towardsdatascience.com/how-to-assess-statistical-significance-in-your-data-with-permutation-tests-8bb925b2113d + + x (np array) : first distibution e.g. R^2 + y (np array) : second distribution e.g. UPDRS + plot_distr (boolean) : if True: permutation histplot and ground truth will be + plotted + x_unit (str) : histplot xlabel + p (int): number of permutations + + returns: + gT (float) : estimated ground truth, here spearman's rho + p (float) : p value of permutation test + """ + + # compute ground truth difference + gT = stats.spearmanr(x, y)[0] + # + pV = np.array((x, y)) + # Initialize permutation: + pD = [] + # Permutation loop: + args_order = np.arange(0, pV.shape[1], 1) + args_order_2 = np.arange(0, pV.shape[1], 1) + for i in range(0, p): + # Shuffle the data: + random.shuffle(args_order) + random.shuffle(args_order_2) + # Compute permuted absolute difference of your two sampled + # distributions and store it in pD: + pD.append(stats.spearmanr(pV[0, args_order], pV[1, args_order_2])[0]) + + # calculate p value + if gT < 0: + p_val = len(np.where(pD <= gT)[0]) / p + else: + p_val = len(np.where(pD >= gT)[0]) / p + + if plot_distr: + plt.hist(pD, bins=30, label="permutation results") + plt.axvline(gT, color="orange", label="ground truth") + plt.title("ground truth " + x_unit + "=" + str(gT) + " p=" + str(p_val)) + plt.xlabel(x_unit) + plt.legend() + plt.show() + return gT, p_val
+ + + +
+[docs] +def permutationTest(x, y, plot_distr=True, x_unit=None, p=5000): + """ + Calculate permutation test + https://towardsdatascience.com/how-to-assess-statistical-significance-in-your-data-with-permutation-tests-8bb925b2113d + + x (np array) : first distr. + y (np array) : first distr. + plot_distr (boolean) : if True: plot permutation histplot and ground truth + x_unit (str) : histplot xlabel + p (int): number of permutations + + returns: + gT (float) : estimated ground truth, here absolute difference of + distribution means + p (float) : p value of permutation test + + """ + # Compute ground truth difference + gT = np.abs(np.average(x) - np.average(y)) + + pV = np.concatenate((x, y), axis=0) + pS = copy.copy(pV) + # Initialize permutation: + pD = [] + # Permutation loop: + for i in range(0, p): + # Shuffle the data: + random.shuffle(pS) + # Compute permuted absolute difference of your two sampled + # distributions and store it in pD: + pD.append( + np.abs( + np.average(pS[0 : int(len(pS) / 2)]) + - np.average(pS[int(len(pS) / 2) :]) + ) + ) + + # Calculate p-value + if gT < 0: + p_val = len(np.where(pD <= gT)[0]) / p + else: + p_val = len(np.where(pD >= gT)[0]) / p + + if plot_distr: + plt.hist(pD, bins=30, label="permutation results") + plt.axvline(gT, color="orange", label="ground truth") + plt.title("ground truth " + x_unit + "=" + str(gT) + " p=" + str(p_val)) + plt.xlabel(x_unit) + plt.legend() + plt.show() + return gT, p_val
+ + + +
+[docs] +def permutationTest_relative(x, y, plot_distr=True, x_unit=None, p=5000): + """ + Calculate permutation test + https://towardsdatascience.com/how-to-assess-statistical-significance-in-your-data-with-permutation-tests-8bb925b2113d + + x (np array) : first distr. + y (np array) : first distr. + plot_distr (boolean) : if True: plot permutation histplot and ground truth + x_unit (str) : histplot xlabel + p (int): number of permutations + + returns: + gT (float) : estimated ground truth, here absolute difference of + distribution means + p (float) : p value of permutation test + + """ + gT = np.abs(np.average(x) - np.average(y)) + pD = [] + for i in range(0, p): + l_ = [] + for i in range(x.shape[0]): + if random.randint(0, 1) == 1: + l_.append((x[i], y[i])) + else: + l_.append((y[i], x[i])) + pD.append( + np.abs(np.average(np.array(l_)[:, 0]) - np.average(np.array(l_)[:, 1])) + ) + if gT < 0: + p_val = len(np.where(pD <= gT)[0]) / p + else: + p_val = len(np.where(pD >= gT)[0]) / p + + if plot_distr: + plt.hist(pD, bins=30, label="permutation results") + plt.axvline(gT, color="orange", label="ground truth") + plt.title("ground truth " + x_unit + "=" + str(gT) + " p=" + str(p_val)) + plt.xlabel(x_unit) + plt.legend() + plt.show() + + return gT, p_val
+ + + +# @njit +
+[docs] +def permutation_numba_onesample(x, y, n_perm, two_tailed=True): + """Perform permutation test with one-sample distribution. + + Parameters + ---------- + x : array_like + First distribution + y : int or float + Baseline against which to check for statistical significane + n_perm : int + Number of permutations + two_tailed : bool, default: True + Set to False if you would like to perform a one-sampled permutation + test, else True + two_tailed : bool, default: True + Set to False if you would like to perform a one-tailed permutation + test, else True + + Returns + ------- + float + Estimated difference of distribution from baseline + float + P-value of permutation test + """ + if two_tailed: + zeroed = x - y + z = np.abs(np.mean(zeroed)) + p = np.empty(n_perm) + # Run the simulation n_perm times + for i in np.arange(n_perm): + sign = np.random.choice(a=np.array([-1.0, 1.0]), size=len(x), replace=True) + p[i] = np.abs(np.mean(zeroed * sign)) + else: + zeroed = x - y + z = np.mean(zeroed) + p = np.empty(n_perm) + # Run the simulation n_perm times + for i in np.arange(n_perm): + sign = np.random.choice(a=np.array([-1.0, 1.0]), size=len(x), replace=True) + p[i] = np.mean(zeroed * sign) + # Return p-value + return z, (np.sum(p >= z)) / n_perm
+ + + +# @njit +
+[docs] +def permutation_numba_twosample(x, y, n_perm, two_tailed=True): + """Perform permutation test. + + Parameters + ---------- + x : array_like + First distribution + y : array_like + Second distribution + n_perm : int + Number of permutations + two_tailed : bool, default: True + Set to False if you would like to perform a one-sampled permutation + test, else True + two_tailed : bool, default: True + Set to False if you would like to perform a one-tailed permutation + test, else True + + Returns + ------- + float + Estimated difference of distribution means + float + P-value of permutation test + """ + if two_tailed: + z = np.abs(np.mean(x) - np.mean(y)) + pS = np.concatenate((x, y), axis=0) + half = int(len(pS) / 2) + p = np.empty(n_perm) + # Run the simulation n_perm times + for i in np.arange(0, n_perm): + # Shuffle the data + np.random.shuffle(pS) + # Compute permuted absolute difference of the two sampled + # distributions + p[i] = np.abs(np.mean(pS[:half]) - np.mean(pS[half:])) + else: + z = np.mean(x) - np.mean(y) + pS = np.concatenate((x, y), axis=0) + half = int(len(pS) / 2) + p = np.empty(n_perm) + # Run the simulation n_perm times + for i in np.arange(0, n_perm): + # Shuffle the data + np.random.shuffle(pS) + # Compute permuted absolute difference of the two sampled + # distributions + p[i] = np.mean(pS[:half]) - np.mean(pS[half:]) + return z, (np.sum(p >= z)) / n_perm
+ + + +
+[docs] +def cluster_wise_p_val_correction(p_arr, p_sig=0.05, num_permutations=10000): + """Obtain cluster-wise corrected p values. + + Based on: https://github.com/neuromodulation/wjn_toolbox/blob/4745557040ad26f3b8498ca5d0c5d5dece2d3ba1/mypcluster.m + https://garstats.wordpress.com/2018/09/06/cluster/ + + Arguments + --------- + p_arr (np.array) : ndim, can be time series or image + p_sig (float) : significance level + num_permutations (int) : no. of random permutations of cluster comparisons + + Returns + ------- + p (float) : significance level of highest cluster + p_min_index : indices of significant samples + """ + from skimage.measure import label as measure_label + + labels, num_clusters = measure_label(p_arr <= p_sig, return_num=True) + + # loop through clusters of p_val series or image + index_cluster = {} + p_cluster_sum = np.zeros(num_clusters) + for cluster_i in np.arange(num_clusters): + # first cluster is assigned to be 1 from measure.label + index_cluster[cluster_i] = np.where(labels == cluster_i + 1)[0] + p_cluster_sum[cluster_i] = np.sum(np.array(1 - p_arr)[index_cluster[cluster_i]]) + # p_min corresponds to the most unlikely cluster + p_min = np.max(p_cluster_sum) + + p_min_index = index_cluster[np.argmax(p_cluster_sum)] + + # loop through random permutation cycles + r_per_arr = np.zeros(num_permutations) + for r in range(num_permutations): + r_per = np.random.randint(low=0, high=p_arr.shape[0], size=p_arr.shape[0]) + + labels, num_clusters = measure_label(p_arr[r_per] <= p_sig, return_num=True) + + index_cluster = {} + if num_clusters == 0: + r_per_arr[r] = 0 + else: + p_cluster_sum = np.zeros(num_clusters) + for cluster_i in np.arange(num_clusters): + index_cluster[cluster_i] = np.where(labels == cluster_i + 1)[ + 0 + ] # first cluster is assigned to be 1 from measure.label + p_cluster_sum[cluster_i] = np.sum( + np.array(1 - p_arr[r_per])[index_cluster[cluster_i]] + ) + # corresponds to the most unlikely cluster + r_per_arr[r] = np.max(p_cluster_sum) + + sorted_r = np.sort(r_per_arr) + + def find_arg_nearest(array, value): + array = np.asarray(array) + idx = (np.abs(array - value)).argmin() + return idx + + p = 1 - find_arg_nearest(sorted_r, p_min) / num_permutations + + return p, p_min_index
+ + + +# @njit +
+[docs] +def cluster_wise_p_val_correction_numba(p_arr, p_sig, n_perm): + """Calculate significant clusters and their corresponding p-values. + + Based on: + https://github.com/neuromodulation/wjn_toolbox/blob/4745557040ad26f3b8498ca5d0c5d5dece2d3ba1/mypcluster.m + https://garstats.wordpress.com/2018/09/06/cluster/ + + Arguments + --------- + p_arr : array-like + Array of p-values. WARNING: MUST be one-dimensional + p_sig : float + Significance level + n_perm : int + No. of random permutations for building cluster null-distribution + + Returns + ------- + p : list of floats + List of p-values for each cluster + p_min_index : list of numpy array + List of indices of each significant cluster + """ + + def cluster(iterable): + """Cluster 1-D array of boolean values. + + Parameters + ---------- + iterable : array-like of bool + Array to be clustered. + + Returns + ------- + cluster_labels : np.ndarray + Array of shape (len(iterable), 1), where each value indicates the + number of the cluster. Values are 0 if the item does not belong to + a cluster + cluster_count : int + Number of detected cluster. Corresponds to the highest value in + cluster_labels + """ + cluster_labels = np.zeros((len(iterable), 1)) + cluster_count = 0 + cluster_len = 0 + for idx, item in enumerate(iterable): + if item: + cluster_labels[idx] = cluster_count + 1 + cluster_len += 1 + elif cluster_len == 0: + pass + else: + cluster_len = 0 + cluster_count += 1 + if cluster_len >= 1: + cluster_count += 1 + return cluster_labels, cluster_count + + def calculate_null_distribution(p_arr_, p_sig_, n_perm_): + """Calculate null distribution of clusters. + + Parameters + ---------- + p_arr_ : numpy array + Array of p-values + p_sig_ : float + Significance level (p-value) + n_perm_ : int + No. of random permutations + + Returns + ------- + r_per_arr : numpy array + Null distribution of shape (n_perm_) + """ + # loop through random permutation cycles + r_per_arr = np.zeros(n_perm_) + for r in range(n_perm_): + r_per = np.random.randint(low=0, high=p_arr_.shape[0], size=p_arr_.shape[0]) + labels_, n_clusters = cluster(p_arr_[r_per] <= p_sig_) + + cluster_ind = {} + if n_clusters == 0: + r_per_arr[r] = 0 + else: + p_sum = np.zeros(n_clusters) + for ind in range(n_clusters): + cluster_ind[ind] = np.where(labels_ == ind + 1)[0] + p_sum[ind] = np.sum(np.asarray(1 - p_arr_[r_per])[cluster_ind[ind]]) + r_per_arr[r] = np.max(p_sum) + return r_per_arr + + labels, num_clusters = cluster(p_arr <= p_sig) + + null_distr = calculate_null_distribution(p_arr, p_sig, n_perm) + # Loop through clusters of p_val series or image + clusters = [] + p_vals = [np.float64(x) for x in range(0)] + # Cluster labels start at 1 + for cluster_i in range(num_clusters): + index_cluster = np.where(labels == cluster_i + 1)[0] + p_cluster_sum = np.sum(np.asarray(1 - p_arr)[index_cluster]) + p_val = 1 - np.sum(p_cluster_sum >= null_distr) / n_perm + if p_val <= p_sig: + clusters.append(index_cluster) + p_vals.append(p_val) + + return p_vals, clusters
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_stream_abc.html b/_modules/nm_stream_abc.html new file mode 100644 index 00000000..c67630c0 --- /dev/null +++ b/_modules/nm_stream_abc.html @@ -0,0 +1,678 @@ + + + + + + + + + + nm_stream_abc — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_stream_abc

+"""Module that contains NMStream ABC."""
+
+from abc import ABC, abstractmethod
+from pathlib import Path
+import pickle
+
+import pandas as pd
+
+from py_neuromodulation.nm_run_analysis import DataProcessor
+from py_neuromodulation.nm_settings import NMSettings
+from py_neuromodulation.nm_types import _PathLike, FeatureName
+from py_neuromodulation import nm_IO, PYNM_DIR
+
+
+
+[docs] +class NMStream(ABC): + def __init__( + self, + sfreq: float, + nm_channels: pd.DataFrame | _PathLike, + settings: "NMSettings | _PathLike | None" = None, + line_noise: float | None = 50, + sampling_rate_features_hz: float | None = None, + path_grids: _PathLike | None = None, + coord_names: list | None = None, + stream_name: str | None = "example_stream", + stream_lsl: bool = False, + coord_list: list | None = None, + verbose: bool = True, + ) -> None: + """Stream initialization + + Parameters + ---------- + sfreq : float + sampling frequency of data in Hertz + nm_channels : pd.DataFrame | _PathLike + parametrization of channels (see nm_define_channels.py for initialization) + settings : dict | _PathLike | None, optional + features settings can be a dictionary or path to the nm_settings.json, by default the py_neuromodulation/nm_settings.json are read + line_noise : float | None, optional + line noise, by default 50 + sampling_rate_features_hz : float | None, optional + feature sampling rate, by default None + path_grids : _PathLike | None, optional + path to grid_cortex.tsv and/or gird_subcortex.tsv, by default Non + coord_names : list | None, optional + coordinate name in the form [coord_1_name, coord_2_name, etc], by default None + coord_list : list | None, optional + coordinates in the form [[coord_1_x, coord_1_y, coord_1_z], [coord_2_x, coord_2_y, coord_2_z],], by default None + verbose : bool, optional + print out stream computation time information, by default True + """ + self.settings: NMSettings = NMSettings.load(settings) + + # If features that use frequency ranges are on, test them against nyquist frequency + use_freq_ranges: list[FeatureName] = [ + "bandpass_filter", + "stft", + "fft", + "welch", + "bursts", + "coherence", + "nolds", + "bispectrum", + ] + + need_nyquist_check = any( + (f in use_freq_ranges for f in self.settings.features.get_enabled()) + ) + + if need_nyquist_check: + assert all( + fb.frequency_high_hz < sfreq / 2 + for fb in self.settings.frequency_ranges_hz.values() + ), ( + "If a feature that uses frequency ranges is selected, " + "the frequency band ranges need to be smaller than the nyquist frequency.\n" + f"Got sfreq = {sfreq} and fband ranges:\n {self.settings.frequency_ranges_hz}" + ) + + if sampling_rate_features_hz is not None: + self.settings.sampling_rate_features_hz = sampling_rate_features_hz + + self.nm_channels = self._load_nm_channels(nm_channels) + if path_grids is None: + path_grids = PYNM_DIR + self.path_grids = path_grids + self.verbose = verbose + self.sfreq = sfreq + self.line_noise = line_noise + self.coord_names = coord_names + self.coord_list = coord_list + self.sess_right = None + self.projection = None + self.model = None + + self.data_processor = DataProcessor( + sfreq=self.sfreq, + settings=self.settings, + nm_channels=self.nm_channels, + path_grids=self.path_grids, + coord_names=coord_names, + coord_list=coord_list, + line_noise=line_noise, + verbose=self.verbose, + ) + +
+[docs] + @abstractmethod + def run(self) -> pd.DataFrame: + """Reinitialize the stream + This might be handy in case the nm_channels or nm_settings changed + """ + + self.data_processor = DataProcessor( + sfreq=self.sfreq, + settings=self.settings, + nm_channels=self.nm_channels, + path_grids=self.path_grids, + coord_names=self.coord_names, + coord_list=self.coord_list, + line_noise=self.line_noise, + verbose=self.verbose, + )
+ + + @abstractmethod + def _add_timestamp(self, feature_series: pd.Series, cnt_samples: int) -> pd.Series: + """Add to feature_series "time" keyword + For Bids specify with fs_features, for real time analysis with current time stamp + """ + + @staticmethod + def _get_sess_lat(coords: dict) -> bool: + if len(coords["cortex_left"]["positions"]) == 0: + return True + if len(coords["cortex_right"]["positions"]) == 0: + return False + raise ValueError( + "Either cortex_left or cortex_right positions must be provided." + ) + + @staticmethod + def _load_nm_channels( + nm_channels: pd.DataFrame | _PathLike, + ) -> pd.DataFrame: + if not isinstance(nm_channels, pd.DataFrame): + nm_channels = nm_IO.load_nm_channels(nm_channels) + + if nm_channels.query("used == 1 and target == 0").shape[0] == 0: + raise ValueError( + "No channels selected for analysis that have column 'used' = 1 and 'target' = 0. Please check your nm_channels" + ) + + return nm_channels + +
+[docs] + def load_model(self, model_name: _PathLike) -> None: + """Load sklearn model, that utilizes predict""" + with open(model_name, "rb") as fid: + self.model = pickle.load(fid)
+ + +
+[docs] + def save_after_stream( + self, + out_path_root: _PathLike = "", + folder_name: str = "sub", + feature_arr: pd.DataFrame | None = None, + ) -> None: + """Save features, settings, nm_channels and sidecar after run""" + + out_path_root = Path.cwd() if not out_path_root else Path(out_path_root) + + # create derivate folder_name output folder if doesn't exist + (out_path_root / folder_name).mkdir(parents=True, exist_ok=True) + + self.PATH_OUT = out_path_root + self.PATH_OUT_folder_name = folder_name + + self.save_sidecar(out_path_root, folder_name) + + if feature_arr is not None: + self.save_features(out_path_root, folder_name, feature_arr) + + self.save_settings(out_path_root, folder_name) + + self.save_nm_channels(out_path_root, folder_name)
+ + + def save_features( + self, + out_path_root: _PathLike, + folder_name: str, + feature_arr: pd.DataFrame, + ) -> None: + nm_IO.save_features(feature_arr, out_path_root, folder_name) + + def save_nm_channels(self, out_path_root: _PathLike, folder_name: str) -> None: + self.data_processor.save_nm_channels(out_path_root, folder_name) + + def save_settings(self, out_path_root: _PathLike, folder_name: str) -> None: + self.data_processor.save_settings(out_path_root, folder_name) + +
+[docs] + def save_sidecar(self, out_path_root: _PathLike, folder_name: str) -> None: + """Save sidecar incduing fs, coords, sess_right to + out_path_root and subfolder 'folder_name'""" + additional_args = {"sess_right": self.sess_right} + self.data_processor.save_sidecar(out_path_root, folder_name, additional_args)
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/nm_stream_offline.html b/_modules/nm_stream_offline.html new file mode 100644 index 00000000..42111a56 --- /dev/null +++ b/_modules/nm_stream_offline.html @@ -0,0 +1,865 @@ + + + + + + + + + + nm_stream_offline — py_neuromodulation documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for nm_stream_offline

+"""Module for offline data streams."""
+
+from typing import TYPE_CHECKING
+import numpy as np
+import pandas as pd
+from pathlib import Path
+from py_neuromodulation.nm_stream_abc import NMStream
+from py_neuromodulation.nm_types import _PathLike
+from py_neuromodulation import logger
+
+if TYPE_CHECKING:
+    from py_neuromodulation.nm_settings import NMSettings
+
+
+class _GenericStream(NMStream):
+    """_GenericStream base class.
+    This class can be inhereted for different types of offline streams
+
+    Parameters
+    ----------
+    nm_stream_abc : nm_stream_abc.NMStream
+    """
+
+    def _add_target(self, feature_dict: dict, data: np.ndarray) -> None:
+        """Add target channels to feature series.
+
+        Parameters
+        ----------
+        feature_series : pd.Series
+        data : np.ndarray
+            Raw data with shape (n_channels, n_samples). Channels not for feature computation are also included
+
+        Returns
+        -------
+        pd.Series
+            feature series with target channels added
+        """
+
+        if self.nm_channels["target"].sum() > 0:
+            if not self.target_idx_initialized:
+                self.target_indexes = self.nm_channels[
+                    self.nm_channels["target"] == 1
+                ].index
+                self.target_names = self.nm_channels.loc[
+                    self.target_indexes, "name"
+                ].to_list()
+                self.target_idx_initialized = True
+
+            for target_idx, target_name in zip(self.target_indexes, self.target_names):
+                feature_dict[target_name] = data[target_idx, -1]
+
+    def _add_timestamp(self, feature_dict: dict, cnt_samples: int) -> None:
+        """Add time stamp in ms.
+
+        Due to normalization DataProcessor needs to keep track of the counted
+        samples. These are accessed here for time conversion.
+        """
+        timestamp = cnt_samples * 1000 / self.sfreq
+        feature_dict["time"] = timestamp
+
+        if self.verbose:
+            logger.info("%.2f seconds of data processed", timestamp / 1000)
+
+    def _handle_data(self, data: np.ndarray | pd.DataFrame) -> np.ndarray:
+        names_expected = self.nm_channels["name"].to_list()
+
+        if isinstance(data, np.ndarray):
+            if not len(names_expected) == data.shape[0]:
+                raise ValueError(
+                    "If data is passed as an array, the first dimension must"
+                    " match the number of channel names in `nm_channels`.\n"
+                    f" Number of data channels (data.shape[0]): {data.shape[0]}\n"
+                    f' Length of nm_channels["name"]: {len(names_expected)}.'
+                )
+            return data
+
+        names_data = data.columns.to_list()
+        if not (
+            len(names_expected) == len(names_data)
+            and sorted(names_expected) == sorted(names_data)
+        ):
+            raise ValueError(
+                "If data is passed as a DataFrame, the"
+                "column names must match the channel names in `nm_channels`.\n"
+                f"Input dataframe column names: {names_data}\n"
+                f'Expected (from nm_channels["name"]): : {names_expected}.'
+            )
+        return data.to_numpy().transpose()
+
+    def _check_settings_for_parallel(self):
+        """Check specified settings and raise error if parallel processing is not possible.
+
+        Raises:
+            ValueError: depending on the settings, parallel processing is not possible
+        """
+
+        if "raw_normalization" in self.settings.preprocessing:
+            raise ValueError(
+                "Parallel processing is not possible with raw_normalization normalization."
+            )
+        if self.settings.postprocessing.feature_normalization:
+            raise ValueError(
+                "Parallel processing is not possible with feature normalization."
+            )
+        if self.settings.features.bursts:
+            raise ValueError(
+                "Parallel processing is not possible with burst estimation."
+            )
+
+    def _process_batch(self, data_batch, cnt_samples):
+        # if isinstance(data_batch, tuple):
+        #     data_batch = np.array(data_batch[1])
+
+        feature_dict = self.data_processor.process(data_batch[1].astype(np.float64))
+        self._add_timestamp(feature_dict, cnt_samples)
+        self._add_target(feature_dict, data_batch[1])
+        return feature_dict
+
+    def _run(
+        self,
+        data: np.ndarray | pd.DataFrame | None = None,
+        out_path_root: _PathLike = "",
+        folder_name: str = "sub",
+        is_stream_lsl: bool = True,
+        stream_lsl_name: str = None,
+        plot_lsl: bool = False,
+        parallel: bool = False,
+        n_jobs: int = -2,
+    ) -> pd.DataFrame:
+        from py_neuromodulation.nm_generator import raw_data_generator
+
+        if not is_stream_lsl:
+            generator = raw_data_generator(
+                data=data,
+                settings=self.settings,
+                sfreq=self.sfreq,
+            )
+        else:
+            from py_neuromodulation.nm_mnelsl_stream import LSLStream
+
+            self.lsl_stream = LSLStream(
+                settings=self.settings, stream_name=stream_lsl_name
+            )
+
+            if plot_lsl:
+                from mne_lsl.stream_viewer import StreamViewer
+
+                viewer = StreamViewer(stream_name=stream_lsl_name)
+                viewer.start()
+
+            if self.sfreq != self.lsl_stream.stream.sinfo.sfreq:
+                error_msg = (
+                    f"Sampling frequency of the lsl-stream ({self.lsl_stream.stream.sinfo.sfreq}) "
+                    f"does not match the settings ({self.sfreq})."
+                    "The sampling frequency read from the stream will be used"
+                )
+                logger.warning(error_msg)
+                self.sfreq = self.lsl_stream.stream.sinfo.sfreq
+
+            generator = self.lsl_stream.get_next_batch()
+
+        sample_add = self.sfreq / self.data_processor.sfreq_features
+
+        offset_time = self.settings.segment_length_features_ms
+        # offset_start = np.ceil(offset_time / 1000 * self.sfreq).astype(int)
+        offset_start = offset_time / 1000 * self.sfreq
+
+        if parallel:
+            from joblib import Parallel, delayed
+            from itertools import count
+
+            # parallel processing can not be utilized if a LSL stream is used
+            if is_stream_lsl:
+                error_msg = "Parallel processing is not possible with LSL stream."
+                logger.error(error_msg)
+                raise ValueError(error_msg)
+
+            l_features = Parallel(n_jobs=n_jobs, verbose=10)(
+                delayed(self._process_batch)(data_batch, cnt_samples)
+                for data_batch, cnt_samples in zip(
+                    generator, count(offset_start, sample_add)
+                )
+            )
+
+        else:
+            l_features: list[dict] = []
+            cnt_samples = offset_start
+
+            while True:
+                next_item = next(generator, None)
+
+                if next_item is not None:
+                    time_, data_batch = next_item
+                else:
+                    break
+
+                if data_batch is None:
+                    break
+                feature_dict = self.data_processor.process(
+                    data_batch.astype(np.float64)
+                )
+                self._add_timestamp(feature_dict, cnt_samples)
+                self._add_target(feature_dict, data_batch)
+
+                l_features.append(feature_dict)
+
+                cnt_samples += sample_add
+
+        feature_df = pd.DataFrame.from_records(l_features).astype(np.float64)
+
+        self.save_after_stream(out_path_root, folder_name, feature_df)
+
+        return feature_df
+
+    def plot_raw_signal(
+        self,
+        sfreq: float | None = None,
+        data: np.ndarray | None = None,
+        lowpass: float | None = None,
+        highpass: float | None = None,
+        picks: list | None = None,
+        plot_time: bool = True,
+        plot_psd: bool = False,
+    ) -> None:
+        """Use MNE-RawArray Plot to investigate PSD or raw_signal plot.
+
+        Parameters
+        ----------
+        sfreq : float
+            sampling frequency [Hz]
+        data : np.ndarray, optional
+            data (n_channels, n_times), by default None
+        plot_time : bool, optional
+            mne.io.RawArray.plot(), by default True
+        plot_psd : bool, optional
+            mne.io.RawArray.plot(), by default True
+
+        Raises
+        ------
+        ValueError
+            raise Exception when no data is passed
+        """
+        if self.data is None and data is None:
+            raise ValueError("No data passed to plot_raw_signal function.")
+
+        if data is None and self.data is not None:
+            data = self.data
+
+        if sfreq is None:
+            sfreq = self.sfreq
+
+        if self.nm_channels is not None:
+            ch_names = self.nm_channels["name"].to_list()
+            ch_types = self.nm_channels["type"].to_list()
+        else:
+            ch_names = [f"ch_{i}" for i in range(data.shape[0])]
+            ch_types = ["ecog" for i in range(data.shape[0])]
+
+        from mne import create_info
+        from mne.io import RawArray
+
+        info = create_info(ch_names=ch_names, sfreq=sfreq, ch_types=ch_types)
+        raw = RawArray(data, info)
+
+        if picks is not None:
+            raw = raw.pick(picks)
+        self.raw = raw
+        if plot_time:
+            raw.plot(highpass=highpass, lowpass=lowpass)
+        if plot_psd:
+            raw.compute_psd().plot()
+
+
+
+[docs] +class Stream(_GenericStream): + def __init__( + self, + sfreq: float, + data: np.ndarray | pd.DataFrame | None = None, + nm_channels: pd.DataFrame | _PathLike | None = None, + settings: "NMSettings | _PathLike | None" = None, + sampling_rate_features_hz: float | None = None, + line_noise: float | None = 50, + path_grids: _PathLike | None = None, + coord_names: list | None = None, + coord_list: list | None = None, + verbose: bool = True, + ) -> None: + """Stream initialization + + Parameters + ---------- + sfreq : float + sampling frequency of data in Hertz + data : np.ndarray | pd.DataFrame | None, optional + data to be streamed with shape (n_channels, n_time), by default None + nm_channels : pd.DataFrame | _PathLike + parametrization of channels (see nm_define_channels.py for initialization) + settings : dict | _PathLike | None, optional + features settings can be a dictionary or path to the nm_settings.json, by default the py_neuromodulation/nm_settings.json are read + line_noise : float | None, optional + line noise, by default 50 + sampling_rate_features_hz : float | None, optional + feature sampling rate, by default None + path_grids : _PathLike | None, optional + path to grid_cortex.tsv and/or gird_subcortex.tsv, by default Non + coord_names : list | None, optional + coordinate name in the form [coord_1_name, coord_2_name, etc], by default None + coord_list : list | None, optional + coordinates in the form [[coord_1_x, coord_1_y, coord_1_z], [coord_2_x, coord_2_y, coord_2_z],], by default None + verbose : bool, optional + log stream computation time information, by default True + """ + + if nm_channels is None and data is not None: + from py_neuromodulation.nm_define_nmchannels import ( + get_default_channels_from_data, + ) + + nm_channels = get_default_channels_from_data(data) + + if nm_channels is None and data is None: + raise ValueError( + "Either `nm_channels` or `data` must be passed to `Stream`." + ) + + super().__init__( + sfreq=sfreq, + nm_channels=nm_channels, + settings=settings, + line_noise=line_noise, + sampling_rate_features_hz=sampling_rate_features_hz, + path_grids=path_grids, + coord_names=coord_names, + coord_list=coord_list, + verbose=verbose, + ) + + self.data = data + + self.target_idx_initialized: bool = False + +
+[docs] + def run( + self, + data: np.ndarray | pd.DataFrame | None = None, + out_path_root: _PathLike = Path.cwd(), + folder_name: str = "sub", + parallel: bool = False, + n_jobs: int = -2, + stream_lsl: bool = False, + stream_lsl_name: str = None, + plot_lsl: bool = False, + ) -> pd.DataFrame: + """Call run function for offline stream. + + Parameters + ---------- + data : np.ndarray | pd.DataFrame + shape (n_channels, n_time) + out_path_root : _PathLike | None, optional + Full path to store estimated features, by default None + If None, data is simply returned and not saved + folder_name : str, optional + folder output name, commonly subject or run name, by default "sub" + + Returns + ------- + pd.DataFrame + feature DataFrame + """ + + super().run() # reinitialize the stream + + self.stream_lsl = stream_lsl + self.stream_lsl_name = stream_lsl_name + + if data is not None: + data = self._handle_data(data) + elif self.data is not None: + data = self._handle_data(self.data) + elif self.data is None and data is None and self.stream_lsl is False: + raise ValueError("No data passed to run function.") + + if parallel: + self._check_settings_for_parallel() + + out_path = Path(out_path_root, folder_name) + out_path.mkdir(parents=True, exist_ok=True) + logger.log_to_file(out_path) + + return self._run( + data, + out_path_root, + folder_name, + parallel=parallel, + n_jobs=n_jobs, + is_stream_lsl=stream_lsl, + stream_lsl_name=stream_lsl_name, + plot_lsl=plot_lsl, + )
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_sources/api_documentation.rst.txt b/_sources/api_documentation.rst.txt new file mode 100644 index 00000000..a3966605 --- /dev/null +++ b/_sources/api_documentation.rst.txt @@ -0,0 +1,51 @@ +API Documentation +================= + +Parametrization +--------------- + +.. toctree:: + :maxdepth: 4 + + nm_stream_abc + nm_stream_offline + nm_settings + nm_define_nmchannels + nm_generator + nm_run_analysis + nm_resample + nm_normalization + nm_rereference + nm_projection + nm_IO + +Feature Estimation +------------------ + +.. toctree:: + :maxdepth: 4 + + nm_features + nm_filter + nm_oscillatory + nm_fooof + nm_kalmanfilter + nm_hjorth + nm_sharpwaves + nm_bursts + nm_coherence + nm_nolds + nm_mne_connectivity + nm_linelength + +Analysis +-------- + +.. toctree:: + :maxdepth: 4 + + nm_analysis + nm_decode + nm_plots + nm_RMAP + nm_stats diff --git a/_sources/auto_examples/index.rst.txt b/_sources/auto_examples/index.rst.txt new file mode 100644 index 00000000..625ff9d0 --- /dev/null +++ b/_sources/auto_examples/index.rst.txt @@ -0,0 +1,207 @@ +:orphan: + +.. _examples-index: + +Examples +======== + + + +.. raw:: html + +
+ +.. thumbnail-parent-div-open + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/images/thumb/sphx_glr_plot_0_first_demo_thumb.png + :alt: + + :ref:`sphx_glr_auto_examples_plot_0_first_demo.py` + +.. raw:: html + +
First Demo
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/images/thumb/sphx_glr_plot_1_example_BIDS_thumb.png + :alt: + + :ref:`sphx_glr_auto_examples_plot_1_example_BIDS.py` + +.. raw:: html + +
ECoG Movement decoding example
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/images/thumb/sphx_glr_plot_2_example_add_feature_thumb.png + :alt: + + :ref:`sphx_glr_auto_examples_plot_2_example_add_feature.py` + +.. raw:: html + +
Adding New Features
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/images/thumb/sphx_glr_plot_3_example_sharpwave_analysis_thumb.png + :alt: + + :ref:`sphx_glr_auto_examples_plot_3_example_sharpwave_analysis.py` + +.. raw:: html + +
Analyzing temporal features
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/images/thumb/sphx_glr_plot_4_example_gridPointProjection_thumb.png + :alt: + + :ref:`sphx_glr_auto_examples_plot_4_example_gridPointProjection.py` + +.. raw:: html + +
Grid Point Projection
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/images/thumb/sphx_glr_plot_5_example_rmap_computing_thumb.png + :alt: + + :ref:`sphx_glr_auto_examples_plot_5_example_rmap_computing.py` + +.. raw:: html + +
R-Map computation
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/images/thumb/sphx_glr_plot_6_real_time_demo_thumb.png + :alt: + + :ref:`sphx_glr_auto_examples_plot_6_real_time_demo.py` + +.. raw:: html + +
Real-time feature estimation
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/images/thumb/sphx_glr_plot_7_lsl_example_thumb.png + :alt: + + :ref:`sphx_glr_auto_examples_plot_7_lsl_example.py` + +.. raw:: html + +
Lab Streaming Layer (LSL) Example
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/images/thumb/sphx_glr_plot_8_cebra_example_thumb.png + :alt: + + :ref:`sphx_glr_auto_examples_plot_8_cebra_example.py` + +.. raw:: html + +
Cebra Decoding with no training Example
+
+ + +.. thumbnail-parent-div-close + +.. raw:: html + +
+ + +.. toctree:: + :hidden: + + /auto_examples/plot_0_first_demo + /auto_examples/plot_1_example_BIDS + /auto_examples/plot_2_example_add_feature + /auto_examples/plot_3_example_sharpwave_analysis + /auto_examples/plot_4_example_gridPointProjection + /auto_examples/plot_5_example_rmap_computing + /auto_examples/plot_6_real_time_demo + /auto_examples/plot_7_lsl_example + /auto_examples/plot_8_cebra_example + + +.. only:: html + + .. container:: sphx-glr-footer sphx-glr-footer-gallery + + .. container:: sphx-glr-download sphx-glr-download-python + + :download:`Download all examples in Python source code: auto_examples_python.zip ` + + .. container:: sphx-glr-download sphx-glr-download-jupyter + + :download:`Download all examples in Jupyter notebooks: auto_examples_jupyter.zip ` + + +.. only:: html + + .. rst-class:: sphx-glr-signature + + `Gallery generated by Sphinx-Gallery `_ diff --git a/_sources/auto_examples/plot_0_first_demo.rst.txt b/_sources/auto_examples/plot_0_first_demo.rst.txt new file mode 100644 index 00000000..b38fb43a --- /dev/null +++ b/_sources/auto_examples/plot_0_first_demo.rst.txt @@ -0,0 +1,691 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples/plot_0_first_demo.py" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. note:: + :class: sphx-glr-download-link-note + + :ref:`Go to the end ` + to download the full example code. + +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_auto_examples_plot_0_first_demo.py: + + +First Demo +========== + +This Demo will showcase the feature estimation and +exemplar analysis using simulated data. + +.. GENERATED FROM PYTHON SOURCE LINES 8-16 + +.. code-block:: Python + + + import numpy as np + from matplotlib import pyplot as plt + + import py_neuromodulation as nm + + from py_neuromodulation import nm_analysis, nm_define_nmchannels, nm_plots, NMSettings + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 17-20 + +Data Simulation +--------------- +We will now generate some exemplar data with 10 second duration for 6 channels with a sample rate of 1 kHz. + +.. GENERATED FROM PYTHON SOURCE LINES 20-47 + +.. code-block:: Python + + + + def generate_random_walk(NUM_CHANNELS, TIME_DATA_SAMPLES): + # from https://towardsdatascience.com/random-walks-with-python-8420981bc4bc + dims = NUM_CHANNELS + step_n = TIME_DATA_SAMPLES - 1 + step_set = [-1, 0, 1] + origin = (np.random.random([1, dims]) - 0.5) * 1 # Simulate steps in 1D + step_shape = (step_n, dims) + steps = np.random.choice(a=step_set, size=step_shape) + path = np.concatenate([origin, steps]).cumsum(0) + return path.T + + + NUM_CHANNELS = 6 + sfreq = 1000 + TIME_DATA_SAMPLES = 10 * sfreq + data = generate_random_walk(NUM_CHANNELS, TIME_DATA_SAMPLES) + time = np.arange(0, TIME_DATA_SAMPLES / sfreq, 1 / sfreq) + + plt.figure(figsize=(8, 4), dpi=100) + for ch_idx in range(data.shape[0]): + plt.plot(time, data[ch_idx, :]) + plt.xlabel("Time [s]") + plt.ylabel("Amplitude") + plt.title("Example random walk data") + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_0_first_demo_001.png + :alt: Example random walk data + :srcset: /auto_examples/images/sphx_glr_plot_0_first_demo_001.png + :class: sphx-glr-single-img + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + + Text(0.5, 1.0, 'Example random walk data') + + + +.. GENERATED FROM PYTHON SOURCE LINES 48-95 + +Now let’s define the necessary setup files we will be using for data +preprocessing and feature estimation. Py_neuromodualtion is based on two +parametrization files: the *nm_channels.tsv* and the *nm_setting.json*. + +nm_channels +~~~~~~~~~~~ + +The *nm_channel* dataframe. This dataframe contains the columns + ++-----------------------------------+-----------------------------------+ +| Column name | Description | ++===================================+===================================+ +| **name** | name of the channel | ++-----------------------------------+-----------------------------------+ +| **rereference** | different channel name for | +| | bipolar re-referencing, or | +| | average for common average | +| | re-referencing | ++-----------------------------------+-----------------------------------+ +| **used** | 0 or 1, channel selection | ++-----------------------------------+-----------------------------------+ +| **target** | 0 or 1, for some decoding | +| | applications we can define target | +| | channels, e.g. EMG channels | ++-----------------------------------+-----------------------------------+ +| **type** | channel type according to the | +| | `mne-python`_ toolbox | +| | | +| | | +| | | +| | | +| | e.g. ecog, eeg, ecg, emg, dbs, | +| | seeg etc. | ++-----------------------------------+-----------------------------------+ +| **status** | good or bad, used for channel | +| | quality indication | ++-----------------------------------+-----------------------------------+ +| **new_name** | this keyword can be specified to | +| | indicate for example the used | +| | rereferncing scheme | ++-----------------------------------+-----------------------------------+ + +.. _mne-python: https://mne.tools/stable/auto_tutorials/raw/10_raw_overview.html#sphx-glr-auto-tutorials-raw-10-raw-overview-py + +The :class:`~nm_stream_abc` can either be created as a *.tsv* text file, or as a pandas +DataFrame. There are some helper functions that let you create the +nm_channels without much effort: + +.. GENERATED FROM PYTHON SOURCE LINES 95-102 + +.. code-block:: Python + + + nm_channels = nm_define_nmchannels.get_default_channels_from_data( + data, car_rereferencing=True + ) + + nm_channels + + + + + + +.. raw:: html + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namerereferenceusedtargettypestatusnew_name
0ch0average10ecoggoodch0-avgref
1ch1average10ecoggoodch1-avgref
2ch2average10ecoggoodch2-avgref
3ch3average10ecoggoodch3-avgref
4ch4average10ecoggoodch4-avgref
5ch5average10ecoggoodch5-avgref
+
+
+
+
+ +.. GENERATED FROM PYTHON SOURCE LINES 103-109 + +Using this function default channel names and a common average re-referencing scheme is specified. +Alternatively the *nm_define_nmchannels.set_channels* function can be used to pass each column values. + +nm_settings +----------- +Next, we will initialize the nm_settings dictionary and use the default settings, reset them, and enable a subset of features: + +.. GENERATED FROM PYTHON SOURCE LINES 109-113 + +.. code-block:: Python + + + settings = NMSettings.get_fast_compute() + + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 114-132 + +The setting itself is a .json file which contains the parametrization for preprocessing, feature estimation, postprocessing and +definition with which sampling rate features are being calculated. +In this example `sampling_rate_features_hz` is specified to be 10 Hz, so every 100ms a new set of features is calculated. + +For many features the `segment_length_features_ms` specifies the time dimension of the raw signal being used for feature calculation. Here it is specified to be 1000 ms. + +We will now enable the features: + +* fft +* bursts +* sharpwave + +and stay with the default preprcessing methods: + +* notch_filter +* re_referencing + +and use *z-score* postprocessing normalization. + +.. GENERATED FROM PYTHON SOURCE LINES 132-138 + +.. code-block:: Python + + + settings.features.fooof = True + settings.features.fft = True + settings.features.bursts = True + settings.features.sharpwave_analysis = True + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 139-140 + +We are now ready to go to instantiate the *Stream* and call the *run* method for feature estimation: + +.. GENERATED FROM PYTHON SOURCE LINES 140-151 + +.. code-block:: Python + + + stream = nm.Stream( + settings=settings, + nm_channels=nm_channels, + verbose=True, + sfreq=sfreq, + line_noise=50, + ) + + features = stream.run(data) + + + + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + /home/runner/.venv/lib/python3.10/site-packages/fooof/core/funcs.py:62: RuntimeWarning: invalid value encountered in log10 + ys = offset - np.log10(knee + xs**exp) + + + + +.. GENERATED FROM PYTHON SOURCE LINES 152-157 + +Feature Analysis +---------------- + +There is a lot of output, which we could omit by verbose being False, but let's have a look what was being computed. +We will therefore use the :class:`~nm_analysis` class to showcase some functions. For multi-run -or subject analysis we will pass here the feature_file "sub" as default directory: + +.. GENERATED FROM PYTHON SOURCE LINES 157-162 + +.. code-block:: Python + + + analyzer = nm_analysis.FeatureReader( + feature_dir=stream.PATH_OUT, feature_file=stream.PATH_OUT_folder_name + ) + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 163-164 + +Let's have a look at the resulting "feature_arr" DataFrame: + +.. GENERATED FROM PYTHON SOURCE LINES 164-167 + +.. code-block:: Python + + + analyzer.feature_arr.iloc[:10, :7] + + + + + + +.. raw:: html + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ch0-avgref_fft_theta_meanch1-avgref_fft_theta_meanch2-avgref_fft_theta_meanch3-avgref_fft_theta_meanch4-avgref_fft_theta_meanch5-avgref_fft_theta_meanch0-avgref_fft_alpha_mean
02.8953783.0717352.8216443.1207092.9583012.8562662.732174
11.000000-1.000000-1.000000-1.0000001.000000-1.0000001.000000
20.687889-0.054673-1.1049040.097095-0.344390-0.8525210.101037
3-0.012401-1.4479051.207557-1.080756-0.6314200.692414-1.247014
40.270615-0.860726-0.1847400.1348730.9987991.950749-0.344932
51.927219-0.727014-0.364885-0.2492860.530672-0.633928-1.265427
60.0002170.9387561.319935-0.3149050.0646040.786164-0.999362
7-1.635194-0.0898410.804808-1.8161791.1672150.7480550.952590
82.1837380.5021030.059130-0.7498440.3406980.4956152.173983
91.3833440.6880580.975407-0.9466631.248008-1.0150451.500909
+
+
+
+
+ +.. GENERATED FROM PYTHON SOURCE LINES 168-177 + +Seems like a lot of features were calculated. The `time` column tells us about each row time index. +For the 6 specified channels, it is each 31 features. +We can now use some in-built plotting functions for visualization. + +.. note:: + + Due to the nature of simulated data, some of the features have constant values, which are not displayed through the image normalization. + + + +.. GENERATED FROM PYTHON SOURCE LINES 177-180 + +.. code-block:: Python + + + analyzer.plot_all_features(ch_used="ch1") + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_0_first_demo_002.png + :alt: Feature Plot sub + :srcset: /auto_examples/images/sphx_glr_plot_0_first_demo_002.png + :class: sphx-glr-single-img + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 181-187 + +.. code-block:: Python + + nm_plots.plot_corr_matrix( + figsize=(25, 25), + show_plot=True, + feature=analyzer.feature_arr, + ) + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_0_first_demo_003.png + :alt: Correlation matrix + :srcset: /auto_examples/images/sphx_glr_plot_0_first_demo_003.png + :class: sphx-glr-single-img + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 188-190 + +The upper correlation matrix shows the correlation of every feature of every channel to every other. +This notebook demonstrated a first demo how features can quickly be generated. For further feature modalities and decoding applications check out the next notebooks. + + +.. rst-class:: sphx-glr-timing + + **Total running time of the script:** (0 minutes 6.226 seconds) + + +.. _sphx_glr_download_auto_examples_plot_0_first_demo.py: + +.. only:: html + + .. container:: sphx-glr-footer sphx-glr-footer-example + + .. container:: sphx-glr-download sphx-glr-download-jupyter + + :download:`Download Jupyter notebook: plot_0_first_demo.ipynb ` + + .. container:: sphx-glr-download sphx-glr-download-python + + :download:`Download Python source code: plot_0_first_demo.py ` + + +.. only:: html + + .. rst-class:: sphx-glr-signature + + `Gallery generated by Sphinx-Gallery `_ diff --git a/_sources/auto_examples/plot_1_example_BIDS.rst.txt b/_sources/auto_examples/plot_1_example_BIDS.rst.txt new file mode 100644 index 00000000..5543f7b3 --- /dev/null +++ b/_sources/auto_examples/plot_1_example_BIDS.rst.txt @@ -0,0 +1,758 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples/plot_1_example_BIDS.py" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. note:: + :class: sphx-glr-download-link-note + + :ref:`Go to the end ` + to download the full example code. + +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_auto_examples_plot_1_example_BIDS.py: + + +ECoG Movement decoding example +============================== + +.. GENERATED FROM PYTHON SOURCE LINES 8-17 + +This example notebook read openly accessible data from the publication +*Electrocorticography is superior to subthalamic local field potentials +for movement decoding in Parkinson’s disease* +(`Merk et al. 2022 _`). +The dataset is available `here `_. + +For simplicity one example subject is automatically shipped within +this repo at the *py_neuromodulation/data* folder, stored in +`iEEG BIDS `_ format. + +.. GENERATED FROM PYTHON SOURCE LINES 19-32 + +.. code-block:: Python + + from sklearn import metrics, model_selection, linear_model + import matplotlib.pyplot as plt + + import py_neuromodulation as nm + from py_neuromodulation import ( + nm_analysis, + nm_decode, + nm_define_nmchannels, + nm_IO, + nm_plots, + NMSettings, + ) + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 33-36 + +Let's read the example using `mne_bids `_. +The resulting raw object is of type `mne.RawArray `_. +We can use the properties such as sampling frequency, channel names, channel types all from the mne array and create the *nm_channels* DataFrame: + +.. GENERATED FROM PYTHON SOURCE LINES 36-67 + +.. code-block:: Python + + + ( + RUN_NAME, + PATH_RUN, + PATH_BIDS, + PATH_OUT, + datatype, + ) = nm_IO.get_paths_example_data() + + ( + raw, + data, + sfreq, + line_noise, + coord_list, + coord_names, + ) = nm_IO.read_BIDS_data( + PATH_RUN=PATH_RUN + ) + + nm_channels = nm_define_nmchannels.set_channels( + ch_names=raw.ch_names, + ch_types=raw.get_channel_types(), + reference="default", + bads=raw.info["bads"], + new_names="default", + used_types=("ecog", "dbs", "seeg"), + target_keywords=["MOV_RIGHT"], + ) + + + + + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + Extracting parameters from /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vhdr... + Setting channel info structure... + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: Did not find any events.tsv associated with sub-testsub_ses-EphysMedOff_task-gripforce_run-0. + + The search_str was "/home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/**/ieeg/sub-testsub_ses-EphysMedOff*events.tsv" + raw_arr = read_raw_bids(bids_path) + Reading channel info from /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_channels.tsv. + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: The unit for channel(s) MOV_RIGHT has changed from V to NA. + raw_arr = read_raw_bids(bids_path) + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: Other is not an MNE-Python coordinate frame for IEEG data and so will be set to 'unknown' + raw_arr = read_raw_bids(bids_path) + Reading electrode coords from /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_electrodes.tsv. + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: There are channels without locations (n/a) that are not marked as bad: ['MOV_RIGHT'] + raw_arr = read_raw_bids(bids_path) + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: Not setting position of 1 misc channel found in montage: + ['MOV_RIGHT'] + Consider setting the channel types to be of EEG/sEEG/ECoG/DBS/fNIRS using inst.set_channel_types before calling inst.set_montage, or omit these channels when creating your montage. + raw_arr = read_raw_bids(bids_path) + + + + +.. GENERATED FROM PYTHON SOURCE LINES 68-70 + +This example contains the grip force movement traces, we'll use the *MOV_RIGHT* channel as a decoding target channel. +Let's check some of the raw feature and time series traces: + +.. GENERATED FROM PYTHON SOURCE LINES 70-88 + +.. code-block:: Python + + + plt.figure(figsize=(12, 4), dpi=300) + plt.subplot(121) + plt.plot(raw.times, data[-1, :]) + plt.xlabel("Time [s]") + plt.ylabel("a.u.") + plt.title("Movement label") + plt.xlim(0, 20) + + plt.subplot(122) + for idx, ch_name in enumerate(nm_channels.query("used == 1").name): + plt.plot(raw.times, data[idx, :] + idx * 300, label=ch_name) + plt.legend(bbox_to_anchor=(1, 0.5), loc="center left") + plt.title("ECoG + STN-LFP time series") + plt.xlabel("Time [s]") + plt.ylabel("Voltage a.u.") + plt.xlim(0, 20) + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_1_example_BIDS_001.png + :alt: Movement label, ECoG + STN-LFP time series + :srcset: /auto_examples/images/sphx_glr_plot_1_example_BIDS_001.png + :class: sphx-glr-single-img + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + + (0.0, 20.0) + + + +.. GENERATED FROM PYTHON SOURCE LINES 89-107 + +.. code-block:: Python + + settings = NMSettings.get_fast_compute() + + settings.features.welch = True + settings.features.fft = True + settings.features.bursts = True + settings.features.sharpwave_analysis = True + settings.features.coherence = True + + settings.coherence.channels = [("LFP_RIGHT_0-LFP_RIGHT_2", "ECOG_RIGHT_0-avgref")] + # TONI: this example was failing because the rereferenced channel have different names than originals + # We need to handle ch_names being changed after reref with settings.coherence.channels validation + + settings.coherence.frequency_bands = ["high beta", "low gamma"] + settings.sharpwave_analysis_settings.estimator["mean"] = [] + settings.sharpwave_analysis_settings.sharpwave_features.enable_all() + for sw_feature in settings.sharpwave_analysis_settings.sharpwave_features.list_all(): + settings.sharpwave_analysis_settings.estimator["mean"].append(sw_feature) + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 108-118 + +.. code-block:: Python + + stream = nm.Stream( + sfreq=sfreq, + nm_channels=nm_channels, + settings=settings, + line_noise=line_noise, + coord_list=coord_list, + coord_names=coord_names, + verbose=True, + ) + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 119-125 + +.. code-block:: Python + + features = stream.run( + data=data, + out_path_root=PATH_OUT, + folder_name=RUN_NAME, + ) + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 126-129 + +Feature Analysis Movement +------------------------- +The obtained performances can now be read and visualized using the :class:`nm_analysis.Feature_Reader`. + +.. GENERATED FROM PYTHON SOURCE LINES 129-138 + +.. code-block:: Python + + + # initialize analyzer + feature_reader = nm_analysis.FeatureReader( + feature_dir=PATH_OUT, + feature_file=RUN_NAME, + ) + feature_reader.label_name = "MOV_RIGHT" + feature_reader.label = feature_reader.feature_arr["MOV_RIGHT"] + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 139-141 + +.. code-block:: Python + + feature_reader.feature_arr.iloc[100:108, -6:] + + + + + + +.. raw:: html + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ECOG_RIGHT_5-avgref_bursts_low gamma_amplitude_meanECOG_RIGHT_5-avgref_bursts_low gamma_amplitude_maxECOG_RIGHT_5-avgref_bursts_low gamma_burst_rate_per_sECOG_RIGHT_5-avgref_bursts_low gamma_in_bursttimeMOV_RIGHT
100-0.588752-1.065146-1.597785-0.79311611000-0.150107
101-0.684612-1.058021-1.440749-0.78679611100-0.280238
102-0.369665-0.399318-0.7842631.25499011200-0.305062
103-0.864353-1.078232-2.376770-0.79056911300-0.312851
104-0.785546-1.094515-1.881985-0.78446511400-0.305154
105-0.849785-0.987497-2.0585291.25911311500-0.305200
106-1.095902-1.288603-2.455767-0.78817011600-0.311440
107-1.048095-1.244034-2.067352-0.78226611700-0.306840
+
+
+
+
+ +.. GENERATED FROM PYTHON SOURCE LINES 142-144 + +.. code-block:: Python + + print(feature_reader.feature_arr.shape) + + + + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + (181, 552) + + + + +.. GENERATED FROM PYTHON SOURCE LINES 145-147 + +.. code-block:: Python + + feature_reader._get_target_ch() + + + + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + + 'MOV_RIGHT' + + + +.. GENERATED FROM PYTHON SOURCE LINES 148-158 + +.. code-block:: Python + + feature_reader.plot_target_averaged_channel( + ch="ECOG_RIGHT_0", + list_feature_keywords=None, + epoch_len=4, + threshold=0.5, + ytick_labelsize=7, + figsize_x=12, + figsize_y=12, + ) + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_1_example_BIDS_002.png + :alt: Movement aligned features channel: ECOG_RIGHT_0, MOV_RIGHT + :srcset: /auto_examples/images/sphx_glr_plot_1_example_BIDS_002.png + :class: sphx-glr-single-img + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 159-170 + +.. code-block:: Python + + feature_reader.plot_all_features( + ytick_labelsize=6, + clim_low=-2, + clim_high=2, + ch_used="ECOG_RIGHT_0", + time_limit_low_s=0, + time_limit_high_s=20, + normalize=True, + save=True, + ) + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_1_example_BIDS_003.png + :alt: Feature Plot sub-testsub_ses-EphysMedOff_task-gripforce_run-0 + :srcset: /auto_examples/images/sphx_glr_plot_1_example_BIDS_003.png + :class: sphx-glr-single-img + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 171-182 + +.. code-block:: Python + + nm_plots.plot_corr_matrix( + feature=feature_reader.feature_arr.filter(regex="ECOG_RIGHT_0"), + ch_name="ECOG_RIGHT_0-avgref", + feature_names=list( + feature_reader.feature_arr.filter(regex="ECOG_RIGHT_0-avgref").columns + ), + feature_file=feature_reader.feature_file, + show_plot=True, + figsize=(15, 15), + ) + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_1_example_BIDS_004.png + :alt: Correlation matrix features channel: ECOG_RIGHT_0-avgref + :srcset: /auto_examples/images/sphx_glr_plot_1_example_BIDS_004.png + :class: sphx-glr-single-img + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 183-196 + +Decoding +-------- + +The main focus of the *py_neuromodulation* pipeline is feature estimation. +Nevertheless, the user can also use the pipeline for machine learning decoding. +It can be used for regression and classification problems and also dimensionality reduction such as PCA and CCA. + +Here, we show an example using the XGBOOST classifier. The used labels came from a continuous grip force movement target, named "MOV_RIGHT". + +First we initialize the :class:`~nm_decode.Decoder` class, which the specified *validation method*, here being a simple 3-fold cross validation, +the evaluation metric, used machine learning model, and the channels we want to evaluate performances for. + +There are many more implemented methods, but we will here limit it to the ones presented. + +.. GENERATED FROM PYTHON SOURCE LINES 196-209 + +.. code-block:: Python + + + model = linear_model.LinearRegression() + + feature_reader.decoder = nm_decode.Decoder( + features=feature_reader.feature_arr, + label=feature_reader.label, + label_name=feature_reader.label_name, + used_chs=feature_reader.used_chs, + model=model, + eval_method=metrics.r2_score, + cv_method=model_selection.KFold(n_splits=3, shuffle=True), + ) + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 210-217 + +.. code-block:: Python + + performances = feature_reader.run_ML_model( + estimate_channels=True, + estimate_gridpoints=False, + estimate_all_channels_combined=True, + save_results=True, + ) + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 218-219 + +The performances are a dictionary that can be transformed into a DataFrame: + +.. GENERATED FROM PYTHON SOURCE LINES 219-224 + +.. code-block:: Python + + + df_per = feature_reader.get_dataframe_performances(performances) + + df_per + + + + + + +.. raw:: html + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
performance_testperformance_trainsubchch_type
00.0465810.815065testsubLFP_RIGHT_0-LFP_RIGHT_2electrode ch
10.0434350.626857testsubLFP_RIGHT_1-LFP_RIGHT_0electrode ch
20.0000000.657631testsubLFP_RIGHT_2-LFP_RIGHT_1electrode ch
30.0000000.740562testsubECOG_RIGHT_0-avgrefelectrode ch
40.0000000.742434testsubECOG_RIGHT_1-avgrefelectrode ch
50.0000000.774169testsubECOG_RIGHT_2-avgrefelectrode ch
60.0446670.780339testsubECOG_RIGHT_3-avgrefelectrode ch
70.0063900.718939testsubECOG_RIGHT_4-avgrefelectrode ch
80.0621890.754536testsubECOG_RIGHT_5-avgrefelectrode ch
90.4334611.000000testsuball_ch_combinedall ch combinded
+
+
+
+
+ +.. GENERATED FROM PYTHON SOURCE LINES 225-237 + +.. code-block:: Python + + ax = nm_plots.plot_df_subjects( + df_per, + x_col="sub", + y_col="performance_test", + hue="ch_type", + PATH_SAVE=PATH_OUT / RUN_NAME / (RUN_NAME + "_decoding_performance.png"), + figsize_tuple=(8, 5), + ) + ax.set_ylabel(r"$R^2$ Correlation") + ax.set_xlabel("Subject 000") + ax.set_title("Performance comparison Movement decoding") + plt.tight_layout() + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_1_example_BIDS_005.png + :alt: Performance comparison Movement decoding + :srcset: /auto_examples/images/sphx_glr_plot_1_example_BIDS_005.png + :class: sphx-glr-single-img + + + + + + +.. rst-class:: sphx-glr-timing + + **Total running time of the script:** (0 minutes 11.072 seconds) + + +.. _sphx_glr_download_auto_examples_plot_1_example_BIDS.py: + +.. only:: html + + .. container:: sphx-glr-footer sphx-glr-footer-example + + .. container:: sphx-glr-download sphx-glr-download-jupyter + + :download:`Download Jupyter notebook: plot_1_example_BIDS.ipynb ` + + .. container:: sphx-glr-download sphx-glr-download-python + + :download:`Download Python source code: plot_1_example_BIDS.py ` + + +.. only:: html + + .. rst-class:: sphx-glr-signature + + `Gallery generated by Sphinx-Gallery `_ diff --git a/_sources/auto_examples/plot_2_example_add_feature.rst.txt b/_sources/auto_examples/plot_2_example_add_feature.rst.txt new file mode 100644 index 00000000..9e91acc6 --- /dev/null +++ b/_sources/auto_examples/plot_2_example_add_feature.rst.txt @@ -0,0 +1,336 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples/plot_2_example_add_feature.py" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. note:: + :class: sphx-glr-download-link-note + + :ref:`Go to the end ` + to download the full example code. + +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_auto_examples_plot_2_example_add_feature.py: + + +=================== +Adding New Features +=================== + +.. GENERATED FROM PYTHON SOURCE LINES 8-12 + +.. code-block:: Python + + import py_neuromodulation as nm + import numpy as np + from typing import Iterable + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 13-16 + +In this example we will demonstrate how a new feature can be added to the existing feature pipeline. +This can be done by creating a new feature class that implements the protocol class :class:`~nm_features.NMFeature` +and registering it with the :func:`~nm_features.AddCustomFeature` function. + +.. GENERATED FROM PYTHON SOURCE LINES 19-24 + +Let's create a new feature class called `ChannelMean` that calculates the mean signal for each channel. +We can optinally make it inherit from :class:`~nm_features.NMFeature` but as long as it has an adequate constructor +and a `calc_feature` method with the appropriate signatures it will work. +The :func:`__init__` method should take the settings, channel names and sampling frequency as arguments. +The `calc_feature` method should take the data and a dictionary of features as arguments and return the updated dictionary. + +.. GENERATED FROM PYTHON SOURCE LINES 24-56 + +.. code-block:: Python + + class ChannelMean: + def __init__( + self, settings: nm.NMSettings, ch_names: Iterable[str], sfreq: float + ) -> None: + # If required for feature calculation, store the settings, + # channel names and sampling frequency (optional) + self.settings = settings + self.ch_names = ch_names + self.sfreq = sfreq + + # Here you can add any additional initialization code + # For example, you could store parameters for the functions\ + # used in the calc_feature method + + self.feature_name = "channel_mean" + + def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict: + # Here you can add any feature calculation code + # This example simply calculates the mean signal for each channel + ch_means = np.mean(data, axis=1) + + # Store the calculated features in the features_compute dictionary + # Be careful to use a unique keyfor each channel and metric you compute + for ch_idx, ch in enumerate(self.ch_names): + features_compute[f"{self.feature_name}_{ch}"] = ch_means[ch_idx] + + # Return the updated features_compute dictionary to the stream + return features_compute + + + nm.add_custom_feature("channel_mean", ChannelMean) + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 57-58 + +Now we can instantiate settings and observe that the new feature has been added to the list of features + +.. GENERATED FROM PYTHON SOURCE LINES 58-62 + +.. code-block:: Python + + settings = nm.NMSettings() # Get default settings + + settings.features + + + + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + + {'bandpass_filter': False, + 'bispectrum': False, + 'bursts': True, + 'channel_mean': True, + 'coherence': False, + 'fft': True, + 'fooof': False, + 'linelength': True, + 'mne_connectivity': False, + 'nolds': False, + 'raw_hjorth': True, + 'return_raw': True, + 'sharpwave_analysis': True, + 'stft': False, + 'welch': True} + + + +.. GENERATED FROM PYTHON SOURCE LINES 63-64 + +Let's create some artificial data to demonstrate the feature calculation. + +.. GENERATED FROM PYTHON SOURCE LINES 64-82 + +.. code-block:: Python + + N_CHANNELS = 5 + N_SAMPLES = 10000 # 10 seconds of random data at 1000 Hz sampling frequency + + data = np.random.random([N_CHANNELS, N_SAMPLES]) + stream = nm.Stream( + sfreq=1000, + data=data, + settings = settings, + sampling_rate_features_hz=10, + verbose=False, + ) + + feature_df = stream.run() + columns = [col for col in feature_df.columns if "channel_mean" in col] + + feature_df[columns] + + + + + + + +.. raw:: html + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
channel_mean_ch0-avgrefchannel_mean_ch1-avgrefchannel_mean_ch2-avgrefchannel_mean_ch3-avgrefchannel_mean_ch4-avgref
0-0.0138760.0024140.003258-0.0005600.008764
1-1.000000-1.000000-1.0000001.0000001.000000
21.339100-0.330012-0.6918761.000813-1.021102
30.815008-0.730820-1.5922951.405574-0.607162
40.1447310.713960-1.5576721.045666-0.758311
..................
86-0.218055-0.5685330.840491-0.2460050.285486
87-0.389856-1.2891530.501413-0.2777981.207531
88-0.409714-0.8916480.292925-0.3129801.104815
89-0.208329-0.2804730.598181-0.5544500.507600
900.255935-0.6102360.175808-0.8829300.888721
+

91 rows × 5 columns

+
+
+
+
+ +.. GENERATED FROM PYTHON SOURCE LINES 83-84 + +Remove feature so that it does not interfere with other examples + +.. GENERATED FROM PYTHON SOURCE LINES 84-88 + +.. code-block:: Python + + nm.remove_custom_feature("channel_mean") + + + + + + + + + + + +.. rst-class:: sphx-glr-timing + + **Total running time of the script:** (0 minutes 1.078 seconds) + + +.. _sphx_glr_download_auto_examples_plot_2_example_add_feature.py: + +.. only:: html + + .. container:: sphx-glr-footer sphx-glr-footer-example + + .. container:: sphx-glr-download sphx-glr-download-jupyter + + :download:`Download Jupyter notebook: plot_2_example_add_feature.ipynb ` + + .. container:: sphx-glr-download sphx-glr-download-python + + :download:`Download Python source code: plot_2_example_add_feature.py ` + + +.. only:: html + + .. rst-class:: sphx-glr-signature + + `Gallery generated by Sphinx-Gallery `_ diff --git a/_sources/auto_examples/plot_3_example_sharpwave_analysis.rst.txt b/_sources/auto_examples/plot_3_example_sharpwave_analysis.rst.txt new file mode 100644 index 00000000..59044dfd --- /dev/null +++ b/_sources/auto_examples/plot_3_example_sharpwave_analysis.rst.txt @@ -0,0 +1,523 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples/plot_3_example_sharpwave_analysis.py" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. note:: + :class: sphx-glr-download-link-note + + :ref:`Go to the end ` + to download the full example code. + +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_auto_examples_plot_3_example_sharpwave_analysis.py: + + +Analyzing temporal features +=========================== + +.. GENERATED FROM PYTHON SOURCE LINES 8-32 + +Time series data can be characterized using oscillatory components, but assumptions of sinusoidality are for real data rarely fulfilled. +See *"Brain Oscillations and the Importance of Waveform Shape"* `Cole et al 2017 `_ for a great motivation. +We implemented here temporal characteristics based on individual trough and peak relations, +based on the :meth:~`scipy.signal.find_peaks` method. The function parameter *distance* can be specified in the *nm_settings.json*. +Temporal features can be calculated twice for troughs and peaks. In the settings, this can be specified by setting *estimate* to true +in *detect_troughs* and/or *detect_peaks*. A statistical measure (e.g. mean, max, median, var) can be defined as a resulting feature from the peak and +trough estimates using the *apply_estimator_between_peaks_and_troughs* setting. + +In py_neuromodulation the following characteristics are implemented: + +.. note:: + The nomenclature is written here for sharpwave troughs, but detection of peak characteristics can be computed in the same way. + +- prominence: + :math:`V_{prominence} = |\frac{V_{peak-left} + V_{peak-right}}{2}| - V_{trough}` +- sharpness: + :math:`V_{sharpnesss} = \frac{(V_{trough} - V_{trough-5 ms}) + (V_{trough} - V_{trough+5 ms})}{2}` +- rise and decay rise time +- rise and decay steepness +- width (between left and right peaks) +- interval (between troughs) + +Additionally, different filter ranges can be parametrized using the *filter_ranges_hz* setting. +Filtering is necessary to remove high frequent signal fluctuations, but limits also the true estimation of sharpness and prominence due to signal smoothing. + +.. GENERATED FROM PYTHON SOURCE LINES 32-49 + +.. code-block:: Python + + + from typing import cast + import seaborn as sb + from matplotlib import pyplot as plt + from scipy import signal + from scipy.signal import fftconvolve + import numpy as np + + import py_neuromodulation as nm + from py_neuromodulation import ( + nm_define_nmchannels, + nm_IO, + NMSettings, + ) + from py_neuromodulation.nm_sharpwaves import SharpwaveAnalyzer + + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 50-51 + +We will first read the example ECoG data and plot the identified features on the filtered time series. + +.. GENERATED FROM PYTHON SOURCE LINES 51-63 + +.. code-block:: Python + + + RUN_NAME, PATH_RUN, PATH_BIDS, PATH_OUT, datatype = nm_IO.get_paths_example_data() + + ( + raw, + data, + sfreq, + line_noise, + coord_list, + coord_names, + ) = nm_IO.read_BIDS_data(PATH_RUN=PATH_RUN) + + + + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + Extracting parameters from /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vhdr... + Setting channel info structure... + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: Did not find any events.tsv associated with sub-testsub_ses-EphysMedOff_task-gripforce_run-0. + + The search_str was "/home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/**/ieeg/sub-testsub_ses-EphysMedOff*events.tsv" + raw_arr = read_raw_bids(bids_path) + Reading channel info from /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_channels.tsv. + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: The unit for channel(s) MOV_RIGHT has changed from V to NA. + raw_arr = read_raw_bids(bids_path) + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: Other is not an MNE-Python coordinate frame for IEEG data and so will be set to 'unknown' + raw_arr = read_raw_bids(bids_path) + Reading electrode coords from /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_electrodes.tsv. + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: There are channels without locations (n/a) that are not marked as bad: ['MOV_RIGHT'] + raw_arr = read_raw_bids(bids_path) + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: Not setting position of 1 misc channel found in montage: + ['MOV_RIGHT'] + Consider setting the channel types to be of EEG/sEEG/ECoG/DBS/fNIRS using inst.set_channel_types before calling inst.set_montage, or omit these channels when creating your montage. + raw_arr = read_raw_bids(bids_path) + + + + +.. GENERATED FROM PYTHON SOURCE LINES 64-99 + +.. code-block:: Python + + settings = NMSettings.get_fast_compute() + + settings.features.fft = True + settings.features.bursts = False + settings.features.sharpwave_analysis = True + settings.features.coherence = False + + settings.sharpwave_analysis_settings.estimator["mean"] = [] + settings.sharpwave_analysis_settings.sharpwave_features.enable_all() + for sw_feature in settings.sharpwave_analysis_settings.sharpwave_features.list_all(): + settings.sharpwave_analysis_settings.estimator["mean"].append(sw_feature) + + nm_channels = nm_define_nmchannels.set_channels( + ch_names=raw.ch_names, + ch_types=raw.get_channel_types(), + reference="default", + bads=raw.info["bads"], + new_names="default", + used_types=("ecog", "dbs", "seeg"), + target_keywords=["MOV_RIGHT"], + ) + + stream = nm.Stream( + sfreq=sfreq, + nm_channels=nm_channels, + settings=settings, + line_noise=line_noise, + coord_list=coord_list, + coord_names=coord_names, + verbose=False, + ) + sw_analyzer = cast( + SharpwaveAnalyzer, stream.data_processor.features.get_feature("sharpwave_analysis") + ) + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 100-101 + +The plotted example time series, visualized on a short time scale, shows the relation of identified peaks, troughs, and estimated features: + +.. GENERATED FROM PYTHON SOURCE LINES 101-198 + +.. code-block:: Python + + data_plt = data[5, 1000:4000] + + filtered_dat = fftconvolve(data_plt, sw_analyzer.list_filter[0][1], mode="same") + + troughs = signal.find_peaks(-filtered_dat, distance=10)[0] + peaks = signal.find_peaks(filtered_dat, distance=5)[0] + + sw_results = sw_analyzer.analyze_waveform(filtered_dat) + + WIDTH = BAR_WIDTH = 4 + BAR_OFFSET = 50 + OFFSET_TIME_SERIES = -100 + SCALE_TIMESERIES = 1 + + hue_colors = sb.color_palette("viridis_r", 6) + + plt.figure(figsize=(5, 3), dpi=300) + plt.plot( + OFFSET_TIME_SERIES + data_plt, + color="gray", + linewidth=0.5, + alpha=0.5, + label="original ECoG data", + ) + plt.plot( + OFFSET_TIME_SERIES + filtered_dat * SCALE_TIMESERIES, + linewidth=0.5, + color="black", + label="[5-30]Hz filtered data", + ) + + plt.plot( + peaks, + OFFSET_TIME_SERIES + filtered_dat[peaks] * SCALE_TIMESERIES, + "x", + label="peaks", + markersize=3, + color="darkgray", + ) + plt.plot( + troughs, + OFFSET_TIME_SERIES + filtered_dat[troughs] * SCALE_TIMESERIES, + "x", + label="troughs", + markersize=3, + color="lightgray", + ) + + plt.bar( + troughs + BAR_WIDTH, + np.array(sw_results["prominence"]) * 4, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[0], + label="Prominence", + alpha=0.5, + ) + plt.bar( + troughs + BAR_WIDTH * 2, + -np.array(sw_results["sharpness"]) * 6, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[1], + label="Sharpness", + alpha=0.5, + ) + plt.bar( + troughs + BAR_WIDTH * 3, + np.array(sw_results["interval"]) * 5, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[2], + label="Interval", + alpha=0.5, + ) + plt.bar( + troughs + BAR_WIDTH * 4, + np.array(sw_results["rise_time"]) * 5, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[3], + label="Rise time", + alpha=0.5, + ) + + plt.xticks( + np.arange(0, data_plt.shape[0], 200), + np.round(np.arange(0, int(data_plt.shape[0] / 1000), 0.2), 2), + ) + plt.xlabel("Time [s]") + plt.title("Temporal waveform shape features") + plt.legend(loc="center left", bbox_to_anchor=(1, 0.5)) + plt.ylim(-550, 700) + plt.xlim(0, 200) + plt.ylabel("a.u.") + plt.tight_layout() + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_3_example_sharpwave_analysis_001.png + :alt: Temporal waveform shape features + :srcset: /auto_examples/images/sphx_glr_plot_3_example_sharpwave_analysis_001.png + :class: sphx-glr-single-img + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 199-200 + +See in the following example a time series example, that is aligned to movement. With movement onset the prominence, sharpness, and interval features are reduced: + +.. GENERATED FROM PYTHON SOURCE LINES 200-284 + +.. code-block:: Python + + + plt.figure(figsize=(8, 5), dpi=300) + plt.plot( + OFFSET_TIME_SERIES + data_plt, + color="gray", + linewidth=0.5, + alpha=0.5, + label="original ECoG data", + ) + plt.plot( + OFFSET_TIME_SERIES + filtered_dat * SCALE_TIMESERIES, + linewidth=0.5, + color="black", + label="[5-30]Hz filtered data", + ) + + plt.plot( + peaks, + OFFSET_TIME_SERIES + filtered_dat[peaks] * SCALE_TIMESERIES, + "x", + label="peaks", + markersize=3, + color="darkgray", + ) + plt.plot( + troughs, + OFFSET_TIME_SERIES + filtered_dat[troughs] * SCALE_TIMESERIES, + "x", + label="troughs", + markersize=3, + color="lightgray", + ) + + plt.bar( + troughs + BAR_WIDTH, + np.array(sw_results["prominence"]) * 4, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[0], + label="Prominence", + alpha=0.5, + ) + plt.bar( + troughs + BAR_WIDTH * 2, + -np.array(sw_results["sharpness"]) * 6, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[1], + label="Sharpness", + alpha=0.5, + ) + plt.bar( + troughs + BAR_WIDTH * 3, + np.array(sw_results["interval"]) * 5, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[2], + label="Interval", + alpha=0.5, + ) + plt.bar( + troughs + BAR_WIDTH * 4, + np.array(sw_results["rise_time"]) * 5, + bottom=BAR_OFFSET, + width=WIDTH, + color=hue_colors[3], + label="Rise time", + alpha=0.5, + ) + + plt.axvline(x=1500, label="Movement start", color="red") + + # plt.xticks(np.arange(0, 2000, 200), np.round(np.arange(0, 2, 0.2), 2)) + plt.xticks( + np.arange(0, data_plt.shape[0], 200), + np.round(np.arange(0, int(data_plt.shape[0] / 1000), 0.2), 2), + ) + plt.xlabel("Time [s]") + plt.title("Temporal waveform shape features") + plt.legend(loc="center left", bbox_to_anchor=(1, 0.5)) + plt.ylim(-450, 400) + plt.ylabel("a.u.") + plt.tight_layout() + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_3_example_sharpwave_analysis_002.png + :alt: Temporal waveform shape features + :srcset: /auto_examples/images/sphx_glr_plot_3_example_sharpwave_analysis_002.png + :class: sphx-glr-single-img + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 285-289 + +In the *sharpwave_analysis_settings* the *estimator* keyword further specifies which statistic is computed based on the individual +features in one batch. The "global" setting *segment_length_features_ms* specifies the time duration for feature computation. +Since there can be a different number of identified waveform shape features for different batches (i.e. different number of peaks/troughs), +taking a statistical measure (e.g. the maximum or mean) will be necessary for feature comparison. + +.. GENERATED FROM PYTHON SOURCE LINES 291-294 + +Example time series computation for movement decoding +----------------------------------------------------- +We will now read the ECoG example/data and investigate if samples differ across movement states. Therefore we compute features and enable the default *sharpwave* features. + +.. GENERATED FROM PYTHON SOURCE LINES 294-315 + +.. code-block:: Python + + + settings = NMSettings.get_default().reset() + + settings.features.sharpwave_analysis = True + settings.sharpwave_analysis_settings.filter_ranges_hz = [[5, 80]] + + nm_channels["used"] = 0 # set only two ECoG channels for faster computation to true + nm_channels.loc[[3, 8], "used"] = 1 + + stream = nm.Stream( + sfreq=sfreq, + nm_channels=nm_channels, + settings=settings, + line_noise=line_noise, + coord_list=coord_list, + coord_names=coord_names, + verbose=True, + ) + + df_features = stream.run(data=data[:, :30000]) + + + + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + /home/runner/.venv/lib/python3.10/site-packages/pydantic/main.py:347: UserWarning: Pydantic serializer warnings: + Expected `FrequencyRange` but got `list` - serialized value may not be as expected + return self.__pydantic_serializer__.to_python( + + + + +.. GENERATED FROM PYTHON SOURCE LINES 316-317 + +We can then plot two exemplary features, prominence and interval, and see that the movement amplitude can be clustered with those two features alone: + +.. GENERATED FROM PYTHON SOURCE LINES 317-333 + +.. code-block:: Python + + + plt.figure(figsize=(5, 3), dpi=300) + print(df_features.columns) + plt.scatter( + df_features["ECOG_RIGHT_0-avgref_Sharpwave_Max_prominence_range_5_80"], + df_features["ECOG_RIGHT_5-avgref_Sharpwave_Mean_interval_range_5_80"], + c=df_features.MOV_RIGHT, + alpha=0.8, + s=30, + ) + cbar = plt.colorbar() + cbar.set_label("Movement amplitude") + plt.xlabel("Prominence a.u.") + plt.ylabel("Interval a.u.") + plt.title("Temporal features predict movement amplitude") + plt.tight_layout() + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_3_example_sharpwave_analysis_003.png + :alt: Temporal features predict movement amplitude + :srcset: /auto_examples/images/sphx_glr_plot_3_example_sharpwave_analysis_003.png + :class: sphx-glr-single-img + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + Index(['ECOG_RIGHT_0-avgref_Sharpwave_Max_prominence_range_5_80', + 'ECOG_RIGHT_0-avgref_Sharpwave_Mean_interval_range_5_80', + 'ECOG_RIGHT_0-avgref_Sharpwave_Max_sharpness_range_5_80', + 'ECOG_RIGHT_5-avgref_Sharpwave_Max_prominence_range_5_80', + 'ECOG_RIGHT_5-avgref_Sharpwave_Mean_interval_range_5_80', + 'ECOG_RIGHT_5-avgref_Sharpwave_Max_sharpness_range_5_80', 'time', + 'MOV_RIGHT'], + dtype='object') + + + + + +.. rst-class:: sphx-glr-timing + + **Total running time of the script:** (0 minutes 1.843 seconds) + + +.. _sphx_glr_download_auto_examples_plot_3_example_sharpwave_analysis.py: + +.. only:: html + + .. container:: sphx-glr-footer sphx-glr-footer-example + + .. container:: sphx-glr-download sphx-glr-download-jupyter + + :download:`Download Jupyter notebook: plot_3_example_sharpwave_analysis.ipynb ` + + .. container:: sphx-glr-download sphx-glr-download-python + + :download:`Download Python source code: plot_3_example_sharpwave_analysis.py ` + + +.. only:: html + + .. rst-class:: sphx-glr-signature + + `Gallery generated by Sphinx-Gallery `_ diff --git a/_sources/auto_examples/plot_4_example_gridPointProjection.rst.txt b/_sources/auto_examples/plot_4_example_gridPointProjection.rst.txt new file mode 100644 index 00000000..9b75a394 --- /dev/null +++ b/_sources/auto_examples/plot_4_example_gridPointProjection.rst.txt @@ -0,0 +1,587 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples/plot_4_example_gridPointProjection.py" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. note:: + :class: sphx-glr-download-link-note + + :ref:`Go to the end ` + to download the full example code. + +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_auto_examples_plot_4_example_gridPointProjection.py: + + +Grid Point Projection +===================== + +.. GENERATED FROM PYTHON SOURCE LINES 8-26 + +In ECoG datasets the electrode locations are usually different. For this reason, we established a grid +with a set of points defined in a standardized MNI brain. +Data is then interpolated to this grid, such that they are common across patients, which allows across patient decoding use cases. + +In this notebook, we will plot these grid points and see how the features extracted from our data can be projected into this grid space. + +In order to do so, we'll read saved features that were computed in the ECoG movement notebook. +Please note that in order to do so, when running the feature estimation, the settings + +.. note:: + + .. code-block:: python + + stream.settings['postprocessing']['project_cortex'] = True + stream.settings['postprocessing']['project_subcortex'] = True + + need to be set to `True` for a cortical and/or subcortical projection. + + +.. GENERATED FROM PYTHON SOURCE LINES 28-41 + +.. code-block:: Python + + import numpy as np + import matplotlib.pyplot as plt + + import py_neuromodulation as nm + from py_neuromodulation import ( + nm_analysis, + nm_plots, + nm_IO, + NMSettings, + nm_define_nmchannels + ) + + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 42-46 + +Read features from BIDS data +---------------------------- + +We first estimate features, with the `grid_point` projection settings enabled for cortex. + +.. GENERATED FROM PYTHON SOURCE LINES 49-92 + +.. code-block:: Python + + RUN_NAME, PATH_RUN, PATH_BIDS, PATH_OUT, datatype = nm_IO.get_paths_example_data() + + ( + raw, + data, + sfreq, + line_noise, + coord_list, + coord_names, + ) = nm_IO.read_BIDS_data( + PATH_RUN=PATH_RUN + ) + + settings = NMSettings.get_fast_compute() + + settings.postprocessing.project_cortex = True + + nm_channels = nm_define_nmchannels.set_channels( + ch_names=raw.ch_names, + ch_types=raw.get_channel_types(), + reference="default", + bads=raw.info["bads"], + new_names="default", + used_types=("ecog", "dbs", "seeg"), + target_keywords=["MOV_RIGHT_CLEAN","MOV_LEFT_CLEAN"] + ) + + stream = nm.Stream( + sfreq=sfreq, + nm_channels=nm_channels, + settings=settings, + line_noise=line_noise, + coord_list=coord_list, + coord_names=coord_names, + verbose=True, + ) + + features = stream.run( + data=data[:, :int(sfreq*5)], + out_path_root=PATH_OUT, + folder_name=RUN_NAME, + ) + + + + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + Extracting parameters from /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vhdr... + Setting channel info structure... + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: Did not find any events.tsv associated with sub-testsub_ses-EphysMedOff_task-gripforce_run-0. + + The search_str was "/home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/**/ieeg/sub-testsub_ses-EphysMedOff*events.tsv" + raw_arr = read_raw_bids(bids_path) + Reading channel info from /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_channels.tsv. + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: The unit for channel(s) MOV_RIGHT has changed from V to NA. + raw_arr = read_raw_bids(bids_path) + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: Other is not an MNE-Python coordinate frame for IEEG data and so will be set to 'unknown' + raw_arr = read_raw_bids(bids_path) + Reading electrode coords from /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_electrodes.tsv. + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: There are channels without locations (n/a) that are not marked as bad: ['MOV_RIGHT'] + raw_arr = read_raw_bids(bids_path) + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: Not setting position of 1 misc channel found in montage: + ['MOV_RIGHT'] + Consider setting the channel types to be of EEG/sEEG/ECoG/DBS/fNIRS using inst.set_channel_types before calling inst.set_montage, or omit these channels when creating your montage. + raw_arr = read_raw_bids(bids_path) + + + + +.. GENERATED FROM PYTHON SOURCE LINES 93-94 + +From nm_analysis.py, we use the :class:~`nm_analysis.FeatureReader` class to load the data. + +.. GENERATED FROM PYTHON SOURCE LINES 94-100 + +.. code-block:: Python + + + # init analyzer + feature_reader = nm_analysis.FeatureReader( + feature_dir=PATH_OUT, feature_file=RUN_NAME + ) + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 101-114 + +To perform the grid projection, for all computed features we check for every grid point if there is any electrode channel within the spatial range ```max_dist_mm```, and weight +this electrode contact by the inverse distance and normalize across all electrode distances within the maximum distance range. +This gives us a projection matrix that we can apply to streamed data, to transform the feature-channel matrix *(n_features, n_channels)* into the grid point matrix *(n_features, n_gridpoints)*. + +To save computation time, this projection matrix is precomputed before the real time run computation. +The cortical grid is stored in *py_neuromodulation/grid_cortex.tsv* and the electrodes coordinates are stored in *_space-mni_electrodes.tsv* in a BIDS dataset. + +.. note:: + + One remark is that our cortical and subcortical grids are defined for the **left** hemisphere of the brain and, therefore, electrode contacts are mapped to the left hemisphere. + +From the analyzer, the user can plot the cortical projection with the function below, display the grid points and ECoG electrodes are crosses. +The yellow grid points are the ones that are active for that specific ECoG electrode location. The inactive grid points are shown in purple. + +.. GENERATED FROM PYTHON SOURCE LINES 114-117 + +.. code-block:: Python + + + feature_reader.plot_cort_projection() + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_4_example_gridPointProjection_001.png + :alt: Cortical grid + :srcset: /auto_examples/images/sphx_glr_plot_4_example_gridPointProjection_001.png + :class: sphx-glr-single-img + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 118-119 + +We can also plot only the ECoG electrodes or the grid points, with the help of the data saved in feature_reader.sidecar. BIDS sidecar files are json files where you store additional information, here it is used to save the ECoG strip positions and the grid coordinates, which are not part of the settings and nm_channels.csv. We can check what is stored in the file and then use the nmplotter.plot_cortex function: + +.. GENERATED FROM PYTHON SOURCE LINES 119-128 + +.. code-block:: Python + + + grid_plotter = nm_plots.NM_Plot( + ecog_strip=np.array(feature_reader.sidecar["coords"]["cortex_right"]["positions"]), + grid_cortex=np.array(feature_reader.sidecar["grid_cortex"]), + # grid_subcortex=np.array(feature_reader.sidecar["grid_subcortex"]), + sess_right=feature_reader.sidecar["sess_right"], + proj_matrix_cortex=np.array(feature_reader.sidecar["proj_matrix_cortex"]) + ) + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 129-137 + +.. code-block:: Python + + grid_plotter.plot_cortex( + grid_color=np.sum(np.array(feature_reader.sidecar["proj_matrix_cortex"]),axis=1), + lower_clim=0., + upper_clim=1.0, + cbar_label="Used Grid Points", + title = "ECoG electrodes projected onto cortical grid" + ) + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_4_example_gridPointProjection_002.png + :alt: ECoG electrodes projected onto cortical grid + :srcset: /auto_examples/images/sphx_glr_plot_4_example_gridPointProjection_002.png + :class: sphx-glr-single-img + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 138-140 + +.. code-block:: Python + + feature_reader.sidecar["coords"]["cortex_right"]["positions"] + + + + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + + [[37.318174, -48.61012664, 61.79765474], [40.1598943, -37.31592983, 64.31171618], [40.94303578, -27.21778456, 64.09518408], [39.78395522, -17.00523081, 63.86618136], [39.68813641, -5.528024572, 61.68254254], [37.51915924, 4.304913414, 60.54126355]] + + + +.. GENERATED FROM PYTHON SOURCE LINES 141-151 + +.. code-block:: Python + + feature_reader.nmplotter.plot_cortex( + ecog_strip=np.array( + feature_reader.sidecar["coords"]["cortex_right"]["positions"], + ), + lower_clim=0., + upper_clim=1.0, + cbar_label="Used ECoG Electrodes", + title = "Plot of ECoG electrodes" + ) + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_4_example_gridPointProjection_003.png + :alt: Plot of ECoG electrodes + :srcset: /auto_examples/images/sphx_glr_plot_4_example_gridPointProjection_003.png + :class: sphx-glr-single-img + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 152-162 + +.. code-block:: Python + + feature_reader.nmplotter.plot_cortex( + np.array( + feature_reader.sidecar["grid_cortex"] + ), + lower_clim=0., + upper_clim=1.0, + cbar_label="All Grid Points", + title = "All grid points" + ) + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_4_example_gridPointProjection_004.png + :alt: All grid points + :srcset: /auto_examples/images/sphx_glr_plot_4_example_gridPointProjection_004.png + :class: sphx-glr-single-img + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 163-169 + +The Projection Matrix +--------------------- +To go from the feature-channel matrix *(n_features, n_channels)* to the grid point matrix *(n_features, n_gridpoints)* +we need a projection matrix that has the shape *(n_channels, n_gridpoints)*. +It maps the strengths of the signals in each ECoG channel to the correspondent ones in the cortical grid. +In the cell below we plot this matrix, that has the property that the column sum over channels for each grid point is either 1 or 0. + +.. GENERATED FROM PYTHON SOURCE LINES 169-177 + +.. code-block:: Python + + + plt.figure(figsize=(8,5)) + plt.imshow(np.array(feature_reader.sidecar['proj_matrix_cortex']), aspect = 'auto') + plt.colorbar(label = "Strength of ECoG signal in each grid point") + plt.xlabel("ECoG channels") + plt.ylabel("Grid points") + plt.title("Matrix mapping from ECoG to grid") + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_4_example_gridPointProjection_005.png + :alt: Matrix mapping from ECoG to grid + :srcset: /auto_examples/images/sphx_glr_plot_4_example_gridPointProjection_005.png + :class: sphx-glr-single-img + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + + Text(0.5, 1.0, 'Matrix mapping from ECoG to grid') + + + +.. GENERATED FROM PYTHON SOURCE LINES 178-181 + +Feature Plot in the Grid: An Example of Post-processing +------------------------------------------------------- +First we take the dataframe with all the features in all time points. + +.. GENERATED FROM PYTHON SOURCE LINES 181-184 + +.. code-block:: Python + + + df = feature_reader.feature_arr + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 185-187 + +.. code-block:: Python + + df.iloc[:5, :5] + + + + + + +.. raw:: html + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LFP_RIGHT_0-LFP_RIGHT_2_fft_theta_meanLFP_RIGHT_1-LFP_RIGHT_0_fft_theta_meanLFP_RIGHT_2-LFP_RIGHT_1_fft_theta_meanECOG_RIGHT_0-avgref_fft_theta_meanECOG_RIGHT_1-avgref_fft_theta_mean
03.5449383.4622663.5166753.7845203.889221
1-1.0000001.0000001.000000-1.0000001.000000
2-0.640741-0.660343-1.1069260.6892750.758821
3-0.2262470.333361-1.5209811.322776-0.454620
4-0.370427-0.840489-0.3209191.093448-1.418237
+
+
+
+
+ +.. GENERATED FROM PYTHON SOURCE LINES 188-189 + +Then we filter for only 'avgref_fft_theta', which gives us the value for fft_theta in all 6 ECoG channels over all time points. Then we take only the 6th time point - as an arbitrary choice. + +.. GENERATED FROM PYTHON SOURCE LINES 189-193 + +.. code-block:: Python + + + fft_theta_oneTimePoint = np.asarray(df[df.columns[df.columns.str.contains(pat = 'avgref_fft_theta')]].iloc[5]) + fft_theta_oneTimePoint + + + + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + + array([-0.35856691, -1.65213409, 0.67152411, 0.08666766, 0.74406089, + 1.17511129]) + + + +.. GENERATED FROM PYTHON SOURCE LINES 194-196 + +Then the projection of the features into the grid is gonna be the color of the grid points in the *plot_cortex* function. +That is the matrix multiplication of the projection matrix of the cortex and 6 values for the *fft_theta* feature above. + +.. GENERATED FROM PYTHON SOURCE LINES 196-202 + +.. code-block:: Python + + + grid_fft_Theta = np.array(feature_reader.sidecar["proj_matrix_cortex"]) @ fft_theta_oneTimePoint + + feature_reader.nmplotter.plot_cortex(np.array( + feature_reader.sidecar["grid_cortex"]),grid_color = grid_fft_Theta, set_clim = True, lower_clim=min(grid_fft_Theta[grid_fft_Theta>0]), upper_clim=max(grid_fft_Theta), cbar_label="FFT Theta Projection to Grid", title = "FFT Theta Projection to Grid") + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_4_example_gridPointProjection_006.png + :alt: FFT Theta Projection to Grid + :srcset: /auto_examples/images/sphx_glr_plot_4_example_gridPointProjection_006.png + :class: sphx-glr-single-img + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 203-204 + +Lower and upper boundaries for clim were chosen to be the max and min values of the projection of the features (minimum value excluding zero). This can be checked in the cell below: + +.. GENERATED FROM PYTHON SOURCE LINES 204-207 + +.. code-block:: Python + + + grid_fft_Theta + + + + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + + array([ 0. , -0.35856691, -0.35856691, 0. , 0. , + -0.88206403, 0. , -0.6634492 , 0. , 0. , + -0.49283354, -0.25995071, 0. , -0.10879517, 0. , + 0. , 0.45885887, 0. , 0. , 0.58028397, + 0.74406089, 1.17511129, 0. , 0.97422633, 1.01903534, + 0. , 0. , 1.03882634, 1.17511129, 0. , + 0. , 0. , 1.17511129, 0. , 0. , + 0. , 0. , 0. , 0. ]) + + + +.. GENERATED FROM PYTHON SOURCE LINES 208-209 + +In the plot above we can see how the intensity of the fast fourier transform in the theta band varies for each grid point in the cortex, for one specific time point. + + +.. rst-class:: sphx-glr-timing + + **Total running time of the script:** (0 minutes 2.917 seconds) + + +.. _sphx_glr_download_auto_examples_plot_4_example_gridPointProjection.py: + +.. only:: html + + .. container:: sphx-glr-footer sphx-glr-footer-example + + .. container:: sphx-glr-download sphx-glr-download-jupyter + + :download:`Download Jupyter notebook: plot_4_example_gridPointProjection.ipynb ` + + .. container:: sphx-glr-download sphx-glr-download-python + + :download:`Download Python source code: plot_4_example_gridPointProjection.py ` + + +.. only:: html + + .. rst-class:: sphx-glr-signature + + `Gallery generated by Sphinx-Gallery `_ diff --git a/_sources/auto_examples/plot_5_example_rmap_computing.rst.txt b/_sources/auto_examples/plot_5_example_rmap_computing.rst.txt new file mode 100644 index 00000000..b98e960c --- /dev/null +++ b/_sources/auto_examples/plot_5_example_rmap_computing.rst.txt @@ -0,0 +1,108 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples/plot_5_example_rmap_computing.py" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. note:: + :class: sphx-glr-download-link-note + + :ref:`Go to the end ` + to download the full example code. + +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_auto_examples_plot_5_example_rmap_computing.py: + + +R-Map computation +================= + +.. GENERATED FROM PYTHON SOURCE LINES 8-64 + +Across patient decoding using R-Map optimal connectivity +-------------------------------------------------------- + +ECoG electrode placement is commonly very heterogeneous across patients and cohorts. +To still facilitate approaches that are able to perform decoding applications without patient individual training, +two across-patient decoding approaches were previously investigated for movement decoding: + + +* grid-point decoding +* optimal connectivity channel decoding + + +First, the grid-point decoding approach relies on definition of a cortical or subcortical grid. +Data from individual grid points is then interpolated onto those common grid points. +The approach was also explained in the :doc:`plot_4_example_gridPointProjection` notebook. + +.. image:: ../_static/RMAP_figure.png + :alt: R-Map and grid point approach for decoding without patient-individual training + +The R-Map decoding approach relies on the other hand on computation of whole brain connectivity. The electrode MNI space locations need to be known, +then the following steps can be performed for decoding without patient individual training: + +#. Using the `wjn_toolbox `_ *wjn_specrical_roi* function, the MNI coordinates can be transformed into NIFTI (.nii) files, containing the electrode contact region of interest (ROI): + + .. code-block:: python + + wjn_spherical_roi(roiname, mni, 4) + +#. For the given *ROI.nii* files, the LeadDBS `LeadMapper `_ tool can be used for functional or structural connectivity estimation. + +#. The py_neuromodulation :class:`~nm_RMAP.py` module can then compute the R-Map given the contact-individual connectivity fingerprints: + + .. code-block:: python + + nm_RMAP.calculate_RMap_numba(fingerprints, performances) + +#. The fingerprints from test-set patients can then be correlated with the calculated R-Map: + + .. code-block:: python + + nm_RMAP.get_corr_numba(fp, fp_test) + +#. The channel with highest correlation can then be selected for decoding without individual training. :class:`~nm_RMAP.py` contain already leave one channel and leave one patient out cross validation functions: + + .. code-block:: python + + nm_RMAP.leave_one_sub_out_cv(l_fps_names, l_fps_dat, l_per, sub_list) + +#. The obtained R-Map correlations can then be estimated statistically and plotted against true correlates: + + .. code-block:: python + + nm_RMAP.plot_performance_prediction_correlation(per_left_out, per_predict, out_path_save) + + +sphinx_gallery_thumbnail_path = '_static/RMAP_figure.png' + + +.. rst-class:: sphx-glr-timing + + **Total running time of the script:** (0 minutes 0.000 seconds) + + +.. _sphx_glr_download_auto_examples_plot_5_example_rmap_computing.py: + +.. only:: html + + .. container:: sphx-glr-footer sphx-glr-footer-example + + .. container:: sphx-glr-download sphx-glr-download-jupyter + + :download:`Download Jupyter notebook: plot_5_example_rmap_computing.ipynb ` + + .. container:: sphx-glr-download sphx-glr-download-python + + :download:`Download Python source code: plot_5_example_rmap_computing.py ` + + +.. only:: html + + .. rst-class:: sphx-glr-signature + + `Gallery generated by Sphinx-Gallery `_ diff --git a/_sources/auto_examples/plot_6_real_time_demo.rst.txt b/_sources/auto_examples/plot_6_real_time_demo.rst.txt new file mode 100644 index 00000000..c05e071d --- /dev/null +++ b/_sources/auto_examples/plot_6_real_time_demo.rst.txt @@ -0,0 +1,192 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples/plot_6_real_time_demo.py" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. note:: + :class: sphx-glr-download-link-note + + :ref:`Go to the end ` + to download the full example code. + +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_auto_examples_plot_6_real_time_demo.py: + + +Real-time feature estimation +============================ + +.. GENERATED FROM PYTHON SOURCE LINES 8-46 + +Implementation of individual nm_streams +--------------------------------------- + +*py_neuromodulation* was optimized for computation of real-time data streams. +There are however center -and lab specific hardware acquisition systems. Therefore, each experiment requires modules to interact with hardware platforms +which periodically acquire data. + +Given the raw data, data can be analyzed using *py_neuromodulation*. Preprocessing methods, such as re-referencing and normalization, +feature computation and decoding can be performed then in real-time. + +For online as well as as offline analysis, the :class:`~nm_stream_abc` class needs to be instantiated. +Here the `nm_settings` and `nm_channels` are required to be defined. +Previously for the offline analysis, an offline :class:`~nm_generator` object was defined that periodically yielded data. +For online data, the :meth:`~nm_stream_abc.run` function therefore needs to be overwritten, which first acquires data and then calls +the :meth:`~nm_run_analysis.process` function. + +The following illustrates in pseudo-code how such a stream could be initialized: + +.. code-block:: python + + from py_neuromodulation import nm_stream_abc + + class MyStream(nm_stream_abc): + def __init__(self, settings, channels): + super().__init__(settings, channels) + + def run(self): + features_ = [] + while True: + data = self.acquire_data() + features_.append(self.run_analysis.process(data)) + # potentially use machine learning model for decoding + + +Computation time examples +------------------------- + +The following example calculates for six channels, CAR re-referencing, z-score normalization and FFT features results the following computation time: + +.. GENERATED FROM PYTHON SOURCE LINES 48-105 + +.. code-block:: Python + + import py_neuromodulation as nm + from py_neuromodulation import NMSettings + import numpy as np + import timeit + + + def get_fast_compute_settings(): + settings = NMSettings.get_fast_compute() + + settings.preprocessing = ["re_referencing", "notch_filter"] + settings.features.fft = True + settings.postprocessing.feature_normalization = True + return settings + + + data = np.random.random([1, 1000]) + + print("FFT Features, CAR re-referencing, z-score normalization") + print() + print("Computation time for single ECoG channel: ") + stream = nm.Stream( + sfreq=1000, + data=data, + sampling_rate_features_hz=10, + verbose=False, + settings=get_fast_compute_settings(), + ) + print( + f"{np.round(timeit.timeit(lambda: stream.data_processor.process(data), number=100)/100, 3)} s" + ) + + print("Computation time for 6 ECoG channels: ") + data = np.random.random([6, 1000]) + stream = nm.Stream( + sfreq=1000, + data=data, + sampling_rate_features_hz=10, + verbose=False, + settings=get_fast_compute_settings(), + ) + print( + f"{np.round(timeit.timeit(lambda: stream.data_processor.process(data), number=100)/100, 3)} s" + ) + + print( + "\nFFT Features & Temporal Waveform Shape & Hjorth & Bursts, CAR re-referencing, z-score normalization" + ) + print("Computation time for single ECoG channel: ") + data = np.random.random([1, 1000]) + stream = nm.Stream( + sfreq=1000, data=data, sampling_rate_features_hz=10, verbose=False + ) + print( + f"{np.round(timeit.timeit(lambda: stream.data_processor.process(data), number=10)/10, 3)} s" + ) + + + + + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + FFT Features, CAR re-referencing, z-score normalization + + Computation time for single ECoG channel: + 0.001 s + Computation time for 6 ECoG channels: + 0.001 s + + FFT Features & Temporal Waveform Shape & Hjorth & Bursts, CAR re-referencing, z-score normalization + Computation time for single ECoG channel: + 0.004 s + + + + +.. GENERATED FROM PYTHON SOURCE LINES 106-122 + +Those results show that the computation time for a typical pipeline (FFT, re-referencing, notch-filtering, feature normalization) +is well below 10 ms, which is fast enough for real-time analysis with feature sampling rates below 100 Hz. +Computation of more complex features could still result in feature sampling rates of more than 30 Hz. + +Real-time movement decoding using the TMSi-SAGA amplifier +--------------------------------------------------------- + +In the following example, we will show how we setup a real-time movement decoding experiment using the TMSi-SAGA amplifier. +First, we relied on different software modules for data streaming and visualization. +`LabStreamingLayer `_ allows for real-time data streaming and synchronization across multiple devices. +We used `timeflux `_ for real-time data visualization of features, decoded output. +For raw data visualization we used `Brain Streaming Layer `_. + +The code for real-time movement decoding is added in the GitHub branch `realtime_decoding `_. +Here we relied on the `TMSI SAGA Python interface `_. + + + +.. rst-class:: sphx-glr-timing + + **Total running time of the script:** (0 minutes 0.373 seconds) + + +.. _sphx_glr_download_auto_examples_plot_6_real_time_demo.py: + +.. only:: html + + .. container:: sphx-glr-footer sphx-glr-footer-example + + .. container:: sphx-glr-download sphx-glr-download-jupyter + + :download:`Download Jupyter notebook: plot_6_real_time_demo.ipynb ` + + .. container:: sphx-glr-download sphx-glr-download-python + + :download:`Download Python source code: plot_6_real_time_demo.py ` + + +.. only:: html + + .. rst-class:: sphx-glr-signature + + `Gallery generated by Sphinx-Gallery `_ diff --git a/_sources/auto_examples/plot_7_lsl_example.rst.txt b/_sources/auto_examples/plot_7_lsl_example.rst.txt new file mode 100644 index 00000000..f58dc44c --- /dev/null +++ b/_sources/auto_examples/plot_7_lsl_example.rst.txt @@ -0,0 +1,314 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples/plot_7_lsl_example.py" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. note:: + :class: sphx-glr-download-link-note + + :ref:`Go to the end ` + to download the full example code. + +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_auto_examples_plot_7_lsl_example.py: + + +Lab Streaming Layer (LSL) Example +================================= + +This toolbox implements the lsl ecosystem which can be utilized for offline use cases as well as live streamings +In this example the data introduced in the first demo is being analyzed +in a similar manner, This time however integrating an lsl stream. + +.. GENERATED FROM PYTHON SOURCE LINES 12-23 + +.. code-block:: Python + + from matplotlib import pyplot as plt + + from py_neuromodulation import ( + nm_mnelsl_generator, + nm_IO, + nm_define_nmchannels, + nm_analysis, + nm_stream_offline, + NMSettings, + ) + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 24-25 + +Let’s get the example data from the provided BIDS dataset and create the nm_channels DataFrame. + +.. GENERATED FROM PYTHON SOURCE LINES 25-53 + +.. code-block:: Python + + + ( + RUN_NAME, + PATH_RUN, + PATH_BIDS, + PATH_OUT, + datatype, + ) = nm_IO.get_paths_example_data() + + ( + raw, + data, + sfreq, + line_noise, + coord_list, + coord_names, + ) = nm_IO.read_BIDS_data(PATH_RUN=PATH_RUN) + + nm_channels = nm_define_nmchannels.set_channels( + ch_names=raw.ch_names, + ch_types=raw.get_channel_types(), + reference="default", + bads=raw.info["bads"], + new_names="default", + used_types=("ecog", "dbs", "seeg"), + target_keywords=["MOV_RIGHT"], + ) + + + + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + Extracting parameters from /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vhdr... + Setting channel info structure... + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: Did not find any events.tsv associated with sub-testsub_ses-EphysMedOff_task-gripforce_run-0. + + The search_str was "/home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/**/ieeg/sub-testsub_ses-EphysMedOff*events.tsv" + raw_arr = read_raw_bids(bids_path) + Reading channel info from /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_channels.tsv. + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: The unit for channel(s) MOV_RIGHT has changed from V to NA. + raw_arr = read_raw_bids(bids_path) + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: Other is not an MNE-Python coordinate frame for IEEG data and so will be set to 'unknown' + raw_arr = read_raw_bids(bids_path) + Reading electrode coords from /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_electrodes.tsv. + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: There are channels without locations (n/a) that are not marked as bad: ['MOV_RIGHT'] + raw_arr = read_raw_bids(bids_path) + /home/runner/.venv/lib/python3.10/site-packages/py_neuromodulation/nm_IO.py:64: RuntimeWarning: Not setting position of 1 misc channel found in montage: + ['MOV_RIGHT'] + Consider setting the channel types to be of EEG/sEEG/ECoG/DBS/fNIRS using inst.set_channel_types before calling inst.set_montage, or omit these channels when creating your montage. + raw_arr = read_raw_bids(bids_path) + + + + +.. GENERATED FROM PYTHON SOURCE LINES 54-66 + +Playing the Data +---------------- + +Now we need our data to be represeted in the LSL stream. +For this example an mne_lsl.Player is utilized, which is playing our earlier +recorded data. However, you could make use of any LSL source (live or +offline). +If you want to bind your own data source, make sure to specify the +necessary parameters (data type, type, name) accordingly. +If you are unsure about the parameters of your data source you can +always search for available lsl streams. + + +.. GENERATED FROM PYTHON SOURCE LINES 66-74 + +.. code-block:: Python + + + settings = NMSettings.get_fast_compute() + + player = nm_mnelsl_generator.LSLOfflinePlayer( + raw=raw, stream_name="example_stream" + ) # TODO: add different keyword + + player.start_player(chunk_size=30) + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 75-81 + +Creating the LSLStream object +----------------------------- + +Next let’s create a Stream analog to the First Demo’s example However as +we run the stream, we will set the *lsl-stream* value to True and pass +the stream name we earlier declared when initializing the player object + +.. GENERATED FROM PYTHON SOURCE LINES 81-88 + +.. code-block:: Python + + + settings.features.welch = False + settings.features.fft = True + settings.features.bursts = False + settings.features.sharpwave_analysis = False + settings.features.coherence = False + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 89-97 + +.. code-block:: Python + + stream = nm_stream_offline.Stream( + sfreq=sfreq, + nm_channels=nm_channels, + settings=settings, + coord_list=coord_list, + verbose=True, + line_noise=line_noise, + ) + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 98-99 + +We then simply have to set the `stream_lsl` parameter to be `True` and specify the `stream_lsl_name`. + +.. GENERATED FROM PYTHON SOURCE LINES 99-108 + +.. code-block:: Python + + + features = stream.run( + stream_lsl=True, + plot_lsl=False, + stream_lsl_name="example_stream", + out_path_root=PATH_OUT, + folder_name=RUN_NAME, + ) + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 109-111 + +We can then look at the computed features and check if the streamed data was processed correctly. +This can be verified by the time label: + +.. GENERATED FROM PYTHON SOURCE LINES 111-115 + +.. code-block:: Python + + + plt.plot(features.time, features.MOV_RIGHT) + + + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_7_lsl_example_001.png + :alt: plot 7 lsl example + :srcset: /auto_examples/images/sphx_glr_plot_7_lsl_example_001.png + :class: sphx-glr-single-img + + +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + + [] + + + +.. GENERATED FROM PYTHON SOURCE LINES 116-121 + +Feature Analysis of Movement +---------------------------- +We can now check the movement averaged features of an ECoG channel. +Note that the path was here adapted to be documentation build compliant. +%% + +.. GENERATED FROM PYTHON SOURCE LINES 121-135 + +.. code-block:: Python + + + feature_reader = nm_analysis.FeatureReader(feature_dir=PATH_OUT, feature_file=RUN_NAME) + feature_reader.label_name = "MOV_RIGHT" + feature_reader.label = feature_reader.feature_arr["MOV_RIGHT"] + + feature_reader.plot_target_averaged_channel( + ch="ECOG_RIGHT_0", + list_feature_keywords=None, + epoch_len=4, + threshold=0.5, + ytick_labelsize=7, + figsize_x=12, + figsize_y=12, + ) + + + +.. image-sg:: /auto_examples/images/sphx_glr_plot_7_lsl_example_002.png + :alt: Movement aligned features channel: ECOG_RIGHT_0, MOV_RIGHT + :srcset: /auto_examples/images/sphx_glr_plot_7_lsl_example_002.png + :class: sphx-glr-single-img + + + + + + +.. rst-class:: sphx-glr-timing + + **Total running time of the script:** (0 minutes 23.484 seconds) + + +.. _sphx_glr_download_auto_examples_plot_7_lsl_example.py: + +.. only:: html + + .. container:: sphx-glr-footer sphx-glr-footer-example + + .. container:: sphx-glr-download sphx-glr-download-jupyter + + :download:`Download Jupyter notebook: plot_7_lsl_example.ipynb ` + + .. container:: sphx-glr-download sphx-glr-download-python + + :download:`Download Python source code: plot_7_lsl_example.py ` + + +.. only:: html + + .. rst-class:: sphx-glr-signature + + `Gallery generated by Sphinx-Gallery `_ diff --git a/_sources/auto_examples/plot_8_cebra_example.rst.txt b/_sources/auto_examples/plot_8_cebra_example.rst.txt new file mode 100644 index 00000000..1ad4ab5a --- /dev/null +++ b/_sources/auto_examples/plot_8_cebra_example.rst.txt @@ -0,0 +1,86 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples/plot_8_cebra_example.py" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. note:: + :class: sphx-glr-download-link-note + + :ref:`Go to the end ` + to download the full example code. + +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_auto_examples_plot_8_cebra_example.py: + + +Cebra Decoding with no training Example +====================================== + +The following example show how to use the Cebra decoding without training. + +.. GENERATED FROM PYTHON SOURCE LINES 8-24 + +.. code-block:: Python + + + import os + + # load example_cebra_decoding.html + with open(os.path.join("..", "examples", "example_cebra_decoding.html"), "rt") as fh: + html_data = fh.read() + + tmp_dir = os.path.join("..", "docs", "source", "auto_examples") + if os.path.exists(tmp_dir): + # building the docs with sphinx-gallery + with open(os.path.join(tmp_dir, "out.html"), "wt") as fh: + fh.write(html_data) + # set example path for thumbnail + # sphinx_gallery_thumbnail_path = '_static/CEBRA_embedding.png' + + + + + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 25-30 + +CEBRA example +------------- + +.. raw:: html + :file: out.html + + +.. rst-class:: sphx-glr-timing + + **Total running time of the script:** (0 minutes 0.003 seconds) + + +.. _sphx_glr_download_auto_examples_plot_8_cebra_example.py: + +.. only:: html + + .. container:: sphx-glr-footer sphx-glr-footer-example + + .. container:: sphx-glr-download sphx-glr-download-jupyter + + :download:`Download Jupyter notebook: plot_8_cebra_example.ipynb ` + + .. container:: sphx-glr-download sphx-glr-download-python + + :download:`Download Python source code: plot_8_cebra_example.py ` + + +.. only:: html + + .. rst-class:: sphx-glr-signature + + `Gallery generated by Sphinx-Gallery `_ diff --git a/_sources/auto_examples/sg_execution_times.rst.txt b/_sources/auto_examples/sg_execution_times.rst.txt new file mode 100644 index 00000000..2bb77e88 --- /dev/null +++ b/_sources/auto_examples/sg_execution_times.rst.txt @@ -0,0 +1,61 @@ + +:orphan: + +.. _sphx_glr_auto_examples_sg_execution_times: + + +Computation times +================= +**00:46.996** total execution time for 9 files **from auto_examples**: + +.. container:: + + .. raw:: html + + + + + + + + .. list-table:: + :header-rows: 1 + :class: table table-striped sg-datatable + + * - Example + - Time + - Mem (MB) + * - :ref:`sphx_glr_auto_examples_plot_7_lsl_example.py` (``plot_7_lsl_example.py``) + - 00:23.484 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_1_example_BIDS.py` (``plot_1_example_BIDS.py``) + - 00:11.072 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_0_first_demo.py` (``plot_0_first_demo.py``) + - 00:06.226 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_4_example_gridPointProjection.py` (``plot_4_example_gridPointProjection.py``) + - 00:02.917 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_3_example_sharpwave_analysis.py` (``plot_3_example_sharpwave_analysis.py``) + - 00:01.843 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_2_example_add_feature.py` (``plot_2_example_add_feature.py``) + - 00:01.078 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_6_real_time_demo.py` (``plot_6_real_time_demo.py``) + - 00:00.373 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_8_cebra_example.py` (``plot_8_cebra_example.py``) + - 00:00.003 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_5_example_rmap_computing.py` (``plot_5_example_rmap_computing.py``) + - 00:00.000 + - 0.0 diff --git a/_sources/index.rst.txt b/_sources/index.rst.txt new file mode 100644 index 00000000..ad1896aa --- /dev/null +++ b/_sources/index.rst.txt @@ -0,0 +1,65 @@ +.. py_neuromodulation documentation master file, created by + sphinx-quickstart on Sun Apr 18 11:04:51 2021. + +Welcome to py_neuromodulation's documentation! +============================================== + +The *py_neuromodulation* toolbox allows for real time capable feature estimation of invasive electrophysiological data. + +.. toctree:: + :maxdepth: 2 + :caption: Contents + + installation + usage + auto_examples/index + api_documentation + +Why py_neuromodulation? +----------------------- + +Analyzing neural data can be a troublesome, trial and error prone, +and beginner unfriendly process. *py_neuromodulation* allows using a simple +interface for extraction of established features and includes commonly applied pre -and postprocessing methods. + +Basically only **time series data** with a corresponding **sampling frequency** are required. + +The output will be a pandas DataFrame including different time-resolved computed features. Internally a **stream** get's initialized, +which simulates an *online* data-stream that can also be be used for real-time analysis. + +The following features are currently included: + +* oscillatory: fft, stft or bandpass filtered band power +* `temporal waveform shape `_ +* `fooof `_ +* `mne_connectivity estimates `_ +* `Hjorth parameter `_ +* `non-linear dynamical estimates `_ +* various burst features +* line length +* and more... + +Find here the preprint of **py_neuromodulation** called *"Invasive neurophysiology and whole brain connectomics for neural decoding in patients with brain implants"* [1]_. + + +How can those features be used? +------------------------------- + +The original intention for writing this toolbox was movement decoding from invasive brain signals [2]_. +The application however could be any neural decoding and analysis problem. +*py_neuromodulation* offers wrappers around common practice machine learning methods for efficient analysis. + +References +---------- + +.. [1] Merk, T. et al. *Invasive neurophysiology and whole brain connectomics for neural decoding in patients with brain implants*, `https://doi.org/10.21203/rs.3.rs-3212709/v1` (2023). +.. [2] Merk, T. et al. *Electrocorticography is superior to subthalamic local field potentials for movement decoding in Parkinson’s disease*. Elife 11, e75126, `https://doi.org/10.7554/eLife.75126` (2022). + + + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/_sources/installation.rst.txt b/_sources/installation.rst.txt new file mode 100644 index 00000000..1ba9a39b --- /dev/null +++ b/_sources/installation.rst.txt @@ -0,0 +1,46 @@ +Installation +============ + +py_neuromodulation requires at least python 3.10. For installation you can use pip: + +.. code-block:: + + pip install py-neuromodulation + +We recommend however installing the package using `rye `_: + +.. code-block:: + + git clone https://github.com/neuromodulation/py_neuromodulation.git + rye pin 3.11 + rye sync + +And then activating the virtual environment e.g. in Windows using: + +.. code-block:: + + .\.venv\Scripts\activate + +Alternatively you can also install the package in a conda environment: + + conda create -n pynm-test python=3.11 + conda activate pynm-test + +Then install the packages listed in the `pyproject.toml`: + +.. code-block:: + + pip install . + + +Optionally the ipython kernel can be specified for the installed pynm-test conda environment: + +.. code-block:: + + ipython kernel install --user --name=pynm-test + +Then *py_neuromodulation* can be imported via: + +.. code-block:: + + import py_neuromodulation as nm \ No newline at end of file diff --git a/_sources/nm_IO.rst.txt b/_sources/nm_IO.rst.txt new file mode 100644 index 00000000..6a0e24be --- /dev/null +++ b/_sources/nm_IO.rst.txt @@ -0,0 +1,5 @@ +nm_IO.py +======== + +.. automodule:: nm_IO + :members: \ No newline at end of file diff --git a/_sources/nm_RMAP.rst.txt b/_sources/nm_RMAP.rst.txt new file mode 100644 index 00000000..65953fa8 --- /dev/null +++ b/_sources/nm_RMAP.rst.txt @@ -0,0 +1,5 @@ +nm_RMAP.py +========== + +.. automodule:: nm_RMAP + :members: \ No newline at end of file diff --git a/_sources/nm_analysis.rst.txt b/_sources/nm_analysis.rst.txt new file mode 100644 index 00000000..fb2e65a9 --- /dev/null +++ b/_sources/nm_analysis.rst.txt @@ -0,0 +1,5 @@ +nm_analysis.py +============== + +.. automodule:: nm_analysis.Feature_Reader + :members: diff --git a/_sources/nm_bursts.rst.txt b/_sources/nm_bursts.rst.txt new file mode 100644 index 00000000..6b79d145 --- /dev/null +++ b/_sources/nm_bursts.rst.txt @@ -0,0 +1,5 @@ +nm_bursts.py +============ + +.. automodule:: nm_bursts + :members: \ No newline at end of file diff --git a/_sources/nm_coherence.rst.txt b/_sources/nm_coherence.rst.txt new file mode 100644 index 00000000..63e394f5 --- /dev/null +++ b/_sources/nm_coherence.rst.txt @@ -0,0 +1,5 @@ +nm_coherence.py +=============== + +.. automodule:: nm_coherence + :members: \ No newline at end of file diff --git a/_sources/nm_decode.rst.txt b/_sources/nm_decode.rst.txt new file mode 100644 index 00000000..e56ce304 --- /dev/null +++ b/_sources/nm_decode.rst.txt @@ -0,0 +1,5 @@ +nm_decode.py +============= + +.. autoclass:: nm_decode.Decoder + :members: \ No newline at end of file diff --git a/_sources/nm_define_nmchannels.rst.txt b/_sources/nm_define_nmchannels.rst.txt new file mode 100644 index 00000000..376aeafb --- /dev/null +++ b/_sources/nm_define_nmchannels.rst.txt @@ -0,0 +1,5 @@ +nm_define_nmchannels.py +======================= + +.. automodule:: nm_define_nmchannels + :members: \ No newline at end of file diff --git a/_sources/nm_features.rst.txt b/_sources/nm_features.rst.txt new file mode 100644 index 00000000..0006d92d --- /dev/null +++ b/_sources/nm_features.rst.txt @@ -0,0 +1,5 @@ +nm_features.py +============== + +.. automodule:: nm_features + :members: \ No newline at end of file diff --git a/_sources/nm_filter.rst.txt b/_sources/nm_filter.rst.txt new file mode 100644 index 00000000..c25ab494 --- /dev/null +++ b/_sources/nm_filter.rst.txt @@ -0,0 +1,5 @@ +nm_filter.py +============ + +.. automodule:: nm_filter + :members: \ No newline at end of file diff --git a/_sources/nm_fooof.rst.txt b/_sources/nm_fooof.rst.txt new file mode 100644 index 00000000..51973496 --- /dev/null +++ b/_sources/nm_fooof.rst.txt @@ -0,0 +1,5 @@ +nm_fooof.py +=========== + +.. automodule:: nm_fooof + :members: \ No newline at end of file diff --git a/_sources/nm_generator.rst.txt b/_sources/nm_generator.rst.txt new file mode 100644 index 00000000..2c7eeafb --- /dev/null +++ b/_sources/nm_generator.rst.txt @@ -0,0 +1,5 @@ +nm_generator.py +=============== + +.. automodule:: nm_generator + :members: \ No newline at end of file diff --git a/_sources/nm_hjorth.rst.txt b/_sources/nm_hjorth.rst.txt new file mode 100644 index 00000000..2899e632 --- /dev/null +++ b/_sources/nm_hjorth.rst.txt @@ -0,0 +1,5 @@ +nm_hjorth_raw.py +================ + +.. automodule:: nm_hjorth_raw + :members: \ No newline at end of file diff --git a/_sources/nm_kalmanfilter.rst.txt b/_sources/nm_kalmanfilter.rst.txt new file mode 100644 index 00000000..6b620839 --- /dev/null +++ b/_sources/nm_kalmanfilter.rst.txt @@ -0,0 +1,5 @@ +nm_kalmanfilter.py +================== + +.. automodule:: nm_kalmanfilter + :members: \ No newline at end of file diff --git a/_sources/nm_linelength.rst.txt b/_sources/nm_linelength.rst.txt new file mode 100644 index 00000000..833586dd --- /dev/null +++ b/_sources/nm_linelength.rst.txt @@ -0,0 +1,5 @@ +nm_linelength.py +================ + +.. automodule:: nm_linelength + :members: \ No newline at end of file diff --git a/_sources/nm_mne_connectivity.rst.txt b/_sources/nm_mne_connectivity.rst.txt new file mode 100644 index 00000000..65302682 --- /dev/null +++ b/_sources/nm_mne_connectivity.rst.txt @@ -0,0 +1,5 @@ +nm_mne_connectivity.py +====================== + +.. automodule:: nm_mne_connectivity + :members: \ No newline at end of file diff --git a/_sources/nm_nolds.rst.txt b/_sources/nm_nolds.rst.txt new file mode 100644 index 00000000..c1f79d41 --- /dev/null +++ b/_sources/nm_nolds.rst.txt @@ -0,0 +1,5 @@ +nm_nolds.py +=========== + +.. automodule:: nm_nolds + :members: \ No newline at end of file diff --git a/_sources/nm_normalization.rst.txt b/_sources/nm_normalization.rst.txt new file mode 100644 index 00000000..3d2ffd72 --- /dev/null +++ b/_sources/nm_normalization.rst.txt @@ -0,0 +1,5 @@ +nm_normalization.py +=================== + +.. automodule:: nm_normalization + :members: \ No newline at end of file diff --git a/_sources/nm_oscillatory.rst.txt b/_sources/nm_oscillatory.rst.txt new file mode 100644 index 00000000..5daa6065 --- /dev/null +++ b/_sources/nm_oscillatory.rst.txt @@ -0,0 +1,11 @@ +nm_oscillatory.py +================= + +.. autoclass:: nm_oscillatory.FFT + :members: + +.. autoclass:: nm_oscillatory.BandPower + :members: + +.. autoclass:: nm_oscillatory.STFT + :members: \ No newline at end of file diff --git a/_sources/nm_plots.rst.txt b/_sources/nm_plots.rst.txt new file mode 100644 index 00000000..0d9bbcd3 --- /dev/null +++ b/_sources/nm_plots.rst.txt @@ -0,0 +1,5 @@ +nm_plots.py +=========== + +.. automodule:: nm_plots + :members: \ No newline at end of file diff --git a/_sources/nm_projection.rst.txt b/_sources/nm_projection.rst.txt new file mode 100644 index 00000000..00c61728 --- /dev/null +++ b/_sources/nm_projection.rst.txt @@ -0,0 +1,5 @@ +nm_projection.py +================ + +.. autoclass:: nm_projection.Projection + :members: \ No newline at end of file diff --git a/_sources/nm_rereference.rst.txt b/_sources/nm_rereference.rst.txt new file mode 100644 index 00000000..40053080 --- /dev/null +++ b/_sources/nm_rereference.rst.txt @@ -0,0 +1,5 @@ +nm_rereference.py +================= + +.. autoclass:: nm_rereference.ReReferencer + :members: \ No newline at end of file diff --git a/_sources/nm_resample.rst.txt b/_sources/nm_resample.rst.txt new file mode 100644 index 00000000..7bc4190b --- /dev/null +++ b/_sources/nm_resample.rst.txt @@ -0,0 +1,5 @@ +nm_resample.py +============== + +.. autoclass:: nm_resample.Resampler + :members: \ No newline at end of file diff --git a/_sources/nm_run_analysis.rst.txt b/_sources/nm_run_analysis.rst.txt new file mode 100644 index 00000000..2c34e80e --- /dev/null +++ b/_sources/nm_run_analysis.rst.txt @@ -0,0 +1,5 @@ +nm_run_analysis.py +================== + +.. autoclass:: nm_run_analysis.DataProcessor + :members: \ No newline at end of file diff --git a/_sources/nm_settings.rst.txt b/_sources/nm_settings.rst.txt new file mode 100644 index 00000000..8288acc0 --- /dev/null +++ b/_sources/nm_settings.rst.txt @@ -0,0 +1,5 @@ +nm_settings.py +============== + +.. automodule:: nm_settings + :members: \ No newline at end of file diff --git a/_sources/nm_sharpwaves.rst.txt b/_sources/nm_sharpwaves.rst.txt new file mode 100644 index 00000000..f5caffc7 --- /dev/null +++ b/_sources/nm_sharpwaves.rst.txt @@ -0,0 +1,5 @@ +nm_sharpwaves.py +================ + +.. autoclass:: nm_sharpwaves.SharpwaveAnalyzer + :members: \ No newline at end of file diff --git a/_sources/nm_stats.rst.txt b/_sources/nm_stats.rst.txt new file mode 100644 index 00000000..1120b60b --- /dev/null +++ b/_sources/nm_stats.rst.txt @@ -0,0 +1,5 @@ +nm_stats.py +============== + +.. automodule:: nm_stats + :members: \ No newline at end of file diff --git a/_sources/nm_stream_abc.rst.txt b/_sources/nm_stream_abc.rst.txt new file mode 100644 index 00000000..ff5e9a15 --- /dev/null +++ b/_sources/nm_stream_abc.rst.txt @@ -0,0 +1,5 @@ +nm_stream_abc.py +================ + +.. automodule:: nm_stream_abc + :members: \ No newline at end of file diff --git a/_sources/nm_stream_offline.rst.txt b/_sources/nm_stream_offline.rst.txt new file mode 100644 index 00000000..a50d166f --- /dev/null +++ b/_sources/nm_stream_offline.rst.txt @@ -0,0 +1,5 @@ +nm_stream_offline.py +==================== + +.. automodule:: nm_stream_offline + :members: \ No newline at end of file diff --git a/_sources/sg_execution_times.rst.txt b/_sources/sg_execution_times.rst.txt new file mode 100644 index 00000000..5b6d3b7e --- /dev/null +++ b/_sources/sg_execution_times.rst.txt @@ -0,0 +1,61 @@ + +:orphan: + +.. _sphx_glr_sg_execution_times: + + +Computation times +================= +**00:46.996** total execution time for 9 files **from all galleries**: + +.. container:: + + .. raw:: html + + + + + + + + .. list-table:: + :header-rows: 1 + :class: table table-striped sg-datatable + + * - Example + - Time + - Mem (MB) + * - :ref:`sphx_glr_auto_examples_plot_7_lsl_example.py` (``../../examples/plot_7_lsl_example.py``) + - 00:23.484 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_1_example_BIDS.py` (``../../examples/plot_1_example_BIDS.py``) + - 00:11.072 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_0_first_demo.py` (``../../examples/plot_0_first_demo.py``) + - 00:06.226 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_4_example_gridPointProjection.py` (``../../examples/plot_4_example_gridPointProjection.py``) + - 00:02.917 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_3_example_sharpwave_analysis.py` (``../../examples/plot_3_example_sharpwave_analysis.py``) + - 00:01.843 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_2_example_add_feature.py` (``../../examples/plot_2_example_add_feature.py``) + - 00:01.078 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_6_real_time_demo.py` (``../../examples/plot_6_real_time_demo.py``) + - 00:00.373 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_8_cebra_example.py` (``../../examples/plot_8_cebra_example.py``) + - 00:00.003 + - 0.0 + * - :ref:`sphx_glr_auto_examples_plot_5_example_rmap_computing.py` (``../../examples/plot_5_example_rmap_computing.py``) + - 00:00.000 + - 0.0 diff --git a/_sources/usage.rst.txt b/_sources/usage.rst.txt new file mode 100644 index 00000000..545ca8a5 --- /dev/null +++ b/_sources/usage.rst.txt @@ -0,0 +1,559 @@ +Usage +===== + + +We will explain here the basic usage of py_neuromodulation. Check out the :doc:`examples ` for directly following along. + +In general only a time series is required with a specified sampling frequency. + +Here is the definition of a minimalistic example: + +.. code-block:: python + + import py_neuromodulation as nm + import numpy as np + + NUM_CHANNELS = 5 + NUM_DATA = 10000 + sfreq = 1000 # Hz + sampling_rate_features_hz = 3 # Hz + + data = np.random.random([NUM_CHANNELS, NUM_DATA]) + + stream = nm.Stream(sfreq=sfreq, data=data, sampling_rate_features_hz=sampling_rate_features_hz) + features = stream.run() + +`features` will be a pd.DataFrame containing the computed features for each channel and each time point. In this example the default signal processing pipeline is estimated. +An offline data-stream was initialized with raw data being common-average re-referenced. FFT, bursting features and temporal waveform-shape were computed. +Features were calculated with a *sampling_rate_features_hz* of 3 Hz and subsequently *z-score* normalized. + +We can however further define channel-specific parametrization such as re-referencing, channel selection, target definition, +and also select and define additional features. + +Check out the example :doc:`auto_examples/plot_0_first_demo` for a first introduction. + +The following sections discuss additional parametrization. In a nutshell, the **settings** dictionary and a **channels** dataframe are used for parametrization. +The above example implicitly used the default settings and channels. However, we can also define directly specific settings and channels: + +.. code-block:: python + + from py_neuromodulation import nm_define_nmchannels, nm_settings + + channels = nm_define_nmchannels.get_default_channels_from_data(data, car_rereferencing=True) + settings = nm_settings.get_default_settings() + +Channel parametrization +----------------------- + +The channel parametrization is defined in a :class:`~pandas.DataFrame`. The following columns are required: + ++-----------------------------------+----------------------------------------+ +| Column name | Description | ++===================================+========================================+ +| **name** (string) | name of the channel | ++-----------------------------------+----------------------------------------+ +| **rereference** (string) | different channel name for | +| | bipolar rereferencing, or | +| | *average* for common average | +| | re-referencing | ++-----------------------------------+----------------------------------------+ +| **used** (int) | 0 or 1, channel selection | ++-----------------------------------+----------------------------------------+ +| **target** (int) | 0 or 1, for some decoding | +| | applications we can define target | +| | chanenls, e.g. EMG channels. | +| | Target channels are not used for | +| | feature computation. | ++-----------------------------------+----------------------------------------+ +| **type** (string) | `mne-python`_ supported channel types | +| | e.g. ecog, eeg, ecg, emg, dbs, | +| | seeg etc. | ++-----------------------------------+----------------------------------------+ +| **status** (string) | *good* or *bad*, used for channel | +| | quality indication | ++-----------------------------------+----------------------------------------+ +| **new_name** (string) | this keyword can be specified to | +| | indicate the used | +| | re-referncing scheme | ++-----------------------------------+----------------------------------------+ + +.. _mne-python: https://mne.tools/stable/glossary.html#term-data-channels + + +**rereferencing** constitutes an important aspect of electrophysiological signal processing. +Most commonly bipolar and common average re-referencing are applied for separate channel modalities. +The following possible parametrization option are available: + +.. list-table:: + :header-rows: 1 + + * - Rereference Type + - Description + - Example + * - average + - common average rereference (across a channel type, e.g. ecog or eeg) + - *average* + * - bipolar + - bipolar rereferencing, specified by the channel name to rereference to + - *LFP_RIGHT_0* + * - *combination* + - combination of different channels separated by "&" + - *LFP_RIGHT_0&LFP_RIGHT_1* + * - none + - no rereferencing being used for the particular channel + - *none* + +The **nm_channels** can either be created as a *.tsv* file, or as a pandas DataFrame. +There are some helper functions that let you create the nm_channels without much effort: + +.. code-block:: python + + nm_channels = nm_define_nmchannels.get_default_channels_from_data(data, car_rereferencing=True) + +When setting up the :class:`~nm_stream_abc`, `nm_settings` and `nm_channels` can also be defined and passed to the init function: + +.. code-block:: python + + import py_neuromodulation as nm + + stream = nm.Stream( + sfreq=sfreq, + nm_channels=nm_channels, + settings=settings, + ) + +Setting definition +------------------ + +The *nm_settings* allow for parametrization of all features. Default settings are passed from the `nm_settings.json` file: + +.. toggle:: + + .. literalinclude:: ../../py_neuromodulation/nm_settings.json + :language: json + + +Preprocessing +^^^^^^^^^^^^^ + +The following preprocessing options can be written in the *preprocessing* field, **which will be executed in the specified order**\ : + +.. code-block:: json + + "documentation_preprocessing_options": [ + "raw_resampling", + "notch_filter", + "re_referencing", + "raw_normalization" + ], + +Resampling +~~~~~~~~~~ + +**raw_resampling** defines a resampling rate to which the original data is downsampled to. This can be of advantage, since high sampling frequencies automatically require usually more computational cost. In the method specific settings the resampling frequency can be defined: + +.. code-block:: json + + "raw_resampling_settings": { + "resample_freq_hz": 1000 + } + +Notch Filtering +~~~~~~~~~~~~~~~ + +**notch_filer** can be enabled with the *line_noise* frequency supplied as a init parameter to :class:`~nm_stream_abc`. + +Normalization +~~~~~~~~~~~~~ + +**normalization** allows for normalizing the past *normalization_time* in seconds according to the following options: + +* mean +* median +* zscore +* zscore-median +* quantile +* power +* robust +* minmax + +The latter four options are obtained via wrappers around the `scikit-learn preprocessing `_ modules. + +*zscore-median* is implemented using the following equation: :math:`X_{norm} = \frac{X - median(X)}{median(X)}` + +The *normalization_time* allows to specify a **past** time window that will be used for normalization. The setting specification for *raw* and *feature* normalization is specified in the same manner: + +.. code-block:: json + + "raw_normalization_settings": { + "normalization_time": 10, + "normalization_method": "median" + } + +Features +^^^^^^^^ + +Features can be enabled and disabled using the *features* key: + +.. code-block:: json + + "features": + { + "fft": true, + "stft": true, + "bandpass_filter": true, + "sharpwave_analysis": true, + "raw_hjorth": true, + "return_raw": true, + "coherence": true, + "fooof": true, + "bursts": true, + "linelength": true, + "nolds": true, + "mne_connectivity": true + } + +Oscillatory features +~~~~~~~~~~~~~~~~~~~~ + +Frequency band specification +"""""""""""""""""""""""""""" + +Frequency bands are specified in the settings within a dictionary of frequency band names and a list of lower and upper band ranges. +The supplied frequency ranges can be utilized by different feature modalities, e.g. fft, coherence, sharpwave etc. + +.. code-block:: json + + "frequency_ranges_hz": { + "theta": [ + 4, + 8 + ], + "alpha": [ + 8, + 12 + ], + +FFT and STFT +"""""""""""" + +Fast Fourier Transform and Short-Time Fourier Transform are both specified using the same settings parametrization: + +.. code-block:: json + + "fft_settings": { + "windowlength_ms": 1000, + "log_transform": true, + "kalman_filter": false + } + +*log_transform* is here a recommended setting. + +Kalman filtering +"""""""""""""""" + +**kalman_filter** can be enabled for all oscillatory features and is motivated by filtering estimated band power features +using the white noise acceleration model +(see `"Improved detection of Parkinsonian resting tremor with feature engineering and Kalman filtering" `_ Yao et al 19). +The white noise acceleration model get's specified by the :math:`T_p` prediction interval (Hz), and the process noise is then defined by :math:`\sigma_w` and :math:`\sigma_v`: + +.. math:: + + Q = \begin{bmatrix} \sigma_w^2 \frac{T_p^{3}}{3} & \sigma_w^2 \frac{T_p^2}{2}\\ + \sigma_w^2 \frac{T_p^2}{3} & \sigma_w^2T_p\ \end{bmatrix} + + + +The settings can be specified as follows: + +.. code-block:: json + + "kalman_filter_settings": { + "Tp": 0.1, + "sigma_w": 0.7, + "sigma_v": 1, + "frequency_bands": [ + "low gamma", + "high gamma", + "all gamma" + ] + } + +Individual frequency bands (specified in the *frequency_ranges_hz*\ ) can be selected for Kalman Filtering (see `Chisci et al 2010 `_ for an example). + +Bandpass filter +""""""""""""""" + +**bandpass_filter** enables band power feature estimation through precomputation of a FIR filter +using the `mne.filter.create_filter `_ function. + +.. code-block:: json + + "bandpass_filter_settings": { + "segment_lengths_ms": { + "theta": 1000, + "alpha": 500, + "low beta": 333, + "high beta": 333, + "low gamma": 100, + "high gamma": 100, + "HFA": 100 + }, + "bandpower_features": { + "activity": true, + "mobility": false, + "complexity": false + }, + "log_transform": true, + "kalman_filter": false + } + +The *segment_length_ms* parameter defines a time range in which FIR filtered data is used for feature estimation. +In this example for the theta frequency band the previous 1000 ms are used to estimate features based +on the FIR filtered signal. This might be beneficial when using shorter frequency bands, e.g. gamma, +where estimating band power in a range of e.g. 100 ms might result in a temporal more specific feature calculation. +A common way to estimate band power is to take the variance of FIR filtered data. This is equivalent to +the activity `Hjorth `_ parameter. +The Hjorth parameters *activity*\ , *mobility* and *complexity* can be computed on bandpass filtered data as well. +For estimating all Hjorth parameters of the raw unfiltered signal, **raw_hjorth** can be enabled. + +Analyzing temporal waveform shape +""""""""""""""""""""""""""""""""" + +**sharpwave_analysis** allows for calculation of temporal waveform features. +See `"Brain Oscillations and the Importance of Waveform Shape" `_ +Cole et al 17 for a great motivation to use these features. Here, sharpwave features are estimated using a prior bandpass filter +between the *filter_low_cutoff* and *filter_high_cutoff* ranges. +The sharpwave peak and trough features can be calculated, defined by the *estimate* key. +According to a current data batch one or more temporal waveform events +can be detected. The subsequent feature is returned as the *mean, median, maximum, minimum* or *variance* +of all events in the feature computation batch, defined by the *estimator*. +For further introduction see the example notebook :doc:`auto_examples/plot_3_example_sharpwave_analysis`. + +Here the full parametrization in the *nm_settings*: + +.. toggle:: + + .. code-block:: json + + "sharpwave_analysis_settings": { + "sharpwave_features": { + "peak_left": false, + "peak_right": false, + "trough": false, + "width": false, + "prominence": true, + "interval": true, + "decay_time": false, + "rise_time": false, + "sharpness": true, + "rise_steepness": false, + "decay_steepness": false, + "slope_ratio": false + }, + "filter_ranges_hz": [ + [ + 5, + 80 + ], + [ + 5, + 30 + ] + ], + "detect_troughs": { + "estimate": true, + "distance_troughs_ms": 10, + "distance_peaks_ms": 5 + }, + "detect_peaks": { + "estimate": true, + "distance_troughs_ms": 5, + "distance_peaks_ms": 10 + }, + "estimator": { + "mean": [ + "interval" + ], + "median": null, + "max": [ + "prominence", + "sharpness" + ], + "min": null, + "var": null + }, + "apply_estimator_between_peaks_and_troughs": true + } + +Raw signals +~~~~~~~~~~~ + +Next, raw signals can be returned, specified by the **return_raw** method. This can be useful for using e.g. +normalization, rereferencing or resampling before feeding data to a deep learning model. + +Characterization of spectral aperiodic component +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There is also a wrapper around the `fooof `_ toolbox for characterization of the periodic and aperiodic components. +Periodic components will be returned with a *peak_idx*\ , the respective center frequency, bandwith, and height over the +aperiodic component. *fooof* specific parameters, e.g. *knee* or *max_n_peaks* are passed to the fooof object as well: + +.. code-block:: json + + "fooof": { + "aperiodic": { + "exponent": true, + "offset": true, + "knee": true + }, + "periodic": { + "center_frequency": false, + "band_width": false, + "height_over_ap": false + }, + "windowlength_ms": 800, + "peak_width_limits": [ + 0.5, + 12 + ], + "max_n_peaks": 3, + "min_peak_height": 0, + "peak_threshold": 2, + "freq_range_hz": [ + 2, + 40 + ], + "knee": true + } + +.. note:: + When using the knee parameter, the *knee_frequency* is returned for every fit. See also the fooof `Aperiodic Component Fitting Notebook `_. + +Nonlinear measures for dynamical systems (nolds) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**nolds** features are estimates as a direct wrapper around the `nolds toolbox `_. +Features can be estimated from raw data directly, or data being filtered in different frequency bands. + +.. warning:: + The computation time for this feature modality is however very high. For real time applications we tested it was not applicable. + +.. code-block:: json + + "nolds_features": { + "sample_entropy": true, + "correlation_dimension": true, + "lyapunov_exponent": true, + "hurst_exponent": true, + "detrended_fluctutaion_analysis": true, + "data": { + "raw": true, + "frequency_bands": [ + "theta", + "alpha", + "low beta", + "high beta", + "low gamma", + "high gamma", + "HFA" + ] + } + } + +Coherence +~~~~~~~~~ + +**coherence** can be calculated for channel pairs that are passed as a list of lists. +Each list contains the channels specified in *nm_channels*. +The mean and/or maximum in a specific frequency band can be calculated. +The maximum for all frequency bands can also be estimated. + +.. code-block:: json + + "coherence": { + "channels": [ + [ + "STN_RIGHT_0", + "ECOG_RIGHT_0" + ] + ], + "frequency_bands": [ + "high beta" + ], + "features": { + "mean_fband": true, + "max_fband": true, + "max_allfbands": true + }, + "method": { + "coh": true, + "icoh": true + } + } + +Bursts +~~~~~~ + +**bursting** features were previously often investigated in invasive electrophysiology. +Here burst features for different frequency bands with specified *time_duration_s* and *threshold* can be estimated: + +.. code-block:: json + + "burst_settings": { + "threshold": 75, + "time_duration_s": 30, + "frequency_bands": [ + "low beta", + "high beta", + "low gamma" + ], + "burst_features": { + "duration": true, + "amplitude": true, + "burst_rate_per_s": true, + "in_burst": true + } + } + +MNE-connectivity +~~~~~~~~~~~~~~~~ + +**MNE-connectivity** is a direct wrapper around the mne_connectivity `spectral_connectivity_epochs `_ function. + +.. code-block:: json + + "mne_connectiviy": { + "method": "plv", + "mode": "multitaper" + } + +Line length +~~~~~~~~~~~ + +**linelength** is a very simple features that calculates in the specified batch the sum of the absolute signal of a channel *x*: + +.. math:: + + LineLength(x) = \sum_{i=0}^{Batch\ Length} |x_i| + +Postprocessing +^^^^^^^^^^^^^^ + +Projection +~~~~~~~~~~ + +**projection_cortex** and **projection_subcortex** allow for feature projection of individual channels to a common subcortical +or cortical grid, defined by the *grid_cortex.tsv* and *subgrid_cortex.tsv* files. +Example *.tsv* files can be found in the shipped py_neuromodulation package. +For both projections a *max_dist_mm* parameter needs to be specified, in which data is linearly interpolated, weighted by their inverse grid point distance. +For further motivation see the example notebook :doc:`auto_examples/plot_4_example_gridPointProjection`. + +.. code-block:: json + + "project_cortex_settings": { + "max_dist_mm": 20 + }, + "project_subcortex_settings": { + "max_dist_mm": 5 + } diff --git a/_static/CEBRA_embedding.png b/_static/CEBRA_embedding.png new file mode 100644 index 00000000..6460156a Binary files /dev/null and b/_static/CEBRA_embedding.png differ diff --git a/_static/RMAP_figure.png b/_static/RMAP_figure.png new file mode 100644 index 00000000..97c798aa Binary files /dev/null and b/_static/RMAP_figure.png differ diff --git a/_static/basic.css b/_static/basic.css new file mode 100644 index 00000000..2af6139e --- /dev/null +++ b/_static/basic.css @@ -0,0 +1,925 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 270px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/_static/binder_badge_logo.svg b/_static/binder_badge_logo.svg new file mode 100644 index 00000000..327f6b63 --- /dev/null +++ b/_static/binder_badge_logo.svg @@ -0,0 +1 @@ + launchlaunchbinderbinder \ No newline at end of file diff --git a/_static/broken_example.png b/_static/broken_example.png new file mode 100644 index 00000000..4fea24e7 Binary files /dev/null and b/_static/broken_example.png differ diff --git a/_static/css/RMAP_figure.png b/_static/css/RMAP_figure.png new file mode 100644 index 00000000..97c798aa Binary files /dev/null and b/_static/css/RMAP_figure.png differ diff --git a/_static/css/project-template.css b/_static/css/project-template.css new file mode 100644 index 00000000..f6caff23 --- /dev/null +++ b/_static/css/project-template.css @@ -0,0 +1,16 @@ +@import url("theme.css"); + +.highlight a { + text-decoration: underline; +} + +.deprecated p { + padding: 10px 7px 10px 10px; + color: #b94a48; + background-color: #F3E5E5; + border: 1px solid #eed3d7; +} + +.deprecated p span.versionmodified { + font-weight: bold; +} diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 00000000..4d67807d --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 00000000..7e4c114f --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 00000000..a858a410 Binary files /dev/null and b/_static/file.png differ diff --git a/_static/jupyterlite_badge_logo.svg b/_static/jupyterlite_badge_logo.svg new file mode 100644 index 00000000..5de36d7f --- /dev/null +++ b/_static/jupyterlite_badge_logo.svg @@ -0,0 +1,3 @@ + + +launchlaunchlitelite \ No newline at end of file diff --git a/_static/language_data.js b/_static/language_data.js new file mode 100644 index 00000000..367b8ed8 --- /dev/null +++ b/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/_static/minus.png b/_static/minus.png new file mode 100644 index 00000000..d96755fd Binary files /dev/null and b/_static/minus.png differ diff --git a/_static/no_image.png b/_static/no_image.png new file mode 100644 index 00000000..8c2d48d5 Binary files /dev/null and b/_static/no_image.png differ diff --git a/_static/plus.png b/_static/plus.png new file mode 100644 index 00000000..7107cec9 Binary files /dev/null and b/_static/plus.png differ diff --git a/_static/pygments.css b/_static/pygments.css new file mode 100644 index 00000000..012e6a00 --- /dev/null +++ b/_static/pygments.css @@ -0,0 +1,152 @@ +html[data-theme="light"] .highlight pre { line-height: 125%; } +html[data-theme="light"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight .hll { background-color: #fae4c2 } +html[data-theme="light"] .highlight { background: #fefefe; color: #080808 } +html[data-theme="light"] .highlight .c { color: #515151 } /* Comment */ +html[data-theme="light"] .highlight .err { color: #a12236 } /* Error */ +html[data-theme="light"] .highlight .k { color: #6730c5 } /* Keyword */ +html[data-theme="light"] .highlight .l { color: #7f4707 } /* Literal */ +html[data-theme="light"] .highlight .n { color: #080808 } /* Name */ +html[data-theme="light"] .highlight .o { color: #00622f } /* Operator */ +html[data-theme="light"] .highlight .p { color: #080808 } /* Punctuation */ +html[data-theme="light"] .highlight .ch { color: #515151 } /* Comment.Hashbang */ +html[data-theme="light"] .highlight .cm { color: #515151 } /* Comment.Multiline */ +html[data-theme="light"] .highlight .cp { color: #515151 } /* Comment.Preproc */ +html[data-theme="light"] .highlight .cpf { color: #515151 } /* Comment.PreprocFile */ +html[data-theme="light"] .highlight .c1 { color: #515151 } /* Comment.Single */ +html[data-theme="light"] .highlight .cs { color: #515151 } /* Comment.Special */ +html[data-theme="light"] .highlight .gd { color: #005b82 } /* Generic.Deleted */ +html[data-theme="light"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="light"] .highlight .gh { color: #005b82 } /* Generic.Heading */ +html[data-theme="light"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="light"] .highlight .gu { color: #005b82 } /* Generic.Subheading */ +html[data-theme="light"] .highlight .kc { color: #6730c5 } /* Keyword.Constant */ +html[data-theme="light"] .highlight .kd { color: #6730c5 } /* Keyword.Declaration */ +html[data-theme="light"] .highlight .kn { color: #6730c5 } /* Keyword.Namespace */ +html[data-theme="light"] .highlight .kp { color: #6730c5 } /* Keyword.Pseudo */ +html[data-theme="light"] .highlight .kr { color: #6730c5 } /* Keyword.Reserved */ +html[data-theme="light"] .highlight .kt { color: #7f4707 } /* Keyword.Type */ +html[data-theme="light"] .highlight .ld { color: #7f4707 } /* Literal.Date */ +html[data-theme="light"] .highlight .m { color: #7f4707 } /* Literal.Number */ +html[data-theme="light"] .highlight .s { color: #00622f } /* Literal.String */ +html[data-theme="light"] .highlight .na { color: #912583 } /* Name.Attribute */ +html[data-theme="light"] .highlight .nb { color: #7f4707 } /* Name.Builtin */ +html[data-theme="light"] .highlight .nc { color: #005b82 } /* Name.Class */ +html[data-theme="light"] .highlight .no { color: #005b82 } /* Name.Constant */ +html[data-theme="light"] .highlight .nd { color: #7f4707 } /* Name.Decorator */ +html[data-theme="light"] .highlight .ni { color: #00622f } /* Name.Entity */ +html[data-theme="light"] .highlight .ne { color: #6730c5 } /* Name.Exception */ +html[data-theme="light"] .highlight .nf { color: #005b82 } /* Name.Function */ +html[data-theme="light"] .highlight .nl { color: #7f4707 } /* Name.Label */ +html[data-theme="light"] .highlight .nn { color: #080808 } /* Name.Namespace */ +html[data-theme="light"] .highlight .nx { color: #080808 } /* Name.Other */ +html[data-theme="light"] .highlight .py { color: #005b82 } /* Name.Property */ +html[data-theme="light"] .highlight .nt { color: #005b82 } /* Name.Tag */ +html[data-theme="light"] .highlight .nv { color: #a12236 } /* Name.Variable */ +html[data-theme="light"] .highlight .ow { color: #6730c5 } /* Operator.Word */ +html[data-theme="light"] .highlight .pm { color: #080808 } /* Punctuation.Marker */ +html[data-theme="light"] .highlight .w { color: #080808 } /* Text.Whitespace */ +html[data-theme="light"] .highlight .mb { color: #7f4707 } /* Literal.Number.Bin */ +html[data-theme="light"] .highlight .mf { color: #7f4707 } /* Literal.Number.Float */ +html[data-theme="light"] .highlight .mh { color: #7f4707 } /* Literal.Number.Hex */ +html[data-theme="light"] .highlight .mi { color: #7f4707 } /* Literal.Number.Integer */ +html[data-theme="light"] .highlight .mo { color: #7f4707 } /* Literal.Number.Oct */ +html[data-theme="light"] .highlight .sa { color: #00622f } /* Literal.String.Affix */ +html[data-theme="light"] .highlight .sb { color: #00622f } /* Literal.String.Backtick */ +html[data-theme="light"] .highlight .sc { color: #00622f } /* Literal.String.Char */ +html[data-theme="light"] .highlight .dl { color: #00622f } /* Literal.String.Delimiter */ +html[data-theme="light"] .highlight .sd { color: #00622f } /* Literal.String.Doc */ +html[data-theme="light"] .highlight .s2 { color: #00622f } /* Literal.String.Double */ +html[data-theme="light"] .highlight .se { color: #00622f } /* Literal.String.Escape */ +html[data-theme="light"] .highlight .sh { color: #00622f } /* Literal.String.Heredoc */ +html[data-theme="light"] .highlight .si { color: #00622f } /* Literal.String.Interpol */ +html[data-theme="light"] .highlight .sx { color: #00622f } /* Literal.String.Other */ +html[data-theme="light"] .highlight .sr { color: #a12236 } /* Literal.String.Regex */ +html[data-theme="light"] .highlight .s1 { color: #00622f } /* Literal.String.Single */ +html[data-theme="light"] .highlight .ss { color: #005b82 } /* Literal.String.Symbol */ +html[data-theme="light"] .highlight .bp { color: #7f4707 } /* Name.Builtin.Pseudo */ +html[data-theme="light"] .highlight .fm { color: #005b82 } /* Name.Function.Magic */ +html[data-theme="light"] .highlight .vc { color: #a12236 } /* Name.Variable.Class */ +html[data-theme="light"] .highlight .vg { color: #a12236 } /* Name.Variable.Global */ +html[data-theme="light"] .highlight .vi { color: #a12236 } /* Name.Variable.Instance */ +html[data-theme="light"] .highlight .vm { color: #7f4707 } /* Name.Variable.Magic */ +html[data-theme="light"] .highlight .il { color: #7f4707 } /* Literal.Number.Integer.Long */ +html[data-theme="dark"] .highlight pre { line-height: 125%; } +html[data-theme="dark"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight .hll { background-color: #ffd9002e } +html[data-theme="dark"] .highlight { background: #2b2b2b; color: #f8f8f2 } +html[data-theme="dark"] .highlight .c { color: #ffd900 } /* Comment */ +html[data-theme="dark"] .highlight .err { color: #ffa07a } /* Error */ +html[data-theme="dark"] .highlight .k { color: #dcc6e0 } /* Keyword */ +html[data-theme="dark"] .highlight .l { color: #ffd900 } /* Literal */ +html[data-theme="dark"] .highlight .n { color: #f8f8f2 } /* Name */ +html[data-theme="dark"] .highlight .o { color: #abe338 } /* Operator */ +html[data-theme="dark"] .highlight .p { color: #f8f8f2 } /* Punctuation */ +html[data-theme="dark"] .highlight .ch { color: #ffd900 } /* Comment.Hashbang */ +html[data-theme="dark"] .highlight .cm { color: #ffd900 } /* Comment.Multiline */ +html[data-theme="dark"] .highlight .cp { color: #ffd900 } /* Comment.Preproc */ +html[data-theme="dark"] .highlight .cpf { color: #ffd900 } /* Comment.PreprocFile */ +html[data-theme="dark"] .highlight .c1 { color: #ffd900 } /* Comment.Single */ +html[data-theme="dark"] .highlight .cs { color: #ffd900 } /* Comment.Special */ +html[data-theme="dark"] .highlight .gd { color: #00e0e0 } /* Generic.Deleted */ +html[data-theme="dark"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="dark"] .highlight .gh { color: #00e0e0 } /* Generic.Heading */ +html[data-theme="dark"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="dark"] .highlight .gu { color: #00e0e0 } /* Generic.Subheading */ +html[data-theme="dark"] .highlight .kc { color: #dcc6e0 } /* Keyword.Constant */ +html[data-theme="dark"] .highlight .kd { color: #dcc6e0 } /* Keyword.Declaration */ +html[data-theme="dark"] .highlight .kn { color: #dcc6e0 } /* Keyword.Namespace */ +html[data-theme="dark"] .highlight .kp { color: #dcc6e0 } /* Keyword.Pseudo */ +html[data-theme="dark"] .highlight .kr { color: #dcc6e0 } /* Keyword.Reserved */ +html[data-theme="dark"] .highlight .kt { color: #ffd900 } /* Keyword.Type */ +html[data-theme="dark"] .highlight .ld { color: #ffd900 } /* Literal.Date */ +html[data-theme="dark"] .highlight .m { color: #ffd900 } /* Literal.Number */ +html[data-theme="dark"] .highlight .s { color: #abe338 } /* Literal.String */ +html[data-theme="dark"] .highlight .na { color: #ffd900 } /* Name.Attribute */ +html[data-theme="dark"] .highlight .nb { color: #ffd900 } /* Name.Builtin */ +html[data-theme="dark"] .highlight .nc { color: #00e0e0 } /* Name.Class */ +html[data-theme="dark"] .highlight .no { color: #00e0e0 } /* Name.Constant */ +html[data-theme="dark"] .highlight .nd { color: #ffd900 } /* Name.Decorator */ +html[data-theme="dark"] .highlight .ni { color: #abe338 } /* Name.Entity */ +html[data-theme="dark"] .highlight .ne { color: #dcc6e0 } /* Name.Exception */ +html[data-theme="dark"] .highlight .nf { color: #00e0e0 } /* Name.Function */ +html[data-theme="dark"] .highlight .nl { color: #ffd900 } /* Name.Label */ +html[data-theme="dark"] .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ +html[data-theme="dark"] .highlight .nx { color: #f8f8f2 } /* Name.Other */ +html[data-theme="dark"] .highlight .py { color: #00e0e0 } /* Name.Property */ +html[data-theme="dark"] .highlight .nt { color: #00e0e0 } /* Name.Tag */ +html[data-theme="dark"] .highlight .nv { color: #ffa07a } /* Name.Variable */ +html[data-theme="dark"] .highlight .ow { color: #dcc6e0 } /* Operator.Word */ +html[data-theme="dark"] .highlight .pm { color: #f8f8f2 } /* Punctuation.Marker */ +html[data-theme="dark"] .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ +html[data-theme="dark"] .highlight .mb { color: #ffd900 } /* Literal.Number.Bin */ +html[data-theme="dark"] .highlight .mf { color: #ffd900 } /* Literal.Number.Float */ +html[data-theme="dark"] .highlight .mh { color: #ffd900 } /* Literal.Number.Hex */ +html[data-theme="dark"] .highlight .mi { color: #ffd900 } /* Literal.Number.Integer */ +html[data-theme="dark"] .highlight .mo { color: #ffd900 } /* Literal.Number.Oct */ +html[data-theme="dark"] .highlight .sa { color: #abe338 } /* Literal.String.Affix */ +html[data-theme="dark"] .highlight .sb { color: #abe338 } /* Literal.String.Backtick */ +html[data-theme="dark"] .highlight .sc { color: #abe338 } /* Literal.String.Char */ +html[data-theme="dark"] .highlight .dl { color: #abe338 } /* Literal.String.Delimiter */ +html[data-theme="dark"] .highlight .sd { color: #abe338 } /* Literal.String.Doc */ +html[data-theme="dark"] .highlight .s2 { color: #abe338 } /* Literal.String.Double */ +html[data-theme="dark"] .highlight .se { color: #abe338 } /* Literal.String.Escape */ +html[data-theme="dark"] .highlight .sh { color: #abe338 } /* Literal.String.Heredoc */ +html[data-theme="dark"] .highlight .si { color: #abe338 } /* Literal.String.Interpol */ +html[data-theme="dark"] .highlight .sx { color: #abe338 } /* Literal.String.Other */ +html[data-theme="dark"] .highlight .sr { color: #ffa07a } /* Literal.String.Regex */ +html[data-theme="dark"] .highlight .s1 { color: #abe338 } /* Literal.String.Single */ +html[data-theme="dark"] .highlight .ss { color: #00e0e0 } /* Literal.String.Symbol */ +html[data-theme="dark"] .highlight .bp { color: #ffd900 } /* Name.Builtin.Pseudo */ +html[data-theme="dark"] .highlight .fm { color: #00e0e0 } /* Name.Function.Magic */ +html[data-theme="dark"] .highlight .vc { color: #ffa07a } /* Name.Variable.Class */ +html[data-theme="dark"] .highlight .vg { color: #ffa07a } /* Name.Variable.Global */ +html[data-theme="dark"] .highlight .vi { color: #ffa07a } /* Name.Variable.Instance */ +html[data-theme="dark"] .highlight .vm { color: #ffd900 } /* Name.Variable.Magic */ +html[data-theme="dark"] .highlight .il { color: #ffd900 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/_static/scripts/bootstrap.js b/_static/scripts/bootstrap.js new file mode 100644 index 00000000..c8178deb --- /dev/null +++ b/_static/scripts/bootstrap.js @@ -0,0 +1,3 @@ +/*! For license information please see bootstrap.js.LICENSE.txt */ +(()=>{"use strict";var t={d:(e,i)=>{for(var n in i)t.o(i,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:i[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};t.r(e),t.d(e,{afterMain:()=>E,afterRead:()=>v,afterWrite:()=>C,applyStyles:()=>$,arrow:()=>J,auto:()=>a,basePlacements:()=>l,beforeMain:()=>y,beforeRead:()=>_,beforeWrite:()=>A,bottom:()=>s,clippingParents:()=>d,computeStyles:()=>it,createPopper:()=>Dt,createPopperBase:()=>St,createPopperLite:()=>$t,detectOverflow:()=>_t,end:()=>h,eventListeners:()=>st,flip:()=>bt,hide:()=>wt,left:()=>r,main:()=>w,modifierPhases:()=>O,offset:()=>Et,placements:()=>g,popper:()=>f,popperGenerator:()=>Lt,popperOffsets:()=>At,preventOverflow:()=>Tt,read:()=>b,reference:()=>p,right:()=>o,start:()=>c,top:()=>n,variationPlacements:()=>m,viewport:()=>u,write:()=>T});var i={};t.r(i),t.d(i,{Alert:()=>Oe,Button:()=>ke,Carousel:()=>li,Collapse:()=>Ei,Dropdown:()=>Ki,Modal:()=>Ln,Offcanvas:()=>Kn,Popover:()=>bs,ScrollSpy:()=>Ls,Tab:()=>Js,Toast:()=>po,Tooltip:()=>fs});var n="top",s="bottom",o="right",r="left",a="auto",l=[n,s,o,r],c="start",h="end",d="clippingParents",u="viewport",f="popper",p="reference",m=l.reduce((function(t,e){return t.concat([e+"-"+c,e+"-"+h])}),[]),g=[].concat(l,[a]).reduce((function(t,e){return t.concat([e,e+"-"+c,e+"-"+h])}),[]),_="beforeRead",b="read",v="afterRead",y="beforeMain",w="main",E="afterMain",A="beforeWrite",T="write",C="afterWrite",O=[_,b,v,y,w,E,A,T,C];function x(t){return t?(t.nodeName||"").toLowerCase():null}function k(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function L(t){return t instanceof k(t).Element||t instanceof Element}function S(t){return t instanceof k(t).HTMLElement||t instanceof HTMLElement}function D(t){return"undefined"!=typeof ShadowRoot&&(t instanceof k(t).ShadowRoot||t instanceof ShadowRoot)}const $={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];S(s)&&x(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});S(n)&&x(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function I(t){return t.split("-")[0]}var N=Math.max,P=Math.min,M=Math.round;function j(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function F(){return!/^((?!chrome|android).)*safari/i.test(j())}function H(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&S(t)&&(s=t.offsetWidth>0&&M(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&M(n.height)/t.offsetHeight||1);var r=(L(t)?k(t):window).visualViewport,a=!F()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function B(t){var e=H(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function W(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&D(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function z(t){return k(t).getComputedStyle(t)}function R(t){return["table","td","th"].indexOf(x(t))>=0}function q(t){return((L(t)?t.ownerDocument:t.document)||window.document).documentElement}function V(t){return"html"===x(t)?t:t.assignedSlot||t.parentNode||(D(t)?t.host:null)||q(t)}function Y(t){return S(t)&&"fixed"!==z(t).position?t.offsetParent:null}function K(t){for(var e=k(t),i=Y(t);i&&R(i)&&"static"===z(i).position;)i=Y(i);return i&&("html"===x(i)||"body"===x(i)&&"static"===z(i).position)?e:i||function(t){var e=/firefox/i.test(j());if(/Trident/i.test(j())&&S(t)&&"fixed"===z(t).position)return null;var i=V(t);for(D(i)&&(i=i.host);S(i)&&["html","body"].indexOf(x(i))<0;){var n=z(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Q(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function X(t,e,i){return N(t,P(e,i))}function U(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function G(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const J={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,a=t.name,c=t.options,h=i.elements.arrow,d=i.modifiersData.popperOffsets,u=I(i.placement),f=Q(u),p=[r,o].indexOf(u)>=0?"height":"width";if(h&&d){var m=function(t,e){return U("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:G(t,l))}(c.padding,i),g=B(h),_="y"===f?n:r,b="y"===f?s:o,v=i.rects.reference[p]+i.rects.reference[f]-d[f]-i.rects.popper[p],y=d[f]-i.rects.reference[f],w=K(h),E=w?"y"===f?w.clientHeight||0:w.clientWidth||0:0,A=v/2-y/2,T=m[_],C=E-g[p]-m[b],O=E/2-g[p]/2+A,x=X(T,O,C),k=f;i.modifiersData[a]=((e={})[k]=x,e.centerOffset=x-O,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&W(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Z(t){return t.split("-")[1]}var tt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function et(t){var e,i=t.popper,a=t.popperRect,l=t.placement,c=t.variation,d=t.offsets,u=t.position,f=t.gpuAcceleration,p=t.adaptive,m=t.roundOffsets,g=t.isFixed,_=d.x,b=void 0===_?0:_,v=d.y,y=void 0===v?0:v,w="function"==typeof m?m({x:b,y}):{x:b,y};b=w.x,y=w.y;var E=d.hasOwnProperty("x"),A=d.hasOwnProperty("y"),T=r,C=n,O=window;if(p){var x=K(i),L="clientHeight",S="clientWidth";x===k(i)&&"static"!==z(x=q(i)).position&&"absolute"===u&&(L="scrollHeight",S="scrollWidth"),(l===n||(l===r||l===o)&&c===h)&&(C=s,y-=(g&&x===O&&O.visualViewport?O.visualViewport.height:x[L])-a.height,y*=f?1:-1),l!==r&&(l!==n&&l!==s||c!==h)||(T=o,b-=(g&&x===O&&O.visualViewport?O.visualViewport.width:x[S])-a.width,b*=f?1:-1)}var D,$=Object.assign({position:u},p&&tt),I=!0===m?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:M(i*s)/s||0,y:M(n*s)/s||0}}({x:b,y},k(i)):{x:b,y};return b=I.x,y=I.y,f?Object.assign({},$,((D={})[C]=A?"0":"",D[T]=E?"0":"",D.transform=(O.devicePixelRatio||1)<=1?"translate("+b+"px, "+y+"px)":"translate3d("+b+"px, "+y+"px, 0)",D)):Object.assign({},$,((e={})[C]=A?y+"px":"",e[T]=E?b+"px":"",e.transform="",e))}const it={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:I(e.placement),variation:Z(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,et(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,et(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var nt={passive:!0};const st={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=k(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,nt)})),a&&l.addEventListener("resize",i.update,nt),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,nt)})),a&&l.removeEventListener("resize",i.update,nt)}},data:{}};var ot={left:"right",right:"left",bottom:"top",top:"bottom"};function rt(t){return t.replace(/left|right|bottom|top/g,(function(t){return ot[t]}))}var at={start:"end",end:"start"};function lt(t){return t.replace(/start|end/g,(function(t){return at[t]}))}function ct(t){var e=k(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ht(t){return H(q(t)).left+ct(t).scrollLeft}function dt(t){var e=z(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function ut(t){return["html","body","#document"].indexOf(x(t))>=0?t.ownerDocument.body:S(t)&&dt(t)?t:ut(V(t))}function ft(t,e){var i;void 0===e&&(e=[]);var n=ut(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=k(n),r=s?[o].concat(o.visualViewport||[],dt(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(ft(V(r)))}function pt(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function mt(t,e,i){return e===u?pt(function(t,e){var i=k(t),n=q(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=F();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+ht(t),y:l}}(t,i)):L(e)?function(t,e){var i=H(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):pt(function(t){var e,i=q(t),n=ct(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=N(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=N(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ht(t),l=-n.scrollTop;return"rtl"===z(s||i).direction&&(a+=N(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(q(t)))}function gt(t){var e,i=t.reference,a=t.element,l=t.placement,d=l?I(l):null,u=l?Z(l):null,f=i.x+i.width/2-a.width/2,p=i.y+i.height/2-a.height/2;switch(d){case n:e={x:f,y:i.y-a.height};break;case s:e={x:f,y:i.y+i.height};break;case o:e={x:i.x+i.width,y:p};break;case r:e={x:i.x-a.width,y:p};break;default:e={x:i.x,y:i.y}}var m=d?Q(d):null;if(null!=m){var g="y"===m?"height":"width";switch(u){case c:e[m]=e[m]-(i[g]/2-a[g]/2);break;case h:e[m]=e[m]+(i[g]/2-a[g]/2)}}return e}function _t(t,e){void 0===e&&(e={});var i=e,r=i.placement,a=void 0===r?t.placement:r,c=i.strategy,h=void 0===c?t.strategy:c,m=i.boundary,g=void 0===m?d:m,_=i.rootBoundary,b=void 0===_?u:_,v=i.elementContext,y=void 0===v?f:v,w=i.altBoundary,E=void 0!==w&&w,A=i.padding,T=void 0===A?0:A,C=U("number"!=typeof T?T:G(T,l)),O=y===f?p:f,k=t.rects.popper,D=t.elements[E?O:y],$=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=ft(V(t)),i=["absolute","fixed"].indexOf(z(t).position)>=0&&S(t)?K(t):t;return L(i)?e.filter((function(t){return L(t)&&W(t,i)&&"body"!==x(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=mt(t,i,n);return e.top=N(s.top,e.top),e.right=P(s.right,e.right),e.bottom=P(s.bottom,e.bottom),e.left=N(s.left,e.left),e}),mt(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(L(D)?D:D.contextElement||q(t.elements.popper),g,b,h),I=H(t.elements.reference),M=gt({reference:I,element:k,strategy:"absolute",placement:a}),j=pt(Object.assign({},k,M)),F=y===f?j:I,B={top:$.top-F.top+C.top,bottom:F.bottom-$.bottom+C.bottom,left:$.left-F.left+C.left,right:F.right-$.right+C.right},R=t.modifiersData.offset;if(y===f&&R){var Y=R[a];Object.keys(B).forEach((function(t){var e=[o,s].indexOf(t)>=0?1:-1,i=[n,s].indexOf(t)>=0?"y":"x";B[t]+=Y[i]*e}))}return B}const bt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,h=t.name;if(!e.modifiersData[h]._skip){for(var d=i.mainAxis,u=void 0===d||d,f=i.altAxis,p=void 0===f||f,_=i.fallbackPlacements,b=i.padding,v=i.boundary,y=i.rootBoundary,w=i.altBoundary,E=i.flipVariations,A=void 0===E||E,T=i.allowedAutoPlacements,C=e.options.placement,O=I(C),x=_||(O!==C&&A?function(t){if(I(t)===a)return[];var e=rt(t);return[lt(t),e,lt(e)]}(C):[rt(C)]),k=[C].concat(x).reduce((function(t,i){return t.concat(I(i)===a?function(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,c=i.allowedAutoPlacements,h=void 0===c?g:c,d=Z(n),u=d?a?m:m.filter((function(t){return Z(t)===d})):l,f=u.filter((function(t){return h.indexOf(t)>=0}));0===f.length&&(f=u);var p=f.reduce((function(e,i){return e[i]=_t(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[I(i)],e}),{});return Object.keys(p).sort((function(t,e){return p[t]-p[e]}))}(e,{placement:i,boundary:v,rootBoundary:y,padding:b,flipVariations:A,allowedAutoPlacements:T}):i)}),[]),L=e.rects.reference,S=e.rects.popper,D=new Map,$=!0,N=k[0],P=0;P=0,B=H?"width":"height",W=_t(e,{placement:M,boundary:v,rootBoundary:y,altBoundary:w,padding:b}),z=H?F?o:r:F?s:n;L[B]>S[B]&&(z=rt(z));var R=rt(z),q=[];if(u&&q.push(W[j]<=0),p&&q.push(W[z]<=0,W[R]<=0),q.every((function(t){return t}))){N=M,$=!1;break}D.set(M,q)}if($)for(var V=function(t){var e=k.find((function(e){var i=D.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return N=e,"break"},Y=A?3:1;Y>0&&"break"!==V(Y);Y--);e.placement!==N&&(e.modifiersData[h]._skip=!0,e.placement=N,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function vt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function yt(t){return[n,o,s,r].some((function(e){return t[e]>=0}))}const wt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=_t(e,{elementContext:"reference"}),a=_t(e,{altBoundary:!0}),l=vt(r,n),c=vt(a,s,o),h=yt(l),d=yt(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Et={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,s=t.name,a=i.offset,l=void 0===a?[0,0]:a,c=g.reduce((function(t,i){return t[i]=function(t,e,i){var s=I(t),a=[r,n].indexOf(s)>=0?-1:1,l="function"==typeof i?i(Object.assign({},e,{placement:t})):i,c=l[0],h=l[1];return c=c||0,h=(h||0)*a,[r,o].indexOf(s)>=0?{x:h,y:c}:{x:c,y:h}}(i,e.rects,l),t}),{}),h=c[e.placement],d=h.x,u=h.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=d,e.modifiersData.popperOffsets.y+=u),e.modifiersData[s]=c}},At={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=gt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},Tt={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,a=t.name,l=i.mainAxis,h=void 0===l||l,d=i.altAxis,u=void 0!==d&&d,f=i.boundary,p=i.rootBoundary,m=i.altBoundary,g=i.padding,_=i.tether,b=void 0===_||_,v=i.tetherOffset,y=void 0===v?0:v,w=_t(e,{boundary:f,rootBoundary:p,padding:g,altBoundary:m}),E=I(e.placement),A=Z(e.placement),T=!A,C=Q(E),O="x"===C?"y":"x",x=e.modifiersData.popperOffsets,k=e.rects.reference,L=e.rects.popper,S="function"==typeof y?y(Object.assign({},e.rects,{placement:e.placement})):y,D="number"==typeof S?{mainAxis:S,altAxis:S}:Object.assign({mainAxis:0,altAxis:0},S),$=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,M={x:0,y:0};if(x){if(h){var j,F="y"===C?n:r,H="y"===C?s:o,W="y"===C?"height":"width",z=x[C],R=z+w[F],q=z-w[H],V=b?-L[W]/2:0,Y=A===c?k[W]:L[W],U=A===c?-L[W]:-k[W],G=e.elements.arrow,J=b&&G?B(G):{width:0,height:0},tt=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},et=tt[F],it=tt[H],nt=X(0,k[W],J[W]),st=T?k[W]/2-V-nt-et-D.mainAxis:Y-nt-et-D.mainAxis,ot=T?-k[W]/2+V+nt+it+D.mainAxis:U+nt+it+D.mainAxis,rt=e.elements.arrow&&K(e.elements.arrow),at=rt?"y"===C?rt.clientTop||0:rt.clientLeft||0:0,lt=null!=(j=null==$?void 0:$[C])?j:0,ct=z+ot-lt,ht=X(b?P(R,z+st-lt-at):R,z,b?N(q,ct):q);x[C]=ht,M[C]=ht-z}if(u){var dt,ut="x"===C?n:r,ft="x"===C?s:o,pt=x[O],mt="y"===O?"height":"width",gt=pt+w[ut],bt=pt-w[ft],vt=-1!==[n,r].indexOf(E),yt=null!=(dt=null==$?void 0:$[O])?dt:0,wt=vt?gt:pt-k[mt]-L[mt]-yt+D.altAxis,Et=vt?pt+k[mt]+L[mt]-yt-D.altAxis:bt,At=b&&vt?function(t,e,i){var n=X(t,e,i);return n>i?i:n}(wt,pt,Et):X(b?wt:gt,pt,b?Et:bt);x[O]=At,M[O]=At-pt}e.modifiersData[a]=M}},requiresIfExists:["offset"]};function Ct(t,e,i){void 0===i&&(i=!1);var n,s,o=S(e),r=S(e)&&function(t){var e=t.getBoundingClientRect(),i=M(e.width)/t.offsetWidth||1,n=M(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=q(e),l=H(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==x(e)||dt(a))&&(c=(n=e)!==k(n)&&S(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:ct(n)),S(e)?((h=H(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=ht(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function Ot(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var xt={placement:"bottom",modifiers:[],strategy:"absolute"};function kt(){for(var t=arguments.length,e=new Array(t),i=0;iIt.has(t)&&It.get(t).get(e)||null,remove(t,e){if(!It.has(t))return;const i=It.get(t);i.delete(e),0===i.size&&It.delete(t)}},Pt="transitionend",Mt=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),jt=t=>{t.dispatchEvent(new Event(Pt))},Ft=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),Ht=t=>Ft(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(Mt(t)):null,Bt=t=>{if(!Ft(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},Wt=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),zt=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?zt(t.parentNode):null},Rt=()=>{},qt=t=>{t.offsetHeight},Vt=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,Yt=[],Kt=()=>"rtl"===document.documentElement.dir,Qt=t=>{var e;e=()=>{const e=Vt();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(Yt.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of Yt)t()})),Yt.push(e)):e()},Xt=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,Ut=(t,e,i=!0)=>{if(!i)return void Xt(t);const n=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let s=!1;const o=({target:i})=>{i===e&&(s=!0,e.removeEventListener(Pt,o),Xt(t))};e.addEventListener(Pt,o),setTimeout((()=>{s||jt(e)}),n)},Gt=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},Jt=/[^.]*(?=\..*)\.|.*/,Zt=/\..*/,te=/::\d+$/,ee={};let ie=1;const ne={mouseenter:"mouseover",mouseleave:"mouseout"},se=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function oe(t,e){return e&&`${e}::${ie++}`||t.uidEvent||ie++}function re(t){const e=oe(t);return t.uidEvent=e,ee[e]=ee[e]||{},ee[e]}function ae(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function le(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=ue(t);return se.has(o)||(o=t),[n,s,o]}function ce(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=le(e,i,n);if(e in ne){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=re(t),c=l[a]||(l[a]={}),h=ae(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=oe(r,e.replace(Jt,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return pe(s,{delegateTarget:r}),n.oneOff&&fe.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return pe(n,{delegateTarget:t}),i.oneOff&&fe.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function he(t,e,i,n,s){const o=ae(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function de(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&he(t,e,i,r.callable,r.delegationSelector)}function ue(t){return t=t.replace(Zt,""),ne[t]||t}const fe={on(t,e,i,n){ce(t,e,i,n,!1)},one(t,e,i,n){ce(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=le(e,i,n),a=r!==e,l=re(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))de(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(te,"");a&&!e.includes(s)||he(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;he(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=Vt();let s=null,o=!0,r=!0,a=!1;e!==ue(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=pe(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function pe(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function me(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function ge(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const _e={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${ge(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${ge(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=me(t.dataset[n])}return e},getDataAttribute:(t,e)=>me(t.getAttribute(`data-bs-${ge(e)}`))};class be{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=Ft(e)?_e.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...Ft(e)?_e.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],o=Ft(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(o))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${o}" but expected type "${s}".`)}var i}}class ve extends be{constructor(t,e){super(),(t=Ht(t))&&(this._element=t,this._config=this._getConfig(e),Nt.set(this._element,this.constructor.DATA_KEY,this))}dispose(){Nt.remove(this._element,this.constructor.DATA_KEY),fe.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){Ut(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return Nt.get(Ht(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.3"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const ye=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>Mt(t))).join(","):null},we={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!Wt(t)&&Bt(t)))},getSelectorFromElement(t){const e=ye(t);return e&&we.findOne(e)?e:null},getElementFromSelector(t){const e=ye(t);return e?we.findOne(e):null},getMultipleElementsFromSelector(t){const e=ye(t);return e?we.find(e):[]}},Ee=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;fe.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),Wt(this))return;const s=we.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},Ae=".bs.alert",Te=`close${Ae}`,Ce=`closed${Ae}`;class Oe extends ve{static get NAME(){return"alert"}close(){if(fe.trigger(this._element,Te).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),fe.trigger(this._element,Ce),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Oe.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}Ee(Oe,"close"),Qt(Oe);const xe='[data-bs-toggle="button"]';class ke extends ve{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=ke.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}fe.on(document,"click.bs.button.data-api",xe,(t=>{t.preventDefault();const e=t.target.closest(xe);ke.getOrCreateInstance(e).toggle()})),Qt(ke);const Le=".bs.swipe",Se=`touchstart${Le}`,De=`touchmove${Le}`,$e=`touchend${Le}`,Ie=`pointerdown${Le}`,Ne=`pointerup${Le}`,Pe={endCallback:null,leftCallback:null,rightCallback:null},Me={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class je extends be{constructor(t,e){super(),this._element=t,t&&je.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Pe}static get DefaultType(){return Me}static get NAME(){return"swipe"}dispose(){fe.off(this._element,Le)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),Xt(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&Xt(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(fe.on(this._element,Ie,(t=>this._start(t))),fe.on(this._element,Ne,(t=>this._end(t))),this._element.classList.add("pointer-event")):(fe.on(this._element,Se,(t=>this._start(t))),fe.on(this._element,De,(t=>this._move(t))),fe.on(this._element,$e,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const Fe=".bs.carousel",He=".data-api",Be="ArrowLeft",We="ArrowRight",ze="next",Re="prev",qe="left",Ve="right",Ye=`slide${Fe}`,Ke=`slid${Fe}`,Qe=`keydown${Fe}`,Xe=`mouseenter${Fe}`,Ue=`mouseleave${Fe}`,Ge=`dragstart${Fe}`,Je=`load${Fe}${He}`,Ze=`click${Fe}${He}`,ti="carousel",ei="active",ii=".active",ni=".carousel-item",si=ii+ni,oi={[Be]:Ve,[We]:qe},ri={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},ai={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class li extends ve{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=we.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===ti&&this.cycle()}static get Default(){return ri}static get DefaultType(){return ai}static get NAME(){return"carousel"}next(){this._slide(ze)}nextWhenVisible(){!document.hidden&&Bt(this._element)&&this.next()}prev(){this._slide(Re)}pause(){this._isSliding&&jt(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?fe.one(this._element,Ke,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void fe.one(this._element,Ke,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?ze:Re;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&fe.on(this._element,Qe,(t=>this._keydown(t))),"hover"===this._config.pause&&(fe.on(this._element,Xe,(()=>this.pause())),fe.on(this._element,Ue,(()=>this._maybeEnableCycle()))),this._config.touch&&je.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of we.find(".carousel-item img",this._element))fe.on(t,Ge,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(qe)),rightCallback:()=>this._slide(this._directionToOrder(Ve)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new je(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=oi[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=we.findOne(ii,this._indicatorsElement);e.classList.remove(ei),e.removeAttribute("aria-current");const i=we.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(ei),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===ze,s=e||Gt(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>fe.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(Ye).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),qt(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(ei),i.classList.remove(ei,c,l),this._isSliding=!1,r(Ke)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return we.findOne(si,this._element)}_getItems(){return we.find(ni,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return Kt()?t===qe?Re:ze:t===qe?ze:Re}_orderToDirection(t){return Kt()?t===Re?qe:Ve:t===Re?Ve:qe}static jQueryInterface(t){return this.each((function(){const e=li.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}fe.on(document,Ze,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=we.getElementFromSelector(this);if(!e||!e.classList.contains(ti))return;t.preventDefault();const i=li.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===_e.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),fe.on(window,Je,(()=>{const t=we.find('[data-bs-ride="carousel"]');for(const e of t)li.getOrCreateInstance(e)})),Qt(li);const ci=".bs.collapse",hi=`show${ci}`,di=`shown${ci}`,ui=`hide${ci}`,fi=`hidden${ci}`,pi=`click${ci}.data-api`,mi="show",gi="collapse",_i="collapsing",bi=`:scope .${gi} .${gi}`,vi='[data-bs-toggle="collapse"]',yi={parent:null,toggle:!0},wi={parent:"(null|element)",toggle:"boolean"};class Ei extends ve{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=we.find(vi);for(const t of i){const e=we.getSelectorFromElement(t),i=we.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return yi}static get DefaultType(){return wi}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Ei.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(fe.trigger(this._element,hi).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(gi),this._element.classList.add(_i),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(_i),this._element.classList.add(gi,mi),this._element.style[e]="",fe.trigger(this._element,di)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(fe.trigger(this._element,ui).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,qt(this._element),this._element.classList.add(_i),this._element.classList.remove(gi,mi);for(const t of this._triggerArray){const e=we.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(_i),this._element.classList.add(gi),fe.trigger(this._element,fi)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(mi)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=Ht(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(vi);for(const e of t){const t=we.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=we.find(bi,this._config.parent);return we.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Ei.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}fe.on(document,pi,vi,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of we.getMultipleElementsFromSelector(this))Ei.getOrCreateInstance(t,{toggle:!1}).toggle()})),Qt(Ei);const Ai="dropdown",Ti=".bs.dropdown",Ci=".data-api",Oi="ArrowUp",xi="ArrowDown",ki=`hide${Ti}`,Li=`hidden${Ti}`,Si=`show${Ti}`,Di=`shown${Ti}`,$i=`click${Ti}${Ci}`,Ii=`keydown${Ti}${Ci}`,Ni=`keyup${Ti}${Ci}`,Pi="show",Mi='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',ji=`${Mi}.${Pi}`,Fi=".dropdown-menu",Hi=Kt()?"top-end":"top-start",Bi=Kt()?"top-start":"top-end",Wi=Kt()?"bottom-end":"bottom-start",zi=Kt()?"bottom-start":"bottom-end",Ri=Kt()?"left-start":"right-start",qi=Kt()?"right-start":"left-start",Vi={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},Yi={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class Ki extends ve{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=we.next(this._element,Fi)[0]||we.prev(this._element,Fi)[0]||we.findOne(Fi,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return Vi}static get DefaultType(){return Yi}static get NAME(){return Ai}toggle(){return this._isShown()?this.hide():this.show()}show(){if(Wt(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!fe.trigger(this._element,Si,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))fe.on(t,"mouseover",Rt);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Pi),this._element.classList.add(Pi),fe.trigger(this._element,Di,t)}}hide(){if(Wt(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!fe.trigger(this._element,ki,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.off(t,"mouseover",Rt);this._popper&&this._popper.destroy(),this._menu.classList.remove(Pi),this._element.classList.remove(Pi),this._element.setAttribute("aria-expanded","false"),_e.removeDataAttribute(this._menu,"popper"),fe.trigger(this._element,Li,t)}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!Ft(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Ai.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===e)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let t=this._element;"parent"===this._config.reference?t=this._parent:Ft(this._config.reference)?t=Ht(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const i=this._getPopperConfig();this._popper=Dt(t,this._menu,i)}_isShown(){return this._menu.classList.contains(Pi)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return Ri;if(t.classList.contains("dropstart"))return qi;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?Bi:Hi:e?zi:Wi}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(_e.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...Xt(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=we.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>Bt(t)));i.length&&Gt(i,e,t===xi,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=Ki.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=we.find(ji);for(const i of e){const e=Ki.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Oi,xi].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Mi)?this:we.prev(this,Mi)[0]||we.next(this,Mi)[0]||we.findOne(Mi,t.delegateTarget.parentNode),o=Ki.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}fe.on(document,Ii,Mi,Ki.dataApiKeydownHandler),fe.on(document,Ii,Fi,Ki.dataApiKeydownHandler),fe.on(document,$i,Ki.clearMenus),fe.on(document,Ni,Ki.clearMenus),fe.on(document,$i,Mi,(function(t){t.preventDefault(),Ki.getOrCreateInstance(this).toggle()})),Qt(Ki);const Qi="backdrop",Xi="show",Ui=`mousedown.bs.${Qi}`,Gi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Ji={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Zi extends be{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Gi}static get DefaultType(){return Ji}static get NAME(){return Qi}show(t){if(!this._config.isVisible)return void Xt(t);this._append();const e=this._getElement();this._config.isAnimated&&qt(e),e.classList.add(Xi),this._emulateAnimation((()=>{Xt(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Xi),this._emulateAnimation((()=>{this.dispose(),Xt(t)}))):Xt(t)}dispose(){this._isAppended&&(fe.off(this._element,Ui),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=Ht(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),fe.on(t,Ui,(()=>{Xt(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){Ut(t,this._getElement(),this._config.isAnimated)}}const tn=".bs.focustrap",en=`focusin${tn}`,nn=`keydown.tab${tn}`,sn="backward",on={autofocus:!0,trapElement:null},rn={autofocus:"boolean",trapElement:"element"};class an extends be{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return on}static get DefaultType(){return rn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),fe.off(document,tn),fe.on(document,en,(t=>this._handleFocusin(t))),fe.on(document,nn,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,fe.off(document,tn))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=we.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===sn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?sn:"forward")}}const ln=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",cn=".sticky-top",hn="padding-right",dn="margin-right";class un{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,hn,(e=>e+t)),this._setElementAttributes(ln,hn,(e=>e+t)),this._setElementAttributes(cn,dn,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,hn),this._resetElementAttributes(ln,hn),this._resetElementAttributes(cn,dn)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&_e.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=_e.getDataAttribute(t,e);null!==i?(_e.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(Ft(t))e(t);else for(const i of we.find(t,this._element))e(i)}}const fn=".bs.modal",pn=`hide${fn}`,mn=`hidePrevented${fn}`,gn=`hidden${fn}`,_n=`show${fn}`,bn=`shown${fn}`,vn=`resize${fn}`,yn=`click.dismiss${fn}`,wn=`mousedown.dismiss${fn}`,En=`keydown.dismiss${fn}`,An=`click${fn}.data-api`,Tn="modal-open",Cn="show",On="modal-static",xn={backdrop:!0,focus:!0,keyboard:!0},kn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Ln extends ve{constructor(t,e){super(t,e),this._dialog=we.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new un,this._addEventListeners()}static get Default(){return xn}static get DefaultType(){return kn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||fe.trigger(this._element,_n,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Tn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(fe.trigger(this._element,pn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Cn),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){fe.off(window,fn),fe.off(this._dialog,fn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Zi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new an({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=we.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),qt(this._element),this._element.classList.add(Cn),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,fe.trigger(this._element,bn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){fe.on(this._element,En,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),fe.on(window,vn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),fe.on(this._element,wn,(t=>{fe.one(this._element,yn,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Tn),this._resetAdjustments(),this._scrollBar.reset(),fe.trigger(this._element,gn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(fe.trigger(this._element,mn).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(On)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(On),this._queueCallback((()=>{this._element.classList.remove(On),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=Kt()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=Kt()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Ln.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}fe.on(document,An,'[data-bs-toggle="modal"]',(function(t){const e=we.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),fe.one(e,_n,(t=>{t.defaultPrevented||fe.one(e,gn,(()=>{Bt(this)&&this.focus()}))}));const i=we.findOne(".modal.show");i&&Ln.getInstance(i).hide(),Ln.getOrCreateInstance(e).toggle(this)})),Ee(Ln),Qt(Ln);const Sn=".bs.offcanvas",Dn=".data-api",$n=`load${Sn}${Dn}`,In="show",Nn="showing",Pn="hiding",Mn=".offcanvas.show",jn=`show${Sn}`,Fn=`shown${Sn}`,Hn=`hide${Sn}`,Bn=`hidePrevented${Sn}`,Wn=`hidden${Sn}`,zn=`resize${Sn}`,Rn=`click${Sn}${Dn}`,qn=`keydown.dismiss${Sn}`,Vn={backdrop:!0,keyboard:!0,scroll:!1},Yn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Kn extends ve{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Vn}static get DefaultType(){return Yn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||fe.trigger(this._element,jn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new un).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Nn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(In),this._element.classList.remove(Nn),fe.trigger(this._element,Fn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(fe.trigger(this._element,Hn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(Pn),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(In,Pn),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new un).reset(),fe.trigger(this._element,Wn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Zi({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():fe.trigger(this._element,Bn)}:null})}_initializeFocusTrap(){return new an({trapElement:this._element})}_addEventListeners(){fe.on(this._element,qn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():fe.trigger(this._element,Bn))}))}static jQueryInterface(t){return this.each((function(){const e=Kn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}fe.on(document,Rn,'[data-bs-toggle="offcanvas"]',(function(t){const e=we.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),Wt(this))return;fe.one(e,Wn,(()=>{Bt(this)&&this.focus()}));const i=we.findOne(Mn);i&&i!==e&&Kn.getInstance(i).hide(),Kn.getOrCreateInstance(e).toggle(this)})),fe.on(window,$n,(()=>{for(const t of we.find(Mn))Kn.getOrCreateInstance(t).show()})),fe.on(window,zn,(()=>{for(const t of we.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Kn.getOrCreateInstance(t).hide()})),Ee(Kn),Qt(Kn);const Qn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Xn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Un=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Gn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Xn.has(i)||Boolean(Un.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Jn={allowList:Qn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Zn={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},ts={entry:"(string|element|function|null)",selector:"(string|element)"};class es extends be{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Jn}static get DefaultType(){return Zn}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},ts)}_setContent(t,e,i){const n=we.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?Ft(e)?this._putElementInTemplate(Ht(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Gn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return Xt(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const is=new Set(["sanitize","allowList","sanitizeFn"]),ns="fade",ss="show",os=".tooltip-inner",rs=".modal",as="hide.bs.modal",ls="hover",cs="focus",hs={AUTO:"auto",TOP:"top",RIGHT:Kt()?"left":"right",BOTTOM:"bottom",LEFT:Kt()?"right":"left"},ds={allowList:Qn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},us={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class fs extends ve{constructor(t,i){if(void 0===e)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,i),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return ds}static get DefaultType(){return us}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),fe.off(this._element.closest(rs),as,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=fe.trigger(this._element,this.constructor.eventName("show")),e=(zt(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),fe.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(ss),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.on(t,"mouseover",Rt);this._queueCallback((()=>{fe.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!fe.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(ss),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.off(t,"mouseover",Rt);this._activeTrigger.click=!1,this._activeTrigger[cs]=!1,this._activeTrigger[ls]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),fe.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ns,ss),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ns),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new es({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[os]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ns)}_isShown(){return this.tip&&this.tip.classList.contains(ss)}_createPopper(t){const e=Xt(this._config.placement,[this,t,this._element]),i=hs[e.toUpperCase()];return Dt(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return Xt(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...Xt(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)fe.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ls?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ls?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");fe.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?cs:ls]=!0,e._enter()})),fe.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?cs:ls]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},fe.on(this._element.closest(rs),as,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=_e.getDataAttributes(this._element);for(const t of Object.keys(e))is.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:Ht(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=fs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}Qt(fs);const ps=".popover-header",ms=".popover-body",gs={...fs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},_s={...fs.DefaultType,content:"(null|string|element|function)"};class bs extends fs{static get Default(){return gs}static get DefaultType(){return _s}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[ps]:this._getTitle(),[ms]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=bs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}Qt(bs);const vs=".bs.scrollspy",ys=`activate${vs}`,ws=`click${vs}`,Es=`load${vs}.data-api`,As="active",Ts="[href]",Cs=".nav-link",Os=`${Cs}, .nav-item > ${Cs}, .list-group-item`,xs={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},ks={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ls extends ve{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return xs}static get DefaultType(){return ks}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=Ht(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(fe.off(this._config.target,ws),fe.on(this._config.target,ws,Ts,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=we.find(Ts,this._config.target);for(const e of t){if(!e.hash||Wt(e))continue;const t=we.findOne(decodeURI(e.hash),this._element);Bt(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(As),this._activateParents(t),fe.trigger(this._element,ys,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))we.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(As);else for(const e of we.parents(t,".nav, .list-group"))for(const t of we.prev(e,Os))t.classList.add(As)}_clearActiveClass(t){t.classList.remove(As);const e=we.find(`${Ts}.${As}`,t);for(const t of e)t.classList.remove(As)}static jQueryInterface(t){return this.each((function(){const e=Ls.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}fe.on(window,Es,(()=>{for(const t of we.find('[data-bs-spy="scroll"]'))Ls.getOrCreateInstance(t)})),Qt(Ls);const Ss=".bs.tab",Ds=`hide${Ss}`,$s=`hidden${Ss}`,Is=`show${Ss}`,Ns=`shown${Ss}`,Ps=`click${Ss}`,Ms=`keydown${Ss}`,js=`load${Ss}`,Fs="ArrowLeft",Hs="ArrowRight",Bs="ArrowUp",Ws="ArrowDown",zs="Home",Rs="End",qs="active",Vs="fade",Ys="show",Ks=".dropdown-toggle",Qs=`:not(${Ks})`,Xs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Us=`.nav-link${Qs}, .list-group-item${Qs}, [role="tab"]${Qs}, ${Xs}`,Gs=`.${qs}[data-bs-toggle="tab"], .${qs}[data-bs-toggle="pill"], .${qs}[data-bs-toggle="list"]`;class Js extends ve{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),fe.on(this._element,Ms,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?fe.trigger(e,Ds,{relatedTarget:t}):null;fe.trigger(t,Is,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(qs),this._activate(we.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),fe.trigger(t,Ns,{relatedTarget:e})):t.classList.add(Ys)}),t,t.classList.contains(Vs)))}_deactivate(t,e){t&&(t.classList.remove(qs),t.blur(),this._deactivate(we.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),fe.trigger(t,$s,{relatedTarget:e})):t.classList.remove(Ys)}),t,t.classList.contains(Vs)))}_keydown(t){if(![Fs,Hs,Bs,Ws,zs,Rs].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!Wt(t)));let i;if([zs,Rs].includes(t.key))i=e[t.key===zs?0:e.length-1];else{const n=[Hs,Ws].includes(t.key);i=Gt(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Js.getOrCreateInstance(i).show())}_getChildren(){return we.find(Us,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=we.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=we.findOne(t,i);s&&s.classList.toggle(n,e)};n(Ks,qs),n(".dropdown-menu",Ys),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(qs)}_getInnerElement(t){return t.matches(Us)?t:we.findOne(Us,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Js.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}fe.on(document,Ps,Xs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),Wt(this)||Js.getOrCreateInstance(this).show()})),fe.on(window,js,(()=>{for(const t of we.find(Gs))Js.getOrCreateInstance(t)})),Qt(Js);const Zs=".bs.toast",to=`mouseover${Zs}`,eo=`mouseout${Zs}`,io=`focusin${Zs}`,no=`focusout${Zs}`,so=`hide${Zs}`,oo=`hidden${Zs}`,ro=`show${Zs}`,ao=`shown${Zs}`,lo="hide",co="show",ho="showing",uo={animation:"boolean",autohide:"boolean",delay:"number"},fo={animation:!0,autohide:!0,delay:5e3};class po extends ve{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return fo}static get DefaultType(){return uo}static get NAME(){return"toast"}show(){fe.trigger(this._element,ro).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(lo),qt(this._element),this._element.classList.add(co,ho),this._queueCallback((()=>{this._element.classList.remove(ho),fe.trigger(this._element,ao),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(fe.trigger(this._element,so).defaultPrevented||(this._element.classList.add(ho),this._queueCallback((()=>{this._element.classList.add(lo),this._element.classList.remove(ho,co),fe.trigger(this._element,oo)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(co),super.dispose()}isShown(){return this._element.classList.contains(co)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){fe.on(this._element,to,(t=>this._onInteraction(t,!0))),fe.on(this._element,eo,(t=>this._onInteraction(t,!1))),fe.on(this._element,io,(t=>this._onInteraction(t,!0))),fe.on(this._element,no,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=po.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}function mo(t){"loading"!=document.readyState?t():document.addEventListener("DOMContentLoaded",t)}Ee(po),Qt(po),mo((function(){[].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')).map((function(t){return new fs(t,{delay:{show:500,hide:100}})}))})),mo((function(){document.getElementById("pst-back-to-top").addEventListener("click",(function(){document.body.scrollTop=0,document.documentElement.scrollTop=0}))})),mo((function(){var t=document.getElementById("pst-back-to-top"),e=document.getElementsByClassName("bd-header")[0].getBoundingClientRect();window.addEventListener("scroll",(function(){this.oldScroll>this.scrollY&&this.scrollY>e.bottom?t.style.display="block":t.style.display="none",this.oldScroll=this.scrollY}))})),window.bootstrap=i})(); +//# sourceMappingURL=bootstrap.js.map \ No newline at end of file diff --git a/_static/scripts/bootstrap.js.LICENSE.txt b/_static/scripts/bootstrap.js.LICENSE.txt new file mode 100644 index 00000000..28755c2c --- /dev/null +++ b/_static/scripts/bootstrap.js.LICENSE.txt @@ -0,0 +1,5 @@ +/*! + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ diff --git a/_static/scripts/bootstrap.js.map b/_static/scripts/bootstrap.js.map new file mode 100644 index 00000000..e9e81589 --- /dev/null +++ b/_static/scripts/bootstrap.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scripts/bootstrap.js","mappings":"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","sources":["webpack://pydata_sphinx_theme/webpack/bootstrap","webpack://pydata_sphinx_theme/webpack/runtime/define property getters","webpack://pydata_sphinx_theme/webpack/runtime/hasOwnProperty shorthand","webpack://pydata_sphinx_theme/webpack/runtime/make namespace object","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/enums.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/instanceOf.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/applyStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getBasePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/math.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/userAgent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/contains.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isTableElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getParentNode.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/within.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergePaddingObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getFreshSideObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/expandToHashMap.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/arrow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getVariation.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/computeStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/eventListeners.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/rectToClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/detectOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/flip.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/hide.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/offset.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/popperOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/preventOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getAltAxis.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/orderModifiers.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/createPopper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/debounce.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergeByName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper-lite.js","webpack://pydata_sphinx_theme/./node_modules/bootstrap/dist/js/bootstrap.esm.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/mixin.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/bootstrap.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export var top = 'top';\nexport var bottom = 'bottom';\nexport var right = 'right';\nexport var left = 'left';\nexport var auto = 'auto';\nexport var basePlacements = [top, bottom, right, left];\nexport var start = 'start';\nexport var end = 'end';\nexport var clippingParents = 'clippingParents';\nexport var viewport = 'viewport';\nexport var popper = 'popper';\nexport var reference = 'reference';\nexport var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n}, []);\nexport var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n}, []); // modifiers that need to read the DOM\n\nexport var beforeRead = 'beforeRead';\nexport var read = 'read';\nexport var afterRead = 'afterRead'; // pure-logic modifiers\n\nexport var beforeMain = 'beforeMain';\nexport var main = 'main';\nexport var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\nexport var beforeWrite = 'beforeWrite';\nexport var write = 'write';\nexport var afterWrite = 'afterWrite';\nexport var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];","export default function getNodeName(element) {\n return element ? (element.nodeName || '').toLowerCase() : null;\n}","export default function getWindow(node) {\n if (node == null) {\n return window;\n }\n\n if (node.toString() !== '[object Window]') {\n var ownerDocument = node.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView || window : window;\n }\n\n return node;\n}","import getWindow from \"./getWindow.js\";\n\nfunction isElement(node) {\n var OwnElement = getWindow(node).Element;\n return node instanceof OwnElement || node instanceof Element;\n}\n\nfunction isHTMLElement(node) {\n var OwnElement = getWindow(node).HTMLElement;\n return node instanceof OwnElement || node instanceof HTMLElement;\n}\n\nfunction isShadowRoot(node) {\n // IE 11 has no ShadowRoot\n if (typeof ShadowRoot === 'undefined') {\n return false;\n }\n\n var OwnElement = getWindow(node).ShadowRoot;\n return node instanceof OwnElement || node instanceof ShadowRoot;\n}\n\nexport { isElement, isHTMLElement, isShadowRoot };","import getNodeName from \"../dom-utils/getNodeName.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // This modifier takes the styles prepared by the `computeStyles` modifier\n// and applies them to the HTMLElements such as popper and arrow\n\nfunction applyStyles(_ref) {\n var state = _ref.state;\n Object.keys(state.elements).forEach(function (name) {\n var style = state.styles[name] || {};\n var attributes = state.attributes[name] || {};\n var element = state.elements[name]; // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n } // Flow doesn't support to extend this property, but it's the most\n // effective way to apply styles to an HTMLElement\n // $FlowFixMe[cannot-write]\n\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (name) {\n var value = attributes[name];\n\n if (value === false) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value === true ? '' : value);\n }\n });\n });\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state;\n var initialStyles = {\n popper: {\n position: state.options.strategy,\n left: '0',\n top: '0',\n margin: '0'\n },\n arrow: {\n position: 'absolute'\n },\n reference: {}\n };\n Object.assign(state.elements.popper.style, initialStyles.popper);\n state.styles = initialStyles;\n\n if (state.elements.arrow) {\n Object.assign(state.elements.arrow.style, initialStyles.arrow);\n }\n\n return function () {\n Object.keys(state.elements).forEach(function (name) {\n var element = state.elements[name];\n var attributes = state.attributes[name] || {};\n var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n var style = styleProperties.reduce(function (style, property) {\n style[property] = '';\n return style;\n }, {}); // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n }\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (attribute) {\n element.removeAttribute(attribute);\n });\n });\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'applyStyles',\n enabled: true,\n phase: 'write',\n fn: applyStyles,\n effect: effect,\n requires: ['computeStyles']\n};","import { auto } from \"../enums.js\";\nexport default function getBasePlacement(placement) {\n return placement.split('-')[0];\n}","export var max = Math.max;\nexport var min = Math.min;\nexport var round = Math.round;","export default function getUAString() {\n var uaData = navigator.userAgentData;\n\n if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n return uaData.brands.map(function (item) {\n return item.brand + \"/\" + item.version;\n }).join(' ');\n }\n\n return navigator.userAgent;\n}","import getUAString from \"../utils/userAgent.js\";\nexport default function isLayoutViewport() {\n return !/^((?!chrome|android).)*safari/i.test(getUAString());\n}","import { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport { round } from \"../utils/math.js\";\nimport getWindow from \"./getWindow.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n if (includeScale === void 0) {\n includeScale = false;\n }\n\n if (isFixedStrategy === void 0) {\n isFixedStrategy = false;\n }\n\n var clientRect = element.getBoundingClientRect();\n var scaleX = 1;\n var scaleY = 1;\n\n if (includeScale && isHTMLElement(element)) {\n scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n }\n\n var _ref = isElement(element) ? getWindow(element) : window,\n visualViewport = _ref.visualViewport;\n\n var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n var width = clientRect.width / scaleX;\n var height = clientRect.height / scaleY;\n return {\n width: width,\n height: height,\n top: y,\n right: x + width,\n bottom: y + height,\n left: x,\n x: x,\n y: y\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\"; // Returns the layout rect of an element relative to its offsetParent. Layout\n// means it doesn't take into account transforms.\n\nexport default function getLayoutRect(element) {\n var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n var width = element.offsetWidth;\n var height = element.offsetHeight;\n\n if (Math.abs(clientRect.width - width) <= 1) {\n width = clientRect.width;\n }\n\n if (Math.abs(clientRect.height - height) <= 1) {\n height = clientRect.height;\n }\n\n return {\n x: element.offsetLeft,\n y: element.offsetTop,\n width: width,\n height: height\n };\n}","import { isShadowRoot } from \"./instanceOf.js\";\nexport default function contains(parent, child) {\n var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n if (parent.contains(child)) {\n return true;\n } // then fallback to custom implementation with Shadow DOM support\n else if (rootNode && isShadowRoot(rootNode)) {\n var next = child;\n\n do {\n if (next && parent.isSameNode(next)) {\n return true;\n } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n next = next.parentNode || next.host;\n } while (next);\n } // Give up, the result is false\n\n\n return false;\n}","import getWindow from \"./getWindow.js\";\nexport default function getComputedStyle(element) {\n return getWindow(element).getComputedStyle(element);\n}","import getNodeName from \"./getNodeName.js\";\nexport default function isTableElement(element) {\n return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n}","import { isElement } from \"./instanceOf.js\";\nexport default function getDocumentElement(element) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n element.document) || window.document).documentElement;\n}","import getNodeName from \"./getNodeName.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport { isShadowRoot } from \"./instanceOf.js\";\nexport default function getParentNode(element) {\n if (getNodeName(element) === 'html') {\n return element;\n }\n\n return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n // $FlowFixMe[incompatible-return]\n // $FlowFixMe[prop-missing]\n element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n element.parentNode || ( // DOM Element detected\n isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n getDocumentElement(element) // fallback\n\n );\n}","import getWindow from \"./getWindow.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isHTMLElement, isShadowRoot } from \"./instanceOf.js\";\nimport isTableElement from \"./isTableElement.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getUAString from \"../utils/userAgent.js\";\n\nfunction getTrueOffsetParent(element) {\n if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n getComputedStyle(element).position === 'fixed') {\n return null;\n }\n\n return element.offsetParent;\n} // `.offsetParent` reports `null` for fixed elements, while absolute elements\n// return the containing block\n\n\nfunction getContainingBlock(element) {\n var isFirefox = /firefox/i.test(getUAString());\n var isIE = /Trident/i.test(getUAString());\n\n if (isIE && isHTMLElement(element)) {\n // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n var elementCss = getComputedStyle(element);\n\n if (elementCss.position === 'fixed') {\n return null;\n }\n }\n\n var currentNode = getParentNode(element);\n\n if (isShadowRoot(currentNode)) {\n currentNode = currentNode.host;\n }\n\n while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n // create a containing block.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n return currentNode;\n } else {\n currentNode = currentNode.parentNode;\n }\n }\n\n return null;\n} // Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\n\n\nexport default function getOffsetParent(element) {\n var window = getWindow(element);\n var offsetParent = getTrueOffsetParent(element);\n\n while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n offsetParent = getTrueOffsetParent(offsetParent);\n }\n\n if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n return window;\n }\n\n return offsetParent || getContainingBlock(element) || window;\n}","export default function getMainAxisFromPlacement(placement) {\n return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n}","import { max as mathMax, min as mathMin } from \"./math.js\";\nexport function within(min, value, max) {\n return mathMax(min, mathMin(value, max));\n}\nexport function withinMaxClamp(min, value, max) {\n var v = within(min, value, max);\n return v > max ? max : v;\n}","import getFreshSideObject from \"./getFreshSideObject.js\";\nexport default function mergePaddingObject(paddingObject) {\n return Object.assign({}, getFreshSideObject(), paddingObject);\n}","export default function getFreshSideObject() {\n return {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n };\n}","export default function expandToHashMap(value, keys) {\n return keys.reduce(function (hashMap, key) {\n hashMap[key] = value;\n return hashMap;\n }, {});\n}","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport contains from \"../dom-utils/contains.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport { within } from \"../utils/within.js\";\nimport mergePaddingObject from \"../utils/mergePaddingObject.js\";\nimport expandToHashMap from \"../utils/expandToHashMap.js\";\nimport { left, right, basePlacements, top, bottom } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar toPaddingObject = function toPaddingObject(padding, state) {\n padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n placement: state.placement\n })) : padding;\n return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n};\n\nfunction arrow(_ref) {\n var _state$modifiersData$;\n\n var state = _ref.state,\n name = _ref.name,\n options = _ref.options;\n var arrowElement = state.elements.arrow;\n var popperOffsets = state.modifiersData.popperOffsets;\n var basePlacement = getBasePlacement(state.placement);\n var axis = getMainAxisFromPlacement(basePlacement);\n var isVertical = [left, right].indexOf(basePlacement) >= 0;\n var len = isVertical ? 'height' : 'width';\n\n if (!arrowElement || !popperOffsets) {\n return;\n }\n\n var paddingObject = toPaddingObject(options.padding, state);\n var arrowRect = getLayoutRect(arrowElement);\n var minProp = axis === 'y' ? top : left;\n var maxProp = axis === 'y' ? bottom : right;\n var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n var arrowOffsetParent = getOffsetParent(arrowElement);\n var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n // outside of the popper bounds\n\n var min = paddingObject[minProp];\n var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n var axisProp = axis;\n state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state,\n options = _ref2.options;\n var _options$element = options.element,\n arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n if (arrowElement == null) {\n return;\n } // CSS selector\n\n\n if (typeof arrowElement === 'string') {\n arrowElement = state.elements.popper.querySelector(arrowElement);\n\n if (!arrowElement) {\n return;\n }\n }\n\n if (!contains(state.elements.popper, arrowElement)) {\n return;\n }\n\n state.elements.arrow = arrowElement;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'arrow',\n enabled: true,\n phase: 'main',\n fn: arrow,\n effect: effect,\n requires: ['popperOffsets'],\n requiresIfExists: ['preventOverflow']\n};","export default function getVariation(placement) {\n return placement.split('-')[1];\n}","import { top, left, right, bottom, end } from \"../enums.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getWindow from \"../dom-utils/getWindow.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getComputedStyle from \"../dom-utils/getComputedStyle.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport { round } from \"../utils/math.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar unsetSides = {\n top: 'auto',\n right: 'auto',\n bottom: 'auto',\n left: 'auto'\n}; // Round the offsets to the nearest suitable subpixel based on the DPR.\n// Zooming can change the DPR, but it seems to report a value that will\n// cleanly divide the values into the appropriate subpixels.\n\nfunction roundOffsetsByDPR(_ref, win) {\n var x = _ref.x,\n y = _ref.y;\n var dpr = win.devicePixelRatio || 1;\n return {\n x: round(x * dpr) / dpr || 0,\n y: round(y * dpr) / dpr || 0\n };\n}\n\nexport function mapToStyles(_ref2) {\n var _Object$assign2;\n\n var popper = _ref2.popper,\n popperRect = _ref2.popperRect,\n placement = _ref2.placement,\n variation = _ref2.variation,\n offsets = _ref2.offsets,\n position = _ref2.position,\n gpuAcceleration = _ref2.gpuAcceleration,\n adaptive = _ref2.adaptive,\n roundOffsets = _ref2.roundOffsets,\n isFixed = _ref2.isFixed;\n var _offsets$x = offsets.x,\n x = _offsets$x === void 0 ? 0 : _offsets$x,\n _offsets$y = offsets.y,\n y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n x: x,\n y: y\n }) : {\n x: x,\n y: y\n };\n\n x = _ref3.x;\n y = _ref3.y;\n var hasX = offsets.hasOwnProperty('x');\n var hasY = offsets.hasOwnProperty('y');\n var sideX = left;\n var sideY = top;\n var win = window;\n\n if (adaptive) {\n var offsetParent = getOffsetParent(popper);\n var heightProp = 'clientHeight';\n var widthProp = 'clientWidth';\n\n if (offsetParent === getWindow(popper)) {\n offsetParent = getDocumentElement(popper);\n\n if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n heightProp = 'scrollHeight';\n widthProp = 'scrollWidth';\n }\n } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n offsetParent = offsetParent;\n\n if (placement === top || (placement === left || placement === right) && variation === end) {\n sideY = bottom;\n var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n offsetParent[heightProp];\n y -= offsetY - popperRect.height;\n y *= gpuAcceleration ? 1 : -1;\n }\n\n if (placement === left || (placement === top || placement === bottom) && variation === end) {\n sideX = right;\n var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n offsetParent[widthProp];\n x -= offsetX - popperRect.width;\n x *= gpuAcceleration ? 1 : -1;\n }\n }\n\n var commonStyles = Object.assign({\n position: position\n }, adaptive && unsetSides);\n\n var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n x: x,\n y: y\n }, getWindow(popper)) : {\n x: x,\n y: y\n };\n\n x = _ref4.x;\n y = _ref4.y;\n\n if (gpuAcceleration) {\n var _Object$assign;\n\n return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n }\n\n return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n}\n\nfunction computeStyles(_ref5) {\n var state = _ref5.state,\n options = _ref5.options;\n var _options$gpuAccelerat = options.gpuAcceleration,\n gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n _options$adaptive = options.adaptive,\n adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n _options$roundOffsets = options.roundOffsets,\n roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n var commonStyles = {\n placement: getBasePlacement(state.placement),\n variation: getVariation(state.placement),\n popper: state.elements.popper,\n popperRect: state.rects.popper,\n gpuAcceleration: gpuAcceleration,\n isFixed: state.options.strategy === 'fixed'\n };\n\n if (state.modifiersData.popperOffsets != null) {\n state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.popperOffsets,\n position: state.options.strategy,\n adaptive: adaptive,\n roundOffsets: roundOffsets\n })));\n }\n\n if (state.modifiersData.arrow != null) {\n state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.arrow,\n position: 'absolute',\n adaptive: false,\n roundOffsets: roundOffsets\n })));\n }\n\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-placement': state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'computeStyles',\n enabled: true,\n phase: 'beforeWrite',\n fn: computeStyles,\n data: {}\n};","import getWindow from \"../dom-utils/getWindow.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar passive = {\n passive: true\n};\n\nfunction effect(_ref) {\n var state = _ref.state,\n instance = _ref.instance,\n options = _ref.options;\n var _options$scroll = options.scroll,\n scroll = _options$scroll === void 0 ? true : _options$scroll,\n _options$resize = options.resize,\n resize = _options$resize === void 0 ? true : _options$resize;\n var window = getWindow(state.elements.popper);\n var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.addEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.addEventListener('resize', instance.update, passive);\n }\n\n return function () {\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.removeEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.removeEventListener('resize', instance.update, passive);\n }\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'eventListeners',\n enabled: true,\n phase: 'write',\n fn: function fn() {},\n effect: effect,\n data: {}\n};","var hash = {\n left: 'right',\n right: 'left',\n bottom: 'top',\n top: 'bottom'\n};\nexport default function getOppositePlacement(placement) {\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}","var hash = {\n start: 'end',\n end: 'start'\n};\nexport default function getOppositeVariationPlacement(placement) {\n return placement.replace(/start|end/g, function (matched) {\n return hash[matched];\n });\n}","import getWindow from \"./getWindow.js\";\nexport default function getWindowScroll(node) {\n var win = getWindow(node);\n var scrollLeft = win.pageXOffset;\n var scrollTop = win.pageYOffset;\n return {\n scrollLeft: scrollLeft,\n scrollTop: scrollTop\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nexport default function getWindowScrollBarX(element) {\n // If has a CSS width greater than the viewport, then this will be\n // incorrect for RTL.\n // Popper 1 is broken in this case and never had a bug report so let's assume\n // it's not an issue. I don't think anyone ever specifies width on \n // anyway.\n // Browsers where the left scrollbar doesn't cause an issue report `0` for\n // this (e.g. Edge 2019, IE11, Safari)\n return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n}","import getComputedStyle from \"./getComputedStyle.js\";\nexport default function isScrollParent(element) {\n // Firefox wants us to check `-x` and `-y` variations as well\n var _getComputedStyle = getComputedStyle(element),\n overflow = _getComputedStyle.overflow,\n overflowX = _getComputedStyle.overflowX,\n overflowY = _getComputedStyle.overflowY;\n\n return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n}","import getParentNode from \"./getParentNode.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nexport default function getScrollParent(node) {\n if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return node.ownerDocument.body;\n }\n\n if (isHTMLElement(node) && isScrollParent(node)) {\n return node;\n }\n\n return getScrollParent(getParentNode(node));\n}","import getScrollParent from \"./getScrollParent.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getWindow from \"./getWindow.js\";\nimport isScrollParent from \"./isScrollParent.js\";\n/*\ngiven a DOM element, return the list of all scroll parents, up the list of ancesors\nuntil we get to the top window object. This list is what we attach scroll listeners\nto, because if any of these parent elements scroll, we'll need to re-calculate the\nreference element's position.\n*/\n\nexport default function listScrollParents(element, list) {\n var _element$ownerDocumen;\n\n if (list === void 0) {\n list = [];\n }\n\n var scrollParent = getScrollParent(element);\n var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n var win = getWindow(scrollParent);\n var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n var updatedList = list.concat(target);\n return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n updatedList.concat(listScrollParents(getParentNode(target)));\n}","export default function rectToClientRect(rect) {\n return Object.assign({}, rect, {\n left: rect.x,\n top: rect.y,\n right: rect.x + rect.width,\n bottom: rect.y + rect.height\n });\n}","import { viewport } from \"../enums.js\";\nimport getViewportRect from \"./getViewportRect.js\";\nimport getDocumentRect from \"./getDocumentRect.js\";\nimport listScrollParents from \"./listScrollParents.js\";\nimport getOffsetParent from \"./getOffsetParent.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport contains from \"./contains.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport rectToClientRect from \"../utils/rectToClientRect.js\";\nimport { max, min } from \"../utils/math.js\";\n\nfunction getInnerBoundingClientRect(element, strategy) {\n var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n rect.top = rect.top + element.clientTop;\n rect.left = rect.left + element.clientLeft;\n rect.bottom = rect.top + element.clientHeight;\n rect.right = rect.left + element.clientWidth;\n rect.width = element.clientWidth;\n rect.height = element.clientHeight;\n rect.x = rect.left;\n rect.y = rect.top;\n return rect;\n}\n\nfunction getClientRectFromMixedType(element, clippingParent, strategy) {\n return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n} // A \"clipping parent\" is an overflowable container with the characteristic of\n// clipping (or hiding) overflowing elements with a position different from\n// `initial`\n\n\nfunction getClippingParents(element) {\n var clippingParents = listScrollParents(getParentNode(element));\n var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n if (!isElement(clipperElement)) {\n return [];\n } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n return clippingParents.filter(function (clippingParent) {\n return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n });\n} // Gets the maximum area that the element is visible in due to any number of\n// clipping parents\n\n\nexport default function getClippingRect(element, boundary, rootBoundary, strategy) {\n var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n var firstClippingParent = clippingParents[0];\n var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n clippingRect.width = clippingRect.right - clippingRect.left;\n clippingRect.height = clippingRect.bottom - clippingRect.top;\n clippingRect.x = clippingRect.left;\n clippingRect.y = clippingRect.top;\n return clippingRect;\n}","import getWindow from \"./getWindow.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getViewportRect(element, strategy) {\n var win = getWindow(element);\n var html = getDocumentElement(element);\n var visualViewport = win.visualViewport;\n var width = html.clientWidth;\n var height = html.clientHeight;\n var x = 0;\n var y = 0;\n\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height;\n var layoutViewport = isLayoutViewport();\n\n if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n\n return {\n width: width,\n height: height,\n x: x + getWindowScrollBarX(element),\n y: y\n };\n}","import getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nimport { max } from \"../utils/math.js\"; // Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable\n\nexport default function getDocumentRect(element) {\n var _element$ownerDocumen;\n\n var html = getDocumentElement(element);\n var winScroll = getWindowScroll(element);\n var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n var y = -winScroll.scrollTop;\n\n if (getComputedStyle(body || html).direction === 'rtl') {\n x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n }\n\n return {\n width: width,\n height: height,\n x: x,\n y: y\n };\n}","import getBasePlacement from \"./getBasePlacement.js\";\nimport getVariation from \"./getVariation.js\";\nimport getMainAxisFromPlacement from \"./getMainAxisFromPlacement.js\";\nimport { top, right, bottom, left, start, end } from \"../enums.js\";\nexport default function computeOffsets(_ref) {\n var reference = _ref.reference,\n element = _ref.element,\n placement = _ref.placement;\n var basePlacement = placement ? getBasePlacement(placement) : null;\n var variation = placement ? getVariation(placement) : null;\n var commonX = reference.x + reference.width / 2 - element.width / 2;\n var commonY = reference.y + reference.height / 2 - element.height / 2;\n var offsets;\n\n switch (basePlacement) {\n case top:\n offsets = {\n x: commonX,\n y: reference.y - element.height\n };\n break;\n\n case bottom:\n offsets = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n\n case right:\n offsets = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n\n case left:\n offsets = {\n x: reference.x - element.width,\n y: commonY\n };\n break;\n\n default:\n offsets = {\n x: reference.x,\n y: reference.y\n };\n }\n\n var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n if (mainAxis != null) {\n var len = mainAxis === 'y' ? 'height' : 'width';\n\n switch (variation) {\n case start:\n offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n break;\n\n case end:\n offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n break;\n\n default:\n }\n }\n\n return offsets;\n}","import getClippingRect from \"../dom-utils/getClippingRect.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getBoundingClientRect from \"../dom-utils/getBoundingClientRect.js\";\nimport computeOffsets from \"./computeOffsets.js\";\nimport rectToClientRect from \"./rectToClientRect.js\";\nimport { clippingParents, reference, popper, bottom, top, right, basePlacements, viewport } from \"../enums.js\";\nimport { isElement } from \"../dom-utils/instanceOf.js\";\nimport mergePaddingObject from \"./mergePaddingObject.js\";\nimport expandToHashMap from \"./expandToHashMap.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport default function detectOverflow(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n _options$placement = _options.placement,\n placement = _options$placement === void 0 ? state.placement : _options$placement,\n _options$strategy = _options.strategy,\n strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n _options$boundary = _options.boundary,\n boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n _options$rootBoundary = _options.rootBoundary,\n rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n _options$elementConte = _options.elementContext,\n elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n _options$altBoundary = _options.altBoundary,\n altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n _options$padding = _options.padding,\n padding = _options$padding === void 0 ? 0 : _options$padding;\n var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n var altContext = elementContext === popper ? reference : popper;\n var popperRect = state.rects.popper;\n var element = state.elements[altBoundary ? altContext : elementContext];\n var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n var referenceClientRect = getBoundingClientRect(state.elements.reference);\n var popperOffsets = computeOffsets({\n reference: referenceClientRect,\n element: popperRect,\n strategy: 'absolute',\n placement: placement\n });\n var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n // 0 or negative = within the clipping rect\n\n var overflowOffsets = {\n top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n };\n var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n if (elementContext === popper && offsetData) {\n var offset = offsetData[placement];\n Object.keys(overflowOffsets).forEach(function (key) {\n var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n overflowOffsets[key] += offset[axis] * multiply;\n });\n }\n\n return overflowOffsets;\n}","import getOppositePlacement from \"../utils/getOppositePlacement.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getOppositeVariationPlacement from \"../utils/getOppositeVariationPlacement.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport computeAutoPlacement from \"../utils/computeAutoPlacement.js\";\nimport { bottom, top, start, right, left, auto } from \"../enums.js\";\nimport getVariation from \"../utils/getVariation.js\"; // eslint-disable-next-line import/no-unused-modules\n\nfunction getExpandedFallbackPlacements(placement) {\n if (getBasePlacement(placement) === auto) {\n return [];\n }\n\n var oppositePlacement = getOppositePlacement(placement);\n return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n}\n\nfunction flip(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n\n if (state.modifiersData[name]._skip) {\n return;\n }\n\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n specifiedFallbackPlacements = options.fallbackPlacements,\n padding = options.padding,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n _options$flipVariatio = options.flipVariations,\n flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n allowedAutoPlacements = options.allowedAutoPlacements;\n var preferredPlacement = state.options.placement;\n var basePlacement = getBasePlacement(preferredPlacement);\n var isBasePlacement = basePlacement === preferredPlacement;\n var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n flipVariations: flipVariations,\n allowedAutoPlacements: allowedAutoPlacements\n }) : placement);\n }, []);\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var checksMap = new Map();\n var makeFallbackChecks = true;\n var firstFittingPlacement = placements[0];\n\n for (var i = 0; i < placements.length; i++) {\n var placement = placements[i];\n\n var _basePlacement = getBasePlacement(placement);\n\n var isStartVariation = getVariation(placement) === start;\n var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n var len = isVertical ? 'width' : 'height';\n var overflow = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n altBoundary: altBoundary,\n padding: padding\n });\n var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n if (referenceRect[len] > popperRect[len]) {\n mainVariationSide = getOppositePlacement(mainVariationSide);\n }\n\n var altVariationSide = getOppositePlacement(mainVariationSide);\n var checks = [];\n\n if (checkMainAxis) {\n checks.push(overflow[_basePlacement] <= 0);\n }\n\n if (checkAltAxis) {\n checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n }\n\n if (checks.every(function (check) {\n return check;\n })) {\n firstFittingPlacement = placement;\n makeFallbackChecks = false;\n break;\n }\n\n checksMap.set(placement, checks);\n }\n\n if (makeFallbackChecks) {\n // `2` may be desired in some cases – research later\n var numberOfChecks = flipVariations ? 3 : 1;\n\n var _loop = function _loop(_i) {\n var fittingPlacement = placements.find(function (placement) {\n var checks = checksMap.get(placement);\n\n if (checks) {\n return checks.slice(0, _i).every(function (check) {\n return check;\n });\n }\n });\n\n if (fittingPlacement) {\n firstFittingPlacement = fittingPlacement;\n return \"break\";\n }\n };\n\n for (var _i = numberOfChecks; _i > 0; _i--) {\n var _ret = _loop(_i);\n\n if (_ret === \"break\") break;\n }\n }\n\n if (state.placement !== firstFittingPlacement) {\n state.modifiersData[name]._skip = true;\n state.placement = firstFittingPlacement;\n state.reset = true;\n }\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'flip',\n enabled: true,\n phase: 'main',\n fn: flip,\n requiresIfExists: ['offset'],\n data: {\n _skip: false\n }\n};","import getVariation from \"./getVariation.js\";\nimport { variationPlacements, basePlacements, placements as allPlacements } from \"../enums.js\";\nimport detectOverflow from \"./detectOverflow.js\";\nimport getBasePlacement from \"./getBasePlacement.js\";\nexport default function computeAutoPlacement(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n placement = _options.placement,\n boundary = _options.boundary,\n rootBoundary = _options.rootBoundary,\n padding = _options.padding,\n flipVariations = _options.flipVariations,\n _options$allowedAutoP = _options.allowedAutoPlacements,\n allowedAutoPlacements = _options$allowedAutoP === void 0 ? allPlacements : _options$allowedAutoP;\n var variation = getVariation(placement);\n var placements = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n return getVariation(placement) === variation;\n }) : basePlacements;\n var allowedPlacements = placements.filter(function (placement) {\n return allowedAutoPlacements.indexOf(placement) >= 0;\n });\n\n if (allowedPlacements.length === 0) {\n allowedPlacements = placements;\n } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n var overflows = allowedPlacements.reduce(function (acc, placement) {\n acc[placement] = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding\n })[getBasePlacement(placement)];\n return acc;\n }, {});\n return Object.keys(overflows).sort(function (a, b) {\n return overflows[a] - overflows[b];\n });\n}","import { top, bottom, left, right } from \"../enums.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\n\nfunction getSideOffsets(overflow, rect, preventedOffsets) {\n if (preventedOffsets === void 0) {\n preventedOffsets = {\n x: 0,\n y: 0\n };\n }\n\n return {\n top: overflow.top - rect.height - preventedOffsets.y,\n right: overflow.right - rect.width + preventedOffsets.x,\n bottom: overflow.bottom - rect.height + preventedOffsets.y,\n left: overflow.left - rect.width - preventedOffsets.x\n };\n}\n\nfunction isAnySideFullyClipped(overflow) {\n return [top, right, bottom, left].some(function (side) {\n return overflow[side] >= 0;\n });\n}\n\nfunction hide(_ref) {\n var state = _ref.state,\n name = _ref.name;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var preventedOffsets = state.modifiersData.preventOverflow;\n var referenceOverflow = detectOverflow(state, {\n elementContext: 'reference'\n });\n var popperAltOverflow = detectOverflow(state, {\n altBoundary: true\n });\n var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n state.modifiersData[name] = {\n referenceClippingOffsets: referenceClippingOffsets,\n popperEscapeOffsets: popperEscapeOffsets,\n isReferenceHidden: isReferenceHidden,\n hasPopperEscaped: hasPopperEscaped\n };\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-reference-hidden': isReferenceHidden,\n 'data-popper-escaped': hasPopperEscaped\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'hide',\n enabled: true,\n phase: 'main',\n requiresIfExists: ['preventOverflow'],\n fn: hide\n};","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { top, left, right, placements } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport function distanceAndSkiddingToXY(placement, rects, offset) {\n var basePlacement = getBasePlacement(placement);\n var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n placement: placement\n })) : offset,\n skidding = _ref[0],\n distance = _ref[1];\n\n skidding = skidding || 0;\n distance = (distance || 0) * invertDistance;\n return [left, right].indexOf(basePlacement) >= 0 ? {\n x: distance,\n y: skidding\n } : {\n x: skidding,\n y: distance\n };\n}\n\nfunction offset(_ref2) {\n var state = _ref2.state,\n options = _ref2.options,\n name = _ref2.name;\n var _options$offset = options.offset,\n offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n var data = placements.reduce(function (acc, placement) {\n acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n return acc;\n }, {});\n var _data$state$placement = data[state.placement],\n x = _data$state$placement.x,\n y = _data$state$placement.y;\n\n if (state.modifiersData.popperOffsets != null) {\n state.modifiersData.popperOffsets.x += x;\n state.modifiersData.popperOffsets.y += y;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'offset',\n enabled: true,\n phase: 'main',\n requires: ['popperOffsets'],\n fn: offset\n};","import computeOffsets from \"../utils/computeOffsets.js\";\n\nfunction popperOffsets(_ref) {\n var state = _ref.state,\n name = _ref.name;\n // Offsets are the actual position the popper needs to have to be\n // properly positioned near its reference element\n // This is the most basic placement, and will be adjusted by\n // the modifiers in the next step\n state.modifiersData[name] = computeOffsets({\n reference: state.rects.reference,\n element: state.rects.popper,\n strategy: 'absolute',\n placement: state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'popperOffsets',\n enabled: true,\n phase: 'read',\n fn: popperOffsets,\n data: {}\n};","import { top, left, right, bottom, start } from \"../enums.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport getAltAxis from \"../utils/getAltAxis.js\";\nimport { within, withinMaxClamp } from \"../utils/within.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport getFreshSideObject from \"../utils/getFreshSideObject.js\";\nimport { min as mathMin, max as mathMax } from \"../utils/math.js\";\n\nfunction preventOverflow(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n padding = options.padding,\n _options$tether = options.tether,\n tether = _options$tether === void 0 ? true : _options$tether,\n _options$tetherOffset = options.tetherOffset,\n tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n var overflow = detectOverflow(state, {\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n altBoundary: altBoundary\n });\n var basePlacement = getBasePlacement(state.placement);\n var variation = getVariation(state.placement);\n var isBasePlacement = !variation;\n var mainAxis = getMainAxisFromPlacement(basePlacement);\n var altAxis = getAltAxis(mainAxis);\n var popperOffsets = state.modifiersData.popperOffsets;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n placement: state.placement\n })) : tetherOffset;\n var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n mainAxis: tetherOffsetValue,\n altAxis: tetherOffsetValue\n } : Object.assign({\n mainAxis: 0,\n altAxis: 0\n }, tetherOffsetValue);\n var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n var data = {\n x: 0,\n y: 0\n };\n\n if (!popperOffsets) {\n return;\n }\n\n if (checkMainAxis) {\n var _offsetModifierState$;\n\n var mainSide = mainAxis === 'y' ? top : left;\n var altSide = mainAxis === 'y' ? bottom : right;\n var len = mainAxis === 'y' ? 'height' : 'width';\n var offset = popperOffsets[mainAxis];\n var min = offset + overflow[mainSide];\n var max = offset - overflow[altSide];\n var additive = tether ? -popperRect[len] / 2 : 0;\n var minLen = variation === start ? referenceRect[len] : popperRect[len];\n var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n // outside the reference bounds\n\n var arrowElement = state.elements.arrow;\n var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n width: 0,\n height: 0\n };\n var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n var arrowPaddingMin = arrowPaddingObject[mainSide];\n var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n // to include its full size in the calculation. If the reference is small\n // and near the edge of a boundary, the popper can overflow even if the\n // reference is not overflowing as well (e.g. virtual elements with no\n // width or height)\n\n var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n var tetherMax = offset + maxOffset - offsetModifierValue;\n var preventedOffset = within(tether ? mathMin(min, tetherMin) : min, offset, tether ? mathMax(max, tetherMax) : max);\n popperOffsets[mainAxis] = preventedOffset;\n data[mainAxis] = preventedOffset - offset;\n }\n\n if (checkAltAxis) {\n var _offsetModifierState$2;\n\n var _mainSide = mainAxis === 'x' ? top : left;\n\n var _altSide = mainAxis === 'x' ? bottom : right;\n\n var _offset = popperOffsets[altAxis];\n\n var _len = altAxis === 'y' ? 'height' : 'width';\n\n var _min = _offset + overflow[_mainSide];\n\n var _max = _offset - overflow[_altSide];\n\n var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n popperOffsets[altAxis] = _preventedOffset;\n data[altAxis] = _preventedOffset - _offset;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'preventOverflow',\n enabled: true,\n phase: 'main',\n fn: preventOverflow,\n requiresIfExists: ['offset']\n};","export default function getAltAxis(axis) {\n return axis === 'x' ? 'y' : 'x';\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getNodeScroll from \"./getNodeScroll.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport { round } from \"../utils/math.js\";\n\nfunction isElementScaled(element) {\n var rect = element.getBoundingClientRect();\n var scaleX = round(rect.width) / element.offsetWidth || 1;\n var scaleY = round(rect.height) / element.offsetHeight || 1;\n return scaleX !== 1 || scaleY !== 1;\n} // Returns the composite rect of an element relative to its offsetParent.\n// Composite means it takes into account transforms as well as layout.\n\n\nexport default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n\n var isOffsetParentAnElement = isHTMLElement(offsetParent);\n var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n var documentElement = getDocumentElement(offsetParent);\n var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n var scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n var offsets = {\n x: 0,\n y: 0\n };\n\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n isScrollParent(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n\n if (isHTMLElement(offsetParent)) {\n offsets = getBoundingClientRect(offsetParent, true);\n offsets.x += offsetParent.clientLeft;\n offsets.y += offsetParent.clientTop;\n } else if (documentElement) {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n }\n\n return {\n x: rect.left + scroll.scrollLeft - offsets.x,\n y: rect.top + scroll.scrollTop - offsets.y,\n width: rect.width,\n height: rect.height\n };\n}","import getWindowScroll from \"./getWindowScroll.js\";\nimport getWindow from \"./getWindow.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getHTMLElementScroll from \"./getHTMLElementScroll.js\";\nexport default function getNodeScroll(node) {\n if (node === getWindow(node) || !isHTMLElement(node)) {\n return getWindowScroll(node);\n } else {\n return getHTMLElementScroll(node);\n }\n}","export default function getHTMLElementScroll(element) {\n return {\n scrollLeft: element.scrollLeft,\n scrollTop: element.scrollTop\n };\n}","import { modifierPhases } from \"../enums.js\"; // source: https://stackoverflow.com/questions/49875255\n\nfunction order(modifiers) {\n var map = new Map();\n var visited = new Set();\n var result = [];\n modifiers.forEach(function (modifier) {\n map.set(modifier.name, modifier);\n }); // On visiting object, check for its dependencies and visit them recursively\n\n function sort(modifier) {\n visited.add(modifier.name);\n var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n requires.forEach(function (dep) {\n if (!visited.has(dep)) {\n var depModifier = map.get(dep);\n\n if (depModifier) {\n sort(depModifier);\n }\n }\n });\n result.push(modifier);\n }\n\n modifiers.forEach(function (modifier) {\n if (!visited.has(modifier.name)) {\n // check for visited object\n sort(modifier);\n }\n });\n return result;\n}\n\nexport default function orderModifiers(modifiers) {\n // order based on dependencies\n var orderedModifiers = order(modifiers); // order based on phase\n\n return modifierPhases.reduce(function (acc, phase) {\n return acc.concat(orderedModifiers.filter(function (modifier) {\n return modifier.phase === phase;\n }));\n }, []);\n}","import getCompositeRect from \"./dom-utils/getCompositeRect.js\";\nimport getLayoutRect from \"./dom-utils/getLayoutRect.js\";\nimport listScrollParents from \"./dom-utils/listScrollParents.js\";\nimport getOffsetParent from \"./dom-utils/getOffsetParent.js\";\nimport orderModifiers from \"./utils/orderModifiers.js\";\nimport debounce from \"./utils/debounce.js\";\nimport mergeByName from \"./utils/mergeByName.js\";\nimport detectOverflow from \"./utils/detectOverflow.js\";\nimport { isElement } from \"./dom-utils/instanceOf.js\";\nvar DEFAULT_OPTIONS = {\n placement: 'bottom',\n modifiers: [],\n strategy: 'absolute'\n};\n\nfunction areValidElements() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return !args.some(function (element) {\n return !(element && typeof element.getBoundingClientRect === 'function');\n });\n}\n\nexport function popperGenerator(generatorOptions) {\n if (generatorOptions === void 0) {\n generatorOptions = {};\n }\n\n var _generatorOptions = generatorOptions,\n _generatorOptions$def = _generatorOptions.defaultModifiers,\n defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n _generatorOptions$def2 = _generatorOptions.defaultOptions,\n defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n return function createPopper(reference, popper, options) {\n if (options === void 0) {\n options = defaultOptions;\n }\n\n var state = {\n placement: 'bottom',\n orderedModifiers: [],\n options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n modifiersData: {},\n elements: {\n reference: reference,\n popper: popper\n },\n attributes: {},\n styles: {}\n };\n var effectCleanupFns = [];\n var isDestroyed = false;\n var instance = {\n state: state,\n setOptions: function setOptions(setOptionsAction) {\n var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n cleanupModifierEffects();\n state.options = Object.assign({}, defaultOptions, state.options, options);\n state.scrollParents = {\n reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n popper: listScrollParents(popper)\n }; // Orders the modifiers based on their dependencies and `phase`\n // properties\n\n var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n state.orderedModifiers = orderedModifiers.filter(function (m) {\n return m.enabled;\n });\n runModifierEffects();\n return instance.update();\n },\n // Sync update – it will always be executed, even if not necessary. This\n // is useful for low frequency updates where sync behavior simplifies the\n // logic.\n // For high frequency updates (e.g. `resize` and `scroll` events), always\n // prefer the async Popper#update method\n forceUpdate: function forceUpdate() {\n if (isDestroyed) {\n return;\n }\n\n var _state$elements = state.elements,\n reference = _state$elements.reference,\n popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n // anymore\n\n if (!areValidElements(reference, popper)) {\n return;\n } // Store the reference and popper rects to be read by modifiers\n\n\n state.rects = {\n reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n popper: getLayoutRect(popper)\n }; // Modifiers have the ability to reset the current update cycle. The\n // most common use case for this is the `flip` modifier changing the\n // placement, which then needs to re-run all the modifiers, because the\n // logic was previously ran for the previous placement and is therefore\n // stale/incorrect\n\n state.reset = false;\n state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n // is filled with the initial data specified by the modifier. This means\n // it doesn't persist and is fresh on each update.\n // To ensure persistent data, use `${name}#persistent`\n\n state.orderedModifiers.forEach(function (modifier) {\n return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n });\n\n for (var index = 0; index < state.orderedModifiers.length; index++) {\n if (state.reset === true) {\n state.reset = false;\n index = -1;\n continue;\n }\n\n var _state$orderedModifie = state.orderedModifiers[index],\n fn = _state$orderedModifie.fn,\n _state$orderedModifie2 = _state$orderedModifie.options,\n _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n name = _state$orderedModifie.name;\n\n if (typeof fn === 'function') {\n state = fn({\n state: state,\n options: _options,\n name: name,\n instance: instance\n }) || state;\n }\n }\n },\n // Async and optimistically optimized update – it will not be executed if\n // not necessary (debounced to run at most once-per-tick)\n update: debounce(function () {\n return new Promise(function (resolve) {\n instance.forceUpdate();\n resolve(state);\n });\n }),\n destroy: function destroy() {\n cleanupModifierEffects();\n isDestroyed = true;\n }\n };\n\n if (!areValidElements(reference, popper)) {\n return instance;\n }\n\n instance.setOptions(options).then(function (state) {\n if (!isDestroyed && options.onFirstUpdate) {\n options.onFirstUpdate(state);\n }\n }); // Modifiers have the ability to execute arbitrary code before the first\n // update cycle runs. They will be executed in the same order as the update\n // cycle. This is useful when a modifier adds some persistent data that\n // other modifiers need to use, but the modifier is run after the dependent\n // one.\n\n function runModifierEffects() {\n state.orderedModifiers.forEach(function (_ref) {\n var name = _ref.name,\n _ref$options = _ref.options,\n options = _ref$options === void 0 ? {} : _ref$options,\n effect = _ref.effect;\n\n if (typeof effect === 'function') {\n var cleanupFn = effect({\n state: state,\n name: name,\n instance: instance,\n options: options\n });\n\n var noopFn = function noopFn() {};\n\n effectCleanupFns.push(cleanupFn || noopFn);\n }\n });\n }\n\n function cleanupModifierEffects() {\n effectCleanupFns.forEach(function (fn) {\n return fn();\n });\n effectCleanupFns = [];\n }\n\n return instance;\n };\n}\nexport var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\nexport { detectOverflow };","export default function debounce(fn) {\n var pending;\n return function () {\n if (!pending) {\n pending = new Promise(function (resolve) {\n Promise.resolve().then(function () {\n pending = undefined;\n resolve(fn());\n });\n });\n }\n\n return pending;\n };\n}","export default function mergeByName(modifiers) {\n var merged = modifiers.reduce(function (merged, current) {\n var existing = merged[current.name];\n merged[current.name] = existing ? Object.assign({}, existing, current, {\n options: Object.assign({}, existing.options, current.options),\n data: Object.assign({}, existing.data, current.data)\n }) : current;\n return merged;\n }, {}); // IE11 does not support Object.values\n\n return Object.keys(merged).map(function (key) {\n return merged[key];\n });\n}","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nimport offset from \"./modifiers/offset.js\";\nimport flip from \"./modifiers/flip.js\";\nimport preventOverflow from \"./modifiers/preventOverflow.js\";\nimport arrow from \"./modifiers/arrow.js\";\nimport hide from \"./modifiers/hide.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles, offset, flip, preventOverflow, arrow, hide];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow }; // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper as createPopperLite } from \"./popper-lite.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport * from \"./modifiers/index.js\";","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow };","/*!\n * Bootstrap v5.3.3 (https://getbootstrap.com/)\n * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\nimport * as Popper from '@popperjs/core';\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/data.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * Constants\n */\n\nconst elementMap = new Map();\nconst Data = {\n set(element, key, instance) {\n if (!elementMap.has(element)) {\n elementMap.set(element, new Map());\n }\n const instanceMap = elementMap.get(element);\n\n // make it clear we only want one instance per element\n // can be removed later when multiple key/instances are fine to be used\n if (!instanceMap.has(key) && instanceMap.size !== 0) {\n // eslint-disable-next-line no-console\n console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`);\n return;\n }\n instanceMap.set(key, instance);\n },\n get(element, key) {\n if (elementMap.has(element)) {\n return elementMap.get(element).get(key) || null;\n }\n return null;\n },\n remove(element, key) {\n if (!elementMap.has(element)) {\n return;\n }\n const instanceMap = elementMap.get(element);\n instanceMap.delete(key);\n\n // free up element references if there are no instances left for an element\n if (instanceMap.size === 0) {\n elementMap.delete(element);\n }\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst MAX_UID = 1000000;\nconst MILLISECONDS_MULTIPLIER = 1000;\nconst TRANSITION_END = 'transitionend';\n\n/**\n * Properly escape IDs selectors to handle weird IDs\n * @param {string} selector\n * @returns {string}\n */\nconst parseSelector = selector => {\n if (selector && window.CSS && window.CSS.escape) {\n // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`);\n }\n return selector;\n};\n\n// Shout-out Angus Croll (https://goo.gl/pxwQGp)\nconst toType = object => {\n if (object === null || object === undefined) {\n return `${object}`;\n }\n return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase();\n};\n\n/**\n * Public Util API\n */\n\nconst getUID = prefix => {\n do {\n prefix += Math.floor(Math.random() * MAX_UID);\n } while (document.getElementById(prefix));\n return prefix;\n};\nconst getTransitionDurationFromElement = element => {\n if (!element) {\n return 0;\n }\n\n // Get transition-duration of the element\n let {\n transitionDuration,\n transitionDelay\n } = window.getComputedStyle(element);\n const floatTransitionDuration = Number.parseFloat(transitionDuration);\n const floatTransitionDelay = Number.parseFloat(transitionDelay);\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0;\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0];\n transitionDelay = transitionDelay.split(',')[0];\n return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;\n};\nconst triggerTransitionEnd = element => {\n element.dispatchEvent(new Event(TRANSITION_END));\n};\nconst isElement = object => {\n if (!object || typeof object !== 'object') {\n return false;\n }\n if (typeof object.jquery !== 'undefined') {\n object = object[0];\n }\n return typeof object.nodeType !== 'undefined';\n};\nconst getElement = object => {\n // it's a jQuery object or a node element\n if (isElement(object)) {\n return object.jquery ? object[0] : object;\n }\n if (typeof object === 'string' && object.length > 0) {\n return document.querySelector(parseSelector(object));\n }\n return null;\n};\nconst isVisible = element => {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false;\n }\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';\n // Handle `details` element as its content may falsie appear visible when it is closed\n const closedDetails = element.closest('details:not([open])');\n if (!closedDetails) {\n return elementIsVisible;\n }\n if (closedDetails !== element) {\n const summary = element.closest('summary');\n if (summary && summary.parentNode !== closedDetails) {\n return false;\n }\n if (summary === null) {\n return false;\n }\n }\n return elementIsVisible;\n};\nconst isDisabled = element => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true;\n }\n if (element.classList.contains('disabled')) {\n return true;\n }\n if (typeof element.disabled !== 'undefined') {\n return element.disabled;\n }\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n};\nconst findShadowRoot = element => {\n if (!document.documentElement.attachShadow) {\n return null;\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode();\n return root instanceof ShadowRoot ? root : null;\n }\n if (element instanceof ShadowRoot) {\n return element;\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null;\n }\n return findShadowRoot(element.parentNode);\n};\nconst noop = () => {};\n\n/**\n * Trick to restart an element's animation\n *\n * @param {HTMLElement} element\n * @return void\n *\n * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n */\nconst reflow = element => {\n element.offsetHeight; // eslint-disable-line no-unused-expressions\n};\nconst getjQuery = () => {\n if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n return window.jQuery;\n }\n return null;\n};\nconst DOMContentLoadedCallbacks = [];\nconst onDOMContentLoaded = callback => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!DOMContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of DOMContentLoadedCallbacks) {\n callback();\n }\n });\n }\n DOMContentLoadedCallbacks.push(callback);\n } else {\n callback();\n }\n};\nconst isRTL = () => document.documentElement.dir === 'rtl';\nconst defineJQueryPlugin = plugin => {\n onDOMContentLoaded(() => {\n const $ = getjQuery();\n /* istanbul ignore if */\n if ($) {\n const name = plugin.NAME;\n const JQUERY_NO_CONFLICT = $.fn[name];\n $.fn[name] = plugin.jQueryInterface;\n $.fn[name].Constructor = plugin;\n $.fn[name].noConflict = () => {\n $.fn[name] = JQUERY_NO_CONFLICT;\n return plugin.jQueryInterface;\n };\n }\n });\n};\nconst execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;\n};\nconst executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n if (!waitForTransition) {\n execute(callback);\n return;\n }\n const durationPadding = 5;\n const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding;\n let called = false;\n const handler = ({\n target\n }) => {\n if (target !== transitionElement) {\n return;\n }\n called = true;\n transitionElement.removeEventListener(TRANSITION_END, handler);\n execute(callback);\n };\n transitionElement.addEventListener(TRANSITION_END, handler);\n setTimeout(() => {\n if (!called) {\n triggerTransitionEnd(transitionElement);\n }\n }, emulatedDuration);\n};\n\n/**\n * Return the previous/next element of a list.\n *\n * @param {array} list The list of elements\n * @param activeElement The active element\n * @param shouldGetNext Choose to get next or previous element\n * @param isCycleAllowed\n * @return {Element|elem} The proper element\n */\nconst getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n const listLength = list.length;\n let index = list.indexOf(activeElement);\n\n // if the element does not exist in the list return an element\n // depending on the direction and if cycle is allowed\n if (index === -1) {\n return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0];\n }\n index += shouldGetNext ? 1 : -1;\n if (isCycleAllowed) {\n index = (index + listLength) % listLength;\n }\n return list[Math.max(0, Math.min(index, listLength - 1))];\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/event-handler.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst namespaceRegex = /[^.]*(?=\\..*)\\.|.*/;\nconst stripNameRegex = /\\..*/;\nconst stripUidRegex = /::\\d+$/;\nconst eventRegistry = {}; // Events storage\nlet uidEvent = 1;\nconst customEvents = {\n mouseenter: 'mouseover',\n mouseleave: 'mouseout'\n};\nconst nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']);\n\n/**\n * Private methods\n */\n\nfunction makeEventUid(element, uid) {\n return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++;\n}\nfunction getElementEvents(element) {\n const uid = makeEventUid(element);\n element.uidEvent = uid;\n eventRegistry[uid] = eventRegistry[uid] || {};\n return eventRegistry[uid];\n}\nfunction bootstrapHandler(element, fn) {\n return function handler(event) {\n hydrateObj(event, {\n delegateTarget: element\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, fn);\n }\n return fn.apply(element, [event]);\n };\n}\nfunction bootstrapDelegationHandler(element, selector, fn) {\n return function handler(event) {\n const domElements = element.querySelectorAll(selector);\n for (let {\n target\n } = event; target && target !== this; target = target.parentNode) {\n for (const domElement of domElements) {\n if (domElement !== target) {\n continue;\n }\n hydrateObj(event, {\n delegateTarget: target\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, selector, fn);\n }\n return fn.apply(target, [event]);\n }\n }\n };\n}\nfunction findHandler(events, callable, delegationSelector = null) {\n return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector);\n}\nfunction normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n const isDelegated = typeof handler === 'string';\n // TODO: tooltip passes `false` instead of selector, so we need to check\n const callable = isDelegated ? delegationFunction : handler || delegationFunction;\n let typeEvent = getTypeEvent(originalTypeEvent);\n if (!nativeEvents.has(typeEvent)) {\n typeEvent = originalTypeEvent;\n }\n return [isDelegated, callable, typeEvent];\n}\nfunction addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n\n // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n if (originalTypeEvent in customEvents) {\n const wrapFunction = fn => {\n return function (event) {\n if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) {\n return fn.call(this, event);\n }\n };\n };\n callable = wrapFunction(callable);\n }\n const events = getElementEvents(element);\n const handlers = events[typeEvent] || (events[typeEvent] = {});\n const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null);\n if (previousFunction) {\n previousFunction.oneOff = previousFunction.oneOff && oneOff;\n return;\n }\n const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''));\n const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable);\n fn.delegationSelector = isDelegated ? handler : null;\n fn.callable = callable;\n fn.oneOff = oneOff;\n fn.uidEvent = uid;\n handlers[uid] = fn;\n element.addEventListener(typeEvent, fn, isDelegated);\n}\nfunction removeHandler(element, events, typeEvent, handler, delegationSelector) {\n const fn = findHandler(events[typeEvent], handler, delegationSelector);\n if (!fn) {\n return;\n }\n element.removeEventListener(typeEvent, fn, Boolean(delegationSelector));\n delete events[typeEvent][fn.uidEvent];\n}\nfunction removeNamespacedHandlers(element, events, typeEvent, namespace) {\n const storeElementEvent = events[typeEvent] || {};\n for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n if (handlerKey.includes(namespace)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n}\nfunction getTypeEvent(event) {\n // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n event = event.replace(stripNameRegex, '');\n return customEvents[event] || event;\n}\nconst EventHandler = {\n on(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, false);\n },\n one(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, true);\n },\n off(element, originalTypeEvent, handler, delegationFunction) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n const inNamespace = typeEvent !== originalTypeEvent;\n const events = getElementEvents(element);\n const storeElementEvent = events[typeEvent] || {};\n const isNamespace = originalTypeEvent.startsWith('.');\n if (typeof callable !== 'undefined') {\n // Simplest case: handler is passed, remove that listener ONLY.\n if (!Object.keys(storeElementEvent).length) {\n return;\n }\n removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null);\n return;\n }\n if (isNamespace) {\n for (const elementEvent of Object.keys(events)) {\n removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1));\n }\n }\n for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n const handlerKey = keyHandlers.replace(stripUidRegex, '');\n if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n },\n trigger(element, event, args) {\n if (typeof event !== 'string' || !element) {\n return null;\n }\n const $ = getjQuery();\n const typeEvent = getTypeEvent(event);\n const inNamespace = event !== typeEvent;\n let jQueryEvent = null;\n let bubbles = true;\n let nativeDispatch = true;\n let defaultPrevented = false;\n if (inNamespace && $) {\n jQueryEvent = $.Event(event, args);\n $(element).trigger(jQueryEvent);\n bubbles = !jQueryEvent.isPropagationStopped();\n nativeDispatch = !jQueryEvent.isImmediatePropagationStopped();\n defaultPrevented = jQueryEvent.isDefaultPrevented();\n }\n const evt = hydrateObj(new Event(event, {\n bubbles,\n cancelable: true\n }), args);\n if (defaultPrevented) {\n evt.preventDefault();\n }\n if (nativeDispatch) {\n element.dispatchEvent(evt);\n }\n if (evt.defaultPrevented && jQueryEvent) {\n jQueryEvent.preventDefault();\n }\n return evt;\n }\n};\nfunction hydrateObj(obj, meta = {}) {\n for (const [key, value] of Object.entries(meta)) {\n try {\n obj[key] = value;\n } catch (_unused) {\n Object.defineProperty(obj, key, {\n configurable: true,\n get() {\n return value;\n }\n });\n }\n }\n return obj;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/manipulator.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nfunction normalizeData(value) {\n if (value === 'true') {\n return true;\n }\n if (value === 'false') {\n return false;\n }\n if (value === Number(value).toString()) {\n return Number(value);\n }\n if (value === '' || value === 'null') {\n return null;\n }\n if (typeof value !== 'string') {\n return value;\n }\n try {\n return JSON.parse(decodeURIComponent(value));\n } catch (_unused) {\n return value;\n }\n}\nfunction normalizeDataKey(key) {\n return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`);\n}\nconst Manipulator = {\n setDataAttribute(element, key, value) {\n element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value);\n },\n removeDataAttribute(element, key) {\n element.removeAttribute(`data-bs-${normalizeDataKey(key)}`);\n },\n getDataAttributes(element) {\n if (!element) {\n return {};\n }\n const attributes = {};\n const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));\n for (const key of bsKeys) {\n let pureKey = key.replace(/^bs/, '');\n pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);\n attributes[pureKey] = normalizeData(element.dataset[key]);\n }\n return attributes;\n },\n getDataAttribute(element, key) {\n return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`));\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/config.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Class definition\n */\n\nclass Config {\n // Getters\n static get Default() {\n return {};\n }\n static get DefaultType() {\n return {};\n }\n static get NAME() {\n throw new Error('You have to implement the static method \"NAME\", for each component!');\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n return config;\n }\n _mergeConfigObj(config, element) {\n const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse\n\n return {\n ...this.constructor.Default,\n ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n ...(typeof config === 'object' ? config : {})\n };\n }\n _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n for (const [property, expectedTypes] of Object.entries(configTypes)) {\n const value = config[property];\n const valueType = isElement(value) ? 'element' : toType(value);\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`);\n }\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap base-component.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst VERSION = '5.3.3';\n\n/**\n * Class definition\n */\n\nclass BaseComponent extends Config {\n constructor(element, config) {\n super();\n element = getElement(element);\n if (!element) {\n return;\n }\n this._element = element;\n this._config = this._getConfig(config);\n Data.set(this._element, this.constructor.DATA_KEY, this);\n }\n\n // Public\n dispose() {\n Data.remove(this._element, this.constructor.DATA_KEY);\n EventHandler.off(this._element, this.constructor.EVENT_KEY);\n for (const propertyName of Object.getOwnPropertyNames(this)) {\n this[propertyName] = null;\n }\n }\n _queueCallback(callback, element, isAnimated = true) {\n executeAfterTransition(callback, element, isAnimated);\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config, this._element);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n\n // Static\n static getInstance(element) {\n return Data.get(getElement(element), this.DATA_KEY);\n }\n static getOrCreateInstance(element, config = {}) {\n return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null);\n }\n static get VERSION() {\n return VERSION;\n }\n static get DATA_KEY() {\n return `bs.${this.NAME}`;\n }\n static get EVENT_KEY() {\n return `.${this.DATA_KEY}`;\n }\n static eventName(name) {\n return `${name}${this.EVENT_KEY}`;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/selector-engine.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst getSelector = element => {\n let selector = element.getAttribute('data-bs-target');\n if (!selector || selector === '#') {\n let hrefAttribute = element.getAttribute('href');\n\n // The only valid content that could double as a selector are IDs or classes,\n // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n // `document.querySelector` will rightfully complain it is invalid.\n // See https://github.com/twbs/bootstrap/issues/32273\n if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) {\n return null;\n }\n\n // Just in case some CMS puts out a full URL with the anchor appended\n if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n hrefAttribute = `#${hrefAttribute.split('#')[1]}`;\n }\n selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null;\n }\n return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null;\n};\nconst SelectorEngine = {\n find(selector, element = document.documentElement) {\n return [].concat(...Element.prototype.querySelectorAll.call(element, selector));\n },\n findOne(selector, element = document.documentElement) {\n return Element.prototype.querySelector.call(element, selector);\n },\n children(element, selector) {\n return [].concat(...element.children).filter(child => child.matches(selector));\n },\n parents(element, selector) {\n const parents = [];\n let ancestor = element.parentNode.closest(selector);\n while (ancestor) {\n parents.push(ancestor);\n ancestor = ancestor.parentNode.closest(selector);\n }\n return parents;\n },\n prev(element, selector) {\n let previous = element.previousElementSibling;\n while (previous) {\n if (previous.matches(selector)) {\n return [previous];\n }\n previous = previous.previousElementSibling;\n }\n return [];\n },\n // TODO: this is now unused; remove later along with prev()\n next(element, selector) {\n let next = element.nextElementSibling;\n while (next) {\n if (next.matches(selector)) {\n return [next];\n }\n next = next.nextElementSibling;\n }\n return [];\n },\n focusableChildren(element) {\n const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable=\"true\"]'].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',');\n return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el));\n },\n getSelectorFromElement(element) {\n const selector = getSelector(element);\n if (selector) {\n return SelectorEngine.findOne(selector) ? selector : null;\n }\n return null;\n },\n getElementFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.findOne(selector) : null;\n },\n getMultipleElementsFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.find(selector) : [];\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/component-functions.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst enableDismissTrigger = (component, method = 'hide') => {\n const clickEvent = `click.dismiss${component.EVENT_KEY}`;\n const name = component.NAME;\n EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`);\n const instance = component.getOrCreateInstance(target);\n\n // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n instance[method]();\n });\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$f = 'alert';\nconst DATA_KEY$a = 'bs.alert';\nconst EVENT_KEY$b = `.${DATA_KEY$a}`;\nconst EVENT_CLOSE = `close${EVENT_KEY$b}`;\nconst EVENT_CLOSED = `closed${EVENT_KEY$b}`;\nconst CLASS_NAME_FADE$5 = 'fade';\nconst CLASS_NAME_SHOW$8 = 'show';\n\n/**\n * Class definition\n */\n\nclass Alert extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$f;\n }\n\n // Public\n close() {\n const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE);\n if (closeEvent.defaultPrevented) {\n return;\n }\n this._element.classList.remove(CLASS_NAME_SHOW$8);\n const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$5);\n this._queueCallback(() => this._destroyElement(), this._element, isAnimated);\n }\n\n // Private\n _destroyElement() {\n this._element.remove();\n EventHandler.trigger(this._element, EVENT_CLOSED);\n this.dispose();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Alert.getOrCreateInstance(this);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nenableDismissTrigger(Alert, 'close');\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Alert);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$e = 'button';\nconst DATA_KEY$9 = 'bs.button';\nconst EVENT_KEY$a = `.${DATA_KEY$9}`;\nconst DATA_API_KEY$6 = '.data-api';\nconst CLASS_NAME_ACTIVE$3 = 'active';\nconst SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle=\"button\"]';\nconst EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$a}${DATA_API_KEY$6}`;\n\n/**\n * Class definition\n */\n\nclass Button extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$e;\n }\n\n // Public\n toggle() {\n // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$3));\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Button.getOrCreateInstance(this);\n if (config === 'toggle') {\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$5, event => {\n event.preventDefault();\n const button = event.target.closest(SELECTOR_DATA_TOGGLE$5);\n const data = Button.getOrCreateInstance(button);\n data.toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Button);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/swipe.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$d = 'swipe';\nconst EVENT_KEY$9 = '.bs.swipe';\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`;\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`;\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`;\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`;\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`;\nconst POINTER_TYPE_TOUCH = 'touch';\nconst POINTER_TYPE_PEN = 'pen';\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event';\nconst SWIPE_THRESHOLD = 40;\nconst Default$c = {\n endCallback: null,\n leftCallback: null,\n rightCallback: null\n};\nconst DefaultType$c = {\n endCallback: '(function|null)',\n leftCallback: '(function|null)',\n rightCallback: '(function|null)'\n};\n\n/**\n * Class definition\n */\n\nclass Swipe extends Config {\n constructor(element, config) {\n super();\n this._element = element;\n if (!element || !Swipe.isSupported()) {\n return;\n }\n this._config = this._getConfig(config);\n this._deltaX = 0;\n this._supportPointerEvents = Boolean(window.PointerEvent);\n this._initEvents();\n }\n\n // Getters\n static get Default() {\n return Default$c;\n }\n static get DefaultType() {\n return DefaultType$c;\n }\n static get NAME() {\n return NAME$d;\n }\n\n // Public\n dispose() {\n EventHandler.off(this._element, EVENT_KEY$9);\n }\n\n // Private\n _start(event) {\n if (!this._supportPointerEvents) {\n this._deltaX = event.touches[0].clientX;\n return;\n }\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX;\n }\n }\n _end(event) {\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX - this._deltaX;\n }\n this._handleSwipe();\n execute(this._config.endCallback);\n }\n _move(event) {\n this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX;\n }\n _handleSwipe() {\n const absDeltaX = Math.abs(this._deltaX);\n if (absDeltaX <= SWIPE_THRESHOLD) {\n return;\n }\n const direction = absDeltaX / this._deltaX;\n this._deltaX = 0;\n if (!direction) {\n return;\n }\n execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback);\n }\n _initEvents() {\n if (this._supportPointerEvents) {\n EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event));\n EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event));\n this._element.classList.add(CLASS_NAME_POINTER_EVENT);\n } else {\n EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event));\n EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event));\n EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event));\n }\n }\n _eventIsPointerPenTouch(event) {\n return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH);\n }\n\n // Static\n static isSupported() {\n return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$c = 'carousel';\nconst DATA_KEY$8 = 'bs.carousel';\nconst EVENT_KEY$8 = `.${DATA_KEY$8}`;\nconst DATA_API_KEY$5 = '.data-api';\nconst ARROW_LEFT_KEY$1 = 'ArrowLeft';\nconst ARROW_RIGHT_KEY$1 = 'ArrowRight';\nconst TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch\n\nconst ORDER_NEXT = 'next';\nconst ORDER_PREV = 'prev';\nconst DIRECTION_LEFT = 'left';\nconst DIRECTION_RIGHT = 'right';\nconst EVENT_SLIDE = `slide${EVENT_KEY$8}`;\nconst EVENT_SLID = `slid${EVENT_KEY$8}`;\nconst EVENT_KEYDOWN$1 = `keydown${EVENT_KEY$8}`;\nconst EVENT_MOUSEENTER$1 = `mouseenter${EVENT_KEY$8}`;\nconst EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$8}`;\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY$8}`;\nconst EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst CLASS_NAME_CAROUSEL = 'carousel';\nconst CLASS_NAME_ACTIVE$2 = 'active';\nconst CLASS_NAME_SLIDE = 'slide';\nconst CLASS_NAME_END = 'carousel-item-end';\nconst CLASS_NAME_START = 'carousel-item-start';\nconst CLASS_NAME_NEXT = 'carousel-item-next';\nconst CLASS_NAME_PREV = 'carousel-item-prev';\nconst SELECTOR_ACTIVE = '.active';\nconst SELECTOR_ITEM = '.carousel-item';\nconst SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM;\nconst SELECTOR_ITEM_IMG = '.carousel-item img';\nconst SELECTOR_INDICATORS = '.carousel-indicators';\nconst SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]';\nconst SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]';\nconst KEY_TO_DIRECTION = {\n [ARROW_LEFT_KEY$1]: DIRECTION_RIGHT,\n [ARROW_RIGHT_KEY$1]: DIRECTION_LEFT\n};\nconst Default$b = {\n interval: 5000,\n keyboard: true,\n pause: 'hover',\n ride: false,\n touch: true,\n wrap: true\n};\nconst DefaultType$b = {\n interval: '(number|boolean)',\n // TODO:v6 remove boolean support\n keyboard: 'boolean',\n pause: '(string|boolean)',\n ride: '(boolean|string)',\n touch: 'boolean',\n wrap: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Carousel extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._interval = null;\n this._activeElement = null;\n this._isSliding = false;\n this.touchTimeout = null;\n this._swipeHelper = null;\n this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element);\n this._addEventListeners();\n if (this._config.ride === CLASS_NAME_CAROUSEL) {\n this.cycle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$b;\n }\n static get DefaultType() {\n return DefaultType$b;\n }\n static get NAME() {\n return NAME$c;\n }\n\n // Public\n next() {\n this._slide(ORDER_NEXT);\n }\n nextWhenVisible() {\n // FIXME TODO use `document.visibilityState`\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && isVisible(this._element)) {\n this.next();\n }\n }\n prev() {\n this._slide(ORDER_PREV);\n }\n pause() {\n if (this._isSliding) {\n triggerTransitionEnd(this._element);\n }\n this._clearInterval();\n }\n cycle() {\n this._clearInterval();\n this._updateInterval();\n this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval);\n }\n _maybeEnableCycle() {\n if (!this._config.ride) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.cycle());\n return;\n }\n this.cycle();\n }\n to(index) {\n const items = this._getItems();\n if (index > items.length - 1 || index < 0) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.to(index));\n return;\n }\n const activeIndex = this._getItemIndex(this._getActive());\n if (activeIndex === index) {\n return;\n }\n const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV;\n this._slide(order, items[index]);\n }\n dispose() {\n if (this._swipeHelper) {\n this._swipeHelper.dispose();\n }\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n config.defaultInterval = config.interval;\n return config;\n }\n _addEventListeners() {\n if (this._config.keyboard) {\n EventHandler.on(this._element, EVENT_KEYDOWN$1, event => this._keydown(event));\n }\n if (this._config.pause === 'hover') {\n EventHandler.on(this._element, EVENT_MOUSEENTER$1, () => this.pause());\n EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle());\n }\n if (this._config.touch && Swipe.isSupported()) {\n this._addTouchEventListeners();\n }\n }\n _addTouchEventListeners() {\n for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault());\n }\n const endCallBack = () => {\n if (this._config.pause !== 'hover') {\n return;\n }\n\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause();\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout);\n }\n this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval);\n };\n const swipeConfig = {\n leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n endCallback: endCallBack\n };\n this._swipeHelper = new Swipe(this._element, swipeConfig);\n }\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return;\n }\n const direction = KEY_TO_DIRECTION[event.key];\n if (direction) {\n event.preventDefault();\n this._slide(this._directionToOrder(direction));\n }\n }\n _getItemIndex(element) {\n return this._getItems().indexOf(element);\n }\n _setActiveIndicatorElement(index) {\n if (!this._indicatorsElement) {\n return;\n }\n const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement);\n activeIndicator.classList.remove(CLASS_NAME_ACTIVE$2);\n activeIndicator.removeAttribute('aria-current');\n const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement);\n if (newActiveIndicator) {\n newActiveIndicator.classList.add(CLASS_NAME_ACTIVE$2);\n newActiveIndicator.setAttribute('aria-current', 'true');\n }\n }\n _updateInterval() {\n const element = this._activeElement || this._getActive();\n if (!element) {\n return;\n }\n const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10);\n this._config.interval = elementInterval || this._config.defaultInterval;\n }\n _slide(order, element = null) {\n if (this._isSliding) {\n return;\n }\n const activeElement = this._getActive();\n const isNext = order === ORDER_NEXT;\n const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap);\n if (nextElement === activeElement) {\n return;\n }\n const nextElementIndex = this._getItemIndex(nextElement);\n const triggerEvent = eventName => {\n return EventHandler.trigger(this._element, eventName, {\n relatedTarget: nextElement,\n direction: this._orderToDirection(order),\n from: this._getItemIndex(activeElement),\n to: nextElementIndex\n });\n };\n const slideEvent = triggerEvent(EVENT_SLIDE);\n if (slideEvent.defaultPrevented) {\n return;\n }\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n // TODO: change tests that use empty divs to avoid this check\n return;\n }\n const isCycling = Boolean(this._interval);\n this.pause();\n this._isSliding = true;\n this._setActiveIndicatorElement(nextElementIndex);\n this._activeElement = nextElement;\n const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END;\n const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV;\n nextElement.classList.add(orderClassName);\n reflow(nextElement);\n activeElement.classList.add(directionalClassName);\n nextElement.classList.add(directionalClassName);\n const completeCallBack = () => {\n nextElement.classList.remove(directionalClassName, orderClassName);\n nextElement.classList.add(CLASS_NAME_ACTIVE$2);\n activeElement.classList.remove(CLASS_NAME_ACTIVE$2, orderClassName, directionalClassName);\n this._isSliding = false;\n triggerEvent(EVENT_SLID);\n };\n this._queueCallback(completeCallBack, activeElement, this._isAnimated());\n if (isCycling) {\n this.cycle();\n }\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_SLIDE);\n }\n _getActive() {\n return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element);\n }\n _getItems() {\n return SelectorEngine.find(SELECTOR_ITEM, this._element);\n }\n _clearInterval() {\n if (this._interval) {\n clearInterval(this._interval);\n this._interval = null;\n }\n }\n _directionToOrder(direction) {\n if (isRTL()) {\n return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT;\n }\n return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV;\n }\n _orderToDirection(order) {\n if (isRTL()) {\n return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT;\n }\n return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT;\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Carousel.getOrCreateInstance(this, config);\n if (typeof config === 'number') {\n data.to(config);\n return;\n }\n if (typeof config === 'string') {\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_SLIDE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n return;\n }\n event.preventDefault();\n const carousel = Carousel.getOrCreateInstance(target);\n const slideIndex = this.getAttribute('data-bs-slide-to');\n if (slideIndex) {\n carousel.to(slideIndex);\n carousel._maybeEnableCycle();\n return;\n }\n if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n carousel.next();\n carousel._maybeEnableCycle();\n return;\n }\n carousel.prev();\n carousel._maybeEnableCycle();\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$3, () => {\n const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE);\n for (const carousel of carousels) {\n Carousel.getOrCreateInstance(carousel);\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Carousel);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$b = 'collapse';\nconst DATA_KEY$7 = 'bs.collapse';\nconst EVENT_KEY$7 = `.${DATA_KEY$7}`;\nconst DATA_API_KEY$4 = '.data-api';\nconst EVENT_SHOW$6 = `show${EVENT_KEY$7}`;\nconst EVENT_SHOWN$6 = `shown${EVENT_KEY$7}`;\nconst EVENT_HIDE$6 = `hide${EVENT_KEY$7}`;\nconst EVENT_HIDDEN$6 = `hidden${EVENT_KEY$7}`;\nconst EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$7}${DATA_API_KEY$4}`;\nconst CLASS_NAME_SHOW$7 = 'show';\nconst CLASS_NAME_COLLAPSE = 'collapse';\nconst CLASS_NAME_COLLAPSING = 'collapsing';\nconst CLASS_NAME_COLLAPSED = 'collapsed';\nconst CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`;\nconst CLASS_NAME_HORIZONTAL = 'collapse-horizontal';\nconst WIDTH = 'width';\nconst HEIGHT = 'height';\nconst SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing';\nconst SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle=\"collapse\"]';\nconst Default$a = {\n parent: null,\n toggle: true\n};\nconst DefaultType$a = {\n parent: '(null|element)',\n toggle: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Collapse extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isTransitioning = false;\n this._triggerArray = [];\n const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$4);\n for (const elem of toggleList) {\n const selector = SelectorEngine.getSelectorFromElement(elem);\n const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element);\n if (selector !== null && filterElement.length) {\n this._triggerArray.push(elem);\n }\n }\n this._initializeChildren();\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._triggerArray, this._isShown());\n }\n if (this._config.toggle) {\n this.toggle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$a;\n }\n static get DefaultType() {\n return DefaultType$a;\n }\n static get NAME() {\n return NAME$b;\n }\n\n // Public\n toggle() {\n if (this._isShown()) {\n this.hide();\n } else {\n this.show();\n }\n }\n show() {\n if (this._isTransitioning || this._isShown()) {\n return;\n }\n let activeChildren = [];\n\n // find active children\n if (this._config.parent) {\n activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, {\n toggle: false\n }));\n }\n if (activeChildren.length && activeChildren[0]._isTransitioning) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n for (const activeInstance of activeChildren) {\n activeInstance.hide();\n }\n const dimension = this._getDimension();\n this._element.classList.remove(CLASS_NAME_COLLAPSE);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.style[dimension] = 0;\n this._addAriaAndCollapsedClass(this._triggerArray, true);\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n this._element.style[dimension] = '';\n EventHandler.trigger(this._element, EVENT_SHOWN$6);\n };\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);\n const scrollSize = `scroll${capitalizedDimension}`;\n this._queueCallback(complete, this._element, true);\n this._element.style[dimension] = `${this._element[scrollSize]}px`;\n }\n hide() {\n if (this._isTransitioning || !this._isShown()) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n const dimension = this._getDimension();\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`;\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n for (const trigger of this._triggerArray) {\n const element = SelectorEngine.getElementFromSelector(trigger);\n if (element && !this._isShown(element)) {\n this._addAriaAndCollapsedClass([trigger], false);\n }\n }\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE);\n EventHandler.trigger(this._element, EVENT_HIDDEN$6);\n };\n this._element.style[dimension] = '';\n this._queueCallback(complete, this._element, true);\n }\n _isShown(element = this._element) {\n return element.classList.contains(CLASS_NAME_SHOW$7);\n }\n\n // Private\n _configAfterMerge(config) {\n config.toggle = Boolean(config.toggle); // Coerce string values\n config.parent = getElement(config.parent);\n return config;\n }\n _getDimension() {\n return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT;\n }\n _initializeChildren() {\n if (!this._config.parent) {\n return;\n }\n const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$4);\n for (const element of children) {\n const selected = SelectorEngine.getElementFromSelector(element);\n if (selected) {\n this._addAriaAndCollapsedClass([element], this._isShown(selected));\n }\n }\n }\n _getFirstLevelChildren(selector) {\n const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent);\n // remove children if greater depth\n return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element));\n }\n _addAriaAndCollapsedClass(triggerArray, isOpen) {\n if (!triggerArray.length) {\n return;\n }\n for (const element of triggerArray) {\n element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen);\n element.setAttribute('aria-expanded', isOpen);\n }\n }\n\n // Static\n static jQueryInterface(config) {\n const _config = {};\n if (typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false;\n }\n return this.each(function () {\n const data = Collapse.getOrCreateInstance(this, _config);\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$4, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') {\n event.preventDefault();\n }\n for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n Collapse.getOrCreateInstance(element, {\n toggle: false\n }).toggle();\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Collapse);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$a = 'dropdown';\nconst DATA_KEY$6 = 'bs.dropdown';\nconst EVENT_KEY$6 = `.${DATA_KEY$6}`;\nconst DATA_API_KEY$3 = '.data-api';\nconst ESCAPE_KEY$2 = 'Escape';\nconst TAB_KEY$1 = 'Tab';\nconst ARROW_UP_KEY$1 = 'ArrowUp';\nconst ARROW_DOWN_KEY$1 = 'ArrowDown';\nconst RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button\n\nconst EVENT_HIDE$5 = `hide${EVENT_KEY$6}`;\nconst EVENT_HIDDEN$5 = `hidden${EVENT_KEY$6}`;\nconst EVENT_SHOW$5 = `show${EVENT_KEY$6}`;\nconst EVENT_SHOWN$5 = `shown${EVENT_KEY$6}`;\nconst EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst CLASS_NAME_SHOW$6 = 'show';\nconst CLASS_NAME_DROPUP = 'dropup';\nconst CLASS_NAME_DROPEND = 'dropend';\nconst CLASS_NAME_DROPSTART = 'dropstart';\nconst CLASS_NAME_DROPUP_CENTER = 'dropup-center';\nconst CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center';\nconst SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)';\nconst SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE$3}.${CLASS_NAME_SHOW$6}`;\nconst SELECTOR_MENU = '.dropdown-menu';\nconst SELECTOR_NAVBAR = '.navbar';\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav';\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)';\nconst PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start';\nconst PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end';\nconst PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start';\nconst PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end';\nconst PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start';\nconst PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start';\nconst PLACEMENT_TOPCENTER = 'top';\nconst PLACEMENT_BOTTOMCENTER = 'bottom';\nconst Default$9 = {\n autoClose: true,\n boundary: 'clippingParents',\n display: 'dynamic',\n offset: [0, 2],\n popperConfig: null,\n reference: 'toggle'\n};\nconst DefaultType$9 = {\n autoClose: '(boolean|string)',\n boundary: '(string|element)',\n display: 'string',\n offset: '(array|string|function)',\n popperConfig: '(null|object|function)',\n reference: '(string|element|object)'\n};\n\n/**\n * Class definition\n */\n\nclass Dropdown extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._popper = null;\n this._parent = this._element.parentNode; // dropdown wrapper\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent);\n this._inNavbar = this._detectNavbar();\n }\n\n // Getters\n static get Default() {\n return Default$9;\n }\n static get DefaultType() {\n return DefaultType$9;\n }\n static get NAME() {\n return NAME$a;\n }\n\n // Public\n toggle() {\n return this._isShown() ? this.hide() : this.show();\n }\n show() {\n if (isDisabled(this._element) || this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$5, relatedTarget);\n if (showEvent.defaultPrevented) {\n return;\n }\n this._createPopper();\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n this._element.focus();\n this._element.setAttribute('aria-expanded', true);\n this._menu.classList.add(CLASS_NAME_SHOW$6);\n this._element.classList.add(CLASS_NAME_SHOW$6);\n EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget);\n }\n hide() {\n if (isDisabled(this._element) || !this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n this._completeHide(relatedTarget);\n }\n dispose() {\n if (this._popper) {\n this._popper.destroy();\n }\n super.dispose();\n }\n update() {\n this._inNavbar = this._detectNavbar();\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Private\n _completeHide(relatedTarget) {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget);\n if (hideEvent.defaultPrevented) {\n return;\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n if (this._popper) {\n this._popper.destroy();\n }\n this._menu.classList.remove(CLASS_NAME_SHOW$6);\n this._element.classList.remove(CLASS_NAME_SHOW$6);\n this._element.setAttribute('aria-expanded', 'false');\n Manipulator.removeDataAttribute(this._menu, 'popper');\n EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget);\n }\n _getConfig(config) {\n config = super._getConfig(config);\n if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') {\n // Popper virtual elements require a getBoundingClientRect method\n throw new TypeError(`${NAME$a.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`);\n }\n return config;\n }\n _createPopper() {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org)');\n }\n let referenceElement = this._element;\n if (this._config.reference === 'parent') {\n referenceElement = this._parent;\n } else if (isElement(this._config.reference)) {\n referenceElement = getElement(this._config.reference);\n } else if (typeof this._config.reference === 'object') {\n referenceElement = this._config.reference;\n }\n const popperConfig = this._getPopperConfig();\n this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig);\n }\n _isShown() {\n return this._menu.classList.contains(CLASS_NAME_SHOW$6);\n }\n _getPlacement() {\n const parentDropdown = this._parent;\n if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n return PLACEMENT_RIGHT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n return PLACEMENT_LEFT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n return PLACEMENT_TOPCENTER;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n return PLACEMENT_BOTTOMCENTER;\n }\n\n // We need to trim the value because custom properties can also include spaces\n const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end';\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP;\n }\n return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM;\n }\n _detectNavbar() {\n return this._element.closest(SELECTOR_NAVBAR) !== null;\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _getPopperConfig() {\n const defaultBsPopperConfig = {\n placement: this._getPlacement(),\n modifiers: [{\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }]\n };\n\n // Disable Popper if we have a static display or Dropdown is in Navbar\n if (this._inNavbar || this._config.display === 'static') {\n Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove\n defaultBsPopperConfig.modifiers = [{\n name: 'applyStyles',\n enabled: false\n }];\n }\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _selectMenuItem({\n key,\n target\n }) {\n const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element));\n if (!items.length) {\n return;\n }\n\n // if target isn't included in items (e.g. when expanding the dropdown)\n // allow cycling to get the last item in case key equals ARROW_UP_KEY\n getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Dropdown.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n static clearMenus(event) {\n if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) {\n return;\n }\n const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN);\n for (const toggle of openToggles) {\n const context = Dropdown.getInstance(toggle);\n if (!context || context._config.autoClose === false) {\n continue;\n }\n const composedPath = event.composedPath();\n const isMenuTarget = composedPath.includes(context._menu);\n if (composedPath.includes(context._element) || context._config.autoClose === 'inside' && !isMenuTarget || context._config.autoClose === 'outside' && isMenuTarget) {\n continue;\n }\n\n // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n continue;\n }\n const relatedTarget = {\n relatedTarget: context._element\n };\n if (event.type === 'click') {\n relatedTarget.clickEvent = event;\n }\n context._completeHide(relatedTarget);\n }\n }\n static dataApiKeydownHandler(event) {\n // If not an UP | DOWN | ESCAPE key => not a dropdown command\n // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n const isInput = /input|textarea/i.test(event.target.tagName);\n const isEscapeEvent = event.key === ESCAPE_KEY$2;\n const isUpOrDownEvent = [ARROW_UP_KEY$1, ARROW_DOWN_KEY$1].includes(event.key);\n if (!isUpOrDownEvent && !isEscapeEvent) {\n return;\n }\n if (isInput && !isEscapeEvent) {\n return;\n }\n event.preventDefault();\n\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$3) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$3, event.delegateTarget.parentNode);\n const instance = Dropdown.getOrCreateInstance(getToggleButton);\n if (isUpOrDownEvent) {\n event.stopPropagation();\n instance.show();\n instance._selectMenuItem(event);\n return;\n }\n if (instance._isShown()) {\n // else is escape and we check if it is shown\n event.stopPropagation();\n instance.hide();\n getToggleButton.focus();\n }\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$3, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$3, function (event) {\n event.preventDefault();\n Dropdown.getOrCreateInstance(this).toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Dropdown);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/backdrop.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$9 = 'backdrop';\nconst CLASS_NAME_FADE$4 = 'fade';\nconst CLASS_NAME_SHOW$5 = 'show';\nconst EVENT_MOUSEDOWN = `mousedown.bs.${NAME$9}`;\nconst Default$8 = {\n className: 'modal-backdrop',\n clickCallback: null,\n isAnimated: false,\n isVisible: true,\n // if false, we use the backdrop helper without adding any element to the dom\n rootElement: 'body' // give the choice to place backdrop under different elements\n};\nconst DefaultType$8 = {\n className: 'string',\n clickCallback: '(function|null)',\n isAnimated: 'boolean',\n isVisible: 'boolean',\n rootElement: '(element|string)'\n};\n\n/**\n * Class definition\n */\n\nclass Backdrop extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isAppended = false;\n this._element = null;\n }\n\n // Getters\n static get Default() {\n return Default$8;\n }\n static get DefaultType() {\n return DefaultType$8;\n }\n static get NAME() {\n return NAME$9;\n }\n\n // Public\n show(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._append();\n const element = this._getElement();\n if (this._config.isAnimated) {\n reflow(element);\n }\n element.classList.add(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n execute(callback);\n });\n }\n hide(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._getElement().classList.remove(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n this.dispose();\n execute(callback);\n });\n }\n dispose() {\n if (!this._isAppended) {\n return;\n }\n EventHandler.off(this._element, EVENT_MOUSEDOWN);\n this._element.remove();\n this._isAppended = false;\n }\n\n // Private\n _getElement() {\n if (!this._element) {\n const backdrop = document.createElement('div');\n backdrop.className = this._config.className;\n if (this._config.isAnimated) {\n backdrop.classList.add(CLASS_NAME_FADE$4);\n }\n this._element = backdrop;\n }\n return this._element;\n }\n _configAfterMerge(config) {\n // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n config.rootElement = getElement(config.rootElement);\n return config;\n }\n _append() {\n if (this._isAppended) {\n return;\n }\n const element = this._getElement();\n this._config.rootElement.append(element);\n EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n execute(this._config.clickCallback);\n });\n this._isAppended = true;\n }\n _emulateAnimation(callback) {\n executeAfterTransition(callback, this._getElement(), this._config.isAnimated);\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/focustrap.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$8 = 'focustrap';\nconst DATA_KEY$5 = 'bs.focustrap';\nconst EVENT_KEY$5 = `.${DATA_KEY$5}`;\nconst EVENT_FOCUSIN$2 = `focusin${EVENT_KEY$5}`;\nconst EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY$5}`;\nconst TAB_KEY = 'Tab';\nconst TAB_NAV_FORWARD = 'forward';\nconst TAB_NAV_BACKWARD = 'backward';\nconst Default$7 = {\n autofocus: true,\n trapElement: null // The element to trap focus inside of\n};\nconst DefaultType$7 = {\n autofocus: 'boolean',\n trapElement: 'element'\n};\n\n/**\n * Class definition\n */\n\nclass FocusTrap extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isActive = false;\n this._lastTabNavDirection = null;\n }\n\n // Getters\n static get Default() {\n return Default$7;\n }\n static get DefaultType() {\n return DefaultType$7;\n }\n static get NAME() {\n return NAME$8;\n }\n\n // Public\n activate() {\n if (this._isActive) {\n return;\n }\n if (this._config.autofocus) {\n this._config.trapElement.focus();\n }\n EventHandler.off(document, EVENT_KEY$5); // guard against infinite focus loop\n EventHandler.on(document, EVENT_FOCUSIN$2, event => this._handleFocusin(event));\n EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event));\n this._isActive = true;\n }\n deactivate() {\n if (!this._isActive) {\n return;\n }\n this._isActive = false;\n EventHandler.off(document, EVENT_KEY$5);\n }\n\n // Private\n _handleFocusin(event) {\n const {\n trapElement\n } = this._config;\n if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n return;\n }\n const elements = SelectorEngine.focusableChildren(trapElement);\n if (elements.length === 0) {\n trapElement.focus();\n } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n elements[elements.length - 1].focus();\n } else {\n elements[0].focus();\n }\n }\n _handleKeydown(event) {\n if (event.key !== TAB_KEY) {\n return;\n }\n this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/scrollBar.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top';\nconst SELECTOR_STICKY_CONTENT = '.sticky-top';\nconst PROPERTY_PADDING = 'padding-right';\nconst PROPERTY_MARGIN = 'margin-right';\n\n/**\n * Class definition\n */\n\nclass ScrollBarHelper {\n constructor() {\n this._element = document.body;\n }\n\n // Public\n getWidth() {\n // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n const documentWidth = document.documentElement.clientWidth;\n return Math.abs(window.innerWidth - documentWidth);\n }\n hide() {\n const width = this.getWidth();\n this._disableOverFlow();\n // give padding to element to balance the hidden scrollbar width\n this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width);\n }\n reset() {\n this._resetElementAttributes(this._element, 'overflow');\n this._resetElementAttributes(this._element, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN);\n }\n isOverflowing() {\n return this.getWidth() > 0;\n }\n\n // Private\n _disableOverFlow() {\n this._saveInitialAttribute(this._element, 'overflow');\n this._element.style.overflow = 'hidden';\n }\n _setElementAttributes(selector, styleProperty, callback) {\n const scrollbarWidth = this.getWidth();\n const manipulationCallBack = element => {\n if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n return;\n }\n this._saveInitialAttribute(element, styleProperty);\n const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty);\n element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _saveInitialAttribute(element, styleProperty) {\n const actualValue = element.style.getPropertyValue(styleProperty);\n if (actualValue) {\n Manipulator.setDataAttribute(element, styleProperty, actualValue);\n }\n }\n _resetElementAttributes(selector, styleProperty) {\n const manipulationCallBack = element => {\n const value = Manipulator.getDataAttribute(element, styleProperty);\n // We only want to remove the property if the value is `null`; the value can also be zero\n if (value === null) {\n element.style.removeProperty(styleProperty);\n return;\n }\n Manipulator.removeDataAttribute(element, styleProperty);\n element.style.setProperty(styleProperty, value);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _applyManipulationCallback(selector, callBack) {\n if (isElement(selector)) {\n callBack(selector);\n return;\n }\n for (const sel of SelectorEngine.find(selector, this._element)) {\n callBack(sel);\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$7 = 'modal';\nconst DATA_KEY$4 = 'bs.modal';\nconst EVENT_KEY$4 = `.${DATA_KEY$4}`;\nconst DATA_API_KEY$2 = '.data-api';\nconst ESCAPE_KEY$1 = 'Escape';\nconst EVENT_HIDE$4 = `hide${EVENT_KEY$4}`;\nconst EVENT_HIDE_PREVENTED$1 = `hidePrevented${EVENT_KEY$4}`;\nconst EVENT_HIDDEN$4 = `hidden${EVENT_KEY$4}`;\nconst EVENT_SHOW$4 = `show${EVENT_KEY$4}`;\nconst EVENT_SHOWN$4 = `shown${EVENT_KEY$4}`;\nconst EVENT_RESIZE$1 = `resize${EVENT_KEY$4}`;\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY$4}`;\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY$4}`;\nconst EVENT_KEYDOWN_DISMISS$1 = `keydown.dismiss${EVENT_KEY$4}`;\nconst EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$4}${DATA_API_KEY$2}`;\nconst CLASS_NAME_OPEN = 'modal-open';\nconst CLASS_NAME_FADE$3 = 'fade';\nconst CLASS_NAME_SHOW$4 = 'show';\nconst CLASS_NAME_STATIC = 'modal-static';\nconst OPEN_SELECTOR$1 = '.modal.show';\nconst SELECTOR_DIALOG = '.modal-dialog';\nconst SELECTOR_MODAL_BODY = '.modal-body';\nconst SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle=\"modal\"]';\nconst Default$6 = {\n backdrop: true,\n focus: true,\n keyboard: true\n};\nconst DefaultType$6 = {\n backdrop: '(boolean|string)',\n focus: 'boolean',\n keyboard: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Modal extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._isShown = false;\n this._isTransitioning = false;\n this._scrollBar = new ScrollBarHelper();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$6;\n }\n static get DefaultType() {\n return DefaultType$6;\n }\n static get NAME() {\n return NAME$7;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._isTransitioning = true;\n this._scrollBar.hide();\n document.body.classList.add(CLASS_NAME_OPEN);\n this._adjustDialog();\n this._backdrop.show(() => this._showElement(relatedTarget));\n }\n hide() {\n if (!this._isShown || this._isTransitioning) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$4);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._isShown = false;\n this._isTransitioning = true;\n this._focustrap.deactivate();\n this._element.classList.remove(CLASS_NAME_SHOW$4);\n this._queueCallback(() => this._hideModal(), this._element, this._isAnimated());\n }\n dispose() {\n EventHandler.off(window, EVENT_KEY$4);\n EventHandler.off(this._dialog, EVENT_KEY$4);\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n handleUpdate() {\n this._adjustDialog();\n }\n\n // Private\n _initializeBackDrop() {\n return new Backdrop({\n isVisible: Boolean(this._config.backdrop),\n // 'static' option will be translated to true, and booleans will keep their value,\n isAnimated: this._isAnimated()\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _showElement(relatedTarget) {\n // try to append dynamic modal\n if (!document.body.contains(this._element)) {\n document.body.append(this._element);\n }\n this._element.style.display = 'block';\n this._element.removeAttribute('aria-hidden');\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.scrollTop = 0;\n const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog);\n if (modalBody) {\n modalBody.scrollTop = 0;\n }\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_SHOW$4);\n const transitionComplete = () => {\n if (this._config.focus) {\n this._focustrap.activate();\n }\n this._isTransitioning = false;\n EventHandler.trigger(this._element, EVENT_SHOWN$4, {\n relatedTarget\n });\n };\n this._queueCallback(transitionComplete, this._dialog, this._isAnimated());\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS$1, event => {\n if (event.key !== ESCAPE_KEY$1) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n this._triggerBackdropTransition();\n });\n EventHandler.on(window, EVENT_RESIZE$1, () => {\n if (this._isShown && !this._isTransitioning) {\n this._adjustDialog();\n }\n });\n EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n if (this._element !== event.target || this._element !== event2.target) {\n return;\n }\n if (this._config.backdrop === 'static') {\n this._triggerBackdropTransition();\n return;\n }\n if (this._config.backdrop) {\n this.hide();\n }\n });\n });\n }\n _hideModal() {\n this._element.style.display = 'none';\n this._element.setAttribute('aria-hidden', true);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n this._isTransitioning = false;\n this._backdrop.hide(() => {\n document.body.classList.remove(CLASS_NAME_OPEN);\n this._resetAdjustments();\n this._scrollBar.reset();\n EventHandler.trigger(this._element, EVENT_HIDDEN$4);\n });\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_FADE$3);\n }\n _triggerBackdropTransition() {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED$1);\n if (hideEvent.defaultPrevented) {\n return;\n }\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const initialOverflowY = this._element.style.overflowY;\n // return if the following background transition hasn't yet completed\n if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n return;\n }\n if (!isModalOverflowing) {\n this._element.style.overflowY = 'hidden';\n }\n this._element.classList.add(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.classList.remove(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.style.overflowY = initialOverflowY;\n }, this._dialog);\n }, this._dialog);\n this._element.focus();\n }\n\n /**\n * The following methods are used to handle overflowing modals\n */\n\n _adjustDialog() {\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const scrollbarWidth = this._scrollBar.getWidth();\n const isBodyOverflowing = scrollbarWidth > 0;\n if (isBodyOverflowing && !isModalOverflowing) {\n const property = isRTL() ? 'paddingLeft' : 'paddingRight';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n if (!isBodyOverflowing && isModalOverflowing) {\n const property = isRTL() ? 'paddingRight' : 'paddingLeft';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n }\n _resetAdjustments() {\n this._element.style.paddingLeft = '';\n this._element.style.paddingRight = '';\n }\n\n // Static\n static jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n const data = Modal.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](relatedTarget);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$2, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n EventHandler.one(target, EVENT_SHOW$4, showEvent => {\n if (showEvent.defaultPrevented) {\n // only register focus restorer if modal will actually get shown\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$4, () => {\n if (isVisible(this)) {\n this.focus();\n }\n });\n });\n\n // avoid conflict when clicking modal toggler while another one is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR$1);\n if (alreadyOpen) {\n Modal.getInstance(alreadyOpen).hide();\n }\n const data = Modal.getOrCreateInstance(target);\n data.toggle(this);\n});\nenableDismissTrigger(Modal);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Modal);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap offcanvas.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$6 = 'offcanvas';\nconst DATA_KEY$3 = 'bs.offcanvas';\nconst EVENT_KEY$3 = `.${DATA_KEY$3}`;\nconst DATA_API_KEY$1 = '.data-api';\nconst EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst ESCAPE_KEY = 'Escape';\nconst CLASS_NAME_SHOW$3 = 'show';\nconst CLASS_NAME_SHOWING$1 = 'showing';\nconst CLASS_NAME_HIDING = 'hiding';\nconst CLASS_NAME_BACKDROP = 'offcanvas-backdrop';\nconst OPEN_SELECTOR = '.offcanvas.show';\nconst EVENT_SHOW$3 = `show${EVENT_KEY$3}`;\nconst EVENT_SHOWN$3 = `shown${EVENT_KEY$3}`;\nconst EVENT_HIDE$3 = `hide${EVENT_KEY$3}`;\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY$3}`;\nconst EVENT_HIDDEN$3 = `hidden${EVENT_KEY$3}`;\nconst EVENT_RESIZE = `resize${EVENT_KEY$3}`;\nconst EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY$3}`;\nconst SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle=\"offcanvas\"]';\nconst Default$5 = {\n backdrop: true,\n keyboard: true,\n scroll: false\n};\nconst DefaultType$5 = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n scroll: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Offcanvas extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isShown = false;\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$5;\n }\n static get DefaultType() {\n return DefaultType$5;\n }\n static get NAME() {\n return NAME$6;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$3, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._backdrop.show();\n if (!this._config.scroll) {\n new ScrollBarHelper().hide();\n }\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.classList.add(CLASS_NAME_SHOWING$1);\n const completeCallBack = () => {\n if (!this._config.scroll || this._config.backdrop) {\n this._focustrap.activate();\n }\n this._element.classList.add(CLASS_NAME_SHOW$3);\n this._element.classList.remove(CLASS_NAME_SHOWING$1);\n EventHandler.trigger(this._element, EVENT_SHOWN$3, {\n relatedTarget\n });\n };\n this._queueCallback(completeCallBack, this._element, true);\n }\n hide() {\n if (!this._isShown) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._focustrap.deactivate();\n this._element.blur();\n this._isShown = false;\n this._element.classList.add(CLASS_NAME_HIDING);\n this._backdrop.hide();\n const completeCallback = () => {\n this._element.classList.remove(CLASS_NAME_SHOW$3, CLASS_NAME_HIDING);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n if (!this._config.scroll) {\n new ScrollBarHelper().reset();\n }\n EventHandler.trigger(this._element, EVENT_HIDDEN$3);\n };\n this._queueCallback(completeCallback, this._element, true);\n }\n dispose() {\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n\n // Private\n _initializeBackDrop() {\n const clickCallback = () => {\n if (this._config.backdrop === 'static') {\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n return;\n }\n this.hide();\n };\n\n // 'static' option will be translated to true, and booleans will keep their value\n const isVisible = Boolean(this._config.backdrop);\n return new Backdrop({\n className: CLASS_NAME_BACKDROP,\n isVisible,\n isAnimated: true,\n rootElement: this._element.parentNode,\n clickCallback: isVisible ? clickCallback : null\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n });\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Offcanvas.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$1, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$3, () => {\n // focus on trigger when it is closed\n if (isVisible(this)) {\n this.focus();\n }\n });\n\n // avoid conflict when clicking a toggler of an offcanvas, while another is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);\n if (alreadyOpen && alreadyOpen !== target) {\n Offcanvas.getInstance(alreadyOpen).hide();\n }\n const data = Offcanvas.getOrCreateInstance(target);\n data.toggle(this);\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$2, () => {\n for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n Offcanvas.getOrCreateInstance(selector).show();\n }\n});\nEventHandler.on(window, EVENT_RESIZE, () => {\n for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n if (getComputedStyle(element).position !== 'fixed') {\n Offcanvas.getOrCreateInstance(element).hide();\n }\n }\n});\nenableDismissTrigger(Offcanvas);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Offcanvas);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n// js-docs-start allow-list\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i;\nconst DefaultAllowlist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n dd: [],\n div: [],\n dl: [],\n dt: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n};\n// js-docs-end allow-list\n\nconst uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']);\n\n/**\n * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n * contexts.\n *\n * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n */\n// eslint-disable-next-line unicorn/better-regex\nconst SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;\nconst allowedAttribute = (attribute, allowedAttributeList) => {\n const attributeName = attribute.nodeName.toLowerCase();\n if (allowedAttributeList.includes(attributeName)) {\n if (uriAttributes.has(attributeName)) {\n return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue));\n }\n return true;\n }\n\n // Check if a regular expression validates the attribute.\n return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName));\n};\nfunction sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n if (!unsafeHtml.length) {\n return unsafeHtml;\n }\n if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n return sanitizeFunction(unsafeHtml);\n }\n const domParser = new window.DOMParser();\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');\n const elements = [].concat(...createdDocument.body.querySelectorAll('*'));\n for (const element of elements) {\n const elementName = element.nodeName.toLowerCase();\n if (!Object.keys(allowList).includes(elementName)) {\n element.remove();\n continue;\n }\n const attributeList = [].concat(...element.attributes);\n const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []);\n for (const attribute of attributeList) {\n if (!allowedAttribute(attribute, allowedAttributes)) {\n element.removeAttribute(attribute.nodeName);\n }\n }\n }\n return createdDocument.body.innerHTML;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/template-factory.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$5 = 'TemplateFactory';\nconst Default$4 = {\n allowList: DefaultAllowlist,\n content: {},\n // { selector : text , selector2 : text2 , }\n extraClass: '',\n html: false,\n sanitize: true,\n sanitizeFn: null,\n template: '
'\n};\nconst DefaultType$4 = {\n allowList: 'object',\n content: 'object',\n extraClass: '(string|function)',\n html: 'boolean',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n template: 'string'\n};\nconst DefaultContentType = {\n entry: '(string|element|function|null)',\n selector: '(string|element)'\n};\n\n/**\n * Class definition\n */\n\nclass TemplateFactory extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n }\n\n // Getters\n static get Default() {\n return Default$4;\n }\n static get DefaultType() {\n return DefaultType$4;\n }\n static get NAME() {\n return NAME$5;\n }\n\n // Public\n getContent() {\n return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean);\n }\n hasContent() {\n return this.getContent().length > 0;\n }\n changeContent(content) {\n this._checkContent(content);\n this._config.content = {\n ...this._config.content,\n ...content\n };\n return this;\n }\n toHtml() {\n const templateWrapper = document.createElement('div');\n templateWrapper.innerHTML = this._maybeSanitize(this._config.template);\n for (const [selector, text] of Object.entries(this._config.content)) {\n this._setContent(templateWrapper, text, selector);\n }\n const template = templateWrapper.children[0];\n const extraClass = this._resolvePossibleFunction(this._config.extraClass);\n if (extraClass) {\n template.classList.add(...extraClass.split(' '));\n }\n return template;\n }\n\n // Private\n _typeCheckConfig(config) {\n super._typeCheckConfig(config);\n this._checkContent(config.content);\n }\n _checkContent(arg) {\n for (const [selector, content] of Object.entries(arg)) {\n super._typeCheckConfig({\n selector,\n entry: content\n }, DefaultContentType);\n }\n }\n _setContent(template, content, selector) {\n const templateElement = SelectorEngine.findOne(selector, template);\n if (!templateElement) {\n return;\n }\n content = this._resolvePossibleFunction(content);\n if (!content) {\n templateElement.remove();\n return;\n }\n if (isElement(content)) {\n this._putElementInTemplate(getElement(content), templateElement);\n return;\n }\n if (this._config.html) {\n templateElement.innerHTML = this._maybeSanitize(content);\n return;\n }\n templateElement.textContent = content;\n }\n _maybeSanitize(arg) {\n return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this]);\n }\n _putElementInTemplate(element, templateElement) {\n if (this._config.html) {\n templateElement.innerHTML = '';\n templateElement.append(element);\n return;\n }\n templateElement.textContent = element.textContent;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$4 = 'tooltip';\nconst DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']);\nconst CLASS_NAME_FADE$2 = 'fade';\nconst CLASS_NAME_MODAL = 'modal';\nconst CLASS_NAME_SHOW$2 = 'show';\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner';\nconst SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;\nconst EVENT_MODAL_HIDE = 'hide.bs.modal';\nconst TRIGGER_HOVER = 'hover';\nconst TRIGGER_FOCUS = 'focus';\nconst TRIGGER_CLICK = 'click';\nconst TRIGGER_MANUAL = 'manual';\nconst EVENT_HIDE$2 = 'hide';\nconst EVENT_HIDDEN$2 = 'hidden';\nconst EVENT_SHOW$2 = 'show';\nconst EVENT_SHOWN$2 = 'shown';\nconst EVENT_INSERTED = 'inserted';\nconst EVENT_CLICK$1 = 'click';\nconst EVENT_FOCUSIN$1 = 'focusin';\nconst EVENT_FOCUSOUT$1 = 'focusout';\nconst EVENT_MOUSEENTER = 'mouseenter';\nconst EVENT_MOUSELEAVE = 'mouseleave';\nconst AttachmentMap = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: isRTL() ? 'left' : 'right',\n BOTTOM: 'bottom',\n LEFT: isRTL() ? 'right' : 'left'\n};\nconst Default$3 = {\n allowList: DefaultAllowlist,\n animation: true,\n boundary: 'clippingParents',\n container: false,\n customClass: '',\n delay: 0,\n fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n html: false,\n offset: [0, 6],\n placement: 'top',\n popperConfig: null,\n sanitize: true,\n sanitizeFn: null,\n selector: false,\n template: '
' + '
' + '
' + '
',\n title: '',\n trigger: 'hover focus'\n};\nconst DefaultType$3 = {\n allowList: 'object',\n animation: 'boolean',\n boundary: '(string|element)',\n container: '(string|element|boolean)',\n customClass: '(string|function)',\n delay: '(number|object)',\n fallbackPlacements: 'array',\n html: 'boolean',\n offset: '(array|string|function)',\n placement: '(string|function)',\n popperConfig: '(null|object|function)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n selector: '(string|boolean)',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string'\n};\n\n/**\n * Class definition\n */\n\nclass Tooltip extends BaseComponent {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org)');\n }\n super(element, config);\n\n // Private\n this._isEnabled = true;\n this._timeout = 0;\n this._isHovered = null;\n this._activeTrigger = {};\n this._popper = null;\n this._templateFactory = null;\n this._newContent = null;\n\n // Protected\n this.tip = null;\n this._setListeners();\n if (!this._config.selector) {\n this._fixTitle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$3;\n }\n static get DefaultType() {\n return DefaultType$3;\n }\n static get NAME() {\n return NAME$4;\n }\n\n // Public\n enable() {\n this._isEnabled = true;\n }\n disable() {\n this._isEnabled = false;\n }\n toggleEnabled() {\n this._isEnabled = !this._isEnabled;\n }\n toggle() {\n if (!this._isEnabled) {\n return;\n }\n this._activeTrigger.click = !this._activeTrigger.click;\n if (this._isShown()) {\n this._leave();\n return;\n }\n this._enter();\n }\n dispose() {\n clearTimeout(this._timeout);\n EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n if (this._element.getAttribute('data-bs-original-title')) {\n this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'));\n }\n this._disposePopper();\n super.dispose();\n }\n show() {\n if (this._element.style.display === 'none') {\n throw new Error('Please use show on visible elements');\n }\n if (!(this._isWithContent() && this._isEnabled)) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2));\n const shadowRoot = findShadowRoot(this._element);\n const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element);\n if (showEvent.defaultPrevented || !isInTheDom) {\n return;\n }\n\n // TODO: v6 remove this or make it optional\n this._disposePopper();\n const tip = this._getTipElement();\n this._element.setAttribute('aria-describedby', tip.getAttribute('id'));\n const {\n container\n } = this._config;\n if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n container.append(tip);\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));\n }\n this._popper = this._createPopper(tip);\n tip.classList.add(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n const complete = () => {\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2));\n if (this._isHovered === false) {\n this._leave();\n }\n this._isHovered = false;\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n hide() {\n if (!this._isShown()) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2));\n if (hideEvent.defaultPrevented) {\n return;\n }\n const tip = this._getTipElement();\n tip.classList.remove(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n this._activeTrigger[TRIGGER_CLICK] = false;\n this._activeTrigger[TRIGGER_FOCUS] = false;\n this._activeTrigger[TRIGGER_HOVER] = false;\n this._isHovered = null; // it is a trick to support manual triggering\n\n const complete = () => {\n if (this._isWithActiveTrigger()) {\n return;\n }\n if (!this._isHovered) {\n this._disposePopper();\n }\n this._element.removeAttribute('aria-describedby');\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2));\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n update() {\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Protected\n _isWithContent() {\n return Boolean(this._getTitle());\n }\n _getTipElement() {\n if (!this.tip) {\n this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());\n }\n return this.tip;\n }\n _createTipElement(content) {\n const tip = this._getTemplateFactory(content).toHtml();\n\n // TODO: remove this check in v6\n if (!tip) {\n return null;\n }\n tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2);\n // TODO: v6 the following can be achieved with CSS only\n tip.classList.add(`bs-${this.constructor.NAME}-auto`);\n const tipId = getUID(this.constructor.NAME).toString();\n tip.setAttribute('id', tipId);\n if (this._isAnimated()) {\n tip.classList.add(CLASS_NAME_FADE$2);\n }\n return tip;\n }\n setContent(content) {\n this._newContent = content;\n if (this._isShown()) {\n this._disposePopper();\n this.show();\n }\n }\n _getTemplateFactory(content) {\n if (this._templateFactory) {\n this._templateFactory.changeContent(content);\n } else {\n this._templateFactory = new TemplateFactory({\n ...this._config,\n // the `content` var has to be after `this._config`\n // to override config.content in case of popover\n content,\n extraClass: this._resolvePossibleFunction(this._config.customClass)\n });\n }\n return this._templateFactory;\n }\n _getContentForTemplate() {\n return {\n [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n };\n }\n _getTitle() {\n return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title');\n }\n\n // Private\n _initializeOnDelegatedTarget(event) {\n return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());\n }\n _isAnimated() {\n return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2);\n }\n _isShown() {\n return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2);\n }\n _createPopper(tip) {\n const placement = execute(this._config.placement, [this, tip, this._element]);\n const attachment = AttachmentMap[placement.toUpperCase()];\n return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment));\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this._element]);\n }\n _getPopperConfig(attachment) {\n const defaultBsPopperConfig = {\n placement: attachment,\n modifiers: [{\n name: 'flip',\n options: {\n fallbackPlacements: this._config.fallbackPlacements\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }, {\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'arrow',\n options: {\n element: `.${this.constructor.NAME}-arrow`\n }\n }, {\n name: 'preSetPlacement',\n enabled: true,\n phase: 'beforeMain',\n fn: data => {\n // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n this._getTipElement().setAttribute('data-popper-placement', data.state.placement);\n }\n }]\n };\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _setListeners() {\n const triggers = this._config.trigger.split(' ');\n for (const trigger of triggers) {\n if (trigger === 'click') {\n EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context.toggle();\n });\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN$1);\n const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1);\n EventHandler.on(this._element, eventIn, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true;\n context._enter();\n });\n EventHandler.on(this._element, eventOut, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget);\n context._leave();\n });\n }\n }\n this._hideModalHandler = () => {\n if (this._element) {\n this.hide();\n }\n };\n EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n }\n _fixTitle() {\n const title = this._element.getAttribute('title');\n if (!title) {\n return;\n }\n if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n this._element.setAttribute('aria-label', title);\n }\n this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility\n this._element.removeAttribute('title');\n }\n _enter() {\n if (this._isShown() || this._isHovered) {\n this._isHovered = true;\n return;\n }\n this._isHovered = true;\n this._setTimeout(() => {\n if (this._isHovered) {\n this.show();\n }\n }, this._config.delay.show);\n }\n _leave() {\n if (this._isWithActiveTrigger()) {\n return;\n }\n this._isHovered = false;\n this._setTimeout(() => {\n if (!this._isHovered) {\n this.hide();\n }\n }, this._config.delay.hide);\n }\n _setTimeout(handler, timeout) {\n clearTimeout(this._timeout);\n this._timeout = setTimeout(handler, timeout);\n }\n _isWithActiveTrigger() {\n return Object.values(this._activeTrigger).includes(true);\n }\n _getConfig(config) {\n const dataAttributes = Manipulator.getDataAttributes(this._element);\n for (const dataAttribute of Object.keys(dataAttributes)) {\n if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n delete dataAttributes[dataAttribute];\n }\n }\n config = {\n ...dataAttributes,\n ...(typeof config === 'object' && config ? config : {})\n };\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n config.container = config.container === false ? document.body : getElement(config.container);\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n };\n }\n if (typeof config.title === 'number') {\n config.title = config.title.toString();\n }\n if (typeof config.content === 'number') {\n config.content = config.content.toString();\n }\n return config;\n }\n _getDelegateConfig() {\n const config = {};\n for (const [key, value] of Object.entries(this._config)) {\n if (this.constructor.Default[key] !== value) {\n config[key] = value;\n }\n }\n config.selector = false;\n config.trigger = 'manual';\n\n // In the future can be replaced with:\n // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n // `Object.fromEntries(keysWithDifferentValues)`\n return config;\n }\n _disposePopper() {\n if (this._popper) {\n this._popper.destroy();\n this._popper = null;\n }\n if (this.tip) {\n this.tip.remove();\n this.tip = null;\n }\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Tooltip.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Tooltip);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$3 = 'popover';\nconst SELECTOR_TITLE = '.popover-header';\nconst SELECTOR_CONTENT = '.popover-body';\nconst Default$2 = {\n ...Tooltip.Default,\n content: '',\n offset: [0, 8],\n placement: 'right',\n template: '
' + '
' + '

' + '
' + '
',\n trigger: 'click'\n};\nconst DefaultType$2 = {\n ...Tooltip.DefaultType,\n content: '(null|string|element|function)'\n};\n\n/**\n * Class definition\n */\n\nclass Popover extends Tooltip {\n // Getters\n static get Default() {\n return Default$2;\n }\n static get DefaultType() {\n return DefaultType$2;\n }\n static get NAME() {\n return NAME$3;\n }\n\n // Overrides\n _isWithContent() {\n return this._getTitle() || this._getContent();\n }\n\n // Private\n _getContentForTemplate() {\n return {\n [SELECTOR_TITLE]: this._getTitle(),\n [SELECTOR_CONTENT]: this._getContent()\n };\n }\n _getContent() {\n return this._resolvePossibleFunction(this._config.content);\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Popover.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Popover);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$2 = 'scrollspy';\nconst DATA_KEY$2 = 'bs.scrollspy';\nconst EVENT_KEY$2 = `.${DATA_KEY$2}`;\nconst DATA_API_KEY = '.data-api';\nconst EVENT_ACTIVATE = `activate${EVENT_KEY$2}`;\nconst EVENT_CLICK = `click${EVENT_KEY$2}`;\nconst EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$2}${DATA_API_KEY}`;\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';\nconst CLASS_NAME_ACTIVE$1 = 'active';\nconst SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]';\nconst SELECTOR_TARGET_LINKS = '[href]';\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';\nconst SELECTOR_NAV_LINKS = '.nav-link';\nconst SELECTOR_NAV_ITEMS = '.nav-item';\nconst SELECTOR_LIST_ITEMS = '.list-group-item';\nconst SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;\nconst SELECTOR_DROPDOWN = '.dropdown';\nconst SELECTOR_DROPDOWN_TOGGLE$1 = '.dropdown-toggle';\nconst Default$1 = {\n offset: null,\n // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: '0px 0px -25%',\n smoothScroll: false,\n target: null,\n threshold: [0.1, 0.5, 1]\n};\nconst DefaultType$1 = {\n offset: '(number|null)',\n // TODO v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: 'string',\n smoothScroll: 'boolean',\n target: 'element',\n threshold: 'array'\n};\n\n/**\n * Class definition\n */\n\nclass ScrollSpy extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n\n // this._element is the observablesContainer and config.target the menu links wrapper\n this._targetLinks = new Map();\n this._observableSections = new Map();\n this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;\n this._activeTarget = null;\n this._observer = null;\n this._previousScrollData = {\n visibleEntryTop: 0,\n parentScrollTop: 0\n };\n this.refresh(); // initialize\n }\n\n // Getters\n static get Default() {\n return Default$1;\n }\n static get DefaultType() {\n return DefaultType$1;\n }\n static get NAME() {\n return NAME$2;\n }\n\n // Public\n refresh() {\n this._initializeTargetsAndObservables();\n this._maybeEnableSmoothScroll();\n if (this._observer) {\n this._observer.disconnect();\n } else {\n this._observer = this._getNewObserver();\n }\n for (const section of this._observableSections.values()) {\n this._observer.observe(section);\n }\n }\n dispose() {\n this._observer.disconnect();\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n config.target = getElement(config.target) || document.body;\n\n // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin;\n if (typeof config.threshold === 'string') {\n config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value));\n }\n return config;\n }\n _maybeEnableSmoothScroll() {\n if (!this._config.smoothScroll) {\n return;\n }\n\n // unregister any previous listeners\n EventHandler.off(this._config.target, EVENT_CLICK);\n EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n const observableSection = this._observableSections.get(event.target.hash);\n if (observableSection) {\n event.preventDefault();\n const root = this._rootElement || window;\n const height = observableSection.offsetTop - this._element.offsetTop;\n if (root.scrollTo) {\n root.scrollTo({\n top: height,\n behavior: 'smooth'\n });\n return;\n }\n\n // Chrome 60 doesn't support `scrollTo`\n root.scrollTop = height;\n }\n });\n }\n _getNewObserver() {\n const options = {\n root: this._rootElement,\n threshold: this._config.threshold,\n rootMargin: this._config.rootMargin\n };\n return new IntersectionObserver(entries => this._observerCallback(entries), options);\n }\n\n // The logic of selection\n _observerCallback(entries) {\n const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`);\n const activate = entry => {\n this._previousScrollData.visibleEntryTop = entry.target.offsetTop;\n this._process(targetElement(entry));\n };\n const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;\n const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;\n this._previousScrollData.parentScrollTop = parentScrollTop;\n for (const entry of entries) {\n if (!entry.isIntersecting) {\n this._activeTarget = null;\n this._clearActiveClass(targetElement(entry));\n continue;\n }\n const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop;\n // if we are scrolling down, pick the bigger offsetTop\n if (userScrollsDown && entryIsLowerThanPrevious) {\n activate(entry);\n // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n if (!parentScrollTop) {\n return;\n }\n continue;\n }\n\n // if we are scrolling up, pick the smallest offsetTop\n if (!userScrollsDown && !entryIsLowerThanPrevious) {\n activate(entry);\n }\n }\n }\n _initializeTargetsAndObservables() {\n this._targetLinks = new Map();\n this._observableSections = new Map();\n const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target);\n for (const anchor of targetLinks) {\n // ensure that the anchor has an id and is not disabled\n if (!anchor.hash || isDisabled(anchor)) {\n continue;\n }\n const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element);\n\n // ensure that the observableSection exists & is visible\n if (isVisible(observableSection)) {\n this._targetLinks.set(decodeURI(anchor.hash), anchor);\n this._observableSections.set(anchor.hash, observableSection);\n }\n }\n }\n _process(target) {\n if (this._activeTarget === target) {\n return;\n }\n this._clearActiveClass(this._config.target);\n this._activeTarget = target;\n target.classList.add(CLASS_NAME_ACTIVE$1);\n this._activateParents(target);\n EventHandler.trigger(this._element, EVENT_ACTIVATE, {\n relatedTarget: target\n });\n }\n _activateParents(target) {\n // Activate dropdown parents\n if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE$1, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE$1);\n return;\n }\n for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n // Set triggered links parents as active\n // With both