FlowCyPy 0.5.15__tar.gz → 0.6.0__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.
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/_version.py +2 -2
- flowcypy-0.6.0/FlowCyPy/coupling_mechanism/mie.py +207 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/coupling_mechanism/rayleigh.py +8 -7
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/coupling_mechanism/uniform.py +2 -2
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/cytometer.py +87 -176
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/detector.py +85 -264
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/distribution/base_class.py +16 -2
- flowcypy-0.6.0/FlowCyPy/distribution/delta.py +104 -0
- flowcypy-0.6.0/FlowCyPy/distribution/lognormal.py +124 -0
- flowcypy-0.6.0/FlowCyPy/distribution/normal.py +128 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/distribution/particle_size_distribution.py +46 -17
- flowcypy-0.6.0/FlowCyPy/distribution/uniform.py +117 -0
- flowcypy-0.6.0/FlowCyPy/distribution/weibull.py +115 -0
- flowcypy-0.6.0/FlowCyPy/experiment.py +398 -0
- flowcypy-0.6.0/FlowCyPy/flow_cell.py +198 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/helper.py +0 -4
- flowcypy-0.6.0/FlowCyPy/logger.py +136 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/particle_count.py +16 -5
- flowcypy-0.6.0/FlowCyPy/plottings.py +269 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/population.py +23 -26
- flowcypy-0.6.0/FlowCyPy/report.py +236 -0
- flowcypy-0.6.0/FlowCyPy/scatterer_collection.py +299 -0
- flowcypy-0.6.0/FlowCyPy/signal_digitizer.py +179 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy.egg-info/PKG-INFO +2 -2
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy.egg-info/SOURCES.txt +14 -1
- {flowcypy-0.5.15 → flowcypy-0.6.0}/PKG-INFO +2 -2
- flowcypy-0.6.0/developments/doc/canto_spec.md +61 -0
- flowcypy-0.6.0/developments/get_started.md +23 -0
- flowcypy-0.6.0/developments/image.png +0 -0
- flowcypy-0.6.0/developments/output_file.prof +0 -0
- flowcypy-0.6.0/developments/scripts/dev_stats_0.py +91 -0
- flowcypy-0.6.0/developments/scripts/dev_stats_1.py +70 -0
- flowcypy-0.6.0/developments/scripts/dev_stats_2.py +17 -0
- flowcypy-0.6.0/developments/scripts/temp.py +35 -0
- flowcypy-0.6.0/developments/test.pdf +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/density_plots/1_populations.py +18 -23
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/density_plots/2_populations.py +24 -32
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/density_plots/3_populations.py +19 -23
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/density_plots/custom_populations.py +22 -21
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/extras/distributions.py +2 -2
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/extras/flow_cytometer_signal.py +19 -32
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/extras/full_workflow.py +21 -29
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/extras/scatterer_distribution.py +6 -22
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/noise_sources/dark_current.py +20 -7
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/noise_sources/shot_noise.py +24 -8
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/noise_sources/thermal.py +23 -11
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/tutorials/limit_of_detection.py +21 -31
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/tutorials/workflow.py +44 -44
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/sg_execution_times.rst +24 -21
- {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_coupling_mechanism.py +31 -15
- {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_detector_noise.py +47 -24
- {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_distribution.py +5 -5
- {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_flow_cytometer.py +26 -27
- {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_noises.py +36 -13
- {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_peak_analyzer.py +46 -53
- {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_population.py +32 -25
- {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_scatterer_distribution.py +40 -31
- flowcypy-0.5.15/FlowCyPy/distribution/delta.py +0 -79
- flowcypy-0.5.15/FlowCyPy/distribution/lognormal.py +0 -87
- flowcypy-0.5.15/FlowCyPy/distribution/normal.py +0 -88
- flowcypy-0.5.15/FlowCyPy/distribution/uniform.py +0 -88
- flowcypy-0.5.15/FlowCyPy/distribution/weibull.py +0 -75
- flowcypy-0.5.15/FlowCyPy/flow_cell.py +0 -295
- flowcypy-0.5.15/FlowCyPy/logger.py +0 -322
- flowcypy-0.5.15/FlowCyPy/scatterer_collection.py +0 -299
- flowcypy-0.5.15/developments/scripts/dev_temp.py +0 -96
- {flowcypy-0.5.15 → flowcypy-0.6.0}/.flake8 +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/.github/dependabot.yml +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/.github/workflows/deploy_PyPi.yml +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/.github/workflows/deploy_anaconda.yml +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/.github/workflows/deploy_coverage.yml +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/.github/workflows/deploy_documentation.yml +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/.gitignore +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/__init__.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/classifier.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/coupling_mechanism/__init__.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/coupling_mechanism/empirical.py +0 -0
- /flowcypy-0.5.15/FlowCyPy/coupling_mechanism/mie.py → /flowcypy-0.6.0/FlowCyPy/coupling_mechanism.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/directories.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/distribution/__init__.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/event_correlator.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/noises.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/peak_locator/__init__.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/peak_locator/base_class.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/peak_locator/basic.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/peak_locator/derivative.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/peak_locator/moving_average.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/physical_constant.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/populations_instances.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/source.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/units.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy/utils.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy.egg-info/dependency_links.txt +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy.egg-info/requires.txt +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/FlowCyPy.egg-info/top_level.txt +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/LICENSE +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/README.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/Untitled.ipynb +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/doc/internship.pdf +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/concentration_comparison.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/create_images.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/data_analysis.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/dev_beads_analysis.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/dev_canto.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/dev_classifier.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/dev_shot_noise_check.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/dev_study_on_ri.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/dev_study_on_size.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/mat2csv.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/developments/scripts/profiler.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/Makefile +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/density_plots/README.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/extras/README.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/noise_sources/README.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/examples/tutorials/README.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/distributions/Delta.png +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/distributions/LogNormal.png +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/distributions/Normal.png +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/distributions/RosinRammler.png +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/distributions/Uniform.png +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/distributions/Weibull.png +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/example_0.png +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/example_1.png +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/example_2.png +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/example_3.png +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/flow_cytometer.png +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/images/logo.png +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/make.bat +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/_static/default.css +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/_static/logo.png +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/_static/thumbnail.png +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/analysis.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/base.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/detector.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/distributions.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/flow_cell.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/flow_cytometer.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/peak_locator.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/scatterer.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code/source.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/code.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/conf.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/examples.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/index.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/core_components.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/getting_started.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/objectives/main.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/objectives/pre.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/objectives/stretch.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/prerequisites/index.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/prerequisites/mathematics.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/prerequisites/optics.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/prerequisites/programming.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/ressources.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal/tasks.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/internal.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/references.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/docs/source/theory.rst +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/meta.yaml +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/notebook.ipynb +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/pyproject.toml +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/setup.cfg +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/__init__.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_extra.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_peak_algorithm.py +0 -0
- {flowcypy-0.5.15 → flowcypy-0.6.0}/tests/test_source.py +0 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from FlowCyPy import ScattererCollection, Detector
|
|
3
|
+
from FlowCyPy.source import BaseBeam
|
|
4
|
+
from PyMieSim.experiment.scatterer import Sphere as PMS_SPHERE
|
|
5
|
+
from PyMieSim.experiment.source import PlaneWave
|
|
6
|
+
from PyMieSim.experiment.detector import Photodiode as PMS_PHOTODIODE
|
|
7
|
+
from PyMieSim.experiment import Setup
|
|
8
|
+
from PyMieSim.units import Quantity, degree, watt, AU, hertz
|
|
9
|
+
from FlowCyPy.noises import NoiseSetting
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def apply_rin_noise(source: BaseBeam, total_size: int, bandwidth: float) -> np.ndarray:
|
|
15
|
+
r"""
|
|
16
|
+
Applies Relative Intensity Noise (RIN) to the source amplitude if enabled, accounting for detection bandwidth.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
source : BaseBeam
|
|
21
|
+
The light source containing amplitude and RIN information.
|
|
22
|
+
total_size : int
|
|
23
|
+
The number of particles being simulated.
|
|
24
|
+
bandwidth : float
|
|
25
|
+
The detection bandwidth in Hz.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
np.ndarray
|
|
30
|
+
Array of amplitudes with RIN noise applied.
|
|
31
|
+
|
|
32
|
+
Equations
|
|
33
|
+
---------
|
|
34
|
+
1. Relative Intensity Noise (RIN):
|
|
35
|
+
RIN quantifies the fluctuations in the laser's intensity relative to its mean intensity.
|
|
36
|
+
RIN is typically specified as a power spectral density (PSD) in units of dB/Hz:
|
|
37
|
+
\[
|
|
38
|
+
\text{RIN (dB/Hz)} = 10 \cdot \log_{10}\left(\frac{\text{Noise Power (per Hz)}}{\text{Mean Power}}\right)
|
|
39
|
+
\]
|
|
40
|
+
|
|
41
|
+
2. Conversion from dB/Hz to Linear Scale:
|
|
42
|
+
To compute noise power, RIN must be converted from dB to a linear scale:
|
|
43
|
+
\[
|
|
44
|
+
\text{RIN (linear)} = 10^{\text{RIN (dB/Hz)} / 10}
|
|
45
|
+
\]
|
|
46
|
+
|
|
47
|
+
3. Total Noise Power:
|
|
48
|
+
The total noise power depends on the bandwidth (\(B\)) of the detection system:
|
|
49
|
+
\[
|
|
50
|
+
P_{\text{noise}} = \text{RIN (linear)} \cdot B
|
|
51
|
+
\]
|
|
52
|
+
|
|
53
|
+
4. Standard Deviation of Amplitude Fluctuations:
|
|
54
|
+
The noise standard deviation for amplitude is derived from the total noise power:
|
|
55
|
+
\[
|
|
56
|
+
\sigma_{\text{amplitude}} = \sqrt{P_{\text{noise}}} \cdot \text{Amplitude}
|
|
57
|
+
\]
|
|
58
|
+
Substituting \(P_{\text{noise}}\), we get:
|
|
59
|
+
\[
|
|
60
|
+
\sigma_{\text{amplitude}} = \sqrt{\text{RIN (linear)} \cdot B} \cdot \text{Amplitude}
|
|
61
|
+
\]
|
|
62
|
+
|
|
63
|
+
Implementation
|
|
64
|
+
--------------
|
|
65
|
+
- The RIN value from the source is converted to linear scale using:
|
|
66
|
+
\[
|
|
67
|
+
\text{RIN (linear)} = 10^{\text{source.RIN} / 10}
|
|
68
|
+
\]
|
|
69
|
+
- The noise standard deviation is scaled by the detection bandwidth (\(B\)) in Hz:
|
|
70
|
+
\[
|
|
71
|
+
\sigma_{\text{amplitude}} = \sqrt{\text{RIN (linear)} \cdot B} \cdot \text{source.amplitude}
|
|
72
|
+
\]
|
|
73
|
+
- Gaussian noise with mean \(0\) and standard deviation \(\sigma_{\text{amplitude}}\) is applied to the source amplitude.
|
|
74
|
+
|
|
75
|
+
Notes
|
|
76
|
+
-----
|
|
77
|
+
- The bandwidth parameter (\(B\)) must be in Hz and reflects the frequency range of the detection system.
|
|
78
|
+
- The function assumes that RIN is specified in dB/Hz. If RIN is already in linear scale, the conversion step can be skipped.
|
|
79
|
+
"""
|
|
80
|
+
amplitude_with_rin = np.ones(total_size) * source.amplitude
|
|
81
|
+
|
|
82
|
+
if NoiseSetting.include_RIN_noise and NoiseSetting.include_noises:
|
|
83
|
+
# Convert RIN from dB/Hz to linear scale if necessary
|
|
84
|
+
rin_linear = 10**(source.RIN / 10)
|
|
85
|
+
|
|
86
|
+
# Compute noise standard deviation, scaled by bandwidth
|
|
87
|
+
std_dev_amplitude = np.sqrt(rin_linear * bandwidth.to(hertz).magnitude) * source.amplitude
|
|
88
|
+
|
|
89
|
+
# Apply Gaussian noise to the amplitude
|
|
90
|
+
amplitude_with_rin += np.random.normal(
|
|
91
|
+
loc=0,
|
|
92
|
+
scale=std_dev_amplitude.to(source.amplitude.units).magnitude,
|
|
93
|
+
size=total_size
|
|
94
|
+
) * source.amplitude.units
|
|
95
|
+
|
|
96
|
+
return amplitude_with_rin
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def initialize_scatterer(scatterer_dataframe: pd.DataFrame, source: PlaneWave, medium_refractive_index: Quantity) -> PMS_SPHERE:
|
|
100
|
+
"""
|
|
101
|
+
Initializes the scatterer object for the PyMieSim experiment.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
scatterer : ScattererCollection
|
|
106
|
+
The scatterer object containing particle data.
|
|
107
|
+
source : PlaneWave
|
|
108
|
+
The light source for the simulation.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
PMS_SPHERE
|
|
113
|
+
Initialized scatterer for the experiment.
|
|
114
|
+
"""
|
|
115
|
+
size_list = scatterer_dataframe['Size'].values
|
|
116
|
+
ri_list = scatterer_dataframe['RefractiveIndex'].values
|
|
117
|
+
|
|
118
|
+
if len(size_list) == 0:
|
|
119
|
+
raise ValueError("ScattererCollection size list is empty.")
|
|
120
|
+
|
|
121
|
+
size_list = size_list.quantity.magnitude * size_list.units
|
|
122
|
+
ri_list = ri_list.quantity.magnitude * ri_list.units
|
|
123
|
+
|
|
124
|
+
return PMS_SPHERE(
|
|
125
|
+
diameter=size_list,
|
|
126
|
+
property=ri_list,
|
|
127
|
+
medium_property=np.ones(len(size_list)) * medium_refractive_index,
|
|
128
|
+
source=source
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def initialize_detector(detector: Detector, total_size: int) -> PMS_PHOTODIODE:
|
|
133
|
+
"""
|
|
134
|
+
Initializes the detector object for the PyMieSim experiment.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
detector : Detector
|
|
139
|
+
The detector object containing configuration data.
|
|
140
|
+
total_size : int
|
|
141
|
+
The number of particles being simulated.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
PMS_PHOTODIODE
|
|
146
|
+
Initialized detector for the experiment.
|
|
147
|
+
"""
|
|
148
|
+
ONES = np.ones(total_size)
|
|
149
|
+
|
|
150
|
+
return PMS_PHOTODIODE(
|
|
151
|
+
NA=ONES * detector.numerical_aperture,
|
|
152
|
+
cache_NA=ONES * 0 * AU,
|
|
153
|
+
gamma_offset=ONES * detector.gamma_angle,
|
|
154
|
+
phi_offset=ONES * detector.phi_angle,
|
|
155
|
+
polarization_filter=ONES * np.nan * degree,
|
|
156
|
+
sampling=ONES * detector.sampling
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer_dataframe: pd.DataFrame, medium_refractive_index: Quantity) -> np.ndarray:
|
|
161
|
+
"""
|
|
162
|
+
Computes the detected signal by analyzing the scattering properties of particles.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
source : BaseBeam
|
|
167
|
+
The light source object containing wavelength, power, and other optical properties.
|
|
168
|
+
detector : Detector
|
|
169
|
+
The detector object containing properties such as numerical aperture and angles.
|
|
170
|
+
scatterer : ScattererCollection
|
|
171
|
+
The scatterer object containing particle size and refractive index data.
|
|
172
|
+
tolerance : float, optional
|
|
173
|
+
The tolerance for deciding if two values of size and refractive index are "close enough" to be cached.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
np.ndarray
|
|
178
|
+
Array of coupling values for each particle, based on the detected signal.
|
|
179
|
+
"""
|
|
180
|
+
size_list = scatterer_dataframe['Size'].values
|
|
181
|
+
|
|
182
|
+
if len(size_list) == 0:
|
|
183
|
+
return np.array([]) * watt
|
|
184
|
+
|
|
185
|
+
total_size = len(size_list)
|
|
186
|
+
amplitude_with_rin = apply_rin_noise(source, total_size, detector.signal_digitizer.bandwidth)
|
|
187
|
+
|
|
188
|
+
pms_source = PlaneWave(
|
|
189
|
+
wavelength=np.ones(total_size) * source.wavelength,
|
|
190
|
+
polarization=np.ones(total_size) * 0 * degree,
|
|
191
|
+
amplitude=amplitude_with_rin
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
pms_scatterer = initialize_scatterer(scatterer_dataframe, pms_source, medium_refractive_index)
|
|
195
|
+
pms_detector = initialize_detector(detector, total_size)
|
|
196
|
+
|
|
197
|
+
# Configure the detector
|
|
198
|
+
pms_detector.mode_number = ['NC00'] * total_size
|
|
199
|
+
pms_detector.rotation = np.ones(total_size) * 0 * degree
|
|
200
|
+
pms_detector.__post_init__()
|
|
201
|
+
|
|
202
|
+
# Set up the experiment
|
|
203
|
+
experiment = Setup(source=pms_source, scatterer=pms_scatterer, detector=pms_detector)
|
|
204
|
+
|
|
205
|
+
# Compute coupling values
|
|
206
|
+
coupling_value = experiment.get_sequential('coupling').squeeze()
|
|
207
|
+
return np.atleast_1d(coupling_value) * watt
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import numpy as np
|
|
3
3
|
from FlowCyPy import ScattererCollection, Detector
|
|
4
4
|
from FlowCyPy.source import BaseBeam
|
|
5
|
-
from FlowCyPy.units import meter
|
|
5
|
+
from FlowCyPy.units import meter, Quantity
|
|
6
|
+
import pandas as pd
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
def compute_scattering_cross_section(
|
|
9
|
+
def compute_scattering_cross_section(scatterer_dataframe: pd.DataFrame, source: BaseBeam, detector: Detector) -> np.ndarray:
|
|
9
10
|
r"""
|
|
10
11
|
Computes the Rayleigh scattering cross-section for a spherical particle with angle dependency.
|
|
11
12
|
|
|
@@ -40,9 +41,8 @@ def compute_scattering_cross_section(scatterer: ScattererCollection, source: Bas
|
|
|
40
41
|
np.ndarray
|
|
41
42
|
The angle-dependent Rayleigh scattering cross-section (in square meters, m²).
|
|
42
43
|
"""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
ri_list = scatterer.dataframe['RefractiveIndex'].values.numpy_data
|
|
44
|
+
size_list = scatterer_dataframe['Size'].pint.to(meter).values.numpy_data
|
|
45
|
+
ri_list = scatterer_dataframe['RefractiveIndex'].values.numpy_data
|
|
46
46
|
|
|
47
47
|
# Extract properties
|
|
48
48
|
wavelength = source.wavelength
|
|
@@ -63,7 +63,8 @@ def compute_scattering_cross_section(scatterer: ScattererCollection, source: Bas
|
|
|
63
63
|
return cross_section.magnitude * meter**2
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer: ScattererCollection) -> float:
|
|
66
|
+
# def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer: ScattererCollection) -> float:
|
|
67
|
+
def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer_dataframe: pd.DataFrame, medium_refractive_index: Quantity) -> np.ndarray:
|
|
67
68
|
r"""
|
|
68
69
|
Computes the power detected by a detector from a Rayleigh scattering event.
|
|
69
70
|
|
|
@@ -98,7 +99,7 @@ def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer: Sca
|
|
|
98
99
|
"""
|
|
99
100
|
scattering_cross_section = compute_scattering_cross_section(
|
|
100
101
|
source=source,
|
|
101
|
-
|
|
102
|
+
scatterer_dataframe=scatterer_dataframe,
|
|
102
103
|
detector=detector
|
|
103
104
|
)
|
|
104
105
|
|
|
@@ -3,7 +3,7 @@ from FlowCyPy import ScattererCollection, Detector, ureg
|
|
|
3
3
|
from FlowCyPy.source import BaseBeam
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def compute_detected_signal(source: BaseBeam, detector: Detector,
|
|
6
|
+
def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer_dataframe: ScattererCollection, medium_refractive_index) -> np.ndarray:
|
|
7
7
|
r"""
|
|
8
8
|
Computes the power detected by a detector from a uniform distribution.
|
|
9
9
|
|
|
@@ -36,4 +36,4 @@ def compute_detected_signal(source: BaseBeam, detector: Detector, scatterer: Sca
|
|
|
36
36
|
float
|
|
37
37
|
The power detected by the detector (in watts, W).
|
|
38
38
|
"""
|
|
39
|
-
return np.ones(len(
|
|
39
|
+
return np.ones(len(scatterer_dataframe)) * ureg.watt
|
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
import logging
|
|
5
5
|
import numpy as np
|
|
6
|
-
import matplotlib.pyplot as plt
|
|
7
6
|
from typing import List, Callable, Optional
|
|
8
7
|
from MPSPlots.styles import mps
|
|
9
8
|
from FlowCyPy.flow_cell import FlowCell
|
|
10
9
|
from FlowCyPy.detector import Detector
|
|
11
10
|
import pandas as pd
|
|
12
11
|
import pint_pandas
|
|
12
|
+
from FlowCyPy import units
|
|
13
13
|
from FlowCyPy.units import Quantity, milliwatt
|
|
14
|
-
from FlowCyPy.
|
|
15
|
-
import seaborn as sns
|
|
14
|
+
from FlowCyPy.experiment import Experiment
|
|
16
15
|
|
|
17
16
|
# Set up logging configuration
|
|
18
17
|
logging.basicConfig(
|
|
@@ -20,7 +19,6 @@ logging.basicConfig(
|
|
|
20
19
|
format='%(levelname)s - %(message)s'
|
|
21
20
|
)
|
|
22
21
|
|
|
23
|
-
|
|
24
22
|
class FlowCytometer:
|
|
25
23
|
"""
|
|
26
24
|
A simulation class for modeling flow cytometer signals, including Forward Scatter (FSC) and Side Scatter (SSC) channels.
|
|
@@ -65,23 +63,26 @@ class FlowCytometer:
|
|
|
65
63
|
"""
|
|
66
64
|
def __init__(
|
|
67
65
|
self,
|
|
66
|
+
scatterer_collection: object,
|
|
68
67
|
flow_cell: FlowCell,
|
|
69
68
|
detectors: List[Detector],
|
|
70
69
|
coupling_mechanism: Optional[str] = 'mie',
|
|
71
70
|
background_power: Optional[Quantity] = 0 * milliwatt):
|
|
72
71
|
|
|
72
|
+
self.scatterer_collection = scatterer_collection
|
|
73
73
|
self.flow_cell = flow_cell
|
|
74
|
-
self.scatterer_collection = flow_cell.scatterer_collection
|
|
75
74
|
self.source = flow_cell.source
|
|
76
75
|
self.detectors = detectors
|
|
77
76
|
self.coupling_mechanism = coupling_mechanism
|
|
78
77
|
self.background_power = background_power
|
|
79
|
-
self.plot = self.PlotInterface(self)
|
|
80
78
|
|
|
81
79
|
assert len(self.detectors) == 2, 'For now, FlowCytometer can only take two detectors for the analysis.'
|
|
82
80
|
assert self.detectors[0].name != self.detectors[1].name, 'Both detectors cannot have the same name'
|
|
83
81
|
|
|
84
|
-
|
|
82
|
+
for detector in detectors:
|
|
83
|
+
detector.cytometer = self
|
|
84
|
+
|
|
85
|
+
def run_coupling_analysis(self, scatterer_dataframe: pd.DataFrame) -> None:
|
|
85
86
|
"""
|
|
86
87
|
Computes and assigns the optical coupling power for each particle-detection event.
|
|
87
88
|
|
|
@@ -106,14 +107,39 @@ class FlowCytometer:
|
|
|
106
107
|
self.coupling_power = detection_mechanism(
|
|
107
108
|
source=self.source,
|
|
108
109
|
detector=detector,
|
|
109
|
-
|
|
110
|
+
scatterer_dataframe=scatterer_dataframe,
|
|
111
|
+
medium_refractive_index=self.scatterer_collection.medium_refractive_index
|
|
110
112
|
)
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
scatterer_dataframe["detector: " + detector.name] = pint_pandas.PintArray(self.coupling_power, dtype=self.coupling_power.units)
|
|
115
|
+
|
|
116
|
+
def _generate_pulse_parameters(self, scatterer_dataframe: pd.DataFrame) -> None:
|
|
117
|
+
"""
|
|
118
|
+
Generates and assigns random Gaussian pulse parameters for each particle event.
|
|
119
|
+
|
|
120
|
+
The generated parameters include:
|
|
121
|
+
- Centers: The time at which each pulse occurs.
|
|
122
|
+
- Widths: The standard deviation (spread) of each pulse in seconds.
|
|
123
|
+
|
|
124
|
+
Effects
|
|
125
|
+
-------
|
|
126
|
+
scatterer_collection.dataframe : pandas.DataFrame
|
|
127
|
+
Adds a 'Widths' column with computed pulse widths for each particle.
|
|
128
|
+
Uses the flow speed and beam waist to calculate pulse widths.
|
|
129
|
+
"""
|
|
130
|
+
columns = pd.MultiIndex.from_product(
|
|
131
|
+
[[p.name for p in self.detectors], ['Centers', 'Heights']]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self.pulse_dataframe = pd.DataFrame(columns=columns)
|
|
135
|
+
|
|
136
|
+
self.pulse_dataframe['Centers'] = scatterer_dataframe['Time']
|
|
113
137
|
|
|
114
|
-
self.
|
|
138
|
+
widths = self.source.waist / self.flow_cell.flow_speed * np.ones(len(scatterer_dataframe))
|
|
115
139
|
|
|
116
|
-
|
|
140
|
+
scatterer_dataframe['Widths'] = pint_pandas.PintArray(widths, dtype=widths.units)
|
|
141
|
+
|
|
142
|
+
def initialize_signal(self, run_time: Quantity) -> None:
|
|
117
143
|
"""
|
|
118
144
|
Initializes the raw signal for each detector based on the source and flow cell configuration.
|
|
119
145
|
|
|
@@ -126,12 +152,19 @@ class FlowCytometer:
|
|
|
126
152
|
based on the flow cell's runtime.
|
|
127
153
|
|
|
128
154
|
"""
|
|
155
|
+
dataframes = []
|
|
156
|
+
|
|
129
157
|
# Initialize the detectors
|
|
130
158
|
for detector in self.detectors:
|
|
131
|
-
|
|
132
|
-
|
|
159
|
+
dataframe = detector.get_initialized_signal(run_time=run_time)
|
|
160
|
+
|
|
161
|
+
dataframes.append(dataframe)
|
|
162
|
+
|
|
163
|
+
self.dataframe = pd.concat(dataframes, keys=[d.name for d in self.detectors])
|
|
133
164
|
|
|
134
|
-
|
|
165
|
+
self.dataframe.index.names = ["Detector", "Index"]
|
|
166
|
+
|
|
167
|
+
def get_continous_acquisition(self, run_time: Quantity) -> None:
|
|
135
168
|
"""
|
|
136
169
|
Simulates the generation of optical signal pulses for each particle event.
|
|
137
170
|
|
|
@@ -150,24 +183,41 @@ class FlowCytometer:
|
|
|
150
183
|
ValueError
|
|
151
184
|
If the scatterer collection lacks required data columns ('Widths', 'Time').
|
|
152
185
|
"""
|
|
153
|
-
|
|
186
|
+
if not run_time.check('second'):
|
|
187
|
+
raise ValueError(f"flow_speed must be in meter per second, but got {run_time.units}")
|
|
188
|
+
|
|
189
|
+
self.initialize_signal(run_time=run_time)
|
|
190
|
+
|
|
191
|
+
scatterer_dataframe = self.flow_cell.generate_event_dataframe(self.scatterer_collection.populations, run_time=run_time)
|
|
192
|
+
|
|
193
|
+
self.scatterer_collection.fill_dataframe_with_sampling(scatterer_dataframe)
|
|
154
194
|
|
|
155
|
-
|
|
156
|
-
|
|
195
|
+
self.run_coupling_analysis(scatterer_dataframe)
|
|
196
|
+
|
|
197
|
+
self._generate_pulse_parameters(scatterer_dataframe)
|
|
198
|
+
|
|
199
|
+
self.scatterer_collection.dataframe = scatterer_dataframe
|
|
200
|
+
|
|
201
|
+
_widths = scatterer_dataframe['Widths'].pint.to('second').pint.quantity.magnitude
|
|
202
|
+
_centers = scatterer_dataframe['Time'].pint.to('second').pint.quantity.magnitude
|
|
157
203
|
|
|
158
204
|
for detector in self.detectors:
|
|
159
|
-
_coupling_power =
|
|
205
|
+
_coupling_power = scatterer_dataframe["detector: " + detector.name].values
|
|
206
|
+
|
|
207
|
+
detector_signal = self.dataframe.xs(detector.name)['Signal']
|
|
160
208
|
|
|
161
209
|
# Generate noise components
|
|
162
|
-
detector._add_thermal_noise_to_raw_signal()
|
|
210
|
+
detector._add_thermal_noise_to_raw_signal(signal=detector_signal)
|
|
163
211
|
|
|
164
|
-
detector._add_dark_current_noise_to_raw_signal()
|
|
212
|
+
detector._add_dark_current_noise_to_raw_signal(signal=detector_signal)
|
|
165
213
|
|
|
166
214
|
# Broadcast the time array to the shape of (number of signals, len(detector.time))
|
|
167
|
-
|
|
215
|
+
time = self.dataframe.xs(detector.name)['Time'].pint.magnitude
|
|
168
216
|
|
|
169
|
-
|
|
170
|
-
|
|
217
|
+
time_grid = np.expand_dims(time, axis=0) * units.second
|
|
218
|
+
|
|
219
|
+
centers = np.expand_dims(_centers, axis=1) * units.second
|
|
220
|
+
widths = np.expand_dims(_widths, axis=1) * units.second
|
|
171
221
|
|
|
172
222
|
# Compute the Gaussian for each height, center, and width using broadcasting
|
|
173
223
|
power_gaussians = _coupling_power[:, np.newaxis] * np.exp(- (time_grid - centers) ** 2 / (2 * widths ** 2))
|
|
@@ -175,36 +225,25 @@ class FlowCytometer:
|
|
|
175
225
|
total_power = np.sum(power_gaussians, axis=0) + self.background_power
|
|
176
226
|
|
|
177
227
|
# Sum all the Gaussians and add them to the detector.raw_signal
|
|
178
|
-
detector._add_optical_power_to_raw_signal(
|
|
228
|
+
detector._add_optical_power_to_raw_signal(
|
|
229
|
+
signal=detector_signal,
|
|
230
|
+
optical_power=total_power,
|
|
231
|
+
wavelength=self.flow_cell.source.wavelength
|
|
232
|
+
)
|
|
179
233
|
|
|
180
|
-
detector.capture_signal()
|
|
234
|
+
digitized_signal, is_saturated = detector.capture_signal(signal=detector_signal)
|
|
181
235
|
|
|
182
|
-
|
|
236
|
+
self.dataframe.loc[detector.name, 'DigitizedSignal'] = digitized_signal
|
|
183
237
|
|
|
184
|
-
|
|
185
|
-
"""
|
|
186
|
-
Logs and displays key statistics about the simulated events.
|
|
238
|
+
detector.is_saturated = is_saturated
|
|
187
239
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
Returns
|
|
195
|
-
-------
|
|
196
|
-
SimulationLogger
|
|
197
|
-
An instance of the logger containing all recorded statistics.
|
|
198
|
-
|
|
199
|
-
Effects
|
|
200
|
-
-------
|
|
201
|
-
Outputs formatted tables to the console or log file, depending on the logger's configuration.
|
|
202
|
-
"""
|
|
203
|
-
logger = SimulationLogger(cytometer=self)
|
|
204
|
-
|
|
205
|
-
logger.log_statistics(include_totals=True, table_format="fancy_grid")
|
|
240
|
+
experiment = Experiment(
|
|
241
|
+
run_time=run_time,
|
|
242
|
+
scatterer_dataframe=scatterer_dataframe,
|
|
243
|
+
detector_dataframe=self.dataframe
|
|
244
|
+
)
|
|
206
245
|
|
|
207
|
-
return
|
|
246
|
+
return experiment
|
|
208
247
|
|
|
209
248
|
def _get_detection_mechanism(self) -> Callable:
|
|
210
249
|
"""
|
|
@@ -242,32 +281,6 @@ class FlowCytometer:
|
|
|
242
281
|
case _:
|
|
243
282
|
raise ValueError("Invalid coupling mechanism. Choose 'rayleigh' or 'uniform'.")
|
|
244
283
|
|
|
245
|
-
def _generate_pulse_parameters(self) -> None:
|
|
246
|
-
"""
|
|
247
|
-
Generates and assigns random Gaussian pulse parameters for each particle event.
|
|
248
|
-
|
|
249
|
-
The generated parameters include:
|
|
250
|
-
- Centers: The time at which each pulse occurs.
|
|
251
|
-
- Widths: The standard deviation (spread) of each pulse in seconds.
|
|
252
|
-
|
|
253
|
-
Effects
|
|
254
|
-
-------
|
|
255
|
-
scatterer_collection.dataframe : pandas.DataFrame
|
|
256
|
-
Adds a 'Widths' column with computed pulse widths for each particle.
|
|
257
|
-
Uses the flow speed and beam waist to calculate pulse widths.
|
|
258
|
-
"""
|
|
259
|
-
columns = pd.MultiIndex.from_product(
|
|
260
|
-
[[p.name for p in self.detectors], ['Centers', 'Heights']]
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
self.pulse_dataframe = pd.DataFrame(columns=columns)
|
|
264
|
-
|
|
265
|
-
self.pulse_dataframe['Centers'] = self.scatterer_collection.dataframe['Time']
|
|
266
|
-
|
|
267
|
-
widths = self.source.waist / self.flow_cell.flow_speed * np.ones(self.scatterer_collection.n_events)
|
|
268
|
-
|
|
269
|
-
self.scatterer_collection.dataframe['Widths'] = pint_pandas.PintArray(widths, dtype=widths.units)
|
|
270
|
-
|
|
271
284
|
def add_detector(self, **kwargs) -> Detector:
|
|
272
285
|
"""
|
|
273
286
|
Dynamically adds a new detector to the system configuration.
|
|
@@ -292,105 +305,3 @@ class FlowCytometer:
|
|
|
292
305
|
|
|
293
306
|
return detector
|
|
294
307
|
|
|
295
|
-
class PlotInterface:
|
|
296
|
-
def __init__(self, cytometer):
|
|
297
|
-
self.cytometer = cytometer
|
|
298
|
-
|
|
299
|
-
def signals(self, figure_size: tuple = (10, 6), add_peak_locator: bool = False, show: bool = True) -> None:
|
|
300
|
-
"""
|
|
301
|
-
Visualizes the raw signals for all detector channels along with the scatterer distribution.
|
|
302
|
-
|
|
303
|
-
Parameters
|
|
304
|
-
----------
|
|
305
|
-
figure_size : tuple, optional
|
|
306
|
-
Dimensions of the generated plot (default: (10, 6)).
|
|
307
|
-
add_peak_locator : bool, optional
|
|
308
|
-
If True, adds visual markers for detected signal peaks (default: False).
|
|
309
|
-
|
|
310
|
-
Effects
|
|
311
|
-
-------
|
|
312
|
-
Displays a multi-panel plot showing:
|
|
313
|
-
- Raw signals for each detector channel.
|
|
314
|
-
- Scatterer distribution along the time axis.
|
|
315
|
-
"""
|
|
316
|
-
logging.info("Plotting the signal for the different channels.")
|
|
317
|
-
|
|
318
|
-
scatterer_collection = self.cytometer.scatterer_collection
|
|
319
|
-
detectors = self.cytometer.detectors
|
|
320
|
-
|
|
321
|
-
n_detectors = len(detectors)
|
|
322
|
-
|
|
323
|
-
with plt.style.context(mps):
|
|
324
|
-
_, axes = plt.subplots(ncols=1, nrows=n_detectors + 1, figsize=figure_size, sharex=True, sharey=True, gridspec_kw={'height_ratios': [1, 1, 0.4]})
|
|
325
|
-
|
|
326
|
-
time_unit, signal_unit = detectors[0].plot(ax=axes[0], show=False, add_peak_locator=add_peak_locator)
|
|
327
|
-
detectors[1].plot(ax=axes[1], show=False, time_unit=time_unit, signal_unit=signal_unit, add_peak_locator=add_peak_locator)
|
|
328
|
-
|
|
329
|
-
axes[-1].get_yaxis().set_visible(False)
|
|
330
|
-
scatterer_collection.add_to_ax(axes[-1])
|
|
331
|
-
|
|
332
|
-
# Add legends to each subplot
|
|
333
|
-
for ax in axes:
|
|
334
|
-
ax.legend()
|
|
335
|
-
|
|
336
|
-
if show: # Display the plot
|
|
337
|
-
plt.show()
|
|
338
|
-
|
|
339
|
-
def coupling_distribution(self, log_scale: bool = False, show: bool = True, equal_limits: bool = False, save_path: str = None) -> None:
|
|
340
|
-
"""
|
|
341
|
-
Plots the density distribution of optical coupling in the FSC and SSC channels.
|
|
342
|
-
|
|
343
|
-
This method generates a joint plot showing the relationship between the signals from
|
|
344
|
-
the forward scatter ('detector: forward') and side scatter ('detector: side') detectors.
|
|
345
|
-
The plot is color-coded by particle population and can optionally display axes on a logarithmic scale.
|
|
346
|
-
|
|
347
|
-
Parameters
|
|
348
|
-
----------
|
|
349
|
-
log_scale : bool, optional
|
|
350
|
-
If True, applies a logarithmic scale to both the x and y axes of the plot (default: False).
|
|
351
|
-
show : bool, optional
|
|
352
|
-
If True, displays the plot immediately. If False, the plot is created but not displayed,
|
|
353
|
-
allowing for further customization or saving externally (default: True).
|
|
354
|
-
equal_limits : bool, optional
|
|
355
|
-
If True, sets the same limits for both the x and y axes based on the maximum range
|
|
356
|
-
across both axes. If False, the limits are set automatically based on the data (default: False).
|
|
357
|
-
|
|
358
|
-
"""
|
|
359
|
-
scatterer_collection = self.cytometer.scatterer_collection
|
|
360
|
-
detector_0, detector_1 = self.cytometer.detectors
|
|
361
|
-
|
|
362
|
-
with plt.style.context(mps):
|
|
363
|
-
joint_plot = sns.jointplot(
|
|
364
|
-
data=scatterer_collection.dataframe,
|
|
365
|
-
x=f'detector: {detector_0.name}',
|
|
366
|
-
y=f'detector: {detector_1.name}',
|
|
367
|
-
hue="Population",
|
|
368
|
-
alpha=0.8,
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
if log_scale:
|
|
372
|
-
joint_plot.ax_joint.set_xscale('log')
|
|
373
|
-
joint_plot.ax_joint.set_yscale('log')
|
|
374
|
-
|
|
375
|
-
if equal_limits:
|
|
376
|
-
# Get data limits
|
|
377
|
-
x_data = scatterer_collection.dataframe[f'detector: {detector_0.name}']
|
|
378
|
-
y_data = scatterer_collection.dataframe[f'detector: {detector_1.name}']
|
|
379
|
-
|
|
380
|
-
x_min, x_max = x_data.min(), x_data.max()
|
|
381
|
-
y_min, y_max = y_data.min(), y_data.max()
|
|
382
|
-
|
|
383
|
-
# Find the overall min and max
|
|
384
|
-
overall_min = min(x_min, y_min)
|
|
385
|
-
overall_max = max(x_max, y_max)
|
|
386
|
-
|
|
387
|
-
# Set equal limits
|
|
388
|
-
joint_plot.ax_joint.set_xlim(overall_min, overall_max)
|
|
389
|
-
joint_plot.ax_joint.set_ylim(overall_min, overall_max)
|
|
390
|
-
|
|
391
|
-
if save_path:
|
|
392
|
-
joint_plot.figure.savefig(save_path, dpi=300, bbox_inches='tight')
|
|
393
|
-
print(f"Plot saved to {save_path}")
|
|
394
|
-
|
|
395
|
-
if show: # Display the plot
|
|
396
|
-
plt.show()
|