py-neuromodulation 0.0.3__py3-none-any.whl → 0.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. py_neuromodulation/ConnectivityDecoding/Automated Anatomical Labeling 3 (Rolls 2020).nii +0 -0
  2. py_neuromodulation/ConnectivityDecoding/_get_grid_hull.m +34 -0
  3. py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py +95 -0
  4. py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py +107 -0
  5. py_neuromodulation/ConnectivityDecoding/mni_coords_cortical_surface.mat +0 -0
  6. py_neuromodulation/ConnectivityDecoding/mni_coords_whole_brain.mat +0 -0
  7. py_neuromodulation/ConnectivityDecoding/rmap_func_all.nii +0 -0
  8. py_neuromodulation/ConnectivityDecoding/rmap_struc.nii +0 -0
  9. py_neuromodulation/FieldTrip.py +589 -589
  10. py_neuromodulation/__init__.py +74 -13
  11. py_neuromodulation/_write_example_dataset_helper.py +83 -65
  12. py_neuromodulation/data/README +6 -0
  13. py_neuromodulation/data/dataset_description.json +8 -0
  14. py_neuromodulation/data/participants.json +32 -0
  15. py_neuromodulation/data/participants.tsv +2 -0
  16. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_coordsystem.json +5 -0
  17. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_electrodes.tsv +11 -0
  18. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_channels.tsv +11 -0
  19. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.eeg +0 -0
  20. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.json +18 -0
  21. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vhdr +35 -0
  22. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vmrk +13 -0
  23. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/sub-testsub_ses-EphysMedOff_scans.tsv +2 -0
  24. py_neuromodulation/grid_cortex.tsv +40 -0
  25. py_neuromodulation/grid_subcortex.tsv +1429 -0
  26. py_neuromodulation/liblsl/libpugixml.so.1.12 +0 -0
  27. py_neuromodulation/liblsl/linux/bionic_amd64/liblsl.1.16.2.so +0 -0
  28. py_neuromodulation/liblsl/linux/bookworm_amd64/liblsl.1.16.2.so +0 -0
  29. py_neuromodulation/liblsl/linux/focal_amd46/liblsl.1.16.2.so +0 -0
  30. py_neuromodulation/liblsl/linux/jammy_amd64/liblsl.1.16.2.so +0 -0
  31. py_neuromodulation/liblsl/linux/jammy_x86/liblsl.1.16.2.so +0 -0
  32. py_neuromodulation/liblsl/linux/noble_amd64/liblsl.1.16.2.so +0 -0
  33. py_neuromodulation/liblsl/macos/amd64/liblsl.1.16.2.dylib +0 -0
  34. py_neuromodulation/liblsl/macos/arm64/liblsl.1.16.0.dylib +0 -0
  35. py_neuromodulation/liblsl/windows/amd64/liblsl.1.16.2.dll +0 -0
  36. py_neuromodulation/liblsl/windows/x86/liblsl.1.16.2.dll +0 -0
  37. py_neuromodulation/nm_IO.py +413 -417
  38. py_neuromodulation/nm_RMAP.py +496 -531
  39. py_neuromodulation/nm_analysis.py +993 -1074
  40. py_neuromodulation/nm_artifacts.py +30 -25
  41. py_neuromodulation/nm_bispectra.py +154 -168
  42. py_neuromodulation/nm_bursts.py +292 -198
  43. py_neuromodulation/nm_coherence.py +251 -205
  44. py_neuromodulation/nm_database.py +149 -0
  45. py_neuromodulation/nm_decode.py +918 -992
  46. py_neuromodulation/nm_define_nmchannels.py +300 -302
  47. py_neuromodulation/nm_features.py +144 -116
  48. py_neuromodulation/nm_filter.py +219 -219
  49. py_neuromodulation/nm_filter_preprocessing.py +79 -91
  50. py_neuromodulation/nm_fooof.py +139 -159
  51. py_neuromodulation/nm_generator.py +45 -37
  52. py_neuromodulation/nm_hjorth_raw.py +52 -73
  53. py_neuromodulation/nm_kalmanfilter.py +71 -58
  54. py_neuromodulation/nm_linelength.py +21 -33
  55. py_neuromodulation/nm_logger.py +66 -0
  56. py_neuromodulation/nm_mne_connectivity.py +149 -112
  57. py_neuromodulation/nm_mnelsl_generator.py +90 -0
  58. py_neuromodulation/nm_mnelsl_stream.py +116 -0
  59. py_neuromodulation/nm_nolds.py +96 -93
  60. py_neuromodulation/nm_normalization.py +173 -214
  61. py_neuromodulation/nm_oscillatory.py +423 -448
  62. py_neuromodulation/nm_plots.py +585 -612
  63. py_neuromodulation/nm_preprocessing.py +83 -0
  64. py_neuromodulation/nm_projection.py +370 -394
  65. py_neuromodulation/nm_rereference.py +97 -95
  66. py_neuromodulation/nm_resample.py +59 -50
  67. py_neuromodulation/nm_run_analysis.py +325 -435
  68. py_neuromodulation/nm_settings.py +289 -68
  69. py_neuromodulation/nm_settings.yaml +244 -0
  70. py_neuromodulation/nm_sharpwaves.py +423 -401
  71. py_neuromodulation/nm_stats.py +464 -480
  72. py_neuromodulation/nm_stream.py +398 -0
  73. py_neuromodulation/nm_stream_abc.py +166 -218
  74. py_neuromodulation/nm_types.py +193 -0
  75. py_neuromodulation/plots/STN_surf.mat +0 -0
  76. py_neuromodulation/plots/Vertices.mat +0 -0
  77. py_neuromodulation/plots/faces.mat +0 -0
  78. py_neuromodulation/plots/grid.mat +0 -0
  79. {py_neuromodulation-0.0.3.dist-info → py_neuromodulation-0.0.5.dist-info}/METADATA +185 -182
  80. py_neuromodulation-0.0.5.dist-info/RECORD +83 -0
  81. {py_neuromodulation-0.0.3.dist-info → py_neuromodulation-0.0.5.dist-info}/WHEEL +1 -2
  82. {py_neuromodulation-0.0.3.dist-info → py_neuromodulation-0.0.5.dist-info/licenses}/LICENSE +21 -21
  83. docs/build/_downloads/09df217f95985497f45d69e2d4bdc5b1/plot_2_example_add_feature.py +0 -68
  84. docs/build/_downloads/3b4900a2b2818ff30362215b76f7d5eb/plot_1_example_BIDS.py +0 -233
  85. docs/build/_downloads/7e92dd2e6cc86b239d14cafad972ae4f/plot_3_example_sharpwave_analysis.py +0 -219
  86. docs/build/_downloads/c2db0bf2b334d541b00662b991682256/plot_6_real_time_demo.py +0 -97
  87. docs/build/_downloads/ce3914826f782cbd1ea8fd024eaf0ac3/plot_5_example_rmap_computing.py +0 -64
  88. docs/build/_downloads/da36848a41e6a3235d91fb7cfb6d59b4/plot_0_first_demo.py +0 -192
  89. docs/build/_downloads/eaa4305c75b19a1e2eea941f742a6331/plot_4_example_gridPointProjection.py +0 -210
  90. docs/build/html/_downloads/09df217f95985497f45d69e2d4bdc5b1/plot_2_example_add_feature.py +0 -68
  91. docs/build/html/_downloads/3b4900a2b2818ff30362215b76f7d5eb/plot_1_example_BIDS.py +0 -239
  92. docs/build/html/_downloads/7e92dd2e6cc86b239d14cafad972ae4f/plot_3_example_sharpwave_analysis.py +0 -219
  93. docs/build/html/_downloads/c2db0bf2b334d541b00662b991682256/plot_6_real_time_demo.py +0 -97
  94. docs/build/html/_downloads/ce3914826f782cbd1ea8fd024eaf0ac3/plot_5_example_rmap_computing.py +0 -64
  95. docs/build/html/_downloads/da36848a41e6a3235d91fb7cfb6d59b4/plot_0_first_demo.py +0 -192
  96. docs/build/html/_downloads/eaa4305c75b19a1e2eea941f742a6331/plot_4_example_gridPointProjection.py +0 -210
  97. docs/source/_build/html/_downloads/09df217f95985497f45d69e2d4bdc5b1/plot_2_example_add_feature.py +0 -76
  98. docs/source/_build/html/_downloads/0d0d0a76e8f648d5d3cbc47da6351932/plot_real_time_demo.py +0 -97
  99. docs/source/_build/html/_downloads/3b4900a2b2818ff30362215b76f7d5eb/plot_1_example_BIDS.py +0 -240
  100. docs/source/_build/html/_downloads/5d73cadc59a8805c47e3b84063afc157/plot_example_BIDS.py +0 -233
  101. docs/source/_build/html/_downloads/7660317fa5a6bfbd12fcca9961457fc4/plot_example_rmap_computing.py +0 -63
  102. docs/source/_build/html/_downloads/7e92dd2e6cc86b239d14cafad972ae4f/plot_3_example_sharpwave_analysis.py +0 -219
  103. docs/source/_build/html/_downloads/839e5b319379f7fd9e867deb00fd797f/plot_example_gridPointProjection.py +0 -210
  104. docs/source/_build/html/_downloads/ae8be19afe5e559f011fc9b138968ba0/plot_first_demo.py +0 -192
  105. docs/source/_build/html/_downloads/b8b06cacc17969d3725a0b6f1d7741c5/plot_example_sharpwave_analysis.py +0 -219
  106. docs/source/_build/html/_downloads/c2db0bf2b334d541b00662b991682256/plot_6_real_time_demo.py +0 -121
  107. docs/source/_build/html/_downloads/c31a86c0b68cb4167d968091ace8080d/plot_example_add_feature.py +0 -68
  108. docs/source/_build/html/_downloads/ce3914826f782cbd1ea8fd024eaf0ac3/plot_5_example_rmap_computing.py +0 -64
  109. docs/source/_build/html/_downloads/da36848a41e6a3235d91fb7cfb6d59b4/plot_0_first_demo.py +0 -189
  110. docs/source/_build/html/_downloads/eaa4305c75b19a1e2eea941f742a6331/plot_4_example_gridPointProjection.py +0 -210
  111. docs/source/auto_examples/plot_0_first_demo.py +0 -189
  112. docs/source/auto_examples/plot_1_example_BIDS.py +0 -240
  113. docs/source/auto_examples/plot_2_example_add_feature.py +0 -76
  114. docs/source/auto_examples/plot_3_example_sharpwave_analysis.py +0 -219
  115. docs/source/auto_examples/plot_4_example_gridPointProjection.py +0 -210
  116. docs/source/auto_examples/plot_5_example_rmap_computing.py +0 -64
  117. docs/source/auto_examples/plot_6_real_time_demo.py +0 -121
  118. docs/source/conf.py +0 -105
  119. examples/plot_0_first_demo.py +0 -189
  120. examples/plot_1_example_BIDS.py +0 -240
  121. examples/plot_2_example_add_feature.py +0 -76
  122. examples/plot_3_example_sharpwave_analysis.py +0 -219
  123. examples/plot_4_example_gridPointProjection.py +0 -210
  124. examples/plot_5_example_rmap_computing.py +0 -64
  125. examples/plot_6_real_time_demo.py +0 -121
  126. packages/realtime_decoding/build/lib/realtime_decoding/__init__.py +0 -4
  127. packages/realtime_decoding/build/lib/realtime_decoding/decoder.py +0 -104
  128. packages/realtime_decoding/build/lib/realtime_decoding/features.py +0 -163
  129. packages/realtime_decoding/build/lib/realtime_decoding/helpers.py +0 -15
  130. packages/realtime_decoding/build/lib/realtime_decoding/run_decoding.py +0 -345
  131. packages/realtime_decoding/build/lib/realtime_decoding/trainer.py +0 -54
  132. packages/tmsi/build/lib/TMSiFileFormats/__init__.py +0 -37
  133. packages/tmsi/build/lib/TMSiFileFormats/file_formats/__init__.py +0 -36
  134. packages/tmsi/build/lib/TMSiFileFormats/file_formats/lsl_stream_writer.py +0 -200
  135. packages/tmsi/build/lib/TMSiFileFormats/file_formats/poly5_file_writer.py +0 -496
  136. packages/tmsi/build/lib/TMSiFileFormats/file_formats/poly5_to_edf_converter.py +0 -236
  137. packages/tmsi/build/lib/TMSiFileFormats/file_formats/xdf_file_writer.py +0 -977
  138. packages/tmsi/build/lib/TMSiFileFormats/file_readers/__init__.py +0 -35
  139. packages/tmsi/build/lib/TMSiFileFormats/file_readers/edf_reader.py +0 -116
  140. packages/tmsi/build/lib/TMSiFileFormats/file_readers/poly5reader.py +0 -294
  141. packages/tmsi/build/lib/TMSiFileFormats/file_readers/xdf_reader.py +0 -229
  142. packages/tmsi/build/lib/TMSiFileFormats/file_writer.py +0 -102
  143. packages/tmsi/build/lib/TMSiPlotters/__init__.py +0 -2
  144. packages/tmsi/build/lib/TMSiPlotters/gui/__init__.py +0 -39
  145. packages/tmsi/build/lib/TMSiPlotters/gui/_plotter_gui.py +0 -234
  146. packages/tmsi/build/lib/TMSiPlotters/gui/plotting_gui.py +0 -440
  147. packages/tmsi/build/lib/TMSiPlotters/plotters/__init__.py +0 -44
  148. packages/tmsi/build/lib/TMSiPlotters/plotters/hd_emg_plotter.py +0 -446
  149. packages/tmsi/build/lib/TMSiPlotters/plotters/impedance_plotter.py +0 -589
  150. packages/tmsi/build/lib/TMSiPlotters/plotters/signal_plotter.py +0 -1326
  151. packages/tmsi/build/lib/TMSiSDK/__init__.py +0 -54
  152. packages/tmsi/build/lib/TMSiSDK/device.py +0 -588
  153. packages/tmsi/build/lib/TMSiSDK/devices/__init__.py +0 -34
  154. packages/tmsi/build/lib/TMSiSDK/devices/saga/TMSi_Device_API.py +0 -1764
  155. packages/tmsi/build/lib/TMSiSDK/devices/saga/__init__.py +0 -34
  156. packages/tmsi/build/lib/TMSiSDK/devices/saga/saga_device.py +0 -1366
  157. packages/tmsi/build/lib/TMSiSDK/devices/saga/saga_types.py +0 -520
  158. packages/tmsi/build/lib/TMSiSDK/devices/saga/xml_saga_config.py +0 -165
  159. packages/tmsi/build/lib/TMSiSDK/error.py +0 -95
  160. packages/tmsi/build/lib/TMSiSDK/sample_data.py +0 -63
  161. packages/tmsi/build/lib/TMSiSDK/sample_data_server.py +0 -99
  162. packages/tmsi/build/lib/TMSiSDK/settings.py +0 -45
  163. packages/tmsi/build/lib/TMSiSDK/tmsi_device.py +0 -111
  164. packages/tmsi/build/lib/__init__.py +0 -4
  165. packages/tmsi/build/lib/apex_sdk/__init__.py +0 -34
  166. packages/tmsi/build/lib/apex_sdk/device/__init__.py +0 -41
  167. packages/tmsi/build/lib/apex_sdk/device/devices/apex/apex_API.py +0 -1009
  168. packages/tmsi/build/lib/apex_sdk/device/devices/apex/apex_API_enums.py +0 -239
  169. packages/tmsi/build/lib/apex_sdk/device/devices/apex/apex_API_structures.py +0 -668
  170. packages/tmsi/build/lib/apex_sdk/device/devices/apex/apex_device.py +0 -1611
  171. packages/tmsi/build/lib/apex_sdk/device/devices/apex/apex_dongle.py +0 -38
  172. packages/tmsi/build/lib/apex_sdk/device/devices/apex/apex_event_reader.py +0 -57
  173. packages/tmsi/build/lib/apex_sdk/device/devices/apex/apex_structures/apex_channel.py +0 -44
  174. packages/tmsi/build/lib/apex_sdk/device/devices/apex/apex_structures/apex_config.py +0 -150
  175. packages/tmsi/build/lib/apex_sdk/device/devices/apex/apex_structures/apex_const.py +0 -36
  176. packages/tmsi/build/lib/apex_sdk/device/devices/apex/apex_structures/apex_impedance_channel.py +0 -48
  177. packages/tmsi/build/lib/apex_sdk/device/devices/apex/apex_structures/apex_info.py +0 -108
  178. packages/tmsi/build/lib/apex_sdk/device/devices/apex/apex_structures/dongle_info.py +0 -39
  179. packages/tmsi/build/lib/apex_sdk/device/devices/apex/measurements/download_measurement.py +0 -77
  180. packages/tmsi/build/lib/apex_sdk/device/devices/apex/measurements/eeg_measurement.py +0 -150
  181. packages/tmsi/build/lib/apex_sdk/device/devices/apex/measurements/impedance_measurement.py +0 -129
  182. packages/tmsi/build/lib/apex_sdk/device/threads/conversion_thread.py +0 -59
  183. packages/tmsi/build/lib/apex_sdk/device/threads/sampling_thread.py +0 -57
  184. packages/tmsi/build/lib/apex_sdk/device/tmsi_channel.py +0 -83
  185. packages/tmsi/build/lib/apex_sdk/device/tmsi_device.py +0 -201
  186. packages/tmsi/build/lib/apex_sdk/device/tmsi_device_enums.py +0 -103
  187. packages/tmsi/build/lib/apex_sdk/device/tmsi_dongle.py +0 -43
  188. packages/tmsi/build/lib/apex_sdk/device/tmsi_event_reader.py +0 -50
  189. packages/tmsi/build/lib/apex_sdk/device/tmsi_measurement.py +0 -118
  190. packages/tmsi/build/lib/apex_sdk/sample_data_server/__init__.py +0 -33
  191. packages/tmsi/build/lib/apex_sdk/sample_data_server/event_data.py +0 -44
  192. packages/tmsi/build/lib/apex_sdk/sample_data_server/sample_data.py +0 -50
  193. packages/tmsi/build/lib/apex_sdk/sample_data_server/sample_data_server.py +0 -136
  194. packages/tmsi/build/lib/apex_sdk/tmsi_errors/error.py +0 -126
  195. packages/tmsi/build/lib/apex_sdk/tmsi_sdk.py +0 -113
  196. packages/tmsi/build/lib/apex_sdk/tmsi_utilities/apex/apex_structure_generator.py +0 -134
  197. packages/tmsi/build/lib/apex_sdk/tmsi_utilities/decorators.py +0 -60
  198. packages/tmsi/build/lib/apex_sdk/tmsi_utilities/logger_filter.py +0 -42
  199. packages/tmsi/build/lib/apex_sdk/tmsi_utilities/singleton.py +0 -42
  200. packages/tmsi/build/lib/apex_sdk/tmsi_utilities/support_functions.py +0 -72
  201. packages/tmsi/build/lib/apex_sdk/tmsi_utilities/tmsi_logger.py +0 -98
  202. py_neuromodulation/nm_EpochStream.py +0 -92
  203. py_neuromodulation/nm_across_patient_decoding.py +0 -927
  204. py_neuromodulation/nm_cohortwrapper.py +0 -435
  205. py_neuromodulation/nm_eval_timing.py +0 -239
  206. py_neuromodulation/nm_features_abc.py +0 -39
  207. py_neuromodulation/nm_stream_offline.py +0 -358
  208. py_neuromodulation/utils/_logging.py +0 -24
  209. py_neuromodulation-0.0.3.dist-info/RECORD +0 -188
  210. py_neuromodulation-0.0.3.dist-info/top_level.txt +0 -5
  211. tests/__init__.py +0 -0
  212. tests/conftest.py +0 -117
  213. tests/test_all_examples.py +0 -10
  214. tests/test_all_features.py +0 -63
  215. tests/test_bispectra.py +0 -70
  216. tests/test_bursts.py +0 -105
  217. tests/test_feature_sampling_rates.py +0 -143
  218. tests/test_fooof.py +0 -16
  219. tests/test_initalization_offline_stream.py +0 -41
  220. tests/test_multiprocessing.py +0 -58
  221. tests/test_nan_values.py +0 -29
  222. tests/test_nm_filter.py +0 -95
  223. tests/test_nm_resample.py +0 -63
  224. tests/test_normalization_settings.py +0 -146
  225. tests/test_notch_filter.py +0 -31
  226. tests/test_osc_features.py +0 -424
  227. tests/test_preprocessing_filter.py +0 -151
  228. tests/test_rereference.py +0 -171
  229. tests/test_sampling.py +0 -57
  230. tests/test_settings_change_after_init.py +0 -76
  231. tests/test_sharpwave.py +0 -165
  232. tests/test_target_channel_add.py +0 -100
  233. tests/test_timing.py +0 -80
@@ -1,33 +1,21 @@
1
- import numpy as np
2
- from typing import Iterable
3
-
4
- from py_neuromodulation import nm_features_abc
5
-
6
-
7
- class LineLength(nm_features_abc.Feature):
8
- def __init__(
9
- self, settings: dict, ch_names: Iterable[str], sfreq: float
10
- ) -> None:
11
- self.s = settings
12
- self.ch_names = ch_names
13
-
14
- @staticmethod
15
- def get_line_length(x: np.ndarray) -> np.ndarray:
16
- return np.mean(np.abs(np.diff(x)) / (x.shape[0] - 1))
17
-
18
- @staticmethod
19
- def test_settings(
20
- settings: dict,
21
- ch_names: Iterable[str],
22
- sfreq: int | float,
23
- ):
24
- # no settings to be checked
25
- pass
26
-
27
- def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
28
- for ch_idx, ch_name in enumerate(self.ch_names):
29
- features_compute[
30
- "_".join([ch_name, "LineLength"])
31
- ] = self.get_line_length(data[ch_idx, :])
32
-
33
- return features_compute
1
+ import numpy as np
2
+ from collections.abc import Sequence
3
+
4
+ from py_neuromodulation.nm_features import NMFeature
5
+
6
+
7
+ class LineLength(NMFeature):
8
+ def __init__(self, settings: dict, ch_names: Sequence[str], sfreq: float) -> None:
9
+ self.ch_names = ch_names
10
+
11
+ def calc_feature(self, data: np.ndarray) -> dict:
12
+
13
+ line_length = np.mean(
14
+ np.abs(np.diff(data, axis=-1)) / (data.shape[1] - 1), axis=-1
15
+ )
16
+
17
+ feature_results = {}
18
+ for ch_idx, ch_name in enumerate(self.ch_names):
19
+ feature_results[f"{ch_name}_LineLength"] = line_length[ch_idx]
20
+
21
+ return feature_results
@@ -0,0 +1,66 @@
1
+ from pathlib import Path
2
+ from py_neuromodulation.nm_types import _PathLike
3
+ import logging
4
+
5
+ INFOFORMAT = "%(name)s:\t%(message)s"
6
+ DEBUGFORMAT = "%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(funcName)s:%(lineno)d:\t%(message)s"
7
+
8
+ LOG_LEVELS = {
9
+ "DEBUG": (logging.DEBUG, DEBUGFORMAT),
10
+ "INFO": (logging.INFO, INFOFORMAT),
11
+ "WARNING": (logging.WARN, DEBUGFORMAT),
12
+ "ERROR": (logging.ERROR, DEBUGFORMAT),
13
+ }
14
+
15
+
16
+ class NMLogger(logging.Logger):
17
+ """
18
+ Subclass of logging.Logger with some extra functionality
19
+ """
20
+
21
+ def __init__(self, name: str, level: str = "INFO") -> None:
22
+ super().__init__(name, LOG_LEVELS[level][0])
23
+
24
+ self.setLevel(level)
25
+
26
+ self._console_handler = logging.StreamHandler()
27
+ self._console_handler.setLevel(level)
28
+ self._console_handler.setFormatter(logging.Formatter(LOG_LEVELS[level][1]))
29
+
30
+ self.addHandler(self._console_handler)
31
+
32
+ def set_level(self, level: str):
33
+ """
34
+ Set console logging level
35
+ """
36
+ self.setLevel(level)
37
+ self._console_handler.setLevel(level)
38
+ self._console_handler.setFormatter(logging.Formatter(LOG_LEVELS[level][1]))
39
+
40
+ def log_to_file(self, path: _PathLike, mode: str = "w"):
41
+ """
42
+ Add file handlers to the logger
43
+
44
+ Parameters
45
+ ----------
46
+ path: directory where to save logfiles
47
+ mode : str, ('w', 'a')
48
+ w: overwrite files
49
+ a: append to files
50
+ """
51
+
52
+ path = Path(path)
53
+ path.mkdir(parents=True, exist_ok=True)
54
+
55
+ self.debug_file_handler = logging.FileHandler(path / "logfile_pynm_debug.log")
56
+ self.debug_file_handler.setLevel(logging.DEBUG)
57
+ self.debug_file_handler.setFormatter(logging.Formatter(DEBUGFORMAT))
58
+
59
+ self.info_file_handler = logging.FileHandler(
60
+ path / "logfile_pynm_info.log", mode=mode
61
+ )
62
+ self.info_file_handler.setLevel(logging.INFO)
63
+ self.info_file_handler.setFormatter(logging.Formatter(INFOFORMAT))
64
+
65
+ self.addHandler(self.info_file_handler)
66
+ self.addHandler(self.debug_file_handler)
@@ -1,112 +1,149 @@
1
- from typing import Iterable
2
- import numpy as np
3
-
4
- import mne
5
- from mne_connectivity import spectral_connectivity_epochs
6
-
7
- from py_neuromodulation import nm_features_abc
8
-
9
-
10
- class MNEConnectivity(nm_features_abc.Feature):
11
- def __init__(
12
- self,
13
- settings: dict,
14
- ch_names: Iterable[str],
15
- sfreq: float,
16
- ) -> None:
17
- self.s = settings
18
- self.ch_names = ch_names
19
- self.mode = settings["mne_connectiviy"]["mode"]
20
- self.method = settings["mne_connectiviy"]["method"]
21
- self.sfreq = sfreq
22
-
23
- self.fbands = list(self.s["frequency_ranges_hz"].keys())
24
- self.fband_ranges = []
25
-
26
- @staticmethod
27
- def test_settings(
28
- settings: dict,
29
- ch_names: Iterable[str],
30
- sfreq: int | float,
31
- ):
32
- # TODO: Double check passed parameters with mne_connectivity
33
- pass
34
-
35
- @staticmethod
36
- def get_epoched_data(
37
- raw: mne.io.RawArray, epoch_length: float = 1
38
- ) -> np.array:
39
- time_samples_s = raw.get_data().shape[1] / raw.info["sfreq"]
40
- if epoch_length > time_samples_s:
41
- raise ValueError(
42
- f"the intended epoch length for mne connectivity: {epoch_length}s"
43
- f" are longer than the passed data array {np.round(time_samples_s, 2)}s"
44
- )
45
- events = mne.make_fixed_length_events(
46
- raw, duration=epoch_length, overlap=0
47
- )
48
- event_id = {"rest": 1}
49
-
50
- epochs = mne.Epochs(
51
- raw,
52
- events=events,
53
- event_id=event_id,
54
- tmin=0,
55
- tmax=epoch_length,
56
- baseline=None,
57
- reject_by_annotation=True,
58
- )
59
- if epochs.events.shape[0] < 2:
60
- raise Exception(
61
- f"A minimum of 2 epochs is required for mne_connectivity,"
62
- f" got only {epochs.events.shape[0]}. Increase settings['segment_length_features_ms']"
63
- )
64
- return epochs
65
-
66
- def estimate_connectivity(self, epochs: mne.Epochs):
67
- # n_jobs is here kept to 1, since setup of the multiprocessing Pool
68
- # takes longer than most batch computing sizes
69
-
70
- spec_out = spectral_connectivity_epochs(
71
- data=epochs,
72
- sfreq=self.sfreq,
73
- n_jobs=1,
74
- method=self.method,
75
- mode=self.mode,
76
- indices=(np.array([0, 0, 1, 1]), np.array([2, 3, 2, 3])),
77
- faverage=False,
78
- block_size=1000,
79
- )
80
- return spec_out
81
-
82
- def calc_feature(self, data: np.array, features_compute: dict) -> dict:
83
-
84
- raw = mne.io.RawArray(
85
- data=data,
86
- info=mne.create_info(ch_names=self.ch_names, sfreq=self.sfreq),
87
- )
88
- epochs = self.get_epoched_data(raw)
89
- # there need to be minimum 2 of two epochs, otherwise mne_connectivity
90
- # is not correctly initialized
91
-
92
- spec_out = self.estimate_connectivity(epochs)
93
- if len(self.fband_ranges) == 0:
94
- for fband in self.fbands:
95
- self.fband_ranges.append(
96
- np.where(
97
- np.logical_and(
98
- np.array(spec_out.freqs)
99
- > self.s["frequency_ranges_hz"][fband][0],
100
- np.array(spec_out.freqs)
101
- < self.s["frequency_ranges_hz"][fband][1],
102
- )
103
- )[0]
104
- )
105
- dat_conn = spec_out.get_data()
106
- for conn in np.arange(dat_conn.shape[0]):
107
- for fband_idx, fband in enumerate(self.fbands):
108
- features_compute[
109
- "_".join(["ch1", self.method, str(conn), fband])
110
- ] = np.mean(dat_conn[conn, self.fband_ranges[fband_idx]])
111
-
112
- return features_compute
1
+ from collections.abc import Iterable
2
+ import numpy as np
3
+ import pandas as pd
4
+ from typing import TYPE_CHECKING
5
+
6
+ from py_neuromodulation.nm_features import NMFeature
7
+ from py_neuromodulation.nm_types import NMBaseModel
8
+
9
+ if TYPE_CHECKING:
10
+ from py_neuromodulation.nm_settings import NMSettings
11
+ from mne.io import RawArray
12
+ from mne import Epochs
13
+
14
+
15
+ class MNEConnectivitySettings(NMBaseModel):
16
+ method: str = "plv"
17
+ mode: str = "multitaper"
18
+
19
+
20
+ class MNEConnectivity(NMFeature):
21
+ def __init__(
22
+ self,
23
+ settings: "NMSettings",
24
+ ch_names: Iterable[str],
25
+ sfreq: float,
26
+ ) -> None:
27
+ from mne import create_info
28
+
29
+ self.settings = settings
30
+
31
+ self.ch_names = ch_names
32
+ self.sfreq = sfreq
33
+
34
+ # Params used by spectral_connectivity_epochs
35
+ self.mode = settings.mne_connectivity.mode
36
+ self.method = settings.mne_connectivity.method
37
+
38
+ self.fbands = settings.frequency_ranges_hz
39
+ self.fband_ranges: list = []
40
+ self.result_keys = []
41
+
42
+ self.raw_info = create_info(ch_names=self.ch_names, sfreq=self.sfreq)
43
+ self.raw_array: "RawArray"
44
+ self.epochs: "Epochs"
45
+ self.prev_batch_shape: tuple = (-1, -1) # sentinel value
46
+
47
+ def calc_feature(self, data: np.ndarray) -> dict:
48
+ from mne.io import RawArray
49
+ from mne import Epochs
50
+ from mne_connectivity import spectral_connectivity_epochs
51
+
52
+ time_samples_s = data.shape[1] / self.sfreq
53
+ epoch_length: float = 1 # TODO: Make this a parameter?
54
+
55
+ if epoch_length > time_samples_s:
56
+ raise ValueError(
57
+ f"the intended epoch length for mne connectivity: {epoch_length}s"
58
+ f" are longer than the passed data array {np.round(time_samples_s, 2)}s"
59
+ )
60
+
61
+ # Only reinitialize the raw_array and epochs object if the data shape has changed
62
+ # That could mean that the channels have been re-selected, or we're in the last batch
63
+ # TODO: If sfreq or channels change, do we re-initialize the whole Stream object?
64
+ if data.shape != self.prev_batch_shape:
65
+ self.raw_array = RawArray(
66
+ data=data,
67
+ info=self.raw_info,
68
+ copy=None, # type: ignore
69
+ verbose=False,
70
+ )
71
+
72
+ # self.events = make_fixed_length_events(self.raw_array, duration=epoch_length)
73
+ # Equivalent code for those parameters:
74
+ event_times = np.arange(
75
+ 0, data.shape[-1], self.sfreq * epoch_length, dtype=int
76
+ )
77
+ events = np.column_stack(
78
+ (
79
+ event_times,
80
+ np.zeros_like(event_times, dtype=int),
81
+ np.ones_like(event_times, dtype=int),
82
+ )
83
+ )
84
+
85
+ # there need to be minimum 2 of two epochs, otherwise mne_connectivity
86
+ # is not correctly initialized
87
+ if events.shape[0] < 2:
88
+ raise RuntimeError(
89
+ f"A minimum of 2 epochs is required for mne_connectivity,"
90
+ f" got only {events.shape[0]}. Increase settings['segment_length_features_ms']"
91
+ )
92
+
93
+ self.epochs = Epochs(
94
+ self.raw_array,
95
+ events=events,
96
+ event_id={"rest": 1},
97
+ tmin=0,
98
+ tmax=epoch_length,
99
+ baseline=None,
100
+ reject_by_annotation=True,
101
+ verbose=False,
102
+ )
103
+
104
+ # Trick the function "spectral_connectivity_epochs" into not calling "add_annotations_to_metadata"
105
+ # TODO: This is a hack, and maybe needs a fix in the mne_connectivity library
106
+ self.epochs._metadata = pd.DataFrame(index=np.arange(events.shape[0]))
107
+
108
+ else:
109
+ # As long as the initialization parameters, channels, sfreq and batch size are the same
110
+ # We can re-use the existing epochs object by updating the raw data
111
+ self.raw_array._data = data
112
+ self.epochs._raw = self.raw_array
113
+
114
+ # n_jobs is here kept to 1, since setup of the multiprocessing Pool
115
+ # takes longer than most batch computing sizes
116
+ spec_out = spectral_connectivity_epochs(
117
+ data=self.epochs,
118
+ sfreq=self.sfreq,
119
+ method=self.method,
120
+ mode=self.mode,
121
+ indices=(np.array([0, 0, 1, 1]), np.array([2, 3, 2, 3])),
122
+ verbose=False,
123
+ )
124
+ dat_conn: np.ndarray = spec_out.get_data()
125
+
126
+ # Get frequency band ranges only for the first batch, it's already the same
127
+ if len(self.fband_ranges) == 0:
128
+ for fband_range in self.fbands.values():
129
+ self.fband_ranges.append(
130
+ np.where(
131
+ (np.array(spec_out.freqs) > fband_range[0])
132
+ & (np.array(spec_out.freqs) < fband_range[1])
133
+ )[0]
134
+ )
135
+
136
+ # TODO: If I compute the mean for the entire fband, results are almost the same before
137
+ # normalization (0.9999999... vs 1.0), but some change wildly after normalization (-3 vs 0)
138
+ # Investigate why, is this a bug in normalization?
139
+ feature_results = {}
140
+ for conn in np.arange(dat_conn.shape[0]):
141
+ for fband_idx, fband in enumerate(self.fbands):
142
+ feature_results["_".join(["ch1", self.method, str(conn), fband])] = (
143
+ np.mean(dat_conn[conn, self.fband_ranges[fband_idx]])
144
+ )
145
+
146
+ # Store current experiment parameters to check if re-initialization is needed
147
+ self.prev_batch_shape = data.shape
148
+
149
+ return feature_results
@@ -0,0 +1,90 @@
1
+ import numpy as np
2
+ import mne
3
+ from pathlib import Path
4
+
5
+ from py_neuromodulation import logger, nm_types, nm_IO
6
+
7
+ class LSLOfflinePlayer:
8
+
9
+ def __init__(
10
+ self,
11
+ stream_name: str | None = "lsl_offline_player",
12
+ f_name: str | nm_types.PathLike = None,
13
+ raw: mne.io.Raw | None = None,
14
+ sfreq: int | float | None = None,
15
+ data: np.ndarray | None = None,
16
+ ch_type: str | None = "dbs",
17
+ ) -> None:
18
+ """Initialization of MNE-LSL offline player.
19
+ Either a filename (PathLike) is provided,
20
+ or data and sampling frequency to initialize an example mock-up stream.
21
+
22
+
23
+ Parameters
24
+ ----------
25
+ stream_name : str, optional
26
+ LSL stream name, by default "example_stream"
27
+ f_name : str | None, optional
28
+ file name used for streaming, by default None
29
+ sfreq : int | float | None, optional
30
+ sampling rate, by default None
31
+ data : np.ndarray | None, optional
32
+ data used for streaming, by default None
33
+ ch_type: str | None, optional
34
+ channel type to select for streaming, by default "dbs"
35
+
36
+ Raises
37
+ ------
38
+ ValueError
39
+ _description_
40
+ """
41
+ self.sfreq = sfreq
42
+ self.stream_name = stream_name
43
+ got_raw = raw is not None
44
+ got_fname = f_name is not None
45
+ got_sfreq_data = sfreq is not None and data is not None
46
+
47
+ if not (got_fname or got_sfreq_data or got_raw):
48
+ error_msg = "Either f_name or raw or sfreq and data must be provided."
49
+ logger.critical(error_msg)
50
+ raise ValueError(error_msg)
51
+
52
+ if got_fname:
53
+ (self._path_raw, data, sfreq, line_noise, coord_list, coord_names) = nm_IO.read_BIDS_data(f_name)
54
+
55
+ elif got_raw:
56
+ self._path_raw = raw
57
+
58
+ elif got_sfreq_data:
59
+ info = mne.create_info(
60
+ ch_names=[f"ch{i}" for i in range(data.shape[0])],
61
+ ch_types=[ch_type for _ in range(data.shape[0])],
62
+ sfreq=sfreq,
63
+ )
64
+ raw = mne.io.RawArray(data, info)
65
+ self._path_raw = Path.cwd() / "temp_raw.fif"
66
+ raw.save(self._path_raw, overwrite=True)
67
+
68
+ def start_player(self, chunk_size: int = 10, n_repeat: int = 1):
69
+ """Start MNE-LSL Player
70
+
71
+ Parameters
72
+ ----------
73
+ chunk_size : int, optional
74
+ _description_, by default 1
75
+ n_repeat : int, optional
76
+ _description_, by default 1
77
+ """
78
+ from mne_lsl.player import PlayerLSL
79
+
80
+ self.player = PlayerLSL(
81
+ self._path_raw,
82
+ name=self.stream_name,
83
+ chunk_size=chunk_size,
84
+ n_repeat=n_repeat,
85
+ )
86
+ self.player = self.player.start()
87
+
88
+ def stop_player(self):
89
+ """Stop MNE-LSL Player"""
90
+ self.player.stop()
@@ -0,0 +1,116 @@
1
+ from collections.abc import Iterator
2
+ import time
3
+ from typing import TYPE_CHECKING
4
+ import numpy as np
5
+ from py_neuromodulation import logger
6
+ from mne_lsl.lsl import resolve_streams
7
+ import os
8
+
9
+ if TYPE_CHECKING:
10
+ from py_neuromodulation import NMSettings
11
+
12
+
13
+ class LSLStream:
14
+ """
15
+ Class is used to create and connect to a LSL stream and pull data from it.
16
+ """
17
+
18
+ def __init__(self, settings: "NMSettings", stream_name: str | None = None) -> None:
19
+ """
20
+ Initialize the LSL stream.
21
+
22
+ Parameters:
23
+ -----------
24
+ settings : nm_settings.NMSettings object
25
+ stream_name : str, optional
26
+ Name of the stream to connect to. If not provided, the first available stream is used.
27
+
28
+ Raises:
29
+ -------
30
+ RuntimeError
31
+ If no stream is running under the provided name or if there are multiple streams running
32
+ under the same name.
33
+ """
34
+ from mne_lsl.stream import StreamLSL
35
+ self.stream: StreamLSL
36
+
37
+ self.settings = settings
38
+ self._n_seconds_wait_before_disconnect = 3
39
+ try:
40
+ if stream_name is None:
41
+ stream_name = resolve_streams()[0].name
42
+ logger.info(
43
+ f"Stream name not provided. Using first available stream: {stream_name}"
44
+ )
45
+ self.stream = StreamLSL(name=stream_name, bufsize=2).connect(timeout=2)
46
+ except Exception as e:
47
+ msg = f"Could not connect to stream: {e}. Either no stream is running under the name {stream_name} or there is several streams under this name."
48
+ logger.exception(msg)
49
+ raise RuntimeError(msg)
50
+
51
+ if self.stream.sinfo is None:
52
+ raise RuntimeError("Stream info is None. Check if the stream is running.")
53
+
54
+ self.winsize = settings.segment_length_features_ms / self.stream.sinfo.sfreq
55
+ self.sampling_interval = 1 / self.settings.sampling_rate_features_hz
56
+
57
+ # If not running the generator when the escape key is pressed.
58
+ self.headless: bool = not os.environ.get("DISPLAY")
59
+ if not self.headless:
60
+ from pynput import keyboard
61
+
62
+ self.listener = keyboard.Listener(
63
+ on_press=lambda key: key != keyboard.Key.esc # type: ignore
64
+ )
65
+ self.listener.start()
66
+
67
+ def get_next_batch(self) -> Iterator[tuple[np.ndarray, np.ndarray]]:
68
+ self.last_time = time.time()
69
+ check_data = None
70
+ data = None
71
+ stream_start_time = None
72
+
73
+ while self.stream.connected:
74
+ time_diff = time.time() - self.last_time # in s
75
+ time.sleep(0.005)
76
+ if time_diff >= self.sampling_interval:
77
+ self.last_time = time.time()
78
+
79
+ logger.debug(f"Pull data - current time: {self.last_time}")
80
+ logger.debug(f"time since last data pull {time_diff} seconds")
81
+
82
+ if time_diff >= 2 * self.sampling_interval:
83
+ logger.warning(
84
+ "Feature computation time between two consecutive samples"
85
+ "was twice the feature sampling interval"
86
+ )
87
+ if data is not None:
88
+ check_data = data
89
+
90
+ data, timestamp = self.stream.get_data(winsize=self.winsize)
91
+ if stream_start_time is None:
92
+ stream_start_time = timestamp[0]
93
+
94
+ for i in range(self._n_seconds_wait_before_disconnect):
95
+ if (
96
+ data is not None
97
+ and check_data is not None
98
+ and np.allclose(data, check_data, atol=1e-7, rtol=1e-7)
99
+ ):
100
+ logger.warning(
101
+ f"No new data incoming. Disconnecting stream in {3-i} seconds."
102
+ )
103
+ time.sleep(1)
104
+ i += 1
105
+ if i == self._n_seconds_wait_before_disconnect:
106
+ self.stream.disconnect()
107
+ logger.warning("Stream disconnected.")
108
+ break
109
+
110
+ yield timestamp, data
111
+
112
+ logger.info(f"Stream time: {timestamp[-1] - stream_start_time}")
113
+
114
+ if not self.headless and not self.listener.running:
115
+ logger.info("Keyboard interrupt")
116
+ self.stream.disconnect()