pykrait 0.3.0__tar.gz → 0.3.2__tar.gz

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 (28) hide show
  1. {pykrait-0.3.0 → pykrait-0.3.2}/PKG-INFO +2 -2
  2. {pykrait-0.3.0 → pykrait-0.3.2}/pyproject.toml +9 -2
  3. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/gui/app.py +8 -0
  4. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/gui/mainwindow.py +8 -7
  5. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/io/files.py +26 -13
  6. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/io/images.py +50 -3
  7. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/pipeline/pipeline.py +14 -6
  8. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/preprocessing/timeseries_extraction.py +20 -20
  9. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/trace_analysis/synchronicity.py +51 -3
  10. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/visualization/image_overlays.py +6 -1
  11. {pykrait-0.3.0 → pykrait-0.3.2}/.gitignore +0 -0
  12. {pykrait-0.3.0 → pykrait-0.3.2}/LICENSE +0 -0
  13. {pykrait-0.3.0 → pykrait-0.3.2}/README.md +0 -0
  14. {pykrait-0.3.0 → pykrait-0.3.2}/src/.python-version +0 -0
  15. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/__init__.py +0 -0
  16. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/__main__.py +0 -0
  17. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/gui/async_workers.py +0 -0
  18. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/gui/custom_widgets.py +0 -0
  19. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/gui/fileselection.py +0 -0
  20. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/gui/resources/logo.png +0 -0
  21. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/io/__init__.py +0 -0
  22. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/pipeline/__init__.py +0 -0
  23. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/preprocessing/__init__.py +0 -0
  24. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/preprocessing/segmentation.py +0 -0
  25. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/trace_analysis/__init__.py +0 -0
  26. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/trace_analysis/filtering.py +0 -0
  27. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/trace_analysis/oscillations.py +0 -0
  28. {pykrait-0.3.0 → pykrait-0.3.2}/src/pykrait/trace_analysis/peak_analysis.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pykrait
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Python Calcium Recording Analysis and Inference Toolbox
5
5
  Author-email: phaleanopsis <22663502+phaleanopsis@users.noreply.github.com>
6
6
  License: MIT
@@ -19,7 +19,7 @@ Requires-Dist: imagecodecs>=2025.3.30
19
19
  Requires-Dist: lxml>=5.4.0
20
20
  Requires-Dist: matplotlib>=3.10.3
21
21
  Requires-Dist: numcodecs<0.16
22
- Requires-Dist: numpy<2
22
+ Requires-Dist: numpy>2
23
23
  Requires-Dist: ome-types>=0.5.3
24
24
  Requires-Dist: ome-zarr>=0.10.2
25
25
  Requires-Dist: pandas>=2.3.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pykrait"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Python Calcium Recording Analysis and Inference Toolbox"
5
5
  readme = { file = "README.md", content-type = "text/markdown" }
6
6
  license = { text = "MIT" }
@@ -22,7 +22,7 @@ dependencies = [
22
22
  "lxml>=5.4.0",
23
23
  "matplotlib>=3.10.3",
24
24
  "numcodecs<0.16",
25
- "numpy<2",
25
+ "numpy>2",
26
26
  "ome-types>=0.5.3",
27
27
  "ome-zarr>=0.10.2",
28
28
  "pip>=25.1.1",
@@ -99,3 +99,10 @@ skip_covered = true
99
99
  src = ["src"]
100
100
  line-length = 88
101
101
  target-version = "py311"
102
+
103
+ [dependency-groups]
104
+ dev = [
105
+ "build>=1.2.2.post1",
106
+ "hatch>=1.14.1",
107
+ "twine>=6.1.0",
108
+ ]
@@ -1,5 +1,6 @@
1
1
  import sys
2
2
  import os
3
+ import warnings
3
4
  from PySide6.QtCore import QSize
4
5
  from PySide6.QtGui import QIcon
5
6
  from PySide6.QtWidgets import QApplication
@@ -7,8 +8,15 @@ from pykrait.gui.fileselection import FileSelectionWindow
7
8
  from pykrait.gui.mainwindow import MainWindow
8
9
  from pykrait.io.files import get_pykrait_version
9
10
 
11
+ def warning_no_source_line(message, category, filename, lineno, file=None, line=None):
12
+ return f"{filename}:{lineno}: {category.__name__}: {message}\n"
13
+
10
14
  class AppController:
11
15
  def __init__(self):
16
+
17
+ # format warnings to not print the source line every time
18
+ warnings.formatwarning = warning_no_source_line
19
+
12
20
  self.app = QApplication(sys.argv)
13
21
 
14
22
  # Loading icon from resources
@@ -1,4 +1,5 @@
1
1
  import numpy as np
2
+ import traceback
2
3
  import dask.array as da
3
4
  from matplotlib import cm
4
5
  from pathlib import Path
@@ -44,8 +45,8 @@ from PySide6.QtWidgets import (
44
45
  from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
45
46
  from matplotlib.figure import Figure
46
47
 
47
- from pykrait.io.files import get_pykrait_version, concat_analysis_outputs
48
- from pykrait.io.images import read_label_image
48
+ from pykrait.io.files import get_pykrait_version, concat_analysis_files
49
+ from pykrait.io.images import read_image
49
50
  from pykrait.pipeline.pipeline import AnalysisParameters, AnalysisOutput
50
51
  from pykrait.gui.async_workers import (
51
52
  DetrendWorker,
@@ -56,7 +57,6 @@ from pykrait.gui.async_workers import (
56
57
  AdjacencyMatrixWorker
57
58
  )
58
59
  from pykrait.gui.custom_widgets import ToggleSwitch, CollapsibleBox
59
- from pykrait.trace_analysis.peak_analysis import calculate_normalized_peaks
60
60
 
61
61
 
62
62
  class ImageDisplay(QGraphicsView):
@@ -292,7 +292,7 @@ class MainWindow(QMainWindow):
292
292
  self.show_detrended: bool = False
293
293
  self.detrended_intensities = None
294
294
  self.peaks = None
295
- self.tproj = read_label_image(self.analysis_output.tproj_path)
295
+ self.tproj = read_image(self.analysis_output.tproj_path)
296
296
  # menu bar
297
297
  self._create_menu_bar()
298
298
  # Central widget and main layout
@@ -560,7 +560,7 @@ class MainWindow(QMainWindow):
560
560
  self.time_window_spin.setDecimals(2)
561
561
  self.time_window_spin.setMinimum(0)
562
562
  self.time_window_spin.setMaximum(100)
563
- self.time_window_spin.setValue(5.0) # default value
563
+ self.time_window_spin.setValue(10.0) # default value
564
564
  synch_layout.addRow("Time Window [s]:", self.time_window_spin)
565
565
 
566
566
  # Action buttons
@@ -1043,7 +1043,8 @@ class MainWindow(QMainWindow):
1043
1043
  self.calculate_synchronous_btn.setEnabled(True)
1044
1044
 
1045
1045
  def _synchronicity_error(self, error):
1046
- QMessageBox.critical(self, "Error", f"Synchronicity analysis failed: {error}")
1046
+ tb_str = "".join(traceback.format_exception(type(error), error, error.__traceback__))
1047
+ QMessageBox.critical(self, "Error", f"Synchronicity analysis failed: {tb_str}")
1047
1048
  self.calculate_synchronous_btn.setEnabled(True)
1048
1049
 
1049
1050
  def _save_analysis_results(self):
@@ -1100,7 +1101,7 @@ class MainWindow(QMainWindow):
1100
1101
  root_folder = image_path.parent
1101
1102
 
1102
1103
  # Call your concatenation function
1103
- concat_analysis_outputs(str(root_folder))
1104
+ concat_analysis_files(str(root_folder))
1104
1105
 
1105
1106
  QMessageBox.information(
1106
1107
  self,
@@ -114,7 +114,7 @@ def save_NroisxF(
114
114
  df.to_csv(filepath, index=True, compression="zstd")
115
115
 
116
116
 
117
- def concat_analysis_outputs(root_folder: str) -> None:
117
+ def concat_analysis_files(root_folder: str, filetype:str="output") -> None:
118
118
  """
119
119
  Concatenates all *_analysis_output.csv files in the given root folder and its subdirectories into a single CSV file named experiment_overview.csv.
120
120
 
@@ -124,17 +124,30 @@ def concat_analysis_outputs(root_folder: str) -> None:
124
124
  """
125
125
  root = Path(root_folder)
126
126
  # Search recursively for *_analysis_output.csv
127
- csv_files = [
128
- f
129
- for f in root.rglob("*_analysis_output.csv")
130
- if not f.name.startswith(".") # skip hidden files
131
- ]
132
-
133
- if not csv_files:
134
- raise FileNotFoundError(
135
- "No *_analysis_output.csv files found in the directory or subdirectories."
136
- )
137
-
127
+ if filetype == "output":
128
+ csv_files = [
129
+ f
130
+ for f in root.rglob("*_analysis_output.csv")
131
+ if not f.name.startswith(".") # skip hidden files
132
+ ]
133
+ if not csv_files:
134
+ raise FileNotFoundError(
135
+ "No *_analysis_output.csv files found in the directory or subdirectories."
136
+ )
137
+ output_path = root / "analysis_output_overview.csv"
138
+ elif filetype == "parameters":
139
+ csv_files = [
140
+ f
141
+ for f in root.rglob("*_analysis_parameters.csv")
142
+ if not f.name.startswith(".") # skip hidden files
143
+ ]
144
+ if not csv_files:
145
+ raise FileNotFoundError(
146
+ "No *_analysis_parameters.csv files found in the directory or subdirectories."
147
+ )
148
+ output_path = root / "analysis_params_overview.csv"
149
+ else:
150
+ raise ValueError(f"Unexpected filetype: {filetype}, use 'parameters' or 'output'")
138
151
  dfs = []
139
152
  for f in csv_files:
140
153
  df = pd.read_csv(f)
@@ -143,7 +156,7 @@ def concat_analysis_outputs(root_folder: str) -> None:
143
156
  dfs.append(df)
144
157
 
145
158
  combined = pd.concat(dfs, ignore_index=True)
146
- output_path = root / "experiment_overview.csv"
159
+
147
160
  combined.to_csv(output_path, index=False)
148
161
  print(f"Saved combined CSV to {output_path}")
149
162
 
@@ -7,6 +7,7 @@ from bioio import BioImage
7
7
  import bioio_tifffile
8
8
  import xml.etree.ElementTree as ET
9
9
  from skimage.measure import find_contours
10
+ from scipy.ndimage import label
10
11
 
11
12
  ALLOWED_IMAGE_EXTENSIONS = [".czi", ".tif", ".tiff"]
12
13
 
@@ -159,6 +160,29 @@ def _reorder_dask_array(
159
160
 
160
161
  return dask_img.transpose(*perm)
161
162
 
163
+ def read_image(
164
+ file_path: str,
165
+ ) -> np.ndarray:
166
+ """Reads a image from a file and returns it as a Dask array.
167
+
168
+ :param file_path: path to the image file
169
+ :type file_path: str
170
+ :return: Dask array of the image
171
+ :rtype: da.Array
172
+ """
173
+ if not os.path.exists(file_path):
174
+ raise FileNotFoundError(f"File not found: {file_path}")
175
+
176
+ try:
177
+ img = BioImage(file_path, reconstruct_mosaic=True).dask_data.compute()
178
+ return img[
179
+ 0, 0, 0, :, :
180
+ ] # Assuming the label image is single-channel, return as (Y, X)
181
+ except Exception as e:
182
+ raise ValueError(
183
+ f"Failed to read label image from {file_path}: {type(e).__name__}: {e}"
184
+ )
185
+ return None
162
186
 
163
187
  def read_label_image(
164
188
  file_path: str,
@@ -175,9 +199,32 @@ def read_label_image(
175
199
 
176
200
  try:
177
201
  img = BioImage(file_path, reconstruct_mosaic=True).dask_data.compute()
178
- return img[
179
- 0, 0, 0, :, :
180
- ] # Assuming the label image is single-channel, return as (Y, X)
202
+ img = img[0, 0, 0, :, :]
203
+ clean_masks = np.zeros_like(img)
204
+ roi_labels = np.unique(img)
205
+ roi_labels = roi_labels[roi_labels != 0]
206
+
207
+ # 8-way connectivity in 2D
208
+ kernel = np.ones((3, 3), dtype=bool)
209
+
210
+ for label_val in roi_labels:
211
+ roi_mask = img == label_val
212
+ labeled, n = label(roi_mask, structure=kernel)
213
+
214
+ # if mask is not connected
215
+ if n > 1:
216
+ component_sizes = np.bincount(labeled.ravel())[1:]
217
+ largest_idx = np.argmax(component_sizes) + 1
218
+ clean_masks[labeled == largest_idx] = label_val
219
+
220
+ warnings.warn(
221
+ f"ROI {label_val} had {n} disconnected components — kept the largest ({component_sizes[largest_idx-1]} px).",
222
+ UserWarning,
223
+ )
224
+ else:
225
+ clean_masks[roi_mask] = label_val
226
+ return clean_masks
227
+
181
228
  except Exception as e:
182
229
  raise ValueError(
183
230
  f"Failed to read label image from {file_path}: {type(e).__name__}: {e}"
@@ -154,6 +154,10 @@ class AnalysisOutput:
154
154
  :vartype median_rise2080_s: float
155
155
  :var median_decay8020_s: median decay time from 80% to 20% of peak height in seconds
156
156
  :vartype median_decay8020_s: float
157
+ :var total_coinciding_peaks_1st_neighbour: total number of coinciding peaks between 1st degree neighbours
158
+ :vartype total_coinciding_peaks_1st_neighbour: int
159
+ :var possible_coinciding_peaks_1st_neighbour: total number of possible coinciding peaks between 1st degree neighbours
160
+ :vartype possible_coinciding_peaks_1st_neighbour: int
157
161
  :var random_1st_neighbour_zscore: z-score of synchronicity of the first neighbours in the random control
158
162
  :vartype random_1st_neighbour_zscore: float
159
163
  :var random_2nd_neighbour_zscore: z-score of synchronicity of the second neighbours in the random control
@@ -197,6 +201,8 @@ class AnalysisOutput:
197
201
  median_peakwidth_s: float = None
198
202
  median_rise2080_s: float = None
199
203
  median_decay8020_s: float = None
204
+ total_coinciding_peaks_1st_neighbour: int = None
205
+ possible_coinciding_peaks_1st_neighbour: int = None
200
206
  random_1st_neighbour_zscore: float = None
201
207
  random_2nd_neighbour_zscore: float = None
202
208
  random_3rd_neighbour_zscore: float = None
@@ -356,11 +362,11 @@ class CalciumVideo:
356
362
  )
357
363
 
358
364
  self.peak_series, peak_properties_df, peak_frame_nonzero, self.analysis_output = do_peak_analysis(
359
- self.detrended_timeseries,
360
- self.analysis_parameters,
361
- self.analysis_output,
362
- peak_min_width_frames,
363
- peak_max_width_frames,
365
+ detrended_timeseries=self.detrended_timeseries,
366
+ peak_min_width_frames=peak_min_width_frames,
367
+ peak_max_width_frames=peak_max_width_frames,
368
+ analysis_parameters=self.analysis_parameters,
369
+ analysis_output=self.analysis_output,
364
370
  )
365
371
 
366
372
  self.analyis_output, self.experimental_stds, self.experimental_covs = (
@@ -672,7 +678,7 @@ def calculate_synchronicity(
672
678
  # calculate synchronicity for 1st, 2nd and 3rd degree neighbours
673
679
  frame_window = int(analysis_parameters.time_window / analysis_output.frame_interval)
674
680
  for neighbour_degree in [3, 2, 1]:
675
- t_zscore, r_zscore, true_synchronous_peaks_matrix, rand_synchronous_peaks_matrix = (
681
+ t_zscore, r_zscore, true_synchronous_peaks_matrix, rand_synchronous_peaks_matrix, potential_synchronous_peaks_matrix = (
676
682
  calculate_synchronicity_zscore(
677
683
  shortest_path_matrix,
678
684
  peak_series,
@@ -682,6 +688,8 @@ def calculate_synchronicity(
682
688
  )
683
689
  )
684
690
  if neighbour_degree == 1:
691
+ analysis_output.total_coinciding_peaks_1st_neighbour = np.nansum(true_synchronous_peaks_matrix)
692
+ analysis_output.possible_coinciding_peaks_1st_neighbour = np.nansum(potential_synchronous_peaks_matrix)
685
693
  analysis_output.experiment_1st_neighbour_zscore = round(t_zscore, 2)
686
694
  analysis_output.random_1st_neighbour_zscore = round(r_zscore, 2)
687
695
  elif neighbour_degree == 2:
@@ -14,7 +14,6 @@ from scipy.sparse.csgraph import floyd_warshall
14
14
  from scipy.spatial import KDTree
15
15
  from scipy.sparse import lil_matrix
16
16
 
17
-
18
17
  def _legacy_extract_mean_intensities(
19
18
  numpy_timelapse: np.ndarray, masks: np.ndarray
20
19
  ) -> np.ndarray:
@@ -112,37 +111,38 @@ def shrink_masks(masks: np.ndarray, shrink_factor: float = 0.7) -> np.ndarray:
112
111
  roi_labels = np.unique(masks)
113
112
  roi_labels = roi_labels[roi_labels != 0] # skip background
114
113
 
114
+ # 8-way connectivity in 2D
115
+ kernel = np.ones((3, 3), dtype=bool)
116
+
115
117
  for label_val in roi_labels:
116
118
  roi_mask = masks == label_val
117
119
 
118
120
  # Compute erosion iterations based on area ratio
119
121
  area_original = roi_mask.sum()
120
122
  area_target = area_original * shrink_factor
123
+
124
+ if area_target < 20:
125
+ warnings.warn(f"ROI {label_val} too small to shrink — keeping original.", UserWarning)
126
+ shrunk_img[roi_mask] = label_val
127
+ continue
121
128
  radius_original = int(np.sqrt(area_original / np.pi))
122
129
  radius_target = int(np.sqrt(area_target / np.pi))
123
130
  iterations = max(radius_original - radius_target, 1)
124
131
 
125
132
  eroded_mask = binary_erosion(roi_mask, structure=disk(1), iterations=iterations)
126
- shrunk_img[eroded_mask] = label_val
127
-
128
- # Check that each ROI in the label image remains connected.
129
- roi_labels = np.unique(shrunk_img)
130
- roi_labels = roi_labels[roi_labels != 0]
131
- for lbl in roi_labels:
132
- roi_mask = shrunk_img == lbl
133
- labeled, n = label(roi_mask)
134
- if n > 1:
135
- component_sizes = [np.sum(labeled == i) for i in range(1, n+1)]
136
- warnings.warn(
137
- f"During shrinking of labels, ROI {lbl} became disconnected into {n} components. "
138
- f"Component sizes: {component_sizes}",
139
- UserWarning,
140
- )
141
133
 
142
- for i, size in enumerate(component_sizes, start=1):
143
- if size < 20:
144
- shrunk_img[labeled == i] = 0
145
- warnings.warn(f"Removed small component of ROI {lbl}: size = {size} pixels", UserWarning)
134
+ labeled, n = label(eroded_mask, structure=kernel)
135
+ if n > 1:
136
+ component_sizes = np.bincount(labeled.ravel())[1:]
137
+ largest_idx = np.argmax(component_sizes) + 1
138
+ shrunk_img[labeled == largest_idx] = label_val
139
+
140
+ warnings.warn(
141
+ f"ROI {label_val} had {n} disconnected components — kept the largest ({component_sizes[largest_idx-1]} px).",
142
+ UserWarning,
143
+ )
144
+ else:
145
+ shrunk_img[eroded_mask] = label_val
146
146
 
147
147
  return shrunk_img
148
148
 
@@ -54,6 +54,49 @@ def find_synchronous_peaks(
54
54
 
55
55
  return synchronous_cells_matrix
56
56
 
57
+ def find_possible_synchronous_peaks(
58
+ shortest_path_matrix: np.ndarray,
59
+ peak_series: np.ndarray,
60
+ neighbour_degree: int,
61
+ frame_window: int,
62
+ ) -> np.ndarray:
63
+ """
64
+ finds the number of possible synchronous peaks between connected cells given a certain neighbourhood degree and time window.
65
+ This assumes that one peak can only be synchronous with one other peak from a connected cell, so the maximum number of synchronous
66
+ events is limited by the cell with the fewer peaks.
67
+
68
+ :param shortest_path_matrix: ROI x ROI matrix with shortest path lengths between cells n and m
69
+ :type shortest_path_matrix: np.ndarray
70
+ :param peak_series: T x ROI binary matrix with 1s at calcium peak frames
71
+ :type peak_series: np.ndarray
72
+ :param neighbour_degree: degree of neighbourhood to consider (1 = direct neighbours)
73
+ :type neighbour_degree: int
74
+ :param frame_window: time window within peaks are considered synchronous
75
+ :type frame_window: int
76
+ :return: returns a ROI x ROI matrix with the number of possible synchronous peaks between connected cells
77
+ :rtype: np.ndarray
78
+ """
79
+ possible_synchronous_cells_matrix = np.zeros(shortest_path_matrix.shape, dtype=np.uint16)
80
+
81
+ # find connected cells
82
+ cell_indices = np.where(
83
+ shortest_path_matrix == neighbour_degree
84
+ )
85
+ if cell_indices[0].size == 0 or cell_indices[1].size == 0:
86
+ warnings.warn(f"No connected cell pairs found for the specified neighbour degree of {neighbour_degree}.", UserWarning)
87
+ return possible_synchronous_cells_matrix
88
+
89
+ n_peaks_per_cell = np.sum(peak_series, axis=0)
90
+
91
+ for synchronous_cellpair in range(0, cell_indices[0].shape[0]):
92
+ possible_synchronous_cells_matrix[
93
+ cell_indices[0][synchronous_cellpair], cell_indices[1][synchronous_cellpair]
94
+ ] = min(
95
+ n_peaks_per_cell[cell_indices[0][synchronous_cellpair]],
96
+ n_peaks_per_cell[cell_indices[1][synchronous_cellpair]],
97
+ )
98
+
99
+ return possible_synchronous_cells_matrix
57
100
 
58
101
  def _random_synchronous_peaks(
59
102
  shortest_path_matrix: np.ndarray,
@@ -106,7 +149,7 @@ def calculate_synchronicity_zscore(
106
149
  neighbour_degree: int,
107
150
  frame_window: int,
108
151
  n_iter=100,
109
- ) -> Tuple[float, float, np.ndarray, np.ndarray]:
152
+ ) -> Tuple[float, float, np.ndarray, np.ndarray, np.ndarray]:
110
153
  """
111
154
  calculates the zscore of the synchronicity between connected cells given a certain neighbourhood degree and time window.
112
155
 
@@ -120,8 +163,8 @@ def calculate_synchronicity_zscore(
120
163
  :type frame_window: int
121
164
  :param n_iter: number of random permutations to bootstrap against, defaults to 100
122
165
  :type n_iter: int, optional
123
- :return: returns the zscore of the synchronicity, the true synchronous peaks matrix and one example random synchronous peaks matrix
124
- :rtype: Tuple[float, float, np.ndarray, np.ndarray]
166
+ :return: returns the zscore of the synchronicity, the true synchronous peaks matrix, one example random synchronous peaks matrix and the possible synchronous peaks matrix
167
+ :rtype: Tuple[float, float, np.ndarray, np.ndarray, np.ndarray]
125
168
  """
126
169
  # finds true connected peaks
127
170
  true_synchronous_peaks_matrix = find_synchronous_peaks(
@@ -148,9 +191,14 @@ def calculate_synchronicity_zscore(
148
191
  true_synchronicity_zscore = (true_synchronous_peaks - np.mean(rand_synchronous_peaks)) / np.std(rand_synchronous_peaks)
149
192
  rand_synchronicity_zscore = (rand_synchronous_peaks[0] - np.mean(rand_synchronous_peaks)) / np.std(rand_synchronous_peaks)
150
193
 
194
+ # calculate possible synchronous peaks matrices
195
+ potential_synchronous_peaks_matrix = find_possible_synchronous_peaks(
196
+ shortest_path_matrix, peak_series, neighbour_degree, frame_window
197
+ )
151
198
  return (
152
199
  true_synchronicity_zscore,
153
200
  rand_synchronicity_zscore,
154
201
  true_synchronous_peaks_matrix,
155
202
  rand_synchronous_peaks_matrix[0, :, :],
203
+ potential_synchronous_peaks_matrix
156
204
  )
@@ -105,7 +105,12 @@ def create_synchronicity_image(tproj:np.ndarray, masks:np.ndarray, synchronous_p
105
105
  :type savepath: str, optional
106
106
  :return: _description_
107
107
  :rtype: Figure
108
- """
108
+ """
109
+ if tproj.ndim >= 2:
110
+ tproj = tproj.squeeze()
111
+ if tproj.ndim != 2:
112
+ raise ValueError(f"tproj must be a 2D array, not of shape {tproj.shape}.")
113
+
109
114
  fig, ax = plt.subplots()
110
115
  ax.imshow(tproj, cmap='gray')
111
116
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes