Run outputs¶
Each pipeline run writes a run directory beneath run.output_dir.
It holds the configuration as given and as resolved, per-fold
artifacts, and the metrics needed to compare experiments reproducibly.
The shared cache, which stores reusable upstream artifacts such as tiling and feature extraction, is documented separately in Caching.
Experiment identity¶
Managed outputs group runs by experiment identity, with a timestamp/W&B suffix
for each run. A run started in the same second as an earlier run of the same
experiment gets -2, -3, … appended instead of reusing its directory.
Identity version 9 hashes the train and tune manifest
slices and the run’s plan (see The plan): the features section as
extraction resolves it, the cohort options, the loaders’ batch size, sampler and
augmentation, the learner with every leaf’s params, the trainer and the
evaluation. Representation-only runs instead hash their configured evaluation
split and representation settings.
The run section (output directory, seed, run id, resume, mirror root, fold
selection), the cohort file paths, the loader worker settings,
trainer.device, the grid-output save toggles and
evaluation.overwrite_test do not change experiment identity. Enabled
attention-heatmap settings do, because they change generated artifacts.
Adding or changing test* rows leaves the training experiment identity
unchanged. A separate test digest identifies the test assignments and their
sample rows; test_results.json records test identities and guards against
accidental re-scoring. See Evaluation for test holdout and overwrite
controls.
Dataset checksums follow semantic manifest identity: storage paths are excluded while semantic values in the selected rows are hashed. Run metadata still records the resolved manifest paths and whole-file SHA-256 values, so relocation or added test rows remain visible in provenance. Cache identity separately includes physical inputs as described in Caching.
Run directory layout¶
The run directory holds config.yaml (the config as given), plan.yaml
(the config as resolved, see The plan) and summary.json.
When splits.csv has no fold column (or a single fold value), the
per-fold artifacts are written directly inside it; with several folds, each
fold_N/ subdirectory holds its own set:
bundle/, the trained model (see Model bundle),metrics.jsonandtraining_history.jsonfeature_adapter.jsonwhennormalizationorprojectionis active: the methods,epsfloor and floored-channel count, dimensions, projection seed, PCA explained variance, and the number of samples and feature rows used to fit each stagepredictions_<split>.parquetfor each test split of a slide, patient or tile run, andsubgroup_metrics_<split>.jsonwhen subgroup columns are configuredattention/<split>/<sample_id>.npzandheatmaps/<sample_id>.pngwhenevaluation.attention_heatmapsis setfor segmentation and detection, the predictions of tune and every test split and the optional overlays, probabilities and heatmaps; see Segmentation and detection outputs
The plan¶
soma.resolve() turns a config into a soma.Plan before anything
runs: the config with every default of every component’s params spelled out,
plus what the cohort decides (for example a classification head’s label map or
a survival head’s bin count). A value the config states wins over what the
cohort suggests; a contradiction (an explicit class count the cohort does not
match) is refused. Feature dimensions are not in the plan: they come from the
features at build time and are recorded in the bundle’s model.json.
plan.yaml is the plan as YAML. It loads as a config, so soma run
plan.yaml replays the run with every value pinned. A component named by a
dotted import path keeps that path in the plan, and the bundle’s
provenance.json records the installed version of each top-level package such
a path imports from (<package>_version); versions are not part of the plan.
A resume into an existing run directory (run.resume or run.run_id)
compares the current plan with the saved plan.yaml and refuses on any
difference, except where and how the launch runs: run.output_dir,
run.run_id, run.resume, run.mirror_root and run.folds. A changed
run.seed is drift. GPU and preprocessing worker counts the config leaves unset
stay unset in the plan, so a run resumes on another machine. The cohort files’
content is then checked against the run’s experiment identity.
Model bundle¶
Each fold saves its selected model as a bundle with soma.save(). A bundle
is a directory of four files:
weights.safetensors: the model’s fullstate_dict. The fitted feature adaptor (normalization/projection) is stored here as buffers.model.json: how to rebuild the model before the weights load: its shape, the builder and the builder’s arguments (aggregator or decoder, task head, adaptor sections, and the encoder of a live run).preprocessing.json: what scoring needs beyond the model: the task family, class names, target schema, metrics, and for grid runs the grid geometry, read spacing and tile size. A detection bundle also records the score threshold chosen on tune and the matching distance.provenance.json: the soma, slide2vec and hs2p versions, the version of each package a dotted-path component comes from, the selected epoch and its monitor value, the fold, the run id and the identity of the feature source (the cache key and feature identity, not a path).
soma.load() rebuilds the model in eval mode and returns a
soma.ModelBundle with model, preprocessing and provenance.
A live run’s frozen encoder is built when the bundle loads, so missing encoder
weights fail at load time. A bundle written by another soma major version is
refused; another minor or patch version loads with a warning:
import soma
bundle = soma.load("runs/.../fold_0/bundle")
predictions = soma.predict(bundle.model, loader)
soma.save writes the bundle atomically: a reader sees the previous bundle
or the new one, never a partial one. A model built by hand in a notebook can be
saved too; soma.load rebuilds it only when soma.learn.record_builder
recorded the function that builds it, and otherwise refuses: load its
weights.safetensors into the same model instead.
Resume is per fold. A fold is complete when its metrics.json and its
bundle/ exist. A relaunch into the same run directory (run.resume or
run.run_id) skips complete folds and trains the others from epoch 0.
Split-specific artifacts¶
When a dataset defines multiple test splits, each split gets its own set of
artifacts, e.g. predictions_test.parquet and
predictions_test_external.parquet.
Prediction files¶
predictions_<split>.parquet holds one row per sample of the split: a
sample_id column, the columns the task head declares (prob_0 …
prob_<k-1> and predicted_label for classification, predicted_label
and raw_score for ordinal classification, predicted_value for
regression (value_0 … value_<G-1> with num_outputs: G),
risk_score for survival), any column a custom learner adds, and
is_placeholder. A placeholder row is a baseline prediction for a sample
without features (see Evaluation). Targets are not stored here; they
come from dataset.csv. The parquet schema metadata records the task family,
the soma version, the role of each column and the class label of each
probability column. Read the file with
soma.evaluation.Predictions.from_parquet() and score it again with
soma evaluate (see CLI).
Metric keys in summary.json are prefixed by split name:
Single fold:
test/auroc,test_external/aurocCross-validation:
test/auroc_mean,test/auroc_std
*_std is the sample standard deviation (ddof=1) across folds. Threshold-free
metrics (AUROC, AUPRC, C-index, …) are nan on a fold whose split holds a single
class or no comparable pairs; such folds are excluded from *_mean / *_std and
counted in test/<metric>_nan_folds, which is only written when the count is non-zero.
Segmentation and detection outputs¶
Segmentation and detection runs write predictions for tune and for every test split:
Segmentation:
predictions_<split>/, one PNG label map of class indices per sample and anindex.parquet. Read it withsoma.evaluation.LabelMapPredictions.load().Detection:
predictions_<split>.parquet, the level-0 point listsample_id, x, y, class, scorewith every peak above the head’smin_score, before any threshold. Read it withsoma.evaluation.PointPredictions.from_parquet().
soma evaluate scores either again (see Segmentation and detection predictions). Optional
artifacts follow evaluation settings:
Setting |
Writes |
Default |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Per-sample confusion matrices are not written; recompute them from the saved
label maps with soma.evaluation.confusion_records().
Run index¶
<run.output_dir>/indexes/runs.csv is append-only: every status change of a
run appends one line, and readers keep the last line per run_id. Run
soma compact-index <output_dir> to rewrite it with one row per run.
Heatmap artifacts¶
When evaluation.attention_heatmaps is set, the pipeline scores each fold’s bundle
once every fold is done and stores raw attention scores in
attention/<split>/<sample_id>.npz and rendered overlays in heatmaps/
(directly in the run directory for single-fold runs, inside each fold_N/
subdir for cross-validation). The rendered overlays can be regenerated with
different visual settings without rerunning inference.
Aggregators that support attention extraction: abmil, clam_sb,
clam_mb, dsmil. Heatmaps are skipped for mean_pool, max_pool,
transmil, dtfdmil, and hipt.
Heatmap appearance is set by the keys of evaluation.attention_heatmaps
({} keeps the defaults): cmap (colormap name, default coolwarm),
alpha (overlay opacity, default 0.5), blur_sigma (Gaussian blur radius
in pixels, default 0).
Log output¶
soma reports progress through Python’s logging module, under the soma
logger. The soma command prints soma’s INFO messages to stderr; other
libraries print warnings and errors only.
In a Python script, soma checks the logging setup when you create a
Pipeline or call soma.extract:
If you have not configured logging (no handler on the root logger or on the
somalogger), soma adds one stderr handler to thesomalogger and sets that logger to INFO. You see messages such as the fold summary without any setup. soma adds this handler only once. A level you set yourself is kept: on thesomalogger, or on the root logger when it is not Python’s default WARNING (logging.getLogger().setLevel(logging.ERROR)keeps soma quiet).If you have configured logging, for example with
logging.basicConfig, soma changes nothing and your configuration decides what is shown.If you configure logging after you create a
Pipeline, soma’s handler stops writing as soon as the root logger or thesomalogger has a handler of yours, so each message prints once, through your handlers. Thesomalogger stays at INFO. If you setpropagate = Falseon thesomalogger, its messages never reach your root handlers, so soma’s handler keeps printing them unless you add a handler to thesomalogger.
To change the output:
import logging
soma_logger = logging.getLogger("soma")
soma_logger.setLevel(logging.WARNING) # hide soma's INFO messages
soma_logger.setLevel(logging.DEBUG) # or show soma's DEBUG messages too
soma_logger.handlers.clear() # remove soma's default handler
soma_logger.propagate = False # keep soma out of your root handlers