Regression Panel (the release gate)¶
Before a release is tagged, the release candidate and the previous release genotype the same panel of real samples on HPC, and the two outputs are compared:
- version against version: every changed cell is attributed to the merge (or the interval of merges) where it changed;
- each version against the sign-out counts, by stratum. The BAM is the truth, so disagreements are adjudicated read by read.
The panel is chosen for code-path coverage, not by hand. Every signed-out variant is
tagged with the strata that exercise distinct code paths, and a greedy set cover
picks the fewest samples that bring every stratum to its target (25 variants; 4
samples for a sample stratum), starting from the existing acceptance samples. The
selection reads the clinical sign-out dump and names samples and BAM paths, so the
selector and its output (the tier folder) stay on the selection host and on HPC.
The tools that run and compare a panel (scripts/regression_panel/) hold no patient
data.
Strata¶
| Group | Strata |
|---|---|
| Allele shape and size | SNV, DNP, TNP, ONP; insertions and deletions of 1, 2–5, 6–19, 20–49 and 50+ bases (deletions also 100+); delins with and without a shared first base, growing or shrinking, spanning 20+ bases |
| Repeat context (every non-SNV) | homopolymer (6+), STR (3+ copies of a 2–6 bp unit), unique; an indel in a run beside another run; an insertion duplicating the reference after it (6+ bases) |
| Co-annotated structure | length-changing variants within 50 bp (tract clusters); same-position rows; an SNV between two indels, or inside an indel's window; an MNP within 10 bp of an indel; a delins overlapping a deletion or insertion row; homopolymer-twin candidates; FLT3-ITDs |
| Signal | sign-out VAF (< 2%, 2–10%, 10–40%, ≥ 40%) and depth (< 100, 100–500, 500–2,000, ≥ 2,000) |
| Loci | X, Y, MT; PMS2 (run at --min-mapq 0); REF == ALT and placeholder rows |
| Samples | assay (each IMPACT panel, IMPACT-HEME, ACCESS duplex + simplex), MSI-high, TMB ≥ 20, ACCESS indels in homopolymers (duplex-masked bases) |
Arms and configurations¶
| Arm | Runs |
|---|---|
dna |
every panel BAM at production defaults (MAF out, PairHMM, MAPQ 20); ACCESS as duplex and simplex |
mfsd |
the ACCESS BAMs with --mfsd --mfsd-parquet --observations-parquet |
mq0 |
the PMS2 rows of samples that have them, at --min-mapq 0 |
sw |
~25 samples chosen for indel strata, --alignment-backend sw |
vcf |
a few samples, --format vcf |
normal |
matched normals genotyped on their tumor's variants (fillout) |
cohort |
a few BAMs genotyped on one MAF holding several samples' variants, a variant shared by two samples listed twice (fillout-style input) |
rna |
the FORTE RNA truth cohort, RNA mode with the Ensembl GTF (hg38) |
The tier folder¶
What a selector hands to HPC (any selector that writes this works):
runs.tsv, one gbcms call per row:run_id,tag,arm,mode(dna/rna),root(dmp: b37 BAMs;forte: hg38),bam_relpath(under the root),variants(relative to the tier folder),extra_args.mafs/<tag>.maf: the sample's signed-out variants, with the sign-out counts assignout_t_ref_count,signout_t_alt_count,signout_n_ref_count,signout_n_alt_count(never a gbcms output name, so no--column-prefixcan overwrite them) and the variant's strata aspanel_strata(;-separated). gbcms passes both through to its MAF output.coverage.tsv(every stratum: available, target, covered) andpanel.tsv.
The strata approximate the code paths they name (a tract cluster is length-changing variants within 50 bp, not the engine's own grouping); they choose samples, and the comparison then judges every row whatever its strata.
Running a build¶
run_panel.sh runs one build over a run list. A run that exits 0 leaves
<run>.ok; re-running skips those and retries every other run, so a run that failed
after writing its MAF is not taken as finished. Every attempt is a row of
times.<shard>.tsv (exit code, wall time, peak memory, end time), and a run's latest
attempt is its outcome. submit_panel.sbatch runs it as a SLURM array (0-based; edit
its CONFIG block):
sbatch scripts/regression_panel/submit_panel.sbatch # BUILD=6.5.0, the release image
sbatch scripts/regression_panel/submit_panel.sbatch # BUILD=6.6.0, the candidate
The previous release runs from its image
(singularity exec -B <every path the run reads> docker://ghcr.io/msk-access/gbcms:X.Y.Z gbcms).
The candidate runs from its checkpoint wheel, the last row of checkpoints.tsv
(below): the commit the release ships.
Comparing¶
matches rows by locus (and occurrence, for a locus listed twice), never by position,
leaves failed runs out (their latest attempt did not exit 0) and writes gate.txt
and the reports it summarises: version_summary.tsv (with rows or records in one
version only),
version_cells.tsv, version_rows.tsv (every changed cell; VCF output field by
field: FILTER, INFO:<id>, FORMAT:<id>), header_diff.tsv (columns, or INFO and
FORMAT IDs, only in one version), concordance_by_stratum.tsv (each version against
the sign-out ALT count, per stratum: median delta and the fraction within
max(2 reads, 10%), at read and fragment level and at the matched level, the one
the sign-out counted: reads for IMPACT, duplex + simplex fragments for ACCESS),
discordant.tsv (the largest disagreements at the matched level, and the top 20 per
stratum), normals.tsv, times.tsv and parquet.tsv (needs pyarrow). PMS2's
concordance at --min-mapq 0 comes from the mq0 arm (strata prefixed mq0:); an
ACCESS sample missing one flavour is left out of concordance and counted. gate.txt
ends with GATE: clean or GATE: ATTENTION and the reasons (failed or missing runs, a
stratum that fell more than 5 points, an arm 1.5× slower or larger), and the script
then exits 1. The rna arm's FORTE MAFs carry no sign-out counts, so it is compared
version against version only; the vcf arm's counts are the dna arm's.
Attributing every changed cell¶
A release changes many cells at once (6.6.0's read-judgment work changed every indel row of the acceptance set), so attribution is mechanical: one build per count-affecting merge since the previous release, run on the changed rows only.
- The checkpoints.
checkpoints.py REPO 6.5.0 develop > checkpoints.tsvlists one build per commit on develop (in practice each merged PR) that touches counting code, skipping those whose own acceptance was byte-identical (NEUTRALin the script). Each row names every commit in its interval, so a change landing in a skipped one is still named. Row 0 is the previous release built as a wheel, so a difference between its image and a wheel build is named as such; the last row is the commit the release ships. - The wheels. One manylinux wheel per commit, built as releases up to 6.6.0 built
theirs (the Checkpoint wheels workflow,
manylinux_2_34with the system OpenSSL, because commits before 6.7.0 still link libcurl; each wheel is an artifact namedwheel-<sha>). Before a release the workflow is not yet on the default branch, where GitHub requires a manually run workflow to be, so start it with a push: a branchcheckpoint-wheels/X.Y.Zthat commits the list asscripts/regression_panel/checkpoints.tsv. Once it is on the default branch, Run workflow with theshacolumn works too. Each artifact carries its commit'srequirements.lockwhen the commit has one; install with it, so every checkpoint gets the dependencies its commit pinned:python3.11 -m venv cp_<sha> && cp_<sha>/bin/pip install --require-hashes -r requirements.lock && cp_<sha>/bin/pip install --no-deps gbcms-*.whl. - The reduced runs.
attribute.py prepare TIER OUT 6.5.0 6.6.0 ATTRIBwrites, for every run with changes (a changed cell, or a row in one version only), a variant file holding the changed rows and every row within 100 bp of a kept row's span, closed transitively (siblings classify together), andATTRIB/runs.tsv. The mfsd and rna arms keep their full variant files: their Benjamini–Hochberg q-values are computed across the run's rows. Likecompare_panel.py, every step reads the builds' time records. It leaves out, and names, a run whose latest attempt failed in either version: a crash partway through writing leaves a partial MAF that would otherwise read as changed rows. No manual setting aside is needed. - Each build on them, into
ATTRIB/out: the two versions first, then every checkpoint ascp_<sha>(run_panel.sh TIER ATTRIB/runs.tsv cp_<sha> cp_<sha>/bin/gbcms … ATTRIB/out). attribute.py checkconfirms the reduction changed nothing: every reduced row equals the full run's, in both versions.gbcms_status_reasonis exempt on context rows only (it names the co-annotation group, which the reduction may cut at its edge), never on a changed row. It exits 1 when a row differs, or when a reduced run's latest attempt failed (it names the run to re-submit).attribute.py attribute TIER OUT 6.5.0 6.6.0 ATTRIB checkpoints.tsvtraces each changed cell through the checkpoints:attribution_cells.tsv(each cell and the intervals where it changed; a row in one version only is traced as its presence, column(row)),attribution_summary.tsv(per checkpoint, arm and column),unattributed.tsv(cells whose trail misses a checkpoint's output or does not end at the candidate's value). A reduced run whose latest attempt failed counts as missing output, whatever its MAF holds, and is named to re-submit. It exits 1 when any cell is unattributed.
The gate¶
The candidate is tagged when:
- no run fails in either build, the reduction check is exact, and every changed
cell is attributed (
unattributed.tsvempty); - concordance by stratum is reported for both versions; any stratum whose fraction within max(2 reads, 10%) of the sign-out (matched level) falls by more than 5 points, and the top discordant rows, are adjudicated read by read (the read census, or the reads themselves) before the tag. A class-level count does not close it: every signed-out row whose matched ALT falls to 0, or below a tenth of the previous release's, is checked on its own (the listed rows around it, the changes its ALT reads share, its count under each read judgment), and the decision is then taken by class;
- run time and peak memory per arm stay within 1.5× of the previous release, or the excess is explained (wall time summed over the runs paired across the builds that took 10 s or more, at least 3 per arm; peak memory over paired runs).
abbreviations