Contributing and documentation policy¶
Development¶
uv sync --extra all --extra gpu --group dev --group docs # or --extra cpu without an NVIDIA GPU
uv run pytest -q
uv run ruff check src tests examples
uv run mkdocs serve # live docs at http://127.0.0.1:8000
CI runs ruff, pytest and mkdocs build --strict on every push and pull request. Docs
deploy to GitHub Pages from main. chunkmirage takes any worldarray below 1.0 at or above
the one it was last moved to, so packages beside it can move to a new worldarray without
waiting for a chunkmirage release; every Monday, and on a manual run of the workflow, CI
also runs pytest against the newest worldarray on PyPI (worldarray-latest).
Documentation must move with the code¶
The docs are part of the definition of done. Any change that adds, removes or renames a user-visible thing updates the relevant page in the same commit:
| change | update |
|---|---|
| new / removed / renamed op | docs/reference/ops.md |
| new / removed frontend or URL | docs/concepts/formats.md |
| REST endpoint | docs/reference/api.md |
| CLI command or flag | docs/reference/cli.md |
| caching behaviour | docs/concepts/caching.md |
| control page / viewer / events | docs/concepts/interactivity.md |
| architectural decision | docs/design.md |
| roadmap item shipped | remove from docs/roadmap.md, document where it landed |
Two mechanisms enforce this:
tests/test_docs.pyfails if a registered op is missing from the ops reference, a frontend is missing from the formats page, or a REST route is missing from the API reference.mkdocs build --strictfails on broken links and missing pages.
CLAUDE.md at the repo root restates this policy for AI-assisted sessions.
Adding an op¶
from chunkmirage.ops import Op, register
from pydantic import Field
import numpy as np
@register
class MyOp(Op):
"""One-line description shown by `chunkmirage ops`."""
name = "my_op"
halo = 4 # voxels of upstream context needed per side (int or per-axis tuple)
cache = False # True for expensive stages (inference)
strength: float = Field(1.0, description="What it does, in words a user understands") # shown in the UI
def output_dtype(self, in_dtype): return np.dtype("float32")
def apply(self, block): return block.astype(np.float32) * self.strength
An op shipped with chunkmirage goes in src/chunkmirage_contrib/ops/: the core keeps only
downsample and python (what stays in the core).
Register it in pyproject.toml under [project.entry-points."chunkmirage.ops"] (or in your
own package's entry points), add a test, and add a row to docs/reference/ops.md (a
shipped op; a plugin's op is documented by its own package). An op that is a few lines of
numpy with sliders needs no class: python_op(name, code) makes one from code written for
the python op, as the demos' Copy as
plugin does. Users' choice between the three is set out under
your own op.
Adding a source¶
import numpy as np
from chunkmirage.core import ArrayInfo, Box
from chunkmirage.sources import MultiscaleSource, Source
class Ramp(Source):
def __init__(self, n):
self._info = ArrayInfo((n, n, n), np.float32, (64, 64, 64), (8, 8, 8), ("nm",) * 3, ("z", "y", "x"))
@property
def info(self): return self._info
def read(self, box: Box): # exactly box.shape, within info.shape
z, y, x = np.mgrid[box.slices()]
return (x + y + z).astype(np.float32)
def open_ramp(url: str) -> MultiscaleSource: # ramp://512
return MultiscaleSource([Ramp(int(url.split("://")[1]))], name="ramp")
A source that assembles a read from parts may also override read_into(box, out), which
writes the read into an array the caller gives (a view of a larger one, pinned memory), to
put each part in place; the default copies read's result.
List it under [project.entry-points."chunkmirage.sources"] as ramp = "mypackage:open_ramp"
(see formats). The core's
own schemes, the ones that compose other sources (scene://, warp://, stack://,
flip://), are listed in chunkmirage.sources.registry; a computed source shipped with
chunkmirage (synthetic://, register://) goes in src/chunkmirage_contrib/sources/ and
under the entry point like any other package's. A format told apart by its path rather than
a scheme (a GeoTIFF by its extension) lists a matcher, path -> opener or None, under
[project.entry-points."chunkmirage.source_matchers"] (register_matcher in a running
process). Either is documented in docs/concepts/formats.md.
Plugin API and versioning¶
Packages build on chunkmirage through its plugin API, which is stable:
- Ops.
Opand its contract: the class attributesname,halo,overlap,cache,packages,output_kindandslots; the methodsapply,apply_at,input_buffer,output_dtype,output_info,input_voxel_size,for_levelandcache_token; parameters as pydantic fields.registerand thechunkmirage.opsentry point.demand.cancelled()anddemand.Cancelled, for a long op to give up once no request waits for it. - Sources.
Source(info,read,read_into,read_padded,cache_key),ChunkedSource,MultiscaleSource,ArrayInfo(itscomputed_fromamong it) andBox; an openeropener(url, *, cache_bytes, cache)returning aMultiscaleSource, registered withregister_sourceor thechunkmirage.sourcesentry point, or for files told apart by their path a matcher registered withregister_matcherorchunkmirage.source_matchers;open_source. - Schema. A parameter model listed under
chunkmirage.modelsappears inchunkmirage schemaunder the entry point's name, for the browser engine's types. - Serving.
PipelineSpec,Pipeline,create_appand its parameters,serveandServer,materialize,gpu.set_device,demand.set_gather_threads,DatasetRegistry(add,get,resolve,refresh,remove,subscribe) and its resolver, thechunkmirage.routesentry point, the REST routes in the API reference, the served URL layout, and the CLI's flags.
Everything else may change in any release: names starting with _, modules not named
above (chunkmirage.fused, the frontends' internals), log messages, the control page and
the browser engine.
Versions follow semantic versioning. Before 1.0, a minor release (0.x.0) may change the stable API, but only with a changelog entry under its own heading saying what to change in a plugin, and, where it can, after one minor release in which the old way still works and warns. A patch release (0.x.y) never breaks it. From 1.0, a change to the stable API waits for a major release.
Releasing¶
- Move the changelog's entries under
Unreleasedto the new version, and set that version inpyproject.tomlandchunkmirage/__init__.py(tests/test_docs.pychecks the three agree). - Merge to
mainwith CI green. - Publish to PyPI only when the owners say so: a GitHub release tagged
v<version>runs.github/workflows/publish.yml, which builds the wheel and uploads it (a manual run of the workflow uploads to TestPyPI instead).