Python API¶
Every command is a thin layer over these modules, and they can be used directly, from a notebook or another pipeline. For example, the published models on a cohort, without the command line:
from dietnet import package, predict
packages = package.find("models/packages") # every package below, in (seed, fold) order
matched = predict.matched_dataset(packages, "mycohort", "work/")
result = predict.predict(packages, matched, device="cpu")
result.table() # one row per sample: prediction, probability, model_agreement
result.scores # models x samples x classes
result.alignment # the matching counts
Upstream's own code is reached through dietnet.upstream: run executes one of
its scripts in a subprocess and modules imports its modules in this process.
The modules below call it; they never reimplement what it does.
Prediction¶
dietnet.predict ¶
Predict a cohort with one or more model packages, at biobank scale.
The arithmetic is upstream's: its DietNetwork and its indep_test_step
(imputation with the training means, standardisation, the D/(D-|C|) scale,
forward pass, softmax), which is what upstream's test_independent_dataset.py
runs; tests/parity checks the two give identical scores.
What differs is the I/O around it. Upstream's script loads the whole cohort
into memory, matches SNPs with a Python loop, and does both again for every
model. Here the cohort is matched once (allele-aware, :mod:dietnet.align) into
an HDF5 file under work_dir, then memory-mapped -- possible because the file
is contiguous -- and read once, a contiguous batch of samples at a time, with
every model scoring each batch before the next is read.
Result
dataclass
¶
table ¶
One row per sample: the ensemble's call and how much the models agree.
Source code in src/dietnet/predict.py
matched_dataset ¶
The cohort on the models' SNPs: built once, reused while inputs are unchanged.
Source code in src/dietnet/predict.py
Model packages¶
dietnet.package ¶
Model packages: one trained (seed, fold) model, self-contained and portable.
A package is a directory holding everything upstream's model needs, so that prediction never needs the training data or the experiment directory::
model.pt upstream's best_model.pt, as train.py saved it
config.yaml the training config (seed included)
embedding.npz ``emb``: this fold's SNP embedding (SNPs x features);
absent for the MLP baseline, which has no auxiliary net
input_features_stats.npz ``means_by_fold`` / ``sd_by_fold`` for this fold only
train_header.h5 ``snp_names``, ``class_label_names``, ``snps/``: the
parts of the training dataset upstream's classes read
metadata.json task, normalisation, seed, fold, upstream commit, ...
The model is rebuilt with upstream's own classes (DietNetworkHandler) and its
weights loaded strictly: a checkpoint that does not fit fails instead of leaving
layers at their random initialisation.
Package
dataclass
¶
handler ¶
Upstream's model handler with this package's weights, in eval mode.
Source code in src/dietnet/package.py
build ¶
build(
out,
*,
checkpoint,
config,
embedding,
means,
sds,
train_dataset,
task,
normalize,
seed,
fold,
model="Dietnet",
extra=None
)
Assemble a package from a finished upstream training run.
embedding is the fold's auxiliary-network input; None for the MLP,
which has no auxiliary network.
Source code in src/dietnet/package.py
find ¶
Every package at or below root, in (seed, fold) order.
Source code in src/dietnet/package.py
Matching a cohort¶
dietnet.align ¶
Match a new cohort's SNPs to a trained model's, by position and allele.
A dosage counts one allele, and which one PLINK 1.9 picks (A1, by default the minor allele) depends on the cohort's own frequencies. In the 1KGP training data a quarter of SNPs count the reference allele and the rest the alternate. So the same SNP can be coded x in one cohort and 2 - x in another; matching by id alone would feed the model mirror-coded genotypes for those SNPs, silently.
Per model SNP, against the cohort's .bim:
- same A1/A2 -> copy
- A1 and A2 swapped -> flip (2 - x; missing stays missing)
- the same on the other strand -> copy or flip, except for A/T and C/G SNPs, whose strand cannot be told from the alleles (a swap and the other strand look the same): those are kept only when both alleles match outright, and never flipped
- otherwise, or absent -> missing
Missing SNPs then follow the published method (Rochefort-Boulanger, Scicluna
et al. 2026; upstream's match_input_features): they are set to -1, which
imputation and standardisation turn into 0, and the observed ones are scaled
by D / (D - |C|), stored as scale in the matched dataset.
The matched dataset has upstream's schema for it (inputs, samples,
snp_names, scale), so upstream's test_independent_dataset.py reads it
with --matched-test-dataset.
Alignment
dataclass
¶
Where each model SNP comes from in the cohort, and whether to flip it.
align ¶
Align a cohort's .bim to model_snps (from :func:dietnet.dataset.snp_table).
Source code in src/dietnet/align.py
fingerprint ¶
Identifies an (model SNP set, cohort) pair, to reuse a matched dataset.
Source code in src/dietnet/align.py
write_matched ¶
Write the cohort on the model's SNPs, in upstream's matched-dataset schema.
Built a block of samples at a time and written as contiguous runs of rows: a biobank cohort is matched in bounded memory and with sequential writes.
Source code in src/dietnet/align.py
The HDF5 dataset¶
dietnet.dataset ¶
Build the HDF5 dataset upstream's scripts read, straight from PLINK.
Upstream makes it from a genotype text table (plink --recode A, then
create_dataset.py). This writes the same file from the .bed directly:
same datasets, same dtypes, same values (tests/parity), without the text
round-trip, which does not scale to a biobank.
Upstream schema, read by partition_data.py, generate_embedding.py, compute_input_features_statistics.py and train.py:
inputs int8, samples x SNPs, contiguous; dosage of the .bim A1, -1 missing
samples bytes, sample ids (.fam IID), in .fam order
snp_names bytes, <.bim id>_<A1>: the plink --recode A column
names, so the counted allele travels with the name (as in
the published 1KGP dataset: chr1:858952:G:A_A)
class_labels int64 index into class_label_names (classification)
class_label_names bytes, sorted unique labels
regression_labels float64 (regression)
Added here, ignored by upstream: snps/{chrom,pos,a1,a2} (what matching a new
cohort by position and allele needs) and provenance attributes.
The layout is contiguous and uncompressed on purpose: chunked or compressed layouts make the random minibatch reads training does 100-300x slower (docs/refactor-plan.md, section 2).
read_labels ¶
Sample id -> label from a TSV with a header row (upstream's format).
Source code in src/dietnet/dataset.py
build ¶
Write out (upstream's HDF5 schema) from a PLINK fileset.
Source code in src/dietnet/dataset.py
snp_table ¶
One row per SNP: name as stored, snp id, a1 counted allele, and
chrom, pos, a2 where they can be known.
Files built here carry them in snps/. Files made by upstream's
create_dataset.py, including the published 1KGP dataset, only have
snp_names; those are parsed: <id>_<A1> gives the counted allele, and
an id of the form chr:pos:ref:alt gives position and other allele.
Source code in src/dietnet/dataset.py
memmap_inputs ¶
inputs as a read-only memory map: no copy into RAM, pages read on demand.
Possible because the dataset is contiguous and uncompressed; HDF5 then stores it as one run of bytes at a fixed offset.
Source code in src/dietnet/dataset.py
Training¶
dietnet.training ¶
Train with upstream's train.py and package what it produces.
Training is upstream's script, run unmodified: the published model, loop, early stopping and checkpoint selection. This module supplies its inputs (one config per seed, since upstream reads the seed from the config), finds its results directory by upstream's own naming, and turns each (seed, fold) into a model package with the fold's held-out predictions next to it.
InputError ¶
Bases: ValueError
Training inputs upstream's model cannot learn from.
default_workers ¶
Loader processes: the CPUs this process may use (a SLURM allocation's, not the node's), less one for training itself, at most 8. Past ~8 the disk, not the CPUs, sets the pace (docs/scaling.md).
Source code in src/dietnet/training.py
fold_slice ¶
The fold's embedding from a per-fold (folds x SNPs x features) array, or a fold-independent (SNPs x features) one as it is -- the two shapes upstream's DietNetwork accepts.
Source code in src/dietnet/training.py
check_inputs ¶
Refuse per-fold inputs upstream's model cannot learn from, saying why.
input_stats may be None: generate-embedding checks before they exist;
embedding may be None: the MLP baseline has none.
Upstream's generate_embedding.py divides each class's genotype counts by the number of its samples in the fold's training split, and sizes the embedding by the largest class index present. A class with no training samples in a fold -- a population of one or two people, split five ways -- therefore either gives 0/0 (NaN, which spreads to every weight: the model trains to NaN and predicts the first class for everyone) or, if it is the last class, silently drops its columns. Either way the class cannot be learned. Caught here, before any training time is spent.
Source code in src/dietnet/training.py
train ¶
train(
*,
dataset,
partition,
embedding,
input_stats,
config,
exp_dir,
seeds,
folds,
task="classification",
normalize=True,
model="Dietnet",
inputs="auto",
loader="fast",
workers=None,
gpus=1,
engine=None
)
Train every (seed, fold); return their packages.
model is upstream's: "Dietnet", or "Mlp", the baseline without an
auxiliary network, which takes no embedding.
engine "upstream" (the default on one GPU) runs upstream's train.py,
unmodified, fed so that the trained model is bit-identical to train.py's:
inputs: the genotype store it reads (:mod:dietnet.genotypes): "ram" (upstream's copy of the whole dataset), "memmap", "packed", or "auto".loader: "fast" reads each batch in one call withworkersloader processes (default :func:default_workers) and fills missing values in one operation; "upstream" leaves train.py's own loader alone.
engine "dietnet" (the default, and the only choice, for gpus > 1)
trains with :mod:dietnet.parallel: upstream's model and epoch logic in a
loop of its own, data-parallel over gpus, packed genotypes decoded on the
GPU, without train.py's per-batch bookkeeping; on Mila's A100 its training
pass ran ~20x upstream's (docs/scaling.md). Its weights differ from train.py's.
Source code in src/dietnet/training.py
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 | |
SNP features¶
dietnet.features ¶
Per-SNP features for the auxiliary network, from several sources.
Upstream's DietNetwork reads its auxiliary-network input from embedding.npz:
a SNPs x features matrix, one per fold (3-D) or shared by all folds (2-D),
whose columns it L2-normalises itself. Anything that can be written as such a
matrix can feed the model, without touching upstream's code. This module
builds one from blocks, described in a YAML spec::
blocks:
- provider: class_genotype_freq # upstream's embedding (per fold)
- provider: precomputed # any per-SNP table, e.g. AlphaGenome scores
path: alphagenome_scores.tsv
orient: effect # values are ALT - REF effects
pca: 64 # reduce to 64 components
- provider: random # control: same width, no information
columns: 64
seed: 0
Providers:
class_genotype_freq
Genotype frequencies per class and fold: upstream's generate_embedding.py,
run unmodified. Alone, the result is exactly what dietnet
generate-embedding writes.
genotype_freq
Genotype frequencies per fold over all its training samples, classes
pooled: the same definition without the split by class, for regression
tasks (which have no classes) or as a baseline. Read a block of samples at a
time from a genotype store (inputs: as for train), so it scales to
a biobank.
precomputed
A table with chrom, pos, ref, alt and numeric feature
columns (TSV, optionally gzipped, or parquet), matched to the dataset's SNPs
by position and alleles, on either strand. With orient: effect a value
describes ALT relative to REF, so it is negated where the dataset counts
REF (plink 1.9 makes the minor allele A1, which is REF for a quarter of the
1KGP SNPs). SNPs the table lacks get zeros and are marked in an added
<name>:missing column.
random
Standard-normal columns: the control for "more columns" rather than "more
information".
shuffled
Another block with its SNP rows permuted (of: that block's index): the
control for "this information" rather than "information of this shape".
Transforms per block, applied after the provider: center: true subtracts
each column's mean over SNPs; pca: k keeps the first k principal
components over SNPs (computed per fold for a per-fold block). Upstream's
per-column L2 normalisation undoes any per-column scaling, so these are the
transforms that matter: centering, and controlling how many columns -- and so
how much weight -- a block brings.
Every column must vary: an all-zero column would be divided by a zero norm inside the model and turn it to NaN, so building refuses one.
Block
dataclass
¶
A provider's output: SNPs x columns, or folds x SNPs x columns.
build ¶
Write out (upstream's embedding.npz) and <out>.json from a spec.
Source code in src/dietnet/features.py
dietnet.alphagenome ¶
AlphaGenome variant-effect scores for a dataset's SNPs, as an auxiliary-network feature table.
AlphaGenome (Avsec et al.) predicts many genomic tracks from DNA sequence, and
scores a variant by how its ALT allele changes them relative to REF. Through
the hosted API (pip install 'dietnetwork[alphagenome]', key in
ALPHAGENOME_API_KEY), :func:score turns every SNP of a dataset into a row
of such effects:
chrom pos ref alt <output_type>|<track_name> ...
one signed ALT-minus-REF raw_score per track. Gene-level scorers report
one score per nearby gene; the gene with the largest effect on the track is
kept. The table is what build-embedding's precomputed provider reads,
with orient: effect: it flips the sign wherever the dataset's dosage
counts REF rather than ALT, and pca: k reduces the thousands of tracks to
a block the size of the rest of the embedding.
Scoring is resumable: rows are appended a chunk at a time, and a rerun skips SNPs already in the file. Hundreds of thousands of API calls will be interrupted at some point.
REF and ALT come from SNP ids of the form chr:pos:REF:ALT (the 1KGP
data's), since a .bim's A1/A2 say which allele is counted, not which is the
reference. Coordinates must be GRCh38, which is what AlphaGenome uses.
Backend ¶
Bases: Protocol
score ¶
Tidy scores for variants: one row per (variant, track[, gene]) with
chrom, pos, ref, alt, output_type, track_name,
raw_score.
AlphaGenomeAPI ¶
The hosted AlphaGenome model, through its Python client.
Source code in src/dietnet/alphagenome.py
variants_of ¶
chrom (chr-prefixed), 1-based pos, ref, alt for every SNP in a dataset, or
in a PLINK .bim: only its SNP ids are read, so no genotypes are needed.
Source code in src/dietnet/alphagenome.py
features_of ¶
One row per variant, one <output_type>|<track_name> column of raw scores.
Where a track has a score per gene, the one with the largest magnitude is kept (signed).
Source code in src/dietnet/alphagenome.py
score ¶
Score every SNP of dataset (or a .bim), then write the feature table out.
Each chunk's scores go to their own file under <out>.parts/ as soon as
they arrive; a rerun scores only the SNPs no part holds yet. When all are
scored the parts are combined: the union of their tracks, with 0 (no
effect) where a variant has no score for a track -- a gene-level scorer
says nothing about a variant with no gene in range.
Source code in src/dietnet/alphagenome.py
The published models¶
dietnet.published ¶
The published models (Rochefort-Boulanger, Scicluna et al., AJHG 2026) as packages.
Zenodo record 21133813 (concept doi:10.5281/zenodo.16943452), file
Dietnet_training.tar.gz, holds the 15 trained models (3 replicates x 5
folds), the configs they were trained with (one per replicate; they differ
only in seed), the per-fold SNP embeddings and normalisation statistics, and
the 1KGP training dataset. :func:import_training_release turns that into
model packages, so dietnet predict runs the published models.
fetch ¶
Download and unpack Dietnet_training.tar.gz into dest; return its directory.
Source code in src/dietnet/published.py
import_training_release ¶
Packages for the 15 published models from an unpacked Dietnet_training/.