Developer guide¶
This guide is intended to contributors of all sorts — developers, maintainers, users willing to report issues, with or without AI programming assistance.
AI programming¶
Usage of AI coding assistants (e.g. Claude Code, Codex, Cursor) is allowed in this project, but only as a tool under human supervision. All changes are authored by a human responsible for them regardless of how they were produced; “the agent wrote it” is not a defence for a regression.
- AI policy
Be sure to read, understand and comply to our AI policy.
- Project context for agents
AGENTS.mdat the repository root is the source of truth for conventions, architecture and gotchas, and is loaded automatically by AI tools. Keep it in sync with the code: when you change a convention or a translation detail, updateAGENTS.md(and this guide) in the same change.- Provenance and attribution
Disclose substantial AI involvement in the pull request description and / or commit messages under an “AI disclosure” section. Do not list your AI tool as a co-author of your commits.
General guidelines¶
The following, general guidelines, apply to everyone, regardless of whether they use AI tools.
- Verification expectations
All changes must pass the full test suite. Where physics is touched, they must additionally be checked against the regression references or the Monte Carlo backend before merging — a green suite alone is not enough if the references themselves were regenerated.
- Regression data discipline
Never regenerate regression test references with
--force-regenjust to make a test pass. Regenerate only when you understand why the expected values changed and have confirmed the new values are correct.- High-risk areas
Pay particular attention to parts of the code falling under the Eradiate-to-DISORT translation section (ordering, azimuth,
utau, allocation ordering, phase-moment scaling). This is where AI output must be reviewed most carefully. Such issues do not show up as obvious errors in a diff and a plausible-looking change can silently break.- Licensing caution
This is a GPLv3 project. Do not paste in code of unknown or incompatible provenance; an agent suggesting a verbatim block from elsewhere is a licensing risk.
- Tooling guardrails
Rely on pre-commit (ruff, taplo) and CI as the source of truth for formatting and lint, not on an agent’s — or your own — claim that a file is clean.
Development environment¶
The project is managed with Pixi. Install Pixi, clone the repository, and initialize the submodules:
git submodule update --init --recursive
Two dependencies live under ext/ as git submodules and are installed as
editable Pixi dependencies:
ext/eradiateThe Eradiate model itself. It depends heavily on Mitsuba and Dr.Jit, and its Monte Carlo backend is what the DISORT results are validated against.
ext/nanodisortThe
nanodisortbindings to CDISORT (the C port of DISORT) through which the solver is reached.
Because both are editable installs, changes made in the submodule trees are picked up without reinstalling.
Activation¶
The dev feature’s activation runs two scripts on environment entry:
scripts/set-macos-deployment-target.sh (a no-op off macOS; pins
MACOSX_DEPLOYMENT_TARGET so locally built wheel tags match uv’s acceptance
ceiling) and ext/eradiate/setpath.sh (puts Eradiate and its kernel on the
path). Activation also adds tests/data to ERADIATE_PATH, so that
Eradiate’s path resolver picks up testing data.
Features and environments¶
The Pixi manifest defines feature sets — pyXXX (interpreter pins),
test, docs, dev and kernel — composed into environments:
defaultThe day-to-day working environment:
py312plustest,docsanddev. Runningpixi run <task>uses this environment unless told otherwise.ghapy310…ghapy313CI environments, one per supported interpreter, combining the corresponding Python pin with
kernelandtest.
Building the Mitsuba kernel¶
Examples and tests that use Eradiate’s Monte Carlo backend as a reference need
the compiled Mitsuba kernel at ext/eradiate/ext/mitsuba. Build it with
pixi run kernel-build (kernel-build depends on kernel-configure,
so building alone is enough); pixi run kernel-clean removes the build tree.
Even when not using the Mitsuba backend, Mitsuba must be compiled to Eradiate to
work.
Task cheatsheet¶
All tasks run as pixi run <task> in the default environment.
Testing and benchmarks |
|
|
Run the test suite ( |
|
Run the benchmarks in |
Linting |
|
|
Ruff with the base rule set. |
|
Ruff with the extended rule set. |
|
REUSE license-compliance check. |
Documentation |
|
|
Build the HTML docs into |
|
Live-serve the docs with sphinx-autobuild. |
|
Remove the |
|
Regenerate |
Notebooks |
|
|
Register the Pixi environment as a Jupyter kernel (idempotent). |
|
Sync and execute one example notebook
( |
|
Sync and execute all
|
Mitsuba kernel |
|
|
Configure the Mitsuba kernel build (CMake,
|
|
Build the Mitsuba kernel (depends on
|
|
Remove the Mitsuba build tree. |
Releasing |
|
|
Bump the version to |
|
List the possible next versions. |
|
Verbose dry run of the version bump. |
Architecture¶
eradiate-disort is a DISORT radiometric backend for Eradiate. It computes
radiances and fluxes for 1D plane-parallel scenes using CDISORT — reached through
the nanodisort Python bindings — as a fast alternative to Eradiate’s Monte
Carlo ray-tracing backend.
Execution flow¶
DisortBackend.run() (in _backend.py) drives three stages that mirror
Eradiate’s experiment lifecycle:
validate()— Rejects unsupported configurations early.process()— First,_setup_globaldoes the spectral-independent setup. Then, the spectral loop runs_setup_spectral→_solve→_collect_resultsonce per spectral context. The raw per-spectral DISORT output is accumulated inself._results.postprocess()— Runs the post-processing pipeline to turn the raw results into anxarray.DataTreewith one subtree per measure, keyed by measure ID.
Main components¶
DisortBackendThe entry point that drives the aforementioned execution flow.
DisortMeasureA custom Eradiate
Measureregistered inmeasure_factoryunder"disort". It records fluxes and intensities at specified altitudes or optical depths. The convenience constructorshplane,aringandgridbuild common direction layouts._phase(private)Evaluation routines for various phase functions (
get_phase()) and their Legendre coefficients (get_pmom())._pipeline(private)Specific post-processing pipeline that processes the raw
nanodisortoutput into aDataTree.ioVarious data formatting and I/O support components, e.g. metadata normalization.
testingShared helpers used for development and testing, such as the
TestModeselector (driven by theERADIATE_TEST_MODEenvironment variable), thecasesa benchmarking case library, thexarray_regression()fixture, and plotting helpers.
Eradiate-to-DISORT translation¶
These are the subtle, error-prone parts of the backend. The points below are the conventions that must be respected when changing it. Bugs here are numerical, not structural, and rarely visible in a diff — see AI / agentic programming.
- Layer ordering
CDISORT expects atmospheric layers ordered top-to-bottom, whereas Eradiate works bottom-to-top. Arrays are reversed at the boundary (
dtauc[::-1],ssalb[::-1],pmom[:, ::-1]).- Azimuth convention
CDISORT’s
phi0is the beam’s travel direction, while Eradiate’sillumination.azimuthis the source direction. They differ by 180°.- Cumulative optical depth (
utau) Measured from the top of the atmosphere (0 at TOA, total τ at BOA).
DisortMeasureaccepts eitherz_levels(snapped to grid boundaries) orutau— mutually exclusive.- Flux-only mode
When
onlyfl=True(nodirection_layout), CDISORT internally overwritesnumuwithnstr. The usernumu/nphi/umu/phimust therefore be re-assigned on every spectral iteration after the first.- Intensity correction
buras_emde(the default) needs actual phase-function values and pads the μ-phase grid with sentinel points at both ends (±(1+eps)); the+2tonphasein_setup_globalaccounts for these.nakajima_tanakauses only Legendre moments and needs no padding.- Phase-moment conventions
Eradiate’s particle data stores
(2l+1)·f_l, whereas DISORT wantsf_l. Components in_phasedivide by(2l+1)and truncate or zero-pad tonmom+1.- Homogeneous atmospheres
These return scalar optical properties; broadcast them to
nlyrbefore assigning.- Allocation ordering
ds.allocate()is called exactly once (first_call=True), afterntauis known and before any array assignment. Never assign DISORT arrays before allocation.
Spectral modes¶
The backend supports Eradiate’s mono and ckd spectral modes, selected with
eradiate.set_mode(). Mode-dependent behaviour is confined to two places:
spectral index generation (
DisortBackend._get_spectral_indices());aggregation step in the post-processing pipeline (
aggregated_data()).
The rest of the backend is mode-agnostic.
Testing¶
Layout and fixtures¶
Tests live in tests/. The tests/conftest.py configuration file pulls in
the shared fixtures, including er_plt (plotting fixture) and
xarray_regression (which wraps pytest-regressions’ ndarrays_regression).
Test modes¶
The ERADIATE_TEST_MODE environment variable, surfaced through
TestMode, selects how shareable code runs:
testUsed by test tasks. Full sample counts, no interactive plots.
benchmarkUsed by benchmarking tasks. Full sample counts (possibly different from
test), no plots.tutorialUsed by notebook tasks. Low sample counts, plots enabled.
TestMode.spp() and TestMode.plt() let a single notebook serve all
three modes. Hidden cells can bypass default settings (e.g. sample count) with
values output by TestMode.
Examples as regression tests¶
The example notebooks double as regression tests.Source files, stored as
tests/examples/example_*.py, are Jupytext percent-format scripts paired to
docs/examples/*.ipynb. The tests/examples/test_examples.py test file
imports each example’s results and asserts against them. These run last,
under pytest.mark.order(-1).
Regression reference data¶
Regression references are stored in tests/data/regression_references/,
under a subdirectory tree that reflects test the module layout. They can be
regenerated them by appending --force-regen to the test invocation. Do this
only when you understand why the reference changed — see
AI / agentic programming.
Benchmarks¶
Benchmarks live in tests/benchmarks and have specific configuration defined
in a separate pytest.ini. They use
pytest-benchmark.
Benchmark definition files, functions and classes use bench_ and Bench
prefixes. They are excluded from the default suite and run with pixi run bench.
Documentation¶
The documentation is built with Sphinx, the Shibuya theme and MyST-NB, and is deployed on Read the Docs.
Example notebook outputs are committed. The nbstripout pre-commit hook
excludes docs/examples/ so MyST-NB renders them without re-executing. This
keeps the docs build fast and reproducible, at the cost of having to regenerate
and commit outputs when an example changes (pixi run nb-execute-all).
Build the docs locally with pixi run docs (HTML into
docs/_build/html); pixi run docs-serve live-rebuilds with
sphinx-autobuild, and pixi run docs-clean removes the build tree.
The pinned doc dependencies in docs/requirements.txt are regenerated with
pixi run docs-lock (a uv pip compile invocation); commit the result when
the doc dependency set changes.
Conventions and tooling¶
Code style¶
Python code is formatted linted with Ruff. Activate pre-commit hooks to enforce formatting and the basic linting rules.
The basic lint rule set is
intentionally small and can be applied with pixi run lint. A more extensive
rule set is available and should be applied to detect issues early during a
refactoring pass, prior to opening a PR or merging a branch into main. The
extended rule set is applied by the pixi run lint-ext task.
Public functions, classes and methods carry Numpydoc-style docstrings, and all signatures are type-hinted. In-code documentation is the primary documentation surface; keep it comprehensive and current rather than deferring explanation to external prose.
Module layout and public API¶
Implementation modules are private and underscore-prefixed (e.g. _backend.py,
_measurements.py, _phase.py, _pipeline.py). The supported public API
is the small surface re-exported from eradiate_disort/__init__.py. Treat
anything not re-exported there as internal and subject to change.
In tests and examples, when relevant, import from the package root rather than reaching into private modules.
Licensing and REUSE¶
The project is licensed under GPL-3.0-or-later (consistent with
nanodisort and CDISORT) and is REUSE-compliant.
Every source file under src/ begins with an SPDX header:
# SPDX-FileCopyrightText: 2026 Rayference
#
# SPDX-License-Identifier: GPL-3.0-or-later
Files that do not carry their own header inherit copyright and license from the
** default annotation in REUSE.toml, and the license texts live under
LICENSES/. When adding a new source file, copy the SPDX header above; when
adding a file of a different license, register it in REUSE.toml and drop the
corresponding text in LICENSES/. Verify compliance with
pixi run lint-reuse.
Important
CDISORT code is licensed under the terms of the GPL, while Eradiate is
licensed under terms of the LGPL. For this reason, this backend must be
only be downstream (in terms of dependency graph) of Eradiate:
eradiate-disort can import eradiate, but not the reverse, unless
done on an entirely optional and opt-in basis, e.g. through a plugin
system.
pre-commit¶
Formatting and lint conventions are enforced automatically through pre-commit
hooks, defined in .pre-commit-config.yaml. Install the hooks once with
pre-commit or prek
(recommended). If necessary, pre-commit run --all-files can run them across
the entire project.
Because the hooks run on commit, do not rely on an agent’s or your own claim that a file is formatted or lint-clean — let pre-commit and CI be the source of truth.
Releasing¶
Versioning¶
The project losely follows semantic versioning with an optional pre-release
suffix (dev / rc / final); the current version is specified in
pyproject.toml and is managed with
bump-my-version,
configured in .bumpversion.toml. Bump the version with:
RELEASE_VERSION=<new-version> pixi run bump
pixi run bump-show lists the possible next versions and pixi run bump-dry
performs a verbose dry run. The bump configuration does not commit or tag
automatically (commit = false, tag = false); make the version-bump commit
yourself, and tag it v<new-version> to match tag_name.
Continuous integration¶
The .github/workflows/test.yml workflow runs the test suite on pushes and
pull requests to main across an OS × interpreter matrix. Draft pull requests
are skipped.
Publishing¶
The .github/workflows/cd.yml workflow builds the wheel and sdist and
publishes to PyPI. It is triggered manually with an upload input: 0
builds only, 1 publishes to PyPI, 2 publishes to TestPyPI. The usual
release sequence is:
bump;
push;
verify the test workflow is green;
dispatch
cd.yml— to TestPyPI first if in doubt, then to PyPI;when the wheel is published on PyPI, tag the published commit;
(optional) create a release on GitHub.