FlowCyPy 0.5.15__tar.gz → 0.6.0__tar.gz

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 (166) hide show
  1. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/_version.py +2 -2
  2. flowcypy-0.6.0/FlowCyPy/coupling_mechanism/mie.py +207 -0
  3. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/coupling_mechanism/rayleigh.py +8 -7
  4. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/coupling_mechanism/uniform.py +2 -2
  5. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/cytometer.py +87 -176
  6. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/detector.py +85 -264
  7. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/distribution/base_class.py +16 -2
  8. flowcypy-0.6.0/FlowCyPy/distribution/delta.py +104 -0
  9. flowcypy-0.6.0/FlowCyPy/distribution/lognormal.py +124 -0
  10. flowcypy-0.6.0/FlowCyPy/distribution/normal.py +128 -0
  11. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/distribution/particle_size_distribution.py +46 -17
  12. flowcypy-0.6.0/FlowCyPy/distribution/uniform.py +117 -0
  13. flowcypy-0.6.0/FlowCyPy/distribution/weibull.py +115 -0
  14. flowcypy-0.6.0/FlowCyPy/experiment.py +398 -0
  15. flowcypy-0.6.0/FlowCyPy/flow_cell.py +198 -0
  16. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/helper.py +0 -4
  17. flowcypy-0.6.0/FlowCyPy/logger.py +136 -0
  18. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/particle_count.py +16 -5
  19. flowcypy-0.6.0/FlowCyPy/plottings.py +269 -0
  20. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/population.py +23 -26
  21. flowcypy-0.6.0/FlowCyPy/report.py +236 -0
  22. flowcypy-0.6.0/FlowCyPy/scatterer_collection.py +299 -0
  23. flowcypy-0.6.0/FlowCyPy/signal_digitizer.py +179 -0
  24. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy.egg-info/PKG-INFO +2 -2
  25. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy.egg-info/SOURCES.txt +14 -1
  26. {flowcypy-0.5.15 → flowcypy-0.6.0}/PKG-INFO +2 -2
  27. flowcypy-0.6.0/developments/doc/canto_spec.md +61 -0
  28. flowcypy-0.6.0/developments/get_started.md +23 -0
  29. flowcypy-0.6.0/developments/image.png +0 -0
  30. flowcypy-0.6.0/developments/output_file.prof +0 -0
  31. flowcypy-0.6.0/developments/scripts/dev_stats_0.py +91 -0
  32. flowcypy-0.6.0/developments/scripts/dev_stats_1.py +70 -0
  33. flowcypy-0.6.0/developments/scripts/dev_stats_2.py +17 -0
  34. flowcypy-0.6.0/developments/scripts/temp.py +35 -0
  35. flowcypy-0.6.0/developments/test.pdf +0 -0
  36. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/density_plots/1_populations.py +18 -23
  37. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/density_plots/2_populations.py +24 -32
  38. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/density_plots/3_populations.py +19 -23
  39. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/density_plots/custom_populations.py +22 -21
  40. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/extras/distributions.py +2 -2
  41. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/extras/flow_cytometer_signal.py +19 -32
  42. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/extras/full_workflow.py +21 -29
  43. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/extras/scatterer_distribution.py +6 -22
  44. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/noise_sources/dark_current.py +20 -7
  45. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/noise_sources/shot_noise.py +24 -8
  46. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/noise_sources/thermal.py +23 -11
  47. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/tutorials/limit_of_detection.py +21 -31
  48. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/tutorials/workflow.py +44 -44
  49. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/sg_execution_times.rst +24 -21
  50. {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_coupling_mechanism.py +31 -15
  51. {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_detector_noise.py +47 -24
  52. {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_distribution.py +5 -5
  53. {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_flow_cytometer.py +26 -27
  54. {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_noises.py +36 -13
  55. {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_peak_analyzer.py +46 -53
  56. {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_population.py +32 -25
  57. {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_scatterer_distribution.py +40 -31
  58. flowcypy-0.5.15/FlowCyPy/distribution/delta.py +0 -79
  59. flowcypy-0.5.15/FlowCyPy/distribution/lognormal.py +0 -87
  60. flowcypy-0.5.15/FlowCyPy/distribution/normal.py +0 -88
  61. flowcypy-0.5.15/FlowCyPy/distribution/uniform.py +0 -88
  62. flowcypy-0.5.15/FlowCyPy/distribution/weibull.py +0 -75
  63. flowcypy-0.5.15/FlowCyPy/flow_cell.py +0 -295
  64. flowcypy-0.5.15/FlowCyPy/logger.py +0 -322
  65. flowcypy-0.5.15/FlowCyPy/scatterer_collection.py +0 -299
  66. flowcypy-0.5.15/developments/scripts/dev_temp.py +0 -96
  67. {flowcypy-0.5.15 → flowcypy-0.6.0}/.flake8 +0 -0
  68. {flowcypy-0.5.15 → flowcypy-0.6.0}/.github/dependabot.yml +0 -0
  69. {flowcypy-0.5.15 → flowcypy-0.6.0}/.github/workflows/deploy_PyPi.yml +0 -0
  70. {flowcypy-0.5.15 → flowcypy-0.6.0}/.github/workflows/deploy_anaconda.yml +0 -0
  71. {flowcypy-0.5.15 → flowcypy-0.6.0}/.github/workflows/deploy_coverage.yml +0 -0
  72. {flowcypy-0.5.15 → flowcypy-0.6.0}/.github/workflows/deploy_documentation.yml +0 -0
  73. {flowcypy-0.5.15 → flowcypy-0.6.0}/.gitignore +0 -0
  74. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/__init__.py +0 -0
  75. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/classifier.py +0 -0
  76. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/coupling_mechanism/__init__.py +0 -0
  77. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/coupling_mechanism/empirical.py +0 -0
  78. /flowcypy-0.5.15/FlowCyPy/coupling_mechanism/mie.py → /flowcypy-0.6.0/FlowCyPy/coupling_mechanism.py +0 -0
  79. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/directories.py +0 -0
  80. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/distribution/__init__.py +0 -0
  81. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/event_correlator.py +0 -0
  82. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/noises.py +0 -0
  83. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/peak_locator/__init__.py +0 -0
  84. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/peak_locator/base_class.py +0 -0
  85. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/peak_locator/basic.py +0 -0
  86. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/peak_locator/derivative.py +0 -0
  87. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/peak_locator/moving_average.py +0 -0
  88. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/physical_constant.py +0 -0
  89. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/populations_instances.py +0 -0
  90. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/source.py +0 -0
  91. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/units.py +0 -0
  92. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/utils.py +0 -0
  93. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy.egg-info/dependency_links.txt +0 -0
  94. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy.egg-info/requires.txt +0 -0
  95. {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy.egg-info/top_level.txt +0 -0
  96. {flowcypy-0.5.15 → flowcypy-0.6.0}/LICENSE +0 -0
  97. {flowcypy-0.5.15 → flowcypy-0.6.0}/README.rst +0 -0
  98. {flowcypy-0.5.15 → flowcypy-0.6.0}/Untitled.ipynb +0 -0
  99. {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/doc/internship.pdf +0 -0
  100. {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/concentration_comparison.py +0 -0
  101. {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/create_images.py +0 -0
  102. {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/data_analysis.py +0 -0
  103. {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/dev_beads_analysis.py +0 -0
  104. {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/dev_canto.py +0 -0
  105. {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/dev_classifier.py +0 -0
  106. {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/dev_shot_noise_check.py +0 -0
  107. {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/dev_study_on_ri.py +0 -0
  108. {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/dev_study_on_size.py +0 -0
  109. {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/mat2csv.py +0 -0
  110. {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/profiler.py +0 -0
  111. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/Makefile +0 -0
  112. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/density_plots/README.rst +0 -0
  113. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/extras/README.rst +0 -0
  114. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/noise_sources/README.rst +0 -0
  115. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/tutorials/README.rst +0 -0
  116. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/distributions/Delta.png +0 -0
  117. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/distributions/LogNormal.png +0 -0
  118. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/distributions/Normal.png +0 -0
  119. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/distributions/RosinRammler.png +0 -0
  120. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/distributions/Uniform.png +0 -0
  121. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/distributions/Weibull.png +0 -0
  122. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/example_0.png +0 -0
  123. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/example_1.png +0 -0
  124. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/example_2.png +0 -0
  125. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/example_3.png +0 -0
  126. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/flow_cytometer.png +0 -0
  127. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/logo.png +0 -0
  128. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/make.bat +0 -0
  129. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/_static/default.css +0 -0
  130. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/_static/logo.png +0 -0
  131. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/_static/thumbnail.png +0 -0
  132. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/analysis.rst +0 -0
  133. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/base.rst +0 -0
  134. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/detector.rst +0 -0
  135. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/distributions.rst +0 -0
  136. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/flow_cell.rst +0 -0
  137. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/flow_cytometer.rst +0 -0
  138. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/peak_locator.rst +0 -0
  139. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/scatterer.rst +0 -0
  140. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/source.rst +0 -0
  141. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code.rst +0 -0
  142. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/conf.py +0 -0
  143. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/examples.rst +0 -0
  144. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/index.rst +0 -0
  145. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/core_components.rst +0 -0
  146. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/getting_started.rst +0 -0
  147. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/objectives/main.rst +0 -0
  148. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/objectives/pre.rst +0 -0
  149. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/objectives/stretch.rst +0 -0
  150. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/prerequisites/index.rst +0 -0
  151. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/prerequisites/mathematics.rst +0 -0
  152. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/prerequisites/optics.rst +0 -0
  153. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/prerequisites/programming.rst +0 -0
  154. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/ressources.rst +0 -0
  155. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/tasks.rst +0 -0
  156. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal.rst +0 -0
  157. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/references.rst +0 -0
  158. {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/theory.rst +0 -0
  159. {flowcypy-0.5.15 → flowcypy-0.6.0}/meta.yaml +0 -0
  160. {flowcypy-0.5.15 → flowcypy-0.6.0}/notebook.ipynb +0 -0
  161. {flowcypy-0.5.15 → flowcypy-0.6.0}/pyproject.toml +0 -0
  162. {flowcypy-0.5.15 → flowcypy-0.6.0}/setup.cfg +0 -0
  163. {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/__init__.py +0 -0
  164. {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_extra.py +0 -0
  165. {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_peak_algorithm.py +0 -0
  166. {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_source.py +0 -0
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.5.15'
16
- __version_tuple__ = version_tuple = (0, 5, 15)
15
+ __version__ = version = '0.6.0'
16
+ __version_tuple__ = version_tuple = (0, 6, 0)
@@ -0,0 +1,207 @@
1
+ import numpy as np
2
+ from FlowCyPy import ScattererCollection, Detector
3
+ from FlowCyPy.source import BaseBeam
4
+ from PyMieSim.experiment.scatterer import Sphere as PMS_SPHERE
5
+ from PyMieSim.experiment.source import PlaneWave
6
+ from PyMieSim.experiment.detector import Photodiode as PMS_PHOTODIODE
7
+ from PyMieSim.experiment import Setup
8
+ from PyMieSim.units import Quantity, degree, watt, AU, hertz
9
+ from FlowCyPy.noises import NoiseSetting
10
+ import pandas as pd
11
+
12
+
13
+
14
+ def apply_rin_noise(source: BaseBeam, total_size: int, bandwidth: float) -> np.ndarray:
15
+ r"""
16
+ Applies Relative Intensity Noise (RIN) to the source amplitude if enabled, accounting for detection bandwidth.
17
+
18
+ Parameters
19
+ ----------
20
+ source : BaseBeam
21
+ The light source containing amplitude and RIN information.
22
+ total_size : int
23
+ The number of particles being simulated.
24
+ bandwidth : float
25
+ The detection bandwidth in Hz.
26
+
27
+ Returns
28
+ -------
29
+ np.ndarray
30
+ Array of amplitudes with RIN noise applied.
31
+
32
+ Equations
33
+ ---------
34
+ 1. Relative Intensity Noise (RIN):
35
+ RIN quantifies the fluctuations in the laser's intensity relative to its mean intensity.
36
+ RIN is typically specified as a power spectral density (PSD) in units of dB/Hz:
37
+ \[
38
+ \text{RIN (dB/Hz)} = 10 \cdot \log_{10}\left(\frac{\text{Noise Power (per Hz)}}{\text{Mean Power}}\right)
39
+ \]
40
+
41
+ 2. Conversion from dB/Hz to Linear Scale:
42
+ To compute noise power, RIN must be converted from dB to a linear scale:
43
+ \[
44
+ \text{RIN (linear)} = 10^{\text{RIN (dB/Hz)} / 10}
45
+ \]
46
+
47
+ 3. Total Noise Power:
48
+ The total noise power depends on the bandwidth (\(B\)) of the detection system:
49
+ \[
50
+ P_{\text{noise}} = \text{RIN (linear)} \cdot B
51
+ \]
52
+
53
+ 4. Standard Deviation of Amplitude Fluctuations:
54
+ The noise standard deviation for amplitude is derived from the total noise power:
55
+ \[
56
+ \sigma_{\text{amplitude}} = \sqrt{P_{\text{noise}}} \cdot \text{Amplitude}
57
+ \]
58
+ Substituting \(P_{\text{noise}}\), we get:
59
+ \[
60
+ \sigma_{\text{amplitude}} = \sqrt{\text{RIN (linear)} \cdot B} \cdot \text{Amplitude}
61
+ \]
62
+
63
+ Implementation
64
+ --------------
65
+ - The RIN value from the source is converted to linear scale using:
66
+ \[
67
+ \text{RIN (linear)} = 10^{\text{source.RIN} / 10}
68
+ \]
69
+ - The noise standard deviation is scaled by the detection bandwidth (\(B\)) in Hz:
70
+ \[
71
+ \sigma_{\text{amplitude}} = \sqrt{\text{RIN (linear)} \cdot B} \cdot \text{source.amplitude}
72
+ \]
73
+ - Gaussian noise with mean \(0\) and standard deviation \(\sigma_{\text{amplitude}}\) is applied to the source amplitude.
74
+
75
+ Notes
76
+ -----
77
+ - The bandwidth parameter (\(B\)) must be in Hz and reflects the frequency range of the detection system.
78
+ - The function assumes that RIN is specified in dB/Hz. If RIN is already in linear scale, the conversion step can be skipped.
79
+ """
80
+ amplitude_with_rin = np.ones(total_size) * source.amplitude
81
+
82
+ if NoiseSetting.include_RIN_noise and NoiseSetting.include_noises:
83
+ # Convert RIN from dB/Hz to linear scale if necessary
84
+ rin_linear = 10**(source.RIN / 10)
85
+
86
+ # Compute noise standard deviation, scaled by bandwidth
87
+ std_dev_amplitude = np.sqrt(rin_linear * bandwidth.to(hertz).magnitude) * source.amplitude
88
+
89
+ # Apply Gaussian noise to the amplitude
90
+ amplitude_with_rin += np.random.normal(
91
+ loc=0,
92
+ scale=std_dev_amplitude.to(source.amplitude.units).magnitude,
93
+ size=total_size
94
+ ) * source.amplitude.units
95
+
96
+ return amplitude_with_rin
97
+
98
+
99
+ def initialize_scatterer(scatterer_dataframe: pd.DataFrame, source: PlaneWave, medium_refractive_index: Quantity) -> PMS_SPHERE:
100
+ """
101
+ Initializes the scatterer object for the PyMieSim experiment.
102
+
103
+ Parameters
104
+ ----------
105
+ scatterer : ScattererCollection
106
+ The scatterer object containing particle data.
107
+ source : PlaneWave
108
+ The light source for the simulation.
109
+
110
+ Returns
111
+ -------
112
+ PMS_SPHERE
113
+ Initialized scatterer for the experiment.
114
+ """
115
+ size_list = scatterer_dataframe['Size'].values
116
+ ri_list = scatterer_dataframe['RefractiveIndex'].values
117
+
118
+ if len(size_list) == 0:
119
+ raise ValueError("ScattererCollection size list is empty.")
120
+
121
+ size_list = size_list.quantity.magnitude * size_list.units
122
+ ri_list = ri_list.quantity.magnitude * ri_list.units
123
+
124
+ return PMS_SPHERE(
125
+ diameter=size_list,
126
+ property=ri_list,
127
+ medium_property=np.ones(len(size_list)) * medium_refractive_index,
128
+ source=source
129
+ )
130
+
131
+
132
+ def initialize_detector(detector: Detector, total_size: int) -> PMS_PHOTODIODE:
133
+ """
134
+ Initializes the detector object for the PyMieSim experiment.
135
+
136
+ Parameters
137
+ ----------
138
+ detector : Detector
139
+ The detector object containing configuration data.
140
+ total_size : int
141
+ The number of particles being simulated.
142
+
143
+ Returns
144
+ -------
145
+ PMS_PHOTODIODE
146
+ Initialized detector for the experiment.
147
+ """
148
+ ONES = np.ones(total_size)
149
+
150
+ return PMS_PHOTODIODE(
151
+ NA=ONES * detector.numerical_aperture,
152
+ cache_NA=ONES * 0 * AU,
153
+ gamma_offset=ONES * detector.gamma_angle,
154
+ phi_offset=ONES * detector.phi_angle,
155
+ polarization_filter=ONES * np.nan * degree,
156
+ sampling=ONES * detector.sampling
157
+ )
158
+
159
+
160
+ def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer_dataframe: pd.DataFrame, medium_refractive_index: Quantity) -> np.ndarray:
161
+ """
162
+ Computes the detected signal by analyzing the scattering properties of particles.
163
+
164
+ Parameters
165
+ ----------
166
+ source : BaseBeam
167
+ The light source object containing wavelength, power, and other optical properties.
168
+ detector : Detector
169
+ The detector object containing properties such as numerical aperture and angles.
170
+ scatterer : ScattererCollection
171
+ The scatterer object containing particle size and refractive index data.
172
+ tolerance : float, optional
173
+ The tolerance for deciding if two values of size and refractive index are "close enough" to be cached.
174
+
175
+ Returns
176
+ -------
177
+ np.ndarray
178
+ Array of coupling values for each particle, based on the detected signal.
179
+ """
180
+ size_list = scatterer_dataframe['Size'].values
181
+
182
+ if len(size_list) == 0:
183
+ return np.array([]) * watt
184
+
185
+ total_size = len(size_list)
186
+ amplitude_with_rin = apply_rin_noise(source, total_size, detector.signal_digitizer.bandwidth)
187
+
188
+ pms_source = PlaneWave(
189
+ wavelength=np.ones(total_size) * source.wavelength,
190
+ polarization=np.ones(total_size) * 0 * degree,
191
+ amplitude=amplitude_with_rin
192
+ )
193
+
194
+ pms_scatterer = initialize_scatterer(scatterer_dataframe, pms_source, medium_refractive_index)
195
+ pms_detector = initialize_detector(detector, total_size)
196
+
197
+ # Configure the detector
198
+ pms_detector.mode_number = ['NC00'] * total_size
199
+ pms_detector.rotation = np.ones(total_size) * 0 * degree
200
+ pms_detector.__post_init__()
201
+
202
+ # Set up the experiment
203
+ experiment = Setup(source=pms_source, scatterer=pms_scatterer, detector=pms_detector)
204
+
205
+ # Compute coupling values
206
+ coupling_value = experiment.get_sequential('coupling').squeeze()
207
+ return np.atleast_1d(coupling_value) * watt
@@ -2,10 +2,11 @@
2
2
  import numpy as np
3
3
  from FlowCyPy import ScattererCollection, Detector
4
4
  from FlowCyPy.source import BaseBeam
5
- from FlowCyPy.units import meter
5
+ from FlowCyPy.units import meter, Quantity
6
+ import pandas as pd
6
7
 
7
8
 
8
- def compute_scattering_cross_section(scatterer: ScattererCollection, source: BaseBeam, detector: Detector) -> np.ndarray:
9
+ def compute_scattering_cross_section(scatterer_dataframe: pd.DataFrame, source: BaseBeam, detector: Detector) -> np.ndarray:
9
10
  r"""
10
11
  Computes the Rayleigh scattering cross-section for a spherical particle with angle dependency.
11
12
 
@@ -40,9 +41,8 @@ def compute_scattering_cross_section(scatterer: ScattererCollection, source: Bas
40
41
  np.ndarray
41
42
  The angle-dependent Rayleigh scattering cross-section (in square meters, m²).
42
43
  """
43
-
44
- size_list = scatterer.dataframe['Size'].pint.to(meter).values.numpy_data
45
- ri_list = scatterer.dataframe['RefractiveIndex'].values.numpy_data
44
+ size_list = scatterer_dataframe['Size'].pint.to(meter).values.numpy_data
45
+ ri_list = scatterer_dataframe['RefractiveIndex'].values.numpy_data
46
46
 
47
47
  # Extract properties
48
48
  wavelength = source.wavelength
@@ -63,7 +63,8 @@ def compute_scattering_cross_section(scatterer: ScattererCollection, source: Bas
63
63
  return cross_section.magnitude * meter**2
64
64
 
65
65
 
66
- def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer: ScattererCollection) -> float:
66
+ # def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer: ScattererCollection) -> float:
67
+ def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer_dataframe: pd.DataFrame, medium_refractive_index: Quantity) -> np.ndarray:
67
68
  r"""
68
69
  Computes the power detected by a detector from a Rayleigh scattering event.
69
70
 
@@ -98,7 +99,7 @@ def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer: Sca
98
99
  """
99
100
  scattering_cross_section = compute_scattering_cross_section(
100
101
  source=source,
101
- scatterer=scatterer,
102
+ scatterer_dataframe=scatterer_dataframe,
102
103
  detector=detector
103
104
  )
104
105
 
@@ -3,7 +3,7 @@ from FlowCyPy import ScattererCollection, Detector, ureg
3
3
  from FlowCyPy.source import BaseBeam
4
4
 
5
5
 
6
- def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer: ScattererCollection) -> np.ndarray:
6
+ def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer_dataframe: ScattererCollection, medium_refractive_index) -> np.ndarray:
7
7
  r"""
8
8
  Computes the power detected by a detector from a uniform distribution.
9
9
 
@@ -36,4 +36,4 @@ def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer: Sca
36
36
  float
37
37
  The power detected by the detector (in watts, W).
38
38
  """
39
- return np.ones(len(scatterer.dataframe)) * ureg.watt
39
+ return np.ones(len(scatterer_dataframe)) * ureg.watt
@@ -3,16 +3,15 @@
3
3
 
4
4
  import logging
5
5
  import numpy as np
6
- import matplotlib.pyplot as plt
7
6
  from typing import List, Callable, Optional
8
7
  from MPSPlots.styles import mps
9
8
  from FlowCyPy.flow_cell import FlowCell
10
9
  from FlowCyPy.detector import Detector
11
10
  import pandas as pd
12
11
  import pint_pandas
12
+ from FlowCyPy import units
13
13
  from FlowCyPy.units import Quantity, milliwatt
14
- from FlowCyPy.logger import SimulationLogger
15
- import seaborn as sns
14
+ from FlowCyPy.experiment import Experiment
16
15
 
17
16
  # Set up logging configuration
18
17
  logging.basicConfig(
@@ -20,7 +19,6 @@ logging.basicConfig(
20
19
  format='%(levelname)s - %(message)s'
21
20
  )
22
21
 
23
-
24
22
  class FlowCytometer:
25
23
  """
26
24
  A simulation class for modeling flow cytometer signals, including Forward Scatter (FSC) and Side Scatter (SSC) channels.
@@ -65,23 +63,26 @@ class FlowCytometer:
65
63
  """
66
64
  def __init__(
67
65
  self,
66
+ scatterer_collection: object,
68
67
  flow_cell: FlowCell,
69
68
  detectors: List[Detector],
70
69
  coupling_mechanism: Optional[str] = 'mie',
71
70
  background_power: Optional[Quantity] = 0 * milliwatt):
72
71
 
72
+ self.scatterer_collection = scatterer_collection
73
73
  self.flow_cell = flow_cell
74
- self.scatterer_collection = flow_cell.scatterer_collection
75
74
  self.source = flow_cell.source
76
75
  self.detectors = detectors
77
76
  self.coupling_mechanism = coupling_mechanism
78
77
  self.background_power = background_power
79
- self.plot = self.PlotInterface(self)
80
78
 
81
79
  assert len(self.detectors) == 2, 'For now, FlowCytometer can only take two detectors for the analysis.'
82
80
  assert self.detectors[0].name != self.detectors[1].name, 'Both detectors cannot have the same name'
83
81
 
84
- def run_coupling_analysis(self) -> None:
82
+ for detector in detectors:
83
+ detector.cytometer = self
84
+
85
+ def run_coupling_analysis(self, scatterer_dataframe: pd.DataFrame) -> None:
85
86
  """
86
87
  Computes and assigns the optical coupling power for each particle-detection event.
87
88
 
@@ -106,14 +107,39 @@ class FlowCytometer:
106
107
  self.coupling_power = detection_mechanism(
107
108
  source=self.source,
108
109
  detector=detector,
109
- scatterer=self.scatterer_collection
110
+ scatterer_dataframe=scatterer_dataframe,
111
+ medium_refractive_index=self.scatterer_collection.medium_refractive_index
110
112
  )
111
113
 
112
- self.scatterer_collection.dataframe["detector: " + detector.name] = pint_pandas.PintArray(self.coupling_power, dtype=self.coupling_power.units)
114
+ scatterer_dataframe["detector: " + detector.name] = pint_pandas.PintArray(self.coupling_power, dtype=self.coupling_power.units)
115
+
116
+ def _generate_pulse_parameters(self, scatterer_dataframe: pd.DataFrame) -> None:
117
+ """
118
+ Generates and assigns random Gaussian pulse parameters for each particle event.
119
+
120
+ The generated parameters include:
121
+ - Centers: The time at which each pulse occurs.
122
+ - Widths: The standard deviation (spread) of each pulse in seconds.
123
+
124
+ Effects
125
+ -------
126
+ scatterer_collection.dataframe : pandas.DataFrame
127
+ Adds a 'Widths' column with computed pulse widths for each particle.
128
+ Uses the flow speed and beam waist to calculate pulse widths.
129
+ """
130
+ columns = pd.MultiIndex.from_product(
131
+ [[p.name for p in self.detectors], ['Centers', 'Heights']]
132
+ )
133
+
134
+ self.pulse_dataframe = pd.DataFrame(columns=columns)
135
+
136
+ self.pulse_dataframe['Centers'] = scatterer_dataframe['Time']
113
137
 
114
- self._generate_pulse_parameters()
138
+ widths = self.source.waist / self.flow_cell.flow_speed * np.ones(len(scatterer_dataframe))
115
139
 
116
- def initialize_signal(self) -> None:
140
+ scatterer_dataframe['Widths'] = pint_pandas.PintArray(widths, dtype=widths.units)
141
+
142
+ def initialize_signal(self, run_time: Quantity) -> None:
117
143
  """
118
144
  Initializes the raw signal for each detector based on the source and flow cell configuration.
119
145
 
@@ -126,12 +152,19 @@ class FlowCytometer:
126
152
  based on the flow cell's runtime.
127
153
 
128
154
  """
155
+ dataframes = []
156
+
129
157
  # Initialize the detectors
130
158
  for detector in self.detectors:
131
- detector.source = self.source
132
- detector.init_raw_signal(run_time=self.flow_cell.run_time)
159
+ dataframe = detector.get_initialized_signal(run_time=run_time)
160
+
161
+ dataframes.append(dataframe)
162
+
163
+ self.dataframe = pd.concat(dataframes, keys=[d.name for d in self.detectors])
133
164
 
134
- def simulate_pulse(self) -> None:
165
+ self.dataframe.index.names = ["Detector", "Index"]
166
+
167
+ def get_continous_acquisition(self, run_time: Quantity) -> None:
135
168
  """
136
169
  Simulates the generation of optical signal pulses for each particle event.
137
170
 
@@ -150,24 +183,41 @@ class FlowCytometer:
150
183
  ValueError
151
184
  If the scatterer collection lacks required data columns ('Widths', 'Time').
152
185
  """
153
- logging.debug("Starting pulse simulation.")
186
+ if not run_time.check('second'):
187
+ raise ValueError(f"flow_speed must be in meter per second, but got {run_time.units}")
188
+
189
+ self.initialize_signal(run_time=run_time)
190
+
191
+ scatterer_dataframe = self.flow_cell.generate_event_dataframe(self.scatterer_collection.populations, run_time=run_time)
192
+
193
+ self.scatterer_collection.fill_dataframe_with_sampling(scatterer_dataframe)
154
194
 
155
- _widths = self.scatterer_collection.dataframe['Widths'].values
156
- _centers = self.scatterer_collection.dataframe['Time'].values
195
+ self.run_coupling_analysis(scatterer_dataframe)
196
+
197
+ self._generate_pulse_parameters(scatterer_dataframe)
198
+
199
+ self.scatterer_collection.dataframe = scatterer_dataframe
200
+
201
+ _widths = scatterer_dataframe['Widths'].pint.to('second').pint.quantity.magnitude
202
+ _centers = scatterer_dataframe['Time'].pint.to('second').pint.quantity.magnitude
157
203
 
158
204
  for detector in self.detectors:
159
- _coupling_power = self.scatterer_collection.dataframe["detector: " + detector.name].values
205
+ _coupling_power = scatterer_dataframe["detector: " + detector.name].values
206
+
207
+ detector_signal = self.dataframe.xs(detector.name)['Signal']
160
208
 
161
209
  # Generate noise components
162
- detector._add_thermal_noise_to_raw_signal()
210
+ detector._add_thermal_noise_to_raw_signal(signal=detector_signal)
163
211
 
164
- detector._add_dark_current_noise_to_raw_signal()
212
+ detector._add_dark_current_noise_to_raw_signal(signal=detector_signal)
165
213
 
166
214
  # Broadcast the time array to the shape of (number of signals, len(detector.time))
167
- time_grid = np.expand_dims(detector.dataframe.Time.values.numpy_data, axis=0) * _centers.units
215
+ time = self.dataframe.xs(detector.name)['Time'].pint.magnitude
168
216
 
169
- centers = np.expand_dims(_centers.numpy_data, axis=1) * _centers.units
170
- widths = np.expand_dims(_widths.numpy_data, axis=1) * _widths.units
217
+ time_grid = np.expand_dims(time, axis=0) * units.second
218
+
219
+ centers = np.expand_dims(_centers, axis=1) * units.second
220
+ widths = np.expand_dims(_widths, axis=1) * units.second
171
221
 
172
222
  # Compute the Gaussian for each height, center, and width using broadcasting
173
223
  power_gaussians = _coupling_power[:, np.newaxis] * np.exp(- (time_grid - centers) ** 2 / (2 * widths ** 2))
@@ -175,36 +225,25 @@ class FlowCytometer:
175
225
  total_power = np.sum(power_gaussians, axis=0) + self.background_power
176
226
 
177
227
  # Sum all the Gaussians and add them to the detector.raw_signal
178
- detector._add_optical_power_to_raw_signal(optical_power=total_power)
228
+ detector._add_optical_power_to_raw_signal(
229
+ signal=detector_signal,
230
+ optical_power=total_power,
231
+ wavelength=self.flow_cell.source.wavelength
232
+ )
179
233
 
180
- detector.capture_signal()
234
+ digitized_signal, is_saturated = detector.capture_signal(signal=detector_signal)
181
235
 
182
- self._log_statistics()
236
+ self.dataframe.loc[detector.name, 'DigitizedSignal'] = digitized_signal
183
237
 
184
- def _log_statistics(self) -> SimulationLogger:
185
- """
186
- Logs and displays key statistics about the simulated events.
238
+ detector.is_saturated = is_saturated
187
239
 
188
- This includes metrics such as:
189
- - Total number of events processed.
190
- - Average time between events.
191
- - First and last event times.
192
- - Minimum time intervals between events.
193
-
194
- Returns
195
- -------
196
- SimulationLogger
197
- An instance of the logger containing all recorded statistics.
198
-
199
- Effects
200
- -------
201
- Outputs formatted tables to the console or log file, depending on the logger's configuration.
202
- """
203
- logger = SimulationLogger(cytometer=self)
204
-
205
- logger.log_statistics(include_totals=True, table_format="fancy_grid")
240
+ experiment = Experiment(
241
+ run_time=run_time,
242
+ scatterer_dataframe=scatterer_dataframe,
243
+ detector_dataframe=self.dataframe
244
+ )
206
245
 
207
- return logger
246
+ return experiment
208
247
 
209
248
  def _get_detection_mechanism(self) -> Callable:
210
249
  """
@@ -242,32 +281,6 @@ class FlowCytometer:
242
281
  case _:
243
282
  raise ValueError("Invalid coupling mechanism. Choose 'rayleigh' or 'uniform'.")
244
283
 
245
- def _generate_pulse_parameters(self) -> None:
246
- """
247
- Generates and assigns random Gaussian pulse parameters for each particle event.
248
-
249
- The generated parameters include:
250
- - Centers: The time at which each pulse occurs.
251
- - Widths: The standard deviation (spread) of each pulse in seconds.
252
-
253
- Effects
254
- -------
255
- scatterer_collection.dataframe : pandas.DataFrame
256
- Adds a 'Widths' column with computed pulse widths for each particle.
257
- Uses the flow speed and beam waist to calculate pulse widths.
258
- """
259
- columns = pd.MultiIndex.from_product(
260
- [[p.name for p in self.detectors], ['Centers', 'Heights']]
261
- )
262
-
263
- self.pulse_dataframe = pd.DataFrame(columns=columns)
264
-
265
- self.pulse_dataframe['Centers'] = self.scatterer_collection.dataframe['Time']
266
-
267
- widths = self.source.waist / self.flow_cell.flow_speed * np.ones(self.scatterer_collection.n_events)
268
-
269
- self.scatterer_collection.dataframe['Widths'] = pint_pandas.PintArray(widths, dtype=widths.units)
270
-
271
284
  def add_detector(self, **kwargs) -> Detector:
272
285
  """
273
286
  Dynamically adds a new detector to the system configuration.
@@ -292,105 +305,3 @@ class FlowCytometer:
292
305
 
293
306
  return detector
294
307
 
295
- class PlotInterface:
296
- def __init__(self, cytometer):
297
- self.cytometer = cytometer
298
-
299
- def signals(self, figure_size: tuple = (10, 6), add_peak_locator: bool = False, show: bool = True) -> None:
300
- """
301
- Visualizes the raw signals for all detector channels along with the scatterer distribution.
302
-
303
- Parameters
304
- ----------
305
- figure_size : tuple, optional
306
- Dimensions of the generated plot (default: (10, 6)).
307
- add_peak_locator : bool, optional
308
- If True, adds visual markers for detected signal peaks (default: False).
309
-
310
- Effects
311
- -------
312
- Displays a multi-panel plot showing:
313
- - Raw signals for each detector channel.
314
- - Scatterer distribution along the time axis.
315
- """
316
- logging.info("Plotting the signal for the different channels.")
317
-
318
- scatterer_collection = self.cytometer.scatterer_collection
319
- detectors = self.cytometer.detectors
320
-
321
- n_detectors = len(detectors)
322
-
323
- with plt.style.context(mps):
324
- _, axes = plt.subplots(ncols=1, nrows=n_detectors + 1, figsize=figure_size, sharex=True, sharey=True, gridspec_kw={'height_ratios': [1, 1, 0.4]})
325
-
326
- time_unit, signal_unit = detectors[0].plot(ax=axes[0], show=False, add_peak_locator=add_peak_locator)
327
- detectors[1].plot(ax=axes[1], show=False, time_unit=time_unit, signal_unit=signal_unit, add_peak_locator=add_peak_locator)
328
-
329
- axes[-1].get_yaxis().set_visible(False)
330
- scatterer_collection.add_to_ax(axes[-1])
331
-
332
- # Add legends to each subplot
333
- for ax in axes:
334
- ax.legend()
335
-
336
- if show: # Display the plot
337
- plt.show()
338
-
339
- def coupling_distribution(self, log_scale: bool = False, show: bool = True, equal_limits: bool = False, save_path: str = None) -> None:
340
- """
341
- Plots the density distribution of optical coupling in the FSC and SSC channels.
342
-
343
- This method generates a joint plot showing the relationship between the signals from
344
- the forward scatter ('detector: forward') and side scatter ('detector: side') detectors.
345
- The plot is color-coded by particle population and can optionally display axes on a logarithmic scale.
346
-
347
- Parameters
348
- ----------
349
- log_scale : bool, optional
350
- If True, applies a logarithmic scale to both the x and y axes of the plot (default: False).
351
- show : bool, optional
352
- If True, displays the plot immediately. If False, the plot is created but not displayed,
353
- allowing for further customization or saving externally (default: True).
354
- equal_limits : bool, optional
355
- If True, sets the same limits for both the x and y axes based on the maximum range
356
- across both axes. If False, the limits are set automatically based on the data (default: False).
357
-
358
- """
359
- scatterer_collection = self.cytometer.scatterer_collection
360
- detector_0, detector_1 = self.cytometer.detectors
361
-
362
- with plt.style.context(mps):
363
- joint_plot = sns.jointplot(
364
- data=scatterer_collection.dataframe,
365
- x=f'detector: {detector_0.name}',
366
- y=f'detector: {detector_1.name}',
367
- hue="Population",
368
- alpha=0.8,
369
- )
370
-
371
- if log_scale:
372
- joint_plot.ax_joint.set_xscale('log')
373
- joint_plot.ax_joint.set_yscale('log')
374
-
375
- if equal_limits:
376
- # Get data limits
377
- x_data = scatterer_collection.dataframe[f'detector: {detector_0.name}']
378
- y_data = scatterer_collection.dataframe[f'detector: {detector_1.name}']
379
-
380
- x_min, x_max = x_data.min(), x_data.max()
381
- y_min, y_max = y_data.min(), y_data.max()
382
-
383
- # Find the overall min and max
384
- overall_min = min(x_min, y_min)
385
- overall_max = max(x_max, y_max)
386
-
387
- # Set equal limits
388
- joint_plot.ax_joint.set_xlim(overall_min, overall_max)
389
- joint_plot.ax_joint.set_ylim(overall_min, overall_max)
390
-
391
- if save_path:
392
- joint_plot.figure.savefig(save_path, dpi=300, bbox_inches='tight')
393
- print(f"Plot saved to {save_path}")
394
-
395
- if show: # Display the plot
396
- plt.show()