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/detector.py ADDED
@@ -0,0 +1,616 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from typing import Optional, Union
4
+ import matplotlib.pyplot as plt
5
+ from FlowCyPy.units import AU, volt, watt, degree, second, ampere, coulomb
6
+ from FlowCyPy.utils import PropertiesReport
7
+ from pydantic.dataclasses import dataclass
8
+ from pydantic import field_validator
9
+ from functools import cached_property
10
+ import pint_pandas
11
+ from FlowCyPy.physical_constant import PhysicalConstant
12
+ from PyMieSim.units import Quantity
13
+ from FlowCyPy.noises import NoiseSetting
14
+ from FlowCyPy.helper import plot_helper
15
+ from FlowCyPy.peak_locator import BasePeakLocator
16
+ import logging
17
+ from copy import copy
18
+
19
+ config_dict = dict(
20
+ arbitrary_types_allowed=True,
21
+ kw_only=True,
22
+ slots=True,
23
+ extra='forbid'
24
+ )
25
+
26
+
27
+ @dataclass(config=config_dict, unsafe_hash=True)
28
+ class Detector(PropertiesReport):
29
+ """
30
+ A class representing a signal detector used in flow cytometry.
31
+
32
+ This class models a photodetector, simulating signal acquisition, noise addition, and signal processing
33
+ for analysis. It can optionally simulate different noise sources: shot noise, thermal noise, and dark current noise.
34
+
35
+ Parameters
36
+ ----------
37
+ name : str
38
+ The name or identifier of the detector.
39
+ sampling_freq : Quantity
40
+ The sampling frequency of the detector in hertz.
41
+ phi_angle : Quantity
42
+ The detection angle in degrees.
43
+ numerical_aperture : Quantity
44
+ The numerical aperture of the detector, a unitless value.
45
+ responsitivity : Quantity
46
+ Detector's responsivity, default is 1 volt per watt.
47
+ noise_level : Quantity
48
+ The base noise level added to the signal, default is 0 volts.
49
+ baseline_shift : Quantity
50
+ The baseline shift applied to the signal, default is 0 volts.
51
+ saturation_level : Quantity
52
+ The maximum signal level in volts before saturation, default is infinity.
53
+ dark_current : Quantity
54
+ The dark current of the detector, default is 0 amperes.
55
+ resistance : Quantity
56
+ Resistance of the detector, used for thermal noise simulation.
57
+ temperature : Quantity
58
+ Temperature of the detector in Kelvin, used for thermal noise simulation.
59
+ n_bins : Union[int, str]
60
+ The number of discretization bins or bit-depth (e.g., '12bit').
61
+ """
62
+ sampling_freq: Quantity
63
+ phi_angle: Quantity
64
+ numerical_aperture: Quantity
65
+
66
+ gamma_angle: Optional[Quantity] = Quantity(0, degree)
67
+ sampling: Optional[Quantity] = 100 * AU
68
+ responsitivity: Optional[Quantity] = Quantity(1, ampere / watt)
69
+ noise_level: Optional[Quantity] = Quantity(0.0, volt)
70
+ baseline_shift: Optional[Quantity] = Quantity(0.0, volt)
71
+ saturation_level: Optional[Quantity] = Quantity(np.inf, volt)
72
+ dark_current: Optional[Quantity] = Quantity(0.0, ampere) # Dark current
73
+ resistance: Optional[Quantity] = Quantity(50.0, 'ohm') # Resistance for thermal noise
74
+ temperature: Optional[Quantity] = Quantity(0.0, 'kelvin') # Temperature for thermal noise
75
+ n_bins: Optional[Union[int, str]] = None
76
+ name: Optional[str] = None
77
+
78
+ @cached_property
79
+ def bandwidth(self) -> Quantity:
80
+ return self.sampling_freq / 2
81
+ """
82
+ Automatically calculates the bandwidth based on the sampling frequency.
83
+
84
+ Returns
85
+ -------
86
+ Quantity
87
+ The bandwidth of the detector, which is half the sampling frequency (Nyquist limit).
88
+ """
89
+ return self.sampling_freq / 2
90
+
91
+ @field_validator('sampling_freq')
92
+ def _validate_sampling_freq(cls, value):
93
+ """
94
+ Validates that the sampling frequency is provided in hertz.
95
+
96
+ Parameters
97
+ ----------
98
+ value : Quantity
99
+ The sampling frequency to validate.
100
+
101
+ Returns
102
+ -------
103
+ Quantity
104
+ The validated sampling frequency.
105
+
106
+ Raises:
107
+ ValueError: If the sampling frequency is not in hertz.
108
+ """
109
+ if not value.check('Hz'):
110
+ raise ValueError(f"sampling_freq must be in hertz, but got {value.units}")
111
+ return value
112
+
113
+ @field_validator('phi_angle', 'gamma_angle')
114
+ def _validate_angles(cls, value):
115
+ """
116
+ Validates that the provided angles are in degrees.
117
+
118
+ Parameters
119
+ ----------
120
+ value : Quantity
121
+ The angle value to validate.
122
+
123
+ Returns
124
+ -------
125
+ Quantity
126
+ The validated angle.
127
+
128
+ Raises:
129
+ ValueError: If the angle is not in degrees.
130
+ """
131
+ if not value.check('degree'):
132
+ raise ValueError(f"Angle must be in degrees, but got {value.units}")
133
+ return value
134
+
135
+ @field_validator('responsitivity')
136
+ def _validate_responsitivity(cls, value):
137
+ """
138
+ Validates that the detector's responsivity is provided in volts per watt.
139
+
140
+ Parameters
141
+ ----------
142
+ value : Quantity
143
+ The responsivity value to validate.
144
+
145
+ Returns:
146
+ Quantity: The validated responsivity.
147
+
148
+ Raises:
149
+ ValueError: If the responsivity is not in volts per watt.
150
+ """
151
+ if not value.check('A / W'):
152
+ raise ValueError(f"Responsitivity must be in ampere per watt, but got {value.units}")
153
+ return value
154
+
155
+ @field_validator('noise_level', 'baseline_shift', 'saturation_level')
156
+ def _validate_voltage_attributes(cls, value):
157
+ """
158
+ Validates that noise level, baseline shift, and saturation level are all in volts.
159
+
160
+ Parameters
161
+ ----------
162
+ value : Quantity
163
+ The voltage attribute to validate (noise level, baseline shift, or saturation).
164
+
165
+ Returns
166
+ -------
167
+ Quantity
168
+ The validated voltage attribute.
169
+
170
+ Raises:
171
+ ValueError: If the attribute is not in volts.
172
+ """
173
+ if not value.check('volt'):
174
+ raise ValueError(f"Voltage attributes must be in volts, but got {value.units}")
175
+ return value
176
+
177
+ def __post_init__(self) -> None:
178
+ """
179
+ Finalizes the initialization of the detector object and processes the number of bins.
180
+ """
181
+ if self.name is None:
182
+ self.name = str(id(self))
183
+
184
+ self._process_n_bins()
185
+
186
+ def _convert_attr_to_SI(self) -> None:
187
+ # Convert all Quantity attributes to base SI units (without any prefixes)
188
+ for attr_name, attr_value in vars(self).items():
189
+ if isinstance(attr_value, Quantity):
190
+ setattr(self, attr_name, attr_value.to_base_units())
191
+
192
+ def _process_n_bins(self) -> None:
193
+ r"""
194
+ Processes the `n_bins` attribute to ensure it is an integer representing the number of bins.
195
+
196
+ If `n_bins` is provided as a bit-depth string (e.g., '12bit'), it converts it to an integer number of bins.
197
+ If no valid `n_bins` is provided, a default of 100 bins is used.
198
+ """
199
+ if isinstance(self.n_bins, str):
200
+ bit_depth = int(self.n_bins.rstrip('bit'))
201
+ self.n_bins = 2 ** bit_depth
202
+
203
+ def _add_thermal_noise_to_raw_signal(self) -> np.ndarray:
204
+ r"""
205
+ Generates thermal noise (Johnson-Nyquist noise) based on temperature, resistance, and bandwidth.
206
+
207
+ Thermal noise is caused by the thermal agitation of charge carriers. It is given by:
208
+ \[
209
+ \sigma_{\text{thermal}} = \sqrt{4 k_B T B R}
210
+ \]
211
+ Where:
212
+ - \( k_B \) is the Boltzmann constant (\(1.38 \times 10^{-23}\) J/K),
213
+ - \( T \) is the temperature in Kelvin,
214
+ - \( B \) is the bandwidth,
215
+ - \( R \) is the resistance.
216
+
217
+ Returns
218
+ -------
219
+ np.ndarray
220
+ An array of thermal noise values.
221
+ """
222
+ if self.resistance.magnitude == 0 or self.temperature.magnitude == 0 or not NoiseSetting.include_thermal_noise or not NoiseSetting.include_noises:
223
+ return
224
+
225
+ noise_std = np.sqrt(
226
+ 4 * PhysicalConstant.kb * self.temperature * self.resistance * self.bandwidth
227
+ )
228
+
229
+ thermal_noise = np.random.normal(0, noise_std.to(volt).magnitude, size=len(self.dataframe)) * volt
230
+
231
+ self.dataframe['RawSignal'] += thermal_noise
232
+
233
+ return thermal_noise
234
+
235
+ def _add_dark_current_noise_to_raw_signal(self) -> np.ndarray:
236
+ r"""
237
+ Generates dark current noise (shot noise from dark current).
238
+
239
+ Dark current noise is a type of shot noise caused by the random generation of electrons in a detector,
240
+ even in the absence of light. It is given by:
241
+ \[
242
+ \sigma_{\text{dark current}} = \sqrt{2 e I_{\text{dark}} B}
243
+ \]
244
+ Where:
245
+ - \( e \) is the elementary charge,
246
+ - \( I_{\text{dark}} \) is the dark current,
247
+ - \( B \) is the bandwidth.
248
+
249
+ Returns
250
+ -------
251
+ np.ndarray
252
+ An array of dark current noise values.
253
+ """
254
+ if self.dark_current.magnitude == 0 or not NoiseSetting.include_dark_current_noise or not NoiseSetting.include_noises:
255
+ return
256
+
257
+ dark_current_std = np.sqrt(
258
+ 2 * 1.602176634e-19 * coulomb * self.dark_current * self.bandwidth
259
+ )
260
+
261
+ dark_current_noise = np.random.normal(0, dark_current_std.to(ampere).magnitude, size=len(self.dataframe)) * ampere
262
+
263
+ dark_voltage_noise = dark_current_noise * self.resistance
264
+
265
+ self.dataframe['RawSignal'] += dark_voltage_noise
266
+
267
+ return dark_voltage_noise
268
+
269
+ def _add_optical_power_to_raw_signal(self, optical_power: Quantity) -> None:
270
+ r"""
271
+ Simulates photon shot noise based on the given optical power and detector bandwidth, and returns
272
+ the corresponding voltage noise due to photon shot noise.
273
+
274
+ Photon shot noise arises from the random and discrete arrival of photons at the detector. The noise
275
+ follows Poisson statistics, and the standard deviation of the shot noise depends on the photon flux
276
+ and the detector bandwidth. The result is a voltage noise that models these fluctuations.
277
+
278
+ Parameters
279
+ ----------
280
+ optical_power : Quantity
281
+ The optical power incident on the detector, in watts (W).
282
+
283
+ Returns
284
+ -------
285
+ np.ndarray
286
+ An array representing the voltage noise due to photon shot noise, in volts (V).
287
+
288
+ Physics:
289
+ - The number of photons arriving at the detector \( N_{\text{ph}} \) is given by:
290
+ \[
291
+ N_{\text{ph}} = \frac{P_{\text{opt}}}{E_{\text{photon}}}
292
+ \]
293
+ where:
294
+ - \( P_{\text{opt}} \) is the optical power (W),
295
+ - \( E_{\text{photon}} = \frac{h \cdot c}{\lambda} \) is the energy of a photon (J),
296
+ - \( h \) is Planck's constant \(6.626 \times 10^{-34}\, \text{J} \cdot \text{s}\),
297
+ - \( c \) is the speed of light \(3 \times 10^8 \, \text{m/s}\),
298
+ - \( \lambda \) is the wavelength of the incident light.
299
+
300
+ - The standard deviation of the current noise due to photon shot noise is:
301
+ \[
302
+ \sigma_{I_{\text{shot}}} = \sqrt{2 \cdot e \cdot I_{\text{photon}} \cdot B}
303
+ \]
304
+ where:
305
+ - \( I_{\text{photon}} \) is the photocurrent generated by the incident optical power (A),
306
+ - \( e \) is the elementary charge \(1.602 \times 10^{-19} \, \text{C}\),
307
+ - \( B \) is the bandwidth of the detector (Hz).
308
+
309
+ - The voltage shot noise \( \sigma_{V_{\text{shot}}} \) is then given by Ohm's law:
310
+ \[
311
+ \sigma_{V_{\text{shot}}} = \sigma_{I_{\text{shot}}} \cdot R_{\text{load}}
312
+ \]
313
+ where:
314
+ - \( R_{\text{load}} \) is the load resistance of the detector (ohms).
315
+ """
316
+ # Step 1: Compute the photocurrent for all time points at once using vectorization
317
+ I_photon = self.responsitivity * optical_power
318
+
319
+ # Step 2: Compute the shot noise current for each time point using vectorization
320
+ i_shot = 2 * PhysicalConstant.e * I_photon * self.bandwidth
321
+
322
+ I_shot = np.sqrt(i_shot)
323
+
324
+ # Step 3: Convert shot noise current to shot noise voltage using vectorization
325
+ V_shot = I_shot * self.resistance
326
+
327
+ # Step 4: Generate Gaussian noise for each time point with standard deviation V_shot
328
+ noise_signal = np.random.normal(0, V_shot.to(volt).magnitude, len(self.dataframe['RawSignal'])) * volt
329
+
330
+ self.dataframe['RawSignal'] += optical_power * self.responsitivity * self.resistance
331
+
332
+ if NoiseSetting.include_shot_noise and NoiseSetting.include_noises:
333
+ self.dataframe['RawSignal'] += noise_signal
334
+
335
+ return noise_signal
336
+
337
+ def init_raw_signal(self, run_time: Quantity) -> None:
338
+ r"""
339
+ Initializes the raw signal and time arrays for the detector and generates optional noise.
340
+
341
+ Parameters
342
+ ----------
343
+ run_time : Quantity
344
+ The total duration of the signal to simulate (in seconds).
345
+
346
+ The photocurrent is calculated as:
347
+ \[
348
+ I_{\text{ph}} = P_{\text{opt}} \times R_{\text{ph}}
349
+ \]
350
+ Where:
351
+ - \( P_{\text{opt}} \) is the optical power,
352
+ - \( R_{\text{ph}} \) is the responsivity in amperes per watt.
353
+ """
354
+ self.run_time = run_time
355
+ time_points = int(self.sampling_freq * run_time)
356
+ time = np.linspace(0, run_time, time_points)
357
+
358
+ self.dataframe = pd.DataFrame(
359
+ data=dict(
360
+ Time=pint_pandas.PintArray(time, dtype=second),
361
+ RawSignal=pint_pandas.PintArray(np.zeros_like(time), dtype=volt),
362
+ Signal=pint_pandas.PintArray(np.zeros_like(time), dtype=volt)
363
+ )
364
+ )
365
+
366
+ def capture_signal(self) -> None:
367
+ """
368
+ Processes and captures the final signal by applying noise, baseline shifts, and saturation.
369
+ """
370
+ self.dataframe['Signal'] = self.dataframe['RawSignal']
371
+
372
+ self._apply_baseline_and_noise()
373
+ self._apply_saturation()
374
+ self._discretize_signal()
375
+
376
+ self.is_saturated = True if np.any(self.dataframe.Signal == self.saturation_level.to(volt).magnitude) else False
377
+
378
+ def _apply_baseline_and_noise(self) -> None:
379
+ """
380
+ Adds baseline shift and base noise to the raw signal.
381
+ """
382
+ w0 = np.pi / 2 / second
383
+ baseline = self.baseline_shift * np.sin(w0 * self.dataframe.Time)
384
+
385
+ # Scale noise level by the square root of the bandwidth
386
+ noise_scaling = np.sqrt(self.bandwidth.to('Hz').magnitude)
387
+ noise = self.noise_level * noise_scaling * np.random.normal(size=len(self.dataframe))
388
+
389
+ self.dataframe.Signal += baseline + noise
390
+
391
+ def _apply_saturation(self) -> None:
392
+ """
393
+ Applies a saturation limit to the signal.
394
+
395
+ Signal values that exceed the saturation level are clipped to the maximum allowed value.
396
+ """
397
+ clipped = np.clip(self.dataframe.Signal, 0 * volt, self.saturation_level)
398
+ self.dataframe.Signal = pint_pandas.PintArray(clipped, clipped.units)
399
+
400
+ def _discretize_signal(self) -> None:
401
+ """
402
+ Discretizes the processed signal into a specified number of bins.
403
+
404
+ The signal is mapped to discrete levels, depending on the number of bins (derived from `n_bins`).
405
+ """
406
+
407
+ if self.n_bins is not None:
408
+ max_level = self.saturation_level if not np.isinf(self.saturation_level) else self.dataframe.Signal.max()
409
+
410
+ bins = np.linspace(0 * max_level, max_level, self.n_bins)
411
+
412
+ digitized = np.digitize(
413
+ x=self.dataframe.Signal.pint.to(volt).pint.magnitude,
414
+ bins=bins.to(volt).magnitude
415
+ ) - 1
416
+ self.dataframe.Signal = pint_pandas.PintArray(bins[digitized], volt)
417
+
418
+ @plot_helper
419
+ def plot(
420
+ self,
421
+ color: str = None,
422
+ ax: Optional[plt.Axes] = None,
423
+ time_unit: Optional[Union[str, Quantity]] = None,
424
+ signal_unit: Optional[Union[str, Quantity]] = None,
425
+ add_captured: bool = True,
426
+ add_peak_locator: bool = False,
427
+ add_raw: bool = False
428
+ ) -> tuple[Quantity, Quantity]:
429
+ """
430
+ Visualizes the signal and optional components (peaks, raw signal) over time.
431
+
432
+ This method generates a customizable plot of the processed signal as a function of time.
433
+ Additional components like raw signals and detected peaks can also be overlaid.
434
+
435
+ Parameters
436
+ ----------
437
+ color : str, optional
438
+ Color for the processed signal line. Default is 'C0' (default Matplotlib color cycle).
439
+ ax : matplotlib.axes.Axes, optional
440
+ An existing Matplotlib Axes object to plot on. If None, a new Axes will be created.
441
+ time_unit : str or Quantity, optional
442
+ Desired unit for the time axis. If None, defaults to the most compact unit of the `Time` column.
443
+ signal_unit : str or Quantity, optional
444
+ Desired unit for the signal axis. If None, defaults to the most compact unit of the `Signal` column.
445
+ add_captured : bool, optional
446
+ If True, overlays the captured signal data on the plot. Default is True.
447
+ add_peak_locator : bool, optional
448
+ If True, adds the detected peaks (if available) to the plot. Default is False.
449
+ add_raw : bool, optional
450
+ If True, overlays the raw signal data on the plot. Default is False.
451
+
452
+ Returns
453
+ -------
454
+ tuple[Quantity, Quantity]
455
+ A tuple containing the units used for the time and signal axes, respectively.
456
+
457
+ Notes
458
+ -----
459
+ - The `Time` and `Signal` data are automatically converted to the specified units for consistency.
460
+ - If no `ax` is provided, a new figure and axis will be generated.
461
+ - Warnings are logged if peak locator data is unavailable when `add_peak_locator` is True.
462
+ """
463
+ # Set default units if not provided
464
+ signal_unit = signal_unit or self.dataframe.Signal.max().to_compact().units
465
+ time_unit = time_unit or self.dataframe.Time.max().to_compact().units
466
+
467
+ # Plot captured signal
468
+ if add_captured:
469
+ self._add_captured_signal_to_ax(ax=ax, time_unit=time_unit, signal_unit=signal_unit, color=color)
470
+
471
+ # Overlay peak locator positions, if requested
472
+ if add_peak_locator:
473
+ self._add_peak_locator_to_ax(ax=ax, time_unit=time_unit, signal_unit=signal_unit)
474
+
475
+ # Overlay raw signal, if requested
476
+ if add_raw:
477
+ self._add_raw_signal_to_ax(ax=ax, time_unit=time_unit, signal_unit=signal_unit)
478
+
479
+
480
+ # Customize labels
481
+ ax.set_xlabel(f"Time [{time_unit:P}]")
482
+ ax.set_ylabel(f"{self.name} [{signal_unit:P}]")
483
+ ax.legend()
484
+
485
+ return time_unit, signal_unit
486
+
487
+ def _add_peak_locator_to_ax(self, ax: plt.Axes, time_unit: Quantity, signal_unit: Quantity) -> None:
488
+ """
489
+ Adds peak positions detected by the algorithm to the plot.
490
+
491
+ Parameters
492
+ ----------
493
+ ax : matplotlib.axes.Axes
494
+ The axis object to plot on.
495
+ time_unit : Quantity
496
+ Unit for the time axis.
497
+ signal_unit : Quantity
498
+ Unit for the signal axis.
499
+ """
500
+ if not hasattr(self, 'algorithm'):
501
+ logging.warning("The detector does not have a peak locator algorithm. Peaks cannot be plotted.")
502
+ return
503
+
504
+ self.algorithm._add_to_ax(ax=ax, signal_unit=signal_unit, time_unit=time_unit)
505
+
506
+ def _add_captured_signal_to_ax(self, ax: plt.Axes, time_unit: Quantity, signal_unit: Quantity, color: str = None) -> None:
507
+ """
508
+ Adds the processed (captured) signal to the plot.
509
+
510
+ Parameters
511
+ ----------
512
+ ax : matplotlib.axes.Axes
513
+ The axis object to plot on.
514
+ time_unit : Quantity
515
+ Unit for the time axis.
516
+ signal_unit : Quantity
517
+ Unit for the signal axis.
518
+ color : str
519
+ Color for the signal line.
520
+ """
521
+ if not hasattr(self.dataframe, 'Signal'):
522
+ logging.warning("The detector does not have a captured signal. Please run .capture_signal() method first.")
523
+ return
524
+
525
+ x = self.dataframe['Time'].pint.to(time_unit)
526
+ y = self.dataframe['Signal'].pint.to(signal_unit)
527
+ ax.plot(x, y, color=color, label='Processed Signal')
528
+
529
+ def _add_raw_signal_to_ax(self, ax: plt.Axes, time_unit: Quantity, signal_unit: Quantity, color: str = None) -> None:
530
+ """
531
+ Adds the raw signal to the plot.
532
+
533
+ Parameters
534
+ ----------
535
+ ax : matplotlib.axes.Axes
536
+ The axis object to plot on.
537
+ time_unit : Quantity
538
+ Unit for the time axis.
539
+ signal_unit : Quantity
540
+ Unit for the signal axis.
541
+ color : str
542
+ Color for the raw signal line.
543
+ """
544
+ if not hasattr(self.dataframe, 'RawSignal'):
545
+ logging.warning("The detector does not have a captured signal. Please run .init_raw_signal(run_time) method first.")
546
+ return
547
+
548
+ x = self.dataframe['Time'].pint.to(time_unit)
549
+ y = self.dataframe['RawSignal'].pint.to(signal_unit)
550
+ ax.plot(x, y, color=color, linestyle='--', label=f'{self.name}: Raw Signal')
551
+
552
+ def set_peak_locator(self, algorithm: BasePeakLocator, compute_peak_area: bool = True) -> None:
553
+ """
554
+ Assigns a peak detection algorithm to the detector, analyzes the signal,
555
+ and extracts peak features such as height, width, and area.
556
+
557
+ Parameters
558
+ ----------
559
+ algorithm : BasePeakLocator
560
+ An instance of a peak detection algorithm derived from BasePeakLocator.
561
+ compute_peak_area : bool, optional
562
+ Whether to compute the area under the detected peaks (default is True).
563
+
564
+ Raises
565
+ ------
566
+ TypeError
567
+ If the provided algorithm is not an instance of BasePeakLocator.
568
+ ValueError
569
+ If the algorithm has already been initialized with peak data.
570
+ RuntimeError
571
+ If the detector's signal data (dataframe) is not available.
572
+
573
+ Notes
574
+ -----
575
+ - The `algorithm` parameter should be a fresh instance of a peak detection algorithm.
576
+ - The method will analyze the detector's signal immediately upon setting the algorithm.
577
+ - Peak detection results are stored in the algorithm's `peak_properties` attribute.
578
+ """
579
+
580
+ # Ensure the algorithm is an instance of BasePeakLocator
581
+ if not isinstance(algorithm, BasePeakLocator):
582
+ raise TypeError("The algorithm must be an instance of BasePeakLocator.")
583
+
584
+ # Ensure the detector has signal data available for analysis
585
+ if not hasattr(self, 'dataframe') or self.dataframe is None:
586
+ raise RuntimeError("The detector does not have signal data available for peak detection.")
587
+
588
+ # Set the algorithm and perform peak detection
589
+ self.algorithm = copy(algorithm)
590
+ self.algorithm.init_data(self.dataframe)
591
+ self.algorithm.detect_peaks(compute_area=compute_peak_area)
592
+
593
+ # Log the result of peak detection
594
+ peak_count = len(self.algorithm.peak_properties) if hasattr(self.algorithm, 'peak_properties') else 0
595
+ logging.info(f"Detector {self.name}: Detected {peak_count} peaks.")
596
+
597
+ def print_properties(self) -> None:
598
+ """
599
+ Prints specific properties of the Detector instance, such as coupling factor and medium refractive index.
600
+ This method calls the parent class method to handle the actual property printing logic.
601
+
602
+ """
603
+ _dict = {
604
+ 'Sampling frequency': self.sampling_freq,
605
+ 'Phi angle': self.phi_angle,
606
+ 'Gamma angle': self.gamma_angle,
607
+ 'Numerical aperture': self.numerical_aperture,
608
+ 'Responsitivity': self.responsitivity,
609
+ 'Saturation Level': self.saturation_level,
610
+ 'Dark Current': self.dark_current,
611
+ 'Resistance': self.resistance,
612
+ 'Temperature': self.temperature,
613
+ 'N Bins': self.n_bins
614
+ }
615
+
616
+ super(Detector, self).print_properties(**_dict)
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from pathlib import Path
5
+ import FlowCyPy
6
+
7
+
8
+ __all__ = [
9
+ 'root_path',
10
+ 'project_path',
11
+ 'doc_path',
12
+ 'doc_css_path',
13
+ 'logo_path'
14
+ ]
15
+
16
+ root_path = Path(FlowCyPy.__path__[0])
17
+
18
+ project_path = root_path.parents[0]
19
+
20
+ example_directory = root_path.joinpath('examples')
21
+
22
+ doc_path = project_path.joinpath('docs')
23
+
24
+ doc_css_path = doc_path.joinpath('source/_static/default.css')
25
+
26
+ logo_path = doc_path.joinpath('images/logo.png')
27
+
28
+ examples_path = root_path.joinpath('examples')
29
+
30
+ if __name__ == '__main__':
31
+ for path_name in __all__:
32
+ path = locals()[path_name]
33
+ print(path)
34
+ assert path.exists(), f"Path {path_name} do not exists"
35
+
36
+ # -
@@ -0,0 +1,16 @@
1
+ from .normal import Normal
2
+ from .lognormal import LogNormal
3
+ from .uniform import Uniform
4
+ from .delta import Delta
5
+ from .weibull import Weibull
6
+ from .base_class import Base
7
+ from .particle_size_distribution import RosinRammler
8
+
9
+ __all__ = [
10
+ Normal,
11
+ LogNormal,
12
+ Weibull,
13
+ Delta,
14
+ Uniform,
15
+ RosinRammler
16
+ ]