Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
542ef8e
Adding Deprecation Warning For hash_type (#9130)
ericspod Oct 15, 2026
9b8aad3
feat(transforms): add MarchingCubes surface extraction (#9107)
aymuos15 Oct 16, 2026
6aa4abf
[Fixes #9116] Include per-class rank in EmbeddingCollapseMetric aggre…
DucAnhValentinoNguyen Oct 16, 2026
8de8122
Fix MeanIoU ignore_index to exclude voxels, not just a channel (#9134)
SID-6921 Oct 17, 2026
33bcc40
Adding per_component functionality to Surface Distance metric (#8855)
VijayVignesh1 Oct 19, 2026
99b782d
Deprecate MMAR APIs and disable load_from_mmar (#9179)
Talhax55z Oct 19, 2026
7c74e0f
docs: add missing upsample_mode parameter documentation to SegResNetD…
Talhax55z Oct 19, 2026
5ff313e
Add anisotropic edge-case tests for SegResNetDS shape validation (#9128)
Talhax55z Oct 19, 2026
d820099
feat(metrics): add likelihood ratios and diagnostic odds ratio to Con…
qinxwew Oct 20, 2026
d06ceb9
Fix spatial_ndim after EnsureChannelFirst adds a channel dim (#9185)
Spenhouet Oct 20, 2026
bbebca6
Remove docs for parameters that don't exist (#9169)
caitlin-leonard Oct 20, 2026
029b880
fix(ZarrAvgMerger): use separate tmpdir attrs to prevent value_store …
Zeesejo Oct 21, 2026
6920610
fix(metrics): return the minimum for Hausdorff percentile=0 (#9033)
aymuos15 Oct 22, 2026
874343b
Fix eager nvimgcodec plugin import (#9176)
vikasreddy11 Oct 23, 2026
4f150a8
fix(metrics): exclude ignored voxels from confusion matrix scores (#9…
JoaoMarcos44 Oct 24, 2026
76015b5
feat(networks): opt-in native-resolution deep supervision output for …
aymuos15 Oct 26, 2026
08dedf9
Fix/intensity remap range (#9155)
devteamaegis Oct 27, 2026
e0cee14
Fix unspecified spatial size for rank-one resampling and add NIfTI re…
moonbirdyang Oct 27, 2026
2c54111
Fix per_component Dice silently ignoring ignore_index (#9171)
SID-6921 Oct 28, 2026
e33ed18
Check nnUNet plans network names against allowed modules (#9237)
garciadias Oct 29, 2026
9de734b
Restore algo state values literally in algo_from_json (#9233)
garciadias Oct 29, 2026
4e9b19c
Quote arguments in the NNI trial command (#9235)
garciadias Oct 30, 2026
65be740
Confine BundleAlgo.export_to_disk to the output directory (#9236)
garciadias Oct 30, 2026
827fa24
Add ConvNeXt for 1D, 2D and 3D inputs (#8995)
VenkateswarluNagineni Oct 30, 2026
5b7e314
Add root-level AGENTS.md for AI coding assistants (#9212)
arxh80 Nov 2, 2026
41df4d9
Fix argument names in QuickNAT docstrings (#9148)
caitlin-leonard Nov 3, 2026
0e9fc83
test(nifti): cover 2D writer without target affine (#9173)
px12811 Nov 3, 2026
c5bddb2
Fix compiled lazy resampling with align_corners=True (#9248)
vikasreddy11 Nov 4, 2026
1f381cd
Include C++ and CUDA sources in source distribution
vikasreddy11 Nov 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# AGENTS.md

Guidance for AI coding assistants (Claude Code, Codex, Copilot agent mode, Cursor, Gemini CLI, Jules, Aider, ...)
working in this repository. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full human contributor guide and
[the tool-assisted contribution policy](CONTRIBUTING.md#tool-assisted-contribution-policy) for what is and isn't
acceptable when using these tools — this file is about *how the codebase works*, not *whether AI assistance is
allowed*.

## Project overview

MONAI is a PyTorch-based, open-source framework for deep learning in healthcare imaging. See
[pyproject.toml](pyproject.toml) and [setup.py](setup.py) for the supported Python and PyTorch versions.

## Repository map

| Path | Purpose |
|------|---------|
| `monai/` | the library itself: transforms, networks, losses, metrics, data, bundle, apps, engines, etc. |
| `tests/` | unit and integration tests, mirroring `monai/`'s layout |
| `docs/` | Sphinx documentation sources (`docs/source/*.rst`, `*.md`) |
| `runtests.sh` | the test and style-check entry point used in CI and locally |

## Required workflow

- Pull requests target the `dev` branch, not `main`.
- Every commit needs a DCO sign-off (`git commit -s`, i.e. a `Signed-off-by:` trailer). The DCO app fails PRs
with commits missing it; see [Signing your work](CONTRIBUTING.md#signing-your-work).
- Format and lint with `./runtests.sh --autofix` (runs `isort`, `black` and `ruff check --fix`) before
committing. If pre-commit hooks are installed (`./runtests.sh --setup`), the same checks also run on every
`git commit`.

## Code conventions

- Optional third-party dependencies are imported lazily via `monai.utils.optional_import`, not a bare
`import`, so the package stays usable without every extra installed.
- A new public API needs a corresponding entry in `docs/source/*.rst` (or the matching `.md` file for guides),
not just a docstring.
- A slow test is decorated with `skip_if_quick` (`tests/test_utils.py`); a test module that needs an optional
dependency is added to `exclude_cases` in `tests/min_tests.py`.

## What stays out of this file

Package-specific traps (e.g. for `monai/transforms`, `monai/networks`, `monai/bundle`) are intentionally not
covered here — this root file only carries rules that apply repo-wide. Nested, package-level `AGENTS.md` files
can follow in later PRs if the maintainers want that detail; see the discussion on #9199.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ include README.md
include LICENSE

prune tests
recursive-include monai/csrc *.cpp *.cu *.h *.cuh
9 changes: 9 additions & 0 deletions docs/source/apps.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ Applications

`Clara MMARs`
-------------
.. deprecated:: 1.7.0
NVIDIA Clara MMAR support is deprecated and will be removed in version 1.9.0.
Use :doc:`MONAI Bundles <bundle>` instead.
For security reasons, ``load_from_mmar`` now raises
:class:`monai.utils.DeprecatedError` before downloading or loading a model.

.. autofunction:: get_model_spec

.. autofunction:: download_mmar

.. autofunction:: load_from_mmar
Expand Down Expand Up @@ -289,3 +297,4 @@ FastMRIReader
.. autofunction:: monai.apps.nnunet.convert_nnunet_to_monai_bundle
.. autofunction:: monai.apps.nnunet.convert_monai_bundle_to_nnunet
.. autofunction:: monai.apps.nnunet.get_network_from_nnunet_plans
.. autodata:: monai.apps.nnunet.DEFAULT_NNUNET_NETWORK_MODULES
30 changes: 30 additions & 0 deletions docs/source/networks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,11 @@ Layers

.. currentmodule:: monai.networks.layers

`LayerNormNd`
~~~~~~~~~~~~~
.. autoclass:: LayerNormNd
:members:

`ChannelPad`
~~~~~~~~~~~~
.. autoclass:: ChannelPad
Expand Down Expand Up @@ -472,6 +477,31 @@ Nets
.. autoclass:: AHNet
:members:

`ConvNeXt`
~~~~~~~~~~
.. autoclass:: ConvNeXt
:members:

`ConvNeXtTiny`
~~~~~~~~~~~~~~
.. autoclass:: ConvNeXtTiny

`ConvNeXtSmall`
~~~~~~~~~~~~~~~
.. autoclass:: ConvNeXtSmall

`ConvNeXtBase`
~~~~~~~~~~~~~~
.. autoclass:: ConvNeXtBase

`ConvNeXtLarge`
~~~~~~~~~~~~~~~
.. autoclass:: ConvNeXtLarge

`ConvNeXtXLarge`
~~~~~~~~~~~~~~~~
.. autoclass:: ConvNeXtXLarge

`DenseNet`
~~~~~~~~~~
.. autoclass:: DenseNet
Expand Down
1 change: 1 addition & 0 deletions monai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def filter(self, record):
# *.so files and folder "_C" may not exist when the cpp extensions are not compiled
excludes = "|".join(
[
"(.*(nvimgcodec_pydicom_plugin)$)",
"(^(monai.handlers))",
"(^(monai.bundle))",
"(^(monai.fl))",
Expand Down
4 changes: 2 additions & 2 deletions monai/apps/auto3dseg/auto_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from monai.apps.auto3dseg.bundle_gen import BundleGen
from monai.apps.auto3dseg.data_analyzer import DataAnalyzer
from monai.apps.auto3dseg.ensemble_builder import EnsembleRunner
from monai.apps.auto3dseg.hpo_gen import NNIGen
from monai.apps.auto3dseg.hpo_gen import NNIGen, _nni_trial_command
from monai.apps.auto3dseg.utils import export_bundle_algo_history, import_bundle_algo_history
from monai.apps.utils import get_logger
from monai.auto3dseg.utils import algo_to_json
Expand Down Expand Up @@ -797,7 +797,7 @@ def _train_algo_in_nni(self, history: list[dict[str, Any]]) -> None:
nni_config[key] = self.hpo_params[key]
nni_config.update({"experimentName": name})
nni_config.update({"search_space": self.search_space})
trial_cmd = "python -m monai.apps.auto3dseg NNIGen run_algo " + obj_filename + " " + self.work_dir
trial_cmd = _nni_trial_command(obj_filename, self.work_dir)
nni_config.update({"trialCommand": trial_cmd})
nni_config_filename = os.path.abspath(os.path.join(self.work_dir, f"{name}_nni_config.yaml"))
ConfigParser.export_config_file(nni_config, nni_config_filename, fmt="yaml", default_flow_style=None)
Expand Down
62 changes: 60 additions & 2 deletions monai/apps/auto3dseg/bundle_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from __future__ import annotations

import importlib
import ntpath
import os
import re
import shutil
Expand Down Expand Up @@ -50,6 +51,52 @@
__all__ = ["BundleAlgo", "BundleGen"]


def _check_single_path_component(value: Any, what: str) -> None:
"""
Require ``value`` to be one non-empty directory name with no path separators, drive or NUL character.

Args:
value: the name to check.
what: a description of the value, used in the error message.

Raises:
ValueError: if ``value`` is not a plain directory name.
"""
name = str(value)
separators = {os.sep, "/"} | ({os.altsep} if os.altsep else set())
if (
not name
or name in (".", "..")
or "\x00" in name
or any(sep in name for sep in separators)
or os.path.isabs(name)
or ntpath.splitdrive(name)[0]
):
raise ValueError(f"Invalid {what} {value!r}: expected a single directory name without path separators.")


def _confined_join(root: str, name: str) -> str:
"""
Join ``name`` to ``root`` and check that the resolved result stays inside ``root``.

Args:
root: the parent directory.
name: the child directory name.

Returns:
the joined path.

Raises:
ValueError: if the resolved path is not inside ``root``.
"""
path = os.path.join(root, name)
real_root = os.path.realpath(root)
real_path = os.path.realpath(path)
if real_path == real_root or os.path.commonpath([real_root, real_path]) != real_root:
raise ValueError(f"Invalid path: {path!r} is not inside {root!r}.")
return path


class BundleAlgo(Algo):
"""
An algorithm represented by a set of bundle configurations and scripts.
Expand Down Expand Up @@ -176,18 +223,29 @@ def export_to_disk(self, output_path: str, algo_name: str, **kwargs: Any) -> Non
Args:
output_path: Path to export the 'scripts' and 'configs' directories.
algo_name: the identifier of the algorithm (usually contains the name and extra info like fold ID).
It must be a single directory name: path separators, ``.`` and ``..`` are rejected.
kwargs: other parameters, including: "copy_dirs=True/False" means whether to copy the template as output
instead of inplace operation, "fill_template=True/False" means whether to fill the placeholders
in the template. other parameters are for `fill_template_config` function.


Raises:
ValueError: if ``algo_name`` or the Algo ``name`` is not a single directory name, or the destination
does not resolve inside ``output_path``.
"""
if kwargs.pop("copy_dirs", True):
self.output_path = os.path.join(output_path, algo_name)
_check_single_path_component(algo_name, "algo_name")
if self.name: # an empty name copies the whole template_path
_check_single_path_component(self.name, "the Algo name")
source = os.path.join(str(self.template_path), self.name)
if not os.path.isdir(source):
raise ValueError(f"Algo template folder {source!r} does not exist.")
self.output_path = _confined_join(output_path, algo_name)
os.makedirs(self.output_path, exist_ok=True)
if os.path.isdir(self.output_path):
shutil.rmtree(self.output_path)
# copy algorithm_templates/<Algo> to the working directory output_path
shutil.copytree(os.path.join(str(self.template_path), self.name), self.output_path)
shutil.copytree(source, self.output_path)
else:
self.output_path = str(self.template_path)
if kwargs.pop("fill_template", True):
Expand Down
82 changes: 81 additions & 1 deletion monai/apps/auto3dseg/hpo_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from __future__ import annotations

import os
import shlex
import sys
from abc import abstractmethod
from copy import deepcopy
from typing import Any, cast
Expand All @@ -32,6 +34,84 @@
__all__ = ["HPOGen", "NNIGen", "OptunaGen"]


# cmd.exe expands or ends a quoted argument at these characters even inside double quotes
_CMD_UNSAFE_CHARS = frozenset('"%!\r\n\x00')


def _quote_trial_arg(arg: Any, windows: bool | None = None) -> str:
"""
Quote one argument of an NNI ``trialCommand`` as a literal for the shell NNI uses.

NNI's local training service runs ``trialCommand`` with bash on Linux and macOS and with ``cmd.exe`` on
Windows. For ``cmd.exe`` the argument is wrapped in double quotes, which make ``&``, ``|``, ``<``, ``>``,
``^`` and parentheses literal, and backslashes before the closing quote are doubled so the program receives
the argument unchanged. Characters that ``cmd.exe`` interprets even inside double quotes are rejected.

Args:
arg: the argument value.
windows: quote for ``cmd.exe`` instead of a POSIX shell. Defaults to ``sys.platform == "win32"``.

Returns:
the quoted argument.

Raises:
ValueError: on Windows, if the argument contains ``"``, ``%``, ``!`` or a line break or NUL character.
"""
text = str(arg)
if sys.platform == "win32" if windows is None else windows:
unsafe = sorted(_CMD_UNSAFE_CHARS.intersection(text))
if unsafe:
raise ValueError(f"Cannot pass {text!r} to an NNI trial on Windows: it contains {unsafe}.")
trailing = len(text) - len(text.rstrip("\\"))
return '"' + text + "\\" * trailing + '"'
return shlex.quote(text)


def _nni_trial_command(obj_filename: Any, work_dir: Any, windows: bool | None = None) -> str:
"""
Build the NNI ``trialCommand`` that runs one algo trial.

The fixed part of the command is plain text; the two paths are quoted with ``_quote_trial_arg`` so they
are passed to ``NNIGen run_algo`` unchanged. On a POSIX shell any path is accepted; ``cmd.exe`` cannot
carry a few characters even inside double quotes, see ``_quote_trial_arg``.

Args:
obj_filename: the serialized algo object file.
work_dir: the Auto3DSeg working directory.
windows: quote for ``cmd.exe`` instead of a POSIX shell. Defaults to ``sys.platform == "win32"``.

Returns:
the command string to store as ``trialCommand``.

Raises:
ValueError: on Windows, if ``obj_filename`` or ``work_dir`` contains ``"``, ``%``, ``!``, a line
break or a NUL character.
"""
paths = " ".join(_quote_trial_arg(p, windows) for p in (obj_filename, work_dir))
return f"python -m monai.apps.auto3dseg NNIGen run_algo {paths}"


def _quote_or_raw(arg: Any, windows: bool | None = None) -> str:
"""
Quote one argument of the printed trialCommand instruction, falling back to the unquoted text.

``print_bundle_algo_instruction`` only reports how to write the command, so a path its shell cannot
quote must not stop the instructions from printing: warn and print the path as it is.

Args:
arg: the argument value.
windows: quote for ``cmd.exe`` instead of a POSIX shell. Defaults to ``sys.platform == "win32"``.

Returns:
the quoted argument, or ``str(arg)`` if the shell cannot quote it.
"""
try:
return _quote_trial_arg(arg, windows)
except ValueError as error:
logger.warning(f"Leaving {str(arg)!r} unquoted in the printed trialCommand: {error}")
return str(arg)


class HPOGen(AlgoGen):
"""
The base class for hyperparameter optimization (HPO) interfaces to generate algos in the Auto3Dseg pipeline.
Expand Down Expand Up @@ -143,7 +223,7 @@ def print_bundle_algo_instruction(self):
logger.info("=" * 140)
logger.info("If NNI will run in your local env: ")
logger.info("1. Add the following line to the trialCommand in your NNI config: ")
logger.info(f"{hint} {self.obj_filename} {{result_dir}}")
logger.info(f"{hint} {_quote_or_raw(self.obj_filename)} {{result_dir}}")
logger.info("-" * 140)
logger.info("If NNI will run in a remote env: ")
logger.info(
Expand Down
2 changes: 0 additions & 2 deletions monai/apps/detection/transforms/dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,6 @@ class AffineBoxToImageCoordinated(MapTransform, InvertibleTransform):
Args:
box_keys: Keys to pick box data for transformation. The box mode is assumed to be ``StandardMode``.
box_ref_image_keys: The single key that represents the reference image to which ``box_keys`` are attached.
remove_empty: whether to remove the boxes that are actually empty
allow_missing_keys: don't raise exception if key is missing.
image_meta_key: explicitly indicate the key of the corresponding metadata dictionary.
for example, for data with key `image`, the metadata by default is in `image_meta_dict`.
Expand Down Expand Up @@ -347,7 +346,6 @@ class AffineBoxToWorldCoordinated(AffineBoxToImageCoordinated):
Args:
box_keys: Keys to pick box data for transformation. The box mode is assumed to be ``StandardMode``.
box_ref_image_keys: The single key that represents the reference image to which ``box_keys`` are attached.
remove_empty: whether to remove the boxes that are actually empty
allow_missing_keys: don't raise exception if key is missing.
image_meta_key: explicitly indicate the key of the corresponding metadata dictionary.
for example, for data with key `image`, the metadata by default is in `image_meta_dict`.
Expand Down
Loading