Guidance for AI coding agents working on spatiomic, the scverse-ecosystem library
behind PathoPlex (Kuehl et al., Nature 2025) for spatial-omics analysis: pixel-level
clustering, spatial statistics, dimensionality reduction and segmentation.
- Never perform git actions. Do not stage, commit, branch, push, or amend. Leave all version control to the user. Edit files only.
- Everything in English. Code, docstrings, comments, commit-worthy messages.
- KISS & DRY. Prefer the simplest correct solution. Do not duplicate logic — factor it out.
- Reuse before you build. Before writing new numerical/algorithmic code or adding a
dependency, check whether an already-present library solves it:
numpy,scipy,scikit-learn,scikit-image,anndata,pandas,networkx,esda/libpysal(PySAL). Read that dependency's own API/docs first. Adding a new dependency is a last resort and must be justified to the user. - Spell names out in full.
disease_variant, notdis_var;neighbor_count, notn. Follow existing domain vocabulary in the codebase.
make check # ruff check --fix, ruff format --check, mypy -p spatiomic, bandit
make unittest # uv run coverage run -m pytest --maxfail=10 -m "not gpu and not no_github_ci"Before handing work back, run make check and the CPU test command above; both must
pass. GPU tests (-m gpu) require hardware and are not run locally — do not delete or
weaken them to make a run green.
- Build/env:
uv+hatchling. Python >=3.12 (targets 3.12/3.13/3.14). - Ruff: line length 120, double quotes, Google docstring convention, mccabe max-complexity 7. Let ruff format the code — never hand-format around it.
- mypy:
disallow_untyped_defs— every function/method needs full type hints. - bandit:
-ll --recursive spatiomic.
- Type hints (mandatory). Use modern syntax in new code:
list[int],tuple[int, int],X | None(notList/Tuple/Optional/Union). Usenumpy.typing.NDArrayfor arrays andtyping.Literal[...]for enumerated string options. - Docstrings (mandatory, Google style) on every public module, class and function,
with
Args:/Returns:/Raises:. Match the existing convention of repeating the type and default inArgs(e.g.node_count (tuple[int, int], optional): ... Defaults to (50, 50).). - Comments minimal. Docstrings carry the documentation. Add an inline comment only where logic is genuinely non-obvious — never to restate what the code says.
- Validation via exceptions, not
assert. RaiseValueError/TypeErrorwith a clear message (asserts are stripped underpython -O):if data.ndim != 3: raise ValueError(f"data must be 3D (y, x, c), got {data.ndim}D")
- GPU/CPU dual path. New array code supports both via the existing pattern: a
use_gpu: bool = Trueflag andxp = import_package("cupy", alternative=np), then usexp.*. Reuse_internalhelpers (import_package,@data_method,@anndata_method).
- Stateful features → an sklearn-style class. Define a
PascalCaseclass in a private module_name.py, re-export it lowercase in the submodule__init__.py(from ._name import Name as name), and declare it inspatiomic/__init__.pyi(the package is lazy-loaded). So the public surface readsso.dimension.som(...). - Follow the scikit-learn estimator conventions the codebase already uses:
- Implement the relevant verbs:
fit(data, ...),transform(data, ...),fit_transform(data, ...), andpredict(...)where meaningful.fit/fit_transformtake the data as the first positional argument. - All hyperparameters are constructor keyword arguments with sensible defaults and are
stored unchanged on
self(e.g.self.use_gpu = use_gpu). Do not mutate them infit. - Learned state is set in
fit, conventionally on attributes distinct from the hyperparameters (e.g.self.estimator,self.nodes).transform/predictrely on it. - For persistable estimators, provide
save(save_path)/load(load_path)(pickle-based, matching existing classes), plus config accessors likeget_config/set_configwhere a class already establishes that shape.
- Implement the relevant verbs:
- Conform to the submodule's
_base.pyinterface. Each submodule defines ABCs (DimensionReducer,LoadableDimensionReducer,Processer) viaabc.ABCMeta+__subclasshook__that check for the required methods. A new class must implement those methods so it registers as a subclass; extend the interface only if the whole family changes. - Pure, stateless helpers → a function, still exported lowercase
(e.g.
so.data.subsample(...)), not a class.
- A test must prove real correctness or integration. Do NOT write tests for trivial implementation details: existence of attributes that obviously exist, one-liners asserting a constant, shape checks with no logic behind them. If a test does not increase confidence that the behavior is correct, it should not exist.
- New behavior in an existing feature → extend that feature's existing tests. Only create a new test module for a genuinely new feature, modeled on the closest existing one.
- Tests live in
test/mirroring the package (test/dimension/test_som.py), functions namedtest_<name>. Import the package asimport spatiomic as soand exercise the public lowercase API. - Reuse the seeded fixtures in
conftest.py(example_data,cycle_pixels, …) for determinism. Add a shared fixture there if broadly useful rather than inlining random arrays. - Use
@pytest.mark.parametrizefor parameter combinations (each case reported/fails independently). Use markerscpu/slow/gpu/no_github_cias appropriate.