FlowCyPy 0.7.0__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 (168) hide show
  1. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/_version.py +2 -2
  2. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/acquisition.py +178 -64
  3. flowcypy-0.7.3/FlowCyPy/classifier.py +182 -0
  4. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/cytometer.py +63 -24
  5. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/detector.py +7 -64
  6. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/distribution/particle_size_distribution.py +6 -5
  7. flowcypy-0.7.3/FlowCyPy/flow_cell.py +137 -0
  8. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/helper.py +22 -14
  9. flowcypy-0.7.3/FlowCyPy/noises.py +87 -0
  10. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/particle_count.py +3 -2
  11. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/population.py +3 -4
  12. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/scatterer_collection.py +7 -7
  13. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/signal_digitizer.py +1 -3
  14. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/source.py +4 -7
  15. flowcypy-0.7.3/FlowCyPy/utils.py +74 -0
  16. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy.egg-info/PKG-INFO +2 -2
  17. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy.egg-info/SOURCES.txt +16 -4
  18. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy.egg-info/requires.txt +1 -1
  19. {flowcypy-0.7.0 → flowcypy-0.7.3}/PKG-INFO +2 -2
  20. flowcypy-0.7.3/developments/Deep_peak_square.ipynb +1049 -0
  21. flowcypy-0.7.3/developments/Physics-informed_AI.ipynb +876 -0
  22. flowcypy-0.7.3/developments/ROI_analysis-Copy1.ipynb +639 -0
  23. flowcypy-0.7.3/developments/ROI_analysis.ipynb +778 -0
  24. flowcypy-0.7.3/developments/Untitled.ipynb +227 -0
  25. flowcypy-0.7.3/developments/Untitled1.ipynb +668 -0
  26. flowcypy-0.7.3/developments/Untitled2.ipynb +313 -0
  27. flowcypy-0.7.3/developments/ai_dev2.ipynb +1745 -0
  28. flowcypy-0.7.3/developments/best_model.h5 +0 -0
  29. flowcypy-0.7.3/developments/best_model.keras +0 -0
  30. flowcypy-0.7.0/developments/scripts/temp.py → flowcypy-0.7.3/developments/concentration_validation.py +34 -61
  31. flowcypy-0.7.3/developments/grad_cam_output.png +0 -0
  32. flowcypy-0.7.3/developments/model.png +0 -0
  33. flowcypy-0.7.3/developments/model_example.png +0 -0
  34. flowcypy-0.7.3/developments/scripts/AI_peak_detection.py +85 -0
  35. flowcypy-0.7.3/developments/scripts/temp.py +207 -0
  36. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/examples/extras/flow_cytometer_signal.py +2 -2
  37. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/examples/extras/scatterer_distribution.py +3 -2
  38. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/examples/extras/signal_acquisition.py +1 -1
  39. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/examples/tutorials/limit_of_detection.py +1 -1
  40. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/examples/tutorials/workflow.py +12 -15
  41. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/sg_execution_times.rst +15 -15
  42. {flowcypy-0.7.0 → flowcypy-0.7.3}/pyproject.toml +1 -1
  43. flowcypy-0.7.3/tests/test_classifiers.py +83 -0
  44. {flowcypy-0.7.0 → flowcypy-0.7.3}/tests/test_coupling_mechanism.py +1 -1
  45. {flowcypy-0.7.0 → flowcypy-0.7.3}/tests/test_flow_cytometer.py +26 -8
  46. {flowcypy-0.7.0 → flowcypy-0.7.3}/tests/test_peak_analyzer.py +1 -1
  47. {flowcypy-0.7.0 → flowcypy-0.7.3}/tests/test_population.py +2 -2
  48. {flowcypy-0.7.0 → flowcypy-0.7.3}/tests/test_scatterer_distribution.py +1 -1
  49. flowcypy-0.7.0/FlowCyPy/classifier.py +0 -208
  50. flowcypy-0.7.0/FlowCyPy/coupling_mechanism.py +0 -205
  51. flowcypy-0.7.0/FlowCyPy/flow_cell.py +0 -198
  52. flowcypy-0.7.0/FlowCyPy/logger.py +0 -136
  53. flowcypy-0.7.0/FlowCyPy/noises.py +0 -34
  54. flowcypy-0.7.0/FlowCyPy/plottings.py +0 -269
  55. flowcypy-0.7.0/FlowCyPy/utils.py +0 -191
  56. flowcypy-0.7.0/tests/test_extra.py +0 -55
  57. {flowcypy-0.7.0 → flowcypy-0.7.3}/.flake8 +0 -0
  58. {flowcypy-0.7.0 → flowcypy-0.7.3}/.github/dependabot.yml +0 -0
  59. {flowcypy-0.7.0 → flowcypy-0.7.3}/.github/workflows/deploy_PyPi.yml +0 -0
  60. {flowcypy-0.7.0 → flowcypy-0.7.3}/.github/workflows/deploy_anaconda.yml +0 -0
  61. {flowcypy-0.7.0 → flowcypy-0.7.3}/.github/workflows/deploy_coverage.yml +0 -0
  62. {flowcypy-0.7.0 → flowcypy-0.7.3}/.github/workflows/deploy_documentation.yml +0 -0
  63. {flowcypy-0.7.0 → flowcypy-0.7.3}/.gitignore +0 -0
  64. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/__init__.py +0 -0
  65. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/coupling_mechanism/__init__.py +0 -0
  66. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/coupling_mechanism/empirical.py +0 -0
  67. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/coupling_mechanism/mie.py +0 -0
  68. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/coupling_mechanism/rayleigh.py +0 -0
  69. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/coupling_mechanism/uniform.py +0 -0
  70. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/directories.py +0 -0
  71. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/distribution/__init__.py +0 -0
  72. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/distribution/base_class.py +0 -0
  73. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/distribution/delta.py +0 -0
  74. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/distribution/lognormal.py +0 -0
  75. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/distribution/normal.py +0 -0
  76. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/distribution/uniform.py +0 -0
  77. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/distribution/weibull.py +0 -0
  78. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/peak_locator/__init__.py +0 -0
  79. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/peak_locator/base_class.py +0 -0
  80. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/peak_locator/basic.py +0 -0
  81. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/peak_locator/derivative.py +0 -0
  82. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/peak_locator/moving_average.py +0 -0
  83. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/physical_constant.py +0 -0
  84. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/populations_instances.py +0 -0
  85. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy/units.py +0 -0
  86. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy.egg-info/dependency_links.txt +0 -0
  87. {flowcypy-0.7.0 → flowcypy-0.7.3}/FlowCyPy.egg-info/top_level.txt +0 -0
  88. {flowcypy-0.7.0 → flowcypy-0.7.3}/LICENSE +0 -0
  89. {flowcypy-0.7.0 → flowcypy-0.7.3}/README.rst +0 -0
  90. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/doc/canto_spec.md +0 -0
  91. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/doc/internship.pdf +0 -0
  92. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/get_started.md +0 -0
  93. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/image.png +0 -0
  94. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/output_file.prof +0 -0
  95. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/scripts/concentration_comparison.py +0 -0
  96. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/scripts/create_images.py +0 -0
  97. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/scripts/data_analysis.py +0 -0
  98. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/scripts/dev_beads_analysis.py +0 -0
  99. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/scripts/dev_canto.py +0 -0
  100. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/scripts/dev_classifier.py +0 -0
  101. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/scripts/dev_shot_noise_check.py +0 -0
  102. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/scripts/dev_stats_0.py +0 -0
  103. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/scripts/dev_stats_1.py +0 -0
  104. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/scripts/dev_stats_2.py +0 -0
  105. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/scripts/dev_study_on_ri.py +0 -0
  106. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/scripts/dev_study_on_size.py +0 -0
  107. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/scripts/mat2csv.py +0 -0
  108. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/scripts/profiler.py +0 -0
  109. {flowcypy-0.7.0 → flowcypy-0.7.3}/developments/test.pdf +0 -0
  110. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/Makefile +0 -0
  111. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/examples/extras/README.rst +0 -0
  112. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/examples/extras/distributions.py +0 -0
  113. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/examples/noise_sources/README.rst +0 -0
  114. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/examples/noise_sources/dark_current.py +0 -0
  115. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/examples/noise_sources/shot_noise.py +0 -0
  116. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/examples/noise_sources/thermal.py +0 -0
  117. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/examples/tutorials/README.rst +0 -0
  118. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/images/distributions/Delta.png +0 -0
  119. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/images/distributions/LogNormal.png +0 -0
  120. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/images/distributions/Normal.png +0 -0
  121. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/images/distributions/RosinRammler.png +0 -0
  122. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/images/distributions/Uniform.png +0 -0
  123. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/images/distributions/Weibull.png +0 -0
  124. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/images/example_0.png +0 -0
  125. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/images/example_1.png +0 -0
  126. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/images/example_2.png +0 -0
  127. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/images/example_3.png +0 -0
  128. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/images/flow_cytometer.png +0 -0
  129. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/images/logo.png +0 -0
  130. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/make.bat +0 -0
  131. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/_static/default.css +0 -0
  132. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/_static/logo.png +0 -0
  133. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/_static/thumbnail.png +0 -0
  134. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/code/base.rst +0 -0
  135. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/code/detector.rst +0 -0
  136. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/code/distributions.rst +0 -0
  137. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/code/flow_cell.rst +0 -0
  138. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/code/flow_cytometer.rst +0 -0
  139. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/code/peak_locator.rst +0 -0
  140. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/code/scatterer.rst +0 -0
  141. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/code/source.rst +0 -0
  142. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/code.rst +0 -0
  143. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/conf.py +0 -0
  144. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/examples.rst +0 -0
  145. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/index.rst +0 -0
  146. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/internal/core_components.rst +0 -0
  147. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/internal/getting_started.rst +0 -0
  148. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/internal/objectives/main.rst +0 -0
  149. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/internal/objectives/pre.rst +0 -0
  150. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/internal/objectives/stretch.rst +0 -0
  151. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/internal/prerequisites/index.rst +0 -0
  152. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/internal/prerequisites/mathematics.rst +0 -0
  153. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/internal/prerequisites/optics.rst +0 -0
  154. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/internal/prerequisites/programming.rst +0 -0
  155. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/internal/ressources.rst +0 -0
  156. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/internal/tasks.rst +0 -0
  157. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/internal.rst +0 -0
  158. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/references.rst +0 -0
  159. {flowcypy-0.7.0 → flowcypy-0.7.3}/docs/source/theory.rst +0 -0
  160. {flowcypy-0.7.0 → flowcypy-0.7.3}/meta.yaml +0 -0
  161. {flowcypy-0.7.0 → flowcypy-0.7.3}/notebook.ipynb +0 -0
  162. {flowcypy-0.7.0 → flowcypy-0.7.3}/setup.cfg +0 -0
  163. {flowcypy-0.7.0 → flowcypy-0.7.3}/tests/__init__.py +0 -0
  164. {flowcypy-0.7.0 → flowcypy-0.7.3}/tests/test_detector_noise.py +0 -0
  165. {flowcypy-0.7.0 → flowcypy-0.7.3}/tests/test_distribution.py +0 -0
  166. {flowcypy-0.7.0 → flowcypy-0.7.3}/tests/test_noises.py +0 -0
  167. {flowcypy-0.7.0 → flowcypy-0.7.3}/tests/test_peak_algorithm.py +0 -0
  168. {flowcypy-0.7.0 → 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.0'
16
- __version_tuple__ = version_tuple = (0, 7, 0)
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,7 +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
+
14
+ from FlowCyPy import helper
15
+ from FlowCyPy.classifier import BaseClassifier
13
16
 
14
17
  class DataAccessor:
15
18
  def __init__(self, outer):
@@ -93,13 +96,13 @@ class Acquisition:
93
96
  results = results.reset_index(drop=True)
94
97
 
95
98
  # Check for multiple peaks and issue a warning
96
- peak_counts = results.groupby(['Detector', 'SegmentID']).size()
97
- multiple_peak_segments = peak_counts[peak_counts > 1]
98
- if not multiple_peak_segments.empty:
99
- warnings.warn(
100
- f"Multiple peaks detected in the following segments: {multiple_peak_segments.index.tolist()}",
101
- UserWarning
102
- )
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
+ # )
103
106
 
104
107
  _temp = results.reset_index()[['Detector', 'SegmentID', 'Height']].pint.dequantify().droplevel('unit', axis=1)
105
108
 
@@ -110,13 +113,36 @@ class Acquisition:
110
113
  )
111
114
 
112
115
  def _get_trigger_indices(
113
- self,
114
- threshold: units.Quantity,
115
- trigger_detector_name: str = None,
116
- pre_buffer: int = 64,
117
- post_buffer: int = 64) -> tuple[np.ndarray, np.ndarray]:
116
+ self,
117
+ threshold: units.Quantity,
118
+ trigger_detector_name: str = None,
119
+ pre_buffer: int = 64,
120
+ post_buffer: int = 64
121
+ ) -> tuple[np.ndarray, np.ndarray]:
118
122
  """
119
- Calculate start and end indices for triggered segments.
123
+ Calculate start and end indices for triggered segments, ensuring no retriggering
124
+ occurs during an active buffer period.
125
+
126
+ Parameters
127
+ ----------
128
+ threshold : units.Quantity
129
+ The threshold value for triggering.
130
+ trigger_detector_name : str, optional
131
+ The name of the detector to use for the triggering signal.
132
+ pre_buffer : int, optional
133
+ Number of samples to include before the trigger point.
134
+ post_buffer : int, optional
135
+ Number of samples to include after the trigger point.
136
+
137
+ Returns
138
+ -------
139
+ tuple[np.ndarray, np.ndarray]
140
+ The start and end indices of non-overlapping triggered segments.
141
+
142
+ Raises
143
+ ------
144
+ ValueError
145
+ If the specified detector is not found in the data.
120
146
  """
121
147
  if trigger_detector_name not in self.data.continuous.index.get_level_values('Detector').unique():
122
148
  raise ValueError(f"Detector '{trigger_detector_name}' not found.")
@@ -128,7 +154,18 @@ class Acquisition:
128
154
  start_indices = np.clip(crossings - pre_buffer, 0, len(trigger_signal) - 1)
129
155
  end_indices = np.clip(crossings + post_buffer, 0, len(trigger_signal) - 1)
130
156
 
131
- return start_indices, end_indices
157
+ # Suppress retriggering within an active buffer period
158
+ suppressed_start_indices = []
159
+ suppressed_end_indices = []
160
+
161
+ last_end = -1
162
+ for start, end in zip(start_indices, end_indices):
163
+ if start > last_end: # Ensure no overlap with the last active buffer
164
+ suppressed_start_indices.append(start)
165
+ suppressed_end_indices.append(end)
166
+ last_end = end # Update the end of the current active buffer
167
+
168
+ return np.array(suppressed_start_indices), np.array(suppressed_end_indices)
132
169
 
133
170
  def run_triggering(self,
134
171
  threshold: units.Quantity,
@@ -137,22 +174,47 @@ class Acquisition:
137
174
  post_buffer: int = 64,
138
175
  max_triggers: int = None) -> None:
139
176
  """
140
- 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`.
141
183
 
142
184
  Parameters
143
185
  ----------
144
186
  threshold : units.Quantity
145
- Trigger threshold value.
146
- trigger_detector_name : str, optional
147
- Detector used for triggering, by default None.
148
- custom_trigger : np.ndarray, optional
149
- 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.
150
192
  pre_buffer : int, optional
151
- Points before trigger, by default 64.
193
+ The number of points to include before the trigger point in each segment.
194
+ Default is 64.
152
195
  post_buffer : int, optional
153
- Points after trigger, by default 64.
196
+ The number of points to include after the trigger point in each segment.
197
+ Default is 64.
154
198
  max_triggers : int, optional
155
- 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.
156
218
  """
157
219
  self.threshold = threshold
158
220
  self.trigger_detector_name = trigger_detector_name
@@ -191,6 +253,37 @@ class Acquisition:
191
253
 
192
254
  self.detect_peaks()
193
255
 
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
+ """
278
+ self.data.peaks = self.data.peaks.unstack('Detector')
279
+ self.classifier = classifier
280
+
281
+ self.classifier.run(
282
+ dataframe=self.data.peaks,
283
+ features=features,
284
+ detectors=detectors
285
+ )
286
+
194
287
  class LoggerInterface:
195
288
  """
196
289
  A nested class for logging statistical information about the experiment.
@@ -419,7 +512,7 @@ class Acquisition:
419
512
  zorder=0,
420
513
  )
421
514
 
422
- ax2.set_ylim(detector._saturation_levels)
515
+ ax2.set_ylim(detector._saturation_levels if detector._saturation_levels[0] != detector._saturation_levels[1] else None)
423
516
 
424
517
  self._add_event_to_ax(ax=axes[-1], time_units=time_units)
425
518
 
@@ -443,7 +536,8 @@ class Acquisition:
443
536
 
444
537
  ax.legend()
445
538
 
446
- def coupling_distribution(self, x_detector: str, y_detector: str, log_scale: bool = False, show: bool = True, equal_limits: bool = False, save_path: str = None) -> None:
539
+ @helper.plot_sns
540
+ def coupling_distribution(self, x_detector: str, y_detector: str, bandwidth_adjust: float = 1) -> None:
447
541
  """
448
542
  Plots the density distribution of optical coupling between two detector channels.
449
543
 
@@ -466,31 +560,17 @@ class Acquisition:
466
560
  y = df[y_detector].pint.to(y_units)
467
561
 
468
562
  with plt.style.context(mps):
469
- joint_plot = sns.jointplot(data=df, x=x, y=y, hue="Population", alpha=0.8)
470
-
471
- if log_scale:
472
- joint_plot.ax_joint.set_xscale("log")
473
- joint_plot.ax_joint.set_yscale("log")
474
-
475
- if equal_limits:
476
- min_limit = min(x.min(), y.min())
477
- max_limit = max(x.max(), y.max())
478
- joint_plot.ax_joint.set_xlim(min_limit, max_limit)
479
- joint_plot.ax_joint.set_ylim(min_limit, max_limit)
563
+ grid = sns.jointplot(data=df, x=x, y=y, hue="Population", alpha=0.8, marginal_kws=dict(bw_adjust=bandwidth_adjust))
480
564
 
481
- joint_plot.ax_joint.set_xlabel(f"Signal {x_detector} [{x_units}]")
482
- joint_plot.ax_joint.set_ylabel(f"Signal {y_detector} [{y_units}]")
565
+ grid.ax_joint.set_xlabel(f"Signal {x_detector} [{x_units}]")
566
+ grid.ax_joint.set_ylabel(f"Signal {y_detector} [{y_units}]")
483
567
 
484
- plt.tight_layout()
568
+ grid.figure.suptitle("Theoretical coupling distribution")
485
569
 
486
- if save_path:
487
- joint_plot.figure.savefig(save_path, dpi=300, bbox_inches="tight")
488
- logging.info(f"Plot saved to {save_path}")
570
+ return grid
489
571
 
490
- if show:
491
- plt.show()
492
-
493
- def scatterer(self, show: bool = True, alpha: float = 0.8, bandwidth_adjust: float = 1, log_scale: bool = False, color_palette: Optional[Union[str, dict]] = None) -> None:
572
+ @helper.plot_sns
573
+ def scatterer(self, alpha: float = 0.8, bandwidth_adjust: float = 1, color_palette: Optional[Union[str, dict]] = None) -> None:
494
574
  """
495
575
  Visualizes the joint distribution of scatterer sizes and refractive indices using a Seaborn jointplot.
496
576
 
@@ -504,8 +584,6 @@ class Acquisition:
504
584
  Transparency level for the scatter plot points, ranging from 0 (fully transparent) to 1 (fully opaque). Default is 0.8.
505
585
  bandwidth_adjust : float, optional
506
586
  Bandwidth adjustment factor for the kernel density estimate of the marginal distributions. Higher values produce smoother density estimates. Default is 1.
507
- log_scale : bool, optional
508
- If `True`, applies a logarithmic scale to both axes of the joint plot and their marginal distributions. Default is `False`.
509
587
  color_palette : str or dict, optional
510
588
  The color palette to use for the hue in the scatterplot. Can be a seaborn palette name
511
589
  (e.g., 'viridis', 'coolwarm') or a dictionary mapping hue levels to specific colors. Default is None.
@@ -540,19 +618,13 @@ class Acquisition:
540
618
  marginal_kws=dict(bw_adjust=bandwidth_adjust)
541
619
  )
542
620
 
543
- grid.ax_joint.set_xlabel(f"Size [{x_unit}]")
544
-
545
- if log_scale:
546
- grid.ax_joint.set_xscale('log')
547
- grid.ax_joint.set_yscale('log')
548
- grid.ax_marg_x.set_xscale('log')
549
- grid.ax_marg_y.set_yscale('log')
621
+ grid.figure.suptitle("Scatterer sampling distribution")
550
622
 
551
- plt.tight_layout()
623
+ grid.ax_joint.set_xlabel(f"Size [{x_unit}]")
552
624
 
553
- if show:
554
- plt.show()
625
+ return grid
555
626
 
627
+ @helper.plot_sns
556
628
  def peaks(self, x_detector: str, y_detector: str, signal: str = 'Height', bandwidth_adjust: float = 0.8) -> None:
557
629
  """
558
630
  Plot the joint KDE distribution of the specified signal between two detectors using seaborn,
@@ -582,15 +654,17 @@ class Acquisition:
582
654
  joint_kws={'bw_adjust': bandwidth_adjust, 'alpha': 0.7}
583
655
  )
584
656
 
657
+ grid.figure.suptitle("Peaks properties")
585
658
  grid.ax_joint.scatter(x_data, y_data, color='C1', alpha=0.6)
586
659
 
587
660
  grid.set_axis_labels(f"{signal} ({x_detector}) [{x_units}]", f"{signal} ({y_detector}) [{y_units}]", fontsize=12)
588
- plt.tight_layout()
589
- plt.show()
661
+
662
+ return grid
590
663
 
591
664
  def trigger(self, show: bool = True) -> None:
592
665
  """Plot detected peaks on signal segments."""
593
666
  n_plots = self.acquisition.n_detectors + 1
667
+
594
668
  with plt.style.context(mps):
595
669
  _, axes = plt.subplots(
596
670
  nrows=n_plots,
@@ -603,12 +677,12 @@ class Acquisition:
603
677
 
604
678
  time_units = self.acquisition.data.triggered['Time'].max().to_compact().units
605
679
 
606
- 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')):
607
681
  detector = self.get_detector(detector_name)
608
682
 
609
683
  ax.set_ylabel(detector_name)
610
684
 
611
- for _, sub_group in group.groupby(level=['SegmentID']):
685
+ for _, sub_group in group.groupby(level='SegmentID'):
612
686
  x = sub_group['Time'].pint.to(time_units)
613
687
  digitized = sub_group['DigitizedSignal']
614
688
  ax.step(x, digitized, where='mid', linewidth=2)
@@ -636,7 +710,7 @@ class Acquisition:
636
710
  ax2.legend()
637
711
 
638
712
 
639
- 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')):
640
714
  x = group['Time'].pint.to(time_units)
641
715
  y = group['Height']
642
716
  ax.scatter(x, y, color='C1')
@@ -646,6 +720,46 @@ class Acquisition:
646
720
  if show:
647
721
  plt.show()
648
722
 
723
+ @helper.plot_sns
724
+ def classifier(self, feature: str, x_detector: str, y_detector: str) -> None:
725
+ """
726
+ Visualize the classification of peaks using a scatter plot.
727
+
728
+ Parameters
729
+ ----------
730
+ feature : str
731
+ The feature to classify (e.g., 'Height', 'Width', 'Area').
732
+ x_detector : str
733
+ The detector to use for the x-axis.
734
+ y_detector : str
735
+ The detector to use for the y-axis.
736
+
737
+ Raises
738
+ ------
739
+ ValueError
740
+ If the 'Label' column is missing in the data, suggesting that
741
+ the `classify_dataset` method must be called first.
742
+ """
743
+ # Check if 'Label' exists in the dataset
744
+ if 'Label' not in self.acquisition.data.peaks.columns:
745
+ raise ValueError(
746
+ "The 'Label' column is missing. Ensure the dataset has been classified "
747
+ "by calling the `classify_dataset` method before using `classifier`."
748
+ )
749
+
750
+ # Set the plotting style
751
+ with plt.style.context(mps):
752
+ grid = sns.jointplot(
753
+ data=self.acquisition.data.peaks,
754
+ x=(feature, x_detector),
755
+ y=(feature, y_detector),
756
+ hue='Label',
757
+ )
758
+
759
+ grid.figure.suptitle('Event classification')
760
+
761
+ return grid
762
+
649
763
  def get_detector(self, name: str):
650
764
  for detector in self.acquisition.cytometer.detectors:
651
765
  if detector.name == name:
@@ -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