Organizing data, experiments, and projects#

smftools separates three things that change at different rates and should live in different places: raw instrument output (never changes), one experiment's pipeline outputs (regenerable from raw + config), and a project that spans multiple experiments (a set of pointers, not a copy). This page lays out a directory convention for that split, walks through the typical experimentproject workflow, and covers what it takes to move or share that directory tree.

Typical workflow#

  1. Run the pipeline for one experiment. Either stage by stage:

    smftools experiment raw analyses/runs/<run_name>/experiment_config.csv
    smftools experiment preprocess analyses/runs/<run_name>/experiment_config.csv
    smftools experiment spatial analyses/runs/<run_name>/experiment_config.csv
    smftools experiment hmm analyses/runs/<run_name>/experiment_config.csv
    

    or as one wrapped call that respects each stage's normal skip/restart behavior:

    smftools experiment full analyses/runs/<run_name>/experiment_config.csv
    

    Processing many runs the same way is one batch call instead of a shell loop:

    smftools experiment batch full analyses/runs/config_paths.csv
    
  2. Register the experiment into a project. Create the project once, then add experiments to it as they finish (append-only — nothing is copied). Every pipeline stage that has run gets recorded, whether you point at the run's top-level output directory or at one stage dir specifically:

    smftools project init analyses/projects/<project_name>
    smftools project add analyses/projects/<project_name> \
        analyses/runs/<run_name>/<date>_outputs
    
  3. Query and combine across the project. project list shows registered experiments (including which stages each has reached), and the reference names smftools has harmonized across them (by sequence identity, so the same locus can be called different things in different experiments' FASTAs); project materialize resolves one canonical reference back to each matching experiment's own name and concatenates the slices (never a global merge):

    smftools project list analyses/projects/<project_name>
    smftools project materialize analyses/projects/<project_name> my_canonical_reference \
        -o analyses/projects/<project_name>/outputs/my_canonical_reference.h5ad.gz
    

    By default this pulls each experiment's most-derived available stage (HMM > spatial > preprocess > raw); pass --stage preprocess (or any other stage name) to pin all experiments to one specific stage instead, skipping any that haven't reached it yet. --read-metrics additionally attaches spatial's per-read outputs (autocorrelation, Lomb-Scargle) where available.

  4. Export raw reads across a project, e.g. for a re-analysis pipeline outside smftools, the same way you would for one experiment:

    smftools project export-fastq analyses/projects/<project_name> --outdir ./fastqs
    

Registering legacy (pre-partitioned-store) runs#

Older smftools runs produced a single monolithic .h5ad/.h5ad.gz per stage (e.g. <experiment>_preprocessed_duplicates_removed.h5ad.gz) instead of the partitioned spine + task-store layout described above. There's no need to convert these before joining a project — project add accepts a file path directly, and materialize() detects a legacy spine (no uns["is_spine"]) and reads it directly instead of through the partition machinery, so every later project query treats it the same as a modern run:

smftools project add analyses/projects/<project_name> \
    /path/to/<experiment>_preprocessed_duplicates_removed.h5ad.gz \
    --id <experiment> --stage preprocess

--stage names which pipeline stage the file represents; omit it and smftools guesses from the filename (_preprocessed → preprocess, _spatial → spatial, _hmm → hmm, ..., defaulting to raw). Register each stage file for the same experiment with repeated calls (same --id) — stages accumulate onto the same registry entry rather than replacing each other, so registering _hmm.h5ad.gz after _preprocessed.h5ad.gz doesn't lose the earlier one.

The legacy file itself is only ever read, never modified: reference identity for cross-experiment harmonization is computed on the fly from uns["References"] at registration time (falling back to it only when the file predates uns["reference_uids"]) rather than being cached back into the source. This keeps the original monolithic file byte-for-byte untouched, so it's safe to register into a project without disturbing whatever else still depends on it.

Every stage is visible to the project#

Within one experiment, later pipeline stages don't lose access to earlier stages' output — but the mechanism differs by what kind of data it is:

  • Per-read metadata (obs columns: QC flags, dedup status, ...) is fully cumulative. Each stage's derived spine is a copy of whatever spine it started from, so everything an earlier stage added is still there in a later stage's spine.

  • Per-position derived layers (binarized methylation, HMM state calls) live in each stage's own task store, addressed by a pointer a later stage's spine carries forward. materialize() resolves these transparently — an HMM task loads preprocess's derived layers as its own model input without them ever being duplicated into HMM's own store.

  • Spatial's per-read outputs (autocorrelation curves, Lomb-Scargle periodograms) are a different shape (read × lag, not read × position), so they're opt-in rather than loaded automatically: pass read_metrics=True (or a specific name subset) to materialize() to attach them.

The project registry builds directly on this: registering an experiment records every stage spine found for it, and project materialize's default stage fallback (most-derived first) means pointing at whichever stage happens to be furthest along per experiment already exposes everything upstream of it — you don't need to separately register or query each stage.

Portability#

As of smftools 2.1, everything a project or a later pipeline stage stores about where its own artifacts live is written as a path relative to a stable anchor (a run's output_directory, or a project's own directory) rather than an absolute, machine-specific string. Concretely:

  • A stage spine's cross-stage pointers (e.g. a spatial spine's pointer back to its source preprocess catalog) resolve relative to the run's output_directory, recovered structurally from wherever that spine file currently lives — not from a value baked in when it was originally written.

  • obs["bam_path"] on the raw spine resolves the same way, since the aligned BAM lives alongside the raw store under raw_outputs/bam_outputs/.

  • A project's registry.json stores each experiment's path relative to the project directory, the same way its runs/ symlinks already do.

Old spines/registries written before this (absolute strings) still work — the reader accepts both — so you don't need to regenerate anything already on disk.

What this means in practice: you can rsync/copy the whole analyses/ tree (or data/ + analyses/ together) to a different machine or mount point, at a different absolute path than the original, and every relative pointer (spine cross-references, project registry, runs/ symlinks) resolves correctly without editing or re-running project add.

What it does not cover: experiment_config.csv itself still holds absolute paths (input_data_path, fasta, output_directory) — those are user-supplied, not internal bookkeeping, so smftools has no way to know they moved. Two ways to handle that:

  • Keep the same absolute mount point on every machine (a shared lab server/NFS mount, or a consistently-named external drive) — configs never need editing, and the whole tree is portable with zero manual steps.

  • Edit the config's paths per machine when you genuinely relocate the data to a new absolute location. You only need to do this for experiment_config.csv; the pipeline's own outputs and the project registry don't need touching.

Sharing a project with a collaborator#

Split the same way the directory layout already splits code from data:

  • Version-control the small, text artifacts: registry.json, sets/, README.md, and any analysis scripts or manifests you've layered on top (e.g. a project.yaml/samples.csv if you maintain your own per-sample metadata alongside the registry). These are cheap, diff-friendly, and this is exactly what git is for.

  • Sync the data separately: the referenced analyses/runs/<run_name>/ directories (and data/<run_name>/ if your collaborator needs the raw instrument files too) are too large for git — use shared storage, rsync, or institutional data transfer instead.

  • Because paths are relative now, your collaborator can put the synced run directories anywhere on their machine, recreate the project's runs/ symlinks (or just re-run project add, which is idempotent), and everything resolves from there — no absolute-path coordination required between you.