PeacoQC-RS
Rust implementation of PeacoQC (Peak-based Quality Control) for flow cytometry, with an efficient, trait-based API so any FCS data structure can plug in to this method.
Overview
- Time-bin quality scoring (isolation forest / MAD modes)
- Margin event removal, consecutive-bin filtering, doublet hints
- Boolean
good_cellsmasks and CSV/JSON export - Optional FCS integration (
flow-fcs) and QC overview plots
Core Features
- Peak Detection: Automatic peak detection using kernel density estimation
- Isolation Forest: Outlier detection using isolation tree method
- MAD Outlier Detection: Median Absolute Deviation-based outlier identification
- Margin Event Removal: Detection and removal of margin events
- Doublet Detection: Identification of doublet/multiplet events
- Monotonic Channel Detection: Detection of channels with monotonic trends (indicating technical issues)
- Consecutive Bins Filtering: Removal of short consecutive regions
- Trait-Based Design: Works with any data structure via
PeacoQCDatatrait
Feature flags:
| Flag | Description | Notes |
|---|---|---|
flow-fcs (default) |
Enable integration with the flow-fcs crate for FCS file support |
|
gpu |
Optional GPU path for some batched kernels | Not recommended in 0.3.x (e2e slower than CPU — see Performance) |
cubecl |
Enable cubeCL custom GPU kernels | requires gpu feature |
Installation
Add this to your Cargo.toml:
[]
= { = "0.3.3", = ["flow-fcs"] }
How it works
PeacoQC bins events along time, estimates per-channel density structure, detects anomalous bins (IT and/or MAD), and optionally removes margin/monotonic/doublet pathologies. The public entry point is peacoqc over any type implementing the PeacoQCData trait. With the flow-fcs crate, PeacoQCConfig::for_fcs fills analysis channels from fluorescence parameters.
Usage
use ;
// Assuming you have an FCS struct that implements PeacoQCData
let config = PeacoQCConfig ;
let result = peacoqc?;
// Apply the `good_cells` boolean mask from the PeacoQCResult struct
let clean_fcs = fcs.filter?;
println!;
// Export QC results for downstream analysis
result.export_csv_boolean?;
result.export_json_metadata?;
See examples/basic_usage.rs for a complete working example.
flow-fcs convenience
With the flow-fcs feature enabled:
PeacoQCConfig::for_fcs(&flow_fcs::Fcs, QCMode)fillschannelsfrom fluorescence parameters on theFcs(same notion as auto-detecting analysis channels from the file).create_qc_plots(moduleqc::plots) can write overview figures for manual review when given the FCS, thePeacoQCResult, an output path, and aQCPlotConfig.PeacoQCResult::export_json_metadatawrites run metadata (percentages, bin counts, etc.) alongside CSV exports.
Interoperability via Traits
PeacoQC-RS uses trait-based design for maximum interoperability. To use PeacoQC with your own FCS data structure, simply implement the PeacoQCData trait:
use ;
Additionally, implement FcsFilter to enable filtering:
use ;
Integration with flow-fcs
If you enable the flow-fcs feature flag, PeacoQC-RS provides trait implementations for the Fcs struct provided by it:
use ;
use Fcs;
API Overview
Main Functions
- Main quality control function that runs the complete PeacoQC pipeline
- Processes channels and bins in parallel for optimal performance
- Remove margin events from FCS data
- Detect and remove doublet/multiplet events
Configuration
PeacoQCConfig: Main configuration for quality control (now with builder pattern)channels: Channels to analyzedetermine_good_cells: QC mode (All, IsolationTree, MAD, None)mad: MAD threshold (default: 6.0)it_limit: Isolation Tree limit (default: 0.6)consecutive_bins: Consecutive bins threshold (default: 5)kde_bandwidth_adjust: KDE bandwidth scaling (default: 1.0) - NEWkde_grid_points: KDE grid resolution (default: 512) - NEWcluster_distance_threshold: Peak clustering threshold (default: None) - NEW
Builder pattern usage:
use PeacoQCConfig;
let config = builder
.channels
.kde_bandwidth_adjust // Tune for smoother peaks
.kde_grid_points // Higher precision
.build
.unwrap;
MarginConfig: Configuration for margin event removalDoubletConfig: Configuration for doublet detection
Results
PeacoQCResult: Complete QC resultsgood_cells: Boolean mask (true = keep, false = remove)removal_reason_per_bin: Optional per-bin removal reason (Isolation Tree, MAD, Consecutive) for plottingpercentage_removed: Percentage of events removedpeaks: Peak detection results per channeln_bins: Number of bins usedevents_per_bin: Events per binexport_csv_boolean(): Export as boolean CSV (0/1 values)export_csv_numeric(): Export as numeric CSV (2000/6000 values, R-compatible)export_json_metadata(): Export comprehensive QC metrics as JSON
RemovalReason: Enum for why a bin was flagged (Isolation Tree, MAD, Consecutive); used when plotting removal reasons
Export Formats
PeacoQC-RS supports multiple export formats for QC results, enabling integration with various downstream analysis tools.
Boolean CSV (Recommended)
Export QC results as a CSV file with 0/1 values:
result.export_csv_boolean?;
Format:
PeacoQC
1
1
0
1
1= good event (keep)0= bad event (remove)
Use cases:
- pandas:
df[df['PeacoQC'] == 1] - R:
df[df$PeacoQC == 1, ] - SQL:
WHERE PeacoQC = 1 - General data analysis workflows
Numeric CSV (R-Compatible)
Export QC results as a CSV file with numeric codes matching the R PeacoQC package:
result.export_csv_numeric?;
Format:
PeacoQC
2000
2000
6000
2000
2000(or custom good_value) = good event (keep)6000(or custom bad_value) = bad event (remove)
Use cases:
- Compatibility with existing R PeacoQC workflows
- FlowJo CSV import
- Legacy analysis pipelines
JSON Metadata
Export comprehensive QC metrics and configuration as JSON:
result.export_json_metadata?;
Format:
Use cases:
- Programmatic access to QC metrics
- Reporting and documentation
- Provenance tracking
- Quality control dashboards
Custom Column Names
You can specify custom column names for CSV exports:
result.export_csv_boolean_with_name?;
result.export_csv_numeric_with_name?;
Quality Control Methods
1. Peak Detection
Uses kernel density estimation (KDE) with Gaussian kernels to detect peaks in binned data. Peaks are identified using Silverman's rule for bandwidth selection.
2. Isolation Tree
An isolation forest-based outlier detection method. Events in bins with low isolation scores are flagged as outliers.
3. MAD (Median Absolute Deviation)
Detects outliers using the median absolute deviation method. Events exceeding a MAD threshold are flagged.
4. Consecutive Bins Filtering
Removes short consecutive regions that may represent artifacts rather than real biological populations.
5. Monotonic Channel Detection
Detects channels with monotonic trends (increasing or decreasing) which may indicate technical problems:
- Increasing: Possible accumulation, clog developing
- Decreasing: Possible depletion, pressure loss
Uses kernel smoothing (matching R's stats::ksmooth with bandwidth=50) to smooth bin medians, then checks if smoothed values satisfy monotonicity conditions using cummax/cummin. Channels are flagged if >75% of smoothed values are non-decreasing (increasing) or non-increasing (decreasing). This matches the original R implementation's algorithm.
Performance
Headline comparison is QC-core wall time versus Bioconductor PeacoQC (load excluded;
same defaults). Method and fairness notes: docs/comparison-with-r.md.
Full sample tables: docs/throughput_vs_r_sample.md.
Representative release results (Apple M5 Max, 2026-08-10; warmup=1, reps=3; PeacoQC 1.22.0 / flowCore 2.24.0 / peacoqc-rs 0.3.2; Gaussian synthetic fixtures):
| Case | R mean (s) | Rust 1-thread (s) | Rust Rayon (s) | Speedup vs R (Rayon) |
|---|---|---|---|---|
| real ~215k×13 | 1.55 | 0.225 | 0.109 | 14.2× |
| real ~263k×13 | 1.36 | 0.222 | 0.093 | 14.5× |
| real ~394k×13 | 1.61 | 0.274 | 0.107 | 15.1× |
| synth 200k×15 | 1.63 | 0.223 | 0.098 | 16.7× |
| synth 1M×15 | 2.98 | 0.581 | 0.182 | 16.4× |
| synth 1M×30 | 5.57 | 1.156 | 0.359 | 15.5× |
On these sizes, default Rayon is about 14–15× faster than R on real stained FCS and about 15–19× on the synthetic grid. Single-thread Rust is already ~5–9× vs R.
Do not enable gpu for full PeacoQC in this version — earlier --gpu runs were far slower than Rayon CPU on every size (investigation: beads flow-crates-aww). Leave GPU off unless you are profiling that path.
Result agreement (R vs Rust)
On the three real FCS cases, % removed agreed closely (|Δ| ≈ 0.3 pp on two samples; +2.1 pp on one). Large synthetic cases (1M events) also track R (|Δ| ≲ 1.1 pp); smaller synthetic grids can still diverge near decision boundaries — see docs/throughput_vs_r_sample.md. Dedicated R-parity tests remain the source of truth for algorithmic fidelity.
Internal notes (not vs R):
- Parallel Processing:
rayonover channels/bins - GPU (optional, not recommended for e2e PeacoQC yet): microbench wins on batched KDE do not currently translate to full-pipeline wall time —
DEV_NOTES.md, beadsflow-crates-aww - Criterion microbenches / alloc A/B:
cargo bench,docs/PERF_AB.md
Benchmarks
Cross-language harness (pass real FCS only via --fcs; do not commit clinical paths):
(Optional GPU row for investigation only: build with --features flow-fcs,gpu and pass --gpu. Not recommended for production timings.)
Criterion (Rust-only):
Testing
The library includes comprehensive unit tests covering:
- Peak detection accuracy
- Isolation tree outlier detection
- MAD outlier identification
- Margin event removal
- Doublet detection
- Monotonic channel detection
- Statistical functions (median, MAD, density estimation)
Run tests with:
Examples
Basic Usage Example
See examples/basic_usage.rs for a complete example demonstrating:
- Creating synthetic FCS data
- Removing margin events
- Removing doublets
- Running full PeacoQC analysis
- Applying the quality control filter
Run with:
Error Handling
All functions return Result<T, PeacoQCError>. The PeacoQCError enum covers:
InvalidChannel: Invalid or non-numeric channelChannelNotFound: Channel not found in dataInsufficientData: Not enough events for analysisStatsError: Statistical computation failedConfigError: Configuration errorNoPeaksDetected: No peaks detected in dataPolarsError: Polars DataFrame error (when using flow-fcs feature)
Attribution
This Rust implementation is based on the original PeacoQC algorithm and R package. We gratefully acknowledge the original authors:
Original Paper:
- [Emmaneel, A., Quintelier, K., Sichien, D., Rybakowska, P., Marañón, C., Alarcón-Riquelme, M. E.,
Van Isterdael, G., Van Gassen, S., & Saeys, Y. (2022). PeacoQC: Peak-based selection of high quality
cytometry data. Cytometry A, 101(4), 325-338.
https://doi.org/10.1002/cyto.a.24501](https://doi. org/10.1002/cyto.a.24501)
Original R Implementation:
- GitHub:
https://github.com/saeyslab/PeacoQC - Authors: Annelies Emmaneel, Katrien Quintelier, and the Saeys Lab
This Rust version provides:
- Improved performance through native compilation
- Better memory efficiency
- Type safety
- Trait-based extensibility
License
MIT
Contributing
Contributions are welcome! Please feel free to open issues or submit a Pull Request on [Github] (https://github.com/jrmoynihan/flow).
Related crates
- Manual gates / Automated scatter gates →
flow-gates - CLI and Python bindings →
peacoqc-cli,peacoqc-py - Shared FFT KDE for gates/plots/general analysis →
flow-density(this crate still vendors a PeacoQC-oriented density helper understats::density; migrating toflow-densityis planned) - Single-stain histogram peak isolation for unmixing medians →
flow-peak-detection(different problem than PeacoQC time-bin peaks) - Long QC preprocessing chain (margins → doublets → compensate/transform → PeacoQC → scatter/debris) →
tru-olslibrary (run_qc_pipeline), not this crate alone - CLI wrapper only →
peacoqc-cli