FlowCyPy 1.2.0__cp314-cp314-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 +63 -0
- FlowCyPy/_utils.cpython-314-darwin.so +0 -0
- FlowCyPy/_version.py +24 -0
- FlowCyPy/analysis.py +177 -0
- FlowCyPy/digital_processing/__init__.py +3 -0
- FlowCyPy/digital_processing/classifier.cpython-314-darwin.so +0 -0
- FlowCyPy/digital_processing/discriminator.cpython-314-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-314-darwin.so +0 -0
- FlowCyPy/digital_processing/pipeline.py +31 -0
- FlowCyPy/directories.py +28 -0
- FlowCyPy/flow_cytometer.py +488 -0
- FlowCyPy/fluidics/__init__.py +72 -0
- FlowCyPy/fluidics/distributions.cpython-314-darwin.so +0 -0
- FlowCyPy/fluidics/event_collection.py +1081 -0
- FlowCyPy/fluidics/flow_cell.cpython-314-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-314-darwin.so +0 -0
- FlowCyPy/fluidics/populations.cpython-314-darwin.so +0 -0
- FlowCyPy/fluidics/scatterer_collection.cpython-314-darwin.so +0 -0
- FlowCyPy/fluidics/system.py +313 -0
- FlowCyPy/interface_pint.cpython-314-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-314-darwin.so +0 -0
- FlowCyPy/opto_electronics/circuits.cpython-314-darwin.so +0 -0
- FlowCyPy/opto_electronics/coupling_model.cpython-314-darwin.so +0 -0
- FlowCyPy/opto_electronics/detector.cpython-314-darwin.so +0 -0
- FlowCyPy/opto_electronics/digitizer.cpython-314-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-314-darwin.so +0 -0
- FlowCyPy/opto_electronics/system.py +284 -0
- FlowCyPy/presets/__init__.py +3 -0
- FlowCyPy/presets/detector.py +89 -0
- FlowCyPy/presets/flow_cytometer.py +425 -0
- FlowCyPy/presets/population.py +110 -0
- FlowCyPy/run_record.py +581 -0
- FlowCyPy/sub_frames/__init__.py +0 -0
- FlowCyPy/sub_frames/acquisition.py +366 -0
- FlowCyPy/sub_frames/classifier.py +59 -0
- FlowCyPy/sub_frames/events.py +147 -0
- FlowCyPy/sub_frames/peak_metrics.py +47 -0
- FlowCyPy/sub_frames/peaks.py +899 -0
- FlowCyPy/sub_frames/triggered.py +242 -0
- FlowCyPy/units.py +28 -0
- FlowCyPy/workflow.py +284 -0
- flowcypy-1.2.0.dist-info/METADATA +495 -0
- flowcypy-1.2.0.dist-info/RECORD +66 -0
- flowcypy-1.2.0.dist-info/WHEEL +6 -0
- flowcypy-1.2.0.dist-info/licenses/LICENSE +21 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
FlowCyPy/__init__.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from ._version import version as __version__
|
|
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
|
+
"DetectionAnalyzer": (".analysis", "DetectionAnalyzer"),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def __getattr__(name: str):
|
|
29
|
+
"""Resolve public objects only when they are first accessed.
|
|
30
|
+
|
|
31
|
+
Keeping the compatibility exports lazy makes ``import FlowCyPy`` cheap
|
|
32
|
+
while retaining the established top-level API.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
module_name, attribute_name = _LAZY_EXPORTS[name]
|
|
36
|
+
except KeyError as error:
|
|
37
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from error
|
|
38
|
+
|
|
39
|
+
from importlib import import_module
|
|
40
|
+
|
|
41
|
+
value = getattr(import_module(module_name, __name__), attribute_name)
|
|
42
|
+
globals()[name] = value
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
# Preserve the established public export order.
|
|
46
|
+
__all__ = [ # noqa: RUF022
|
|
47
|
+
"__version__",
|
|
48
|
+
"FlowCytometer",
|
|
49
|
+
"Workflow",
|
|
50
|
+
"Fluidics",
|
|
51
|
+
"distributions",
|
|
52
|
+
"populations",
|
|
53
|
+
"OptoElectronics",
|
|
54
|
+
"circuits",
|
|
55
|
+
"source",
|
|
56
|
+
"DigitalProcessing",
|
|
57
|
+
"classifier",
|
|
58
|
+
"discriminator",
|
|
59
|
+
"peak_locator",
|
|
60
|
+
"ureg",
|
|
61
|
+
"debug_mode",
|
|
62
|
+
"DetectionAnalyzer",
|
|
63
|
+
]
|
|
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.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 2, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = 'g04a10e5f9'
|
FlowCyPy/analysis.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Analysis helpers for simulated flow-cytometry acquisitions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterable, Sequence
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from .units import ureg
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DetectionAnalyzer:
|
|
13
|
+
"""Relate digital trigger windows to simulated population events.
|
|
14
|
+
|
|
15
|
+
The analyzer uses the ground-truth event arrival time and population label
|
|
16
|
+
stored in a :class:`~FlowCyPy.run_record.RunRecord`. An event is considered
|
|
17
|
+
detected when its arrival time falls within a digital trigger segment.
|
|
18
|
+
|
|
19
|
+
This measures trigger capture, not successful peak localization.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def _to_seconds(values, unit=None) -> np.ndarray:
|
|
24
|
+
"""Convert quantities or numeric values to seconds."""
|
|
25
|
+
if hasattr(values, "pint"):
|
|
26
|
+
return np.asarray(values.pint.to("second").magnitude, dtype=float)
|
|
27
|
+
|
|
28
|
+
if hasattr(values, "to") and hasattr(values, "magnitude"):
|
|
29
|
+
return np.asarray(values.to("second").magnitude, dtype=float)
|
|
30
|
+
|
|
31
|
+
raw_values = list(values) if hasattr(values, "__iter__") else [values]
|
|
32
|
+
seconds = []
|
|
33
|
+
for value in raw_values:
|
|
34
|
+
if hasattr(value, "to") and hasattr(value, "magnitude"):
|
|
35
|
+
seconds.append(value.to("second").magnitude)
|
|
36
|
+
elif unit is not None:
|
|
37
|
+
unit_value = ureg(unit) if isinstance(unit, str) else unit
|
|
38
|
+
seconds.append((value * unit_value).to("second").magnitude)
|
|
39
|
+
else:
|
|
40
|
+
seconds.append(value)
|
|
41
|
+
return np.asarray(seconds, dtype=float)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _event_dataframe(run_record) -> pd.DataFrame:
|
|
45
|
+
"""Return the simulated event table with population labels exposed."""
|
|
46
|
+
return (
|
|
47
|
+
run_record.event_collection
|
|
48
|
+
.get_concatenated_dataframe()
|
|
49
|
+
.reset_index()
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def annotate(self, run_record) -> pd.DataFrame:
|
|
53
|
+
"""Annotate events with ``Detected`` and ``TriggerID`` columns.
|
|
54
|
+
|
|
55
|
+
Events without a matching trigger segment are retained and marked
|
|
56
|
+
``Detected=False``. Runs without any digital triggers are supported.
|
|
57
|
+
|
|
58
|
+
An event is detected when its simulated arrival time satisfies
|
|
59
|
+
``segment_start <= event_time <= segment_stop`` for a digital trigger
|
|
60
|
+
segment. If an event falls into more than one segment, the first
|
|
61
|
+
matching segment in the digital table is assigned.
|
|
62
|
+
"""
|
|
63
|
+
events = self._event_dataframe(run_record).copy()
|
|
64
|
+
events["Detected"] = False
|
|
65
|
+
events["TriggerID"] = pd.Series(
|
|
66
|
+
pd.NA, index=events.index, dtype="Int64"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
digital = run_record.signal.digital
|
|
70
|
+
if digital is None or "Time" not in events or len(events) == 0:
|
|
71
|
+
return events
|
|
72
|
+
|
|
73
|
+
digital_table = digital.reset_index()
|
|
74
|
+
segment_column = (
|
|
75
|
+
"SegmentID" if "SegmentID" in digital_table else "segment_id"
|
|
76
|
+
)
|
|
77
|
+
if segment_column not in digital_table:
|
|
78
|
+
return events
|
|
79
|
+
|
|
80
|
+
digital_unit = digital.attrs.get("units", {}).get("Time", "second")
|
|
81
|
+
digital_table["_time_s"] = self._to_seconds(
|
|
82
|
+
digital_table["Time"], unit=digital_unit
|
|
83
|
+
)
|
|
84
|
+
event_times = self._to_seconds(events["Time"])
|
|
85
|
+
detected = np.zeros(len(events), dtype=bool)
|
|
86
|
+
trigger_ids = np.full(len(events), -1, dtype=int)
|
|
87
|
+
|
|
88
|
+
for segment_id, segment in digital_table.groupby(segment_column):
|
|
89
|
+
start = segment["_time_s"].min()
|
|
90
|
+
stop = segment["_time_s"].max()
|
|
91
|
+
matches = (
|
|
92
|
+
(event_times >= start)
|
|
93
|
+
& (event_times <= stop)
|
|
94
|
+
& ~detected
|
|
95
|
+
)
|
|
96
|
+
detected[matches] = True
|
|
97
|
+
trigger_ids[matches] = int(segment_id)
|
|
98
|
+
|
|
99
|
+
events["Detected"] = detected
|
|
100
|
+
events.loc[detected, "TriggerID"] = trigger_ids[detected]
|
|
101
|
+
return events
|
|
102
|
+
|
|
103
|
+
def summary(
|
|
104
|
+
self,
|
|
105
|
+
run_records: Sequence,
|
|
106
|
+
population_names: Iterable[str] | None = None,
|
|
107
|
+
condition_names: Sequence[str] | None = None,
|
|
108
|
+
) -> pd.DataFrame:
|
|
109
|
+
"""Return simulated counts, detected counts, and efficiencies.
|
|
110
|
+
|
|
111
|
+
``SimulatedEvents`` counts events of the selected population in the
|
|
112
|
+
event collection after the SEC cutoff. ``DetectedEvents`` counts those
|
|
113
|
+
events whose arrival times overlap a digital trigger segment.
|
|
114
|
+
``DetectionEfficiency`` is calculated as:
|
|
115
|
+
|
|
116
|
+
``DetectedEvents / SimulatedEvents``
|
|
117
|
+
"""
|
|
118
|
+
if condition_names is None:
|
|
119
|
+
condition_names = [
|
|
120
|
+
f"Run {index}" for index in range(len(run_records))
|
|
121
|
+
]
|
|
122
|
+
if len(condition_names) != len(run_records):
|
|
123
|
+
raise ValueError("condition_names must match run_records.")
|
|
124
|
+
|
|
125
|
+
rows = []
|
|
126
|
+
for condition, run_record in zip(condition_names, run_records):
|
|
127
|
+
events = self.annotate(run_record)
|
|
128
|
+
available = sorted(events["Population"].dropna().unique())
|
|
129
|
+
selected = list(population_names) if population_names else available
|
|
130
|
+
for population in selected:
|
|
131
|
+
statistics = run_record.detection_statistics(population)
|
|
132
|
+
rows.append({"Condition": condition, **statistics})
|
|
133
|
+
return pd.DataFrame(rows)
|
|
134
|
+
|
|
135
|
+
def coincidence_statistics(self, run_record) -> dict:
|
|
136
|
+
"""Summarize multiple simulated events sharing one trigger segment.
|
|
137
|
+
|
|
138
|
+
A segment is coincident when at least two events are assigned to it by
|
|
139
|
+
:meth:`annotate`. Population-combination counts distinguish, for
|
|
140
|
+
example, EV--EV from EV--LP coincidences.
|
|
141
|
+
"""
|
|
142
|
+
events = self.annotate(run_record)
|
|
143
|
+
detected = events[
|
|
144
|
+
events["Detected"] & events["TriggerID"].notna()
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
if detected.empty:
|
|
148
|
+
return {
|
|
149
|
+
"TriggerSegments": 0,
|
|
150
|
+
"DetectedEvents": 0,
|
|
151
|
+
"CoincidentSegments": 0,
|
|
152
|
+
"CoincidenceRate": np.nan,
|
|
153
|
+
"PopulationCombinations": {},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
by_segment = detected.groupby("TriggerID", dropna=True)
|
|
157
|
+
segment_sizes = by_segment.size()
|
|
158
|
+
coincident_segments = segment_sizes[segment_sizes >= 2]
|
|
159
|
+
combinations = {}
|
|
160
|
+
|
|
161
|
+
for _, segment in by_segment:
|
|
162
|
+
if len(segment) < 2:
|
|
163
|
+
continue
|
|
164
|
+
populations = "+".join(sorted(segment["Population"].astype(str)))
|
|
165
|
+
combinations[populations] = combinations.get(populations, 0) + 1
|
|
166
|
+
|
|
167
|
+
trigger_segments = len(segment_sizes)
|
|
168
|
+
return {
|
|
169
|
+
"TriggerSegments": trigger_segments,
|
|
170
|
+
"DetectedEvents": len(detected),
|
|
171
|
+
"CoincidentSegments": len(coincident_segments),
|
|
172
|
+
"CoincidenceRate": (
|
|
173
|
+
len(coincident_segments) / trigger_segments
|
|
174
|
+
if trigger_segments else np.nan
|
|
175
|
+
),
|
|
176
|
+
"PopulationCombinations": combinations,
|
|
177
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Initialize the shared unit registry before loading native processing modules.
|
|
2
|
+
import FlowCyPy.units as _ # noqa: F401
|
|
3
|
+
|
|
4
|
+
from . import discriminator, peak_locator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DigitalProcessing:
|
|
8
|
+
"""
|
|
9
|
+
Container for analog and digital signal processing stages in a simulated flow cytometry pipeline.
|
|
10
|
+
|
|
11
|
+
This class orchestrates the application of analog electronic filtering, digitization, triggering,
|
|
12
|
+
and peak detection algorithms. It integrates key building blocks of the signal acquisition chain
|
|
13
|
+
in a modular and extensible way.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
discriminator : discriminator.BaseDiscriminator
|
|
18
|
+
Component responsible for extracting signal segments based on threshold-crossing
|
|
19
|
+
or window-based logic applied to the analog signal.
|
|
20
|
+
peak_algorithm : peak_locator.BasePeakLocator
|
|
21
|
+
Peak detection algorithm that locates and characterizes peaks in the digitized signal
|
|
22
|
+
(e.g., based on amplitude, area, or width).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
discriminator: discriminator.BaseDiscriminator = None,
|
|
28
|
+
peak_algorithm: peak_locator.BasePeakLocator = None,
|
|
29
|
+
):
|
|
30
|
+
self.discriminator = discriminator
|
|
31
|
+
self.peak_algorithm = peak_algorithm
|
FlowCyPy/directories.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import FlowCyPy
|
|
5
|
+
|
|
6
|
+
__all__ = ["doc_css_path", "doc_path", "logo_path", "project_path", "root_path"]
|
|
7
|
+
|
|
8
|
+
root_path = Path(FlowCyPy.__path__[0])
|
|
9
|
+
|
|
10
|
+
project_path = root_path.parents[0]
|
|
11
|
+
|
|
12
|
+
example_directory = root_path.joinpath("examples")
|
|
13
|
+
|
|
14
|
+
doc_path = project_path.joinpath("docs")
|
|
15
|
+
|
|
16
|
+
doc_css_path = doc_path.joinpath("source/_static/default.css")
|
|
17
|
+
|
|
18
|
+
logo_path = doc_path.joinpath("images/logo.png")
|
|
19
|
+
|
|
20
|
+
examples_path = root_path.joinpath("examples")
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
for path_name in __all__:
|
|
24
|
+
path = locals()[path_name]
|
|
25
|
+
print(path)
|
|
26
|
+
assert path.exists(), f"Path {path_name} do not exists"
|
|
27
|
+
|
|
28
|
+
# -
|