pykrait 0.3.0__tar.gz → 0.3.1__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.
- {pykrait-0.3.0 → pykrait-0.3.1}/PKG-INFO +1 -1
- {pykrait-0.3.0 → pykrait-0.3.1}/pyproject.toml +1 -1
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/gui/app.py +8 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/gui/mainwindow.py +8 -7
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/io/files.py +26 -13
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/io/images.py +50 -3
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/pipeline/pipeline.py +14 -6
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/preprocessing/timeseries_extraction.py +20 -20
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/trace_analysis/synchronicity.py +51 -3
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/visualization/image_overlays.py +6 -1
- {pykrait-0.3.0 → pykrait-0.3.1}/.gitignore +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/LICENSE +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/README.md +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/.python-version +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/__init__.py +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/__main__.py +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/gui/async_workers.py +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/gui/custom_widgets.py +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/gui/fileselection.py +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/gui/resources/logo.png +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/io/__init__.py +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/pipeline/__init__.py +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/preprocessing/__init__.py +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/preprocessing/segmentation.py +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/trace_analysis/__init__.py +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/trace_analysis/filtering.py +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/trace_analysis/oscillations.py +0 -0
- {pykrait-0.3.0 → pykrait-0.3.1}/src/pykrait/trace_analysis/peak_analysis.py +0 -0
|
@@ -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,
|
|
48
|
-
from pykrait.io.images import
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|