diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..16994f0c52 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,94 @@ +# AGENTS.md + +Notes for AI coding assistants working in this repository. Human contributors should read +[CONTRIBUTING.md](CONTRIBUTING.md) — this file only adds the machine-facing details and links back +to it rather than repeating it. + +## Project overview + +MONAI is an open-source framework for deep learning in medical imaging, built on PyTorch. It ships +transforms, networks, losses, metrics, data loading, engines and bundle workflows for 2D and 3D +medical images. Supported Python and PyTorch versions are declared in +[pyproject.toml](pyproject.toml) and [setup.py](setup.py); read them there instead of copying +version numbers into prose. + +## Repository map + +| Path | Purpose | +|------|---------| +| `monai/transforms` | array, dictionary (`*d`) and random (`Rand*`) transforms, lazy resampling, invertible transforms | +| `monai/networks` | `nets`, `blocks`, `layers`, `schedulers` | +| `monai/data` | datasets, image readers, `MetaTensor` | +| `monai/losses` | loss functions | +| `monai/metrics` | metrics and the confusion-matrix machinery | +| `monai/bundle` | YAML workflow configuration and the scripts that run it | +| `monai/apps` | applications built on the frameworks (pathology, radiology, ...) | +| `monai/auto3dseg` | automated 3D segmentation | +| `monai/engines` | engine base classes | +| `monai/handlers` | handler base classes and the concrete handlers | +| `monai/fl` | federated learning | +| `monai/inferers` | inference helpers | +| `monai/optimizers` | optimizers and learning-rate schedulers | +| `monai/visualize` | visualization helpers | +| `monai/utils` | shared helpers, `optional_import`, deprecation decorators | +| `monai/csrc` | optional C++/CUDA extensions | +| `monai/config` | the `python -m monai.config` command line entry point | +| `monai/_extensions` | optional compiled extensions loaded at runtime | +| `tests` | unit tests, mirroring the `monai` tree | +| `docs` | Sphinx sources: `docs/source/*.rst` and `docs/source/*.md` | + +## Where the detail lives + +| File | Covers | +|------|--------| +| [monai/transforms/AGENTS.md](monai/transforms/AGENTS.md) | transform families, `MetaTensor`, invertibility, random state, lazy resampling | +| [tests/AGENTS.md](tests/AGENTS.md) | test layout, `min_tests.py`, skip decorators, running a single test | + +## Setup and commands + +```bash +./runtests.sh --setup # install the pre-commit hooks (black, isort, ruff, DCO sign-off) +./runtests.sh --autofix # format the working tree (isort + black) +./runtests.sh --codeformat # style and static analysis checks +./runtests.sh --quick --unittests # fast local verification +./runtests.sh -f -u --net --coverage # recommended before opening a pull request +python -m tests.losses.test_dice_loss # run a single test module +``` + +`./runtests.sh -h` lists every option. + +## Repo-wide conventions + +- Pull requests target the `dev` branch, and every commit carries a `Signed-off-by` line + (`git commit -s`). See [CONTRIBUTING.md](CONTRIBUTING.md#submitting-pull-requests). +- Line length is 120; `black`, `isort` and `ruff` all read it from + [pyproject.toml](pyproject.toml). +- New source files start with the Apache 2.0 header quoted in + [CONTRIBUTING.md](CONTRIBUTING.md#licensing-information). +- Optional dependencies are only ever reached through `monai.utils.optional_import` + (`monai/utils/module.py`); never import an optional package at module scope. +- A new public symbol is added to `__all__` in the module that defines it, re-exported from the + package `__init__.py`, and listed in the matching `docs/source/.rst`. +- Deprecations go through `monai.utils.deprecated` and `monai.utils.deprecated_arg` + (`monai/utils/deprecate_utils.py`) with `since` and `removed` versions. +- Tests mirror the source tree: `monai/transforms/spatial/array.py` is covered by + `tests/transforms/test_*.py`. +- Use American English spelling, and f-strings rather than `%` formatting. See + [CONTRIBUTING.md](CONTRIBUTING.md#utility-functions). +- Prefer the shared helpers in `monai/utils` and `monai/networks/utils.py` over re-implementing + them. + +## AI-assisted contributions + +MONAI accepts tool-assisted contributions under the conditions set out in +[CONTRIBUTING.md](CONTRIBUTING.md#tool-assisted-contribution-policy): the submitter reviews the +content, takes responsibility for it, signs off the work, and labels AI-generated content. The +policy is not repeated here. + +## Do not + +- Edit generated files: `monai/_version.py` and the versioneer output. +- Commit data, weights or binaries. Test data is downloaded at test time, see + [CONTRIBUTING.md](CONTRIBUTING.md#testing-data). +- Change CI workflows unless the task is about them. +- Add personal tooling configuration, machine-specific paths or secrets to the repository. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28a449e6c0..7b73e1b727 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,6 +45,11 @@ We encourage you to create pull requests early. It helps us track the contributi Please note that, as per PyTorch, MONAI uses American English spelling. This means classes and variables should be: normali**z**e, visuali**z**e, colo~~u~~r, etc. +The repository also carries [`AGENTS.md`](AGENTS.md) files, which describe how the code base works for +AI coding assistants. They are treated as part of the documentation: when a change makes a +convention they describe obsolete, update the matching file in the same pull request, and add a +leaf file next to the code it describes when a package needs its own notes. + ### Preparing pull requests To ensure the code quality, MONAI relies on several linting tools ([black](https://github.com/psf/black), [isort](https://github.com/timothycrosley/isort), [ruff](https://github.com/astral-sh/ruff)), diff --git a/monai/transforms/AGENTS.md b/monai/transforms/AGENTS.md new file mode 100644 index 0000000000..a80cd5d7e5 --- /dev/null +++ b/monai/transforms/AGENTS.md @@ -0,0 +1,61 @@ +# AGENTS.md — `monai/transforms` + +Notes for AI coding assistants changing `monai/transforms`. The repository-wide rules live in the +root [AGENTS.md](../../AGENTS.md); human contributors should read +[CONTRIBUTING.md](../../CONTRIBUTING.md). + +## Module map + +| Path | Contents | +|------|----------| +| `transform.py` | base classes: `Transform`, `MapTransform`, `RandomizableTransform`, `LazyTransform` | +| `traits.py` | `LazyTrait`, `InvertibleTrait`, `RandomizableTrait`, `MultiSampleTrait`, `ReduceTrait` | +| `inverse.py` | `TraceableTransform`, `InvertibleTransform` — the transform stack and its inversion | +| `compose.py` | `Compose`, the pipeline that runs the transforms | +| `lazy/` | lazy resampling: `ApplyPending` and the pending `Affine`/`DisplacementField` bookkeeping | +| `croppad/`, `intensity/`, `io/`, `post/`, `regularization/`, `signal/`, `smooth_field/`, `spatial/`, `utility/` | the transform families | +| `meta_utility/` | dictionary-only transforms that work on `MetaTensor` metadata | +| `utils.py` | helpers shared by the transforms | + +Each family directory follows the same convention: + +- `array.py` — the transform applied to a single tensor, e.g. `Spacing`. +- `dictionary.py` — the dictionary variant named with a `d` suffix, e.g. `Spacingd`. +- `functional.py` — the functional API, where present (`croppad`, `lazy`, `spatial`). +- `batch.py` — batched dictionary transforms (`croppad` only). + +## Traps + +**A transform is usually three or four classes.** Adding a transform means the array class, the `*d` +dictionary class, and a `Rand*` class when the transform is randomizable. A dictionary transform +derives from `MapTransform` and selects the keys it applies to with `key_iterator`; it is not the +array class with a different name. + +**`MetaTensor` must survive the call.** `MetaTensor` (`monai/data/meta_tensor.py`) carries the +affine matrix, the original spatial shape and the stack of applied transforms. Converting to a +plain `torch.Tensor` or to numpy inside a transform silently drops that metadata and breaks +inversion and resampling, so keep the input type. + +**Randomness goes through `self.R`.** `RandomizableTransform` owns a `numpy.random.RandomState` +that `set_random_state` configures, and `randomize()` runs it on every call. Using the global +`np.random` bypasses the seed, and the transform is no longer reproducible. + +**Invertibility has a contract.** Inherit `InvertibleTransform`, call `push_transform` in +`__call__` and `pop_transform` in `inverse`, and keep the extra information needed by the inverse +in `extra_info`. `extra_info` may only hold values that survive the data loader's collate +function, so `None` is not allowed. The stack itself lives in +`MetaTensor.applied_operations` and is unwound in last-in-first-out order. + +**Lazy resampling is opt-in and deferred.** A `LazyTransform` describes its operation as an affine +matrix and lets the pipeline resample once, at the end. `requires_current_data` marks the +transforms that cannot be deferred, and `ApplyPending` forces the pending resampling to happen. +Turning `lazy` on changes when the pixels are computed, so a change here needs a test that pins +the output, not only the shape. + +**A new transform is also a docs and export change.** Re-export it from +`monai/transforms/__init__.py`, add an `.. autoclass::` entry in `docs/source/transforms.rst`, and +add the module to `docs/source/transforms_idx.rst` when it is a new module. + +**Tests mirror the family.** `tests/transforms/test_.py` covers the array transform and +`tests/transforms/test_d.py` the dictionary one; the inversion tests live in +`tests/transforms/inverse/`. A transform that is not covered there is not covered. diff --git a/tests/AGENTS.md b/tests/AGENTS.md new file mode 100644 index 0000000000..ba09a396ac --- /dev/null +++ b/tests/AGENTS.md @@ -0,0 +1,57 @@ +# AGENTS.md — `tests` + +Notes for AI coding assistants changing the MONAI test suite. The repository-wide rules live in the +root [AGENTS.md](../AGENTS.md); human contributors should read +[CONTRIBUTING.md](../CONTRIBUTING.md). + +## Layout + +`tests/` mirrors the `monai` package: `tests/transforms`, `tests/networks`, `tests/data`, +`tests/losses`, `tests/metrics`, `tests/bundle`, `tests/apps`, `tests/auto3dseg`, +`tests/engines`, `tests/handlers`, `tests/inferers`, `tests/optimizers`, `tests/utils`, +`tests/visualize`, plus `tests/integration` for the end-to-end workflows. + +File names follow the convention in [CONTRIBUTING.md](../CONTRIBUTING.md#unit-testing): + +- `test_[module_name].py` — a unit test module. +- `test_[module_name]_dist.py` — a unit test module that needs a distributed environment. +- `test_integration_[workflow_name].py` — an end-to-end test. + +## Running tests + +```bash +python -m tests.losses.test_dice_loss # a single test module +./runtests.sh --quick --unittests # fast local verification +./runtests.sh -f -u --net --coverage # recommended before opening a pull request +QUICKTEST=true python -m tests.min_tests # the minimal suite, without optional dependencies +``` + +`./runtests.sh -h` lists every option. + +## Traps + +**Optional dependencies decide how a test is written.** Reach an optional package through +`monai.utils.optional_import` and skip the test when it is missing, either with the +`SkipIfNoModule` / `SkipIfModule` decorators in `tests/test_utils.py` or with the +`skipUnless(has_matplotlib, ...)` pattern. When a whole module needs an optional package, add its +name to `exclude_cases` in `tests/min_tests.py` so the minimal runner skips it; that list is +checked for duplicates, so add the name once. + +**Test data is not in the repository.** The URLs live in +`tests/testing_data/data_config.json` and are read with `testing_data_config`, and the download +itself goes through `download_url_or_skip_test` — both in `tests/test_utils.py`. The `--net` runs +exercise the download path. Never add images, weights or other binaries to the repository, and do +not edit `tests/testing_data` to make a test pass. + +**Import the helpers from `tests.test_utils`.** The shared helpers — `skip_if_quick`, +`skip_if_downloading_fails`, `skip_if_no_cpp_extension`, `skip_if_no_cuda`, `skip_if_windows`, +`skip_if_darwin`, `assert_allclose`, `dict_product` — live in `tests/test_utils.py`; the +`tests/utils` package holds the tests of `monai.utils` itself. + +**`skip_if_quick` is the marker of a slow test.** A test that takes more than a couple of seconds +is expected to carry it, otherwise `--quick` and the minimal runner pay for it. + +**A new test module should stay small in its dependencies.** The recommended set is the Python +standard library, `torch`, `numpy`, `coverage` and `parameterized`; anything else has to be +declared in `pyproject.toml` and, when it is optional, excluded from the minimal run as described +above.