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.
- FlowCyPy/__init__.py +15 -0
- FlowCyPy/_version.py +16 -0
- FlowCyPy/classifier.py +196 -0
- FlowCyPy/coupling_mechanism/__init__.py +4 -0
- FlowCyPy/coupling_mechanism/empirical.py +47 -0
- FlowCyPy/coupling_mechanism/mie.py +205 -0
- FlowCyPy/coupling_mechanism/rayleigh.py +115 -0
- FlowCyPy/coupling_mechanism/uniform.py +39 -0
- FlowCyPy/cytometer.py +198 -0
- FlowCyPy/detector.py +616 -0
- FlowCyPy/directories.py +36 -0
- FlowCyPy/distribution/__init__.py +16 -0
- FlowCyPy/distribution/base_class.py +59 -0
- FlowCyPy/distribution/delta.py +86 -0
- FlowCyPy/distribution/lognormal.py +94 -0
- FlowCyPy/distribution/normal.py +95 -0
- FlowCyPy/distribution/particle_size_distribution.py +110 -0
- FlowCyPy/distribution/uniform.py +96 -0
- FlowCyPy/distribution/weibull.py +80 -0
- FlowCyPy/event_correlator.py +244 -0
- FlowCyPy/flow_cell.py +122 -0
- FlowCyPy/helper.py +85 -0
- FlowCyPy/logger.py +322 -0
- FlowCyPy/noises.py +29 -0
- FlowCyPy/particle_count.py +102 -0
- FlowCyPy/peak_locator/__init__.py +4 -0
- FlowCyPy/peak_locator/base_class.py +163 -0
- FlowCyPy/peak_locator/basic.py +108 -0
- FlowCyPy/peak_locator/derivative.py +143 -0
- FlowCyPy/peak_locator/moving_average.py +114 -0
- FlowCyPy/physical_constant.py +19 -0
- FlowCyPy/plottings.py +270 -0
- FlowCyPy/population.py +239 -0
- FlowCyPy/populations_instances.py +49 -0
- FlowCyPy/report.py +236 -0
- FlowCyPy/scatterer.py +373 -0
- FlowCyPy/source.py +249 -0
- FlowCyPy/units.py +26 -0
- FlowCyPy/utils.py +191 -0
- FlowCyPy-0.5.0.dist-info/LICENSE +21 -0
- FlowCyPy-0.5.0.dist-info/METADATA +252 -0
- FlowCyPy-0.5.0.dist-info/RECORD +44 -0
- FlowCyPy-0.5.0.dist-info/WHEEL +5 -0
- 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
|