FlowCyPy 0.12.0__cp312-cp312-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. FlowCyPy/__init__.py +18 -0
  2. FlowCyPy/_version.py +21 -0
  3. FlowCyPy/acquisition.py +182 -0
  4. FlowCyPy/binary/__init__.py +0 -0
  5. FlowCyPy/binary/filtering.cp310-win_amd64.pyd +0 -0
  6. FlowCyPy/binary/filtering.cp311-win_amd64.pyd +0 -0
  7. FlowCyPy/binary/filtering.cp312-win_amd64.pyd +0 -0
  8. FlowCyPy/binary/triggering_system.cp310-win_amd64.pyd +0 -0
  9. FlowCyPy/binary/triggering_system.cp311-win_amd64.pyd +0 -0
  10. FlowCyPy/binary/triggering_system.cp312-win_amd64.pyd +0 -0
  11. FlowCyPy/circuits.py +125 -0
  12. FlowCyPy/classifier.py +182 -0
  13. FlowCyPy/coupling_mechanism/__init__.py +4 -0
  14. FlowCyPy/coupling_mechanism/empirical.py +47 -0
  15. FlowCyPy/coupling_mechanism/mie.py +223 -0
  16. FlowCyPy/coupling_mechanism/rayleigh.py +116 -0
  17. FlowCyPy/coupling_mechanism/uniform.py +40 -0
  18. FlowCyPy/cytometer.py +391 -0
  19. FlowCyPy/dataframe_subclass.py +679 -0
  20. FlowCyPy/detector.py +282 -0
  21. FlowCyPy/directories.py +36 -0
  22. FlowCyPy/distribution/__init__.py +16 -0
  23. FlowCyPy/distribution/base_class.py +79 -0
  24. FlowCyPy/distribution/delta.py +104 -0
  25. FlowCyPy/distribution/lognormal.py +124 -0
  26. FlowCyPy/distribution/normal.py +128 -0
  27. FlowCyPy/distribution/particle_size_distribution.py +133 -0
  28. FlowCyPy/distribution/uniform.py +117 -0
  29. FlowCyPy/distribution/weibull.py +115 -0
  30. FlowCyPy/filters.py +92 -0
  31. FlowCyPy/flow_cell.py +144 -0
  32. FlowCyPy/helper.py +242 -0
  33. FlowCyPy/noises.py +87 -0
  34. FlowCyPy/particle_count.py +128 -0
  35. FlowCyPy/peak_locator/DeepPeak.py +152 -0
  36. FlowCyPy/peak_locator/__init__.py +6 -0
  37. FlowCyPy/peak_locator/base_class.py +163 -0
  38. FlowCyPy/peak_locator/basic.py +100 -0
  39. FlowCyPy/peak_locator/derivative.py +96 -0
  40. FlowCyPy/peak_locator/moving_average.py +121 -0
  41. FlowCyPy/peak_locator/scipy.py +129 -0
  42. FlowCyPy/physical_constant.py +19 -0
  43. FlowCyPy/population.py +135 -0
  44. FlowCyPy/populations_instances.py +65 -0
  45. FlowCyPy/scatterer_collection.py +305 -0
  46. FlowCyPy/signal_digitizer.py +123 -0
  47. FlowCyPy/source.py +246 -0
  48. FlowCyPy/triggered_acquisition.py +272 -0
  49. FlowCyPy/units.py +30 -0
  50. FlowCyPy/utils.py +145 -0
  51. flowcypy-0.12.0.dist-info/METADATA +319 -0
  52. flowcypy-0.12.0.dist-info/RECORD +54 -0
  53. flowcypy-0.12.0.dist-info/WHEEL +5 -0
  54. flowcypy-0.12.0.dist-info/licenses/LICENSE +21 -0
FlowCyPy/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ try:
2
+ from ._version import version as __version__ # noqa: F401
3
+
4
+ except ImportError:
5
+ __version__ = "0.0.0"
6
+
7
+ from PyMieSim.experiment.detector import CoherentMode
8
+ from PyMieSim.experiment.scatterer import Sphere
9
+ from PyMieSim.experiment.source import Gaussian, PlaneWave
10
+ from PyMieSim.experiment import Setup
11
+ from .cytometer import FlowCytometer
12
+ from .scatterer_collection import ScattererCollection, CouplingModel
13
+ from .population import Population
14
+ from .detector import Detector
15
+ from .flow_cell import FlowCell
16
+ from .source import GaussianBeam
17
+ from .noises import NoiseSetting
18
+ from .signal_digitizer import SignalDigitizer
FlowCyPy/_version.py ADDED
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '0.12.0'
21
+ __version_tuple__ = version_tuple = (0, 12, 0)
@@ -0,0 +1,182 @@
1
+ from typing import List
2
+ import warnings
3
+ import pandas as pd
4
+ from FlowCyPy import units
5
+ from FlowCyPy import dataframe_subclass
6
+ from FlowCyPy.triggered_acquisition import TriggeredAcquisitions
7
+ from FlowCyPy.binary import triggering_system
8
+ import pint_pandas
9
+
10
+ class Acquisition:
11
+ """
12
+ Represents a flow cytometry experiment, including runtime, dataframes, logging, and visualization.
13
+
14
+ Attributes
15
+ ----------
16
+ run_time : units.second
17
+ Total runtime of the experiment.
18
+ scatterer_dataframe : pd.DataFrame
19
+ DataFrame containing scatterer data, indexed by population and time.
20
+ detector_dataframe : pd.DataFrame
21
+ DataFrame containing detector signal data, indexed by detector and time.
22
+ """
23
+
24
+ def __init__(self, run_time: units.second, cytometer: object, scatterer_dataframe: pd.DataFrame, detector_dataframe: pd.DataFrame):
25
+ """
26
+ Initializes the Experiment instance.
27
+
28
+ Parameters
29
+ ----------
30
+ run_time : Quantity
31
+ Total runtime of the experiment.
32
+ scatterer_dataframe : pd.DataFrame
33
+ DataFrame with scatterer data.
34
+ detector_dataframe : pd.DataFrame
35
+ DataFrame with detector signal data.
36
+ """
37
+ self.cytometer = cytometer
38
+ self.analog = detector_dataframe
39
+ self.scatterer = scatterer_dataframe
40
+ self.run_time = run_time
41
+
42
+ @property
43
+ def n_detectors(self) -> int:
44
+ return len(self.detector_names)
45
+
46
+ @property
47
+ def detector_names(self) -> List[str]:
48
+ return self.analog.index.get_level_values('Detector').unique()
49
+
50
+ @property
51
+ def signal_digitizer(self) -> object:
52
+ return self.cytometer.signal_digitizer
53
+
54
+ def run_triggering(self,
55
+ threshold: units.Quantity,
56
+ trigger_detector_name: str,
57
+ pre_buffer: int = 64,
58
+ post_buffer: int = 64,
59
+ max_triggers: int = None) -> TriggeredAcquisitions:
60
+ """
61
+ Execute triggered acquisition analysis for signal data.
62
+
63
+ This method identifies segments of signal data based on a triggering threshold
64
+ and specified detector. It extracts segments of interest from the signal,
65
+ including a pre-trigger buffer and post-trigger buffer.
66
+
67
+ Parameters
68
+ ----------
69
+ threshold : units.Quantity
70
+ The threshold value for triggering. Only signal values exceeding this threshold
71
+ will be considered as trigger events.
72
+ trigger_detector_name : str
73
+ The name of the detector used for triggering. This determines which detector's
74
+ signal is analyzed for trigger events.
75
+ pre_buffer : int, optional
76
+ The number of points to include before the trigger point in each segment.
77
+ Default is 64.
78
+ post_buffer : int, optional
79
+ The number of points to include after the trigger point in each segment.
80
+ Default is 64.
81
+ max_triggers : int, optional
82
+ The maximum number of triggers to process. If None, all triggers will be processed.
83
+ Default is None.
84
+
85
+ Raises
86
+ ------
87
+ ValueError
88
+ If the specified `trigger_detector_name` is not found in the dataset.
89
+
90
+ Warnings
91
+ --------
92
+ UserWarning
93
+ If no triggers are detected for the specified threshold, the method raises a warning
94
+ indicating that no signals met the criteria.
95
+
96
+ Notes
97
+ -----
98
+ - The peak detection function `self.detect_peaks` is automatically called at the end of this method to analyze triggered segments.
99
+ """
100
+ # Ensure the trigger detector exists
101
+ if trigger_detector_name not in self.detector_names:
102
+ raise ValueError(f"Detector '{trigger_detector_name}' not found in dataset.")
103
+
104
+ signal_length = pre_buffer + post_buffer
105
+
106
+ # Convert threshold to plain numeric value
107
+ threshold_value = threshold.to(self.analog['Signal'].pint.units).magnitude
108
+
109
+ # Prepare detector-specific signal & time mappings for C++ function
110
+ signal_units = self.analog.xs(self.detector_names[0])['Signal'].pint.units
111
+ time_units = self.analog.xs(self.detector_names[0])['Time'].pint.units
112
+
113
+ signal_map = {det: self.analog.xs(det)['Signal'].pint.to(signal_units).pint.magnitude.to_numpy(copy=False) for det in self.detector_names}
114
+ time_map = {det: self.analog.xs(det)['Time'].pint.to(time_units).pint.magnitude.to_numpy(copy=False) for det in self.detector_names}
115
+
116
+ # Call the C++ function for fast triggering detection
117
+ times, signals, detectors, segment_ids = triggering_system.run(
118
+ signal_map=signal_map,
119
+ time_map=time_map,
120
+ trigger_detector_name=trigger_detector_name,
121
+ threshold=threshold_value,
122
+ pre_buffer=pre_buffer,
123
+ post_buffer=post_buffer,
124
+ max_triggers=max_triggers or -1
125
+ )
126
+
127
+ # Convert back to PintArray (restore units)
128
+ times = pint_pandas.PintArray(times, time_units)
129
+ signals = pint_pandas.PintArray(signals, signal_units)
130
+
131
+ # If no triggers are found, warn the user and return None
132
+ if len(times) == 0:
133
+ warnings.warn(
134
+ f"No signal met the trigger criteria. Try adjusting the threshold. "
135
+ f"Signal min-max: {self.analog['Signal'].min().to_compact()}, {self.analog['Signal'].max().to_compact()}",
136
+ UserWarning
137
+ )
138
+ return None
139
+
140
+ # Convert NumPy arrays to Pandas DataFrame
141
+ triggered_signal = pd.DataFrame({
142
+ "Time": times,
143
+ "Signal": signals,
144
+ "Detector": detectors,
145
+ "SegmentID": segment_ids
146
+ }).set_index(['Detector', 'SegmentID'])
147
+
148
+ # Create a specialized DataFrame class
149
+ triggered_signal = dataframe_subclass.TriggeredAnalogAcquisitionDataFrame(triggered_signal)
150
+
151
+ # Copy metadata attributes
152
+ triggered_signal.attrs['bit_depth'] = self.analog.attrs.get('bit_depth', None)
153
+ triggered_signal.attrs['saturation_levels'] = self.analog.attrs.get('saturation_levels', None)
154
+ triggered_signal.attrs['scatterer_dataframe'] = self.analog.attrs.get('scatterer_dataframe', None)
155
+ triggered_signal.attrs['threshold'] = {'detector': trigger_detector_name, 'value': threshold}
156
+
157
+ # Wrap inside a TriggeredAcquisitions object
158
+ triggered_acquisition = TriggeredAcquisitions(parent=self, dataframe=triggered_signal)
159
+ triggered_acquisition.scatterer = self.scatterer
160
+ triggered_acquisition.signal_length = signal_length
161
+
162
+ return triggered_acquisition
163
+
164
+
165
+ @property
166
+ def digital(self) -> pd.DataFrame:
167
+ dataframe = dataframe_subclass.DigitizedAcquisitionDataFrame(
168
+ index=self.analog.index,
169
+ data=dict(Time=self.analog.Time)
170
+ )
171
+
172
+ dataframe.attrs['saturation_levels'] = dict()
173
+ dataframe.attrs['scatterer_dataframe'] = self.analog.attrs.get('scatterer_dataframe', None)
174
+
175
+ for detector_name, group in self.analog.groupby('Detector'):
176
+ digitized_signal, _ = self.signal_digitizer.capture_signal(signal=group['Signal'])
177
+
178
+ dataframe.attrs['saturation_levels'][detector_name] = [0, self.signal_digitizer.bit_depth]
179
+
180
+ dataframe.loc[detector_name, 'Signal'] = pint_pandas.PintArray(digitized_signal, units.bit_bins)
181
+
182
+ return dataframe
File without changes
FlowCyPy/circuits.py ADDED
@@ -0,0 +1,125 @@
1
+ from abc import ABC, abstractmethod
2
+ import numpy as np
3
+ from FlowCyPy.binary import filtering
4
+ from FlowCyPy.helper import validate_units
5
+ from FlowCyPy import units
6
+
7
+ class SignalProcessor(ABC):
8
+ """
9
+ Abstract base class for signal processing operations.
10
+ """
11
+ @abstractmethod
12
+ def apply(self, signal: np.ndarray) -> None:
13
+ """
14
+ Applies a signal processing transformation **in-place**.
15
+
16
+ Parameters
17
+ ----------
18
+ signal : np.ndarray
19
+ The signal to be modified in-place.
20
+ """
21
+ pass
22
+
23
+ class BaselineRestorator(SignalProcessor):
24
+ """
25
+ Applies a baseline restoration filter by subtracting the minimum value over a given window size.
26
+
27
+ Parameters
28
+ ----------
29
+ window_size : int
30
+ Number of past samples to consider for the minimum value.
31
+ If set to -1, it acts as if the window is infinite.
32
+ """
33
+ def __init__(self, window_size: int):
34
+ self.window_size = window_size
35
+
36
+ def apply(self, signal: np.ndarray, sampling_rate: units.Quantity, **kwargs) -> np.ndarray:
37
+ """
38
+ Applies baseline restoration in-place.
39
+
40
+ Parameters
41
+ ----------
42
+ signal : np.ndarray
43
+ The signal to be modified in-place.
44
+ """
45
+ window_size_bin = int((self.window_size * sampling_rate).to('').magnitude)
46
+ filtering.apply_baseline_restoration(signal=signal, window_size=window_size_bin)
47
+
48
+ return signal
49
+
50
+
51
+ class BesselLowPass(SignalProcessor):
52
+ """
53
+ Applies a Bessel low-pass filter to smooth the signal.
54
+
55
+ Parameters
56
+ ----------
57
+ cutoff : float
58
+ The cutoff frequency for the filter.
59
+ order : int
60
+ The order of the filter.
61
+ gain : float
62
+ The gain applied after filtering.
63
+ """
64
+ @validate_units(cutoff=units.hertz)
65
+ def __init__(self, cutoff: units.Quantity, order: int, gain: float):
66
+ self.cutoff = cutoff
67
+ self.order = order
68
+ self.gain = gain
69
+
70
+ def apply(self, signal: np.ndarray, sampling_rate: units.Quantity, **kwargs) -> None:
71
+ """
72
+ Applies Bessel low-pass filtering in-place.
73
+
74
+ Parameters
75
+ ----------
76
+ signal : np.ndarray
77
+ The signal to be modified in-place.
78
+ """
79
+ filtering.apply_bessel_lowpass_filter(
80
+ signal=signal,
81
+ sampling_rate=sampling_rate.to('hertz').magnitude,
82
+ cutoff=self.cutoff.to('hertz').magnitude,
83
+ order=self.order,
84
+ gain=self.gain
85
+ )
86
+
87
+ return signal
88
+
89
+ class ButterworthlLowPass(SignalProcessor):
90
+ """
91
+ Applies a Bessel low-pass filter to smooth the signal.
92
+
93
+ Parameters
94
+ ----------
95
+ cutoff : float
96
+ The cutoff frequency for the filter.
97
+ order : int
98
+ The order of the filter.
99
+ gain : float
100
+ The gain applied after filtering.
101
+ """
102
+ @validate_units(cutoff=units.hertz)
103
+ def __init__(self, cutoff: units.Quantity, order: int, gain: float):
104
+ self.cutoff = cutoff
105
+ self.order = order
106
+ self.gain = gain
107
+
108
+ def apply(self, signal: np.ndarray, sampling_rate: units.Quantity, **kwargs) -> None:
109
+ """
110
+ Applies Bessel low-pass filtering in-place.
111
+
112
+ Parameters
113
+ ----------
114
+ signal : np.ndarray
115
+ The signal to be modified in-place.
116
+ """
117
+ filtering.apply_butterworth_lowpass_filter(
118
+ signal=signal,
119
+ sampling_rate=sampling_rate.to('hertz').magnitude,
120
+ cutoff=self.cutoff.to('hertz').magnitude,
121
+ order=self.order,
122
+ gain=self.gain
123
+ )
124
+
125
+ return signal
FlowCyPy/classifier.py ADDED
@@ -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 FlowCyPy.dataframe_subclass import ClassifierDataFrame
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 ClassifierDataFrame(dataframe)
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 ClassifierDataFrame(dataframe)
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 ClassifierDataFrame(dataframe)
@@ -0,0 +1,4 @@
1
+ from . import uniform
2
+ from . import rayleigh
3
+ from . import mie
4
+ from . import empirical
@@ -0,0 +1,47 @@
1
+ import numpy as np
2
+ from FlowCyPy import ScattererCollection, Detector
3
+ from FlowCyPy.source import BaseBeam
4
+ from FlowCyPy.units import watt, meter
5
+
6
+
7
+ def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer: ScattererCollection, granularity: float = 1.0, A: float = 1.5, n: float = 2.0) -> float:
8
+ """
9
+ Empirical model for scattering intensity based on particle size, granularity, and detector angle.
10
+
11
+ This function models forward scatter (FSC) as proportional to the particle's size squared and
12
+ side scatter (SSC) as proportional to the granularity and modulated by angular dependence
13
+ (sin^n(theta)). Granularity is a dimensionless measure of the particle's internal complexity or
14
+ surface irregularities:
15
+
16
+ - A default value of 1.0 is used for moderate granularity (e.g., typical white blood cells).
17
+ - Granularity values < 1.0 represent smoother particles with less internal complexity (e.g., bacteria).
18
+ - Granularity values > 1.0 represent particles with higher internal complexity or surface irregularities (e.g., granulocytes).
19
+
20
+ Parameters
21
+ ----------
22
+ detector : Detector
23
+ The detector object containing theta_angle (in radians).
24
+ particle_size : float
25
+ The size of the particle (in meters).
26
+ granularity : float, optional
27
+ A measure of the particle's internal complexity or surface irregularities (dimensionless).
28
+ Default is 1.0.
29
+ A : float, optional
30
+ Empirical scaling factor for angular dependence. Default is 1.5.
31
+ n : float, optional
32
+ Power of sine function for angular dependence. Default is 2.0.
33
+
34
+ Returns
35
+ -------
36
+ float
37
+ The detected scattering intensity for the given particle and detector.
38
+ """
39
+ size_list = scatterer.dataframe['Size'].pint.to(meter).values.numpy_data
40
+
41
+ # Forward scatter is proportional to size^2
42
+ fsc_intensity = size_list**2
43
+
44
+ # Side scatter is proportional to granularity and modulated by angular dependence
45
+ ssc_intensity = granularity * (1 + A * np.sin(np.radians(detector.phi_angle))**n) * np.ones_like(size_list)
46
+
47
+ return fsc_intensity * watt if detector.phi_angle < np.radians(10) else ssc_intensity * watt