Migrated from the legacy magma_annotate_container wrapper in nodes-io.
One directory = one plugin family = one git-able unit.
manifest.toml— node kindsmagma_annotate,magma_gene_analysis,magma_gene_set: ports, panels, image provenancescripts/annotate.sh— SNP-to-gene annotation, referenced relatively and inlined by the loader at startupscripts/gene_analysis.sh— summary-statistics gene-level analysis against the 1000G EUR reference panelscripts/gene_set.sh— competitive gene-set analysis on the.genes.rawoutputDockerfile— image build definition (moved verbatim fromcontainers/magma/; build + push still go through GHCR); all three kinds share the one official-binary imagetest_magma_annotate.sh— image + catalog-panel smoke test (moved fromcontainers/magma/,root=repointed to this directory)test_magma_gene_analysis.sh— publishes the 1000G EUR reference panel and smoke-tests the gene-analysis command end to endtest_magma_gene_set.sh— self-contained annotate → gene → GSA chain on a deterministic genome-wide SNP sample
- Image:
ghcr.io/auto-nomics/autonomics/magma@sha256:2ca854…— the official MAGMA v1.10 static executable (ctg.cncr.nl/software/magma) copied ontodebian:bookworm-slim; no license label is published upstream, so none is asserted here. - Panel:
wjixiang/catalog-magma-gene-loc-ncbi37-3(official NCBI37.3 gene-location table) mounted at/panels/gene_loc. - Panel:
wjixiang/catalog-magma-ref-g1000-eur(official MAGMA v1.10 1000 Genomes Phase 3 EUR LD reference: whole-genome PLINK bfile plus the.synonymsfile MAGMA auto-detects for rsID remapping) mounted at/panels/magma_ref. The mixer signed-LD panel was considered and rejected: its BIM useschr:pos:alleleIDs that never align with rsID-based sumstats.
The three layers of the MAGMA pipeline, one kind each:
magma_annotate— assigns SNPs to genes, emits.genes.annot(formattsvso it wires straight into the two nodes below in one DAG).magma_gene_analysis—magma --bfile <g1000_eur> --pval <sumstats> N=<n>|ncol=N --gene-annot <annot> --gene-model <model>; emits the official.genes.outtable plus the.genes.rawintermediate.magma_gene_set—magma --gene-results <raw> --set-annot <sets>; emits the official competitive.gsa.outtable.
Design notes verified against the v1.10 manual (deviations from the original node-suite design doc):
- With p-value input the official default model is
snp-wise=mean;multiis the aggregate of the snp-wise mean and top models — the principal-components regression (linreg) model needs raw genotype data, so there is non_pcparameter to expose.modeldefaults tomultias the design doc planned, for its broader-architecture sensitivity. max_perc,min_snps, GSA permutations and a minimum-set-size filter have no v1.10 flags; none is emulated. GSA is analytical, andNGENESis reported per set for downstream filtering.- GSA reads
.genes.raw, not.genes.out— the gene-analysis node emits it as port 1 for exactly this hand-off. - Gene IDs are NCBI37.3 Entrez numeric IDs throughout. Gene-set files pass through untouched: MAGMA discards GMT description columns itself, but symbol-named sets (plain MSigDB GMTs) never match — use the Entrez editions.
MAGMA's positional readers and NCBI37.3 coordinate table make three input mistakes degrade quietly rather than loudly. The scripts pre-flight them:
- Summary-statistics tables rejected. MAGMA reads
--snp-locpositionally, so anSNP A1 A2 FRQ BETA SE P …table has its A1 allele parsed as the chromosome code.scripts/annotate.shvalidates the coordinate column and fails with an explicit error naming the column and the offending value. - GRCh38 coordinates rejected. Positions past the GRCh37 chr1 length
(308,832,240 bp) — or simply no parseable positions — exit non-zero with
a lift-over hint. Without this, mismatched coordinates match almost
nothing and the run either terminates with "no valid SNPs" or returns a
quietly underpowered annotation.
scripts/gene_analysis.shadds a post-hoc check on SNPs-per-gene for the same reason. - Symbol gene sets rejected.
scripts/gene_set.shsamples the gene fields of the set file and refuses a file whose IDs are mostly non-numeric (the shape of an MSigDB symbol GMT), instead of letting every set come back withNGENES=0.
magma_annotate also exposes the official annotation window as
window_kb (kilobases; --annotate window=N, default 10, 0 disables,
up,down with ignore_strand for asymmetric). Note the MAGMA 1.10 flag
is window=, not --gene-annot-window — the latter is an unknown flag.
MAGMA v1.10 is single-threaded and exposes no thread flag, so batches is
the only way to use more than one core: the official --batch P N mode
runs one batch of the genes per process, and --merge combines them. The
node launches the N batches concurrently and merges, which is why its
cpus is 8 while batches is capped at 8.
Two properties of this design, both verified against the binary:
- Merged output is indistinguishable from a single run — same file
names, same columns, same gene count (checked 2,189 genes: 1 run and
4 batches + merge produce byte-identical row counts and the merged
.genes.rawis whatmagma_gene_setconsumes). - The gene-level p-values differ from a single run by ~1e-4, but so do two identical single runs: the multi model's gene-gene correlation step is not deterministic, and batch mode inherits that, not adds to it. Top genes can reorder between runs at the 1e-4 scale.
Batch mode has a real cost: every batch process loads the 2.9 GB reference independently (~15 s each), so batching only pays off once the gene analysis itself dominates. On a small subset it is a net loss.
An earlier attempt to grow these nodes through the greenfield dev-plugin
lifecycle (~/.autonomics/plugins/dev/magma, kind magma_gene) is
policy-blocked ("panels are not allowed in the greenfield MVP") and took
a per-chromosome, custom-output-schema route; its empirically verified
flag spellings and the 8Gi multi-model OOM observation (memory here is
16Gi) informed this implementation.
The golden test (container-plugin/tests/magma_migration.rs) compares the
compiled ContainerCommandSpec against the legacy Rust wrapper's output:
image, panel bundle, outputs, resources, timeout are equal. The legacy
script was fully static — no optional flags, no gzip input handling — so
the plugin script is byte-identical to it (unlike the ldsc scripts, which
moved optional flags into env-driven if stanzas).
Two deliberate deltas:
- The kind drops the
_containersuffix (magma_annotate_container→magma_annotate), andartifact_prefixfollows the kind (/artifacts/magma_annotate, was/artifacts/magma_annotate_container). This is also the DSL-derived default whenartifact_prefixis omitted. - The legacy spec exposed
artifact_prefix/timeout_secsas per-instance overridable fields with defaults; the plugin DSL bakes them as fixed node-level manifest values (their validation rules — absolute path, non-zero timeout — are enforced at manifest load). The node therefore accepts only the empty spec{}.