Contributing to Chorus
August 17, 2026 · View on GitHub
Thank you for your interest in contributing to Chorus! Most of this guide is a step-by-step for implementing a new oracle (a genomic sequence prediction model), because that is the largest kind of contribution — but it is not the only one we want, and the smaller ones are not lesser.
Start here — what are you contributing?
| you want to add | go to | rough size |
|---|---|---|
| a new model / oracle | Implementing a new oracle below | a day or more; 9 registration sites, an env, a background |
| a worked example or walkthrough | Contributing an example | an hour; one list entry and one script run |
| a bug fix, a doc fix, a test | Running the tests then open the PR | minutes |
You do not need a HuggingFace account, our GPU, or our HuggingFace dataset for any of these — see You do not need our infrastructure.
Overview
Chorus provides a unified interface for genomic sequence oracles. Each oracle runs in its own isolated conda environment to avoid dependency conflicts. To add a new oracle, you'll need to:
- Create the oracle implementation
- Define the conda environment requirements
- Implement required methods
- Add tests and examples
- Submit a pull request
Step-by-Step Guide to Implementing a New Oracle
Step 1: Fork and Clone the Repository
# Fork the repository on GitHub, then:
git clone https://github.com/YOUR_USERNAME/chorus.git
cd chorus
python -m pip install -e .
Step 2: Create Your Oracle Implementation
Create a new file in chorus/oracles/ named after your oracle (e.g., mymodel.py):
# chorus/oracles/mymodel.py
"""MyModel oracle implementation."""
import numpy as np
from typing import List, Dict, Optional, Tuple, Union, Any
import logging
from ..core.base import OracleBase
from ..core.exceptions import ModelNotLoadedError
logger = logging.getLogger(__name__)
class MyModelOracle(OracleBase):
"""MyModel oracle implementation."""
def __init__(self, use_environment: bool = True, reference_fasta: Optional[str] = None):
"""
Initialize MyModel oracle.
Args:
use_environment: Whether to use isolated conda environment
reference_fasta: Path to reference genome FASTA file
"""
# Set oracle name before calling super().__init__
self.oracle_name = 'mymodel'
super().__init__(use_environment=use_environment)
# Model-specific parameters
self.sequence_length = 524288 # Example: MyModel uses 524kb sequences
self.bin_size = 128
self.num_tracks = 7919 # Example track count
# Store reference genome path
self.reference_fasta = reference_fasta
# Model components (will be loaded later)
self._model = None
Step 3: Implement Required Methods
Your oracle must implement these abstract methods from OracleBase:
3.1 Model Loading
def load_pretrained_model(self, weights: Optional[str] = None) -> None:
"""Load pre-trained model weights."""
if weights is None:
weights = "default_model_path_or_url"
logger.info(f"Loading {self.oracle_name} model from {weights}")
if self.use_environment:
# Run loading in isolated environment
load_code = f"""
import torch # or tensorflow, depending on your model
# Your model loading code here
model = load_your_model('{weights}')
result = {{'loaded': True, 'description': 'Model loaded successfully'}}
"""
result = self.run_code_in_environment(load_code, timeout=300)
if result and result['loaded']:
self.loaded = True
logger.info(f"{self.oracle_name} model loaded successfully!")
else:
raise ModelNotLoadedError(f"Failed to load {self.oracle_name} model")
else:
# Direct loading if not using environment
self._load_direct(weights)
3.2 Track Information
Implement _describe_tracks() — the one method that answers "what can this oracle predict?".
Return a list of TrackRecord objects whose track_id is exactly what your
predict(..., assay_ids=[...]) accepts:
def _describe_tracks(self) -> list:
from ..core.tracks import TrackRecord
return [
TrackRecord(track_id=f"{assay}:{cell}", assay=assay, cell_type=cell,
description=f"{assay} in {cell}")
for cell in MY_CELL_TYPES for assay in MY_ASSAYS
]
Three rules, each learned the hard way:
- Make it work before
load_pretrained_model(). Discovery should not cost a multi-GB model load, andtests/test_describe_tracks_is_uniform.pyasserts it. - Populate
assayon every record.classify_track_layerdispatches on it, and a null one classifies asother, whose scorer config isNone— so the track produces no score. Sei shipped 21,907 such tracks: built, verified, and unscoreable. - Enumerate from the same source your background builder uses. ChromBPNet's raw metadata table offers 1,268 cell/TF combinations against a 753-row null; reading it directly instead of the builder's own helper reproduces exactly that gap, and a catalogue that disagrees with the background is worse than none — callers filter on it and then find no percentile.
describe_tracks() (the public wrapper) is provided by the base and handles query / limit
filtering, so all oracles share one search behaviour. Do not override it.
The two older methods below are still required:
def list_assay_types(self) -> List[str]:
"""Return list of available assay types."""
return [
"DNase", "ATAC-seq", "ChIP-seq", "RNA-seq",
# Add your model's supported assay types
]
def list_cell_types(self) -> List[str]:
"""Return list of available cell types."""
return [
"K562", "GM12878", "HepG2", "H1-hESC",
# Add your model's supported cell types
]
3.3 Prediction Method
def _predict(self, seq: Union[str, Tuple[str, int, int]], assay_ids: List[str]) -> np.ndarray:
"""
Make predictions for given sequence and assays.
Args:
seq: Either DNA sequence string or (chrom, start, end) tuple
assay_ids: List of assay identifiers
Returns:
numpy array of shape (num_bins, num_tracks)
"""
if not self.loaded:
raise ModelNotLoadedError("Model not loaded")
# Handle genomic coordinates if provided
if isinstance(seq, tuple):
if self.reference_fasta is None:
raise ValueError("Reference FASTA required for coordinate input")
chrom, start, end = seq
# Use the utility function to extract sequence with padding
from ..utils.sequence import extract_sequence_with_padding
seq = extract_sequence_with_padding(
self.reference_fasta, chrom, start, end, self.sequence_length
)
if self.use_environment:
# Run prediction in isolated environment
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
f.write(seq)
seq_path = f.name
predict_code = f"""
# Read sequence
with open('{seq_path}', 'r') as f:
seq = f.read().strip()
# Your prediction code here
import torch # or tensorflow
model = load_cached_model() # Load from cache
predictions = model.predict(seq, {repr(assay_ids)})
result = predictions.tolist()
"""
predictions = self.run_code_in_environment(predict_code, timeout=120)
return np.array(predictions)
else:
# Direct prediction
return self._predict_direct(seq, assay_ids)
3.4 Required Helper Methods
def _get_context_size(self) -> int:
"""Return the required context size for the model."""
return self.sequence_length
def _get_sequence_length_bounds(self) -> Tuple[int, int]:
"""Return min and max sequence lengths accepted by the model."""
return (1000, self.sequence_length)
def _get_bin_size(self) -> int:
"""Return the bin size for predictions."""
return self.bin_size
Step 4: Define the Conda Environment
Create an environment configuration that we can integrate into the setup system. Provide us with:
- Conda packages needed:
# Example for a PyTorch-based model. Compare against a real one, e.g.
# environments/chorus-sei.yml, which carries an in-file note on why its old
# pytorch<2.0 + cudatoolkit=11.7 pin was itself the bug.
name: chorus-mymodel # REQUIRED — EnvironmentManager derives both the env name
# and the file path as chorus-{oracle}
channels:
- pytorch
- conda-forge
- bioconda
- defaults
dependencies:
- python=3.10
- pytorch>=2.0.0
- torchvision
- numpy
- pandas
- scikit-learn
- pysam
- bedtools
- pip
- pip:
- your-special-package==1.0.0
- Installation commands:
# Any special setup commands
# For example, downloading model weights:
wget https://example.com/model_weights.pt -O ~/.cache/mymodel/weights.pt
Step 5: Register Your Oracle
This guide is the canonical one.
environments/README.mdanddocs/IMPLEMENTATION_GUIDE.mdused to carry their own shorter recipes and both were wrong — the latter told contributors to edit anORACLE_REGISTRYthat has never existed in this codebase. They now point here. If you change the registration surface, change it here.
Registration is not automatic. Dropping a chorus-{name}.yml into environments/ makes your
oracle appear in chorus list, and that is genuinely all it does — the oracle is not loadable,
not scoreable, and invisible to the MCP server until every site below names it. There is no
single registry; these are hand-edited, and the list is in dependency order.
Required — without these the oracle does not work at all:
-
chorus/oracles/mymodel.py— the class (Steps 2–3 above). It must declaretraining_genomeon the subclass.OracleBase.training_genomeis deliberatelyNoneso a new oracle cannot inherit"hg38"by saying nothing;tests/test_genome_is_asserted_not_assumed.pyenumerates the subclasses and fails if yours is silent. -
chorus/oracles/__init__.py— the import, theORACLESdict, and__all__:from .mymodel import MyModelOracle ORACLES = {'enformer': EnformerOracle, 'mymodel': MyModelOracle, ...} -
chorus/__init__.py(create_oracle) — oneelifbranch and the valid-names string in theelse. Miss the string and the error message for a typo silently omits your oracle:elif oracle_name.lower() == 'mymodel': from .oracles.mymodel import MyModelOracle return MyModelOracle(use_environment=True, **kwargs) -
chorus/mcp/server.py—ORACLE_SPECS.tests/test_mcp.pyasserts the exact key set, so the suite goes red until you add yours; that is intentional.
Required for the CLI to report your oracle honestly:
-
chorus/core/weights_probe.py—_ARTIFACT_PROBES, sochorus healthandchorus setupcan tell "not installed" from "unhealthy" without spawning a subprocess. -
The two dependency probes —
chorus/core/environment/runner.py'sdependenciesandchorus/core/environment/manager.py'soracle_deps. These are near-duplicates that have drifted apart before: as of this writingrunnerwas missing bothcherimoyaandalphagenome_pt, andmanagerwas missingalphagenome_pt, sochorus healthreported Healthy for those two even with a broken env, because an absent key means an empty dependency list rather than an error. Add yours to both. -
chorus/cli/_setup_prefetch.py—_DEFAULT_CTOR_KWARGS/_DEFAULT_LOAD_KWARGS, needed whenever a bareload_pretrained_model()will not work. The file's own comments record LegNet raisingTypeErrorand Cherimoya raisingInvalidAssayErrorfrom getting this wrong. -
chorus/cli/_backgrounds.py_KNOWN_ORACLESandchorus/cli/_cleanup.py— sochorus backgrounds statuslists you andchorus cleanup --oracle mymodelremoves you. -
environments/chorus-mymodel.yml— the filename is load-bearing:EnvironmentManager.list_available_oraclesglobschorus-*.ymland strips the prefix. Include thename: chorus-mymodelkey (Step 4).
Required before any percentile is meaningful:
- Background nulls — read
docs/BACKGROUND_NULL_PROTOCOL.md§8, "Adding a new oracle", and follow it. This is not optional polish. Ifclassify_track_layerreturns"other"for your track ids, every score comes backNonewith no error — Sei shipped 40 built, verified, unreachable rows that way for months. You will also needscripts/build_backgrounds_mymodel.pyand anACTIVITY_POPULATIONSentry inscripts/stamp_provenance_v4.py.
You do not need our infrastructure to develop, or to open the PR
Read plainly, step 10 sounds like a wall: percentiles need a per-track null, the canonical nulls live in a HuggingFace dataset you cannot write to, and building one means scoring ~18,000 positions through your model. None of that has to happen before you open a PR, and two supported paths exist. Both are load-bearing code, not workarounds:
# (a) point chorus at a background you host yourself
export CHORUS_BACKGROUNDS_REPO=your-username/your-backgrounds
# (b) or keep it entirely local — no HuggingFace account involved
from chorus.analysis.normalization import get_pertrack_normalizer
norm = get_pertrack_normalizer("mymodel", cache_dir="/path/holding/mymodel_pertrack.npz's/dir")
cache_dir is checked before any download, so a mymodel_pertrack.npz sitting on your disk is used
as-is for an oracle name the canonical dataset has never heard of. Verified: a fresh name loads from a
local directory and reports its tracks.
What we would rather have than nothing: a correct oracle class, registered, with predict()
working and a small background built on whatever hardware you have — even a few thousand positions.
Say so in the PR. Percentiles from a small null are wide, not wrong, and mirroring a rebuilt null into
the canonical dataset is a maintainer step that does not need to block your contribution. An oracle
that predicts and returns None percentiles, clearly labelled as such, is still a useful PR.
The one thing that is not negotiable is the layer classification in Step 1 of §8 — because that
failure is silent, and a silently-None column looks like a working feature.
Three tests pin the registry and will fail until you update them — that is the design, not an
obstacle: tests/test_mcp.py (exact ORACLE_SPECS key set),
tests/test_reference_position_sets.py (every oracle needs a reference SNP family), and
tests/test_genome_is_asserted_not_assumed.py (training_genome declared).
tests/test_registries_cover_every_oracle.py checks the sites above mechanically, so a missing
entry fails with the site named rather than surfacing as a mystery later.
Step 6: Add Tests
Create a test file tests/test_mymodel.py:
import pytest
import chorus
def test_mymodel_creation():
"""Test MyModel oracle creation."""
oracle = chorus.create_oracle('mymodel', use_environment=False)
assert oracle.oracle_name == 'mymodel'
assert oracle.sequence_length == 524288
def test_mymodel_tracks():
"""Test track listing."""
oracle = chorus.create_oracle('mymodel', use_environment=False)
assays = oracle.list_assay_types()
assert 'DNase' in assays
cells = oracle.list_cell_types()
assert 'K562' in cells
# Add more tests for predictions, etc.
Step 7: Create an Example Notebook
Create examples/notebooks/mymodel_example.ipynb demonstrating your oracle's features (library tutorials live in examples/notebooks/; the per-walkthrough examples/walkthroughs/*/notebook.ipynb files are code-generated by scripts/generate_walkthrough_notebooks.py and should not be hand-written):
# Example notebook structure
1. Oracle initialization
2. Model loading
3. Basic sequence prediction
4. Genomic coordinate prediction (if supported)
5. Track visualization
6. Special features of your model
Step 8: Document Your Oracle
Add a section to the README.md describing:
- Model capabilities
- Sequence length requirements
- Number of tracks
- Special features
- Citation information
Environment Configuration Format
When submitting your oracle, provide the environment configuration in this format:
# In your oracle implementation or a separate config file
BORZOI_ENV_CONFIG = {
'channels': ['pytorch', 'conda-forge', 'bioconda', 'defaults'],
'dependencies': [
'python=3.10',
'pytorch>=2.0.0',
'numpy',
'pandas',
# ... other conda packages
],
'pip_packages': [
'special-package==1.0.0',
# ... other pip packages
],
'post_install_commands': [
'wget https://example.com/weights.pt -O ~/.cache/mymodel/weights.pt',
# ... other setup commands
]
}
Best Practices
-
Lazy Imports: Import model-specific packages inside methods to avoid import errors:
def _load_direct(self, weights): import torch # Import here, not at module level -
Memory Management: Be mindful of memory usage, especially for large models
-
Error Handling: Provide clear error messages for common issues
-
Logging: Use the logger for important status updates
-
Type Hints: Use proper type annotations for all methods
-
Documentation: Include docstrings for all public methods
Contributing an example or walkthrough
The fastest useful contribution. If there is a variant, locus or question you care about and chorus already ships an oracle that can answer it, adding it as a worked example is roughly one list entry plus one script run.
Chorus commits two different kinds of example:
- Walkthroughs —
examples/walkthroughs/<category>/<name>/. A real variant or locus, pre-run, committed together with its HTML report, JSON and TSV, so a reader sees the answer with no GPU and no install. These are declarative: you add a dict, a script produces everything else. - Notebooks —
examples/notebooks/*.ipynb, hand-written library tutorials. Note thenotebook.ipynbinside each walkthrough is code-generated byscripts/generate_walkthrough_notebooks.pyand must not be hand-edited.
1. Add the entry
For a variant walkthrough, append to the matching list in scripts/regenerate_examples.py
(ALPHAGENOME_EXAMPLES, ENFORMER_EXAMPLES or CHROMBPNET_EXAMPLES). An entry is just:
{
"name": "SORT1 rs12740374 (Enformer)",
"dir": f"{BASE}/variant_analysis/SORT1_enformer",
"type": "discovery",
"position": "chr1:109274968",
"ref": "G", "alt": "T",
"gene": "SORT1",
"html_name": "rs12740374_SORT1_enformer_report.html",
}
ref must match the reference genome at that position — strict_ref=True is the default, so a
wrong ref raises ReferenceAlleleMismatchError rather than quietly substituting.
2. Run the right script, in the right env
This table is the part that costs people an afternoon. Getting it wrong usually does not fail loudly — see the traps below.
| what you added | script | valid --oracle | conda env |
|---|---|---|---|
| a variant walkthrough | scripts/regenerate_examples.py | alphagenome, enformer, chrombpnet, all | chorus (these use use_environment=True, so they spawn the per-oracle env themselves) |
| a per-oracle multioracle report | scripts/regenerate_multioracle.py --oracle X | chrombpnet, cherimoya, legnet, alphagenome — no enformer | chorus-X |
| the unified IGV panel | scripts/regenerate_multioracle.py --consolidate | — | chorus |
| discovery / causal / region_swap / integration / batch / TERT | scripts/regenerate_remaining_examples.py --only all | — | chorus-alphagenome |
the walkthrough's notebook.ipynb | scripts/generate_walkthrough_notebooks.py | — (codegen only, no GPU) | chorus |
Enformer has no --oracle enformer in the multioracle script; its single-oracle report comes from
regenerate_examples.py.
3. Traps worth knowing before you start
- The wrong env does not fail fast. A missing oracle package logs
Failed to load <track>once per track and carries on, so a long run can spend an hour loading nothing and then die at the end. regenerate_multioracle.pyhas no--gpuflag (the other two do). Passing one is an argparse error that scrolls past in a tailed log.- Do not write
mamba run -n X --no-capture-output ...— a flag after-nmakes the wrapper die withexec: --: invalid optionbefore your script starts, which reads like a completed step. - Finish editing before you start regenerating. Python reads source at process start, so a change made mid-run reaches only the examples generated after it, leaving a committed set that is internally inconsistent in a way no test compares.
conda runbuffers stdout, so a long build's log stays empty until it exits. Useconda run --no-capture-output ... python -uwhen you want to watch it.
4. What will go red
Committed examples are guarded, deliberately — an example whose HTML no longer matches its JSON is worse than no example. Expect to run:
pytest tests/test_committed_examples.py tests/test_json_tsv_parity.py \
tests/test_walkthrough_readmes_match_artefacts.py \
tests/test_summary_tables_match_their_artefact_by_label.py \
tests/test_batch_rows_reconcile_with_headline.py \
tests/test_rerender_refuses_to_degrade.py
If a guard fails, it is usually telling you a regeneration step was skipped or run in the wrong env, not that the guard is wrong.
Running the tests
From the chorus base env, at the repo root. This is the command CI runs, and a guard test enforces
that it stays the same one:
pytest tests/ # the fast suite — no GPU, no network, ~5 min
No marker flag: pytest.ini sets addopts = -m "not integration", so the exclusion is already
applied and lives in one place. That matters for the two heavier suites, because passing no -m
does not mean "run everything" — you have to override the default explicitly:
# Integration — needs the per-oracle conda envs, a GPU, and hg38 on disk. ~19 min.
pytest tests/ -m integration
# Browser — renders every committed HTML report in headless Chromium. ~2 min, no GPU.
pip install playwright && playwright install chromium
pytest tests/test_committed_reports_render_in_a_browser.py -m ""
The -m "" is load-bearing. That file sets pytestmark = pytest.mark.integration at module level,
so without it pytest.ini's default deselects the lot and you get no tests collected (46 deselected) — which scrolls past looking like success. Set CHORUS_BROWSER_SMOKE=1 only if you want
CI's reduced 3-report subset (12 tests) rather than all 46.
Both heavier suites skip cleanly rather than failing when their prerequisites are missing — the
browser one names exactly what is absent (playwright not installed, or no chromium in <cache>).
If you are only changing Python, the fast suite is what you need; CI runs the rest.
Do not run the notebooks and the integration suite at the same time, even pinned to different
CUDA_VISIBLE_DEVICES.scripts/gate_end_to_end_determinism.pysets its own mask and spawns two AlphaGenome processes with JAX preallocating, which produces a falseCUDA_ERROR_OUT_OF_MEMORYin whichever suite loses the race.
Submitting Your Contribution
-
Create a Pull Request with:
- Your oracle implementation
- Environment configuration
- Tests
- Example notebook
- Documentation updates
-
PR Description should include:
- Model description and capabilities
- Environment setup instructions
- Any special requirements
- Link to model paper/repository
-
Testing: Ensure all tests pass and the oracle works in both modes:
- With environment isolation (
use_environment=True) - Without environment isolation (
use_environment=False)
- With environment isolation (
Example PR Structure
chorus/
├── oracles/
│ └── mymodel.py # Your oracle implementation
├── tests/
│ └── test_mymodel.py # Tests
├── examples/
│ └── notebooks/mymodel_example.ipynb # Example notebook
└── README.md # Updated with your oracle info
Getting Help
- Open an issue for questions
- Join discussions in existing oracle implementation PRs
- Tag maintainers for review: @pinellolab
Current Priorities
All eight core oracles (Enformer, Borzoi, ChromBPNet/BPNet, Sei, LegNet, AlphaGenome, Cherimoya/CATv1, EPInformer-seq) are implemented — nine registered names, since AlphaGenome ships both a JAX and a PyTorch backend. We're interested in contributions for:
Models
- Custom fine-tuned models — models trained on specific tissues or conditions
- Species-specific oracles — mouse, drosophila, etc.
- New architectures — HyenaDNA, Evo, Nucleotide Transformer, etc.
Everything else — genuinely wanted, and much smaller 4. Worked examples — a variant, locus or question you care about, run through an oracle we already ship. This is the most useful contribution per hour of your time: it costs one entry in a declarative list plus one script run, and it is how most people discover what chorus does. See Contributing an example. 5. Documentation that was wrong or missing when you read it — including "I followed this and it failed". A copy-pasteable repro of where you got stuck is a useful issue even with no patch. 6. Bug fixes and tests, especially a test that pins something you found the hard way.
Thank you for contributing to Chorus! Your implementation will help make genomic deep learning models more accessible to the research community.