Note
Go to the end to download the full example code.
Workflow#
This tutorial demonstrates how to simulate a flow cytometry experiment using the FlowCyPy library. The simulation involves configuring a flow setup, defining a single population of particles, and analyzing scattering signals from two detectors to produce a 2D density plot of scattering intensities.
Overview:#
Configure the flow cell and particle population.
Define the laser source and detector parameters.
Simulate the flow cytometry experiment.
Analyze the generated signals and visualize results.
Step 0: Import Necessary Libraries#
Here, we import the necessary libraries and units for the simulation. The units module helps us define physical quantities like meters, seconds, and watts in a concise and consistent manner.
import numpy as np
from FlowCyPy import units
Step 1: Configure Noise Settings#
Noise settings are configured to simulate real-world imperfections. In this example, we include noise globally but exclude specific types, such as shot noise and thermal noise.
from FlowCyPy import NoiseSetting
NoiseSetting.include_noises = True
NoiseSetting.include_shot_noise = True
NoiseSetting.include_dark_current_noise = True
NoiseSetting.include_source_noise = True
NoiseSetting.include_amplifier_noise = True
np.random.seed(3) # Ensure reproducibility
Step 2: Configure the Laser Source#
The laser source generates light that interacts with the particles. Its parameters, like numerical aperture and wavelength, affect how light scatters, governed by Mie theory.
from FlowCyPy import GaussianBeam
source = GaussianBeam(
numerical_aperture=0.1 * units.AU, # Numerical aperture
wavelength=450 * units.nanometer, # Wavelength
optical_power=200 * units.milliwatt, # Optical power
RIN=-140
)
Step 3: Set Up the Flow Cell#
The flow cell models the movement of particles in the cytometer. For example, the volume of fluid passing through the cross-sectional area is calculated as:
from FlowCyPy.flow_cell import FlowCell
flow_cell = FlowCell(
sample_volume_flow=80 * units.microliter / units.minute,
sheath_volume_flow=1 * units.milliliter / units.minute,
width=100 * units.micrometer,
height=100 * units.micrometer,
)
# flow_cell.plot(n_samples=300)
Step 4: Define ScattererCollection and Population#
The scatterer represents particles in the flow. The concentration of particles in the flow cell is given by:
from FlowCyPy import ScattererCollection
from FlowCyPy.population import Exosome, Sphere, distribution
scatterer_collection = ScattererCollection(medium_refractive_index=1.33 * units.RIU)
exosome = Exosome(particle_count=5e9 * units.particle / units.milliliter)
custom_population = Sphere(
name='Pop 0',
particle_count=5e9 * units.particle / units.milliliter,
diameter=distribution.RosinRammler(characteristic_property=150 * units.nanometer, spread=30),
refractive_index=distribution.Normal(mean=1.44 * units.RIU, std_dev=0.002 * units.RIU)
)
# Add an Exosome population
scatterer_collection.add_population(custom_population)
scatterer_collection.dilute(factor=8)
# Initialize the scatterer with the flow cell
df = scatterer_collection.get_population_dataframe(total_sampling=600, use_ratio=False) # Visualize the particle population
# df.plot(x='Diameter', bins='auto')
Step 5: Define Detectors#
Detectors measure light intensity. Parameters like responsivity define the conversion of optical power to electronic signals, and saturation level represents the maximum signal they can handle.
from FlowCyPy.detector import Detector
from FlowCyPy.signal_digitizer import SignalDigitizer
from FlowCyPy.amplifier import TransimpedanceAmplifier
digitizer = SignalDigitizer(
bit_depth='14bit',
saturation_levels='auto',
sampling_rate=60 * units.megahertz,
)
detector_0 = Detector(
name='forward',
phi_angle=0 * units.degree, # Forward scatter angle
numerical_aperture=0.3 * units.AU,
responsivity=1 * units.ampere / units.watt,
)
detector_1 = Detector(
name='side',
phi_angle=90 * units.degree, # Side scatter angle
numerical_aperture=0.3 * units.AU,
responsivity=1 * units.ampere / units.watt,
)
detector_2 = Detector(
name='det_2',
phi_angle=30 * units.degree, # Side scatter angle
numerical_aperture=0.3 * units.AU,
responsivity=1 * units.ampere / units.watt,
)
amplifier = TransimpedanceAmplifier(
gain=10 * units.volt / units.ampere,
bandwidth=10 * units.megahertz,
voltage_noise_density=.1 * units.nanovolt / units.sqrt_hertz,
current_noise_density=.2 * units.femtoampere / units.sqrt_hertz
)
Step 6: Simulate Flow Cytometry Experiment#
The FlowCytometer combines all components to simulate scattering. The interaction between light and particles follows Mie theory:
from FlowCyPy import FlowCytometer, circuits
cytometer = FlowCytometer(
source=source,
transimpedance_amplifier=amplifier,
scatterer_collection=scatterer_collection,
digitizer=digitizer,
detectors=[detector_0, detector_1, detector_2],
flow_cell=flow_cell,
background_power=0.000 * units.milliwatt
)
processing_steps = [
circuits.BaselineRestorator(window_size=100 * units.microsecond),
circuits.BesselLowPass(cutoff=1 * units.megahertz, order=4, gain=2)
]
# Run the flow cytometry simulation
cytometer.prepare_acquisition(run_time=0.5 * units.millisecond)
acquisition = cytometer.get_acquisition(processing_steps=processing_steps)
_ = acquisition.scatterer.plot(
x='side',
y='forward',
z='RefractiveIndex'
)

/opt/hostedtoolcache/Python/3.11.12/x64/lib/python3.11/site-packages/FlowCyPy/source.py:269: UserWarning: Transverse distribution of particle flow exceed the waist of the source
warnings.warn('Transverse distribution of particle flow exceed the waist of the source')
/opt/hostedtoolcache/Python/3.11.12/x64/lib/python3.11/site-packages/FlowCyPy/source.py:269: UserWarning: Transverse distribution of particle flow exceed the waist of the source
warnings.warn('Transverse distribution of particle flow exceed the waist of the source')
/opt/hostedtoolcache/Python/3.11.12/x64/lib/python3.11/site-packages/FlowCyPy/source.py:269: UserWarning: Transverse distribution of particle flow exceed the waist of the source
warnings.warn('Transverse distribution of particle flow exceed the waist of the source')
Visualize the scatter signals from both detectors
acquisition.plot()

Step 7: Analyze Detected Signals#
The Peak algorithm detects peaks in signals by analyzing local maxima within a defined window size and threshold.
from FlowCyPy.triggering_system import TriggeringSystem
trigger = TriggeringSystem(
threshold=2 * units.microvolt,
max_triggers=-1,
pre_buffer=20,
post_buffer=20,
digitizer=digitizer
)
analog_triggered = trigger.run(
signal_dataframe=acquisition,
trigger_detector_name='forward',
)
analog_triggered.plot()

Getting and plotting the extracted peaks.
from FlowCyPy import peak_locator
peak_algorithm = peak_locator.GlobalPeakLocator(compute_width=False)
digital_signal = analog_triggered.digitalize(digitizer=digitizer)
peaks = peak_algorithm.run(digital_signal)
peaks.plot(feature='Height', x='side', y='forward')

<seaborn.axisgrid.JointGrid object at 0x7fa06c572ad0>
Step 8: Classifying the collected dataset
from FlowCyPy.classifier import KmeansClassifier
classifier = KmeansClassifier(number_of_cluster=2)
data = classifier.run(
dataframe=peaks.unstack('Detector'),
features=['Height'],
detectors=['side', 'forward']
)
_ = data.plot(feature='Height', x='side', y='forward')

Total running time of the script: (0 minutes 12.115 seconds)