pykrait 0.2.4__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.2.4 → pykrait-0.3.1}/PKG-INFO +3 -1
- {pykrait-0.2.4 → pykrait-0.3.1}/pyproject.toml +15 -2
- {pykrait-0.2.4 → pykrait-0.3.1}/src/pykrait/__main__.py +1 -0
- {pykrait-0.2.4 → pykrait-0.3.1}/src/pykrait/gui/app.py +29 -4
- pykrait-0.3.1/src/pykrait/gui/async_workers.py +323 -0
- pykrait-0.3.1/src/pykrait/gui/custom_widgets.py +159 -0
- {pykrait-0.2.4 → pykrait-0.3.1}/src/pykrait/gui/fileselection.py +224 -80
- pykrait-0.3.1/src/pykrait/gui/mainwindow.py +1114 -0
- pykrait-0.3.1/src/pykrait/io/files.py +191 -0
- pykrait-0.3.1/src/pykrait/io/images.py +263 -0
- pykrait-0.3.1/src/pykrait/pipeline/pipeline.py +804 -0
- {pykrait-0.2.4 → pykrait-0.3.1}/src/pykrait/preprocessing/segmentation.py +57 -32
- {pykrait-0.2.4 → pykrait-0.3.1}/src/pykrait/preprocessing/timeseries_extraction.py +175 -63
- {pykrait-0.2.4 → pykrait-0.3.1}/src/pykrait/trace_analysis/filtering.py +26 -14
- {pykrait-0.2.4 → pykrait-0.3.1}/src/pykrait/trace_analysis/oscillations.py +79 -19
- pykrait-0.3.1/src/pykrait/trace_analysis/peak_analysis.py +176 -0
- pykrait-0.3.1/src/pykrait/trace_analysis/synchronicity.py +204 -0
- pykrait-0.3.1/src/pykrait/visualization/image_overlays.py +236 -0
- pykrait-0.2.4/src/pykrait/gui/async_workers.py +0 -218
- pykrait-0.2.4/src/pykrait/gui/mainwindow.py +0 -614
- pykrait-0.2.4/src/pykrait/io/io.py +0 -248
- pykrait-0.2.4/src/pykrait/pipeline/pipeline.py +0 -407
- pykrait-0.2.4/src/pykrait/trace_analysis/peak_analysis.py +0 -45
- {pykrait-0.2.4 → pykrait-0.3.1}/.gitignore +0 -0
- {pykrait-0.2.4 → pykrait-0.3.1}/LICENSE +0 -0
- {pykrait-0.2.4 → pykrait-0.3.1}/README.md +0 -0
- {pykrait-0.2.4 → pykrait-0.3.1}/src/.python-version +0 -0
- {pykrait-0.2.4 → pykrait-0.3.1}/src/pykrait/__init__.py +0 -0
- {pykrait-0.2.4 → pykrait-0.3.1}/src/pykrait/gui/resources/logo.png +0 -0
- {pykrait-0.2.4 → pykrait-0.3.1}/src/pykrait/io/__init__.py +0 -0
- {pykrait-0.2.4 → pykrait-0.3.1}/src/pykrait/pipeline/__init__.py +0 -0
- {pykrait-0.2.4 → pykrait-0.3.1}/src/pykrait/preprocessing/__init__.py +0 -0
- {pykrait-0.2.4 → pykrait-0.3.1}/src/pykrait/trace_analysis/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pykrait
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -28,8 +28,10 @@ Requires-Dist: pydantic<2
|
|
|
28
28
|
Requires-Dist: pyyaml>=6.0.2
|
|
29
29
|
Requires-Dist: resource-backed-dask-array>=0.1.0
|
|
30
30
|
Requires-Dist: scikit-image>=0.25.2
|
|
31
|
+
Requires-Dist: scikit-learn>=1.7.2
|
|
31
32
|
Requires-Dist: scipy>=1.15.3
|
|
32
33
|
Requires-Dist: xarray>2025
|
|
34
|
+
Requires-Dist: zstandard>=0.23.0
|
|
33
35
|
Provides-Extra: dev
|
|
34
36
|
Requires-Dist: build>=1.2.2.post1; extra == 'dev'
|
|
35
37
|
Requires-Dist: hatch>=1.14.1; extra == 'dev'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pykrait"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
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" }
|
|
@@ -33,6 +33,8 @@ dependencies = [
|
|
|
33
33
|
"scipy>=1.15.3",
|
|
34
34
|
"xarray>2025",
|
|
35
35
|
"pandas>=2.3.1",
|
|
36
|
+
"zstandard>=0.23.0",
|
|
37
|
+
"scikit-learn>=1.7.2",
|
|
36
38
|
]
|
|
37
39
|
|
|
38
40
|
[project.optional-dependencies]
|
|
@@ -68,6 +70,17 @@ packages = ["src/pykrait"]
|
|
|
68
70
|
[tool.hatch.build.targets.sdist]
|
|
69
71
|
include = ["src/**", "README.md", "pyproject.toml"]
|
|
70
72
|
|
|
73
|
+
[tool.hatch.envs.tests]
|
|
74
|
+
dependencies = [
|
|
75
|
+
"pytest",
|
|
76
|
+
"pytest-cov",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
[tool.hatch.envs.coverage]
|
|
80
|
+
dependencies = [
|
|
81
|
+
"coverage[toml]",
|
|
82
|
+
]
|
|
83
|
+
|
|
71
84
|
[tool.pytest.ini_options]
|
|
72
85
|
pythonpath = ["src"]
|
|
73
86
|
testpaths = ["tests"]
|
|
@@ -85,4 +98,4 @@ skip_covered = true
|
|
|
85
98
|
[tool.ruff]
|
|
86
99
|
src = ["src"]
|
|
87
100
|
line-length = 88
|
|
88
|
-
target-version = "py311"
|
|
101
|
+
target-version = "py311"
|
|
@@ -1,23 +1,41 @@
|
|
|
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
|
|
6
7
|
from pykrait.gui.fileselection import FileSelectionWindow
|
|
7
8
|
from pykrait.gui.mainwindow import MainWindow
|
|
9
|
+
from pykrait.io.files import get_pykrait_version
|
|
10
|
+
|
|
11
|
+
def warning_no_source_line(message, category, filename, lineno, file=None, line=None):
|
|
12
|
+
return f"{filename}:{lineno}: {category.__name__}: {message}\n"
|
|
8
13
|
|
|
9
14
|
class AppController:
|
|
10
15
|
def __init__(self):
|
|
16
|
+
|
|
17
|
+
# format warnings to not print the source line every time
|
|
18
|
+
warnings.formatwarning = warning_no_source_line
|
|
19
|
+
|
|
11
20
|
self.app = QApplication(sys.argv)
|
|
12
21
|
|
|
13
22
|
# Loading icon from resources
|
|
14
23
|
appIcon = QIcon()
|
|
15
24
|
try:
|
|
16
25
|
from importlib.resources import files
|
|
26
|
+
|
|
17
27
|
icon_path = files("pykrait.gui.resources").joinpath("logo.png")
|
|
18
28
|
os.path.exists(icon_path)
|
|
19
29
|
except Exception:
|
|
20
|
-
icon_path = os.path.abspath(
|
|
30
|
+
icon_path = os.path.abspath(
|
|
31
|
+
os.path.join(
|
|
32
|
+
os.path.dirname(__file__),
|
|
33
|
+
"..",
|
|
34
|
+
"..",
|
|
35
|
+
"pykrait/gui/resources",
|
|
36
|
+
"logo.png",
|
|
37
|
+
)
|
|
38
|
+
)
|
|
21
39
|
os.path.exists(icon_path)
|
|
22
40
|
if os.path.exists(icon_path):
|
|
23
41
|
appIcon.addFile(str(icon_path), QSize(16, 16))
|
|
@@ -32,10 +50,17 @@ class AppController:
|
|
|
32
50
|
|
|
33
51
|
# Set application name and version
|
|
34
52
|
self.app.setApplicationName("pyKrait")
|
|
35
|
-
|
|
53
|
+
version = get_pykrait_version()
|
|
54
|
+
if version is not None:
|
|
55
|
+
self.app.setApplicationVersion(version)
|
|
36
56
|
self.file_selection = FileSelectionWindow()
|
|
37
57
|
self.file_selection.analysis_complete.connect(self.on_analysis_complete)
|
|
38
58
|
self.file_selection.show()
|
|
59
|
+
|
|
60
|
+
#updating the style
|
|
61
|
+
self.app.setStyle("Fusion")
|
|
62
|
+
light_palette = self.app.style().standardPalette()
|
|
63
|
+
self.app.setPalette(light_palette)
|
|
39
64
|
|
|
40
65
|
# Keep references to windows
|
|
41
66
|
self.main_window = None
|
|
@@ -52,7 +77,7 @@ class AppController:
|
|
|
52
77
|
mean_intensities=results["mean_intensities"],
|
|
53
78
|
analysis_output=results["analysis_output"],
|
|
54
79
|
analysis_params=results["analysis_parameters"],
|
|
55
|
-
app_controller=self
|
|
80
|
+
app_controller=self,
|
|
56
81
|
)
|
|
57
82
|
self.main_window.show()
|
|
58
83
|
|
|
@@ -66,4 +91,4 @@ class AppController:
|
|
|
66
91
|
|
|
67
92
|
self.file_selection = FileSelectionWindow()
|
|
68
93
|
self.file_selection.analysis_complete.connect(self.on_analysis_complete)
|
|
69
|
-
self.file_selection.show()
|
|
94
|
+
self.file_selection.show()
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
from PySide6.QtCore import QObject, Signal, Slot
|
|
2
|
+
import traceback
|
|
3
|
+
import numpy as np
|
|
4
|
+
from pykrait.io.images import read_label_image, get_pixel_contour_from_label_img
|
|
5
|
+
from pykrait.io.files import read_Txnrois
|
|
6
|
+
from pykrait.preprocessing.segmentation import timelapse_projection
|
|
7
|
+
import tifffile
|
|
8
|
+
from pykrait.preprocessing.timeseries_extraction import extract_mean_intensities, get_adjacency_matrix
|
|
9
|
+
from pykrait.trace_analysis.filtering import detrend_with_sinc_filter
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from pykrait.pipeline.pipeline import (
|
|
13
|
+
AnalysisParameters,
|
|
14
|
+
AnalysisOutput,
|
|
15
|
+
create_analysis_folder,
|
|
16
|
+
load_timelapse,
|
|
17
|
+
create_masks,
|
|
18
|
+
do_peak_analysis,
|
|
19
|
+
calculate_periodic_cells,
|
|
20
|
+
calculate_synchronicity,
|
|
21
|
+
save_analysis_results,
|
|
22
|
+
save_params
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
class ExtractIntensitiesWorker(QObject):
|
|
26
|
+
"""
|
|
27
|
+
Worker for extracting mean intensities from a timelapse dataset, using either Cellpose segmentation or a label image.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
progress_changed = Signal(int, str) # progress percent, status message
|
|
31
|
+
finished = Signal(dict) # analysis results
|
|
32
|
+
error = Signal(str) # error message
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
analysis_params: AnalysisParameters,
|
|
37
|
+
output_params: AnalysisOutput,
|
|
38
|
+
mode: str,
|
|
39
|
+
mean_intensities_path: str = None,
|
|
40
|
+
):
|
|
41
|
+
super().__init__()
|
|
42
|
+
self.analysis_parameters = analysis_params
|
|
43
|
+
self.analysis_output = output_params
|
|
44
|
+
self.mode = mode
|
|
45
|
+
self.mean_intensities_path = mean_intensities_path
|
|
46
|
+
|
|
47
|
+
@Slot()
|
|
48
|
+
def run(self):
|
|
49
|
+
try:
|
|
50
|
+
self.analysis_output = create_analysis_folder(self.analysis_output)
|
|
51
|
+
print(f"Running analysis on {self.analysis_output.filename}")
|
|
52
|
+
# loading the timelapse data and analysing frame interval
|
|
53
|
+
self.timelapse_data, self.analysis_output = load_timelapse(
|
|
54
|
+
self.analysis_output, self.analysis_parameters
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if self.mode == "cellpose":
|
|
58
|
+
# perform the tproj
|
|
59
|
+
self.progress_changed.emit(
|
|
60
|
+
10, "Performing T Projection and Cellpose Segmentation..."
|
|
61
|
+
)
|
|
62
|
+
# creating the masks and saving them to the analysis folder
|
|
63
|
+
_, _, self.masks, self.analysis_output = create_masks(
|
|
64
|
+
self.timelapse_data, self.analysis_output, self.analysis_parameters
|
|
65
|
+
)
|
|
66
|
+
# extracts the mean intensities of the masks
|
|
67
|
+
self.progress_changed.emit(60, "Extracting intensities...")
|
|
68
|
+
mean_intensities = extract_mean_intensities(
|
|
69
|
+
self.timelapse_data, self.masks
|
|
70
|
+
)
|
|
71
|
+
elif self.mode == "label_image":
|
|
72
|
+
if self.analysis_output.tproj_path is None:
|
|
73
|
+
print("Performing T Projection...")
|
|
74
|
+
tproj = timelapse_projection(
|
|
75
|
+
self.timelapse_data,
|
|
76
|
+
method="std",
|
|
77
|
+
normalize=True,
|
|
78
|
+
verbose=True,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
filename_wo_ext = os.path.splitext(self.analysis_output.filename)[0]
|
|
82
|
+
self.analysis_output.tproj_path = os.path.join(
|
|
83
|
+
self.analysis_output.analysis_folder, f"{filename_wo_ext}_tproj.ome.tif"
|
|
84
|
+
)
|
|
85
|
+
tifffile.imwrite(
|
|
86
|
+
self.analysis_output.tproj_path,
|
|
87
|
+
tproj.astype(np.uint16),
|
|
88
|
+
metadata={"axes": "CYX"},
|
|
89
|
+
compression="zlib",
|
|
90
|
+
)
|
|
91
|
+
self.progress_changed.emit(20, "Loading label image...")
|
|
92
|
+
self.masks = read_label_image(self.analysis_output.masks_path)
|
|
93
|
+
# extracts the mean intensities of the masks
|
|
94
|
+
self.progress_changed.emit(60, "Extracting intensities...")
|
|
95
|
+
mean_intensities = extract_mean_intensities(
|
|
96
|
+
self.timelapse_data, self.masks
|
|
97
|
+
)
|
|
98
|
+
elif self.mode == "csv":
|
|
99
|
+
self.progress_changed.emit(20, "Loading label image...")
|
|
100
|
+
self.masks = read_label_image(self.analysis_output.masks_path)
|
|
101
|
+
self.progress_changed.emit(60, "Reading intensities...")
|
|
102
|
+
mean_intensities = read_Txnrois(
|
|
103
|
+
self.mean_intensities_path,
|
|
104
|
+
n_frames=self.timelapse_data.shape[0],
|
|
105
|
+
n_rois=self.masks.max(),
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
raise ValueError("Unknown mode.")
|
|
109
|
+
|
|
110
|
+
(
|
|
111
|
+
self.analysis_output.number_of_frames,
|
|
112
|
+
self.analysis_output.number_of_cells,
|
|
113
|
+
) = mean_intensities.shape
|
|
114
|
+
|
|
115
|
+
# All done
|
|
116
|
+
self.progress_changed.emit(90, "Calculating ROI boundaries...")
|
|
117
|
+
rois = get_pixel_contour_from_label_img(
|
|
118
|
+
self.masks, orig_shape=self.masks.shape, target_shape=(512, 512)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
self.progress_changed.emit(100, "Done.")
|
|
122
|
+
|
|
123
|
+
results = {
|
|
124
|
+
"frames": self.timelapse_data,
|
|
125
|
+
"analysis_output": self.analysis_output,
|
|
126
|
+
"analysis_parameters": self.analysis_parameters,
|
|
127
|
+
"masks": self.masks,
|
|
128
|
+
"rois": rois,
|
|
129
|
+
"mean_intensities": mean_intensities,
|
|
130
|
+
}
|
|
131
|
+
save_params(self.analysis_output, self.analysis_parameters, overwrite=True)
|
|
132
|
+
self.finished.emit(results)
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
tb = traceback.format_exc()
|
|
136
|
+
print(f"Error during analysis: {e}\n{tb}")
|
|
137
|
+
self.error.emit(str(e))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class DetrendWorker(QObject):
|
|
141
|
+
"""
|
|
142
|
+
_summary_
|
|
143
|
+
|
|
144
|
+
:param QObject: _description_
|
|
145
|
+
:type QObject: _type_
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
finished = Signal(np.ndarray)
|
|
149
|
+
error = Signal(Exception)
|
|
150
|
+
|
|
151
|
+
def __init__(self, intensities, sinc_window, frame_interval):
|
|
152
|
+
super().__init__()
|
|
153
|
+
self.intensities = intensities
|
|
154
|
+
self.sinc_window = sinc_window
|
|
155
|
+
self.frame_interval = frame_interval
|
|
156
|
+
|
|
157
|
+
def run(self):
|
|
158
|
+
try:
|
|
159
|
+
# Call your actual function here
|
|
160
|
+
detrended = detrend_with_sinc_filter(
|
|
161
|
+
signals=self.intensities,
|
|
162
|
+
cutoff_period=self.sinc_window,
|
|
163
|
+
sampling_interval=self.frame_interval,
|
|
164
|
+
)
|
|
165
|
+
self.finished.emit(detrended)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
self.error.emit(e)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class PeakDetectionWorker(QObject):
|
|
171
|
+
"""
|
|
172
|
+
Worker for detecting peaks in detrended traces, wraps the `find_peaks` function.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
finished = Signal(np.ndarray)
|
|
176
|
+
error = Signal(Exception)
|
|
177
|
+
|
|
178
|
+
def __init__(self, detrended_traces, min_width, max_width, analysis_params, analysis_output):
|
|
179
|
+
super().__init__()
|
|
180
|
+
self.traces = detrended_traces
|
|
181
|
+
self.min_width = min_width
|
|
182
|
+
self.max_width = max_width
|
|
183
|
+
self.analysis_params = analysis_params
|
|
184
|
+
self.analysis_output = analysis_output
|
|
185
|
+
|
|
186
|
+
def run(self):
|
|
187
|
+
try:
|
|
188
|
+
peak_series, peak_properties_df, peak_frame_nonzero, analysis_output = do_peak_analysis(self.traces, self.min_width, self.max_width, self.analysis_params, self.analysis_output)
|
|
189
|
+
result = [peak_series, analysis_output]
|
|
190
|
+
self.finished.emit(result)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
self.error.emit(e)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class PeriodicCellWorker(QObject):
|
|
196
|
+
"""
|
|
197
|
+
Worker for finding oscillating ROIs in the peak series, wraps the `find_oscillating_rois` function.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
finished = Signal(AnalysisOutput, np.ndarray, np.ndarray)
|
|
201
|
+
error = Signal(Exception)
|
|
202
|
+
|
|
203
|
+
def __init__(self, peaks: np.ndarray, analysis_params, analysis_output):
|
|
204
|
+
super().__init__()
|
|
205
|
+
self.peaks = peaks
|
|
206
|
+
self.analysis_output = analysis_output
|
|
207
|
+
self.analysis_params = analysis_params
|
|
208
|
+
|
|
209
|
+
def run(self):
|
|
210
|
+
try:
|
|
211
|
+
analysis_output, stds, covs = calculate_periodic_cells(
|
|
212
|
+
self.peaks, self.analysis_params, self.analysis_output
|
|
213
|
+
)
|
|
214
|
+
self.finished.emit(analysis_output, stds, covs)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
self.error.emit(e)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class AdjacencyMatrixWorker(QObject):
|
|
220
|
+
"""
|
|
221
|
+
Worker for calculating the adjacency matrix from the masks, wraps the `get_adjacency_matrix` function.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
finished = Signal(np.ndarray, np.ndarray)
|
|
225
|
+
error = Signal(Exception)
|
|
226
|
+
|
|
227
|
+
def __init__(self, masks: np.ndarray, neighbour_tolerance_pixels: int = 1):
|
|
228
|
+
super().__init__()
|
|
229
|
+
self.masks = masks
|
|
230
|
+
self.neighbour_tolerance_pixels = neighbour_tolerance_pixels
|
|
231
|
+
|
|
232
|
+
def run(self):
|
|
233
|
+
try:
|
|
234
|
+
adjacency_matrix, shortest_path_matrix = get_adjacency_matrix(
|
|
235
|
+
self.masks, neighbour_tolerance=self.neighbour_tolerance_pixels
|
|
236
|
+
)
|
|
237
|
+
self.finished.emit(adjacency_matrix, shortest_path_matrix)
|
|
238
|
+
except Exception as e:
|
|
239
|
+
self.error.emit(e)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class SynchronicityWorker(QObject):
|
|
243
|
+
"""
|
|
244
|
+
Worker for calculating the synchronicity matrix from the detrended intensities, wraps the `calculate_synchronicity_matrix` function.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
finished = Signal(np.ndarray)
|
|
248
|
+
error = Signal(Exception)
|
|
249
|
+
|
|
250
|
+
def __init__(self, analysis_output:AnalysisOutput, analysis_parameters:AnalysisParameters, peak_series: np.ndarray, shortest_path_matrix: np.ndarray):
|
|
251
|
+
super().__init__()
|
|
252
|
+
self.peak_series = peak_series
|
|
253
|
+
self.shortest_path_matrix = shortest_path_matrix
|
|
254
|
+
self.analysis_output = analysis_output
|
|
255
|
+
self.analysis_parameters = analysis_parameters
|
|
256
|
+
|
|
257
|
+
def run(self):
|
|
258
|
+
try:
|
|
259
|
+
results = calculate_synchronicity(
|
|
260
|
+
self.shortest_path_matrix,
|
|
261
|
+
self.peak_series,
|
|
262
|
+
self.analysis_parameters,
|
|
263
|
+
self.analysis_output
|
|
264
|
+
)
|
|
265
|
+
self.finished.emit(results)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
self.error.emit(e)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class AnalysisSaveWorker(QObject):
|
|
271
|
+
"""
|
|
272
|
+
Worker for saving the analysis results, wraps the `save_analysis_results` function.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
finished = Signal(str) # emit output file path or summary message
|
|
276
|
+
error = Signal(Exception)
|
|
277
|
+
|
|
278
|
+
def __init__(
|
|
279
|
+
self,
|
|
280
|
+
tproj:np.ndarray,
|
|
281
|
+
masks:np.ndarray,
|
|
282
|
+
synchronous_peaks_matrix:np.ndarray,
|
|
283
|
+
analysis_output,
|
|
284
|
+
analysis_params,
|
|
285
|
+
peaks: np.ndarray,
|
|
286
|
+
detrended_intensities: np.ndarray,
|
|
287
|
+
intensities: np.ndarray,
|
|
288
|
+
stds: np.ndarray = None,
|
|
289
|
+
covs: np.ndarray = None,
|
|
290
|
+
overwrite: bool = False,
|
|
291
|
+
):
|
|
292
|
+
super().__init__()
|
|
293
|
+
self.tproj = tproj
|
|
294
|
+
self.masks = masks
|
|
295
|
+
self.synchronous_peaks_matrix = synchronous_peaks_matrix
|
|
296
|
+
self.analysis_output = analysis_output
|
|
297
|
+
self.analysis_params = analysis_params
|
|
298
|
+
self.peaks = peaks
|
|
299
|
+
self.detrended_intensities = detrended_intensities
|
|
300
|
+
self.intensities = intensities
|
|
301
|
+
self.overwrite = overwrite
|
|
302
|
+
self.stds = stds
|
|
303
|
+
self.covs = covs
|
|
304
|
+
|
|
305
|
+
def run(self):
|
|
306
|
+
try:
|
|
307
|
+
save_analysis_results(
|
|
308
|
+
tproj=self.tproj,
|
|
309
|
+
masks=self.masks,
|
|
310
|
+
synchronous_peaks_matrix=self.synchronous_peaks_matrix,
|
|
311
|
+
analysis_output=self.analysis_output,
|
|
312
|
+
analysis_params=self.analysis_params,
|
|
313
|
+
detrended_intensities=self.detrended_intensities,
|
|
314
|
+
peaks=self.peaks,
|
|
315
|
+
intensities=self.intensities,
|
|
316
|
+
stds=self.stds,
|
|
317
|
+
covs=self.covs,
|
|
318
|
+
overwrite=self.overwrite,
|
|
319
|
+
)
|
|
320
|
+
self.finished.emit(f"Saved to {self.analysis_output.analysis_folder}")
|
|
321
|
+
|
|
322
|
+
except Exception as e:
|
|
323
|
+
self.error.emit(e)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from PySide6.QtWidgets import (
|
|
2
|
+
QWidget, QLabel, QHBoxLayout, QVBoxLayout, QToolButton, QFrame
|
|
3
|
+
)
|
|
4
|
+
from PySide6.QtCore import Qt, Signal, QRectF, QSize
|
|
5
|
+
from PySide6.QtGui import QPainter, QColor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Slider(QWidget):
|
|
9
|
+
"""Inner slider that paints the track and knob."""
|
|
10
|
+
toggled = Signal(bool)
|
|
11
|
+
|
|
12
|
+
def __init__(self, width=50, height=20, parent=None):
|
|
13
|
+
super().__init__(parent)
|
|
14
|
+
self._checked = False
|
|
15
|
+
self.setFixedSize(width, height)
|
|
16
|
+
self.setCursor(Qt.PointingHandCursor)
|
|
17
|
+
|
|
18
|
+
def paintEvent(self, event):
|
|
19
|
+
palette = self.palette()
|
|
20
|
+
painter = QPainter(self)
|
|
21
|
+
painter.setRenderHint(QPainter.Antialiasing)
|
|
22
|
+
|
|
23
|
+
# Theme colors
|
|
24
|
+
color_on = QColor("#DC5801") # ON highlight
|
|
25
|
+
color_off = QColor("#8D0F70") # OFF highlight
|
|
26
|
+
|
|
27
|
+
base = palette.base().color()
|
|
28
|
+
knob_color = palette.window().color() if base.lightness() < 128 else QColor("white")
|
|
29
|
+
|
|
30
|
+
# Track
|
|
31
|
+
track_rect = QRectF(0, 0, self.width(), self.height())
|
|
32
|
+
track_color = color_on if self._checked else color_off
|
|
33
|
+
painter.setBrush(track_color)
|
|
34
|
+
painter.setPen(Qt.NoPen)
|
|
35
|
+
painter.drawRoundedRect(track_rect, self.height() / 2, self.height() / 2)
|
|
36
|
+
|
|
37
|
+
# Knob
|
|
38
|
+
knob_size = self.height() - 4
|
|
39
|
+
knob_x = self.width() - knob_size - 2 if self._checked else 2
|
|
40
|
+
knob_rect = QRectF(knob_x, 2, knob_size, knob_size)
|
|
41
|
+
painter.setBrush(knob_color)
|
|
42
|
+
painter.drawEllipse(knob_rect)
|
|
43
|
+
|
|
44
|
+
def mousePressEvent(self, event):
|
|
45
|
+
self._checked = not self._checked
|
|
46
|
+
self.toggled.emit(self._checked)
|
|
47
|
+
self.update()
|
|
48
|
+
|
|
49
|
+
def is_checked(self):
|
|
50
|
+
return self._checked
|
|
51
|
+
|
|
52
|
+
def set_checked(self, value: bool):
|
|
53
|
+
self._checked = value
|
|
54
|
+
self.update()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ToggleSwitch(QWidget):
|
|
58
|
+
"""Boolean toggle with labels outside the slider."""
|
|
59
|
+
toggled = Signal(bool)
|
|
60
|
+
|
|
61
|
+
def __init__(self, label_off="STD", label_on="CoV", parent=None):
|
|
62
|
+
super().__init__(parent)
|
|
63
|
+
self.label_off = label_off
|
|
64
|
+
self.label_on = label_on
|
|
65
|
+
|
|
66
|
+
layout = QHBoxLayout(self)
|
|
67
|
+
layout.setContentsMargins(5, 0, 5, 0)
|
|
68
|
+
layout.setSpacing(5)
|
|
69
|
+
|
|
70
|
+
self.left_label = QLabel(self.label_off)
|
|
71
|
+
self.left_label.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
|
72
|
+
layout.addWidget(self.left_label)
|
|
73
|
+
|
|
74
|
+
self.slider = Slider()
|
|
75
|
+
self.slider.toggled.connect(self._emit_signal)
|
|
76
|
+
layout.addWidget(self.slider)
|
|
77
|
+
|
|
78
|
+
self.right_label = QLabel(self.label_on)
|
|
79
|
+
self.right_label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
|
|
80
|
+
layout.addWidget(self.right_label)
|
|
81
|
+
|
|
82
|
+
def _emit_signal(self, state):
|
|
83
|
+
self.toggled.emit(state)
|
|
84
|
+
|
|
85
|
+
def is_checked(self):
|
|
86
|
+
return self.slider.is_checked()
|
|
87
|
+
|
|
88
|
+
def set_checked(self, value: bool):
|
|
89
|
+
self.slider.set_checked(value)
|
|
90
|
+
|
|
91
|
+
def value(self) -> str:
|
|
92
|
+
return self.label_on if self.slider.is_checked() else self.label_off
|
|
93
|
+
|
|
94
|
+
def set_labels(self, label_on: str, label_off: str):
|
|
95
|
+
self.label_on = label_on
|
|
96
|
+
self.label_off = label_off
|
|
97
|
+
self.left_label.setText(label_off)
|
|
98
|
+
self.right_label.setText(label_on)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class CollapsibleBox(QWidget):
|
|
102
|
+
"""Collapsible box that adapts to theme colors."""
|
|
103
|
+
def __init__(self, title="", parent=None, title_font_size=12, arrow_size=10):
|
|
104
|
+
super().__init__(parent)
|
|
105
|
+
|
|
106
|
+
self.toggle_button = QToolButton(text=title, checkable=True, checked=True)
|
|
107
|
+
self.toggle_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
|
|
108
|
+
self.toggle_button.setArrowType(Qt.DownArrow)
|
|
109
|
+
self.toggle_button.setIconSize(QSize(arrow_size, arrow_size))
|
|
110
|
+
self.toggle_button.setStyleSheet(f"""
|
|
111
|
+
QToolButton {{
|
|
112
|
+
border: none;
|
|
113
|
+
font-weight: bold;
|
|
114
|
+
font-size: {title_font_size}pt;
|
|
115
|
+
padding: 5px;
|
|
116
|
+
}}
|
|
117
|
+
""")
|
|
118
|
+
self.toggle_button.clicked.connect(self.on_toggle)
|
|
119
|
+
|
|
120
|
+
# Background frame uses palette colors
|
|
121
|
+
self.bg_frame = QFrame()
|
|
122
|
+
self.bg_frame.setAutoFillBackground(True)
|
|
123
|
+
self.update_bg_color()
|
|
124
|
+
|
|
125
|
+
self.content_container = QWidget(self.bg_frame)
|
|
126
|
+
content_layout = QVBoxLayout(self.content_container)
|
|
127
|
+
content_layout.setContentsMargins(10, 10, 10, 10)
|
|
128
|
+
content_layout.setSpacing(5)
|
|
129
|
+
self.content_layout = content_layout
|
|
130
|
+
|
|
131
|
+
frame_layout = QVBoxLayout(self.bg_frame)
|
|
132
|
+
frame_layout.setContentsMargins(0, 0, 0, 0)
|
|
133
|
+
frame_layout.addWidget(self.content_container)
|
|
134
|
+
|
|
135
|
+
main_layout = QVBoxLayout(self)
|
|
136
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
137
|
+
main_layout.addWidget(self.toggle_button)
|
|
138
|
+
main_layout.addWidget(self.bg_frame)
|
|
139
|
+
|
|
140
|
+
self._expanded = True
|
|
141
|
+
|
|
142
|
+
def update_bg_color(self):
|
|
143
|
+
"""Set background color dynamically based on theme."""
|
|
144
|
+
palette = self.palette()
|
|
145
|
+
bg = palette.alternateBase().color()
|
|
146
|
+
self.bg_frame.setStyleSheet(f"""
|
|
147
|
+
QFrame {{
|
|
148
|
+
background-color: {bg.name()};
|
|
149
|
+
border-radius: 8px;
|
|
150
|
+
}}
|
|
151
|
+
""")
|
|
152
|
+
|
|
153
|
+
def on_toggle(self):
|
|
154
|
+
self._expanded = not self._expanded
|
|
155
|
+
self.bg_frame.setVisible(self._expanded)
|
|
156
|
+
self.toggle_button.setArrowType(Qt.DownArrow if self._expanded else Qt.RightArrow)
|
|
157
|
+
|
|
158
|
+
def add_widget(self, widget):
|
|
159
|
+
self.content_layout.addWidget(widget)
|