FlowCyPy 0.5.0__py3-none-any.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 (44) hide show
  1. FlowCyPy/__init__.py +15 -0
  2. FlowCyPy/_version.py +16 -0
  3. FlowCyPy/classifier.py +196 -0
  4. FlowCyPy/coupling_mechanism/__init__.py +4 -0
  5. FlowCyPy/coupling_mechanism/empirical.py +47 -0
  6. FlowCyPy/coupling_mechanism/mie.py +205 -0
  7. FlowCyPy/coupling_mechanism/rayleigh.py +115 -0
  8. FlowCyPy/coupling_mechanism/uniform.py +39 -0
  9. FlowCyPy/cytometer.py +198 -0
  10. FlowCyPy/detector.py +616 -0
  11. FlowCyPy/directories.py +36 -0
  12. FlowCyPy/distribution/__init__.py +16 -0
  13. FlowCyPy/distribution/base_class.py +59 -0
  14. FlowCyPy/distribution/delta.py +86 -0
  15. FlowCyPy/distribution/lognormal.py +94 -0
  16. FlowCyPy/distribution/normal.py +95 -0
  17. FlowCyPy/distribution/particle_size_distribution.py +110 -0
  18. FlowCyPy/distribution/uniform.py +96 -0
  19. FlowCyPy/distribution/weibull.py +80 -0
  20. FlowCyPy/event_correlator.py +244 -0
  21. FlowCyPy/flow_cell.py +122 -0
  22. FlowCyPy/helper.py +85 -0
  23. FlowCyPy/logger.py +322 -0
  24. FlowCyPy/noises.py +29 -0
  25. FlowCyPy/particle_count.py +102 -0
  26. FlowCyPy/peak_locator/__init__.py +4 -0
  27. FlowCyPy/peak_locator/base_class.py +163 -0
  28. FlowCyPy/peak_locator/basic.py +108 -0
  29. FlowCyPy/peak_locator/derivative.py +143 -0
  30. FlowCyPy/peak_locator/moving_average.py +114 -0
  31. FlowCyPy/physical_constant.py +19 -0
  32. FlowCyPy/plottings.py +270 -0
  33. FlowCyPy/population.py +239 -0
  34. FlowCyPy/populations_instances.py +49 -0
  35. FlowCyPy/report.py +236 -0
  36. FlowCyPy/scatterer.py +373 -0
  37. FlowCyPy/source.py +249 -0
  38. FlowCyPy/units.py +26 -0
  39. FlowCyPy/utils.py +191 -0
  40. FlowCyPy-0.5.0.dist-info/LICENSE +21 -0
  41. FlowCyPy-0.5.0.dist-info/METADATA +252 -0
  42. FlowCyPy-0.5.0.dist-info/RECORD +44 -0
  43. FlowCyPy-0.5.0.dist-info/WHEEL +5 -0
  44. FlowCyPy-0.5.0.dist-info/top_level.txt +1 -0
FlowCyPy/cytometer.py ADDED
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import logging
5
+ import numpy as np
6
+ import matplotlib.pyplot as plt
7
+ from typing import List, Callable, Optional
8
+ from MPSPlots.styles import mps
9
+ from FlowCyPy.scatterer import Scatterer
10
+ from FlowCyPy.detector import Detector
11
+ from FlowCyPy.source import GaussianBeam
12
+ import pandas as pd
13
+ import pint_pandas
14
+ from FlowCyPy.units import Quantity, milliwatt
15
+ from FlowCyPy.logger import SimulationLogger
16
+
17
+ # Set up logging configuration
18
+ logging.basicConfig(
19
+ level=logging.INFO,
20
+ format='%(levelname)s - %(message)s'
21
+ )
22
+
23
+
24
+ class FlowCytometer:
25
+ """
26
+ A class to simulate flow cytometer signals for Forward Scatter (FSC) and Side Scatter (SSC) channels.
27
+
28
+ This class models the particle distribution, flow characteristics, and detector configurations to
29
+ simulate the signal generated as particles pass through the flow cytometer's laser.
30
+
31
+ Parameters
32
+ ----------
33
+ scatterer : Scatterer
34
+ The distribution of particle sizes which affects scattering signals.
35
+ source : GaussianBeam
36
+ The laser source object representing the illumination scheme.
37
+ detectors : List[Detector]
38
+ List of `Detector` objects representing the detectors in the system.
39
+
40
+ """
41
+ def __init__(
42
+ self,
43
+ scatterer: Scatterer,
44
+ source: GaussianBeam,
45
+ detectors: List[Detector],
46
+ coupling_mechanism: Optional[str] = 'mie',
47
+ background_power: Optional[Quantity] = 0 * milliwatt):
48
+
49
+ self.scatterer = scatterer
50
+ self.source = source
51
+ self.detectors = detectors
52
+ self.coupling_mechanism = coupling_mechanism
53
+ self.background_power = background_power
54
+
55
+ assert len(self.detectors) == 2, 'For now, FlowCytometer can only take two detectors for the analysis.'
56
+ assert self.detectors[0].name != self.detectors[1].name, 'Both detectors cannot have the same name'
57
+
58
+ def simulate_pulse(self) -> None:
59
+ """
60
+ Simulates the signal pulses for the FSC and SSC channels by generating Gaussian pulses for
61
+ each particle event and distributing them across the detectors.
62
+ """
63
+ logging.debug("Starting pulse simulation.")
64
+
65
+ columns = pd.MultiIndex.from_product(
66
+ [[p.name for p in self.detectors], ['Centers', 'Heights']]
67
+ )
68
+
69
+ self.pulse_dataframe = pd.DataFrame(columns=columns)
70
+
71
+ self._generate_pulse_parameters()
72
+
73
+ _widths = self.scatterer.dataframe['Widths'].values
74
+ _centers = self.scatterer.dataframe['Time'].values
75
+
76
+ detection_mechanism = self._get_detection_mechanism()
77
+
78
+ # Initialize the detectors
79
+ for detector in self.detectors:
80
+ detector.source = self.source
81
+ detector.init_raw_signal(run_time=self.scatterer.flow_cell.run_time)
82
+
83
+ # Fetch the coupling power for each scatterer
84
+ for detector in self.detectors:
85
+ coupling_power = detection_mechanism(
86
+ source=self.source,
87
+ detector=detector,
88
+ scatterer=self.scatterer
89
+ )
90
+
91
+ self.scatterer.dataframe['CouplingPower'] = pint_pandas.PintArray(coupling_power, dtype=coupling_power.units)
92
+
93
+ for detector in self.detectors:
94
+ # Generate noise components
95
+ detector._add_thermal_noise_to_raw_signal()
96
+
97
+ detector._add_dark_current_noise_to_raw_signal()
98
+
99
+ # Broadcast the time array to the shape of (number of signals, len(detector.time))
100
+ time_grid = np.expand_dims(detector.dataframe.Time.values.numpy_data, axis=0) * _centers.units
101
+
102
+ centers = np.expand_dims(_centers.numpy_data, axis=1) * _centers.units
103
+ widths = np.expand_dims(_widths.numpy_data, axis=1) * _widths.units
104
+
105
+ # Compute the Gaussian for each height, center, and width using broadcasting
106
+ power_gaussians = coupling_power[:, np.newaxis] * np.exp(- (time_grid - centers) ** 2 / (2 * widths ** 2))
107
+
108
+ total_power = np.sum(power_gaussians, axis=0) + self.background_power
109
+
110
+ # Sum all the Gaussians and add them to the detector.raw_signal
111
+ detector._add_optical_power_to_raw_signal(optical_power=total_power)
112
+
113
+ detector.capture_signal()
114
+
115
+ self._log_statistics()
116
+
117
+ def _log_statistics(self) -> SimulationLogger:
118
+ """
119
+ Logs key statistics about the simulated pulse events for each detector using tabulate for better formatting.
120
+ Includes total events, average time between events, gfirst and last event times, and minimum time between events.
121
+ """
122
+ logger = SimulationLogger(cytometer=self)
123
+
124
+ logger.log_statistics(include_totals=True, table_format="fancy_grid")
125
+
126
+ return logger
127
+
128
+ def _get_detection_mechanism(self) -> Callable:
129
+ """
130
+ Generates coupling factors for the scatterer sizes based on the selected coupling mechanism.
131
+
132
+ Returns
133
+ -------
134
+ Callable
135
+ The generated coupling factors for the scatterer sizes.
136
+ """
137
+ from FlowCyPy import coupling_mechanism
138
+
139
+ # Determine which coupling mechanism to use and compute the corresponding factors
140
+ match self.coupling_mechanism.lower():
141
+ case 'rayleigh':
142
+ return coupling_mechanism.rayleigh.compute_detected_signal
143
+ case 'uniform':
144
+ return coupling_mechanism.uniform.compute_detected_signal
145
+ case 'mie':
146
+ return coupling_mechanism.mie.compute_detected_signal
147
+ case 'empirical':
148
+ return coupling_mechanism.empirical.compute_detected_signal
149
+ case _:
150
+ raise ValueError("Invalid coupling mechanism. Choose 'rayleigh' or 'uniform'.")
151
+
152
+ def _generate_pulse_parameters(self) -> None:
153
+ """
154
+ Generates random parameters for a Gaussian pulse, including the center and width.
155
+
156
+ Returns
157
+ -------
158
+ center : np.ndarray
159
+ The center of the pulse in time.
160
+ width : np.ndarray
161
+ The width of the pulse (standard deviation of the Gaussian, in seconds).
162
+ """
163
+ self.pulse_dataframe['Centers'] = self.scatterer.dataframe['Time']
164
+
165
+ widths = self.source.waist / self.scatterer.flow_cell.flow_speed * np.ones(self.scatterer.n_events)
166
+
167
+ self.scatterer.dataframe['Widths'] = pint_pandas.PintArray(widths, dtype=widths.units)
168
+
169
+ def plot(self, figure_size: tuple = (10, 6), add_peak_locator: bool = False) -> None:
170
+ """Plots the signals generated for each detector channel."""
171
+ logging.info("Plotting the signal for the different channels.")
172
+
173
+ n_detectors = len(self.detectors)
174
+
175
+ with plt.style.context(mps):
176
+ _, axes = plt.subplots(ncols=1, nrows=n_detectors + 1, figsize=figure_size, sharex=True, sharey=True, gridspec_kw={'height_ratios': [1, 1, 0.4]})
177
+
178
+ time_unit, signal_unit = self.detectors[0].plot(ax=axes[0], show=False, add_peak_locator=add_peak_locator)
179
+ self.detectors[1].plot(ax=axes[1], show=False, time_unit=time_unit, signal_unit=signal_unit, add_peak_locator=add_peak_locator)
180
+
181
+ axes[-1].get_yaxis().set_visible(False)
182
+ self.scatterer.add_to_ax(axes[-1])
183
+
184
+ # Add legends to each subplot
185
+ for ax in axes:
186
+ ax.legend()
187
+
188
+ # Display the plot
189
+ plt.show()
190
+
191
+ def add_detector(self, **kwargs) -> Detector:
192
+ detector = Detector(
193
+ **kwargs
194
+ )
195
+
196
+ self.detectors.append(detector)
197
+
198
+ return detector