canns 0.13.1__py3-none-any.whl → 0.14.0__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 (99) hide show
  1. canns/analyzer/data/__init__.py +5 -1
  2. canns/analyzer/data/asa/__init__.py +27 -12
  3. canns/analyzer/data/asa/cohospace.py +336 -10
  4. canns/analyzer/data/asa/config.py +3 -0
  5. canns/analyzer/data/asa/embedding.py +48 -45
  6. canns/analyzer/data/asa/path.py +104 -2
  7. canns/analyzer/data/asa/plotting.py +88 -19
  8. canns/analyzer/data/asa/tda.py +11 -4
  9. canns/analyzer/data/cell_classification/__init__.py +97 -0
  10. canns/analyzer/data/cell_classification/core/__init__.py +26 -0
  11. canns/analyzer/data/cell_classification/core/grid_cells.py +633 -0
  12. canns/analyzer/data/cell_classification/core/grid_modules_leiden.py +288 -0
  13. canns/analyzer/data/cell_classification/core/head_direction.py +347 -0
  14. canns/analyzer/data/cell_classification/core/spatial_analysis.py +431 -0
  15. canns/analyzer/data/cell_classification/io/__init__.py +5 -0
  16. canns/analyzer/data/cell_classification/io/matlab_loader.py +417 -0
  17. canns/analyzer/data/cell_classification/utils/__init__.py +39 -0
  18. canns/analyzer/data/cell_classification/utils/circular_stats.py +383 -0
  19. canns/analyzer/data/cell_classification/utils/correlation.py +318 -0
  20. canns/analyzer/data/cell_classification/utils/geometry.py +442 -0
  21. canns/analyzer/data/cell_classification/utils/image_processing.py +416 -0
  22. canns/analyzer/data/cell_classification/visualization/__init__.py +19 -0
  23. canns/analyzer/data/cell_classification/visualization/grid_plots.py +292 -0
  24. canns/analyzer/data/cell_classification/visualization/hd_plots.py +200 -0
  25. canns/analyzer/metrics/__init__.py +2 -1
  26. canns/analyzer/visualization/core/config.py +46 -4
  27. canns/data/__init__.py +6 -1
  28. canns/data/datasets.py +154 -1
  29. canns/data/loaders.py +37 -0
  30. canns/pipeline/__init__.py +13 -9
  31. canns/pipeline/__main__.py +6 -0
  32. canns/pipeline/asa/runner.py +105 -41
  33. canns/pipeline/asa_gui/__init__.py +68 -0
  34. canns/pipeline/asa_gui/__main__.py +6 -0
  35. canns/pipeline/asa_gui/analysis_modes/__init__.py +42 -0
  36. canns/pipeline/asa_gui/analysis_modes/base.py +39 -0
  37. canns/pipeline/asa_gui/analysis_modes/batch_mode.py +21 -0
  38. canns/pipeline/asa_gui/analysis_modes/cohomap_mode.py +56 -0
  39. canns/pipeline/asa_gui/analysis_modes/cohospace_mode.py +194 -0
  40. canns/pipeline/asa_gui/analysis_modes/decode_mode.py +52 -0
  41. canns/pipeline/asa_gui/analysis_modes/fr_mode.py +81 -0
  42. canns/pipeline/asa_gui/analysis_modes/frm_mode.py +92 -0
  43. canns/pipeline/asa_gui/analysis_modes/gridscore_mode.py +123 -0
  44. canns/pipeline/asa_gui/analysis_modes/pathcompare_mode.py +199 -0
  45. canns/pipeline/asa_gui/analysis_modes/tda_mode.py +112 -0
  46. canns/pipeline/asa_gui/app.py +29 -0
  47. canns/pipeline/asa_gui/controllers/__init__.py +6 -0
  48. canns/pipeline/asa_gui/controllers/analysis_controller.py +59 -0
  49. canns/pipeline/asa_gui/controllers/preprocess_controller.py +89 -0
  50. canns/pipeline/asa_gui/core/__init__.py +15 -0
  51. canns/pipeline/asa_gui/core/cache.py +14 -0
  52. canns/pipeline/asa_gui/core/runner.py +1936 -0
  53. canns/pipeline/asa_gui/core/state.py +324 -0
  54. canns/pipeline/asa_gui/core/worker.py +260 -0
  55. canns/pipeline/asa_gui/main_window.py +184 -0
  56. canns/pipeline/asa_gui/models/__init__.py +7 -0
  57. canns/pipeline/asa_gui/models/config.py +14 -0
  58. canns/pipeline/asa_gui/models/job.py +31 -0
  59. canns/pipeline/asa_gui/models/presets.py +21 -0
  60. canns/pipeline/asa_gui/resources/__init__.py +16 -0
  61. canns/pipeline/asa_gui/resources/dark.qss +167 -0
  62. canns/pipeline/asa_gui/resources/light.qss +163 -0
  63. canns/pipeline/asa_gui/resources/styles.qss +130 -0
  64. canns/pipeline/asa_gui/utils/__init__.py +1 -0
  65. canns/pipeline/asa_gui/utils/formatters.py +15 -0
  66. canns/pipeline/asa_gui/utils/io_adapters.py +40 -0
  67. canns/pipeline/asa_gui/utils/validators.py +41 -0
  68. canns/pipeline/asa_gui/views/__init__.py +1 -0
  69. canns/pipeline/asa_gui/views/help_content.py +171 -0
  70. canns/pipeline/asa_gui/views/pages/__init__.py +6 -0
  71. canns/pipeline/asa_gui/views/pages/analysis_page.py +565 -0
  72. canns/pipeline/asa_gui/views/pages/preprocess_page.py +492 -0
  73. canns/pipeline/asa_gui/views/panels/__init__.py +1 -0
  74. canns/pipeline/asa_gui/views/widgets/__init__.py +21 -0
  75. canns/pipeline/asa_gui/views/widgets/artifacts_tab.py +44 -0
  76. canns/pipeline/asa_gui/views/widgets/drop_zone.py +80 -0
  77. canns/pipeline/asa_gui/views/widgets/file_list.py +27 -0
  78. canns/pipeline/asa_gui/views/widgets/gridscore_tab.py +308 -0
  79. canns/pipeline/asa_gui/views/widgets/help_dialog.py +27 -0
  80. canns/pipeline/asa_gui/views/widgets/image_tab.py +50 -0
  81. canns/pipeline/asa_gui/views/widgets/image_viewer.py +97 -0
  82. canns/pipeline/asa_gui/views/widgets/log_box.py +16 -0
  83. canns/pipeline/asa_gui/views/widgets/pathcompare_tab.py +200 -0
  84. canns/pipeline/asa_gui/views/widgets/popup_combo.py +25 -0
  85. canns/pipeline/gallery/__init__.py +15 -5
  86. canns/pipeline/gallery/__main__.py +11 -0
  87. canns/pipeline/gallery/app.py +705 -0
  88. canns/pipeline/gallery/runner.py +790 -0
  89. canns/pipeline/gallery/state.py +51 -0
  90. canns/pipeline/gallery/styles.tcss +123 -0
  91. canns/pipeline/launcher.py +81 -0
  92. {canns-0.13.1.dist-info → canns-0.14.0.dist-info}/METADATA +11 -1
  93. canns-0.14.0.dist-info/RECORD +163 -0
  94. canns-0.14.0.dist-info/entry_points.txt +5 -0
  95. canns/pipeline/_base.py +0 -50
  96. canns-0.13.1.dist-info/RECORD +0 -89
  97. canns-0.13.1.dist-info/entry_points.txt +0 -3
  98. {canns-0.13.1.dist-info → canns-0.14.0.dist-info}/WHEEL +0 -0
  99. {canns-0.13.1.dist-info → canns-0.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,417 @@
1
+ """
2
+ MATLAB Data Loader
3
+
4
+ Functions and classes for loading neuroscience data from MATLAB .mat files.
5
+ """
6
+
7
+ import warnings
8
+ from dataclasses import dataclass, field
9
+ from typing import Any
10
+
11
+ import h5py
12
+ import numpy as np
13
+ import scipy.io
14
+
15
+
16
+ @dataclass
17
+ class TuningCurve:
18
+ """
19
+ Represents a tuning curve (e.g., head direction or spatial tuning).
20
+
21
+ Attributes
22
+ ----------
23
+ bins : np.ndarray
24
+ Bin centers (e.g., angles for HD, positions for spatial)
25
+ rates : np.ndarray
26
+ Firing rates in each bin (Hz)
27
+ mvl : float, optional
28
+ Mean Vector Length (for directional tuning)
29
+ center_of_mass : float, optional
30
+ Preferred direction/position
31
+ peak_rate : float, optional
32
+ Maximum firing rate
33
+ """
34
+
35
+ bins: np.ndarray
36
+ rates: np.ndarray
37
+ mvl: float | None = None
38
+ center_of_mass: float | None = None
39
+ peak_rate: float | None = None
40
+
41
+ def __post_init__(self):
42
+ """Compute derived properties."""
43
+ if self.peak_rate is None:
44
+ self.peak_rate = np.max(self.rates) if len(self.rates) > 0 else 0.0
45
+
46
+
47
+ @dataclass
48
+ class Unit:
49
+ """
50
+ Represents a single neural unit (neuron).
51
+
52
+ Attributes
53
+ ----------
54
+ unit_id : int or str
55
+ Unique identifier for this unit
56
+ spike_times : np.ndarray
57
+ Spike times in seconds
58
+ spike_indices : np.ndarray, optional
59
+ Indices into session time array
60
+ hd_tuning : TuningCurve, optional
61
+ Head direction tuning curve
62
+ pos_tuning : TuningCurve, optional
63
+ Spatial position tuning (2D rate map)
64
+ theta_tuning : TuningCurve, optional
65
+ Theta phase tuning
66
+ is_grid : bool, optional
67
+ Whether this is a grid cell
68
+ is_hd : bool, optional
69
+ Whether this is a head direction cell
70
+ gridness_score : float, optional
71
+ Grid cell score
72
+ metadata : dict
73
+ Additional metadata
74
+ """
75
+
76
+ unit_id: Any
77
+ spike_times: np.ndarray
78
+ spike_indices: np.ndarray | None = None
79
+ hd_tuning: TuningCurve | None = None
80
+ pos_tuning: TuningCurve | None = None
81
+ theta_tuning: TuningCurve | None = None
82
+ is_grid: bool | None = None
83
+ is_hd: bool | None = None
84
+ gridness_score: float | None = None
85
+ metadata: dict[str, Any] = field(default_factory=dict)
86
+
87
+
88
+ class MATFileLoader:
89
+ """
90
+ Loader for MATLAB .mat files containing neuroscience data.
91
+
92
+ Handles both MATLAB v5/v7 files (via scipy.io) and v7.3+ files (via h5py).
93
+ """
94
+
95
+ @staticmethod
96
+ def load(filepath: str) -> dict[str, Any]:
97
+ """
98
+ Load a .mat file, automatically detecting the version.
99
+
100
+ Parameters
101
+ ----------
102
+ filepath : str
103
+ Path to .mat file
104
+
105
+ Returns
106
+ -------
107
+ data : dict
108
+ Dictionary containing the loaded data
109
+
110
+ Examples
111
+ --------
112
+ >>> loader = MATFileLoader()
113
+ >>> data = loader.load("example.mat")
114
+ >>> print(data.keys())
115
+ """
116
+ try:
117
+ # Try scipy.io first (works for v5/v7)
118
+ data = scipy.io.loadmat(filepath, struct_as_record=False, squeeze_me=True)
119
+ # Remove MATLAB metadata
120
+ data = {k: v for k, v in data.items() if not k.startswith("__")}
121
+ return data
122
+ except NotImplementedError:
123
+ # Fall back to h5py for v7.3+
124
+ return MATFileLoader._load_h5py(filepath)
125
+
126
+ @staticmethod
127
+ def _load_h5py(filepath: str) -> dict[str, Any]:
128
+ """Load MATLAB v7.3+ file using h5py."""
129
+ data = {}
130
+ with h5py.File(filepath, "r") as f:
131
+ for key in f.keys():
132
+ if not key.startswith("#"):
133
+ data[key] = MATFileLoader._h5py_to_numpy(f[key])
134
+ return data
135
+
136
+ @staticmethod
137
+ def _h5py_to_numpy(h5_obj):
138
+ """Recursively convert h5py objects to numpy arrays or dicts."""
139
+ if isinstance(h5_obj, h5py.Dataset):
140
+ return np.array(h5_obj)
141
+ elif isinstance(h5_obj, h5py.Group):
142
+ return {k: MATFileLoader._h5py_to_numpy(v) for k, v in h5_obj.items()}
143
+ else:
144
+ return h5_obj
145
+
146
+ @staticmethod
147
+ def load_unit_data(filepath: str) -> list[Unit]:
148
+ """
149
+ Load unit data from a .mat file.
150
+
151
+ Expected structure (from unit_data_25953.mat):
152
+ - units: struct array with fields:
153
+ - id or spikeInds or spikeTimes
154
+ - rmf.hd, rmf.pos, rmf.theta (tuning structures)
155
+ - isGrid (boolean)
156
+
157
+ Parameters
158
+ ----------
159
+ filepath : str
160
+ Path to unit data .mat file
161
+
162
+ Returns
163
+ -------
164
+ units : list of Unit
165
+ List of Unit objects
166
+
167
+ Examples
168
+ --------
169
+ >>> loader = MATFileLoader()
170
+ >>> units = loader.load_unit_data("../results/unit_data_25953.mat")
171
+ >>> print(f"Loaded {len(units)} units")
172
+ >>> print(f"Grid cells: {sum(u.is_grid for u in units if u.is_grid)}")
173
+ """
174
+ data = MATFileLoader.load(filepath)
175
+
176
+ # Find the units structure
177
+ if "units" in data:
178
+ units_struct = data["units"]
179
+ else:
180
+ raise ValueError("Could not find 'units' in .mat file")
181
+
182
+ # Handle both array of structs and single struct
183
+ if not isinstance(units_struct, np.ndarray):
184
+ units_struct = [units_struct]
185
+
186
+ units = []
187
+ for i, unit_data in enumerate(units_struct):
188
+ try:
189
+ unit = MATFileLoader._parse_unit_struct(unit_data, unit_id=i)
190
+ units.append(unit)
191
+ except Exception as e:
192
+ warnings.warn(f"Failed to parse unit {i}: {e}", stacklevel=2)
193
+ continue
194
+
195
+ return units
196
+
197
+ @staticmethod
198
+ def _parse_unit_struct(unit_struct, unit_id: Any = None) -> Unit:
199
+ """Parse a MATLAB unit structure into a Unit object."""
200
+ # Extract spike times
201
+ spike_times = None
202
+ spike_indices = None
203
+
204
+ if hasattr(unit_struct, "spikeTimes"):
205
+ spike_times = np.asarray(unit_struct.spikeTimes).ravel()
206
+ elif hasattr(unit_struct, "spikeInds"):
207
+ spike_indices = np.asarray(unit_struct.spikeInds).ravel()
208
+
209
+ # Extract unit ID
210
+ if unit_id is None:
211
+ if hasattr(unit_struct, "id"):
212
+ unit_id = unit_struct.id
213
+ else:
214
+ unit_id = 0
215
+
216
+ # Extract tuning curves from rmf (rate map field) structure
217
+ hd_tuning = None
218
+ pos_tuning = None
219
+ theta_tuning = None
220
+
221
+ if hasattr(unit_struct, "rmf"):
222
+ rmf = unit_struct.rmf
223
+
224
+ # Head direction tuning
225
+ if hasattr(rmf, "hd"):
226
+ hd_tuning = MATFileLoader._parse_tuning_curve(rmf.hd)
227
+
228
+ # Position tuning
229
+ if hasattr(rmf, "pos"):
230
+ pos_tuning = MATFileLoader._parse_tuning_curve(rmf.pos)
231
+
232
+ # Theta phase tuning
233
+ if hasattr(rmf, "theta"):
234
+ theta_tuning = MATFileLoader._parse_tuning_curve(rmf.theta)
235
+
236
+ # Extract classification flags
237
+ is_grid = None
238
+ if hasattr(unit_struct, "isGrid"):
239
+ is_grid = bool(unit_struct.isGrid)
240
+
241
+ # Create Unit object
242
+ unit = Unit(
243
+ unit_id=unit_id,
244
+ spike_times=spike_times if spike_times is not None else np.array([]),
245
+ spike_indices=spike_indices,
246
+ hd_tuning=hd_tuning,
247
+ pos_tuning=pos_tuning,
248
+ theta_tuning=theta_tuning,
249
+ is_grid=is_grid,
250
+ )
251
+
252
+ return unit
253
+
254
+ @staticmethod
255
+ def _parse_tuning_curve(tuning_struct) -> TuningCurve | None:
256
+ """Parse a MATLAB tuning structure into a TuningCurve object."""
257
+ if tuning_struct is None:
258
+ return None
259
+
260
+ try:
261
+ # Extract bins and rates
262
+ bins = None
263
+ rates = None
264
+ mvl = None
265
+ center_of_mass = None
266
+
267
+ if hasattr(tuning_struct, "z"):
268
+ # 'z' typically contains the tuning curve values
269
+ rates = np.asarray(tuning_struct.z)
270
+
271
+ # For 1D tuning curves, create default bins
272
+ if rates.ndim == 1:
273
+ bins = np.linspace(-np.pi, np.pi, len(rates))
274
+ else:
275
+ # For 2D rate maps, bins might not be meaningful
276
+ bins = np.arange(rates.shape[0])
277
+
278
+ if hasattr(tuning_struct, "mvl"):
279
+ mvl = float(tuning_struct.mvl)
280
+
281
+ if hasattr(tuning_struct, "centerOfMass"):
282
+ center_of_mass = float(tuning_struct.centerOfMass)
283
+
284
+ if rates is not None:
285
+ return TuningCurve(bins=bins, rates=rates, mvl=mvl, center_of_mass=center_of_mass)
286
+
287
+ except Exception as e:
288
+ warnings.warn(f"Failed to parse tuning curve: {e}", stacklevel=2)
289
+
290
+ return None
291
+
292
+ @staticmethod
293
+ def load_example_cells(filepath: str) -> list[Unit]:
294
+ """
295
+ Load example cell data from exampleIdCells.mat format.
296
+
297
+ Expected structure:
298
+ - res: struct array with fields:
299
+ - recName, id
300
+ - hdTuning, posTuning
301
+ - tempAcorr (temporal autocorrelation)
302
+
303
+ Parameters
304
+ ----------
305
+ filepath : str
306
+ Path to example cells .mat file
307
+
308
+ Returns
309
+ -------
310
+ units : list of Unit
311
+ List of Unit objects
312
+
313
+ Examples
314
+ --------
315
+ >>> loader = MATFileLoader()
316
+ >>> cells = loader.load_example_cells("../results/exampleIdCells.mat")
317
+ >>> print(f"Loaded {len(cells)} example cells")
318
+ """
319
+ data = MATFileLoader.load(filepath)
320
+
321
+ # Find the result structure
322
+ if "res" in data:
323
+ res_struct = data["res"]
324
+ else:
325
+ raise ValueError("Could not find 'res' in .mat file")
326
+
327
+ if not isinstance(res_struct, np.ndarray):
328
+ res_struct = [res_struct]
329
+
330
+ units = []
331
+ for i, cell_data in enumerate(res_struct):
332
+ try:
333
+ # Extract basic info
334
+ unit_id = cell_data.id if hasattr(cell_data, "id") else i
335
+ rec_name = cell_data.recName if hasattr(cell_data, "recName") else ""
336
+
337
+ # Extract HD tuning
338
+ hd_tuning = None
339
+ if hasattr(cell_data, "hdTuning"):
340
+ hd_struct = cell_data.hdTuning
341
+ if hasattr(hd_struct, "z") and hasattr(hd_struct, "mvl"):
342
+ rates = np.asarray(hd_struct.z).ravel()
343
+ bins = np.linspace(-np.pi, np.pi, len(rates))
344
+ mvl = float(hd_struct.mvl)
345
+ hd_tuning = TuningCurve(bins=bins, rates=rates, mvl=mvl)
346
+
347
+ # Extract position tuning
348
+ pos_tuning = None
349
+ if hasattr(cell_data, "posTuning"):
350
+ pos_struct = cell_data.posTuning
351
+ if hasattr(pos_struct, "z"):
352
+ rates = np.asarray(pos_struct.z)
353
+ # For 2D rate maps, bins are spatial coordinates
354
+ bins = np.arange(rates.shape[0])
355
+ pos_tuning = TuningCurve(bins=bins, rates=rates)
356
+
357
+ # Create Unit
358
+ unit = Unit(
359
+ unit_id=unit_id,
360
+ spike_times=np.array([]), # Not provided in example format
361
+ hd_tuning=hd_tuning,
362
+ pos_tuning=pos_tuning,
363
+ metadata={"recording": rec_name},
364
+ )
365
+ units.append(unit)
366
+
367
+ except Exception as e:
368
+ warnings.warn(f"Failed to parse example cell {i}: {e}", stacklevel=2)
369
+ continue
370
+
371
+ return units
372
+
373
+
374
+ if __name__ == "__main__":
375
+ # Simple tests (will only work if data files exist)
376
+ print("Testing MATLAB data loader...")
377
+
378
+ loader = MATFileLoader()
379
+
380
+ # Test 1: Try to load example cells
381
+ try:
382
+ print("\nTest 1 - Loading example cells:")
383
+ example_path = "../results/exampleIdCells.mat"
384
+ cells = loader.load_example_cells(example_path)
385
+ print(f" Loaded {len(cells)} example cells")
386
+
387
+ if len(cells) > 0:
388
+ cell = cells[0]
389
+ print(f" First cell ID: {cell.unit_id}")
390
+ if cell.hd_tuning:
391
+ print(f" HD tuning MVL: {cell.hd_tuning.mvl:.3f}")
392
+ print(f" HD tuning curve shape: {cell.hd_tuning.rates.shape}")
393
+
394
+ except FileNotFoundError:
395
+ print(" File not found (expected if running outside results directory)")
396
+ except Exception as e:
397
+ print(f" Error: {e}")
398
+
399
+ # Test 2: Try to load unit data
400
+ try:
401
+ print("\nTest 2 - Loading unit data:")
402
+ unit_path = "../results/unit_data_25953.mat"
403
+ units = loader.load_unit_data(unit_path)
404
+ print(f" Loaded {len(units)} units")
405
+
406
+ if len(units) > 0:
407
+ grid_cells = [u for u in units if u.is_grid]
408
+ print(f" Grid cells: {len(grid_cells)}")
409
+ if len(grid_cells) > 0:
410
+ print(f" First grid cell ID: {grid_cells[0].unit_id}")
411
+
412
+ except FileNotFoundError:
413
+ print(" File not found (expected if running outside results directory)")
414
+ except Exception as e:
415
+ print(f" Error: {e}")
416
+
417
+ print("\nData loader tests completed!")
@@ -0,0 +1,39 @@
1
+ """Utility modules."""
2
+
3
+ from .circular_stats import circ_dist, circ_dist2, circ_mean, circ_r, circ_rtest, circ_std
4
+ from .correlation import autocorrelation_2d, normalized_xcorr2, pearson_correlation
5
+ from .geometry import cart2pol, fit_ellipse, pol2cart, polyarea, squared_distance, wrap_to_pi
6
+ from .image_processing import (
7
+ dilate_image,
8
+ find_contours_at_level,
9
+ find_regional_maxima,
10
+ gaussian_filter_2d,
11
+ label_connected_components,
12
+ regionprops,
13
+ rotate_image,
14
+ )
15
+
16
+ __all__ = [
17
+ "circ_r",
18
+ "circ_mean",
19
+ "circ_std",
20
+ "circ_dist",
21
+ "circ_dist2",
22
+ "circ_rtest",
23
+ "pearson_correlation",
24
+ "normalized_xcorr2",
25
+ "autocorrelation_2d",
26
+ "fit_ellipse",
27
+ "squared_distance",
28
+ "polyarea",
29
+ "wrap_to_pi",
30
+ "cart2pol",
31
+ "pol2cart",
32
+ "rotate_image",
33
+ "find_regional_maxima",
34
+ "find_contours_at_level",
35
+ "gaussian_filter_2d",
36
+ "dilate_image",
37
+ "label_connected_components",
38
+ "regionprops",
39
+ ]