FlowCyPy 0.7.1__tar.gz → 0.7.3__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 (169) hide show
  1. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/_version.py +2 -2
  2. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/acquisition.py +74 -26
  3. flowcypy-0.7.3/FlowCyPy/classifier.py +182 -0
  4. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/cytometer.py +63 -24
  5. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/detector.py +7 -64
  6. flowcypy-0.7.3/FlowCyPy/flow_cell.py +137 -0
  7. flowcypy-0.7.3/FlowCyPy/helper.py +89 -0
  8. flowcypy-0.7.3/FlowCyPy/noises.py +87 -0
  9. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/particle_count.py +3 -2
  10. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/population.py +3 -4
  11. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/scatterer_collection.py +7 -7
  12. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/signal_digitizer.py +1 -3
  13. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/source.py +4 -7
  14. flowcypy-0.7.3/FlowCyPy/utils.py +74 -0
  15. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy.egg-info/PKG-INFO +2 -2
  16. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy.egg-info/SOURCES.txt +16 -4
  17. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy.egg-info/requires.txt +1 -1
  18. {flowcypy-0.7.1 → flowcypy-0.7.3}/PKG-INFO +2 -2
  19. flowcypy-0.7.3/developments/Deep_peak_square.ipynb +1049 -0
  20. flowcypy-0.7.3/developments/Physics-informed_AI.ipynb +876 -0
  21. flowcypy-0.7.3/developments/ROI_analysis-Copy1.ipynb +639 -0
  22. flowcypy-0.7.3/developments/ROI_analysis.ipynb +778 -0
  23. flowcypy-0.7.3/developments/Untitled.ipynb +227 -0
  24. flowcypy-0.7.3/developments/Untitled1.ipynb +668 -0
  25. flowcypy-0.7.3/developments/Untitled2.ipynb +313 -0
  26. flowcypy-0.7.3/developments/ai_dev2.ipynb +1745 -0
  27. flowcypy-0.7.3/developments/best_model.h5 +0 -0
  28. flowcypy-0.7.3/developments/best_model.keras +0 -0
  29. flowcypy-0.7.3/developments/concentration_validation.py +100 -0
  30. flowcypy-0.7.3/developments/grad_cam_output.png +0 -0
  31. flowcypy-0.7.3/developments/model.png +0 -0
  32. flowcypy-0.7.3/developments/model_example.png +0 -0
  33. flowcypy-0.7.3/developments/scripts/AI_peak_detection.py +85 -0
  34. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/temp.py +18 -30
  35. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/examples/extras/flow_cytometer_signal.py +2 -2
  36. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/examples/extras/scatterer_distribution.py +3 -2
  37. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/examples/extras/signal_acquisition.py +1 -1
  38. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/examples/tutorials/limit_of_detection.py +1 -1
  39. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/examples/tutorials/workflow.py +2 -4
  40. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/sg_execution_times.rst +15 -15
  41. {flowcypy-0.7.1 → flowcypy-0.7.3}/pyproject.toml +1 -1
  42. flowcypy-0.7.3/tests/test_classifiers.py +83 -0
  43. {flowcypy-0.7.1 → flowcypy-0.7.3}/tests/test_coupling_mechanism.py +1 -1
  44. {flowcypy-0.7.1 → flowcypy-0.7.3}/tests/test_flow_cytometer.py +26 -8
  45. {flowcypy-0.7.1 → flowcypy-0.7.3}/tests/test_peak_analyzer.py +1 -1
  46. {flowcypy-0.7.1 → flowcypy-0.7.3}/tests/test_population.py +2 -2
  47. {flowcypy-0.7.1 → flowcypy-0.7.3}/tests/test_scatterer_distribution.py +1 -1
  48. flowcypy-0.7.1/FlowCyPy/classifier.py +0 -210
  49. flowcypy-0.7.1/FlowCyPy/coupling_mechanism.py +0 -205
  50. flowcypy-0.7.1/FlowCyPy/flow_cell.py +0 -197
  51. flowcypy-0.7.1/FlowCyPy/helper.py +0 -166
  52. flowcypy-0.7.1/FlowCyPy/logger.py +0 -136
  53. flowcypy-0.7.1/FlowCyPy/noises.py +0 -34
  54. flowcypy-0.7.1/FlowCyPy/plottings.py +0 -269
  55. flowcypy-0.7.1/FlowCyPy/utils.py +0 -191
  56. flowcypy-0.7.1/tests/test_extra.py +0 -55
  57. {flowcypy-0.7.1 → flowcypy-0.7.3}/.flake8 +0 -0
  58. {flowcypy-0.7.1 → flowcypy-0.7.3}/.github/dependabot.yml +0 -0
  59. {flowcypy-0.7.1 → flowcypy-0.7.3}/.github/workflows/deploy_PyPi.yml +0 -0
  60. {flowcypy-0.7.1 → flowcypy-0.7.3}/.github/workflows/deploy_anaconda.yml +0 -0
  61. {flowcypy-0.7.1 → flowcypy-0.7.3}/.github/workflows/deploy_coverage.yml +0 -0
  62. {flowcypy-0.7.1 → flowcypy-0.7.3}/.github/workflows/deploy_documentation.yml +0 -0
  63. {flowcypy-0.7.1 → flowcypy-0.7.3}/.gitignore +0 -0
  64. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/__init__.py +0 -0
  65. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/coupling_mechanism/__init__.py +0 -0
  66. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/coupling_mechanism/empirical.py +0 -0
  67. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/coupling_mechanism/mie.py +0 -0
  68. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/coupling_mechanism/rayleigh.py +0 -0
  69. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/coupling_mechanism/uniform.py +0 -0
  70. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/directories.py +0 -0
  71. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/distribution/__init__.py +0 -0
  72. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/distribution/base_class.py +0 -0
  73. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/distribution/delta.py +0 -0
  74. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/distribution/lognormal.py +0 -0
  75. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/distribution/normal.py +0 -0
  76. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/distribution/particle_size_distribution.py +0 -0
  77. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/distribution/uniform.py +0 -0
  78. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/distribution/weibull.py +0 -0
  79. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/peak_locator/__init__.py +0 -0
  80. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/peak_locator/base_class.py +0 -0
  81. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/peak_locator/basic.py +0 -0
  82. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/peak_locator/derivative.py +0 -0
  83. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/peak_locator/moving_average.py +0 -0
  84. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/physical_constant.py +0 -0
  85. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/populations_instances.py +0 -0
  86. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy/units.py +0 -0
  87. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy.egg-info/dependency_links.txt +0 -0
  88. {flowcypy-0.7.1 → flowcypy-0.7.3}/FlowCyPy.egg-info/top_level.txt +0 -0
  89. {flowcypy-0.7.1 → flowcypy-0.7.3}/LICENSE +0 -0
  90. {flowcypy-0.7.1 → flowcypy-0.7.3}/README.rst +0 -0
  91. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/doc/canto_spec.md +0 -0
  92. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/doc/internship.pdf +0 -0
  93. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/get_started.md +0 -0
  94. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/image.png +0 -0
  95. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/output_file.prof +0 -0
  96. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/concentration_comparison.py +0 -0
  97. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/create_images.py +0 -0
  98. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/data_analysis.py +0 -0
  99. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/dev_beads_analysis.py +0 -0
  100. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/dev_canto.py +0 -0
  101. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/dev_classifier.py +0 -0
  102. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/dev_shot_noise_check.py +0 -0
  103. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/dev_stats_0.py +0 -0
  104. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/dev_stats_1.py +0 -0
  105. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/dev_stats_2.py +0 -0
  106. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/dev_study_on_ri.py +0 -0
  107. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/dev_study_on_size.py +0 -0
  108. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/mat2csv.py +0 -0
  109. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/scripts/profiler.py +0 -0
  110. {flowcypy-0.7.1 → flowcypy-0.7.3}/developments/test.pdf +0 -0
  111. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/Makefile +0 -0
  112. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/examples/extras/README.rst +0 -0
  113. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/examples/extras/distributions.py +0 -0
  114. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/examples/noise_sources/README.rst +0 -0
  115. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/examples/noise_sources/dark_current.py +0 -0
  116. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/examples/noise_sources/shot_noise.py +0 -0
  117. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/examples/noise_sources/thermal.py +0 -0
  118. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/examples/tutorials/README.rst +0 -0
  119. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/images/distributions/Delta.png +0 -0
  120. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/images/distributions/LogNormal.png +0 -0
  121. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/images/distributions/Normal.png +0 -0
  122. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/images/distributions/RosinRammler.png +0 -0
  123. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/images/distributions/Uniform.png +0 -0
  124. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/images/distributions/Weibull.png +0 -0
  125. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/images/example_0.png +0 -0
  126. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/images/example_1.png +0 -0
  127. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/images/example_2.png +0 -0
  128. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/images/example_3.png +0 -0
  129. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/images/flow_cytometer.png +0 -0
  130. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/images/logo.png +0 -0
  131. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/make.bat +0 -0
  132. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/_static/default.css +0 -0
  133. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/_static/logo.png +0 -0
  134. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/_static/thumbnail.png +0 -0
  135. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/code/base.rst +0 -0
  136. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/code/detector.rst +0 -0
  137. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/code/distributions.rst +0 -0
  138. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/code/flow_cell.rst +0 -0
  139. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/code/flow_cytometer.rst +0 -0
  140. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/code/peak_locator.rst +0 -0
  141. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/code/scatterer.rst +0 -0
  142. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/code/source.rst +0 -0
  143. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/code.rst +0 -0
  144. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/conf.py +0 -0
  145. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/examples.rst +0 -0
  146. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/index.rst +0 -0
  147. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/internal/core_components.rst +0 -0
  148. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/internal/getting_started.rst +0 -0
  149. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/internal/objectives/main.rst +0 -0
  150. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/internal/objectives/pre.rst +0 -0
  151. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/internal/objectives/stretch.rst +0 -0
  152. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/internal/prerequisites/index.rst +0 -0
  153. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/internal/prerequisites/mathematics.rst +0 -0
  154. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/internal/prerequisites/optics.rst +0 -0
  155. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/internal/prerequisites/programming.rst +0 -0
  156. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/internal/ressources.rst +0 -0
  157. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/internal/tasks.rst +0 -0
  158. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/internal.rst +0 -0
  159. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/references.rst +0 -0
  160. {flowcypy-0.7.1 → flowcypy-0.7.3}/docs/source/theory.rst +0 -0
  161. {flowcypy-0.7.1 → flowcypy-0.7.3}/meta.yaml +0 -0
  162. {flowcypy-0.7.1 → flowcypy-0.7.3}/notebook.ipynb +0 -0
  163. {flowcypy-0.7.1 → flowcypy-0.7.3}/setup.cfg +0 -0
  164. {flowcypy-0.7.1 → flowcypy-0.7.3}/tests/__init__.py +0 -0
  165. {flowcypy-0.7.1 → flowcypy-0.7.3}/tests/test_detector_noise.py +0 -0
  166. {flowcypy-0.7.1 → flowcypy-0.7.3}/tests/test_distribution.py +0 -0
  167. {flowcypy-0.7.1 → flowcypy-0.7.3}/tests/test_noises.py +0 -0
  168. {flowcypy-0.7.1 → flowcypy-0.7.3}/tests/test_peak_algorithm.py +0 -0
  169. {flowcypy-0.7.1 → flowcypy-0.7.3}/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.7.1'
16
- __version_tuple__ = version_tuple = (0, 7, 1)
15
+ __version__ = version = '0.7.3'
16
+ __version_tuple__ = version_tuple = (0, 7, 3)
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import warnings
2
3
  from typing import Optional, Union, List
3
4
  from MPSPlots.styles import mps
4
5
  import pandas as pd
@@ -9,8 +10,9 @@ from scipy.signal import find_peaks
9
10
  import matplotlib.pyplot as plt
10
11
  import seaborn as sns
11
12
  from tabulate import tabulate
12
- import warnings
13
+
13
14
  from FlowCyPy import helper
15
+ from FlowCyPy.classifier import BaseClassifier
14
16
 
15
17
  class DataAccessor:
16
18
  def __init__(self, outer):
@@ -94,13 +96,13 @@ class Acquisition:
94
96
  results = results.reset_index(drop=True)
95
97
 
96
98
  # Check for multiple peaks and issue a warning
97
- peak_counts = results.groupby(['Detector', 'SegmentID']).size()
98
- multiple_peak_segments = peak_counts[peak_counts > 1]
99
- if not multiple_peak_segments.empty:
100
- warnings.warn(
101
- f"Multiple peaks detected in the following segments: {multiple_peak_segments.index.tolist()}",
102
- UserWarning
103
- )
99
+ # peak_counts = results.groupby(['Detector', 'SegmentID']).size()
100
+ # multiple_peak_segments = peak_counts[peak_counts > 1]
101
+ # if not multiple_peak_segments.empty:
102
+ # warnings.warn(
103
+ # f"Multiple peaks detected in the following segments: {multiple_peak_segments.index.tolist()}",
104
+ # UserWarning
105
+ # )
104
106
 
105
107
  _temp = results.reset_index()[['Detector', 'SegmentID', 'Height']].pint.dequantify().droplevel('unit', axis=1)
106
108
 
@@ -172,22 +174,47 @@ class Acquisition:
172
174
  post_buffer: int = 64,
173
175
  max_triggers: int = None) -> None:
174
176
  """
175
- Executes the triggered acquisition analysis.
177
+ Execute triggered acquisition analysis for signal data.
178
+
179
+ This method identifies segments of signal data based on a triggering threshold
180
+ and specified detector. It extracts segments of interest from the signal,
181
+ including a pre-trigger buffer and post-trigger buffer, and stores the results
182
+ in `self.data.triggered`.
176
183
 
177
184
  Parameters
178
185
  ----------
179
186
  threshold : units.Quantity
180
- Trigger threshold value.
181
- trigger_detector_name : str, optional
182
- Detector used for triggering, by default None.
183
- custom_trigger : np.ndarray, optional
184
- Custom trigger array, by default None.
187
+ The threshold value for triggering. Only signal values exceeding this threshold
188
+ will be considered as trigger events.
189
+ trigger_detector_name : str
190
+ The name of the detector used for triggering. This determines which detector's
191
+ signal is analyzed for trigger events.
185
192
  pre_buffer : int, optional
186
- Points before trigger, by default 64.
193
+ The number of points to include before the trigger point in each segment.
194
+ Default is 64.
187
195
  post_buffer : int, optional
188
- Points after trigger, by default 64.
196
+ The number of points to include after the trigger point in each segment.
197
+ Default is 64.
189
198
  max_triggers : int, optional
190
- Maximum number of triggers to process, by default None.
199
+ The maximum number of triggers to process. If None, all triggers will be processed.
200
+ Default is None.
201
+
202
+ Raises
203
+ ------
204
+ ValueError
205
+ If the specified `trigger_detector_name` is not found in the dataset.
206
+
207
+ Warnings
208
+ --------
209
+ UserWarning
210
+ If no triggers are detected for the specified threshold, the method raises a warning
211
+ indicating that no signals met the criteria.
212
+
213
+ Notes
214
+ -----
215
+ - Triggered segments are stored in `self.data.triggered` as a pandas DataFrame with a hierarchical index on `['Detector', 'SegmentID']`.
216
+ - This method modifies `self.data.triggered` in place.
217
+ - The peak detection function `self.detect_peaks` is automatically called at the end of this method to analyze triggered segments.
191
218
  """
192
219
  self.threshold = threshold
193
220
  self.trigger_detector_name = trigger_detector_name
@@ -226,7 +253,28 @@ class Acquisition:
226
253
 
227
254
  self.detect_peaks()
228
255
 
229
- def classify_dataset(self, classifier: object, features: List[str], detectors: list[str]) -> None:
256
+ def classify_dataset(self, classifier: BaseClassifier, features: List[str], detectors: list[str]) -> None:
257
+ """
258
+ Classify the dataset using the specified classifier and features.
259
+
260
+ This method applies a classification algorithm to the dataset by first unstacking
261
+ the "Detector" level of the DataFrame's index. It then uses the provided classifier
262
+ object to classify the dataset based on the specified features and detectors.
263
+
264
+ Parameters
265
+ ----------
266
+ classifier : BaseClassifier
267
+ An object implementing a `run` method for classification.
268
+ features : List[str]
269
+ A list of column names corresponding to the features to be used for classification (e.g., 'Height', 'Width', 'Area').
270
+ detectors : list[str]
271
+ A list of detector names to filter the data before classification. Only data from these detectors will be included in the classification process.
272
+
273
+ Returns
274
+ -------
275
+ None
276
+ This method updates the `self.data.peaks` attribute in place with the classified data.
277
+ """
230
278
  self.data.peaks = self.data.peaks.unstack('Detector')
231
279
  self.classifier = classifier
232
280
 
@@ -464,7 +512,7 @@ class Acquisition:
464
512
  zorder=0,
465
513
  )
466
514
 
467
- ax2.set_ylim(detector._saturation_levels)
515
+ ax2.set_ylim(detector._saturation_levels if detector._saturation_levels[0] != detector._saturation_levels[1] else None)
468
516
 
469
517
  self._add_event_to_ax(ax=axes[-1], time_units=time_units)
470
518
 
@@ -489,7 +537,7 @@ class Acquisition:
489
537
  ax.legend()
490
538
 
491
539
  @helper.plot_sns
492
- def coupling_distribution(self, x_detector: str, y_detector: str, equal_limits: bool = False) -> None:
540
+ def coupling_distribution(self, x_detector: str, y_detector: str, bandwidth_adjust: float = 1) -> None:
493
541
  """
494
542
  Plots the density distribution of optical coupling between two detector channels.
495
543
 
@@ -512,7 +560,7 @@ class Acquisition:
512
560
  y = df[y_detector].pint.to(y_units)
513
561
 
514
562
  with plt.style.context(mps):
515
- grid = sns.jointplot(data=df, x=x, y=y, hue="Population", alpha=0.8)
563
+ grid = sns.jointplot(data=df, x=x, y=y, hue="Population", alpha=0.8, marginal_kws=dict(bw_adjust=bandwidth_adjust))
516
564
 
517
565
  grid.ax_joint.set_xlabel(f"Signal {x_detector} [{x_units}]")
518
566
  grid.ax_joint.set_ylabel(f"Signal {y_detector} [{y_units}]")
@@ -522,7 +570,7 @@ class Acquisition:
522
570
  return grid
523
571
 
524
572
  @helper.plot_sns
525
- def scatterer(self, alpha: float = 0.8, bandwidth_adjust: float = 1, log_scale: bool = False, color_palette: Optional[Union[str, dict]] = None) -> None:
573
+ def scatterer(self, alpha: float = 0.8, bandwidth_adjust: float = 1, color_palette: Optional[Union[str, dict]] = None) -> None:
526
574
  """
527
575
  Visualizes the joint distribution of scatterer sizes and refractive indices using a Seaborn jointplot.
528
576
 
@@ -616,6 +664,7 @@ class Acquisition:
616
664
  def trigger(self, show: bool = True) -> None:
617
665
  """Plot detected peaks on signal segments."""
618
666
  n_plots = self.acquisition.n_detectors + 1
667
+
619
668
  with plt.style.context(mps):
620
669
  _, axes = plt.subplots(
621
670
  nrows=n_plots,
@@ -628,12 +677,12 @@ class Acquisition:
628
677
 
629
678
  time_units = self.acquisition.data.triggered['Time'].max().to_compact().units
630
679
 
631
- for ax, (detector_name, group) in zip(axes, self.acquisition.data.triggered.groupby(level=['Detector'])):
680
+ for ax, (detector_name, group) in zip(axes, self.acquisition.data.triggered.groupby(level='Detector')):
632
681
  detector = self.get_detector(detector_name)
633
682
 
634
683
  ax.set_ylabel(detector_name)
635
684
 
636
- for _, sub_group in group.groupby(level=['SegmentID']):
685
+ for _, sub_group in group.groupby(level='SegmentID'):
637
686
  x = sub_group['Time'].pint.to(time_units)
638
687
  digitized = sub_group['DigitizedSignal']
639
688
  ax.step(x, digitized, where='mid', linewidth=2)
@@ -661,7 +710,7 @@ class Acquisition:
661
710
  ax2.legend()
662
711
 
663
712
 
664
- for ax, (detector_name, group) in zip(axes, self.acquisition.data.peaks.groupby(level=['Detector'], axis=0)):
713
+ for ax, (detector_name, group) in zip(axes, self.acquisition.data.peaks.groupby(level='Detector')):
665
714
  x = group['Time'].pint.to(time_units)
666
715
  y = group['Height']
667
716
  ax.scatter(x, y, color='C1')
@@ -700,7 +749,6 @@ class Acquisition:
700
749
 
701
750
  # Set the plotting style
702
751
  with plt.style.context(mps):
703
- # Generate a scatter plot using seaborn's jointplot
704
752
  grid = sns.jointplot(
705
753
  data=self.acquisition.data.peaks,
706
754
  x=(feature, x_detector),
@@ -0,0 +1,182 @@
1
+ from sklearn.cluster import KMeans
2
+ from sklearn.cluster import DBSCAN
3
+ from sklearn.mixture import GaussianMixture
4
+ import pandas as pd
5
+ from typing import Dict, Tuple
6
+
7
+
8
+ class BaseClassifier:
9
+ def filter_dataframe(self, dataframe: pd.DataFrame, features: list, detectors: list = None) -> object:
10
+ """
11
+ Filter the DataFrame based on the selected features and detectors.
12
+
13
+ Parameters
14
+ ----------
15
+ features : list
16
+ List of features to use for filtering. Options include 'Heights', 'Widths', 'Areas'.
17
+ detectors : list, optional
18
+ List of detectors to use. If None, use all detectors.
19
+
20
+ Returns
21
+ -------
22
+ DataFrame
23
+ A filtered DataFrame containing only the selected detectors and features.
24
+
25
+ Raises
26
+ ------
27
+ ValueError
28
+ If no matching features are found for the given detectors and features.
29
+ """
30
+ # Determine detectors to use
31
+
32
+ if detectors is None:
33
+ detectors = dataframe.columns.get_level_values(1).unique().tolist()
34
+
35
+ return dataframe.loc[:, (features, detectors)]
36
+
37
+
38
+ class KmeansClassifier(BaseClassifier):
39
+ def __init__(self, number_of_cluster: int) -> None:
40
+ """
41
+ Initialize the Classifier.
42
+
43
+ Parameters
44
+ ----------
45
+ dataframe : DataFrame
46
+ The input dataframe with multi-index columns.
47
+ """
48
+ self.number_of_cluster = number_of_cluster
49
+
50
+ def run(self, dataframe: pd.DataFrame, features: list = ['Height'], detectors: list = None, random_state: int = 42) -> pd.DataFrame:
51
+ """
52
+ Run KMeans clustering on the selected features and detectors.
53
+
54
+ Parameters
55
+ ----------
56
+ dataframe : pd.DataFrame
57
+ The input DataFrame with multi-index (e.g., by 'Detector').
58
+ features : list
59
+ List of features to use for clustering. Options include 'Height', 'Width', 'Area'.
60
+ detectors : list, optional
61
+ List of detectors to use. If None, use all detectors.
62
+ random_state : int, optional
63
+ Random state for KMeans, by default 42.
64
+
65
+ Returns
66
+ -------
67
+ pd.DataFrame
68
+ DataFrame with clustering labels added.
69
+ """
70
+ # Filter the DataFrame
71
+ sub_dataframe = self.filter_dataframe(dataframe=dataframe, features=features, detectors=detectors)
72
+
73
+ # Ensure data is dequantified if it uses Pint quantities
74
+ if hasattr(sub_dataframe, 'pint'):
75
+ sub_dataframe = sub_dataframe.pint.dequantify().droplevel('unit', axis=1)
76
+
77
+ # Run KMeans
78
+ kmeans = KMeans(n_clusters=self.number_of_cluster, random_state=random_state)
79
+ labels = kmeans.fit_predict(sub_dataframe)
80
+
81
+ dataframe['Label'] = labels
82
+
83
+ return labels
84
+
85
+ class GaussianMixtureClassifier(BaseClassifier):
86
+ def __init__(self, number_of_components: int) -> None:
87
+ """
88
+ Initialize the Gaussian Mixture Classifier.
89
+
90
+ Parameters
91
+ ----------
92
+ number_of_components : int
93
+ Number of Gaussian components (clusters) to use for the model.
94
+ """
95
+ self.number_of_components = number_of_components
96
+
97
+ def run(self, dataframe: pd.DataFrame, features: list = ['Height'], detectors: list = None, random_state: int = 42) -> pd.DataFrame:
98
+ """
99
+ Run Gaussian Mixture Model (GMM) clustering on the selected features and detectors.
100
+
101
+ Parameters
102
+ ----------
103
+ dataframe : pd.DataFrame
104
+ The input DataFrame with multi-index (e.g., by 'Detector').
105
+ features : list
106
+ List of features to use for clustering. Options include 'Height', 'Width', 'Area'.
107
+ detectors : list, optional
108
+ List of detectors to use. If None, use all detectors.
109
+ random_state : int, optional
110
+ Random state for reproducibility, by default 42.
111
+
112
+ Returns
113
+ -------
114
+ pd.DataFrame
115
+ DataFrame with clustering labels added.
116
+ """
117
+ # Filter the DataFrame
118
+ sub_dataframe = self.filter_dataframe(dataframe=dataframe, features=features, detectors=detectors)
119
+
120
+ # Ensure data is dequantified if it uses Pint quantities
121
+ if hasattr(sub_dataframe, 'pint'):
122
+ sub_dataframe = sub_dataframe.pint.dequantify().droplevel('unit', axis=1)
123
+
124
+ # Run Gaussian Mixture Model
125
+ gmm = GaussianMixture(n_components=self.number_of_components, random_state=random_state)
126
+ labels = gmm.fit_predict(sub_dataframe)
127
+
128
+ # Add labels to the original DataFrame
129
+ dataframe['Label'] = labels
130
+
131
+ return labels
132
+
133
+ class DBSCANClassifier(BaseClassifier):
134
+ def __init__(self, epsilon: float = 0.5, min_samples: int = 5) -> None:
135
+ """
136
+ Initialize the DBSCAN Classifier.
137
+
138
+ Parameters
139
+ ----------
140
+ epsilon : float, optional
141
+ The maximum distance between two samples for them to be considered as neighbors.
142
+ Default is 0.5.
143
+ min_samples : int, optional
144
+ The number of samples in a neighborhood for a point to be considered a core point.
145
+ Default is 5.
146
+ """
147
+ self.epsilon = epsilon
148
+ self.min_samples = min_samples
149
+
150
+ def run(self, dataframe: pd.DataFrame, features: list = ['Height'], detectors: list = None) -> pd.DataFrame:
151
+ """
152
+ Run DBSCAN clustering on the selected features and detectors.
153
+
154
+ Parameters
155
+ ----------
156
+ dataframe : pd.DataFrame
157
+ The input DataFrame with multi-index (e.g., by 'Detector').
158
+ features : list
159
+ List of features to use for clustering. Options include 'Height', 'Width', 'Area'.
160
+ detectors : list, optional
161
+ List of detectors to use. If None, use all detectors.
162
+
163
+ Returns
164
+ -------
165
+ pd.DataFrame
166
+ DataFrame with clustering labels added. Noise points are labeled as -1.
167
+ """
168
+ # Filter the DataFrame
169
+ sub_dataframe = self.filter_dataframe(dataframe=dataframe, features=features, detectors=detectors)
170
+
171
+ # Ensure data is dequantified if it uses Pint quantities
172
+ if hasattr(sub_dataframe, 'pint'):
173
+ sub_dataframe = sub_dataframe.pint.dequantify().droplevel('unit', axis=1)
174
+
175
+ # Run DBSCAN
176
+ dbscan = DBSCAN(eps=self.epsilon, min_samples=self.min_samples)
177
+ labels = dbscan.fit_predict(sub_dataframe)
178
+
179
+ # Add labels to the original DataFrame
180
+ dataframe['Label'] = labels
181
+
182
+ return labels
@@ -4,14 +4,13 @@
4
4
  import logging
5
5
  import numpy as np
6
6
  from typing import List, Callable, Optional
7
- from MPSPlots.styles import mps
8
- from FlowCyPy.flow_cell import FlowCell
9
- from FlowCyPy.detector import Detector
10
7
  import pandas as pd
11
- import pint_pandas
8
+ from pint_pandas import PintArray
9
+
12
10
  from FlowCyPy import units
13
11
  from FlowCyPy.units import Quantity, milliwatt
14
- from pint_pandas import PintArray
12
+ from FlowCyPy.flow_cell import FlowCell
13
+ from FlowCyPy.detector import Detector
15
14
  from FlowCyPy.acquisition import Acquisition
16
15
  from FlowCyPy.signal_digitizer import SignalDigitizer
17
16
 
@@ -117,33 +116,73 @@ class FlowCytometer:
117
116
  medium_refractive_index=self.scatterer_collection.medium_refractive_index
118
117
  )
119
118
 
120
- scatterer_dataframe[detector.name] = pint_pandas.PintArray(self.coupling_power, dtype=self.coupling_power.units)
119
+ scatterer_dataframe[detector.name] = PintArray(self.coupling_power, dtype=self.coupling_power.units)
121
120
 
122
121
  def _generate_pulse_parameters(self, scatterer_dataframe: pd.DataFrame) -> None:
123
- """
122
+ r"""
124
123
  Generates and assigns random Gaussian pulse parameters for each particle event.
125
124
 
126
- The generated parameters include:
127
- - Centers: The time at which each pulse occurs.
128
- - Widths: The standard deviation (spread) of each pulse in seconds.
125
+ The pulse shape follows the Gaussian beam’s spatial intensity profile:
129
126
 
130
- Effects
131
- -------
132
- scatterer_collection.dataframe : pandas.DataFrame
133
- Adds a 'Widths' column with computed pulse widths for each particle.
134
- Uses the flow speed and beam waist to calculate pulse widths.
135
- """
136
- columns = pd.MultiIndex.from_product(
137
- [[p.name for p in self.detectors], ['Centers', 'Heights']]
138
- )
127
+ .. math::
128
+
129
+ I(r) = I_0 \exp\left(-\frac{2r^2}{w_0^2}\right),
130
+
131
+ where :math:`w_0` is the beam waist (the :math:`1/e^2` radius of the intensity distribution).
132
+ This profile can be rewritten in standard Gaussian form:
133
+
134
+ .. math::
135
+
136
+ I(r) = I_0 \exp\left(-\frac{r^2}{2\sigma_x^2}\right),
139
137
 
140
- self.pulse_dataframe = pd.DataFrame(columns=columns)
138
+ which implies the spatial standard deviation:
141
139
 
142
- self.pulse_dataframe['Centers'] = scatterer_dataframe['Time']
140
+ .. math::
143
141
 
144
- widths = self.source.waist / self.flow_cell.flow_speed * np.ones(len(scatterer_dataframe))
142
+ \sigma_x = \frac{w_0}{2}.
145
143
 
146
- scatterer_dataframe['Widths'] = pint_pandas.PintArray(widths, dtype=widths.units)
144
+ When a particle moves at a constant flow speed :math:`v`, the spatial coordinate :math:`r`
145
+ is related to time :math:`t` via :math:`r = v t`. Substituting this into the intensity profile
146
+ gives a temporal Gaussian:
147
+
148
+ .. math::
149
+
150
+ I(t) = I_0 \exp\left(-\frac{2 (v t)^2}{w_0^2}\right).
151
+
152
+ This is equivalent to a Gaussian in time:
153
+
154
+ .. math::
155
+
156
+ I(t) = I_0 \exp\left(-\frac{t^2}{2\sigma_t^2}\right),
157
+
158
+ so that the temporal standard deviation is:
159
+
160
+ .. math::
161
+
162
+ \sigma_t = \frac{\sigma_x}{v} = \frac{w_0}{2v}.
163
+
164
+ The full width at half maximum (FWHM) in time is then:
165
+
166
+ .. math::
167
+
168
+ \text{FWHM} = 2\sqrt{2 \ln2} \, \sigma_t = \frac{w_0}{v} \sqrt{2 \ln2}.
169
+
170
+ **Generated Parameters:**
171
+ - **Centers:** The time at which each pulse occurs (randomly determined).
172
+ - **Widths:** The pulse width (:math:`\sigma_t`) in seconds, computed as :math:`w_0 / (2 v)`.
173
+
174
+ **Effects**
175
+ -----------
176
+ Modifies `scatterer_dataframe` in place by adding:
177
+ - A `'Centers'` column with the pulse center times.
178
+ - A `'Widths'` column with the computed pulse widths.
179
+ """
180
+ # Calculate the pulse width (standard deviation in time, σₜ) based on the beam waist and flow speed.
181
+ pulse_width = self.source.waist / (2 * self.flow_cell.flow_speed)
182
+
183
+ widths = pulse_width * np.ones(len(scatterer_dataframe))
184
+
185
+ scatterer_dataframe['Widths'] = PintArray(widths, dtype=widths.units)
147
186
 
148
187
  def initialize_signal(self, run_time: Quantity) -> None:
149
188
  """
@@ -221,11 +260,11 @@ class FlowCytometer:
221
260
  time = self.dataframe.xs(detector.name)['Time'].pint.magnitude
222
261
 
223
262
  time_grid = np.expand_dims(time, axis=0) * units.second
224
-
225
263
  centers = np.expand_dims(_centers, axis=1) * units.second
226
264
  widths = np.expand_dims(_widths, axis=1) * units.second
227
265
 
228
266
  # Compute the Gaussian for each height, center, and width using broadcasting
267
+ # To be noted that widths is defined as: waist / (2 * flow_speed)
229
268
  power_gaussians = _coupling_power[:, np.newaxis] * np.exp(- (time_grid - centers) ** 2 / (2 * widths ** 2))
230
269
 
231
270
  total_power = np.sum(power_gaussians, axis=0) + self.background_power
@@ -1,21 +1,20 @@
1
+ import logging
2
+ from copy import copy
1
3
  import numpy as np
2
4
  import pandas as pd
3
5
  from typing import Optional, Union
4
6
  import matplotlib.pyplot as plt
5
- from FlowCyPy import units
6
- from FlowCyPy.units import AU, volt, watt, degree, ampere, coulomb, particle, meter
7
- from FlowCyPy.utils import PropertiesReport
8
7
  from pydantic.dataclasses import dataclass
9
8
  from pydantic import field_validator
10
9
  import pint_pandas
11
- from FlowCyPy.physical_constant import PhysicalConstant
10
+
12
11
  from PyMieSim.units import Quantity
12
+ from FlowCyPy import units
13
+ from FlowCyPy.units import AU, volt, watt, degree, ampere, coulomb
13
14
  from FlowCyPy.noises import NoiseSetting
14
- from FlowCyPy.helper import plot_helper
15
15
  from FlowCyPy.peak_locator import BasePeakLocator
16
- import logging
17
- from copy import copy
18
16
  from FlowCyPy.signal_digitizer import SignalDigitizer
17
+ from FlowCyPy.physical_constant import PhysicalConstant
19
18
 
20
19
  config_dict = dict(
21
20
  arbitrary_types_allowed=True,
@@ -26,7 +25,7 @@ config_dict = dict(
26
25
 
27
26
 
28
27
  @dataclass(config=config_dict, unsafe_hash=True)
29
- class Detector(PropertiesReport):
28
+ class Detector():
30
29
  """
31
30
  A class representing a signal detector used in flow cytometry.
32
31
 
@@ -337,62 +336,6 @@ class Detector(PropertiesReport):
337
336
 
338
337
  return digitized_signal
339
338
 
340
- @plot_helper
341
- def plot_raw(
342
- self,
343
- ax: Optional[plt.Axes] = None,
344
- time_unit: Optional[Union[str, Quantity]] = None,
345
- signal_unit: Optional[Union[str, Quantity]] = None,
346
- add_peak_locator: bool = False
347
- ) -> None:
348
- """
349
- Visualizes the signal and optional components (peaks, raw signal) over time.
350
-
351
- This method generates a customizable plot of the processed signal as a function of time.
352
- Additional components like raw signals and detected peaks can also be overlaid.
353
-
354
- Parameters
355
- ----------
356
- ax : matplotlib.axes.Axes, optional
357
- An existing Matplotlib Axes object to plot on. If None, a new Axes will be created.
358
- time_unit : str or Quantity, optional
359
- Desired unit for the time axis. If None, defaults to the most compact unit of the `Time` column.
360
- signal_unit : str or Quantity, optional
361
- Desired unit for the signal axis. If None, defaults to the most compact unit of the `Signal` column.
362
- add_peak_locator : bool, optional
363
- If True, adds the detected peaks (if available) to the plot. Default is False.
364
-
365
- Returns
366
- -------
367
- tuple[Quantity, Quantity]
368
- A tuple containing the units used for the time and signal axes, respectively.
369
-
370
- Notes
371
- -----
372
- - The `Time` and `Signal` data are automatically converted to the specified units for consistency.
373
- - If no `ax` is provided, a new figure and axis will be generated.
374
- - Warnings are logged if peak locator data is unavailable when `add_peak_locator` is True.
375
- """
376
- # Set default units if not provided
377
- signal_unit = signal_unit or self.dataframe['Signal'].max().to_compact().units
378
- time_unit = time_unit or self.dataframe['Time'].max().to_compact().units
379
-
380
- x = self.dataframe['Time'].pint.to(time_unit)
381
-
382
- ax.plot(x, self.dataframe['Signal'].pint.to(signal_unit), color='C1', linestyle='--', label=f'{self.name}: Raw', linewidth=1)
383
- ax.legend(loc='upper right')
384
-
385
- # Overlay peak locator positions, if requested
386
- if add_peak_locator:
387
- if not hasattr(self, 'algorithm'):
388
- logging.warning("The detector does not have a peak locator algorithm. Peaks cannot be plotted.")
389
-
390
- self.algorithm._add_to_ax(ax=ax, signal_unit=signal_unit, time_unit=time_unit)
391
-
392
- # Customize labels
393
- ax.set_xlabel(f"Time [{time_unit:P}]")
394
- ax.set_ylabel(f"{self.name} [{signal_unit:P}]")
395
-
396
339
  def set_peak_locator(self, algorithm: BasePeakLocator, compute_peak_area: bool = True) -> None:
397
340
  """
398
341
  Assigns a peak detection algorithm to the detector, analyzes the signal,