Thanks for your interest in improving SpaceBlocks!
This guide covers the architecture you need to know, the conventions the codebase relies on, and how to validate a change before opening a PR. For the full design rationale, see the documentation.
SpaceBlocks is split into two halves that meet at one object (the full picture is in Design & architecture):
- a technology-specific HeadBlock (
visiumhd,xenium5k,atera, ormerscope) turns raw data into a standardized AnnData object saved as an h5ad file, which we refer to as a contract h5ad for brevity; - a technology-agnostic CoreBlock validates that contract and runs all the analysis.
mode: decoupled skips the head entirely and consumes contract h5ads you provide. The contract is the only coupling point: get it right and everything downstream just works.
Its exact shape (raw counts in X, obsm["spatial"], optional uns["spatial"] image, obs keys) is specified in Design → the validation object contract, and validate_input enforces it before any core rule runs.
The practical consequence: to support a new platform you write a new HeadBlock that emits the contract, and the CoreBlock is untouched.
workflow/
├── Snakefile globals, mode selection, named targets
├── rules/*.smk one file per rule (+ common.smk for shared helpers)
├── scripts/*.py,*.R rule implementations
├── envs/*.yaml one Conda env per rule group (+ *_linux-64.lock)
└── schemas/*.yaml config + sample-sheet validation
config/ config.yaml, README.md (config reference), sample sheets
docs/ the MkDocs site
.test/ tiny synthetic decoupled fixture for CI
reproduction/ public-data worked examples
git clone https://github.com/cbib/SpaceBlocks && cd SpaceBlocks
# Snakemake >= 8 and Conda/Mamba are the only host requirements.
snakemake -n --sdm conda # dry-run: builds the DAG, validates the config, provisions envs
snakemake -s workflow/Snakefile -d .test -n --workflow-profile none # decoupled smoke testEach rule group has its own Conda env, provisioned by --sdm conda. Keep the loose envs/*.yaml
as the maintainable surface; the *_linux-64.lock files are the exact-reproducibility surface (see
Environments).
These are load-bearing, and most past bugs we experienced during development came from breaking one of them.
- Targeted changes. Add, don't reformat. Keep diffs minimal and reviewable; don't restructure code you aren't changing.
- Rules pass params explicitly. A
.smkrule reads fromconfigand passes values throughparams:; the script readssnakemake.params, neverconfigdirectly. After any change, cross-check that every param name matches between the.smkand its script. - Resources scale with retries. Don't hardcode resources in a rule. Every compute rule draws
mem_mb/runtime/threadsfromconfig["resources"](with adefault), andmem_mbgrows with the attempt number. - The contract convention.
obs["cell_id"]must equalobs_names(as strings); downstream joins key on it. Head-produced contracts live atSAMPLES_DIR/{sample}/{sample}_unfiltered.h5ad(nested); decoupled contracts live atcontract_dir/{sample}.h5ad(flat). - String indices when reading TSVs. When a metadata/annotation TSV is read with
pd.read_csv(..., index_col=0), castdf.index = df.index.astype(str)before joining onobs_names. All-numeric cell ids are otherwise inferred asint64and silently misalign. - Guard plotting. Wrap plot generation in
try/except(Python) /tryCatch(R) so one failed figure doesn't crash the rule. - Palettes. SpaceBlocks allows color palette customization directly from the config. Levels not referenced in config fall back to grey.
When no palette is configured for a column, leave colours to scanpy and drop any stale
*_colorsfromuns.
A new rule. Add workflow/rules/<name>.smk (with params:) + workflow/scripts/<name>.py, a
resources.<name> block, and (if it introduces config keys) the corresponding entries in
workflow/schemas/config.schema.yaml. Wire it into the relevant named target
(run_preprocessing / run_postprocessing / run_exploration) in the Snakefile.
A new platform (HeadBlock). Add a rules/*_<code>.smk chain that emits the contract, a <code>
entry in KNOWN_HEADS and the mode schema enum, and its env under envs/. The CoreBlock needs no
changes. See the xenium5k head as a reference implementation.
Run the applicable checks locally before pushing. The first two mirror CI and use the committed
mode: decoupled fixture under .test/:
# 1. Workflow parses + lints (against the committed test fixture)
snakemake -s workflow/Snakefile -d .test --lint --workflow-profile none
# 2. The decoupled CoreBlock DAG builds
snakemake -s workflow/Snakefile -d .test -n --workflow-profile none
# 3. Config still validates against the schema
python -c "import yaml,jsonschema; jsonschema.validate(yaml.safe_load(open('config/config.yaml')), yaml.safe_load(open('workflow/schemas/config.schema.yaml'))); print('Configuration schema validation passed')"
# 4. Docs build cleanly (only if you touched docs/)
pip install mkdocs-material pymdown-extensions
mkdocs build --strictBefore a catalogue release, generate the decoupled rule graph once manually:
snakemake -s workflow/Snakefile -d .test --forceall --rulegraph --workflow-profile none > pipeline_rulegraph.dotRendering the DOT file to SVG is optional and requires Graphviz:
dot -Tsvg pipeline_rulegraph.dot > images/rulegraph.svgIf you modify a HeadBlock, also dry-run the affected mode using a complete platform-specific configuration and representative mock or real inputs. Changing only mode in the generic config/config.yaml is not sufficient because its input paths are placeholders. Supported modes are visiumhd, xenium5k, atera, merscope, and decoupled.
Quick per-file sanity checks are cheap and worth it: python -c "import ast; ast.parse(open('file.py').read())"
for Python, and yaml.safe_load for any YAML you edit.
Docs live in docs/ and are built with MkDocs Material; mkdocs build --strict must pass
(broken links and anchors fail the build). Inside docs/, use relative .md links: MkDocs
rewrites them and --strict validates them. In files rendered outside docs/ (the root
README.md, config/README.md), use absolute https://cbib.github.io/SpaceBlocks/... URLs with
a trailing slash. config/README.md is also scraped by the Snakemake Workflow Catalog, so keep it
self-contained.
Two workflows run on every PR and must pass:
tests—snakemake --lintand a decoupled dry-run against.test/.docs—mkdocs build --strict. Deployment to GitHub Pages is gated behind the repo variablePUBLISH_DOCS=trueand only runs onmain.
- Branch from
main, keep the change focused, and make sure the validation commands above pass. - Write a clear PR description: what changed and why. If behaviour changed, update the affected
page under
docs/. - For bug fixes, a one-line note of the root cause in the PR helps reviewers.
Questions or a larger design idea? Open an issue first so we can scope it together — especially anything touching the contract or the HeadBlock/CoreBlock boundary. Thanks for contributing!