Skip to contents

Run the pipeline

One call does everything. The stage functions below are the same code, split so a step can be run, inspected or replaced on its own.

run_cyraven()
run_cyraven

Reading and transformation

Getting FCS files into a matrix on a sensible scale. The transform is selectable, and its parameters are derived once per panel so every sample stays on one ruler.

read_fcs_resolved()
Read one FCS file and resolve marker symbols
fingerprint_panels()
Group files into panels by their exact marker set
maybe_compensate()
Apply the spillover matrix if one is present
read_spillover_file()
Read a spillover matrix from a CSV file
make_transform()
Build the intensity transform the whole run uses
derive_cofactor()
Derive the arcsinh cofactor from the data
derive_cofactor_pooled()
Derive the panel cofactor from SEVERAL samples, not from whichever file was read first
derive_logicle_pooled()
Derive one set of transform parameters for a panel

Gating

Thresholds derived from each sample’s own density rather than transferred from fixed coordinates. The reference for a marker that resolves no minimum can be an unstained tube or a fluorescence-minus-one control, and a single sample’s cut can be corrected by hand with the correction recorded.

density_valley()
Find the deepest density valley between two modes
resolve_threshold()
Resolve a threshold for one marker, recording where the value came from
apply_gate_hierarchy()
Apply the full gate hierarchy to one file
staining_verdict()
Staining QC verdict per file
sample_override()
Look up a per-sample, per-marker threshold override
parse_fmo_map()
Parse the fmo_for column into a marker-to-file map
fmo_for_sample()
Which FMO file, if any, controls a given marker for a given sample
control_cut()
Place a cut on a control channel, and refuse a control that cannot be one
fmo_agreement()
Distance between a derived cut and its FMO-anchored equivalent

Acquisition and panel quality

Two questions the gating stage cannot answer on its own: whether the instrument was doing the same thing throughout the acquisition, and whether a marker that resolves no density minimum is failing because of the panel.

find_time_column()
Locate the Time channel in a read FCS file
detect_time_anomalies()
Flag acquisition intervals whose rate or signal departs from the file
run_acquisition_qc()
Run acquisition-time QC across a cohort
frequency_delta_if_cleaned()
How far each population moves if the flagged windows are excluded
fig_acquisition_qc()
Event rate across the acquisition, with the flagged intervals marked
spreading_pairs()
Spreading received by each marker from each other marker
run_spreading_report()
Spreading across a cohort, and which markers it is costing
spillover_spreading_matrix()
Spillover spreading matrix from single-stain controls

Calibration

Channel units are comparable within an instrument and never between two. Beads of assigned fluorophore equivalence convert them to units that are.

bead_peaks()
Locate bead populations in one channel
fit_bead_calibration()
Fit a channel-units to assigned-units calibration from a bead acquisition
apply_bead_calibration()
Apply a fitted calibration to a sample's linear channel values

Uncertainty

How far a threshold can move, how many cells were behind the frequency it produced, and what each of those costs. Every cut is resampled and re-derived over the settings that placed it, and the resulting spread is propagated to the populations that read it; the counting term is reported beside it, because a well-placed cut and a sufficient number of events are different guarantees. None of this changes a reported number; it puts a second number next to it.

threshold_uncertainty()
Uncertainty in one per-sample threshold
counting_uncertainty()
Counting uncertainty and detection limits for a population frequency
population_frequency_uncertainty()
Propagate threshold uncertainty into one sample's population frequencies
run_gate_uncertainty()
Run the whole uncertainty analysis over a scored cohort
annotate_gate_uncertainty()
Compare a between-group difference against the gate uncertainty behind it
valley_setting_grid()
The settings sweep that produces the method component

Populations

Scoring a declarative population specification.

score_populations()
Evaluate the population spec against one file's thresholded markers
resolve_population_parents()
Resolve parent: declarations in a population specification
derive_scoped_cuts()
Derive the cuts a specification places within parent populations
default_population_spec()
Built-in population spec, derived from the supplied gating strategy
default_functional_blocks()
Functional marker blocks, transcribed from the document's four scoping rules
expected_positive_markers()
Markers each population's gate definition requires to be POSITIVE
compute_population_ratios()
Derived abundance ratios (e.g. CD4:CD8), from the config's ratios: block
immune_subset_spec()
Immune subset definitions resolvable from a given marker panel

Absolute cell counts

An externally measured cell yield per acquisition, joined on patient and timepoint, turns a share into a number of cells. That is the one thing a frequency table cannot express, because shares are constrained to sum to 100 and so cannot separate one population expanding from every other contracting.

load_total_counts()
Load, normalize, parse and sample-match a –total-counts input
attach_absolute_cells()
Attach absolute cell numbers to a per-sample abundance table

Embedding

One shared UMAP per marker panel, plus model persistence for later batches.

select_umap_features()
Choose the markers that define the embedding space
plan_subsample()
Draw a size-balanced subsample of gated cells across samples
density_weights()
Inverse-density sampling weights in marker space
draw_subsample_rare()
Draw a subsample that preserves sparse regions of marker space
run_umap()
Build the UMAP input matrix and run the embedding
save_umap_model()
Save a trained UMAP model plus the scaling needed to reproduce its input
load_umap_model()
Load a saved UMAP model
project_umap()
Project new cells into a saved embedding

Clustering

Unsupervised clustering as a cross-check on the gate specification, and reference-defined subclustering.

run_unsupervised_clusters()
Unsupervised clustering of the embedded cells
cluster_gate_agreement()
Cross-tabulate unsupervised clusters against gate labels
subcluster_by_reference()
Split each cluster into subclusters DEFINED ON THE REFERENCE GROUP
choose_subcluster_k()
Choose k per population from the reference group's cells
write_suggested_config()
Write the declared specification plus a draft entry per uncovered cluster

Learned gates

The inverse of the gating stage: given a set of labelled cells, learn a two-marker gating strategy that selects them. This is what turns “this cluster matches no described population” into a gate someone can draw. Descriptive throughout – it proposes gates and never changes a scored one.

learn_convex_gate()
Learn a convex two-marker gate for a labelled set of cells
explain_cluster()
Learn a hierarchical gating strategy for one labelled population
explain_unmatched_clusters()
Propose gating strategies for clusters the spec does not describe
rank_gate_markers()
Rank markers by how differently they are distributed in targets and the rest
fig_gate_strategy()
Draw a proposed gating strategy, one panel per gate

Labels from another tool

Take a cell labelling this package did not produce, a cyCONDOR clustering for instance, learn a gate that selects it, and find out whether that gate holds on a donor it was not fitted to. Held-out cells come from the same donors as the training cells and overstate transferability; refitting with one donor withheld does not.

read_external_labels()
Read cell labels produced by another tool
join_external_labels()
Attach external labels to the embedding cell table
explain_external_labels()
Learn and validate a gate for each externally supplied label
gate_transferability()
Does the gate work on a donor it was not fitted to
apply_gate_strategy()
Apply a learned gating strategy to new cells

Gate export

A learned gate that stays in R cannot be sorted on. These write it out in the units the FCS file stores, as ISAC Gating-ML 2.0 and as a plain table of vertices.

write_gating_ml()
Write learned gating strategies as a Gating-ML 2.0 document
polygons_linear_table()
The same polygons as a plain table in linear units
polygon_to_linear()
Subdivide polygon edges and return them in linear units

Statistics

Every test below uses per-sample values, so the replicates are donors rather than cells.

abundance_measure()
Pick the best available abundance measure, and say what it means
stats_group_comparison()
Test each population's abundance between groups
fig_group_volcano()
Between-group differences for every population in one figure
stats_marker_state()
Sample-level differential-state test: population x marker x group
stats_subcluster_shifts()
Per (cluster, subcluster, marker, study): how far is this study from reference
clr_frequencies()
Zero-safe centred log-ratio of each sample's population composition
exclusive_label_frequencies()
Each sample's composition by exclusive label
compositional_concordance()
Concordance between the raw-percentage test and the CLR test
stats_confounding()
Is a covariate confounded with cohort, and does it track the outcome?
stats_rank_ancova()
Rank ANCOVA: cohort effect after adjusting for covariates (EXPLORATORY)
stats_paired_comparison()
Paired / repeated-measures abundance test

Clinical variables

A severity score, a laboratory value or an outcome flag is neither the study group nor a confounder: a confounder is screened to decide whether a group difference can be believed, a clinical variable is the question. The test follows the variable’s type – Spearman for numeric, Wilcoxon with Cliff’s delta for two levels, Kruskal-Wallis for more – and multiplicity is corrected within each variable rather than across all of them. Association only: a 28-day flag is a two-group comparison, and no time-to-event model is fitted because the sheet carries no follow-up time. The figures are in reading order: the variables against each other, then the association across all of them, then the cohort as one annotated matrix, then per variable the effects with their bootstrap intervals. Every signed effect carries a percentile bootstrap interval, because on ten patients the interval usually spans zero and that is the finding.

stats_clinical_association()
Clinical variables against population frequencies and marker intensities
fig_clinical_correlogram()
Spearman correlation between the clinical variables themselves
fig_clinical_landscape()
The cohort as one picture: populations, samples and every clinical variable
fig_clinical_forest()
Ordered effect sizes with bootstrap intervals for one clinical variable
fig_clinical_trajectory()
Per-patient trajectories across timepoints, split by outcome

Batch structure

Measure the batch effect first, then decide. Correction is opt-in and is refused when batch and study group overlap far enough that removing one removes the other.

lisi_score()
Local Inverse Simpson's Index of a label over a neighbourhood graph
batch_mixing_report()
Batch-mixing diagnostic with a permutation null and a confounding check
marker_batch_drift()
Per-marker distributional drift between acquisition batches
emd_1d()
One-dimensional Earth Mover's distance between two samples
correct_batch()
Correct batch effects in the transformed marker matrix

Diagnostics

Questions about the data and the gating that the analysis itself cannot answer.

stats_threshold_drift()
Test each marker's per-sample threshold for a cohort difference
write_run_manifest()
Write the run manifest
write_miflowcyt()
Write a MIFlowCyt-structured report of the run
write_run_report()
Write a single self-contained HTML report of everything a run produced

Conformance across runs

The within-run check in thresholds_used.csv compares each sample against its peers and catches one bad tube. It cannot catch a cohort that moved as a whole, because the peer median moves with it. A baseline written from an accepted run is the fixed reference that can.

write_spec_baseline()
Write a conformance baseline from an accepted run
read_spec_baseline()
Read a conformance baseline
specification_conformance()
Test this run against a baseline
spec_fingerprint()
Canonical text form of a population specification

Figures

fig_absolute_counts_qc()
QC heatmap for –absolute-counts: one tile per sample x population
fig_acquisition_qc()
Event rate across the acquisition, with the flagged intervals marked
fig_batch_diagnostic()
Batch diagnostic figure: the embedding coloured by batch, plus the LISI distribution against its permutation null
fig_clinical_correlogram()
Spearman correlation between the clinical variables themselves
fig_clinical_detail()
Per-population detail against one clinical variable
fig_clinical_forest()
Ordered effect sizes with bootstrap intervals for one clinical variable
fig_clinical_heatmap()
Heatmap of clinical association across populations or markers
fig_clinical_landscape()
The cohort as one picture: populations, samples and every clinical variable
fig_clinical_trajectory()
Per-patient trajectories across timepoints, split by outcome
fig_cohort_confusion()
Cohort composition heatmap ("confusion matrix")
fig_density_by_sample()
Per-sample (or per-group) density comparison over the shared embedding
fig_detection_limits()
How many samples each population is actually measurable in
fig_frequency_uncertainty()
Population frequencies with the uncertainty of the gate that produced them
fig_functional_markers()
Functional-marker positivity figure
fig_gate_strategy()
Draw a proposed gating strategy, one panel per gate
fig_gating_qc()
Per-gate QC figure: every applied gate shown per sample with its threshold
fig_group_comparison()
Grouped abundance figure: one panel per population, bar + SD + every sample
fig_group_volcano()
Between-group differences for every population in one figure
fig_marker_grid()
Marker-expression grid over the shared embedding
fig_marker_state()
Differential-state figure, drawn by the baseline's own comparison figure
fig_marker_umaps_by_group()
One UMAP per marker, pooled and split by every category present
fig_multigraph_overlay()
FlowJo-style Multigraph Overlay: every cluster x every marker, one peak per group
fig_population_frequencies()
Population frequency figure – one lettered panel per population
fig_population_marker_heatmap()
Population x marker phenotype heatmap
fig_population_ratios()
Population-ratio figure – same layout as fig_group_comparison(), one panel per ratio defined in the config's ratios: block
fig_populations_by_batch()
Population abundance per batch
fig_recon_diagnostics()
Initial reconnaissance / QC diagnostic figure – run on EVERY batch
fig_threshold_drift()
Threshold-drift figure: per-sample cut for every marker, coloured by cohort
fig_umap_overview()
Population + sample + covariate colouring panel set
fig_umap_overview_by_group()
Combined + per-group UMAP small multiples
fig_uncertainty_budget()
Where each population's uncertainty comes from
fig_unsupervised_clusters()
Unsupervised-clustering figure: the embedding by cluster, next to the same embedding by gate label
theme_cyto()
Publication-oriented minimal theme WHY: consistent, legible defaults across every emitted figure; outward ticks and frameless legends read better in print than ggplot2 defaults.
theme_panel_borders()
Panel borders and gutters for any faceted figure
natural_cluster_factor()
Order cluster labels k1, k2, ... k30 rather than k1, k10, k11, ... k2

Inputs

The sample sheet is one row per FCS file, carrying its identity, its subject’s attributes, its study variables and any externally measured counts. It is read into the same structures the three separate loaders below produce, by calling the same code, so the two routes cannot diverge.

write_demo_cohort()
Write the demonstration cohort
write_config_template()
Write the annotated config template
read_samplesheet()
Read the unified sample sheet
samplesheet_columns()
Reserved column names of the unified sample sheet
load_patient_table()
Read, filter, rename and translate the patient table
load_sample_map()
Read and validate the sample map
load_absolute_counts()
Load, normalize, parse and sample-match an –absolute-counts input

Metadata and export

export_flowjo_fcs()
Write UMAP-annotated FCS files for FlowJo

Appearance and session state

default_colors()
Every colour this script draws with, in one place
fcs_colors() set_fcs_colors()
The colour palette every figure draws with
reference_group() set_reference_group()
The cohort every comparison is drawn against