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:#

  1. Configure the flow cell and particle population.

  2. Define the laser source and detector parameters.

  3. Simulate the flow cytometry experiment.

  4. 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:

\[\text{Flow Volume} = \text{Flow Speed} \times \text{Flow Area} \times \text{Run Time}\]
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:

\[\text{Concentration} = \frac{\text{Number of Particles}}{\text{Volume of Flow}}\]
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:

\[\sigma_s = \frac{2 \pi}{k} \sum_{n=1}^\infty (2n + 1) (\lvert a_n \rvert^2 + \lvert b_n \rvert^2)\]
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'
)
Scatterer Sampling Distribution
/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()
workflow

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()
workflow

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')
Peaks properties
<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')
Event classification

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

Gallery generated by Sphinx-Gallery