FlowCyPy 1.0.4__cp313-cp313-macosx_26_0_arm64.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.
- FlowCyPy/.dylibs/libfftw3.3.dylib +0 -0
- FlowCyPy/.dylibs/libgcc_s.1.1.dylib +0 -0
- FlowCyPy/.dylibs/libgomp.1.dylib +0 -0
- FlowCyPy/.dylibs/libstdc++.6.dylib +0 -0
- FlowCyPy/__init__.py +60 -0
- FlowCyPy/_utils.cpython-313-darwin.so +0 -0
- FlowCyPy/_version.py +24 -0
- FlowCyPy/digital_processing/__init__.py +3 -0
- FlowCyPy/digital_processing/classifier.cpython-313-darwin.so +0 -0
- FlowCyPy/digital_processing/discriminator.cpython-313-darwin.so +0 -0
- FlowCyPy/digital_processing/libclassifier_lib.a +0 -0
- FlowCyPy/digital_processing/libdiscriminator_lib.a +0 -0
- FlowCyPy/digital_processing/libpeak_locator_lib.a +0 -0
- FlowCyPy/digital_processing/peak_locator.cpython-313-darwin.so +0 -0
- FlowCyPy/digital_processing/pipeline.py +32 -0
- FlowCyPy/directories.py +30 -0
- FlowCyPy/flow_cytometer.py +492 -0
- FlowCyPy/fluidics/__init__.py +72 -0
- FlowCyPy/fluidics/distributions.cpython-313-darwin.so +0 -0
- FlowCyPy/fluidics/event_collection.py +1082 -0
- FlowCyPy/fluidics/flow_cell.cpython-313-darwin.so +0 -0
- FlowCyPy/fluidics/libdistributions_lib.a +0 -0
- FlowCyPy/fluidics/libflow_cell_lib.a +0 -0
- FlowCyPy/fluidics/libpopulation_events_lib.a +0 -0
- FlowCyPy/fluidics/libpopulations_lib.a +0 -0
- FlowCyPy/fluidics/libscatterer_collection_lib.a +0 -0
- FlowCyPy/fluidics/population_events.cpython-313-darwin.so +0 -0
- FlowCyPy/fluidics/populations.cpython-313-darwin.so +0 -0
- FlowCyPy/fluidics/scatterer_collection.cpython-313-darwin.so +0 -0
- FlowCyPy/fluidics/system.py +307 -0
- FlowCyPy/interface_pint.cpython-313-darwin.so +0 -0
- FlowCyPy/libpint.a +0 -0
- FlowCyPy/libutils_lib.a +0 -0
- FlowCyPy/opto_electronics/__init__.py +50 -0
- FlowCyPy/opto_electronics/amplifier.cpython-313-darwin.so +0 -0
- FlowCyPy/opto_electronics/circuits.cpython-313-darwin.so +0 -0
- FlowCyPy/opto_electronics/coupling_model.cpython-313-darwin.so +0 -0
- FlowCyPy/opto_electronics/detector.cpython-313-darwin.so +0 -0
- FlowCyPy/opto_electronics/digitizer.cpython-313-darwin.so +0 -0
- FlowCyPy/opto_electronics/libamplifier_lib.a +0 -0
- FlowCyPy/opto_electronics/libcircuits_lib.a +0 -0
- FlowCyPy/opto_electronics/libcoupling_model_lib.a +0 -0
- FlowCyPy/opto_electronics/libdetector_lib.a +0 -0
- FlowCyPy/opto_electronics/libdigitizer_lib.a +0 -0
- FlowCyPy/opto_electronics/libsource_lib.a +0 -0
- FlowCyPy/opto_electronics/source.cpython-313-darwin.so +0 -0
- FlowCyPy/opto_electronics/system.py +285 -0
- FlowCyPy/presets/__init__.py +3 -0
- FlowCyPy/presets/detector.py +86 -0
- FlowCyPy/presets/flow_cytometer.py +426 -0
- FlowCyPy/presets/population.py +110 -0
- FlowCyPy/run_record.py +479 -0
- FlowCyPy/sub_frames/__init__.py +0 -0
- FlowCyPy/sub_frames/acquisition.py +363 -0
- FlowCyPy/sub_frames/classifier.py +60 -0
- FlowCyPy/sub_frames/events.py +147 -0
- FlowCyPy/sub_frames/peak_metrics.py +47 -0
- FlowCyPy/sub_frames/peaks.py +901 -0
- FlowCyPy/sub_frames/triggered.py +243 -0
- FlowCyPy/units.py +17 -0
- FlowCyPy/workflow.py +286 -0
- flowcypy-1.0.4.dist-info/METADATA +493 -0
- flowcypy-1.0.4.dist-info/RECORD +65 -0
- flowcypy-1.0.4.dist-info/WHEEL +6 -0
- flowcypy-1.0.4.dist-info/licenses/LICENSE +21 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
FlowCyPy/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from ._version import version as __version__ # noqa: F401
|
|
3
|
+
|
|
4
|
+
except ImportError:
|
|
5
|
+
__version__ = "0.0.0"
|
|
6
|
+
|
|
7
|
+
debug_mode = False
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_LAZY_EXPORTS = {
|
|
11
|
+
"FlowCytometer": (".flow_cytometer", "FlowCytometer"),
|
|
12
|
+
"Workflow": (".workflow", "Workflow"),
|
|
13
|
+
"Fluidics": (".fluidics", "Fluidics"),
|
|
14
|
+
"distributions": (".fluidics", "distributions"),
|
|
15
|
+
"populations": (".fluidics", "populations"),
|
|
16
|
+
"OptoElectronics": (".opto_electronics", "OptoElectronics"),
|
|
17
|
+
"circuits": (".opto_electronics", "circuits"),
|
|
18
|
+
"source": (".opto_electronics", "source"),
|
|
19
|
+
"DigitalProcessing": (".digital_processing", "DigitalProcessing"),
|
|
20
|
+
"classifier": (".digital_processing", "classifier"),
|
|
21
|
+
"discriminator": (".digital_processing", "discriminator"),
|
|
22
|
+
"peak_locator": (".digital_processing", "peak_locator"),
|
|
23
|
+
"ureg": (".units", "ureg"),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def __getattr__(name: str):
|
|
28
|
+
"""Resolve public objects only when they are first accessed.
|
|
29
|
+
|
|
30
|
+
Keeping the compatibility exports lazy makes ``import FlowCyPy`` cheap
|
|
31
|
+
while retaining the established top-level API.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
module_name, attribute_name = _LAZY_EXPORTS[name]
|
|
35
|
+
except KeyError as error:
|
|
36
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from error
|
|
37
|
+
|
|
38
|
+
from importlib import import_module
|
|
39
|
+
|
|
40
|
+
value = getattr(import_module(module_name, __name__), attribute_name)
|
|
41
|
+
globals()[name] = value
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"__version__",
|
|
46
|
+
"FlowCytometer",
|
|
47
|
+
"Workflow",
|
|
48
|
+
"Fluidics",
|
|
49
|
+
"distributions",
|
|
50
|
+
"populations",
|
|
51
|
+
"OptoElectronics",
|
|
52
|
+
"circuits",
|
|
53
|
+
"source",
|
|
54
|
+
"DigitalProcessing",
|
|
55
|
+
"classifier",
|
|
56
|
+
"discriminator",
|
|
57
|
+
"peak_locator",
|
|
58
|
+
"ureg",
|
|
59
|
+
"debug_mode",
|
|
60
|
+
]
|
|
Binary file
|
FlowCyPy/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '1.0.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 4)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = 'g347e1bfc3'
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
import FlowCyPy.units as _
|
|
4
|
+
from . import discriminator
|
|
5
|
+
from . import peak_locator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DigitalProcessing:
|
|
9
|
+
"""
|
|
10
|
+
Container for analog and digital signal processing stages in a simulated flow cytometry pipeline.
|
|
11
|
+
|
|
12
|
+
This class orchestrates the application of analog electronic filtering, digitization, triggering,
|
|
13
|
+
and peak detection algorithms. It integrates key building blocks of the signal acquisition chain
|
|
14
|
+
in a modular and extensible way.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
discriminator : discriminator.BaseDiscriminator
|
|
19
|
+
Component responsible for extracting signal segments based on threshold-crossing
|
|
20
|
+
or window-based logic applied to the analog signal.
|
|
21
|
+
peak_algorithm : peak_locator.BasePeakLocator
|
|
22
|
+
Peak detection algorithm that locates and characterizes peaks in the digitized signal
|
|
23
|
+
(e.g., based on amplitude, area, or width).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
discriminator: discriminator.BaseDiscriminator = None,
|
|
29
|
+
peak_algorithm: peak_locator.BasePeakLocator = None,
|
|
30
|
+
):
|
|
31
|
+
self.discriminator = discriminator
|
|
32
|
+
self.peak_algorithm = peak_algorithm
|
FlowCyPy/directories.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import FlowCyPy
|
|
7
|
+
|
|
8
|
+
__all__ = ["root_path", "project_path", "doc_path", "doc_css_path", "logo_path"]
|
|
9
|
+
|
|
10
|
+
root_path = Path(FlowCyPy.__path__[0])
|
|
11
|
+
|
|
12
|
+
project_path = root_path.parents[0]
|
|
13
|
+
|
|
14
|
+
example_directory = root_path.joinpath("examples")
|
|
15
|
+
|
|
16
|
+
doc_path = project_path.joinpath("docs")
|
|
17
|
+
|
|
18
|
+
doc_css_path = doc_path.joinpath("source/_static/default.css")
|
|
19
|
+
|
|
20
|
+
logo_path = doc_path.joinpath("images/logo.png")
|
|
21
|
+
|
|
22
|
+
examples_path = root_path.joinpath("examples")
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
for path_name in __all__:
|
|
26
|
+
path = locals()[path_name]
|
|
27
|
+
print(path)
|
|
28
|
+
assert path.exists(), f"Path {path_name} do not exists"
|
|
29
|
+
|
|
30
|
+
# -
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
import logging
|
|
4
|
+
import math
|
|
5
|
+
from typing import Optional
|
|
6
|
+
import numpy as np
|
|
7
|
+
from TypedUnit import Power, Time, ureg, validate_units
|
|
8
|
+
|
|
9
|
+
from FlowCyPy.fluidics import Fluidics
|
|
10
|
+
from FlowCyPy.fluidics import populations
|
|
11
|
+
from FlowCyPy.fluidics.event_collection import EventCollection
|
|
12
|
+
from FlowCyPy.opto_electronics import OptoElectronics
|
|
13
|
+
from FlowCyPy.run_record import RunRecord
|
|
14
|
+
from FlowCyPy.digital_processing import DigitalProcessing
|
|
15
|
+
from FlowCyPy.sub_frames.acquisition import AcquisitionDataFrame
|
|
16
|
+
from FlowCyPy.sub_frames.peaks import PeakDataFrame
|
|
17
|
+
from FlowCyPy.sub_frames.triggered import TriggerDataFrame
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FlowCytometer:
|
|
24
|
+
"""
|
|
25
|
+
High level orchestrator for the flow cytometry simulation pipeline.
|
|
26
|
+
|
|
27
|
+
This class keeps only the persistent fluidic definition and the constant
|
|
28
|
+
optical background level. All other configuration objects are passed
|
|
29
|
+
explicitly to the methods that use them.
|
|
30
|
+
|
|
31
|
+
This design keeps the workflow fully visible to the caller and avoids any
|
|
32
|
+
ambiguity about which opto electronic or signal processing configuration is
|
|
33
|
+
active for a given computation.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
fluidics : Fluidics
|
|
38
|
+
Fluidic subsystem, including flow cell and population definitions.
|
|
39
|
+
background_power : pint.Quantity, optional
|
|
40
|
+
Constant optical background added to every detector channel.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@validate_units
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
fluidics: Fluidics,
|
|
47
|
+
background_power: Optional[Power] = 0 * ureg.milliwatt,
|
|
48
|
+
):
|
|
49
|
+
self.fluidics = fluidics
|
|
50
|
+
self.background_power = background_power
|
|
51
|
+
|
|
52
|
+
def _add_explicit_model_signals(
|
|
53
|
+
self,
|
|
54
|
+
event_collection: EventCollection,
|
|
55
|
+
signal_dict: dict,
|
|
56
|
+
opto_electronics: OptoElectronics,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Add explicit model detector signals to the optical power dictionary.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
event_collection : EventCollection
|
|
64
|
+
Population event blocks.
|
|
65
|
+
signal_dict : dict
|
|
66
|
+
Optical power signal dictionary updated in place.
|
|
67
|
+
opto_electronics : OptoElectronics
|
|
68
|
+
Opto electronic configuration.
|
|
69
|
+
"""
|
|
70
|
+
for events in event_collection:
|
|
71
|
+
if not isinstance(events.sampling_method, populations.ExplicitModel):
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if events.empty:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
time_array_seconds = signal_dict["Time"].to("second").magnitude
|
|
78
|
+
|
|
79
|
+
if time_array_seconds.size == 0:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
if time_array_seconds.size == 1:
|
|
83
|
+
acquisition_window_seconds = time_array_seconds[0]
|
|
84
|
+
else:
|
|
85
|
+
acquisition_window_seconds = (
|
|
86
|
+
time_array_seconds[-1] - time_array_seconds[0]
|
|
87
|
+
+ (time_array_seconds[1] - time_array_seconds[0])
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
event_times = events.get_quantity("Time").to("second")
|
|
91
|
+
event_velocities = events.get_quantity("Velocity").to("meter / second")
|
|
92
|
+
pulse_widths = opto_electronics.source.get_particle_width(
|
|
93
|
+
event_velocities
|
|
94
|
+
).to("second")
|
|
95
|
+
|
|
96
|
+
event_time_values = event_times.magnitude
|
|
97
|
+
velocity_values = event_velocities.magnitude
|
|
98
|
+
pulse_width_values = pulse_widths.magnitude
|
|
99
|
+
support_margin_seconds = 4.0 * pulse_width_values
|
|
100
|
+
|
|
101
|
+
prepend_mask = event_time_values >= (
|
|
102
|
+
acquisition_window_seconds - support_margin_seconds
|
|
103
|
+
)
|
|
104
|
+
append_mask = event_time_values < support_margin_seconds
|
|
105
|
+
|
|
106
|
+
wrapped_time_values = np.concatenate(
|
|
107
|
+
[
|
|
108
|
+
event_time_values[prepend_mask] - acquisition_window_seconds,
|
|
109
|
+
event_time_values,
|
|
110
|
+
event_time_values[append_mask] + acquisition_window_seconds,
|
|
111
|
+
]
|
|
112
|
+
)
|
|
113
|
+
wrapped_velocity_values = np.concatenate(
|
|
114
|
+
[
|
|
115
|
+
velocity_values[prepend_mask],
|
|
116
|
+
velocity_values,
|
|
117
|
+
velocity_values[append_mask],
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
wrapped_times = wrapped_time_values * ureg.second
|
|
122
|
+
wrapped_velocities = wrapped_velocity_values * ureg.meter / ureg.second
|
|
123
|
+
|
|
124
|
+
for detector in opto_electronics.detectors:
|
|
125
|
+
event_amplitudes = events.get_quantity(detector.name).to("watt")
|
|
126
|
+
amplitude_values = event_amplitudes.magnitude
|
|
127
|
+
wrapped_amplitude_values = np.concatenate(
|
|
128
|
+
[
|
|
129
|
+
amplitude_values[prepend_mask],
|
|
130
|
+
amplitude_values,
|
|
131
|
+
amplitude_values[append_mask],
|
|
132
|
+
]
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
generated_pulse_signal = opto_electronics.source.generate_pulses(
|
|
136
|
+
velocities=wrapped_velocities,
|
|
137
|
+
pulse_centers=wrapped_times,
|
|
138
|
+
pulse_amplitudes=wrapped_amplitude_values * ureg.watt,
|
|
139
|
+
time_array=signal_dict["Time"],
|
|
140
|
+
base_level=0 * ureg.watt,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
signal_dict[detector.name] += generated_pulse_signal
|
|
144
|
+
|
|
145
|
+
def _add_gamma_model_signals(
|
|
146
|
+
self,
|
|
147
|
+
event_collection: EventCollection,
|
|
148
|
+
signal_dict: dict,
|
|
149
|
+
opto_electronics: OptoElectronics,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""
|
|
152
|
+
Add gamma model detector signals to the optical power dictionary.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
event_collection : EventCollection
|
|
157
|
+
Population event blocks.
|
|
158
|
+
signal_dict : dict
|
|
159
|
+
Optical power signal dictionary updated in place.
|
|
160
|
+
opto_electronics : OptoElectronics
|
|
161
|
+
Opto electronic configuration.
|
|
162
|
+
"""
|
|
163
|
+
for events in event_collection:
|
|
164
|
+
if not isinstance(events.sampling_method, populations.GammaModel):
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
if events.empty:
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
expected_number_of_particles = events.metadata["ExpectedNumberOfParticles"]
|
|
171
|
+
|
|
172
|
+
if expected_number_of_particles == 0:
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
if len(opto_electronics.detectors) == 0:
|
|
176
|
+
raise ValueError("opto_electronics must contain at least one detector.")
|
|
177
|
+
|
|
178
|
+
reference_detector = opto_electronics.detectors[0]
|
|
179
|
+
|
|
180
|
+
reference_mean_amplitude = events.get_quantity(
|
|
181
|
+
reference_detector.name
|
|
182
|
+
).mean()
|
|
183
|
+
|
|
184
|
+
reference_power_trace = opto_electronics.source.get_gamma_trace(
|
|
185
|
+
time_array=signal_dict["Time"],
|
|
186
|
+
shape=expected_number_of_particles,
|
|
187
|
+
scale=reference_mean_amplitude,
|
|
188
|
+
mean_velocity=events.metadata["VelocityMean"],
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if reference_mean_amplitude.magnitude == 0:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
"Gamma Model reference mean amplitude is zero, cannot normalize."
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
particle_trace = reference_power_trace / reference_mean_amplitude
|
|
197
|
+
|
|
198
|
+
events.metadata["ParticleTrace"] = particle_trace
|
|
199
|
+
events.metadata["TimeTrace"] = signal_dict["Time"]
|
|
200
|
+
|
|
201
|
+
for detector in opto_electronics.detectors:
|
|
202
|
+
mean_amplitude = events.get_quantity(detector.name).mean()
|
|
203
|
+
detector_power_trace = particle_trace * mean_amplitude
|
|
204
|
+
signal_dict[detector.name] += detector_power_trace
|
|
205
|
+
|
|
206
|
+
def compute_analog(
|
|
207
|
+
self,
|
|
208
|
+
run_time: Time,
|
|
209
|
+
event_collection: EventCollection,
|
|
210
|
+
opto_electronics: OptoElectronics,
|
|
211
|
+
) -> dict:
|
|
212
|
+
"""
|
|
213
|
+
Compute the full analog detector waveforms from an event collection.
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
run_time : Time
|
|
218
|
+
Acquisition duration.
|
|
219
|
+
event_collection : EventCollection
|
|
220
|
+
Population event blocks with optical coupling already evaluated.
|
|
221
|
+
opto_electronics : OptoElectronics
|
|
222
|
+
Opto electronic configuration.
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
dict
|
|
227
|
+
Analog voltage signal dictionary.
|
|
228
|
+
"""
|
|
229
|
+
self._validate_run_time(run_time)
|
|
230
|
+
power_signal_dict = opto_electronics.initialize_optical_signal_dict(
|
|
231
|
+
run_time=run_time,
|
|
232
|
+
background_power=self.background_power,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
self._add_explicit_model_signals(
|
|
236
|
+
event_collection=event_collection,
|
|
237
|
+
signal_dict=power_signal_dict,
|
|
238
|
+
opto_electronics=opto_electronics,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
self._add_gamma_model_signals(
|
|
242
|
+
event_collection=event_collection,
|
|
243
|
+
signal_dict=power_signal_dict,
|
|
244
|
+
opto_electronics=opto_electronics,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return opto_electronics.convert_optical_power_to_voltage(power_signal_dict)
|
|
248
|
+
|
|
249
|
+
def process_analog(
|
|
250
|
+
self,
|
|
251
|
+
run_time: Time,
|
|
252
|
+
event_collection: EventCollection,
|
|
253
|
+
analog_dict: dict,
|
|
254
|
+
digital_processing: DigitalProcessing,
|
|
255
|
+
opto_electronics: OptoElectronics,
|
|
256
|
+
) -> RunRecord:
|
|
257
|
+
"""
|
|
258
|
+
Process an existing analog acquisition through the downstream signal
|
|
259
|
+
processing chain.
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
run_time : Time
|
|
264
|
+
Acquisition duration.
|
|
265
|
+
event_collection : EventCollection
|
|
266
|
+
Event collection associated with the analog signals.
|
|
267
|
+
analog_dict : dict
|
|
268
|
+
Analog voltage signal dictionary.
|
|
269
|
+
digital_processing : DigitalProcessing
|
|
270
|
+
Signal processing configuration.
|
|
271
|
+
opto_electronics : OptoElectronics
|
|
272
|
+
Opto electronic configuration associated with the analog signal.
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
RunRecord
|
|
277
|
+
Run record containing the analog acquisition and all downstream
|
|
278
|
+
results that could be computed.
|
|
279
|
+
"""
|
|
280
|
+
run_record = RunRecord(
|
|
281
|
+
run_time=run_time,
|
|
282
|
+
event_collection=event_collection,
|
|
283
|
+
opto_electronics=opto_electronics,
|
|
284
|
+
digital_processing=digital_processing,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
processed_analog_dict = opto_electronics.apply_analog_processing(
|
|
288
|
+
analog_dict=analog_dict,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
run_record.signal.analog = AcquisitionDataFrame._construct_from_signal_dict(
|
|
292
|
+
signal_dict=processed_analog_dict,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
triggered_analog_dict = (
|
|
296
|
+
digital_processing.discriminator.run_with_dict(processed_analog_dict)
|
|
297
|
+
if digital_processing.discriminator is not None
|
|
298
|
+
else None
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if triggered_analog_dict is None:
|
|
302
|
+
return run_record
|
|
303
|
+
|
|
304
|
+
if len(triggered_analog_dict["segment_id"]) == 0:
|
|
305
|
+
logger.debug(
|
|
306
|
+
"No triggers detected. Returning analog signal without digital processing."
|
|
307
|
+
)
|
|
308
|
+
return run_record
|
|
309
|
+
|
|
310
|
+
triggered_digital_dict = opto_electronics.digitizer.digitize_data_dict(
|
|
311
|
+
triggered_analog_dict
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
run_record.signal.digital = TriggerDataFrame._construct_from_flat_dict(
|
|
315
|
+
triggered_digital_dict,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if digital_processing.peak_algorithm is not None:
|
|
319
|
+
peaks_dict = digital_processing.peak_algorithm.run(triggered_digital_dict)
|
|
320
|
+
|
|
321
|
+
run_record.peaks = PeakDataFrame._construct_from_dict(peaks_dict)
|
|
322
|
+
|
|
323
|
+
return run_record
|
|
324
|
+
|
|
325
|
+
def process_run(
|
|
326
|
+
self,
|
|
327
|
+
run_record: RunRecord,
|
|
328
|
+
digital_processing: DigitalProcessing,
|
|
329
|
+
) -> RunRecord:
|
|
330
|
+
"""
|
|
331
|
+
Process an existing acquired run through the downstream signal
|
|
332
|
+
processing chain.
|
|
333
|
+
|
|
334
|
+
This method is intended for notebook workflows where the caller already
|
|
335
|
+
has a ``RunRecord`` returned by ``acquire()`` and wants to apply
|
|
336
|
+
downstream processing without passing the acquisition configuration again.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
run_record : RunRecord
|
|
341
|
+
Existing run record containing analog acquisition data and the
|
|
342
|
+
opto electronic configuration used to produce it.
|
|
343
|
+
digital_processing : DigitalProcessing
|
|
344
|
+
Signal processing configuration.
|
|
345
|
+
|
|
346
|
+
Returns
|
|
347
|
+
-------
|
|
348
|
+
RunRecord
|
|
349
|
+
Processed run record.
|
|
350
|
+
|
|
351
|
+
Raises
|
|
352
|
+
------
|
|
353
|
+
ValueError
|
|
354
|
+
If the input run record does not contain an ``opto_electronics``
|
|
355
|
+
configuration.
|
|
356
|
+
ValueError
|
|
357
|
+
If the input run record does not contain analog signal data.
|
|
358
|
+
"""
|
|
359
|
+
if run_record.opto_electronics is None:
|
|
360
|
+
raise ValueError(
|
|
361
|
+
"The provided run_record does not contain opto_electronics. "
|
|
362
|
+
"Make sure it was created with acquire() or manually populated."
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if run_record.signal is None or run_record.signal.analog is None:
|
|
366
|
+
raise ValueError(
|
|
367
|
+
"The provided run_record does not contain analog signal data."
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
return self.process_analog(
|
|
371
|
+
run_time=run_record.run_time,
|
|
372
|
+
event_collection=run_record.event_collection,
|
|
373
|
+
analog_dict=run_record.signal.analog.raw_data,
|
|
374
|
+
digital_processing=digital_processing,
|
|
375
|
+
opto_electronics=run_record.opto_electronics,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def acquire(
|
|
379
|
+
self,
|
|
380
|
+
run_time: Time,
|
|
381
|
+
opto_electronics: OptoElectronics,
|
|
382
|
+
random_state: int | np.random.Generator | None = None,
|
|
383
|
+
) -> RunRecord:
|
|
384
|
+
"""
|
|
385
|
+
Run the acquisition pipeline up to the analog stage.
|
|
386
|
+
|
|
387
|
+
Parameters
|
|
388
|
+
----------
|
|
389
|
+
run_time : Time
|
|
390
|
+
Acquisition duration.
|
|
391
|
+
opto_electronics : OptoElectronics
|
|
392
|
+
Opto electronic configuration.
|
|
393
|
+
|
|
394
|
+
Returns
|
|
395
|
+
-------
|
|
396
|
+
RunRecord
|
|
397
|
+
Run record containing the event collection, analog voltage
|
|
398
|
+
waveforms, and the opto electronic configuration used to produce
|
|
399
|
+
them.
|
|
400
|
+
"""
|
|
401
|
+
self._validate_run_time(run_time)
|
|
402
|
+
self._validate_opto_electronics(opto_electronics)
|
|
403
|
+
event_collection = self.fluidics.generate_event_collection(
|
|
404
|
+
run_time=run_time,
|
|
405
|
+
sampling_rate=opto_electronics.digitizer.sampling_rate,
|
|
406
|
+
random_state=random_state,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
opto_electronics.add_coupling_to_dataframe(
|
|
410
|
+
event_collection=event_collection,
|
|
411
|
+
compute_cross_section=False,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
processed_analog_dict = self.compute_analog(
|
|
415
|
+
run_time=run_time,
|
|
416
|
+
event_collection=event_collection,
|
|
417
|
+
opto_electronics=opto_electronics,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
run_record = RunRecord(
|
|
421
|
+
run_time=run_time,
|
|
422
|
+
event_collection=event_collection,
|
|
423
|
+
opto_electronics=opto_electronics,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
run_record.signal.analog = AcquisitionDataFrame._construct_from_signal_dict(
|
|
427
|
+
signal_dict=processed_analog_dict,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
return run_record
|
|
431
|
+
|
|
432
|
+
@staticmethod
|
|
433
|
+
def _validate_run_time(run_time: Time) -> None:
|
|
434
|
+
"""Reject empty, negative, non-finite, or unitless run durations."""
|
|
435
|
+
try:
|
|
436
|
+
seconds = float(run_time.to("second").magnitude)
|
|
437
|
+
except (AttributeError, TypeError, ValueError) as error:
|
|
438
|
+
raise TypeError("run_time must be a time quantity.") from error
|
|
439
|
+
|
|
440
|
+
if not math.isfinite(seconds) or seconds <= 0:
|
|
441
|
+
raise ValueError("run_time must be finite and strictly positive.")
|
|
442
|
+
|
|
443
|
+
@staticmethod
|
|
444
|
+
def _validate_opto_electronics(opto_electronics: OptoElectronics) -> None:
|
|
445
|
+
"""Validate detector channels before starting an acquisition."""
|
|
446
|
+
detectors = getattr(opto_electronics, "detectors", None)
|
|
447
|
+
if not detectors:
|
|
448
|
+
raise ValueError("opto_electronics must contain at least one detector.")
|
|
449
|
+
|
|
450
|
+
names = [getattr(detector, "name", None) for detector in detectors]
|
|
451
|
+
if any(not isinstance(name, str) or not name.strip() for name in names):
|
|
452
|
+
raise ValueError("Each detector must have a non-empty name.")
|
|
453
|
+
if len(names) != len(set(names)):
|
|
454
|
+
raise ValueError("Detector names must be unique.")
|
|
455
|
+
|
|
456
|
+
def run(
|
|
457
|
+
self,
|
|
458
|
+
run_time: Time,
|
|
459
|
+
opto_electronics: OptoElectronics,
|
|
460
|
+
digital_processing: Optional[DigitalProcessing] = None,
|
|
461
|
+
random_state: int | np.random.Generator | None = None,
|
|
462
|
+
) -> RunRecord:
|
|
463
|
+
"""
|
|
464
|
+
Run the full simulation pipeline from event generation to peak extraction.
|
|
465
|
+
|
|
466
|
+
Parameters
|
|
467
|
+
----------
|
|
468
|
+
run_time : Time
|
|
469
|
+
Acquisition duration.
|
|
470
|
+
opto_electronics : OptoElectronics
|
|
471
|
+
Opto electronic configuration.
|
|
472
|
+
digital_processing : DigitalProcessing, optional
|
|
473
|
+
Signal processing configuration. If None, no digital processing is applied.
|
|
474
|
+
|
|
475
|
+
Returns
|
|
476
|
+
-------
|
|
477
|
+
RunRecord
|
|
478
|
+
Full run record containing all available stages.
|
|
479
|
+
"""
|
|
480
|
+
if digital_processing is None:
|
|
481
|
+
digital_processing = DigitalProcessing()
|
|
482
|
+
|
|
483
|
+
acquired_run_record = self.acquire(
|
|
484
|
+
run_time=run_time,
|
|
485
|
+
opto_electronics=opto_electronics,
|
|
486
|
+
random_state=random_state,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
return self.process_run(
|
|
490
|
+
run_record=acquired_run_record,
|
|
491
|
+
digital_processing=digital_processing,
|
|
492
|
+
)
|