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.
Files changed (66) hide show
  1. FlowCyPy/.dylibs/libfftw3.3.dylib +0 -0
  2. FlowCyPy/.dylibs/libgcc_s.1.1.dylib +0 -0
  3. FlowCyPy/.dylibs/libgomp.1.dylib +0 -0
  4. FlowCyPy/.dylibs/libstdc++.6.dylib +0 -0
  5. FlowCyPy/__init__.py +63 -0
  6. FlowCyPy/_utils.cpython-314-darwin.so +0 -0
  7. FlowCyPy/_version.py +24 -0
  8. FlowCyPy/analysis.py +177 -0
  9. FlowCyPy/digital_processing/__init__.py +3 -0
  10. FlowCyPy/digital_processing/classifier.cpython-314-darwin.so +0 -0
  11. FlowCyPy/digital_processing/discriminator.cpython-314-darwin.so +0 -0
  12. FlowCyPy/digital_processing/libclassifier_lib.a +0 -0
  13. FlowCyPy/digital_processing/libdiscriminator_lib.a +0 -0
  14. FlowCyPy/digital_processing/libpeak_locator_lib.a +0 -0
  15. FlowCyPy/digital_processing/peak_locator.cpython-314-darwin.so +0 -0
  16. FlowCyPy/digital_processing/pipeline.py +31 -0
  17. FlowCyPy/directories.py +28 -0
  18. FlowCyPy/flow_cytometer.py +488 -0
  19. FlowCyPy/fluidics/__init__.py +72 -0
  20. FlowCyPy/fluidics/distributions.cpython-314-darwin.so +0 -0
  21. FlowCyPy/fluidics/event_collection.py +1081 -0
  22. FlowCyPy/fluidics/flow_cell.cpython-314-darwin.so +0 -0
  23. FlowCyPy/fluidics/libdistributions_lib.a +0 -0
  24. FlowCyPy/fluidics/libflow_cell_lib.a +0 -0
  25. FlowCyPy/fluidics/libpopulation_events_lib.a +0 -0
  26. FlowCyPy/fluidics/libpopulations_lib.a +0 -0
  27. FlowCyPy/fluidics/libscatterer_collection_lib.a +0 -0
  28. FlowCyPy/fluidics/population_events.cpython-314-darwin.so +0 -0
  29. FlowCyPy/fluidics/populations.cpython-314-darwin.so +0 -0
  30. FlowCyPy/fluidics/scatterer_collection.cpython-314-darwin.so +0 -0
  31. FlowCyPy/fluidics/system.py +313 -0
  32. FlowCyPy/interface_pint.cpython-314-darwin.so +0 -0
  33. FlowCyPy/libpint.a +0 -0
  34. FlowCyPy/libutils_lib.a +0 -0
  35. FlowCyPy/opto_electronics/__init__.py +50 -0
  36. FlowCyPy/opto_electronics/amplifier.cpython-314-darwin.so +0 -0
  37. FlowCyPy/opto_electronics/circuits.cpython-314-darwin.so +0 -0
  38. FlowCyPy/opto_electronics/coupling_model.cpython-314-darwin.so +0 -0
  39. FlowCyPy/opto_electronics/detector.cpython-314-darwin.so +0 -0
  40. FlowCyPy/opto_electronics/digitizer.cpython-314-darwin.so +0 -0
  41. FlowCyPy/opto_electronics/libamplifier_lib.a +0 -0
  42. FlowCyPy/opto_electronics/libcircuits_lib.a +0 -0
  43. FlowCyPy/opto_electronics/libcoupling_model_lib.a +0 -0
  44. FlowCyPy/opto_electronics/libdetector_lib.a +0 -0
  45. FlowCyPy/opto_electronics/libdigitizer_lib.a +0 -0
  46. FlowCyPy/opto_electronics/libsource_lib.a +0 -0
  47. FlowCyPy/opto_electronics/source.cpython-314-darwin.so +0 -0
  48. FlowCyPy/opto_electronics/system.py +284 -0
  49. FlowCyPy/presets/__init__.py +3 -0
  50. FlowCyPy/presets/detector.py +89 -0
  51. FlowCyPy/presets/flow_cytometer.py +425 -0
  52. FlowCyPy/presets/population.py +110 -0
  53. FlowCyPy/run_record.py +581 -0
  54. FlowCyPy/sub_frames/__init__.py +0 -0
  55. FlowCyPy/sub_frames/acquisition.py +366 -0
  56. FlowCyPy/sub_frames/classifier.py +59 -0
  57. FlowCyPy/sub_frames/events.py +147 -0
  58. FlowCyPy/sub_frames/peak_metrics.py +47 -0
  59. FlowCyPy/sub_frames/peaks.py +899 -0
  60. FlowCyPy/sub_frames/triggered.py +242 -0
  61. FlowCyPy/units.py +28 -0
  62. FlowCyPy/workflow.py +284 -0
  63. flowcypy-1.2.0.dist-info/METADATA +495 -0
  64. flowcypy-1.2.0.dist-info/RECORD +66 -0
  65. flowcypy-1.2.0.dist-info/WHEEL +6 -0
  66. 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
+ }
@@ -0,0 +1,3 @@
1
+ from .pipeline import DigitalProcessing, discriminator, peak_locator
2
+
3
+ __all__ = ["DigitalProcessing", "discriminator", "peak_locator"]
@@ -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
@@ -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
+ # -