FlowCyPy 0.7.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 (45) hide show
  1. FlowCyPy/__init__.py +13 -0
  2. FlowCyPy/_version.py +16 -0
  3. FlowCyPy/acquisition.py +652 -0
  4. FlowCyPy/classifier.py +208 -0
  5. FlowCyPy/coupling_mechanism/__init__.py +4 -0
  6. FlowCyPy/coupling_mechanism/empirical.py +47 -0
  7. FlowCyPy/coupling_mechanism/mie.py +207 -0
  8. FlowCyPy/coupling_mechanism/rayleigh.py +116 -0
  9. FlowCyPy/coupling_mechanism/uniform.py +40 -0
  10. FlowCyPy/coupling_mechanism.py +205 -0
  11. FlowCyPy/cytometer.py +314 -0
  12. FlowCyPy/detector.py +439 -0
  13. FlowCyPy/directories.py +36 -0
  14. FlowCyPy/distribution/__init__.py +16 -0
  15. FlowCyPy/distribution/base_class.py +79 -0
  16. FlowCyPy/distribution/delta.py +104 -0
  17. FlowCyPy/distribution/lognormal.py +124 -0
  18. FlowCyPy/distribution/normal.py +128 -0
  19. FlowCyPy/distribution/particle_size_distribution.py +132 -0
  20. FlowCyPy/distribution/uniform.py +117 -0
  21. FlowCyPy/distribution/weibull.py +115 -0
  22. FlowCyPy/flow_cell.py +198 -0
  23. FlowCyPy/helper.py +81 -0
  24. FlowCyPy/logger.py +136 -0
  25. FlowCyPy/noises.py +34 -0
  26. FlowCyPy/particle_count.py +127 -0
  27. FlowCyPy/peak_locator/__init__.py +4 -0
  28. FlowCyPy/peak_locator/base_class.py +163 -0
  29. FlowCyPy/peak_locator/basic.py +108 -0
  30. FlowCyPy/peak_locator/derivative.py +143 -0
  31. FlowCyPy/peak_locator/moving_average.py +166 -0
  32. FlowCyPy/physical_constant.py +19 -0
  33. FlowCyPy/plottings.py +269 -0
  34. FlowCyPy/population.py +136 -0
  35. FlowCyPy/populations_instances.py +65 -0
  36. FlowCyPy/scatterer_collection.py +306 -0
  37. FlowCyPy/signal_digitizer.py +90 -0
  38. FlowCyPy/source.py +249 -0
  39. FlowCyPy/units.py +30 -0
  40. FlowCyPy/utils.py +191 -0
  41. FlowCyPy-0.7.0.dist-info/LICENSE +21 -0
  42. FlowCyPy-0.7.0.dist-info/METADATA +252 -0
  43. FlowCyPy-0.7.0.dist-info/RECORD +45 -0
  44. FlowCyPy-0.7.0.dist-info/WHEEL +5 -0
  45. FlowCyPy-0.7.0.dist-info/top_level.txt +1 -0
FlowCyPy/plottings.py ADDED
@@ -0,0 +1,269 @@
1
+ from typing import Optional, Union, Tuple
2
+ from MPSPlots.styles import mps
3
+ import matplotlib.pyplot as plt
4
+ import seaborn as sns
5
+ import pandas as pd
6
+ import numpy as np
7
+
8
+
9
+ class MetricPlotter:
10
+ """
11
+ A class for creating 2D density and scatter plots of scattering intensities from two detectors.
12
+
13
+ Parameters
14
+ ----------
15
+ coincidence_dataframe : pd.DataFrame
16
+ The dataframe containing the coincidence data, including detector and feature columns.
17
+ detector_names : tuple of str
18
+ A tuple containing the names of the two detectors (detector 0 and detector 1).
19
+ """
20
+
21
+ def __init__(self, coincidence_dataframe: pd.DataFrame, detector_names: Tuple[str, str]):
22
+ self.coincidence_dataframe = coincidence_dataframe.reset_index()
23
+ self.detector_names = detector_names
24
+
25
+ def _extract_feature_data(self, feature: str):
26
+ """
27
+ Extracts and processes the feature data for the two detectors.
28
+
29
+ Parameters
30
+ ----------
31
+ feature : str
32
+ The feature to extract.
33
+
34
+ Returns
35
+ -------
36
+ Tuple[pd.Series, pd.Series, str, str]
37
+ Processed x_data, y_data, x_units, and y_units.
38
+ """
39
+ name_0, name_1 = self.detector_names
40
+ x_data = self.coincidence_dataframe[(name_0, feature)]
41
+ y_data = self.coincidence_dataframe[(name_1, feature)]
42
+
43
+ x_units = x_data.max().to_compact().units
44
+ y_units = y_data.max().to_compact().units
45
+
46
+ x_data = x_data.pint.to(x_units)
47
+ y_data = y_data.pint.to(y_units)
48
+
49
+ return x_data, y_data, x_units, y_units
50
+
51
+ def _create_density_plot(
52
+ self,
53
+ x_data: pd.Series,
54
+ y_data: pd.Series,
55
+ bandwidth_adjust: float,
56
+ ):
57
+ """
58
+ Creates a KDE density plot.
59
+
60
+ Parameters
61
+ ----------
62
+ x_data : pd.Series
63
+ The x-axis data.
64
+ y_data : pd.Series
65
+ The y-axis data.
66
+ bandwidth_adjust : float
67
+ Adjustment factor for the KDE bandwidth.
68
+
69
+ Returns
70
+ -------
71
+ sns.JointGrid
72
+ The seaborn JointGrid object.
73
+ """
74
+ return sns.jointplot(
75
+ data=self.coincidence_dataframe,
76
+ x=x_data,
77
+ y=y_data,
78
+ kind="kde",
79
+ alpha=0.8,
80
+ fill=True,
81
+ joint_kws={"alpha": 0.7, "bw_adjust": bandwidth_adjust},
82
+ )
83
+
84
+ def _add_scatterplot(
85
+ self,
86
+ g: sns.JointGrid,
87
+ x_data: pd.Series,
88
+ y_data: pd.Series,
89
+ color_palette: Optional[Union[str, dict]],
90
+ ):
91
+ """
92
+ Adds a scatterplot layer to the KDE density plot.
93
+
94
+ Parameters
95
+ ----------
96
+ g : sns.JointGrid
97
+ The seaborn JointGrid object to which the scatterplot is added.
98
+ x_data : pd.Series
99
+ The x-axis data.
100
+ y_data : pd.Series
101
+ The y-axis data.
102
+ color_palette : str or dict, optional
103
+ The color palette to use for the hue in the scatterplot.
104
+
105
+ Returns
106
+ -------
107
+ None
108
+ """
109
+ sns.scatterplot(
110
+ data=self.coincidence_dataframe,
111
+ x=x_data,
112
+ y=y_data,
113
+ hue="Label",
114
+ palette=color_palette,
115
+ ax=g.ax_joint,
116
+ alpha=0.6,
117
+ zorder=1,
118
+ )
119
+
120
+ def _apply_axis_labels(
121
+ self,
122
+ g: sns.JointGrid,
123
+ feature: str,
124
+ x_units: str,
125
+ y_units: str) -> None:
126
+ """
127
+ Sets the x and y labels with units on the plot.
128
+
129
+ Parameters
130
+ ----------
131
+ g : sns.JointGrid
132
+ The seaborn JointGrid object.
133
+ feature : str
134
+ The feature being plotted.
135
+ x_units : str
136
+ Units of the x-axis data.
137
+ y_units : str
138
+ Units of the y-axis data.
139
+
140
+ Returns
141
+ -------
142
+ None
143
+ """
144
+ name_0, name_1 = self.detector_names
145
+ g.ax_joint.set_xlabel(f"{feature} : {name_0} [{x_units:P}]")
146
+ g.ax_joint.set_ylabel(f"{feature}: {name_1} [{y_units:P}]")
147
+
148
+ def _apply_axis_limits(
149
+ self,
150
+ g: sns.JointGrid,
151
+ x_limits: Optional[Tuple],
152
+ y_limits: Optional[Tuple],
153
+ x_units: str,
154
+ y_units: str):
155
+ """
156
+ Sets the axis limits if specified.
157
+
158
+ Parameters
159
+ ----------
160
+ g : sns.JointGrid
161
+ The seaborn JointGrid object.
162
+ x_limits : tuple, optional
163
+ The x-axis limits (min, max), by default None.
164
+ y_limits : tuple, optional
165
+ The y-axis limits (min, max), by default None.
166
+ x_units : str
167
+ Units of the x-axis data.
168
+ y_units : str
169
+ Units of the y-axis data.
170
+
171
+ Returns
172
+ -------
173
+ None
174
+ """
175
+ if x_limits:
176
+ x0, x1 = x_limits
177
+ x0 = x0.to(x_units).magnitude
178
+ x1 = x1.to(x_units).magnitude
179
+ g.ax_joint.set_xlim(x0, x1)
180
+
181
+ if y_limits:
182
+ y0, y1 = y_limits
183
+ y0 = y0.to(y_units).magnitude
184
+ y1 = y1.to(y_units).magnitude
185
+ g.ax_joint.set_ylim(y0, y1)
186
+
187
+ def plot(
188
+ self,
189
+ feature: str,
190
+ show: bool = True,
191
+ log_plot: bool = True,
192
+ x_limits: Optional[Tuple] = None,
193
+ y_limits: Optional[Tuple] = None,
194
+ equal_axes: bool = False,
195
+ bandwidth_adjust: float = 1.0,
196
+ color_palette: Optional[Union[str, dict]] = 'tab10') -> None:
197
+ """
198
+ Generates a 2D density plot of the scattering intensities, overlaid with individual peak heights.
199
+
200
+ Parameters
201
+ ----------
202
+ feature : str
203
+ The feature to plot (e.g., 'intensity').
204
+ show : bool, optional
205
+ Whether to display the plot immediately, by default True.
206
+ log_plot : bool, optional
207
+ Whether to use logarithmic scaling for the plot axes, by default True.
208
+ x_limits : tuple, optional
209
+ The x-axis limits (min, max), by default None.
210
+ y_limits : tuple, optional
211
+ The y-axis limits (min, max), by default None.
212
+ equal_axes : bool, optional
213
+ Whether to enforce the same range for the x and y axes, by default False.
214
+ bandwidth_adjust : float, optional
215
+ Bandwidth adjustment factor for the kernel density estimate of the marginal distributions. Default is 1.0.
216
+ color_palette : str or dict, optional
217
+ The color palette to use for the hue in the scatterplot.
218
+
219
+ Returns
220
+ -------
221
+ None
222
+ """
223
+ x_data, y_data, x_units, y_units = self._extract_feature_data(feature)
224
+
225
+ # Determine equal axis limits if required
226
+ if equal_axes:
227
+ min_x = x_limits[0].to(x_units).magnitude if x_limits else x_data.min()
228
+ max_x = x_limits[1].to(x_units).magnitude if x_limits else x_data.max()
229
+ min_y = y_limits[0].to(y_units).magnitude if y_limits else y_data.min()
230
+ max_y = y_limits[1].to(y_units).magnitude if y_limits else y_data.max()
231
+
232
+ # Set common limits
233
+ min_val = min(min_x, min_y)
234
+ max_val = max(max_x, max_y)
235
+ x_limits = (min_val, max_val)
236
+ y_limits = (min_val, max_val)
237
+
238
+ with plt.style.context(mps):
239
+ if not log_plot:
240
+ # KDE + Scatterplot for linear plots
241
+ g = self._create_density_plot(x_data, y_data, bandwidth_adjust)
242
+ self._add_scatterplot(g, x_data, y_data, color_palette)
243
+ self._apply_axis_labels(g, feature, x_units, y_units)
244
+ self._apply_axis_limits(g, x_limits, y_limits, x_units, y_units)
245
+ else:
246
+ # Scatterplot only for log-scaled plots
247
+ fig, ax = plt.subplots()
248
+ sns.scatterplot(
249
+ x=x_data,
250
+ y=y_data,
251
+ hue=self.coincidence_dataframe["Label"],
252
+ palette=color_palette,
253
+ alpha=0.6,
254
+ ax=ax,
255
+ )
256
+ ax.set_xscale("log")
257
+ ax.set_yscale("log")
258
+ ax.set_xlabel(f"{feature} : {self.detector_names[0]} [{x_units:P}]")
259
+ ax.set_ylabel(f"{feature} : {self.detector_names[1]} [{y_units:P}]")
260
+ if x_limits:
261
+ ax.set_xlim([lim.to(x_units).magnitude for lim in x_limits])
262
+ if y_limits:
263
+ ax.set_ylim([lim.to(y_units).magnitude for lim in y_limits])
264
+ ax.legend()
265
+
266
+ plt.tight_layout()
267
+ if show:
268
+ plt.show()
269
+
FlowCyPy/population.py ADDED
@@ -0,0 +1,136 @@
1
+
2
+ from typing import Union
3
+ from FlowCyPy import distribution
4
+ import pandas as pd
5
+ from pydantic.dataclasses import dataclass
6
+ from pydantic import field_validator
7
+ from FlowCyPy import units
8
+ from FlowCyPy.utils import PropertiesReport
9
+ from PyMieSim.units import Quantity, RIU, meter
10
+ from FlowCyPy.particle_count import ParticleCount
11
+ from pint_pandas import PintArray
12
+
13
+
14
+ config_dict = dict(
15
+ arbitrary_types_allowed=True,
16
+ kw_only=True,
17
+ slots=True,
18
+ extra='forbid'
19
+ )
20
+
21
+
22
+ @dataclass(config=config_dict)
23
+ class Population(PropertiesReport):
24
+ """
25
+ A class representing a population of scatterers in a flow cytometry setup.
26
+
27
+ Parameters
28
+ ----------
29
+ name : str
30
+ Name of the population distribution.
31
+ refractive_index : Union[distribution.Base, Quantity]
32
+ Refractive index or refractive index distributions.
33
+ size : Union[distribution.Base, Quantity]
34
+ Particle size or size distributions.
35
+ particle_count : ParticleCount
36
+ Scatterer density in particles per cubic meter, default is 1 particle/m³.
37
+
38
+ """
39
+ name: str
40
+ refractive_index: Union[distribution.Base, Quantity]
41
+ size: Union[distribution.Base, Quantity]
42
+ particle_count: ParticleCount | Quantity
43
+
44
+ def __post_init__(self):
45
+ """
46
+ Automatically converts all Quantity attributes to their base SI units (i.e., without any prefixes).
47
+ This strips units like millimeter to meter, kilogram to gram, etc.
48
+ """
49
+ self.particle_count = ParticleCount(self.particle_count)
50
+ # Convert all Quantity attributes to base SI units (without any prefixes)
51
+ for attr_name, attr_value in vars(self).items():
52
+ if isinstance(attr_value, Quantity):
53
+ # Convert the quantity to its base unit (strip prefix)
54
+ setattr(self, attr_name, attr_value.to_base_units())
55
+
56
+ @field_validator('refractive_index')
57
+ def _validate_refractive_index(cls, value):
58
+ """
59
+ Validates that the refractive index is either a Quantity or a valid distribution.Base instance.
60
+
61
+ Parameters
62
+ ----------
63
+ value : Union[distribution.Base, Quantity]
64
+ The refractive index to validate.
65
+
66
+ Returns
67
+ -------
68
+ Union[distribution.Base, Quantity]
69
+ The validated refractive index.
70
+
71
+ Raises
72
+ ------
73
+ TypeError
74
+ If the refractive index is not of type Quantity or distribution.Base.
75
+ """
76
+ if isinstance(value, Quantity):
77
+ assert value.check(RIU), "The refractive index value provided does not have refractive index units [RIU]"
78
+ return distribution.Delta(position=value)
79
+
80
+ if isinstance(value, distribution.Base):
81
+ return value
82
+
83
+ raise TypeError(f"refractive_index must be of type Quantity<RIU or refractive_index_units> or distribution.Base, but got {type(value)}")
84
+
85
+ @field_validator('size')
86
+ def _validate_size(cls, value):
87
+ """
88
+ Validates that the size is either a Quantity or a valid distribution.Base instance.
89
+
90
+ Parameters
91
+ ----------
92
+ value : Union[distribution.Base, Quantity]
93
+ The size to validate.
94
+
95
+ Returns
96
+ -------
97
+ Union[distribution.Base, Quantity]
98
+ The validated size.
99
+
100
+ Raises
101
+ ------
102
+ TypeError
103
+ If the size is not of type Quantity or distribution.Base.
104
+ """
105
+ if isinstance(value, Quantity):
106
+ assert value.check(meter), "The size value provided does not have length units [meter]"
107
+ return distribution.Delta(position=value)
108
+
109
+ if isinstance(value, distribution.Base):
110
+ return value
111
+
112
+ raise TypeError(f"suze must be of type Quantity or distribution.Base, but got {type(value)}")
113
+
114
+ def dilute(self, factor: float) -> None:
115
+ self.particle_count /= factor
116
+
117
+ def generate_sampling(self, sampling: Quantity) -> tuple:
118
+ size = self.size.generate(sampling)
119
+
120
+ ri = self.refractive_index.generate(sampling)
121
+
122
+ return size, ri
123
+
124
+ def _get_sampling(self, sampling: int) -> pd.DataFrame:
125
+ size = self.size.generate(sampling)
126
+
127
+ ri = self.refractive_index.generate(sampling)
128
+
129
+ dataframe = pd.DataFrame(columns=['Size', 'RefractiveIndex'])
130
+
131
+ dataframe['Size'] = PintArray(size, dtype=size.units)
132
+ dataframe['RefractiveIndex'] = PintArray(ri, dtype=ri.units)
133
+
134
+ return dataframe
135
+
136
+ from FlowCyPy.populations_instances import * # noqa F403
@@ -0,0 +1,65 @@
1
+ from FlowCyPy.units import Quantity, nanometer, RIU, micrometer, particle
2
+ from FlowCyPy.population import Population
3
+ from FlowCyPy import distribution
4
+
5
+ class CallablePopulationMeta(type):
6
+ def __getattr__(cls, attr):
7
+ raise AttributeError(f"{cls.__name__} must be called as {cls.__name__}() to access its population instance.")
8
+
9
+
10
+ class CallablePopulation(metaclass=CallablePopulationMeta):
11
+ def __init__(self, name, size_dist, ri_dist):
12
+ self._name = name
13
+ self._size_distribution = size_dist
14
+ self._ri_distribution = ri_dist
15
+
16
+ def __call__(self, particle_count: Quantity = 1 * particle):
17
+ return Population(
18
+ particle_count=particle_count,
19
+ name=self._name,
20
+ size=self._size_distribution,
21
+ refractive_index=self._ri_distribution,
22
+ )
23
+
24
+
25
+ # Define populations
26
+ _populations = (
27
+ ('Exosome', 70 * nanometer, 2.0, 1.39 * RIU, 0.02 * RIU),
28
+ ('MicroVesicle', 400 * nanometer, 1.5, 1.39 * RIU, 0.02 * RIU),
29
+ ('ApoptoticBodies', 2 * micrometer, 1.2, 1.40 * RIU, 0.03 * RIU),
30
+ ('HDL', 10 * nanometer, 3.5, 1.33 * RIU, 0.01 * RIU),
31
+ ('LDL', 20 * nanometer, 3.0, 1.35 * RIU, 0.02 * RIU),
32
+ ('VLDL', 50 * nanometer, 2.0, 1.445 * RIU, 0.0005 * RIU),
33
+ ('Platelet', 2000 * nanometer, 2.5, 1.38 * RIU, 0.01 * RIU),
34
+ ('CellularDebris', 3 * micrometer, 1.0, 1.40 * RIU, 0.03 * RIU),
35
+ )
36
+
37
+ # Dynamically create population classes
38
+ for (name, size, size_spread, ri, ri_spread) in _populations:
39
+ size_distribution = distribution.RosinRammler(
40
+ characteristic_size=size,
41
+ spread=size_spread
42
+ )
43
+
44
+ ri_distribution = distribution.Normal(
45
+ mean=ri,
46
+ std_dev=ri_spread
47
+ )
48
+
49
+ # Create a class dynamically for each population
50
+ cls = type(name, (CallablePopulation,), {})
51
+ globals()[name] = cls(name, size_distribution, ri_distribution)
52
+
53
+
54
+ # Helper function for microbeads
55
+ def get_microbeads(size: Quantity, refractive_index: Quantity, name: str) -> Population:
56
+ size_distribution = distribution.Delta(position=size)
57
+ ri_distribution = distribution.Delta(position=refractive_index)
58
+
59
+ microbeads = Population(
60
+ name=name,
61
+ size=size_distribution,
62
+ refractive_index=ri_distribution
63
+ )
64
+
65
+ return microbeads