FlowCyPy 0.5.6__tar.gz → 0.5.8__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 (155) hide show
  1. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/_version.py +2 -2
  2. flowcypy-0.5.8/FlowCyPy/cytometer.py +396 -0
  3. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/event_correlator.py +0 -19
  4. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/flow_cell.py +7 -7
  5. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/noises.py +6 -1
  6. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/particle_count.py +24 -10
  7. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/population.py +5 -1
  8. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/populations_instances.py +27 -11
  9. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/scatterer_collection.py +3 -7
  10. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy.egg-info/PKG-INFO +2 -2
  11. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy.egg-info/SOURCES.txt +14 -17
  12. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy.egg-info/requires.txt +1 -1
  13. {flowcypy-0.5.6 → flowcypy-0.5.8}/PKG-INFO +2 -2
  14. flowcypy-0.5.8/developments/scripts/concentration_comparison.py +109 -0
  15. flowcypy-0.5.8/developments/scripts/data_analysis.py +43 -0
  16. flowcypy-0.5.8/developments/scripts/dev_classifier.py +149 -0
  17. flowcypy-0.5.8/developments/scripts/dev_temp.py +96 -0
  18. flowcypy-0.5.8/developments/scripts/mat2csv.py +65 -0
  19. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/density_plots/1_populations.py +8 -2
  20. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/density_plots/2_populations.py +12 -4
  21. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/density_plots/3_populations.py +13 -4
  22. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/density_plots/custom_populations.py +9 -3
  23. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/extras/flow_cytometer_signal.py +7 -2
  24. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/extras/full_workflow.py +6 -1
  25. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/extras/scatterer_distribution.py +3 -3
  26. flowcypy-0.5.8/docs/examples/tutorials/limit_of_detection.py +114 -0
  27. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/tutorials/workflow.py +10 -6
  28. flowcypy-0.5.8/docs/source/internal/objectives/main.rst +111 -0
  29. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/internal.rst +1 -0
  30. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/sg_execution_times.rst +16 -16
  31. {flowcypy-0.5.6 → flowcypy-0.5.8}/pyproject.toml +1 -1
  32. {flowcypy-0.5.6 → flowcypy-0.5.8}/tests/test_coupling_mechanism.py +2 -1
  33. {flowcypy-0.5.6 → flowcypy-0.5.8}/tests/test_flow_cytometer.py +15 -4
  34. {flowcypy-0.5.6 → flowcypy-0.5.8}/tests/test_peak_analyzer.py +8 -3
  35. {flowcypy-0.5.6 → flowcypy-0.5.8}/tests/test_population.py +1 -3
  36. {flowcypy-0.5.6 → flowcypy-0.5.8}/tests/test_scatterer_distribution.py +31 -3
  37. flowcypy-0.5.6/.condarc +0 -37
  38. flowcypy-0.5.6/.readthedocs.yml +0 -35
  39. flowcypy-0.5.6/FlowCyPy/cytometer.py +0 -198
  40. flowcypy-0.5.6/FlowCyPy/plottings.py +0 -270
  41. flowcypy-0.5.6/FlowCyPy/report.py +0 -236
  42. flowcypy-0.5.6/developments/dev_classifier.py +0 -140
  43. flowcypy-0.5.6/developments/get_started.md +0 -23
  44. flowcypy-0.5.6/developments/image.png +0 -0
  45. flowcypy-0.5.6/developments/output_file.prof +0 -0
  46. flowcypy-0.5.6/developments/test.pdf +0 -0
  47. flowcypy-0.5.6/docs/source/internal/objectives/main.rst +0 -83
  48. {flowcypy-0.5.6 → flowcypy-0.5.8}/.flake8 +0 -0
  49. {flowcypy-0.5.6 → flowcypy-0.5.8}/.github/dependabot.yml +0 -0
  50. {flowcypy-0.5.6 → flowcypy-0.5.8}/.github/workflows/deploy_PyPi.yml +0 -0
  51. {flowcypy-0.5.6 → flowcypy-0.5.8}/.github/workflows/deploy_anaconda.yml +0 -0
  52. {flowcypy-0.5.6 → flowcypy-0.5.8}/.github/workflows/deploy_coverage.yml +0 -0
  53. {flowcypy-0.5.6 → flowcypy-0.5.8}/.github/workflows/deploy_documentation.yml +0 -0
  54. {flowcypy-0.5.6 → flowcypy-0.5.8}/.gitignore +0 -0
  55. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/__init__.py +0 -0
  56. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/classifier.py +0 -0
  57. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/coupling_mechanism/__init__.py +0 -0
  58. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/coupling_mechanism/empirical.py +0 -0
  59. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/coupling_mechanism/mie.py +0 -0
  60. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/coupling_mechanism/rayleigh.py +0 -0
  61. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/coupling_mechanism/uniform.py +0 -0
  62. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/detector.py +0 -0
  63. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/directories.py +0 -0
  64. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/distribution/__init__.py +0 -0
  65. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/distribution/base_class.py +0 -0
  66. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/distribution/delta.py +0 -0
  67. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/distribution/lognormal.py +0 -0
  68. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/distribution/normal.py +0 -0
  69. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/distribution/particle_size_distribution.py +0 -0
  70. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/distribution/uniform.py +0 -0
  71. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/distribution/weibull.py +0 -0
  72. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/helper.py +0 -0
  73. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/logger.py +0 -0
  74. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/peak_locator/__init__.py +0 -0
  75. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/peak_locator/base_class.py +0 -0
  76. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/peak_locator/basic.py +0 -0
  77. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/peak_locator/derivative.py +0 -0
  78. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/peak_locator/moving_average.py +0 -0
  79. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/physical_constant.py +0 -0
  80. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/source.py +0 -0
  81. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/units.py +0 -0
  82. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy/utils.py +0 -0
  83. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy.egg-info/dependency_links.txt +0 -0
  84. {flowcypy-0.5.6 → flowcypy-0.5.8}/FlowCyPy.egg-info/top_level.txt +0 -0
  85. {flowcypy-0.5.6 → flowcypy-0.5.8}/LICENSE +0 -0
  86. {flowcypy-0.5.6 → flowcypy-0.5.8}/README.rst +0 -0
  87. {flowcypy-0.5.6 → flowcypy-0.5.8}/Untitled.ipynb +0 -0
  88. {flowcypy-0.5.6/developments → flowcypy-0.5.8/developments/doc}/internship.pdf +0 -0
  89. {flowcypy-0.5.6/developments → flowcypy-0.5.8/developments/scripts}/create_images.py +0 -0
  90. {flowcypy-0.5.6/developments → flowcypy-0.5.8/developments/scripts}/dev_beads_analysis.py +0 -0
  91. {flowcypy-0.5.6/developments → flowcypy-0.5.8/developments/scripts}/dev_canto.py +0 -0
  92. {flowcypy-0.5.6/developments → flowcypy-0.5.8/developments/scripts}/dev_shot_noise_check.py +0 -0
  93. {flowcypy-0.5.6/developments → flowcypy-0.5.8/developments/scripts}/dev_study_on_ri.py +0 -0
  94. {flowcypy-0.5.6/developments → flowcypy-0.5.8/developments/scripts}/dev_study_on_size.py +0 -0
  95. {flowcypy-0.5.6/developments → flowcypy-0.5.8/developments/scripts}/profiler.py +0 -0
  96. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/Makefile +0 -0
  97. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/density_plots/README.rst +0 -0
  98. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/extras/README.rst +0 -0
  99. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/extras/distributions.py +0 -0
  100. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/noise_sources/README.rst +0 -0
  101. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/noise_sources/dark_current.py +0 -0
  102. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/noise_sources/shot_noise.py +0 -0
  103. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/noise_sources/thermal.py +0 -0
  104. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/examples/tutorials/README.rst +0 -0
  105. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/images/distributions/Delta.png +0 -0
  106. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/images/distributions/LogNormal.png +0 -0
  107. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/images/distributions/Normal.png +0 -0
  108. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/images/distributions/RosinRammler.png +0 -0
  109. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/images/distributions/Uniform.png +0 -0
  110. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/images/distributions/Weibull.png +0 -0
  111. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/images/example_0.png +0 -0
  112. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/images/example_1.png +0 -0
  113. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/images/example_2.png +0 -0
  114. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/images/example_3.png +0 -0
  115. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/images/flow_cytometer.png +0 -0
  116. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/images/logo.png +0 -0
  117. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/make.bat +0 -0
  118. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/_static/default.css +0 -0
  119. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/_static/logo.png +0 -0
  120. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/_static/thumbnail.png +0 -0
  121. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/code/analysis.rst +0 -0
  122. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/code/base.rst +0 -0
  123. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/code/detector.rst +0 -0
  124. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/code/distributions.rst +0 -0
  125. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/code/flow_cell.rst +0 -0
  126. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/code/flow_cytometer.rst +0 -0
  127. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/code/peak_locator.rst +0 -0
  128. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/code/scatterer.rst +0 -0
  129. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/code/source.rst +0 -0
  130. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/code.rst +0 -0
  131. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/conf.py +0 -0
  132. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/examples.rst +0 -0
  133. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/index.rst +0 -0
  134. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/internal/core_components.rst +0 -0
  135. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/internal/getting_started.rst +0 -0
  136. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/internal/objectives/pre.rst +0 -0
  137. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/internal/objectives/stretch.rst +0 -0
  138. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/internal/prerequisites/index.rst +0 -0
  139. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/internal/prerequisites/mathematics.rst +0 -0
  140. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/internal/prerequisites/optics.rst +0 -0
  141. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/internal/prerequisites/programming.rst +0 -0
  142. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/internal/ressources.rst +0 -0
  143. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/internal/tasks.rst +0 -0
  144. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/references.rst +0 -0
  145. {flowcypy-0.5.6 → flowcypy-0.5.8}/docs/source/theory.rst +0 -0
  146. {flowcypy-0.5.6 → flowcypy-0.5.8}/meta.yaml +0 -0
  147. {flowcypy-0.5.6 → flowcypy-0.5.8}/notebook.ipynb +0 -0
  148. {flowcypy-0.5.6 → flowcypy-0.5.8}/setup.cfg +0 -0
  149. {flowcypy-0.5.6 → flowcypy-0.5.8}/tests/__init__.py +0 -0
  150. {flowcypy-0.5.6 → flowcypy-0.5.8}/tests/test_detector_noise.py +0 -0
  151. {flowcypy-0.5.6 → flowcypy-0.5.8}/tests/test_distribution.py +0 -0
  152. {flowcypy-0.5.6 → flowcypy-0.5.8}/tests/test_extra.py +0 -0
  153. {flowcypy-0.5.6 → flowcypy-0.5.8}/tests/test_noises.py +0 -0
  154. {flowcypy-0.5.6 → flowcypy-0.5.8}/tests/test_peak_algorithm.py +0 -0
  155. {flowcypy-0.5.6 → flowcypy-0.5.8}/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.6'
16
- __version_tuple__ = version_tuple = (0, 5, 6)
15
+ __version__ = version = '0.5.8'
16
+ __version_tuple__ = version_tuple = (0, 5, 8)
@@ -0,0 +1,396 @@
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.flow_cell import FlowCell
10
+ from FlowCyPy.detector import Detector
11
+ import pandas as pd
12
+ import pint_pandas
13
+ from FlowCyPy.units import Quantity, milliwatt
14
+ from FlowCyPy.logger import SimulationLogger
15
+ import seaborn as sns
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 simulation class for modeling flow cytometer signals, including Forward Scatter (FSC) and Side Scatter (SSC) channels.
27
+
28
+ The FlowCytometer class integrates optical and flow dynamics to simulate signal generation in a flow cytometer.
29
+ It handles particle distributions, flow cell properties, laser source configurations, and detector behavior to
30
+ replicate realistic cytometry conditions. This includes the generation of synthetic signal pulses for each
31
+ particle event and noise modeling for accurate signal representation.
32
+
33
+ Parameters
34
+ ----------
35
+ flow_cell : FlowCell
36
+ The flow cell object representing the fluidic and optical environment through which particles travel.
37
+ detectors : List[Detector]
38
+ A list of `Detector` objects representing the detectors used to measure optical signals (e.g., FSC and SSC). Exactly two detectors must be provided.
39
+ coupling_mechanism : str, optional
40
+ The scattering mechanism used to couple the signal from the particles to the detectors.
41
+ Supported mechanisms include: 'mie' (default): Mie scattering, 'rayleigh': Rayleigh scattering, 'uniform': Uniform signal coupling, 'empirical': Empirical data-driven coupling
42
+ background_power : Quantity, optional
43
+ The background optical power added to the detector signal. Defaults to 0 milliwatts.
44
+
45
+ Attributes
46
+ ----------
47
+ flow_cell : FlowCell
48
+ The flow cell instance representing the system environment.
49
+ scatterer_collection : ScattererCollection
50
+ A collection of particles or scatterers passing through the flow cytometer.
51
+ source : GaussianBeam
52
+ The laser beam source providing illumination to the flow cytometer.
53
+ detectors : List[Detector]
54
+ The detectors used to collect and process signals from the scatterers.
55
+ coupling_mechanism : str
56
+ The selected mechanism for signal coupling.
57
+ background_power : Quantity
58
+ The optical background power added to the detector signals.
59
+
60
+ Raises
61
+ ------
62
+ AssertionError
63
+ If the number of detectors provided is not exactly two, or if both detectors share the same name.
64
+
65
+ """
66
+ def __init__(
67
+ self,
68
+ flow_cell: FlowCell,
69
+ detectors: List[Detector],
70
+ coupling_mechanism: Optional[str] = 'mie',
71
+ background_power: Optional[Quantity] = 0 * milliwatt):
72
+
73
+ self.flow_cell = flow_cell
74
+ self.scatterer_collection = flow_cell.scatterer_collection
75
+ self.source = flow_cell.source
76
+ self.detectors = detectors
77
+ self.coupling_mechanism = coupling_mechanism
78
+ self.background_power = background_power
79
+ self.plot = self.PlotInterface(self)
80
+
81
+ assert len(self.detectors) == 2, 'For now, FlowCytometer can only take two detectors for the analysis.'
82
+ assert self.detectors[0].name != self.detectors[1].name, 'Both detectors cannot have the same name'
83
+
84
+ def run_coupling_analysis(self) -> None:
85
+ """
86
+ Computes and assigns the optical coupling power for each particle-detection event.
87
+
88
+ This method evaluates the coupling between the scatterers in the flow cell and the detectors
89
+ using the specified detection mechanism. The computed coupling power is stored in the
90
+ `scatterer_collection` dataframe under detector-specific columns.
91
+
92
+ Updates
93
+ -------
94
+ scatterer_collection.dataframe : pandas.DataFrame
95
+ Adds columns for each detector, labeled as "detector: <detector_name>", containing the computed
96
+ coupling power for all particle events.
97
+
98
+ Raises
99
+ ------
100
+ ValueError
101
+ If an invalid coupling mechanism is specified during initialization.
102
+ """
103
+ detection_mechanism = self._get_detection_mechanism()
104
+
105
+ for detector in self.detectors:
106
+ self.coupling_power = detection_mechanism(
107
+ source=self.source,
108
+ detector=detector,
109
+ scatterer=self.scatterer_collection
110
+ )
111
+
112
+ self.scatterer_collection.dataframe["detector: " + detector.name] = pint_pandas.PintArray(self.coupling_power, dtype=self.coupling_power.units)
113
+
114
+ self._generate_pulse_parameters()
115
+
116
+ def initialize_signal(self) -> None:
117
+ """
118
+ Initializes the raw signal for each detector based on the source and flow cell configuration.
119
+
120
+ This method prepares the detectors for signal capture by associating each detector with the
121
+ light source and generating a time-dependent raw signal placeholder.
122
+
123
+ Effects
124
+ -------
125
+ Each detector's `raw_signal` attribute is initialized with time-dependent values
126
+ based on the flow cell's runtime.
127
+
128
+ """
129
+ # Initialize the detectors
130
+ for detector in self.detectors:
131
+ detector.source = self.source
132
+ detector.init_raw_signal(run_time=self.flow_cell.run_time)
133
+
134
+ def simulate_pulse(self) -> None:
135
+ """
136
+ Simulates the generation of optical signal pulses for each particle event.
137
+
138
+ This method calculates Gaussian signal pulses based on particle positions, coupling power, and
139
+ widths. It adds the generated pulses, background power, and noise components (thermal and dark current)
140
+ to each detector's raw signal.
141
+
142
+ Notes
143
+ -----
144
+ - Adds Gaussian pulses to each detector's `raw_signal`.
145
+ - Includes noise and background power in the simulated signals.
146
+ - Updates detector dataframes with captured signal information.
147
+
148
+ Raises
149
+ ------
150
+ ValueError
151
+ If the scatterer collection lacks required data columns ('Widths', 'Time').
152
+ """
153
+ logging.debug("Starting pulse simulation.")
154
+
155
+ _widths = self.scatterer_collection.dataframe['Widths'].values
156
+ _centers = self.scatterer_collection.dataframe['Time'].values
157
+
158
+ for detector in self.detectors:
159
+ _coupling_power = self.scatterer_collection.dataframe["detector: " + detector.name].values
160
+
161
+ # Generate noise components
162
+ detector._add_thermal_noise_to_raw_signal()
163
+
164
+ detector._add_dark_current_noise_to_raw_signal()
165
+
166
+ # 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
168
+
169
+ centers = np.expand_dims(_centers.numpy_data, axis=1) * _centers.units
170
+ widths = np.expand_dims(_widths.numpy_data, axis=1) * _widths.units
171
+
172
+ # Compute the Gaussian for each height, center, and width using broadcasting
173
+ power_gaussians = _coupling_power[:, np.newaxis] * np.exp(- (time_grid - centers) ** 2 / (2 * widths ** 2))
174
+
175
+ total_power = np.sum(power_gaussians, axis=0) + self.background_power
176
+
177
+ # Sum all the Gaussians and add them to the detector.raw_signal
178
+ detector._add_optical_power_to_raw_signal(optical_power=total_power)
179
+
180
+ detector.capture_signal()
181
+
182
+ self._log_statistics()
183
+
184
+ def _log_statistics(self) -> SimulationLogger:
185
+ """
186
+ Logs and displays key statistics about the simulated events.
187
+
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")
206
+
207
+ return logger
208
+
209
+ def _get_detection_mechanism(self) -> Callable:
210
+ """
211
+ Retrieves the detection mechanism function for signal coupling based on the selected method.
212
+
213
+ Supported Coupling Mechanisms
214
+ -----------------------------
215
+ - 'mie': Mie scattering.
216
+ - 'rayleigh': Rayleigh scattering.
217
+ - 'uniform': Uniform scattering.
218
+ - 'empirical': Empirical (data-driven) scattering.
219
+
220
+ Returns
221
+ -------
222
+ Callable
223
+ A function that computes the detected signal for scatterer sizes and particle distributions.
224
+
225
+ Raises
226
+ ------
227
+ ValueError
228
+ If an unsupported coupling mechanism is specified.
229
+ """
230
+ from FlowCyPy import coupling_mechanism
231
+
232
+ # Determine which coupling mechanism to use and compute the corresponding factors
233
+ match self.coupling_mechanism.lower():
234
+ case 'rayleigh':
235
+ return coupling_mechanism.rayleigh.compute_detected_signal
236
+ case 'uniform':
237
+ return coupling_mechanism.uniform.compute_detected_signal
238
+ case 'mie':
239
+ return coupling_mechanism.mie.compute_detected_signal
240
+ case 'empirical':
241
+ return coupling_mechanism.empirical.compute_detected_signal
242
+ case _:
243
+ raise ValueError("Invalid coupling mechanism. Choose 'rayleigh' or 'uniform'.")
244
+
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
+ def add_detector(self, **kwargs) -> Detector:
272
+ """
273
+ Dynamically adds a new detector to the system configuration.
274
+
275
+ Parameters
276
+ ----------
277
+ **kwargs : dict
278
+ Keyword arguments passed to the `Detector` constructor.
279
+
280
+ Returns
281
+ -------
282
+ Detector
283
+ The newly added detector instance.
284
+
285
+ Effects
286
+ -------
287
+ - Appends the created detector to the `detectors` list.
288
+ """
289
+ detector = Detector(**kwargs)
290
+
291
+ self.detectors.append(detector)
292
+
293
+ return detector
294
+
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()
@@ -8,7 +8,6 @@ from FlowCyPy.units import second
8
8
  import warnings
9
9
  from FlowCyPy.cytometer import FlowCytometer
10
10
  from FlowCyPy.logger import EventCorrelatorLogger
11
- from FlowCyPy.report import Report
12
11
 
13
12
 
14
13
  class EventCorrelator:
@@ -224,21 +223,3 @@ class EventCorrelator:
224
223
 
225
224
  if show:
226
225
  plt.show()
227
-
228
- def generate_report(self, filename: str) -> None:
229
- """
230
- Generates a detailed report summarizing the analysis, including peak features
231
- and detected events.
232
-
233
- Parameters
234
- ----------
235
- filename : str
236
- The filename where the report will be saved.
237
- """
238
- report = Report(
239
- flow_cell=self.cytometer.scatterer.flow_cell,
240
- scatterer=self.cytometer.scatterer,
241
- analyzer=self
242
- )
243
-
244
- report.generate_report()
@@ -130,14 +130,14 @@ class FlowCell(object):
130
130
  ['Flow Area', f"{self.flow_area:.2f~#P}"],
131
131
  ['Total Time', f"{self.run_time:.2f~#P}"]
132
132
  ]
133
-
133
+
134
134
  # def initialize(self, scatterer: Population | ScattererCollection) -> None:
135
135
  # if isinstance(scatterer, Population):
136
136
  # return self._initialize_population(scatterer)
137
137
 
138
138
  # elif isinstance(scatterer, ScattererCollection):
139
139
  # return self._initialize_scatterer_collection(scatterer)
140
-
140
+
141
141
  def _initialize_population(self, population: Population) -> None:
142
142
  population.dataframe = pandas.DataFrame()
143
143
 
@@ -164,13 +164,13 @@ class FlowCell(object):
164
164
  scatterer : Scatterer
165
165
  An instance of the Scatterer class that describes the scatterer collection being used.
166
166
 
167
- """
167
+ """
168
168
  self.scatterer_collection = scatterer_collection
169
169
 
170
170
  for population in self.scatterer_collection.populations:
171
171
  self._initialize_population(population)
172
172
  population.dataframe.Size = population.dataframe.Size.pint.to(size_units)
173
-
173
+
174
174
  if len(self.scatterer_collection.populations) != 0:
175
175
  self.scatterer_collection.dataframe = pandas.concat(
176
176
  [population.dataframe for population in self.scatterer_collection.populations],
@@ -195,7 +195,7 @@ class FlowCell(object):
195
195
  index=multi_index
196
196
  )
197
197
 
198
- self.scatterer_collection.n_events = len(self.scatterer_collection.dataframe)
198
+ self.scatterer_collection.n_events = len(self.scatterer_collection.dataframe)
199
199
 
200
200
  def distribute_time_linearly(self, sequential_population: bool = False) -> None:
201
201
  """
@@ -210,14 +210,14 @@ class FlowCell(object):
210
210
 
211
211
  """
212
212
  # Generate linearly spaced time values across the flow cell runtime
213
- linear_spacing = numpy.linspace(0, self.run_time, self.n_events)
213
+ linear_spacing = numpy.linspace(0, self.run_time, self.scatterer_collection.n_events)
214
214
 
215
215
  # Optionally randomize the linear spacing
216
216
  if not sequential_population:
217
217
  numpy.random.shuffle(linear_spacing)
218
218
 
219
219
  # Assign the linearly spaced or randomized times to the scatterer DataFrame
220
- self.scatterer_collectionscatterer.dataframe.Time = PintArray(linear_spacing, dtype=self.scatterer_collection.dataframe.Time.pint.units)
220
+ self.scatterer_collection.dataframe.Time = PintArray(linear_spacing, dtype=self.scatterer_collection.dataframe.Time.pint.units)
221
221
 
222
222
  def _generate_longitudinal_positions(self, population: Population) -> None:
223
223
  r"""
@@ -1,6 +1,11 @@
1
+ class RestrictiveMeta(type):
2
+ def __setattr__(cls, name, value):
3
+ if not hasattr(cls, name):
4
+ raise AttributeError(f"Cannot set unknown class-level attribute '{name}' in {cls.__name__}.")
5
+ super().__setattr__(name, value)
1
6
 
2
7
 
3
- class NoiseSetting:
8
+ class NoiseSetting(metaclass=RestrictiveMeta):
4
9
  _instance = None
5
10
 
6
11
  def __new__(cls, *args, **kwargs):
@@ -27,14 +27,17 @@ class ParticleCount:
27
27
  ValueError
28
28
  If the input value does not have the expected dimensionality.
29
29
  """
30
+ if isinstance(value, ParticleCount):
31
+ self = value
32
+ return
33
+
30
34
  if value.check(particle):
31
35
  # Fixed number of particles
32
36
  self.num_particles = value.to(particle)
33
- self.concentration = None
37
+
34
38
  elif value.check(particle / liter):
35
39
  # Concentration of particles
36
40
  self.concentration = value.to(particle / liter)
37
- self.num_particles = None
38
41
  else:
39
42
  raise ValueError(
40
43
  "Value must have dimensions of either 'particles' or 'particles per unit volume'."
@@ -61,10 +64,10 @@ class ParticleCount:
61
64
  """
62
65
  flow_volume = flow_area * flow_speed * run_time
63
66
 
64
- if self.num_particles is not None:
65
- return self.num_particles
66
- elif self.concentration is not None:
67
+ if hasattr(self, 'concentration'):
67
68
  return (self.concentration * flow_volume).to(particle)
69
+ elif hasattr(self, 'num_particles'):
70
+ return self.num_particles
68
71
  else:
69
72
  raise ValueError("Either a number of particles or a concentration must be defined.")
70
73
 
@@ -87,7 +90,7 @@ class ParticleCount:
87
90
  Quantity
88
91
  The particle flux in particles per second (particle/second).
89
92
  """
90
- if self.concentration is None:
93
+ if hasattr(self, 'num_particles'):
91
94
  return self.num_particles / run_time
92
95
 
93
96
  flow_volume_per_second = (flow_speed * flow_area).to(liter / second)
@@ -95,8 +98,19 @@ class ParticleCount:
95
98
  return particle_flux
96
99
 
97
100
  def __repr__(self):
98
- if self.num_particles is not None:
99
- return f"{self.num_particles}"
100
- elif self.concentration is not None:
101
+ if hasattr(self, 'concentration'):
101
102
  return f"{self.concentration}"
102
- return "Undefined"
103
+ else:
104
+ return f"{self.num_particles}"
105
+
106
+ def __truediv__(self, factor: float):
107
+ if hasattr(self, 'concentration'):
108
+ self.concentration /= factor
109
+ else:
110
+ self.num_particles /= factor
111
+
112
+ def __mul__(self, factor: float):
113
+ if hasattr(self, 'concentration'):
114
+ self.concentration *= factor
115
+ else:
116
+ self.num_particles *= factor
@@ -38,13 +38,14 @@ class Population(PropertiesReport):
38
38
  name: str
39
39
  refractive_index: Union[distribution.Base, Quantity]
40
40
  size: Union[distribution.Base, Quantity]
41
- particle_count: ParticleCount = field(init=False)
41
+ particle_count: ParticleCount | Quantity
42
42
 
43
43
  def __post_init__(self):
44
44
  """
45
45
  Automatically converts all Quantity attributes to their base SI units (i.e., without any prefixes).
46
46
  This strips units like millimeter to meter, kilogram to gram, etc.
47
47
  """
48
+ self.particle_count = ParticleCount(self.particle_count)
48
49
  # Convert all Quantity attributes to base SI units (without any prefixes)
49
50
  for attr_name, attr_value in vars(self).items():
50
51
  if isinstance(attr_value, Quantity):
@@ -131,5 +132,8 @@ class Population(PropertiesReport):
131
132
  return value
132
133
 
133
134
  raise TypeError(f"suze must be of type Quantity or distribution.Base, but got {type(value)}")
135
+
136
+ def dilute(self, factor: float) -> None:
137
+ self.particle_count /= factor
134
138
 
135
139
  from FlowCyPy.populations_instances import * # noqa F403
@@ -1,7 +1,28 @@
1
- from FlowCyPy.units import Quantity, nanometer, RIU, micrometer
1
+ from FlowCyPy.units import Quantity, nanometer, RIU, micrometer, particle
2
2
  from FlowCyPy.population import Population
3
3
  from FlowCyPy import distribution
4
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
5
26
  _populations = (
6
27
  ('Exosome', 70 * nanometer, 2.0, 1.39 * RIU, 0.02 * RIU),
7
28
  ('MicroVesicle', 400 * nanometer, 1.5, 1.39 * RIU, 0.02 * RIU),
@@ -13,7 +34,7 @@ _populations = (
13
34
  ('CellularDebris', 3 * micrometer, 1.0, 1.40 * RIU, 0.03 * RIU),
14
35
  )
15
36
 
16
-
37
+ # Dynamically create population classes
17
38
  for (name, size, size_spread, ri, ri_spread) in _populations:
18
39
  size_distribution = distribution.RosinRammler(
19
40
  characteristic_size=size,
@@ -25,19 +46,14 @@ for (name, size, size_spread, ri, ri_spread) in _populations:
25
46
  std_dev=ri_spread
26
47
  )
27
48
 
28
- population = Population(
29
- name=name,
30
- size=size_distribution,
31
- refractive_index=ri_distribution,
32
- )
33
-
34
- locals()[name] = population
49
+ # Create a class dynamically for each population
50
+ cls = type(name, (CallablePopulation,), {})
51
+ globals()[name] = cls(name, size_distribution, ri_distribution)
35
52
 
36
53
 
54
+ # Helper function for microbeads
37
55
  def get_microbeads(size: Quantity, refractive_index: Quantity, name: str) -> Population:
38
-
39
56
  size_distribution = distribution.Delta(position=size)
40
-
41
57
  ri_distribution = distribution.Delta(position=refractive_index)
42
58
 
43
59
  microbeads = Population(