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.
- FlowCyPy/__init__.py +18 -0
- FlowCyPy/_version.py +21 -0
- FlowCyPy/acquisition.py +182 -0
- FlowCyPy/binary/__init__.py +0 -0
- FlowCyPy/binary/filtering.cp310-win_amd64.pyd +0 -0
- FlowCyPy/binary/filtering.cp311-win_amd64.pyd +0 -0
- FlowCyPy/binary/filtering.cp312-win_amd64.pyd +0 -0
- FlowCyPy/binary/triggering_system.cp310-win_amd64.pyd +0 -0
- FlowCyPy/binary/triggering_system.cp311-win_amd64.pyd +0 -0
- FlowCyPy/binary/triggering_system.cp312-win_amd64.pyd +0 -0
- FlowCyPy/circuits.py +125 -0
- FlowCyPy/classifier.py +182 -0
- FlowCyPy/coupling_mechanism/__init__.py +4 -0
- FlowCyPy/coupling_mechanism/empirical.py +47 -0
- FlowCyPy/coupling_mechanism/mie.py +223 -0
- FlowCyPy/coupling_mechanism/rayleigh.py +116 -0
- FlowCyPy/coupling_mechanism/uniform.py +40 -0
- FlowCyPy/cytometer.py +391 -0
- FlowCyPy/dataframe_subclass.py +679 -0
- FlowCyPy/detector.py +282 -0
- FlowCyPy/directories.py +36 -0
- FlowCyPy/distribution/__init__.py +16 -0
- FlowCyPy/distribution/base_class.py +79 -0
- FlowCyPy/distribution/delta.py +104 -0
- FlowCyPy/distribution/lognormal.py +124 -0
- FlowCyPy/distribution/normal.py +128 -0
- FlowCyPy/distribution/particle_size_distribution.py +133 -0
- FlowCyPy/distribution/uniform.py +117 -0
- FlowCyPy/distribution/weibull.py +115 -0
- FlowCyPy/filters.py +92 -0
- FlowCyPy/flow_cell.py +144 -0
- FlowCyPy/helper.py +242 -0
- FlowCyPy/noises.py +87 -0
- FlowCyPy/particle_count.py +128 -0
- FlowCyPy/peak_locator/DeepPeak.py +152 -0
- FlowCyPy/peak_locator/__init__.py +6 -0
- FlowCyPy/peak_locator/base_class.py +163 -0
- FlowCyPy/peak_locator/basic.py +100 -0
- FlowCyPy/peak_locator/derivative.py +96 -0
- FlowCyPy/peak_locator/moving_average.py +121 -0
- FlowCyPy/peak_locator/scipy.py +129 -0
- FlowCyPy/physical_constant.py +19 -0
- FlowCyPy/population.py +135 -0
- FlowCyPy/populations_instances.py +65 -0
- FlowCyPy/scatterer_collection.py +305 -0
- FlowCyPy/signal_digitizer.py +123 -0
- FlowCyPy/source.py +246 -0
- FlowCyPy/triggered_acquisition.py +272 -0
- FlowCyPy/units.py +30 -0
- FlowCyPy/utils.py +145 -0
- flowcypy-0.12.0.dist-info/METADATA +319 -0
- flowcypy-0.12.0.dist-info/RECORD +54 -0
- flowcypy-0.12.0.dist-info/WHEEL +5 -0
- 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)
|
FlowCyPy/acquisition.py
ADDED
|
@@ -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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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,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
|