From 75dd1ef8de57cb8a576c9b2ff15f250fad4a0b52 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 1 Oct 2025 06:48:10 +0000
Subject: [PATCH 01/61] Bump actions/download-artifact from 4 to 5 (#8590)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 4 to 5.
Release notes
Sourced from actions/download-artifact's
releases.
v5.0.0
What's Changed
v5.0.0
🚨 Breaking Change
This release fixes an inconsistency in path behavior for single
artifact downloads by ID. If you're downloading single artifacts
by ID, the output path may change.
What Changed
Previously, single artifact downloads behaved
differently depending on how you specified the artifact:
- By name:
name: my-artifact → extracted
to path/ (direct)
- By ID:
artifact-ids: 12345 → extracted
to path/my-artifact/ (nested)
Now both methods are consistent:
- By name:
name: my-artifact → extracted
to path/ (unchanged)
- By ID:
artifact-ids: 12345 → extracted
to path/ (fixed - now direct)
Migration Guide
✅ No Action Needed If:
- You download artifacts by name
- You download multiple artifacts by ID
- You already use
merge-multiple: true as a
workaround
⚠️ Action Required If:
You download single artifacts by ID and your
workflows expect the nested directory structure.
Before v5 (nested structure):
- uses: actions/download-artifact@v4
with:
artifact-ids: 12345
path: dist
# Files were in: dist/my-artifact/
Where my-artifact is the name of the artifact you
previously uploaded
To maintain old behavior (if needed):
</tr></table>
... (truncated)
Commits
634f93c
Merge pull request #416
from actions/single-artifact-id-download-path
b19ff43
refactor: resolve download path correctly in artifact download tests
(mainly ...
e262cbe
bundle dist
bff23f9
update docs
fff8c14
fix download path logic when downloading a single artifact by id
448e3f8
Merge pull request #407
from actions/nebuk89-patch-1
47225c4
Update README.md
- See full diff in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Signed-off-by: jirka
---
.github/workflows/docker.yml | 2 +-
.github/workflows/release.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 17ffe4cf90..821808bab2 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -57,7 +57,7 @@ jobs:
with:
ref: dev
- name: Download version
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v5
with:
name: _version.py
- name: docker_build
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cb0e109bb7..5815bca649 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -127,7 +127,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Download version
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v5
with:
name: _version.py
- name: Set tag
From 42901bbbc220238fbf473cf8ff6020ba6c548cc6 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 1 Oct 2025 14:39:47 +0000
Subject: [PATCH 02/61] Bump actions/checkout from 4 to 5 (#8588)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to
5.
Release notes
Sourced from actions/checkout's
releases.
v5.0.0
What's Changed
⚠️ Minimum Compatible Runner Version
v2.327.1
Release
Notes
Make sure your runner is updated to this version or newer to use this
release.
Full Changelog: https://github.com/actions/checkout/compare/v4...v5.0.0
v4.3.0
What's Changed
New Contributors
Full Changelog: https://github.com/actions/checkout/compare/v4...v4.3.0
v4.2.2
What's Changed
Full Changelog: https://github.com/actions/checkout/compare/v4.2.1...v4.2.2
v4.2.1
What's Changed
New Contributors
Full Changelog: https://github.com/actions/checkout/compare/v4.2.0...v4.2.1
... (truncated)
Changelog
Sourced from actions/checkout's
changelog.
Changelog
V5.0.0
V4.3.0
v4.2.2
v4.2.1
v4.2.0
v4.1.7
v4.1.6
v4.1.5
v4.1.4
v4.1.3
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Signed-off-by: jirka
---
.github/workflows/blossom-ci.yml | 2 +-
.github/workflows/codeql-analysis.yml | 2 +-
.github/workflows/conda.yml | 2 +-
.github/workflows/cron-ngc-bundle.yml | 2 +-
.github/workflows/cron.yml | 8 ++++----
.github/workflows/docker.yml | 4 ++--
.github/workflows/integration.yml | 4 ++--
.github/workflows/pythonapp-gpu.yml | 2 +-
.github/workflows/pythonapp-min.yml | 6 +++---
.github/workflows/pythonapp.yml | 8 ++++----
.github/workflows/release.yml | 6 +++---
.github/workflows/setupapp.yml | 6 +++---
.github/workflows/weekly-preview.yml | 4 ++--
13 files changed, 28 insertions(+), 28 deletions(-)
diff --git a/.github/workflows/blossom-ci.yml b/.github/workflows/blossom-ci.yml
index 63ac5536d8..66a15381bb 100644
--- a/.github/workflows/blossom-ci.yml
+++ b/.github/workflows/blossom-ci.yml
@@ -51,7 +51,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
repository: ${{ fromJson(needs.Authorization.outputs.args).repo }}
ref: ${{ fromJson(needs.Authorization.outputs.args).ref }}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 18f1519b5a..60b68e3c31 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
diff --git a/.github/workflows/conda.yml b/.github/workflows/conda.yml
index 275c7052f2..fd680e14c8 100644
--- a/.github/workflows/conda.yml
+++ b/.github/workflows/conda.yml
@@ -32,7 +32,7 @@ jobs:
minimum-size: 8GB
maximum-size: 16GB
disk-root: "D:"
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Clean up disk space
run: |
find /opt/hostedtoolcache/* -maxdepth 0 ! -name 'Python' -exec rm -rf {} \;
diff --git a/.github/workflows/cron-ngc-bundle.yml b/.github/workflows/cron-ngc-bundle.yml
index d4b45e1d55..e3bf232f34 100644
--- a/.github/workflows/cron-ngc-bundle.yml
+++ b/.github/workflows/cron-ngc-bundle.yml
@@ -17,7 +17,7 @@ jobs:
if: github.repository == 'Project-MONAI/MONAI'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml
index 77fe9ca3a2..f33faf8c1a 100644
--- a/.github/workflows/cron.yml
+++ b/.github/workflows/cron.yml
@@ -32,7 +32,7 @@ jobs:
options: "--gpus all"
runs-on: [self-hosted, linux, x64, common]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: apt install
run: |
apt-get update
@@ -82,7 +82,7 @@ jobs:
options: "--gpus all"
runs-on: [self-hosted, linux, x64, integration]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Install APT dependencies
run: |
apt-get update
@@ -131,7 +131,7 @@ jobs:
options: "--gpus all"
runs-on: [self-hosted, linux, x64, integration]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install the dependencies
@@ -233,7 +233,7 @@ jobs:
options: "--gpus all --ipc=host"
runs-on: [self-hosted, linux, x64, integration]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Install MONAI
id: monai-install
run: |
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 821808bab2..29d5be0905 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -21,7 +21,7 @@ jobs:
if: ${{ false }} # disable docker build job project-monai/monai#7450
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
# full history so that we can git describe
with:
ref: dev
@@ -53,7 +53,7 @@ jobs:
needs: versioning_dev
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
ref: dev
- name: Download version
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 5be2ebb86c..b63ae98943 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: [self-hosted, linux, x64, command]
steps:
# checkout the pull request branch
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
token: ${{ secrets.PR_MAINTAIN }}
repository: ${{ github.event.client_payload.pull_request.head.repo.full_name }}
@@ -89,7 +89,7 @@ jobs:
runs-on: [self-hosted, linux, x64, command1]
steps:
# checkout the pull request branch
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
token: ${{ secrets.PR_MAINTAIN }}
repository: ${{ github.event.client_payload.pull_request.head.repo.full_name }}
diff --git a/.github/workflows/pythonapp-gpu.yml b/.github/workflows/pythonapp-gpu.yml
index 6b0a5084a2..347b49bba3 100644
--- a/.github/workflows/pythonapp-gpu.yml
+++ b/.github/workflows/pythonapp-gpu.yml
@@ -44,7 +44,7 @@ jobs:
options: --gpus all --env NVIDIA_DISABLE_REQUIRE=true # workaround for unsatisfied condition: cuda>=11.6
runs-on: [self-hosted, linux, x64, common]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: apt install
if: github.event.pull_request.merged != true
run: |
diff --git a/.github/workflows/pythonapp-min.yml b/.github/workflows/pythonapp-min.yml
index 0d147ca264..91b2647641 100644
--- a/.github/workflows/pythonapp-min.yml
+++ b/.github/workflows/pythonapp-min.yml
@@ -30,7 +30,7 @@ jobs:
os: [windows-latest, macOS-latest, ubuntu-latest]
timeout-minutes: 40
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
@@ -79,7 +79,7 @@ jobs:
python-version: ['3.9', '3.10', '3.11', '3.12']
timeout-minutes: 40
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
@@ -127,7 +127,7 @@ jobs:
pytorch-version: ['2.5.1', '2.6.0', '2.7.1', '2.8.0']
timeout-minutes: 40
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml
index c68c879231..ae74c83e58 100644
--- a/.github/workflows/pythonapp.yml
+++ b/.github/workflows/pythonapp.yml
@@ -28,7 +28,7 @@ jobs:
matrix:
opt: ["codeformat", "pytype", "mypy"]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
@@ -70,7 +70,7 @@ jobs:
minimum-size: 8GB
maximum-size: 16GB
disk-root: "D:"
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
@@ -129,7 +129,7 @@ jobs:
QUICKTEST: True
shell: bash
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Python 3.9
@@ -213,7 +213,7 @@ jobs:
build-docs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5815bca649..f62f2ad301 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -15,7 +15,7 @@ jobs:
matrix:
python-version: ['3.9', '3.10', '3.11']
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
@@ -93,7 +93,7 @@ jobs:
needs: packaging
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
# full history so that we can git describe
with:
fetch-depth: 0
@@ -125,7 +125,7 @@ jobs:
needs: versioning
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Download version
uses: actions/download-artifact@v5
with:
diff --git a/.github/workflows/setupapp.yml b/.github/workflows/setupapp.yml
index d9ce9976b8..878dd8e93e 100644
--- a/.github/workflows/setupapp.yml
+++ b/.github/workflows/setupapp.yml
@@ -28,7 +28,7 @@ jobs:
options: --gpus all
runs-on: [self-hosted, linux, x64, integration]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: cache weekly timestamp
id: pip-cache
run: |
@@ -83,7 +83,7 @@ jobs:
matrix:
python-version: ['3.9', '3.10', '3.11']
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
@@ -159,7 +159,7 @@ jobs:
python -c 'import monai; monai.config.print_config()'
- name: Get the test cases (dev branch only)
if: github.ref == 'refs/heads/dev'
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
ref: dev
- name: Quick test installed (dev branch only)
diff --git a/.github/workflows/weekly-preview.yml b/.github/workflows/weekly-preview.yml
index 7975eb8b1d..066ffed632 100644
--- a/.github/workflows/weekly-preview.yml
+++ b/.github/workflows/weekly-preview.yml
@@ -11,7 +11,7 @@ jobs:
matrix:
opt: ["codeformat", "pytype", "mypy"]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
@@ -42,7 +42,7 @@ jobs:
if: github.repository == 'Project-MONAI/MONAI'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
ref: dev
fetch-depth: 0
From bb5b425fb682bdbf8ca090fc317be50d96cfd459 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 10 Oct 2025 11:20:24 +0800
Subject: [PATCH 03/61] Bump actions/setup-python from 5 to 6 (#8589)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [actions/setup-python](https://github.com/actions/setup-python)
from 5 to 6.
Release notes
Sourced from actions/setup-python's
releases.
v6.0.0
What's Changed
Breaking Changes
Make sure your runner is on version v2.327.1 or later to ensure
compatibility with this release. See
Release Notes
Enhancements:
Bug fixes:
Dependency updates:
New Contributors
Full Changelog: https://github.com/actions/setup-python/compare/v5...v6.0.0
v5.6.0
What's Changed
Full Changelog: https://github.com/actions/setup-python/compare/v5...v5.6.0
v5.5.0
What's Changed
Enhancements:
Bug fixes:
... (truncated)
Commits
e797f83
Upgrade to node 24 (#1164)
3d1e2d2
Revert "Enhance cache-dependency-path handling to support files
outside the w...
65b0712
Clarify pythonLocation behavior for PyPy and GraalPy in environment
variables...
5b668cf
Bump actions/checkout from 4 to 5 (#1181)
f62a0e2
Change missing cache directory error to warning (#1182)
9322b3c
Upgrade setuptools to 78.1.1 to fix path traversal vulnerability in
PackageIn...
fbeb884
Bump form-data to fix critical vulnerabilities #182
& #183
(#1163)
03bb615
Bump idna from 2.9 to 3.7 in /tests/data (#843)
36da51d
Add version parsing from Pipfile (#1067)
3c6f142
update documentation (#1156)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Signed-off-by: jirka
---
.github/workflows/blossom-ci.yml | 2 +-
.github/workflows/cron-ngc-bundle.yml | 2 +-
.github/workflows/docker.yml | 2 +-
.github/workflows/pythonapp-min.yml | 6 +++---
.github/workflows/pythonapp.yml | 8 ++++----
.github/workflows/release.yml | 4 ++--
.github/workflows/setupapp.yml | 4 ++--
.github/workflows/weekly-preview.yml | 4 ++--
8 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/.github/workflows/blossom-ci.yml b/.github/workflows/blossom-ci.yml
index 66a15381bb..63ac5536d8 100644
--- a/.github/workflows/blossom-ci.yml
+++ b/.github/workflows/blossom-ci.yml
@@ -51,7 +51,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v5
+ uses: actions/checkout@v4
with:
repository: ${{ fromJson(needs.Authorization.outputs.args).repo }}
ref: ${{ fromJson(needs.Authorization.outputs.args).ref }}
diff --git a/.github/workflows/cron-ngc-bundle.yml b/.github/workflows/cron-ngc-bundle.yml
index e3bf232f34..c9bebc7bff 100644
--- a/.github/workflows/cron-ngc-bundle.yml
+++ b/.github/workflows/cron-ngc-bundle.yml
@@ -19,7 +19,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Set up Python 3.9
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: '3.9'
- name: cache weekly timestamp
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 29d5be0905..51e76756b2 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -27,7 +27,7 @@ jobs:
ref: dev
fetch-depth: 0
- name: Set up Python 3.9
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: '3.9'
- shell: bash
diff --git a/.github/workflows/pythonapp-min.yml b/.github/workflows/pythonapp-min.yml
index 91b2647641..1b193b01de 100644
--- a/.github/workflows/pythonapp-min.yml
+++ b/.github/workflows/pythonapp-min.yml
@@ -32,7 +32,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Set up Python 3.9
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: '3.9'
- name: Prepare pip wheel
@@ -81,7 +81,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Prepare pip wheel
@@ -129,7 +129,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Set up Python 3.9
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: '3.9'
- name: Prepare pip wheel
diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml
index ae74c83e58..535ba187a8 100644
--- a/.github/workflows/pythonapp.yml
+++ b/.github/workflows/pythonapp.yml
@@ -30,7 +30,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Set up Python 3.9
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: '3.9'
- name: cache weekly timestamp
@@ -72,7 +72,7 @@ jobs:
disk-root: "D:"
- uses: actions/checkout@v5
- name: Set up Python 3.9
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: '3.9'
- name: Prepare pip wheel
@@ -133,7 +133,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up Python 3.9
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: '3.9'
- name: cache weekly timestamp
@@ -215,7 +215,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Set up Python 3.9
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: '3.9'
- name: cache weekly timestamp
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f62f2ad301..b01a20336d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,7 +19,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install setuptools
@@ -98,7 +98,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up Python 3.9
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: '3.9'
- shell: bash
diff --git a/.github/workflows/setupapp.yml b/.github/workflows/setupapp.yml
index 878dd8e93e..acd72eaf6f 100644
--- a/.github/workflows/setupapp.yml
+++ b/.github/workflows/setupapp.yml
@@ -87,7 +87,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: cache weekly timestamp
@@ -128,7 +128,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Python 3.9
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: '3.9'
- name: cache weekly timestamp
diff --git a/.github/workflows/weekly-preview.yml b/.github/workflows/weekly-preview.yml
index 066ffed632..93d6f67f09 100644
--- a/.github/workflows/weekly-preview.yml
+++ b/.github/workflows/weekly-preview.yml
@@ -13,7 +13,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Set up Python 3.9
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: '3.9'
- name: cache weekly timestamp
@@ -47,7 +47,7 @@ jobs:
ref: dev
fetch-depth: 0
- name: Set up Python 3.9
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: '3.9'
- name: Install setuptools
From e465d66ce700133f236135bb6b9eb2d213b1d371 Mon Sep 17 00:00:00 2001
From: jirka
Date: Tue, 28 Oct 2025 20:33:24 +0100
Subject: [PATCH 04/61] Replace `pyupgrade` with builtin Ruff's UP rule
Signed-off-by: jirka
---
.pre-commit-config.yaml | 16 ++++------------
pyproject.toml | 1 +
2 files changed, 5 insertions(+), 12 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9621a1fe95..86c01b09dc 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -30,22 +30,14 @@ repos:
rev: v0.7.0
hooks:
- id: ruff
- args:
- - --fix
-
- - repo: https://github.com/asottile/pyupgrade
- rev: v3.19.0
- hooks:
- - id: pyupgrade
- args: [--py39-plus, --keep-runtime-typing]
- name: Upgrade code with exceptions
+ args: ["--fix"]
exclude: |
(?x)(
^versioneer.py|
^monai/_version.py|
- ^monai/networks/| # avoid typing rewrites
- ^monai/apps/detection/utils/anchor_utils.py| # avoid typing rewrites
- ^tests/test_compute_panoptic_quality.py # avoid typing rewrites
+ ^monai/networks/| # todo: avoid typing rewrites
+ ^monai/apps/detection/utils/anchor_utils.py| # todo: avoid typing rewrites
+ ^tests/test_compute_panoptic_quality.py # todo: avoid typing rewrites
)
- repo: https://github.com/asottile/yesqa
diff --git a/pyproject.toml b/pyproject.toml
index 76b26731bf..ed8c2a32dd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -45,6 +45,7 @@ target-version = "py39"
select = [
"E", "F", "W", # flake8
"NPY", # NumPy specific rules
+ "UP", # pyupgrade
]
extend-ignore = [
"E741", # ambiguous variable name
From cf427fe4c171eb1d96f9920c645321f4823ae7dd Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 28 Oct 2025 19:35:03 +0000
Subject: [PATCH 05/61] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
Signed-off-by: jirka
---
monai/apps/deepgrow/dataset.py | 18 ++----------------
monai/handlers/stats_handler.py | 4 ++--
monai/handlers/tensorboard_handlers.py | 4 ++--
monai/metrics/utils.py | 4 ++--
tests/profile_subclass/profiling.py | 4 +---
5 files changed, 9 insertions(+), 25 deletions(-)
diff --git a/monai/apps/deepgrow/dataset.py b/monai/apps/deepgrow/dataset.py
index 802d86e0c7..9609390cb3 100644
--- a/monai/apps/deepgrow/dataset.py
+++ b/monai/apps/deepgrow/dataset.py
@@ -201,14 +201,7 @@ def _save_data_2d(vol_idx, vol_image, vol_label, dataset_dir, relative_path):
logging.warning(f"Unique labels {unique_labels_count} exceeds 20. Please check if this is correct.")
logging.info(
- "{} => Image Shape: {} => {}; Label Shape: {} => {}; Unique Labels: {}".format(
- vol_idx,
- vol_image.shape,
- image_count,
- vol_label.shape if vol_label is not None else None,
- label_count,
- unique_labels_count,
- )
+ f"{vol_idx} => Image Shape: {vol_image.shape} => {image_count}; Label Shape: {vol_label.shape if vol_label is not None else None} => {label_count}; Unique Labels: {unique_labels_count}"
)
return data_list
@@ -259,13 +252,6 @@ def _save_data_3d(vol_idx, vol_image, vol_label, dataset_dir, relative_path):
logging.warning(f"Unique labels {unique_labels_count} exceeds 20. Please check if this is correct.")
logging.info(
- "{} => Image Shape: {} => {}; Label Shape: {} => {}; Unique Labels: {}".format(
- vol_idx,
- vol_image.shape,
- image_count,
- vol_label.shape if vol_label is not None else None,
- label_count,
- unique_labels_count,
- )
+ f"{vol_idx} => Image Shape: {vol_image.shape} => {image_count}; Label Shape: {vol_label.shape if vol_label is not None else None} => {label_count}; Unique Labels: {unique_labels_count}"
)
return data_list
diff --git a/monai/handlers/stats_handler.py b/monai/handlers/stats_handler.py
index 214872fef4..1dafde0f54 100644
--- a/monai/handlers/stats_handler.py
+++ b/monai/handlers/stats_handler.py
@@ -260,7 +260,7 @@ def _default_iteration_print(self, engine: Engine) -> None:
"ignoring non-scalar output in StatsHandler,"
" make sure `output_transform(engine.state.output)` returns"
" a scalar or dictionary of key and scalar pairs to avoid this warning."
- " {}:{}".format(name, type(value))
+ f" {name}:{type(value)}"
)
continue # not printing multi dimensional output
out_str += self.key_var_format.format(name, value.item() if isinstance(value, torch.Tensor) else value)
@@ -273,7 +273,7 @@ def _default_iteration_print(self, engine: Engine) -> None:
"ignoring non-scalar output in StatsHandler,"
" make sure `output_transform(engine.state.output)` returns"
" a scalar or a dictionary of key and scalar pairs to avoid this warning."
- " {}".format(type(loss))
+ f" {type(loss)}"
)
if not out_str:
diff --git a/monai/handlers/tensorboard_handlers.py b/monai/handlers/tensorboard_handlers.py
index 44a03710de..20e2d74c8c 100644
--- a/monai/handlers/tensorboard_handlers.py
+++ b/monai/handlers/tensorboard_handlers.py
@@ -257,7 +257,7 @@ def _default_iteration_writer(self, engine: Engine, writer: SummaryWriter | Summ
"ignoring non-scalar output in TensorBoardStatsHandler,"
" make sure `output_transform(engine.state.output)` returns"
" a scalar or dictionary of key and scalar pairs to avoid this warning."
- " {}:{}".format(name, type(value))
+ f" {name}:{type(value)}"
)
continue # not plot multi dimensional output
self._write_scalar(
@@ -280,7 +280,7 @@ def _default_iteration_writer(self, engine: Engine, writer: SummaryWriter | Summ
"ignoring non-scalar output in TensorBoardStatsHandler,"
" make sure `output_transform(engine.state.output)` returns"
" a scalar or a dictionary of key and scalar pairs to avoid this warning."
- " {}".format(type(loss))
+ f" {type(loss)}"
)
writer.flush()
diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py
index 972ec0061e..606a54669b 100644
--- a/monai/metrics/utils.py
+++ b/monai/metrics/utils.py
@@ -13,7 +13,7 @@
import warnings
from collections.abc import Iterable, Sequence
-from functools import lru_cache, partial
+from functools import partial, cache
from types import ModuleType
from typing import Any
@@ -465,7 +465,7 @@ def prepare_spacing(
ENCODING_KERNEL = {2: [[8, 4], [2, 1]], 3: [[[128, 64], [32, 16]], [[8, 4], [2, 1]]]}
-@lru_cache(maxsize=None)
+@cache
def _get_neighbour_code_to_normals_table(device=None):
"""
returns a lookup table. For every binary neighbour code (2x2x2 neighbourhood = 8 neighbours = 8 bits = 256 codes)
diff --git a/tests/profile_subclass/profiling.py b/tests/profile_subclass/profiling.py
index ffa6a8b17d..3e1d99e920 100644
--- a/tests/profile_subclass/profiling.py
+++ b/tests/profile_subclass/profiling.py
@@ -63,9 +63,7 @@ def main():
b_min, b_avg, b_med, b_std = bench(tensor_1, tensor_2)
print(
- "Type {} time (microseconds): min: {}, avg: {}, median: {}, and std {}.".format(
- t.__name__, (10**6 * b_min), (10**6) * b_avg, (10**6) * b_med, (10**6) * b_std
- )
+ f"Type {t.__name__} time (microseconds): min: {10**6 * b_min}, avg: {(10**6) * b_avg}, median: {(10**6) * b_med}, and std {(10**6) * b_std}."
)
From fc10d4d024d3933f18693dcf0c9b39018c83257c Mon Sep 17 00:00:00 2001
From: jirka
Date: Tue, 28 Oct 2025 20:37:51 +0100
Subject: [PATCH 06/61] --unsafe-fixes
Signed-off-by: jirka
---
monai/apps/deepgrow/dataset.py | 8 ++++++--
monai/apps/nnunet/nnunet_bundle.py | 18 +++++++++---------
monai/losses/adversarial_loss.py | 3 +--
monai/losses/dice.py | 2 +-
monai/losses/ds_loss.py | 3 +--
monai/losses/focal_loss.py | 7 +++----
monai/losses/perceptual.py | 3 +--
monai/losses/spatial_mask.py | 4 ++--
monai/losses/sure_loss.py | 16 ++++++++--------
monai/transforms/utility/array.py | 6 +++---
tests/profile_subclass/profiling.py | 3 ++-
11 files changed, 37 insertions(+), 36 deletions(-)
diff --git a/monai/apps/deepgrow/dataset.py b/monai/apps/deepgrow/dataset.py
index 9609390cb3..e597188e74 100644
--- a/monai/apps/deepgrow/dataset.py
+++ b/monai/apps/deepgrow/dataset.py
@@ -201,7 +201,9 @@ def _save_data_2d(vol_idx, vol_image, vol_label, dataset_dir, relative_path):
logging.warning(f"Unique labels {unique_labels_count} exceeds 20. Please check if this is correct.")
logging.info(
- f"{vol_idx} => Image Shape: {vol_image.shape} => {image_count}; Label Shape: {vol_label.shape if vol_label is not None else None} => {label_count}; Unique Labels: {unique_labels_count}"
+ f"{vol_idx} => Image Shape: {vol_image.shape} => {image_count};"
+ f" Label Shape: {vol_label.shape if vol_label is not None else None} => {label_count};"
+ f" Unique Labels: {unique_labels_count}"
)
return data_list
@@ -252,6 +254,8 @@ def _save_data_3d(vol_idx, vol_image, vol_label, dataset_dir, relative_path):
logging.warning(f"Unique labels {unique_labels_count} exceeds 20. Please check if this is correct.")
logging.info(
- f"{vol_idx} => Image Shape: {vol_image.shape} => {image_count}; Label Shape: {vol_label.shape if vol_label is not None else None} => {label_count}; Unique Labels: {unique_labels_count}"
+ f"{vol_idx} => Image Shape: {vol_image.shape} => {image_count};"
+ f" Label Shape: {vol_label.shape if vol_label is not None else None} => {label_count};"
+ f" Unique Labels: {unique_labels_count}"
)
return data_list
diff --git a/monai/apps/nnunet/nnunet_bundle.py b/monai/apps/nnunet/nnunet_bundle.py
index df8f09bf4b..47a0755ddf 100644
--- a/monai/apps/nnunet/nnunet_bundle.py
+++ b/monai/apps/nnunet/nnunet_bundle.py
@@ -13,7 +13,7 @@
import os
import shutil
from pathlib import Path
-from typing import Any, Optional, Union
+from typing import Any
import numpy as np
import torch
@@ -36,9 +36,9 @@
def get_nnunet_trainer(
- dataset_name_or_id: Union[str, int],
+ dataset_name_or_id: str | int,
configuration: str,
- fold: Union[int, str],
+ fold: int | str,
trainer_class_name: str = "nnUNetTrainer",
plans_identifier: str = "nnUNetPlans",
use_compressed_data: bool = False,
@@ -46,7 +46,7 @@ def get_nnunet_trainer(
only_run_validation: bool = False,
disable_checkpointing: bool = False,
device: str = "cuda",
- pretrained_model: Optional[str] = None,
+ pretrained_model: str | None = None,
) -> Any: # type: ignore
"""
Get the nnUNet trainer instance based on the provided configuration.
@@ -166,7 +166,7 @@ class ModelnnUNetWrapper(torch.nn.Module):
restoring network architecture, and setting up the predictor for inference.
"""
- def __init__(self, predictor: object, model_folder: Union[str, Path], model_name: str = "model.pt"): # type: ignore
+ def __init__(self, predictor: object, model_folder: str | Path, model_name: str = "model.pt"): # type: ignore
super().__init__()
self.predictor = predictor
@@ -294,7 +294,7 @@ def forward(self, x: MetaTensor) -> MetaTensor:
return MetaTensor(out_tensor, meta=x.meta)
-def get_nnunet_monai_predictor(model_folder: Union[str, Path], model_name: str = "model.pt") -> ModelnnUNetWrapper:
+def get_nnunet_monai_predictor(model_folder: str | Path, model_name: str = "model.pt") -> ModelnnUNetWrapper:
"""
Initializes and returns a `nnUNetMONAIModelWrapper` containing the corresponding `nnUNetPredictor`.
The model folder should contain the following files, created during training:
@@ -426,9 +426,9 @@ def get_network_from_nnunet_plans(
plans_file: str,
dataset_file: str,
configuration: str,
- model_ckpt: Optional[str] = None,
+ model_ckpt: str | None = None,
model_key_in_ckpt: str = "model",
-) -> Union[torch.nn.Module, Any]:
+) -> torch.nn.Module | Any:
"""
Load and initialize a nnUNet network based on nnUNet plans and configuration.
@@ -518,7 +518,7 @@ def convert_monai_bundle_to_nnunet(nnunet_config: dict, bundle_root_folder: str,
from nnunetv2.utilities.dataset_name_id_conversion import maybe_convert_to_dataset_name
def subfiles(
- folder: Union[str, Path], prefix: Optional[str] = None, suffix: Optional[str] = None, sort: bool = True
+ folder: str | Path, prefix: str | None = None, suffix: str | None = None, sort: bool = True
) -> list[str]:
res = [
i.name
diff --git a/monai/losses/adversarial_loss.py b/monai/losses/adversarial_loss.py
index c7be79243f..3f7dc80098 100644
--- a/monai/losses/adversarial_loss.py
+++ b/monai/losses/adversarial_loss.py
@@ -57,8 +57,7 @@ def __init__(
if criterion.lower() not in list(AdversarialCriterions):
raise ValueError(
- "Unrecognised criterion entered for Adversarial Loss. Must be one in: %s"
- % ", ".join(AdversarialCriterions)
+ "Unrecognised criterion entered for Adversarial Loss. Must be one in: {}".format(", ".join(AdversarialCriterions))
)
# Depending on the criterion, a different activation layer is used.
diff --git a/monai/losses/dice.py b/monai/losses/dice.py
index ed88100edd..99baa34286 100644
--- a/monai/losses/dice.py
+++ b/monai/losses/dice.py
@@ -494,7 +494,7 @@ def __init__(
raise ValueError(f"dist_matrix must be C x C, got {dist_matrix.shape[0]} x {dist_matrix.shape[1]}.")
if weighting_mode not in ["default", "GDL"]:
- raise ValueError("weighting_mode must be either 'default' or 'GDL, got %s." % weighting_mode)
+ raise ValueError(f"weighting_mode must be either 'default' or 'GDL, got {weighting_mode}.")
self.m = dist_matrix
if isinstance(self.m, np.ndarray):
diff --git a/monai/losses/ds_loss.py b/monai/losses/ds_loss.py
index 6a604aa22d..ef359bcfd0 100644
--- a/monai/losses/ds_loss.py
+++ b/monai/losses/ds_loss.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-from typing import Union
import torch
import torch.nn.functional as F
@@ -70,7 +69,7 @@ def get_loss(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
target = F.interpolate(target, size=input.shape[2:], mode=self.interp_mode)
return self.loss(input, target) # type: ignore[no-any-return]
- def forward(self, input: Union[None, torch.Tensor, list[torch.Tensor]], target: torch.Tensor) -> torch.Tensor:
+ def forward(self, input: None | torch.Tensor | list[torch.Tensor], target: torch.Tensor) -> torch.Tensor:
if isinstance(input, (list, tuple)):
weights = self.get_weights(levels=len(input))
loss = torch.tensor(0, dtype=torch.float, device=target.device)
diff --git a/monai/losses/focal_loss.py b/monai/losses/focal_loss.py
index 28d1c0cdc9..a1145c521e 100644
--- a/monai/losses/focal_loss.py
+++ b/monai/losses/focal_loss.py
@@ -13,7 +13,6 @@
import warnings
from collections.abc import Sequence
-from typing import Optional
import torch
import torch.nn.functional as F
@@ -153,7 +152,7 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
if target.shape != input.shape:
raise ValueError(f"ground truth has different shape ({target.shape}) from input ({input.shape})")
- loss: Optional[torch.Tensor] = None
+ loss: torch.Tensor | None = None
input = input.float()
target = target.float()
if self.use_softmax:
@@ -203,7 +202,7 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
def softmax_focal_loss(
- input: torch.Tensor, target: torch.Tensor, gamma: float = 2.0, alpha: Optional[float] = None
+ input: torch.Tensor, target: torch.Tensor, gamma: float = 2.0, alpha: float | None = None
) -> torch.Tensor:
"""
FL(pt) = -alpha * (1 - pt)**gamma * log(pt)
@@ -225,7 +224,7 @@ def softmax_focal_loss(
def sigmoid_focal_loss(
- input: torch.Tensor, target: torch.Tensor, gamma: float = 2.0, alpha: Optional[float] = None
+ input: torch.Tensor, target: torch.Tensor, gamma: float = 2.0, alpha: float | None = None
) -> torch.Tensor:
"""
FL(pt) = -alpha * (1 - pt)**gamma * log(pt)
diff --git a/monai/losses/perceptual.py b/monai/losses/perceptual.py
index 2ae03bc8dc..b818d497c1 100644
--- a/monai/losses/perceptual.py
+++ b/monai/losses/perceptual.py
@@ -95,8 +95,7 @@ def __init__(
if network_type.lower() not in list(PercetualNetworkType):
raise ValueError(
- "Unrecognised criterion entered for Adversarial Loss. Must be one in: %s"
- % ", ".join(PercetualNetworkType)
+ "Unrecognised criterion entered for Adversarial Loss. Must be one in: {}".format(", ".join(PercetualNetworkType))
)
if cache_dir:
diff --git a/monai/losses/spatial_mask.py b/monai/losses/spatial_mask.py
index a4c16236a2..0f823410dd 100644
--- a/monai/losses/spatial_mask.py
+++ b/monai/losses/spatial_mask.py
@@ -14,7 +14,7 @@
import inspect
import warnings
from collections.abc import Callable
-from typing import Any, Optional
+from typing import Any
import torch
from torch.nn.modules.loss import _Loss
@@ -47,7 +47,7 @@ def __init__(
if not callable(self.loss):
raise ValueError("The loss function is not callable.")
- def forward(self, input: torch.Tensor, target: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
+ def forward(self, input: torch.Tensor, target: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:
"""
Args:
input: the shape should be BNH[WD].
diff --git a/monai/losses/sure_loss.py b/monai/losses/sure_loss.py
index fa8820885d..5d0e1c28c5 100644
--- a/monai/losses/sure_loss.py
+++ b/monai/losses/sure_loss.py
@@ -11,7 +11,7 @@
from __future__ import annotations
-from typing import Callable, Optional
+from typing import Callable
import torch
import torch.nn as nn
@@ -42,10 +42,10 @@ def sure_loss_function(
operator: Callable,
x: torch.Tensor,
y_pseudo_gt: torch.Tensor,
- y_ref: Optional[torch.Tensor] = None,
- eps: Optional[float] = -1.0,
- perturb_noise: Optional[torch.Tensor] = None,
- complex_input: Optional[bool] = False,
+ y_ref: torch.Tensor | None = None,
+ eps: float | None = -1.0,
+ perturb_noise: torch.Tensor | None = None,
+ complex_input: bool | None = False,
) -> torch.Tensor:
"""
Args:
@@ -131,7 +131,7 @@ class SURELoss(_Loss):
(https://arxiv.org/pdf/2310.01799.pdf)
"""
- def __init__(self, perturb_noise: Optional[torch.Tensor] = None, eps: Optional[float] = None) -> None:
+ def __init__(self, perturb_noise: torch.Tensor | None = None, eps: float | None = None) -> None:
"""
Args:
perturb_noise (torch.Tensor, optional): The noise vector of shape
@@ -149,8 +149,8 @@ def forward(
operator: Callable,
x: torch.Tensor,
y_pseudo_gt: torch.Tensor,
- y_ref: Optional[torch.Tensor] = None,
- complex_input: Optional[bool] = False,
+ y_ref: torch.Tensor | None = None,
+ complex_input: bool | None = False,
) -> torch.Tensor:
"""
Args:
diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py
index 18a0f7f32f..e322852962 100644
--- a/monai/transforms/utility/array.py
+++ b/monai/transforms/utility/array.py
@@ -21,7 +21,7 @@
from collections.abc import Hashable, Mapping, Sequence
from copy import deepcopy
from functools import partial
-from typing import Any, Callable, Union
+from typing import Any, Callable
import numpy as np
import torch
@@ -1216,7 +1216,7 @@ def __init__(self, name: str, *args, **kwargs) -> None:
transform, _ = optional_import("torchio.transforms", "0.18.0", min_version, name=name)
self.trans = transform(*args, **kwargs)
- def __call__(self, img: Union[NdarrayOrTensor, Mapping[Hashable, NdarrayOrTensor]]):
+ def __call__(self, img: NdarrayOrTensor | Mapping[Hashable, NdarrayOrTensor]):
"""
Args:
img: an instance of torchio.Subject, torchio.Image, numpy.ndarray, torch.Tensor, SimpleITK.Image,
@@ -1248,7 +1248,7 @@ def __init__(self, name: str, *args, **kwargs) -> None:
transform, _ = optional_import("torchio.transforms", "0.18.0", min_version, name=name)
self.trans = transform(*args, **kwargs)
- def __call__(self, img: Union[NdarrayOrTensor, Mapping[Hashable, NdarrayOrTensor]]):
+ def __call__(self, img: NdarrayOrTensor | Mapping[Hashable, NdarrayOrTensor]):
"""
Args:
img: an instance of torchio.Subject, torchio.Image, numpy.ndarray, torch.Tensor, SimpleITK.Image,
diff --git a/tests/profile_subclass/profiling.py b/tests/profile_subclass/profiling.py
index 3e1d99e920..18aecea2fb 100644
--- a/tests/profile_subclass/profiling.py
+++ b/tests/profile_subclass/profiling.py
@@ -63,7 +63,8 @@ def main():
b_min, b_avg, b_med, b_std = bench(tensor_1, tensor_2)
print(
- f"Type {t.__name__} time (microseconds): min: {10**6 * b_min}, avg: {(10**6) * b_avg}, median: {(10**6) * b_med}, and std {(10**6) * b_std}."
+ f"Type {t.__name__} time (microseconds):"
+ f" min: {10**6 * b_min}, avg: {(10**6) * b_avg}, median: {(10**6) * b_med}, and std {(10**6) * b_std}."
)
From 12ec249d2df0d33e2ca97e8f09a01bacb1e2cbc6 Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Tue, 28 Oct 2025 20:41:42 +0100
Subject: [PATCH 07/61] Apply suggestions from code review
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Signed-off-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Signed-off-by: jirka
---
monai/losses/dice.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/monai/losses/dice.py b/monai/losses/dice.py
index 99baa34286..948749606b 100644
--- a/monai/losses/dice.py
+++ b/monai/losses/dice.py
@@ -494,7 +494,7 @@ def __init__(
raise ValueError(f"dist_matrix must be C x C, got {dist_matrix.shape[0]} x {dist_matrix.shape[1]}.")
if weighting_mode not in ["default", "GDL"]:
- raise ValueError(f"weighting_mode must be either 'default' or 'GDL, got {weighting_mode}.")
+ raise ValueError(f"weighting_mode must be either 'default' or 'GDL', got {weighting_mode}.")
self.m = dist_matrix
if isinstance(self.m, np.ndarray):
From 30c85d9c66d4ac275e4b1ec65ed64b9884ed7297 Mon Sep 17 00:00:00 2001
From: NabJa <32510324+NabJa@users.noreply.github.com>
Date: Fri, 31 Oct 2025 12:56:25 +0100
Subject: [PATCH 08/61] 8564 fourier positional encoding (#8570)
Fixes #8564 .
### Description
Add Fourier feature positional encodings to `PatchEmbeddingBlock`. It
has been shown, that Fourier feature positional encodings are better
suited for Anistropic images and videos:
https://arxiv.org/abs/2509.02488
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [x] New tests added to cover the changes.
- [x] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [x] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [x] In-line docstrings updated.
- [x] Documentation updated, tested `make html` command in the `docs/`
folder.
---------
Signed-off-by: NabJa
Co-authored-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
Signed-off-by: jirka
---
monai/networks/blocks/patchembedding.py | 22 ++++++--
monai/networks/blocks/pos_embed_utils.py | 55 +++++++++++++++++++-
tests/networks/blocks/test_patchembedding.py | 39 ++++++++++++++
3 files changed, 112 insertions(+), 4 deletions(-)
diff --git a/monai/networks/blocks/patchembedding.py b/monai/networks/blocks/patchembedding.py
index fca566591a..4e8a6a0463 100644
--- a/monai/networks/blocks/patchembedding.py
+++ b/monai/networks/blocks/patchembedding.py
@@ -12,6 +12,7 @@
from __future__ import annotations
from collections.abc import Sequence
+from typing import Optional
import numpy as np
import torch
@@ -19,14 +20,14 @@
import torch.nn.functional as F
from torch.nn import LayerNorm
-from monai.networks.blocks.pos_embed_utils import build_sincos_position_embedding
+from monai.networks.blocks.pos_embed_utils import build_fourier_position_embedding, build_sincos_position_embedding
from monai.networks.layers import Conv, trunc_normal_
from monai.utils import ensure_tuple_rep, optional_import
from monai.utils.module import look_up_option
Rearrange, _ = optional_import("einops.layers.torch", name="Rearrange")
SUPPORTED_PATCH_EMBEDDING_TYPES = {"conv", "perceptron"}
-SUPPORTED_POS_EMBEDDING_TYPES = {"none", "learnable", "sincos"}
+SUPPORTED_POS_EMBEDDING_TYPES = {"none", "learnable", "sincos", "fourier"}
class PatchEmbeddingBlock(nn.Module):
@@ -53,6 +54,7 @@ def __init__(
pos_embed_type: str = "learnable",
dropout_rate: float = 0.0,
spatial_dims: int = 3,
+ pos_embed_kwargs: Optional[dict] = None,
) -> None:
"""
Args:
@@ -65,6 +67,8 @@ def __init__(
pos_embed_type: position embedding layer type.
dropout_rate: fraction of the input units to drop.
spatial_dims: number of spatial dimensions.
+ pos_embed_kwargs: additional arguments for position embedding. For `sincos`, it can contain
+ `temperature` and for fourier it can contain `scales`.
"""
super().__init__()
@@ -105,6 +109,8 @@ def __init__(
self.position_embeddings = nn.Parameter(torch.zeros(1, self.n_patches, hidden_size))
self.dropout = nn.Dropout(dropout_rate)
+ pos_embed_kwargs = {} if pos_embed_kwargs is None else pos_embed_kwargs
+
if self.pos_embed_type == "none":
pass
elif self.pos_embed_type == "learnable":
@@ -114,7 +120,17 @@ def __init__(
for in_size, pa_size in zip(img_size, patch_size):
grid_size.append(in_size // pa_size)
- self.position_embeddings = build_sincos_position_embedding(grid_size, hidden_size, spatial_dims)
+ self.position_embeddings = build_sincos_position_embedding(
+ grid_size, hidden_size, spatial_dims, **pos_embed_kwargs
+ )
+ elif self.pos_embed_type == "fourier":
+ grid_size = []
+ for in_size, pa_size in zip(img_size, patch_size):
+ grid_size.append(in_size // pa_size)
+
+ self.position_embeddings = build_fourier_position_embedding(
+ grid_size, hidden_size, spatial_dims, **pos_embed_kwargs
+ )
else:
raise ValueError(f"pos_embed_type {self.pos_embed_type} not supported.")
diff --git a/monai/networks/blocks/pos_embed_utils.py b/monai/networks/blocks/pos_embed_utils.py
index a9c5176bc2..266be5e28c 100644
--- a/monai/networks/blocks/pos_embed_utils.py
+++ b/monai/networks/blocks/pos_embed_utils.py
@@ -18,7 +18,7 @@
import torch
import torch.nn as nn
-__all__ = ["build_sincos_position_embedding"]
+__all__ = ["build_fourier_position_embedding", "build_sincos_position_embedding"]
# From PyTorch internals
@@ -32,6 +32,59 @@ def parse(x):
return parse
+def build_fourier_position_embedding(
+ grid_size: Union[int, List[int]], embed_dim: int, spatial_dims: int = 3, scales: Union[float, List[float]] = 1.0
+) -> torch.nn.Parameter:
+ """
+ Builds a (Anistropic) Fourier feature position embedding based on the given grid size, embed dimension,
+ spatial dimensions, and scales. The scales control the variance of the Fourier features, higher values make distant
+ points more distinguishable.
+ Position embedding is made anistropic by allowing setting different scales for each spatial dimension.
+ Reference: https://arxiv.org/abs/2509.02488
+
+ Args:
+ grid_size (int | List[int]): The size of the grid in each spatial dimension.
+ embed_dim (int): The dimension of the embedding.
+ spatial_dims (int): The number of spatial dimensions (2 for 2D, 3 for 3D).
+ scales (float | List[float]): The scale for every spatial dimension. If a single float is provided,
+ the same scale is used for all dimensions.
+
+ Returns:
+ pos_embed (nn.Parameter): The Fourier feature position embedding as a fixed parameter.
+ """
+
+ to_tuple = _ntuple(spatial_dims)
+ grid_size_t = to_tuple(grid_size)
+ if len(grid_size_t) != spatial_dims:
+ raise ValueError(f"Length of grid_size ({len(grid_size_t)}) must be the same as spatial_dims.")
+
+ if embed_dim % 2 != 0:
+ raise ValueError("embed_dim must be even for Fourier position embedding")
+
+ # Ensure scales is a tensor of shape (spatial_dims,)
+ if isinstance(scales, float):
+ scales_tensor = torch.full((spatial_dims,), scales, dtype=torch.float)
+ elif isinstance(scales, (list, tuple)):
+ if len(scales) != spatial_dims:
+ raise ValueError(f"Length of scales {len(scales)} does not match spatial_dims {spatial_dims}")
+ scales_tensor = torch.tensor(scales, dtype=torch.float)
+ else:
+ raise TypeError(f"scales must be float or list of floats, got {type(scales)}")
+
+ gaussians = torch.randn(embed_dim // 2, spatial_dims, dtype=torch.float32) * scales_tensor
+
+ position_indices = [torch.linspace(0, 1, x, dtype=torch.float32) for x in grid_size_t]
+ positions = torch.stack(torch.meshgrid(*position_indices, indexing="ij"), dim=-1)
+ positions = positions.flatten(end_dim=-2)
+
+ x_proj = (2.0 * torch.pi * positions) @ gaussians.T
+
+ pos_emb = torch.cat([torch.sin(x_proj), torch.cos(x_proj)], dim=-1)
+ pos_emb = nn.Parameter(pos_emb[None, :, :], requires_grad=False)
+
+ return pos_emb
+
+
def build_sincos_position_embedding(
grid_size: Union[int, List[int]], embed_dim: int, spatial_dims: int = 3, temperature: float = 10000.0
) -> torch.nn.Parameter:
diff --git a/tests/networks/blocks/test_patchembedding.py b/tests/networks/blocks/test_patchembedding.py
index 2945482649..95eba14e6f 100644
--- a/tests/networks/blocks/test_patchembedding.py
+++ b/tests/networks/blocks/test_patchembedding.py
@@ -87,6 +87,19 @@ def test_sincos_pos_embed(self):
self.assertEqual(net.position_embeddings.requires_grad, False)
+ def test_fourier_pos_embed(self):
+ net = PatchEmbeddingBlock(
+ in_channels=1,
+ img_size=(32, 32, 32),
+ patch_size=(8, 8, 8),
+ hidden_size=96,
+ num_heads=8,
+ pos_embed_type="fourier",
+ dropout_rate=0.5,
+ )
+
+ self.assertEqual(net.position_embeddings.requires_grad, False)
+
def test_learnable_pos_embed(self):
net = PatchEmbeddingBlock(
in_channels=1,
@@ -101,6 +114,32 @@ def test_learnable_pos_embed(self):
self.assertEqual(net.position_embeddings.requires_grad, True)
def test_ill_arg(self):
+ with self.assertRaises(ValueError):
+ PatchEmbeddingBlock(
+ in_channels=1,
+ img_size=(128, 128, 128),
+ patch_size=(16, 16, 16),
+ hidden_size=128,
+ num_heads=12,
+ proj_type="conv",
+ dropout_rate=0.1,
+ pos_embed_type="fourier",
+ pos_embed_kwargs=dict(scales=[1.0, 1.0]),
+ )
+
+ with self.assertRaises(ValueError):
+ PatchEmbeddingBlock(
+ in_channels=1,
+ img_size=(128, 128),
+ patch_size=(16, 16),
+ hidden_size=128,
+ num_heads=12,
+ proj_type="conv",
+ dropout_rate=0.1,
+ pos_embed_type="fourier",
+ pos_embed_kwargs=dict(scales=[1.0, 1.0, 1.0]),
+ )
+
with self.assertRaises(ValueError):
PatchEmbeddingBlock(
in_channels=1,
From 5e66c7361dc4d5a877b3ede49457077316c006aa Mon Sep 17 00:00:00 2001
From: YunLiu <55491388+KumoLiu@users.noreply.github.com>
Date: Sat, 1 Nov 2025 00:17:32 +0800
Subject: [PATCH 09/61] Include more-itertools in build env (#8611)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fixes #8610.
### Description
setuptools’ import chain requires more-itertools, but it isn’t installed
when setup.py runs.
Edit pyproject.toml to include it under [build-system].
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Breaking change (fix or new feature that would cause existing
functionality to change).
- [ ] New tests added to cover the changes.
- [ ] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [ ] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [ ] In-line docstrings updated.
- [ ] Documentation updated, tested `make html` command in the `docs/`
folder.
---------
Signed-off-by: Yun Liu
Signed-off-by: jirka
---
.github/workflows/pythonapp-min.yml | 1 +
pyproject.toml | 1 +
2 files changed, 2 insertions(+)
diff --git a/.github/workflows/pythonapp-min.yml b/.github/workflows/pythonapp-min.yml
index 1b193b01de..55d681ea6d 100644
--- a/.github/workflows/pythonapp-min.yml
+++ b/.github/workflows/pythonapp-min.yml
@@ -136,6 +136,7 @@ jobs:
run: |
which python
python -m pip install --user --upgrade pip setuptools wheel
+ python -m pip install --user more-itertools>=8.0
- name: cache weekly timestamp
id: pip-cache
run: |
diff --git a/pyproject.toml b/pyproject.toml
index ed8c2a32dd..ef85068ad7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,6 +2,7 @@
requires = [
"wheel",
"setuptools",
+ "more-itertools>=8.0",
"torch>=2.4.1",
"ninja",
"packaging"
From 1e5a4921e1b253b9c5911c70b6b8ebb09a292582 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 1 Nov 2025 15:08:18 +0000
Subject: [PATCH 10/61] Bump peter-evans/create-or-update-comment from 4 to 5
(#8612)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps
[peter-evans/create-or-update-comment](https://github.com/peter-evans/create-or-update-comment)
from 4 to 5.
Release notes
Sourced from peter-evans/create-or-update-comment's
releases.
Create or Update Comment v5.0.0
⚙️ Requires Actions
Runner v2.327.1 or later if you are using a self-hosted runner for
Node 24 support.
What's Changed
... (truncated)
Commits
e8674b0
feat: v5 (#439)
fffe59e
build(deps-dev): bump @types/node from 18.19.127 to
18.19.129 (#438)
076d572
build(deps-dev): bump @types/node from 18.19.126 to
18.19.127 (#437)
86a2645
build(deps-dev): bump @vercel/ncc from 0.38.3 to 0.38.4
(#436)
be17e0c
build(deps-dev): bump @types/node from 18.19.124 to
18.19.126 (#435)
ef75eae
build(deps-dev): bump @types/node from 18.19.123 to
18.19.124 (#433)
82a7ad0
build(deps): bump actions/setup-node from 4 to 5 (#432)
f7c845d
build(deps-dev): bump @types/node from 18.19.122 to
18.19.123 (#430)
5da8e07
build(deps-dev): bump eslint-plugin-prettier from 5.5.3 to 5.5.4 (#428)
2de7f66
build(deps-dev): bump @types/node from 18.19.121 to
18.19.122 (#427)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
---------
Signed-off-by: dependabot[bot]
Signed-off-by: Yun Liu
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Yun Liu
Signed-off-by: jirka
---
.github/workflows/integration.yml | 4 ++--
.github/workflows/pythonapp-min.yml | 1 +
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index b63ae98943..597f59f767 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -74,7 +74,7 @@ jobs:
run: ./runtests.sh --build --net
- name: Add reaction
- uses: peter-evans/create-or-update-comment@v4
+ uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.PR_MAINTAIN }}
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
@@ -154,7 +154,7 @@ jobs:
python -m tests.test_integration_gpu_customization
- name: Add reaction
- uses: peter-evans/create-or-update-comment@v4
+ uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.PR_MAINTAIN }}
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
diff --git a/.github/workflows/pythonapp-min.yml b/.github/workflows/pythonapp-min.yml
index 55d681ea6d..34a328672a 100644
--- a/.github/workflows/pythonapp-min.yml
+++ b/.github/workflows/pythonapp-min.yml
@@ -88,6 +88,7 @@ jobs:
run: |
which python
python -m pip install --user --upgrade pip setuptools wheel
+ python -m pip install --user more-itertools>=8.0
- name: cache weekly timestamp
id: pip-cache
run: |
From 9e9b5d06e74aebd49a37126db3810deb4b6925cd Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 1 Nov 2025 16:52:16 +0000
Subject: [PATCH 11/61] Bump actions/download-artifact from 5 to 6 (#8614)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 5 to 6.
Release notes
Sourced from actions/download-artifact's
releases.
v6.0.0
What's Changed
BREAKING CHANGE: this update supports Node
v24.x. This is not a breaking change per-se but we're
treating it as such.
New Contributors
Full Changelog: https://github.com/actions/download-artifact/compare/v5...v6.0.0
Commits
018cc2c
Merge pull request #438
from actions/danwkennedy/prepare-6.0.0
815651c
Revert "Remove github.dep.yml"
bb3a066
Remove github.dep.yml
fa1ce46
Prepare v6.0.0
4a24838
Merge pull request #431
from danwkennedy/patch-1
5e3251c
Readme: spell out the first use of GHES
abefc31
Merge pull request #424
from actions/yacaovsnc/update_readme
ac43a60
Update README with artifact extraction details
de96f46
Merge pull request #417
from actions/yacaovsnc/update_readme
7993cb4
Remove migration guide for artifact download changes
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Signed-off-by: jirka
---
.github/workflows/docker.yml | 2 +-
.github/workflows/release.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 51e76756b2..c76c1e8af0 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -57,7 +57,7 @@ jobs:
with:
ref: dev
- name: Download version
- uses: actions/download-artifact@v5
+ uses: actions/download-artifact@v6
with:
name: _version.py
- name: docker_build
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b01a20336d..1d5e9a9bc3 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -127,7 +127,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Download version
- uses: actions/download-artifact@v5
+ uses: actions/download-artifact@v6
with:
name: _version.py
- name: Set tag
From d0b78af95c7cc2cd1d537f1528b35bd7878061a2 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 3 Nov 2025 04:19:10 +0000
Subject: [PATCH 12/61] Bump actions/upload-artifact from 4 to 5 (#8615)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 4 to 5.
Release notes
Sourced from actions/upload-artifact's
releases.
v5.0.0
What's Changed
BREAKING CHANGE: this update supports Node
v24.x. This is not a breaking change per-se but we're
treating it as such.
New Contributors
Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v5.0.0
v4.6.2
What's Changed
New Contributors
Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.6.2
v4.6.1
What's Changed
Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.6.1
v4.6.0
What's Changed
Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.6.0
v4.5.0
What's Changed
New Contributors
... (truncated)
Commits
330a01c
Merge pull request #734
from actions/danwkennedy/prepare-5.0.0
03f2824
Update github.dep.yml
905a1ec
Prepare v5.0.0
2d9f9cd
Merge pull request #725
from patrikpolyak/patch-1
9687587
Merge branch 'main' into patch-1
2848b2c
Merge pull request #727
from danwkennedy/patch-1
9b51177
Spell out the first use of GHES
cd231ca
Update GHES guidance to include reference to Node 20 version
de65e23
Merge pull request #712
from actions/nebuk89-patch-1
8747d8c
Update README.md
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Signed-off-by: jirka
---
.github/workflows/docker.yml | 2 +-
.github/workflows/release.yml | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index c76c1e8af0..4741dca858 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -37,7 +37,7 @@ jobs:
python setup.py build
cat build/lib/monai/_version.py
- name: Upload version
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: _version.py
path: build/lib/monai/_version.py
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1d5e9a9bc3..3a8e8ffdf6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -66,7 +66,7 @@ jobs:
- if: matrix.python-version == '3.9' && startsWith(github.ref, 'refs/tags/')
name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: dist
path: dist/
@@ -109,7 +109,7 @@ jobs:
python setup.py build
cat build/lib/monai/_version.py
- name: Upload version
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: _version.py
path: build/lib/monai/_version.py
From bdea238e3f41a768bc8fd6c5007f38dcc713386a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 3 Nov 2025 07:35:44 +0000
Subject: [PATCH 13/61] Bump github/codeql-action from 3 to 4 (#8616)
Bumps [github/codeql-action](https://github.com/github/codeql-action)
from 3 to 4.
Release notes
Sourced from github/codeql-action's
releases.
v3.31.2
CodeQL Action Changelog
See the releases
page for the relevant changes to the CodeQL CLI and language
packs.
3.31.2 - 30 Oct 2025
No user facing changes.
See the full CHANGELOG.md
for more information.
v3.31.1
CodeQL Action Changelog
See the releases
page for the relevant changes to the CodeQL CLI and language
packs.
3.31.1 - 30 Oct 2025
- The
add-snippets input has been removed from the
analyze action. This input has been deprecated since CodeQL
Action 3.26.4 in August 2024 when this removal was announced.
See the full CHANGELOG.md
for more information.
v3.31.0
CodeQL Action Changelog
See the releases
page for the relevant changes to the CodeQL CLI and language
packs.
3.31.0 - 24 Oct 2025
- Bump minimum CodeQL bundle version to 2.17.6. #3223
- When SARIF files are uploaded by the
analyze or
upload-sarif actions, the CodeQL Action automatically
performs post-processing steps to prepare the data for the upload.
Previously, these post-processing steps were only performed before an
upload took place. We are now changing this so that the post-processing
steps will always be performed, even when the SARIF files are not
uploaded. This does not change anything for the
upload-sarif action. For analyze, this may
affect Advanced Setup for CodeQL users who specify a value other than
always for the upload input. #3222
See the full CHANGELOG.md
for more information.
v3.30.9
CodeQL Action Changelog
See the releases
page for the relevant changes to the CodeQL CLI and language
packs.
3.30.9 - 17 Oct 2025
- Update default CodeQL bundle version to 2.23.3. #3205
- Experimental: A new
setup-codeql action has been added
which is similar to init, except it only installs the
CodeQL CLI and does not initialize a database. Do not use this in
production as it is part of an internal experiment and subject to change
at any time. #3204
See the full CHANGELOG.md
for more information.
v3.30.8
CodeQL Action Changelog
See the releases
page for the relevant changes to the CodeQL CLI and language
packs.
... (truncated)
Changelog
Sourced from github/codeql-action's
changelog.
4.31.2 - 30 Oct 2025
No user facing changes.
4.31.1 - 30 Oct 2025
- The
add-snippets input has been removed from the
analyze action. This input has been deprecated since CodeQL
Action 3.26.4 in August 2024 when this removal was announced.
4.31.0 - 24 Oct 2025
- Bump minimum CodeQL bundle version to 2.17.6. #3223
- When SARIF files are uploaded by the
analyze or
upload-sarif actions, the CodeQL Action automatically
performs post-processing steps to prepare the data for the upload.
Previously, these post-processing steps were only performed before an
upload took place. We are now changing this so that the post-processing
steps will always be performed, even when the SARIF files are not
uploaded. This does not change anything for the
upload-sarif action. For analyze, this may
affect Advanced Setup for CodeQL users who specify a value other than
always for the upload input. #3222
4.30.9 - 17 Oct 2025
- Update default CodeQL bundle version to 2.23.3. #3205
- Experimental: A new
setup-codeql action has been added
which is similar to init, except it only installs the
CodeQL CLI and does not initialize a database. Do not use this in
production as it is part of an internal experiment and subject to change
at any time. #3204
4.30.8 - 10 Oct 2025
No user facing changes.
4.30.7 - 06 Oct 2025
- [v4+ only] The CodeQL Action now runs on Node.js v24. #3169
3.30.6 - 02 Oct 2025
- Update default CodeQL bundle version to 2.23.2. #3168
3.30.5 - 26 Sep 2025
- We fixed a bug that was introduced in
3.30.4 with
upload-sarif which resulted in files without a
.sarif extension not getting uploaded. #3160
3.30.4 - 25 Sep 2025
- We have improved the CodeQL Action's ability to validate that the
workflow it is used in does not use different versions of the CodeQL
Action for different workflow steps. Mixing different versions of the
CodeQL Action in the same workflow is unsupported and can lead to
unpredictable results. A warning will now be emitted from the
codeql-action/init step if different versions of the CodeQL
Action are detected in the workflow file. Additionally, an error will
now be thrown by the other CodeQL Action steps if they load a
configuration file that was generated by a different version of the
codeql-action/init step. #3099
and #3100
- We added support for reducing the size of dependency caches for Java
analyses, which will reduce cache usage and speed up workflows. This
will be enabled automatically at a later time. #3107
- You can now run the latest CodeQL nightly bundle by passing
tools: nightly to the init action. In general,
the nightly bundle is unstable and we only recommend running it when
directed by GitHub staff. #3130
- Update default CodeQL bundle version to 2.23.1. #3118
3.30.3 - 10 Sep 2025
No user facing changes.
3.30.2 - 09 Sep 2025
- Fixed a bug which could cause language autodetection to fail. #3084
- Experimental: The
quality-queries input that was added
in 3.29.2 as part of an internal experiment is now
deprecated and will be removed in an upcoming version of the CodeQL
Action. It has been superseded by a new analysis-kinds
input, which is part of the same internal experiment. Do not use this in
production as it is subject to change at any time. #3064
... (truncated)
Commits
74c8748
Update analyze/action.yml
34c50c1
Merge pull request #3251
from github/mbg/user-error/enablement
4ae68af
Warn if the add-snippets input is used
52a7bd7
Check for 403 status
194ba0e
Make error message tests less brittle
53acf0b
Turn enablement errors into configuration errors
ac9aeee
Merge pull request #3249
from github/henrymercer/api-logging
d49e837
Merge branch 'main' into henrymercer/api-logging
3d988b2
Pass minimal copy of core
8cc18ac
Merge pull request #3250
from github/henrymercer/prefer-fs-delete
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Signed-off-by: jirka
---
.github/workflows/codeql-analysis.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 60b68e3c31..10ca752a5e 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -42,7 +42,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v3
+ uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -72,4 +72,4 @@ jobs:
BUILD_MONAI=1 ./runtests.sh --build
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3
+ uses: github/codeql-action/analyze@v4
From f6b72c2c4d830974ef6d7fbdcb31fb3d03a9f86c Mon Sep 17 00:00:00 2001
From: Lukas Folle <126877803+lukas-folle-snkeos@users.noreply.github.com>
Date: Mon, 3 Nov 2025 13:05:20 +0100
Subject: [PATCH 14/61] added ReduceTrait and FlattenSequence (#8531)
Fixes #8528.
### Description
This PR adds the `FlatttenSequence` transform (a flavor of the also
newly added `ReduceTrait`) which can flatten a nested data structure by
one level. This way, #8528 can be tackled without the need to change the
`apply_transform` of `Compose` significantly.
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Breaking change (fix or new feature that would cause existing
functionality to change).
- [x] New tests added to cover the changes.
- [x] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [x] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [x] In-line docstrings updated.
- [x] Documentation updated, tested `make html` command in the `docs/`
folder.
---------
Signed-off-by: Lukas Folle
Co-authored-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
Co-authored-by: YunLiu <55491388+KumoLiu@users.noreply.github.com>
Signed-off-by: jirka
---
docs/source/transforms.rst | 17 +++++++++++
monai/transforms/__init__.py | 6 +++-
monai/transforms/traits.py | 13 +++++++-
monai/transforms/transform.py | 7 ++---
monai/transforms/utility/array.py | 39 +++++++++++++++++++++++-
monai/transforms/utility/dictionary.py | 29 +++++++++++++++++-
tests/transforms/compose/test_compose.py | 34 +++++++++++++++++++++
7 files changed, 137 insertions(+), 8 deletions(-)
diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst
index d2585daf63..2d5d452dc0 100644
--- a/docs/source/transforms.rst
+++ b/docs/source/transforms.rst
@@ -37,6 +37,11 @@ Generic Interfaces
.. autoclass:: MultiSampleTrait
:members:
+`ReduceTrait`
+^^^^^^^^^^^^^^^^^^
+.. autoclass:: ReduceTrait
+ :members:
+
`Randomizable`
^^^^^^^^^^^^^^
.. autoclass:: Randomizable
@@ -1252,6 +1257,12 @@ Utility
:members:
:special-members: __call__
+`FlattenSequence`
+""""""""""""""""""""""""
+.. autoclass:: FlattenSequence
+ :members:
+ :special-members: __call__
+
Dictionary Transforms
---------------------
@@ -2337,6 +2348,12 @@ Utility (Dict)
:members:
:special-members: __call__
+`FlattenSequenced`
+"""""""""""""""""""""""""
+.. autoclass:: FlattenSequenced
+ :members:
+ :special-members: __call__
+
MetaTensor
^^^^^^^^^^
diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py
index d15042181b..0ab9fe63d5 100644
--- a/monai/transforms/__init__.py
+++ b/monai/transforms/__init__.py
@@ -506,7 +506,7 @@
ZoomDict,
)
from .spatial.functional import spatial_resample
-from .traits import LazyTrait, MultiSampleTrait, RandomizableTrait, ThreadUnsafe
+from .traits import LazyTrait, MultiSampleTrait, RandomizableTrait, ReduceTrait, ThreadUnsafe
from .transform import LazyTransform, MapTransform, Randomizable, RandomizableTransform, Transform, apply_transform
from .utility.array import (
AddCoordinateChannels,
@@ -521,6 +521,7 @@
EnsureChannelFirst,
EnsureType,
FgBgToIndices,
+ FlattenSequence,
Identity,
ImageFilter,
IntensityStats,
@@ -593,6 +594,9 @@
FgBgToIndicesd,
FgBgToIndicesD,
FgBgToIndicesDict,
+ FlattenSequenced,
+ FlattenSequenceD,
+ FlattenSequenceDict,
FlattenSubKeysd,
FlattenSubKeysD,
FlattenSubKeysDict,
diff --git a/monai/transforms/traits.py b/monai/transforms/traits.py
index 016effc59d..45d081f2e6 100644
--- a/monai/transforms/traits.py
+++ b/monai/transforms/traits.py
@@ -14,7 +14,7 @@
from __future__ import annotations
-__all__ = ["LazyTrait", "InvertibleTrait", "RandomizableTrait", "MultiSampleTrait", "ThreadUnsafe"]
+__all__ = ["LazyTrait", "InvertibleTrait", "RandomizableTrait", "MultiSampleTrait", "ThreadUnsafe", "ReduceTrait"]
from typing import Any
@@ -99,3 +99,14 @@ class ThreadUnsafe:
"""
pass
+
+
+class ReduceTrait:
+ """
+ An interface to indicate that the transform has the capability to reduce multiple samples
+ into a single sample.
+ This interface can be extended from by people adapting transforms to the MONAI framework as well
+ as by implementors of MONAI transforms.
+ """
+
+ pass
diff --git a/monai/transforms/transform.py b/monai/transforms/transform.py
index 1a365b8d8e..1eedc7c333 100644
--- a/monai/transforms/transform.py
+++ b/monai/transforms/transform.py
@@ -25,7 +25,7 @@
from monai import config, transforms
from monai.config import KeysCollection
from monai.data.meta_tensor import MetaTensor
-from monai.transforms.traits import LazyTrait, RandomizableTrait, ThreadUnsafe
+from monai.transforms.traits import LazyTrait, RandomizableTrait, ReduceTrait, ThreadUnsafe
from monai.utils import MAX_SEED, ensure_tuple, first
from monai.utils.enums import TransformBackends
from monai.utils.misc import MONAIEnvVars
@@ -142,7 +142,7 @@ def apply_transform(
"""
try:
map_items_ = int(map_items) if isinstance(map_items, bool) else map_items
- if isinstance(data, (list, tuple)) and map_items_ > 0:
+ if isinstance(data, (list, tuple)) and map_items_ > 0 and not isinstance(transform, ReduceTrait):
return [
apply_transform(transform, item, map_items_ - 1, unpack_items, log_stats, lazy, overrides)
for item in data
@@ -482,8 +482,7 @@ def key_iterator(self, data: Mapping[Hashable, Any], *extra_iterables: Iterable
yield (key,) + tuple(_ex_iters) if extra_iterables else key
elif not self.allow_missing_keys:
raise KeyError(
- f"Key `{key}` of transform `{self.__class__.__name__}` was missing in the data"
- " and allow_missing_keys==False."
+ f"Key `{key}` of transform `{self.__class__.__name__}` was missing in the data and allow_missing_keys==False."
)
def first_key(self, data: dict[Hashable, Any]):
diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py
index e322852962..3dc7897feb 100644
--- a/monai/transforms/utility/array.py
+++ b/monai/transforms/utility/array.py
@@ -43,7 +43,7 @@
median_filter,
)
from monai.transforms.inverse import InvertibleTransform, TraceableTransform
-from monai.transforms.traits import MultiSampleTrait
+from monai.transforms.traits import MultiSampleTrait, ReduceTrait
from monai.transforms.transform import Randomizable, RandomizableTrait, RandomizableTransform, Transform
from monai.transforms.utils import (
apply_affine_to_points,
@@ -110,6 +110,7 @@
"ImageFilter",
"RandImageFilter",
"ApplyTransformToPoints",
+ "FlattenSequence",
]
@@ -1950,3 +1951,39 @@ def inverse(self, data: torch.Tensor) -> torch.Tensor:
data = inverse_transform(data, transform[TraceKeys.EXTRA_INFO]["image_affine"])
return data
+
+
+class FlattenSequence(Transform, ReduceTrait):
+ """
+ Flatten a nested sequence (list or tuple) by one level.
+ If the input is a sequence of sequences, it will flatten them into a single sequence.
+ Non-nested sequences and other data types are returned unchanged.
+
+ For example:
+
+ .. code-block:: python
+
+ flatten = FlattenSequence()
+ data = [[1, 2], [3, 4], [5, 6]]
+ print(flatten(data))
+ [1, 2, 3, 4, 5, 6]
+
+ """
+
+ def __init__(self):
+ super().__init__()
+
+ def __call__(self, data: list | tuple | Any) -> list | tuple | Any:
+ """
+ Flatten a nested sequence by one level.
+ Args:
+ data: Input data, can be a nested sequence.
+ Returns:
+ Flattened list if input is a nested sequence, otherwise returns data unchanged.
+ """
+ if isinstance(data, (list, tuple)):
+ if len(data) == 0:
+ return data
+ if all(isinstance(item, (list, tuple)) for item in data):
+ return [item for sublist in data for item in sublist]
+ return data
diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py
index 7dd2397a74..95c59e07bc 100644
--- a/monai/transforms/utility/dictionary.py
+++ b/monai/transforms/utility/dictionary.py
@@ -30,7 +30,7 @@
from monai.data.meta_tensor import MetaObj, MetaTensor
from monai.data.utils import no_collation
from monai.transforms.inverse import InvertibleTransform
-from monai.transforms.traits import MultiSampleTrait, RandomizableTrait
+from monai.transforms.traits import MultiSampleTrait, RandomizableTrait, ReduceTrait
from monai.transforms.transform import MapTransform, Randomizable, RandomizableTransform
from monai.transforms.utility.array import (
AddCoordinateChannels,
@@ -45,6 +45,7 @@
EnsureChannelFirst,
EnsureType,
FgBgToIndices,
+ FlattenSequence,
Identity,
ImageFilter,
IntensityStats,
@@ -191,6 +192,9 @@
"ApplyTransformToPointsd",
"ApplyTransformToPointsD",
"ApplyTransformToPointsDict",
+ "FlattenSequenced",
+ "FlattenSequenceD",
+ "FlattenSequenceDict",
]
DEFAULT_POST_FIX = PostFix.meta()
@@ -1906,6 +1910,28 @@ def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> dict[Hashable, torch
return d
+class FlattenSequenced(MapTransform, ReduceTrait):
+ """
+ Dictionary-based wrapper of :py:class:`monai.transforms.FlattenSequence`.
+
+ Args:
+ keys: keys of the corresponding items to be transformed.
+ See also: monai.transforms.MapTransform
+ allow_missing_keys:
+ Don't raise exception if key is missing.
+ """
+
+ def __init__(self, keys: KeysCollection, allow_missing_keys: bool = False, **kwargs) -> None:
+ super().__init__(keys, allow_missing_keys)
+ self.flatten_sequence = FlattenSequence(**kwargs)
+
+ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, NdarrayOrTensor]:
+ d = dict(data)
+ for key in self.key_iterator(d):
+ d[key] = self.flatten_sequence(d[key]) # type: ignore[assignment]
+ return d
+
+
RandImageFilterD = RandImageFilterDict = RandImageFilterd
ImageFilterD = ImageFilterDict = ImageFilterd
IdentityD = IdentityDict = Identityd
@@ -1949,3 +1975,4 @@ def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> dict[Hashable, torch
AddCoordinateChannelsD = AddCoordinateChannelsDict = AddCoordinateChannelsd
FlattenSubKeysD = FlattenSubKeysDict = FlattenSubKeysd
ApplyTransformToPointsD = ApplyTransformToPointsDict = ApplyTransformToPointsd
+FlattenSequenceD = FlattenSequenceDict = FlattenSequenced
diff --git a/tests/transforms/compose/test_compose.py b/tests/transforms/compose/test_compose.py
index e6727c976f..12547f9ec2 100644
--- a/tests/transforms/compose/test_compose.py
+++ b/tests/transforms/compose/test_compose.py
@@ -282,6 +282,40 @@ def test_flatten_and_len(self):
def test_backwards_compatible_imports(self):
from monai.transforms.transform import MapTransform, RandomizableTransform, Transform # noqa: F401
+ def test_list_extend_multi_sample_trait(self):
+ center_crop = mt.CenterSpatialCrop([128, 128])
+ multi_sample_transform = mt.RandSpatialCropSamples([64, 64], 1)
+ flatten_sequence_transform = mt.FlattenSequence()
+
+ img = torch.zeros([1, 512, 512])
+
+ self.assertEqual(execute_compose(img, [center_crop]).shape, torch.Size([1, 128, 128]))
+ single_multi_sample_trait_result = execute_compose(
+ img, [multi_sample_transform, center_crop, flatten_sequence_transform]
+ )
+ self.assertIsInstance(single_multi_sample_trait_result, list)
+ self.assertEqual(len(single_multi_sample_trait_result), 1)
+ self.assertEqual(single_multi_sample_trait_result[0].shape, torch.Size([1, 64, 64]))
+
+ double_multi_sample_trait_result = execute_compose(
+ img, [multi_sample_transform, multi_sample_transform, flatten_sequence_transform, center_crop]
+ )
+ self.assertIsInstance(double_multi_sample_trait_result, list)
+ self.assertEqual(len(double_multi_sample_trait_result), 1)
+ self.assertEqual(double_multi_sample_trait_result[0].shape, torch.Size([1, 64, 64]))
+
+ def test_multi_sample_trait_cardinality(self):
+ img = torch.zeros([1, 128, 128])
+ t2 = mt.RandSpatialCropSamples([32, 32], num_samples=2)
+ flatten_sequence_transform = mt.FlattenSequence()
+
+ # chaining should multiply counts: 2 x 2 = 4, flattened
+ res = execute_compose(img, [t2, t2, flatten_sequence_transform])
+ self.assertIsInstance(res, list)
+ self.assertEqual(len(res), 4)
+ for r in res:
+ self.assertEqual(r.shape, torch.Size([1, 32, 32]))
+
TEST_COMPOSE_EXECUTE_TEST_CASES = [
[None, tuple()],
From 0f5a188f75dedef2a1eb997958b304d5a353d924 Mon Sep 17 00:00:00 2001
From: reworld223 <99582363+reworld223@users.noreply.github.com>
Date: Mon, 3 Nov 2025 22:48:12 +0800
Subject: [PATCH 15/61] Fix box_iou returning 0 for floating-point results less
than 1. #8369 (#8553)
Fixes # 8369
### Description
Fixes an issue where the result of `box_iou` was 0 when the first
argument was an integer, as described in #8369.
IOU values range from 0 to 1. If the first argument is an integer, the
function could return an integer less than 1, resulting in a return
value of 0.
This pull request changes the return type of `box_iou` to `float32`, or
to match the float data type of the first argument.
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [ ] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [x] In-line docstrings updated.
---------
Signed-off-by: reworld223
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: YunLiu <55491388+KumoLiu@users.noreply.github.com>
Signed-off-by: jirka
---
monai/data/box_utils.py | 31 +++++++++++++++++-----
tests/data/test_box_utils.py | 51 ++++++++++++++++++++++++++++++++++++
2 files changed, 75 insertions(+), 7 deletions(-)
diff --git a/monai/data/box_utils.py b/monai/data/box_utils.py
index a982b01427..b09b86b605 100644
--- a/monai/data/box_utils.py
+++ b/monai/data/box_utils.py
@@ -826,7 +826,10 @@ def box_iou(boxes1: NdarrayOrTensor, boxes2: NdarrayOrTensor) -> NdarrayOrTensor
boxes2: bounding boxes, Mx4 or Mx6 torch tensor or ndarray. The box mode is assumed to be ``StandardMode``
Returns:
- IoU, with size of (N,M) and same data type as ``boxes1``
+ An array/tensor matching the container type of ``boxes1`` (NumPy ndarray or Torch tensor), always
+ floating-point with size ``(N, M)``:
+ - if ``boxes1`` has a floating-point dtype, the same dtype is used.
+ - if ``boxes1`` has an integer dtype, the result is returned as ``torch.float32``.
"""
@@ -842,8 +845,10 @@ def box_iou(boxes1: NdarrayOrTensor, boxes2: NdarrayOrTensor) -> NdarrayOrTensor
inter, union = _box_inter_union(boxes1_t, boxes2_t, compute_dtype=COMPUTE_DTYPE)
- # compute IoU and convert back to original box_dtype
+ # compute IoU and convert back to original box_dtype or torch.float32
iou_t = inter / (union + torch.finfo(COMPUTE_DTYPE).eps) # (N,M)
+ if not box_dtype.is_floating_point:
+ box_dtype = COMPUTE_DTYPE
iou_t = iou_t.to(dtype=box_dtype)
# check if NaN or Inf
@@ -851,7 +856,7 @@ def box_iou(boxes1: NdarrayOrTensor, boxes2: NdarrayOrTensor) -> NdarrayOrTensor
raise ValueError("Box IoU is NaN or Inf.")
# convert tensor back to numpy if needed
- iou, *_ = convert_to_dst_type(src=iou_t, dst=boxes1)
+ iou, *_ = convert_to_dst_type(src=iou_t, dst=boxes1, dtype=box_dtype)
return iou
@@ -867,7 +872,10 @@ def box_giou(boxes1: NdarrayOrTensor, boxes2: NdarrayOrTensor) -> NdarrayOrTenso
boxes2: bounding boxes, Mx4 or Mx6 torch tensor or ndarray. The box mode is assumed to be ``StandardMode``
Returns:
- GIoU, with size of (N,M) and same data type as ``boxes1``
+ An array/tensor matching the container type of ``boxes1`` (NumPy ndarray or Torch tensor), always
+ floating-point with size ``(N, M)``:
+ - if ``boxes1`` has a floating-point dtype, the same dtype is used.
+ - if ``boxes1`` has an integer dtype, the result is returned as ``torch.float32``.
Reference:
https://giou.stanford.edu/GIoU.pdf
@@ -904,12 +912,15 @@ def box_giou(boxes1: NdarrayOrTensor, boxes2: NdarrayOrTensor) -> NdarrayOrTenso
# GIoU
giou_t = iou - (enclosure - union) / (enclosure + torch.finfo(COMPUTE_DTYPE).eps)
+ if not box_dtype.is_floating_point:
+ box_dtype = COMPUTE_DTYPE
giou_t = giou_t.to(dtype=box_dtype)
+
if torch.isnan(giou_t).any() or torch.isinf(giou_t).any():
raise ValueError("Box GIoU is NaN or Inf.")
# convert tensor back to numpy if needed
- giou, *_ = convert_to_dst_type(src=giou_t, dst=boxes1)
+ giou, *_ = convert_to_dst_type(src=giou_t, dst=boxes1, dtype=box_dtype)
return giou
@@ -925,7 +936,10 @@ def box_pair_giou(boxes1: NdarrayOrTensor, boxes2: NdarrayOrTensor) -> NdarrayOr
boxes2: bounding boxes, same shape with boxes1. The box mode is assumed to be ``StandardMode``
Returns:
- paired GIoU, with size of (N,) and same data type as ``boxes1``
+ An array/tensor matching the container type of ``boxes1`` (NumPy ndarray or Torch tensor), always
+ floating-point with size ``(N, )``:
+ - if ``boxes1`` has a floating-point dtype, the same dtype is used.
+ - if ``boxes1`` has an integer dtype, the result is returned as ``torch.float32``.
Reference:
https://giou.stanford.edu/GIoU.pdf
@@ -982,12 +996,15 @@ def box_pair_giou(boxes1: NdarrayOrTensor, boxes2: NdarrayOrTensor) -> NdarrayOr
enclosure = torch.prod(wh, dim=-1, keepdim=False) # (N,)
giou_t: torch.Tensor = iou - (enclosure - union) / (enclosure + torch.finfo(COMPUTE_DTYPE).eps) # type: ignore
+ if not box_dtype.is_floating_point:
+ box_dtype = COMPUTE_DTYPE
giou_t = giou_t.to(dtype=box_dtype) # (N,spatial_dims)
+
if torch.isnan(giou_t).any() or torch.isinf(giou_t).any():
raise ValueError("Box GIoU is NaN or Inf.")
# convert tensor back to numpy if needed
- giou, *_ = convert_to_dst_type(src=giou_t, dst=boxes1)
+ giou, *_ = convert_to_dst_type(src=giou_t, dst=boxes1, dtype=box_dtype)
return giou
diff --git a/tests/data/test_box_utils.py b/tests/data/test_box_utils.py
index 390fd901fd..05778f691b 100644
--- a/tests/data/test_box_utils.py
+++ b/tests/data/test_box_utils.py
@@ -14,6 +14,7 @@
import unittest
import numpy as np
+import torch
from parameterized import parameterized
from monai.data.box_utils import (
@@ -218,5 +219,55 @@ def test_value(self, input_data, mode2, expected_box, expected_area):
assert_allclose(nms_box, [1], type_test=False)
+class TestBoxUtilsDtype(unittest.TestCase):
+ @parameterized.expand(
+ [
+ # numpy dtypes
+ (np.array([[1, 1, 1, 2, 2, 2]], dtype=np.int32), np.array([[1, 1, 1, 2, 2, 2]], dtype=np.int32)),
+ (np.array([[1, 1, 1, 2, 2, 2]], dtype=np.float32), np.array([[1, 1, 1, 2, 2, 2]], dtype=np.float32)),
+ # torch dtypes
+ (
+ torch.tensor([[1, 1, 1, 2, 2, 2]], dtype=torch.int64),
+ torch.tensor([[1, 1, 1, 2, 2, 2]], dtype=torch.int64),
+ ),
+ (
+ torch.tensor([[1, 1, 1, 2, 2, 2]], dtype=torch.float32),
+ torch.tensor([[1, 1, 1, 2, 2, 2]], dtype=torch.float32),
+ ),
+ # mixed numpy (int + float)
+ (np.array([[1, 1, 1, 2, 2, 2]], dtype=np.int32), np.array([[1, 1, 1, 2, 2, 2]], dtype=np.float32)),
+ # mixed torch (int + float)
+ (
+ torch.tensor([[1, 1, 1, 2, 2, 2]], dtype=torch.int64),
+ torch.tensor([[1, 1, 1, 2, 2, 2]], dtype=torch.float32),
+ ),
+ ]
+ )
+ def test_dtype_behavior(self, boxes1, boxes2):
+ funcs = [box_iou, box_giou, box_pair_giou]
+ for func in funcs:
+ result = func(boxes1, boxes2)
+
+ if isinstance(result, np.ndarray):
+ self.assertTrue(
+ np.issubdtype(result.dtype, np.floating), f"{func.__name__} expected float, got {result.dtype}"
+ )
+ elif torch.is_tensor(result):
+ self.assertTrue(
+ torch.is_floating_point(result), f"{func.__name__} expected float tensor, got {result.dtype}"
+ )
+ else:
+ self.fail(f"Unexpected return type {type(result)}")
+
+ def test_integer_truncation_bug(self):
+ # Verify fix for #8553: IoU < 1.0 with integer inputs should not truncate to 0
+ boxes1 = np.array([[0, 0, 0, 2, 2, 2]], dtype=np.int32)
+ boxes2 = np.array([[1, 1, 1, 3, 3, 3]], dtype=np.int32)
+
+ iou = box_iou(boxes1, boxes2)
+ self.assertTrue(np.issubdtype(iou.dtype, np.floating))
+ self.assertGreater(iou[0, 0], 0.0, "IoU should not be truncated to 0")
+
+
if __name__ == "__main__":
unittest.main()
From b3ccf8d1615892d271fe05ba4dc7f2cc6fd35f88 Mon Sep 17 00:00:00 2001
From: Rafael Garcia-Dias
Date: Thu, 6 Nov 2025 02:18:55 +0000
Subject: [PATCH 16/61] 8620 ModuleNotFoundError: No module named
\'onnxscript\' in test-py3x (3.11) pipeline (#8621)
Fixes #8620
### Description
On October 8th, there was a new release of the ONNX library
([1.19.1](https://github.com/onnx/onnx/tree/v1.19.1)).
Looking into previous versions of our CICD, which run successfully, the
errors coincide with that date. For example, [this
one](https://github.com/Project-MONAI/MONAI/actions/runs/18153916782/job/51669624135#logs)
was ok with onnx 1.19.0.
[This issue](https://github.com/onnx/onnx/issues/7257) in the ONNX
project suggests that there were some recent breaking changes. This is
not exactly our issue, but it suggests that there may be something
similar going on.
I have added the condition `<1.19.1; python == 3.11` to prevent updates
on this Python version, since our original issue referred to Python
3.11.
It may be that this affects other versions. Let's see if the pipelines
execute successfully.
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Breaking change (fix or new feature that would cause existing
functionality to change).
- [ ] New tests added to cover the changes.
- [ ] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [x] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [ ] In-line docstrings updated.
- [ ] Documentation updated, tested `make html` command in the `docs/`
folder.
---------
Signed-off-by: R. Garcia-Dias
Signed-off-by: jirka
---
README.md | 2 +-
requirements-dev.txt | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 3dbcfa0ae5..e4bcdbb815 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
**M**edical **O**pen **N**etwork for **AI**
diff --git a/requirements-dev.txt b/requirements-dev.txt
index fda405eff2..ff234c856e 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -51,7 +51,7 @@ h5py
nni==2.10.1; platform_system == "Linux" and "arm" not in platform_machine and "aarch" not in platform_machine
optuna
git+https://github.com/Project-MONAI/MetricsReloaded@monai-support#egg=MetricsReloaded
-onnx>=1.13.0
+onnx>=1.13.0, <1.19.1
onnxruntime; python_version <= '3.10'
typeguard<3 # https://github.com/microsoft/nni/issues/5457
filelock<3.12.0 # https://github.com/microsoft/nni/issues/5523
From af4580bb6917cb0507f8c2d5df887558626be3fc Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Fri, 7 Nov 2025 10:06:51 +0100
Subject: [PATCH 17/61] Update monai/losses/perceptual.py
Signed-off-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Signed-off-by: jirka
---
monai/losses/perceptual.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/monai/losses/perceptual.py b/monai/losses/perceptual.py
index b818d497c1..7774c39f10 100644
--- a/monai/losses/perceptual.py
+++ b/monai/losses/perceptual.py
@@ -95,7 +95,7 @@ def __init__(
if network_type.lower() not in list(PercetualNetworkType):
raise ValueError(
- "Unrecognised criterion entered for Adversarial Loss. Must be one in: {}".format(", ".join(PercetualNetworkType))
+ "Unrecognised criterion entered for Perceptual Loss. Must be one in: {}".format(", ".join(PercetualNetworkType))
)
if cache_dir:
From 502c4028c302d86fb3e8ffe8490b85689db51e06 Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Fri, 7 Nov 2025 10:13:14 +0100
Subject: [PATCH 18/61] Update monai/losses/sure_loss.py
Signed-off-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Signed-off-by: jirka
---
monai/losses/sure_loss.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/monai/losses/sure_loss.py b/monai/losses/sure_loss.py
index 5d0e1c28c5..39ef5b234a 100644
--- a/monai/losses/sure_loss.py
+++ b/monai/losses/sure_loss.py
@@ -43,7 +43,7 @@ def sure_loss_function(
x: torch.Tensor,
y_pseudo_gt: torch.Tensor,
y_ref: torch.Tensor | None = None,
- eps: float | None = -1.0,
+ eps: float = -1.0,
perturb_noise: torch.Tensor | None = None,
complex_input: bool | None = False,
) -> torch.Tensor:
From dbac655e5c750e3e0889b42e3b2aec3db6e1ab5b Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Fri, 7 Nov 2025 10:13:26 +0100
Subject: [PATCH 19/61] Update monai/losses/sure_loss.py
Signed-off-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Signed-off-by: jirka
---
monai/losses/sure_loss.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/monai/losses/sure_loss.py b/monai/losses/sure_loss.py
index 39ef5b234a..a06b06dbda 100644
--- a/monai/losses/sure_loss.py
+++ b/monai/losses/sure_loss.py
@@ -45,7 +45,7 @@ def sure_loss_function(
y_ref: torch.Tensor | None = None,
eps: float = -1.0,
perturb_noise: torch.Tensor | None = None,
- complex_input: bool | None = False,
+ complex_input: bool = False,
) -> torch.Tensor:
"""
Args:
From 22bcd9b4edc174138c8f0986aeff6c2c4b159692 Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Fri, 7 Nov 2025 10:13:54 +0100
Subject: [PATCH 20/61] Update monai/losses/sure_loss.py
Signed-off-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Signed-off-by: jirka
---
monai/losses/sure_loss.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/monai/losses/sure_loss.py b/monai/losses/sure_loss.py
index a06b06dbda..30aed2a4dc 100644
--- a/monai/losses/sure_loss.py
+++ b/monai/losses/sure_loss.py
@@ -150,7 +150,7 @@ def forward(
x: torch.Tensor,
y_pseudo_gt: torch.Tensor,
y_ref: torch.Tensor | None = None,
- complex_input: bool | None = False,
+ complex_input: bool = False,
) -> torch.Tensor:
"""
Args:
From 0334fc05c18d487cde70a50f8e1ad3903a3e4731 Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Fri, 7 Nov 2025 10:15:33 +0100
Subject: [PATCH 21/61] Update monai/losses/adversarial_loss.py
Signed-off-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Signed-off-by: jirka
---
monai/losses/adversarial_loss.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/monai/losses/adversarial_loss.py b/monai/losses/adversarial_loss.py
index 3f7dc80098..b2c27a41ee 100644
--- a/monai/losses/adversarial_loss.py
+++ b/monai/losses/adversarial_loss.py
@@ -57,7 +57,7 @@ def __init__(
if criterion.lower() not in list(AdversarialCriterions):
raise ValueError(
- "Unrecognised criterion entered for Adversarial Loss. Must be one in: {}".format(", ".join(AdversarialCriterions))
+ f"Unrecognised criterion entered for Adversarial Loss. Must be one in: {', '.join(AdversarialCriterions)}"
)
# Depending on the criterion, a different activation layer is used.
From 83aa7d53b7d4426507c4cfe359f41e285cb19e89 Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Sun, 9 Nov 2025 10:11:42 +0100
Subject: [PATCH 22/61] Apply suggestions from code review
Signed-off-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Signed-off-by: jirka
---
.pre-commit-config.yaml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 86c01b09dc..271980fd7a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -35,9 +35,9 @@ repos:
(?x)(
^versioneer.py|
^monai/_version.py|
- ^monai/networks/| # todo: avoid typing rewrites
- ^monai/apps/detection/utils/anchor_utils.py| # todo: avoid typing rewrites
- ^tests/test_compute_panoptic_quality.py # todo: avoid typing rewrites
+ ^monai/networks/| # avoid typing rewrites
+ ^monai/apps/detection/utils/anchor_utils.py| # avoid typing rewrites
+ ^tests/test_compute_panoptic_quality.py # avoid typing rewrites
)
- repo: https://github.com/asottile/yesqa
From 9a92dd3f37f4a6414734c3da0fee8767e79b3917 Mon Sep 17 00:00:00 2001
From: jirka
Date: Sun, 9 Nov 2025 10:15:49 +0100
Subject: [PATCH 23/61] --unsafe-fixes
Signed-off-by: jirka
---
.pre-commit-config.yaml | 5 +----
monai/apps/detection/utils/anchor_utils.py | 6 +++---
monai/networks/blocks/attention_utils.py | 3 +--
monai/networks/blocks/crossattention.py | 9 ++++-----
monai/networks/blocks/denseblock.py | 2 +-
monai/networks/blocks/mlp.py | 5 ++---
monai/networks/blocks/patchembedding.py | 3 +--
monai/networks/blocks/pos_embed_utils.py | 5 ++---
monai/networks/blocks/rel_pos_embedding.py | 4 ++--
monai/networks/blocks/selfattention.py | 15 +++++++--------
monai/networks/blocks/spatialattention.py | 3 +--
monai/networks/blocks/transformerblock.py | 3 +--
monai/networks/layers/simplelayers.py | 2 +-
monai/networks/layers/utils.py | 3 +--
monai/networks/layers/vector_quantizer.py | 8 ++++----
monai/networks/nets/ahnet.py | 3 +--
monai/networks/nets/autoencoderkl.py | 5 ++---
monai/networks/nets/basic_unet.py | 3 +--
monai/networks/nets/dints.py | 12 +++++-------
monai/networks/nets/dynunet.py | 17 +++++++++--------
monai/networks/nets/netadapter.py | 4 ++--
monai/networks/nets/quicknat.py | 10 +++++-----
monai/networks/nets/resnet.py | 2 +-
monai/networks/nets/segresnet_ds.py | 7 +++----
monai/networks/nets/senet.py | 2 +-
monai/networks/nets/spade_network.py | 6 +++---
monai/networks/nets/swin_unetr.py | 2 +-
monai/networks/nets/vista3d.py | 13 +++++++------
monai/networks/nets/vqvae.py | 9 ++++-----
monai/networks/schedulers/rectified_flow.py | 3 +--
monai/networks/trt_compiler.py | 16 ++++++++--------
monai/networks/utils.py | 3 ++-
32 files changed, 88 insertions(+), 105 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 271980fd7a..db2d0f7534 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -34,10 +34,7 @@ repos:
exclude: |
(?x)(
^versioneer.py|
- ^monai/_version.py|
- ^monai/networks/| # avoid typing rewrites
- ^monai/apps/detection/utils/anchor_utils.py| # avoid typing rewrites
- ^tests/test_compute_panoptic_quality.py # avoid typing rewrites
+ ^monai/_version.py
)
- repo: https://github.com/asottile/yesqa
diff --git a/monai/apps/detection/utils/anchor_utils.py b/monai/apps/detection/utils/anchor_utils.py
index cbde3ebae9..b750fe0de8 100644
--- a/monai/apps/detection/utils/anchor_utils.py
+++ b/monai/apps/detection/utils/anchor_utils.py
@@ -39,7 +39,7 @@
from __future__ import annotations
-from typing import List, Sequence
+from collections.abc import Sequence
import torch
from torch import Tensor, nn
@@ -106,7 +106,7 @@ class AnchorGenerator(nn.Module):
anchor_generator = AnchorGenerator(sizes, aspect_ratios)
"""
- __annotations__ = {"cell_anchors": List[torch.Tensor]}
+ __annotations__ = {"cell_anchors": list[torch.Tensor]}
def __init__(
self,
@@ -364,7 +364,7 @@ class AnchorGeneratorWithAnchorShape(AnchorGenerator):
anchor_generator = AnchorGeneratorWithAnchorShape(feature_map_scales, base_anchor_shapes)
"""
- __annotations__ = {"cell_anchors": List[torch.Tensor]}
+ __annotations__ = {"cell_anchors": list[torch.Tensor]}
def __init__(
self,
diff --git a/monai/networks/blocks/attention_utils.py b/monai/networks/blocks/attention_utils.py
index 8c9002a16e..c5666d7728 100644
--- a/monai/networks/blocks/attention_utils.py
+++ b/monai/networks/blocks/attention_utils.py
@@ -9,7 +9,6 @@
from __future__ import annotations
-from typing import Tuple
import torch
import torch.nn.functional as F
@@ -50,7 +49,7 @@ def get_rel_pos(q_size: int, k_size: int, rel_pos: torch.Tensor) -> torch.Tensor
def add_decomposed_rel_pos(
- attn: torch.Tensor, q: torch.Tensor, rel_pos_lst: nn.ParameterList, q_size: Tuple, k_size: Tuple
+ attn: torch.Tensor, q: torch.Tensor, rel_pos_lst: nn.ParameterList, q_size: tuple, k_size: tuple
) -> torch.Tensor:
r"""
Calculate decomposed Relative Positional Embeddings from mvitv2 implementation:
diff --git a/monai/networks/blocks/crossattention.py b/monai/networks/blocks/crossattention.py
index be31d2d8fb..e00ff406b6 100644
--- a/monai/networks/blocks/crossattention.py
+++ b/monai/networks/blocks/crossattention.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-from typing import Optional, Tuple
import torch
import torch.nn as nn
@@ -41,9 +40,9 @@ def __init__(
save_attn: bool = False,
causal: bool = False,
sequence_length: int | None = None,
- rel_pos_embedding: Optional[str] = None,
- input_size: Optional[Tuple] = None,
- attention_dtype: Optional[torch.dtype] = None,
+ rel_pos_embedding: str | None = None,
+ input_size: tuple | None = None,
+ attention_dtype: torch.dtype | None = None,
use_flash_attention: bool = False,
) -> None:
"""
@@ -134,7 +133,7 @@ def __init__(
)
self.input_size = input_size
- def forward(self, x: torch.Tensor, context: Optional[torch.Tensor] = None):
+ def forward(self, x: torch.Tensor, context: torch.Tensor | None = None):
"""
Args:
x (torch.Tensor): input tensor. B x (s_dim_1 * ... * s_dim_n) x C
diff --git a/monai/networks/blocks/denseblock.py b/monai/networks/blocks/denseblock.py
index 8c67584f5f..ecccab9d5a 100644
--- a/monai/networks/blocks/denseblock.py
+++ b/monai/networks/blocks/denseblock.py
@@ -11,7 +11,7 @@
from __future__ import annotations
-from typing import Sequence
+from collections.abc import Sequence
import torch
import torch.nn as nn
diff --git a/monai/networks/blocks/mlp.py b/monai/networks/blocks/mlp.py
index 8771711d25..aee76846e3 100644
--- a/monai/networks/blocks/mlp.py
+++ b/monai/networks/blocks/mlp.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-from typing import Union
import torch.nn as nn
@@ -56,8 +55,8 @@ def __init__(
self.linear2 = nn.Linear(mlp_dim, hidden_size)
self.fn = get_act_layer(act)
# Use Union[nn.Dropout, nn.Identity] for type annotations
- self.drop1: Union[nn.Dropout, nn.Identity]
- self.drop2: Union[nn.Dropout, nn.Identity]
+ self.drop1: nn.Dropout | nn.Identity
+ self.drop2: nn.Dropout | nn.Identity
dropout_opt = look_up_option(dropout_mode, SUPPORTED_DROPOUT_MODE)
if dropout_opt == "vit":
diff --git a/monai/networks/blocks/patchembedding.py b/monai/networks/blocks/patchembedding.py
index 4e8a6a0463..2a05ef964c 100644
--- a/monai/networks/blocks/patchembedding.py
+++ b/monai/networks/blocks/patchembedding.py
@@ -12,7 +12,6 @@
from __future__ import annotations
from collections.abc import Sequence
-from typing import Optional
import numpy as np
import torch
@@ -54,7 +53,7 @@ def __init__(
pos_embed_type: str = "learnable",
dropout_rate: float = 0.0,
spatial_dims: int = 3,
- pos_embed_kwargs: Optional[dict] = None,
+ pos_embed_kwargs: dict | None = None,
) -> None:
"""
Args:
diff --git a/monai/networks/blocks/pos_embed_utils.py b/monai/networks/blocks/pos_embed_utils.py
index 266be5e28c..0612a02c28 100644
--- a/monai/networks/blocks/pos_embed_utils.py
+++ b/monai/networks/blocks/pos_embed_utils.py
@@ -13,7 +13,6 @@
import collections.abc
from itertools import repeat
-from typing import List, Union
import torch
import torch.nn as nn
@@ -33,7 +32,7 @@ def parse(x):
def build_fourier_position_embedding(
- grid_size: Union[int, List[int]], embed_dim: int, spatial_dims: int = 3, scales: Union[float, List[float]] = 1.0
+ grid_size: int | list[int], embed_dim: int, spatial_dims: int = 3, scales: float | list[float] = 1.0
) -> torch.nn.Parameter:
"""
Builds a (Anistropic) Fourier feature position embedding based on the given grid size, embed dimension,
@@ -86,7 +85,7 @@ def build_fourier_position_embedding(
def build_sincos_position_embedding(
- grid_size: Union[int, List[int]], embed_dim: int, spatial_dims: int = 3, temperature: float = 10000.0
+ grid_size: int | list[int], embed_dim: int, spatial_dims: int = 3, temperature: float = 10000.0
) -> torch.nn.Parameter:
"""
Builds a sin-cos position embedding based on the given grid size, embed dimension, spatial dimensions, and temperature.
diff --git a/monai/networks/blocks/rel_pos_embedding.py b/monai/networks/blocks/rel_pos_embedding.py
index e53e5841b0..995e179c6e 100644
--- a/monai/networks/blocks/rel_pos_embedding.py
+++ b/monai/networks/blocks/rel_pos_embedding.py
@@ -9,7 +9,7 @@
from __future__ import annotations
-from typing import Iterable, Tuple
+from collections.abc import Iterable
import torch
from torch import nn
@@ -19,7 +19,7 @@
class DecomposedRelativePosEmbedding(nn.Module):
- def __init__(self, s_input_dims: Tuple[int, int] | Tuple[int, int, int], c_dim: int, num_heads: int) -> None:
+ def __init__(self, s_input_dims: tuple[int, int] | tuple[int, int, int], c_dim: int, num_heads: int) -> None:
"""
Args:
s_input_dims (Tuple): input spatial dimension. (H, W) or (H, W, D)
diff --git a/monai/networks/blocks/selfattention.py b/monai/networks/blocks/selfattention.py
index dac7e5c5da..0bca6a24bd 100644
--- a/monai/networks/blocks/selfattention.py
+++ b/monai/networks/blocks/selfattention.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-from typing import Optional, Tuple, Union
import torch
import torch.nn as nn
@@ -41,7 +40,7 @@ def __init__(
causal: bool = False,
sequence_length: int | None = None,
rel_pos_embedding: str | None = None,
- input_size: Tuple | None = None,
+ input_size: tuple | None = None,
attention_dtype: torch.dtype | None = None,
include_fc: bool = True,
use_combined_linear: bool = True,
@@ -101,16 +100,16 @@ def __init__(
self.num_heads = num_heads
self.hidden_input_size = hidden_input_size if hidden_input_size else hidden_size
- self.out_proj: Union[nn.Linear, nn.Identity]
+ self.out_proj: nn.Linear | nn.Identity
if include_fc:
self.out_proj = nn.Linear(self.inner_dim, self.hidden_input_size)
else:
self.out_proj = nn.Identity()
- self.qkv: Union[nn.Linear, nn.Identity]
- self.to_q: Union[nn.Linear, nn.Identity]
- self.to_k: Union[nn.Linear, nn.Identity]
- self.to_v: Union[nn.Linear, nn.Identity]
+ self.qkv: nn.Linear | nn.Identity
+ self.to_q: nn.Linear | nn.Identity
+ self.to_k: nn.Linear | nn.Identity
+ self.to_v: nn.Linear | nn.Identity
if use_combined_linear:
self.qkv = nn.Linear(self.hidden_input_size, self.inner_dim * 3, bias=qkv_bias)
@@ -153,7 +152,7 @@ def __init__(
)
self.input_size = input_size
- def forward(self, x, attn_mask: Optional[torch.Tensor] = None):
+ def forward(self, x, attn_mask: torch.Tensor | None = None):
"""
Args:
x (torch.Tensor): input tensor. B x (s_dim_1 * ... * s_dim_n) x C
diff --git a/monai/networks/blocks/spatialattention.py b/monai/networks/blocks/spatialattention.py
index 60a89a7840..52513c961b 100644
--- a/monai/networks/blocks/spatialattention.py
+++ b/monai/networks/blocks/spatialattention.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-from typing import Optional
import torch
import torch.nn as nn
@@ -46,7 +45,7 @@ def __init__(
num_head_channels: int | None = None,
norm_num_groups: int = 32,
norm_eps: float = 1e-6,
- attention_dtype: Optional[torch.dtype] = None,
+ attention_dtype: torch.dtype | None = None,
include_fc: bool = True,
use_combined_linear: bool = False,
use_flash_attention: bool = False,
diff --git a/monai/networks/blocks/transformerblock.py b/monai/networks/blocks/transformerblock.py
index 6f0da73e7b..0fd656b2b6 100644
--- a/monai/networks/blocks/transformerblock.py
+++ b/monai/networks/blocks/transformerblock.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-from typing import Optional
import torch
import torch.nn as nn
@@ -91,7 +90,7 @@ def __init__(
)
def forward(
- self, x: torch.Tensor, context: Optional[torch.Tensor] = None, attn_mask: Optional[torch.Tensor] = None
+ self, x: torch.Tensor, context: torch.Tensor | None = None, attn_mask: torch.Tensor | None = None
) -> torch.Tensor:
x = x + self.attn(self.norm1(x), attn_mask=attn_mask)
if self.with_cross_attention:
diff --git a/monai/networks/layers/simplelayers.py b/monai/networks/layers/simplelayers.py
index 6d34c3fa77..ab5cad2b92 100644
--- a/monai/networks/layers/simplelayers.py
+++ b/monai/networks/layers/simplelayers.py
@@ -13,7 +13,7 @@
import math
from copy import deepcopy
-from typing import Sequence
+from collections.abc import Sequence
import torch
import torch.nn.functional as F
diff --git a/monai/networks/layers/utils.py b/monai/networks/layers/utils.py
index 8676f74638..c76eab934b 100644
--- a/monai/networks/layers/utils.py
+++ b/monai/networks/layers/utils.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-from typing import Optional
import torch.nn
@@ -128,7 +127,7 @@ def get_pool_layer(name: tuple | str, spatial_dims: int | None = 1):
return pool_type(**pool_args)
-def get_rel_pos_embedding_layer(name: tuple | str, s_input_dims: Optional[tuple], c_dim: int, num_heads: int):
+def get_rel_pos_embedding_layer(name: tuple | str, s_input_dims: tuple | None, c_dim: int, num_heads: int):
embedding_name, embedding_args = split_args(name)
embedding_type = RelPosEmbedding[embedding_name]
# create a dictionary with the default values which can be overridden by embedding_args
diff --git a/monai/networks/layers/vector_quantizer.py b/monai/networks/layers/vector_quantizer.py
index 0ff7143b69..388f93fe2d 100644
--- a/monai/networks/layers/vector_quantizer.py
+++ b/monai/networks/layers/vector_quantizer.py
@@ -11,7 +11,7 @@
from __future__ import annotations
-from typing import Sequence, Tuple
+from collections.abc import Sequence
import torch
from torch import nn
@@ -87,7 +87,7 @@ def __init__(
range(1, self.spatial_dims + 1)
)
- def quantize(self, inputs: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
+ def quantize(self, inputs: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""
Given an input it projects it to the quantized space and returns additional tensors needed for EMA loss.
@@ -164,7 +164,7 @@ def distributed_synchronization(self, encodings_sum: torch.Tensor, dw: torch.Ten
else:
pass
- def forward(self, inputs: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
+ def forward(self, inputs: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
flat_input, encodings, encoding_indices = self.quantize(inputs)
quantized = self.embed(encoding_indices)
@@ -211,7 +211,7 @@ def __init__(self, quantizer: EMAQuantizer):
self.perplexity: torch.Tensor = torch.rand(1)
- def forward(self, inputs: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
+ def forward(self, inputs: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
quantized, loss, encoding_indices = self.quantizer(inputs)
# Perplexity calculations
avg_probs = (
diff --git a/monai/networks/nets/ahnet.py b/monai/networks/nets/ahnet.py
index 5e280d7f24..bdacfd23a0 100644
--- a/monai/networks/nets/ahnet.py
+++ b/monai/networks/nets/ahnet.py
@@ -13,7 +13,6 @@
import math
from collections.abc import Sequence
-from typing import Union
import torch
import torch.nn as nn
@@ -286,7 +285,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
else:
for project_module, pool_module in zip(self.project_modules, self.pool_modules):
interpolate_size = x.shape[2:]
- align_corners: Union[bool, None] = None
+ align_corners: bool | None = None
if self.upsample_mode in ["trilinear", "bilinear"]:
align_corners = True
output = F.interpolate(
diff --git a/monai/networks/nets/autoencoderkl.py b/monai/networks/nets/autoencoderkl.py
index f9cec5800e..b5a282a340 100644
--- a/monai/networks/nets/autoencoderkl.py
+++ b/monai/networks/nets/autoencoderkl.py
@@ -12,7 +12,6 @@
from __future__ import annotations
from collections.abc import Sequence
-from typing import List
import torch
import torch.nn as nn
@@ -188,7 +187,7 @@ def __init__(
self.norm_eps = norm_eps
self.attention_levels = attention_levels
- blocks: List[nn.Module] = []
+ blocks: list[nn.Module] = []
# Initial convolution
blocks.append(
Convolution(
@@ -338,7 +337,7 @@ def __init__(
reversed_block_out_channels = list(reversed(channels))
- blocks: List[nn.Module] = []
+ blocks: list[nn.Module] = []
# Initial convolution
blocks.append(
diff --git a/monai/networks/nets/basic_unet.py b/monai/networks/nets/basic_unet.py
index b9970d4113..d2a655f981 100644
--- a/monai/networks/nets/basic_unet.py
+++ b/monai/networks/nets/basic_unet.py
@@ -12,7 +12,6 @@
from __future__ import annotations
from collections.abc import Sequence
-from typing import Optional
import torch
import torch.nn as nn
@@ -150,7 +149,7 @@ def __init__(
self.convs = TwoConv(spatial_dims, cat_chns + up_chns, out_chns, act, norm, bias, dropout)
self.is_pad = is_pad
- def forward(self, x: torch.Tensor, x_e: Optional[torch.Tensor]):
+ def forward(self, x: torch.Tensor, x_e: torch.Tensor | None):
"""
Args:
diff --git a/monai/networks/nets/dints.py b/monai/networks/nets/dints.py
index 43c0cd031b..18e24373a0 100644
--- a/monai/networks/nets/dints.py
+++ b/monai/networks/nets/dints.py
@@ -13,7 +13,6 @@
import datetime
import warnings
-from typing import Optional
import numpy as np
import torch
@@ -41,7 +40,7 @@
class CellInterface(torch.nn.Module):
"""interface for torchscriptable Cell"""
- def forward(self, x: torch.Tensor, weight: Optional[torch.Tensor]) -> torch.Tensor: # type: ignore
+ def forward(self, x: torch.Tensor, weight: torch.Tensor | None) -> torch.Tensor: # type: ignore
pass
@@ -175,7 +174,7 @@ def __init__(self, c: int, ops: dict, arch_code_c=None):
if arch_c > 0:
self.ops.append(ops[op_name](c))
- def forward(self, x: torch.Tensor, weight: Optional[torch.Tensor] = None):
+ def forward(self, x: torch.Tensor, weight: torch.Tensor | None = None):
"""
Args:
x: input tensor.
@@ -303,7 +302,7 @@ def __init__(
self.op = MixedOp(c, self.OPS, arch_code_c)
- def forward(self, x: torch.Tensor, weight: Optional[torch.Tensor]) -> torch.Tensor:
+ def forward(self, x: torch.Tensor, weight: torch.Tensor | None) -> torch.Tensor:
"""
Args:
x: input tensor
@@ -574,9 +573,8 @@ def __init__(
self.num_blocks = num_blocks
self.num_depths = num_depths
print(
- "{} - Length of input patch is recommended to be a multiple of {:d}.".format(
- datetime.datetime.now(), 2 ** (num_depths + int(use_downsample))
- )
+ f"{datetime.datetime.now()}"
+ f" - Length of input patch is recommended to be a multiple of {2 ** (num_depths + int(use_downsample)):d}."
)
self._spatial_dims = spatial_dims
diff --git a/monai/networks/nets/dynunet.py b/monai/networks/nets/dynunet.py
index 59e1e4a758..c5b7efd22c 100644
--- a/monai/networks/nets/dynunet.py
+++ b/monai/networks/nets/dynunet.py
@@ -11,7 +11,8 @@
# isort: dont-add-import: from __future__ import annotations
-from typing import List, Optional, Sequence, Tuple, Union
+from typing import Optional, Union
+from collections.abc import Sequence
import torch
import torch.nn as nn
@@ -32,7 +33,7 @@ class DynUNetSkipLayer(nn.Module):
forward passes of the network.
"""
- heads: Optional[List[torch.Tensor]]
+ heads: Optional[list[torch.Tensor]]
def __init__(self, index, downsample, upsample, next_layer, heads=None, super_head=None):
super().__init__()
@@ -136,9 +137,9 @@ def __init__(
strides: Sequence[Union[Sequence[int], int]],
upsample_kernel_size: Sequence[Union[Sequence[int], int]],
filters: Optional[Sequence[int]] = None,
- dropout: Optional[Union[Tuple, str, float]] = None,
- norm_name: Union[Tuple, str] = ("INSTANCE", {"affine": True}),
- act_name: Union[Tuple, str] = ("leakyrelu", {"inplace": True, "negative_slope": 0.01}),
+ dropout: Optional[Union[tuple, str, float]] = None,
+ norm_name: Union[tuple, str] = ("INSTANCE", {"affine": True}),
+ act_name: Union[tuple, str] = ("leakyrelu", {"inplace": True, "negative_slope": 0.01}),
deep_supervision: bool = False,
deep_supr_num: int = 1,
res_block: bool = False,
@@ -169,7 +170,7 @@ def __init__(
self.deep_supervision = deep_supervision
self.deep_supr_num = deep_supr_num
# initialize the typed list of supervision head outputs so that Torchscript can recognize what's going on
- self.heads: List[torch.Tensor] = [torch.rand(1)] * self.deep_supr_num
+ self.heads: list[torch.Tensor] = [torch.rand(1)] * self.deep_supr_num
if self.deep_supervision:
self.deep_supervision_heads = self.get_deep_supervision_heads()
self.check_deep_supr_num()
@@ -323,8 +324,8 @@ def get_upsamples(self):
def get_module_list(
self,
- in_channels: List[int],
- out_channels: List[int],
+ in_channels: list[int],
+ out_channels: list[int],
kernel_size: Sequence[Union[Sequence[int], int]],
strides: Sequence[Union[Sequence[int], int]],
conv_block: nn.Module,
diff --git a/monai/networks/nets/netadapter.py b/monai/networks/nets/netadapter.py
index 452c31be37..f87120ba21 100644
--- a/monai/networks/nets/netadapter.py
+++ b/monai/networks/nets/netadapter.py
@@ -11,7 +11,7 @@
from __future__ import annotations
-from typing import Any, Dict
+from typing import Any
import torch
@@ -109,7 +109,7 @@ def forward(self, x):
x = self.features(x)
if isinstance(x, tuple):
x = x[0] # it might be a namedtuple such as torchvision.model.InceptionOutputs
- elif torch.jit.isinstance(x, Dict[str, torch.Tensor]):
+ elif torch.jit.isinstance(x, dict[str, torch.Tensor]):
x = x[self.node_name] # torchvision create_feature_extractor
if self.pool is not None:
x = self.pool(x)
diff --git a/monai/networks/nets/quicknat.py b/monai/networks/nets/quicknat.py
index 7e0f9c6b38..f82e3d7e78 100644
--- a/monai/networks/nets/quicknat.py
+++ b/monai/networks/nets/quicknat.py
@@ -11,7 +11,7 @@
from __future__ import annotations
-from typing import Optional, Sequence, Tuple, Union
+from collections.abc import Sequence
import torch
import torch.nn as nn
@@ -123,8 +123,8 @@ class ConvConcatDenseBlock(ConvDenseBlock):
def __init__(
self,
in_channels: int,
- se_layer: Optional[nn.Module] = None,
- dropout_layer: Optional[nn.Dropout2d] = None,
+ se_layer: nn.Module | None = None,
+ dropout_layer: nn.Dropout2d | None = None,
kernel_size: Sequence[int] | int = 5,
num_filters: int = 64,
):
@@ -360,8 +360,8 @@ def __init__(
# Valid options : NONE, CSE, SSE, CSSE
se_block: str = "None",
drop_out: float = 0,
- act: Union[Tuple, str] = Act.PRELU,
- norm: Union[Tuple, str] = Norm.INSTANCE,
+ act: tuple | str = Act.PRELU,
+ norm: tuple | str = Norm.INSTANCE,
adn_ordering: str = "NA",
) -> None:
self.act = act
diff --git a/monai/networks/nets/resnet.py b/monai/networks/nets/resnet.py
index d24b86d27d..9142116ae4 100644
--- a/monai/networks/nets/resnet.py
+++ b/monai/networks/nets/resnet.py
@@ -240,7 +240,7 @@ def __init__(
elif block == "bottleneck":
block = ResNetBottleneck
else:
- raise ValueError("Unknown block '%s', use basic or bottleneck" % block)
+ raise ValueError(f"Unknown block '{block}', use basic or bottleneck")
conv_type: type[nn.Conv1d | nn.Conv2d | nn.Conv3d] = Conv[Conv.CONV, spatial_dims]
pool_type: type[nn.MaxPool1d | nn.MaxPool2d | nn.MaxPool3d] = Pool[Pool.MAX, spatial_dims]
diff --git a/monai/networks/nets/segresnet_ds.py b/monai/networks/nets/segresnet_ds.py
index 8f575f4793..9dda6821d5 100644
--- a/monai/networks/nets/segresnet_ds.py
+++ b/monai/networks/nets/segresnet_ds.py
@@ -13,7 +13,6 @@
import copy
from collections.abc import Callable
-from typing import Union
import numpy as np
import torch
@@ -388,7 +387,7 @@ def is_valid_shape(self, x):
a = [i % j == 0 for i, j in zip(x.shape[2:], self.shape_factor())]
return all(a)
- def _forward(self, x: torch.Tensor) -> Union[None, torch.Tensor, list[torch.Tensor]]:
+ def _forward(self, x: torch.Tensor) -> None | torch.Tensor | list[torch.Tensor]:
if self.preprocess is not None:
x = self.preprocess(x)
@@ -424,7 +423,7 @@ def _forward(self, x: torch.Tensor) -> Union[None, torch.Tensor, list[torch.Tens
# return a list of DS outputs
return outputs
- def forward(self, x: torch.Tensor) -> Union[None, torch.Tensor, list[torch.Tensor]]:
+ def forward(self, x: torch.Tensor) -> None | torch.Tensor | list[torch.Tensor]:
return self._forward(x)
@@ -485,7 +484,7 @@ def __init__(
def forward( # type: ignore
self, x: torch.Tensor, with_point: bool = True, with_label: bool = True
- ) -> tuple[Union[None, torch.Tensor, list[torch.Tensor]], Union[None, torch.Tensor, list[torch.Tensor]]]:
+ ) -> tuple[None | torch.Tensor | list[torch.Tensor], None | torch.Tensor | list[torch.Tensor]]:
"""
Args:
x: input tensor.
diff --git a/monai/networks/nets/senet.py b/monai/networks/nets/senet.py
index c14118ad20..4c7dd0f0c2 100644
--- a/monai/networks/nets/senet.py
+++ b/monai/networks/nets/senet.py
@@ -119,7 +119,7 @@ def __init__(
block = SEResNeXtBottleneck
else:
raise ValueError(
- "Unknown block '%s', use se_bottleneck, se_resnet_bottleneck or se_resnetxt_bottleneck" % block
+ f"Unknown block '{block}', use se_bottleneck, se_resnet_bottleneck or se_resnetxt_bottleneck"
)
relu_type: type[nn.ReLU] = Act[Act.RELU]
diff --git a/monai/networks/nets/spade_network.py b/monai/networks/nets/spade_network.py
index 9164541f27..a4707e89eb 100644
--- a/monai/networks/nets/spade_network.py
+++ b/monai/networks/nets/spade_network.py
@@ -11,7 +11,7 @@
from __future__ import annotations
-from typing import Sequence
+from collections.abc import Sequence
import numpy as np
import torch
@@ -156,7 +156,7 @@ def __init__(
self.z_dim = z_dim
self.channels = channels
if len(input_shape) != spatial_dims:
- raise ValueError("Length of parameter input shape must match spatial_dims; got %s" % (input_shape))
+ raise ValueError(f"Length of parameter input shape must match spatial_dims; got {input_shape}")
for s_ind, s_ in enumerate(input_shape):
if s_ / (2 ** len(channels)) != s_ // (2 ** len(channels)):
raise ValueError(
@@ -255,7 +255,7 @@ def __init__(
self.label_nc = label_nc
self.num_channels = channels
if len(input_shape) != spatial_dims:
- raise ValueError("Length of parameter input shape must match spatial_dims; got %s" % (input_shape))
+ raise ValueError(f"Length of parameter input shape must match spatial_dims; got {input_shape}")
for s_ind, s_ in enumerate(input_shape):
if s_ / (2 ** len(channels)) != s_ // (2 ** len(channels)):
raise ValueError(
diff --git a/monai/networks/nets/swin_unetr.py b/monai/networks/nets/swin_unetr.py
index 4566a96856..b4d93c9afe 100644
--- a/monai/networks/nets/swin_unetr.py
+++ b/monai/networks/nets/swin_unetr.py
@@ -811,7 +811,7 @@ def compute_mask(dims, window_size, shift_size, device):
mask_windows = window_partition(img_mask, window_size)
mask_windows = mask_windows.squeeze(-1)
attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2)
- attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill(attn_mask == 0, float(0.0))
+ attn_mask = attn_mask.masked_fill(attn_mask != 0, -100.0).masked_fill(attn_mask == 0, 0.0)
return attn_mask
diff --git a/monai/networks/nets/vista3d.py b/monai/networks/nets/vista3d.py
index a5c2cc13ef..55d84afe28 100644
--- a/monai/networks/nets/vista3d.py
+++ b/monai/networks/nets/vista3d.py
@@ -12,7 +12,8 @@
from __future__ import annotations
import math
-from typing import Any, Callable, Optional, Sequence, Tuple
+from typing import Any, Callable
+from collections.abc import Sequence
import numpy as np
import torch
@@ -691,7 +692,7 @@ def __init__(
def forward(
self, image_embedding: torch.Tensor, image_pe: torch.Tensor, point_embedding: torch.Tensor
- ) -> Tuple[torch.Tensor, torch.Tensor]:
+ ) -> tuple[torch.Tensor, torch.Tensor]:
"""
Args:
image_embedding: image to attend to. Should be shape
@@ -768,7 +769,7 @@ def __init__(
def forward(
self, queries: torch.Tensor, keys: torch.Tensor, query_pe: torch.Tensor, key_pe: torch.Tensor
- ) -> Tuple[torch.Tensor, torch.Tensor]:
+ ) -> tuple[torch.Tensor, torch.Tensor]:
# Self attention block
if self.skip_first_layer_pe:
queries = self.self_attn(q=queries, k=queries, v=queries)
@@ -872,7 +873,7 @@ class PositionEmbeddingRandom(nn.Module):
scale: the scale of the positional encoding.
"""
- def __init__(self, num_pos_feats: int = 64, scale: Optional[float] = None) -> None:
+ def __init__(self, num_pos_feats: int = 64, scale: float | None = None) -> None:
super().__init__()
if scale is None or scale <= 0.0:
scale = 1.0
@@ -890,7 +891,7 @@ def _pe_encoding(self, coords: torch.torch.Tensor) -> torch.torch.Tensor:
# [bs=1, N=2, 128+128=256]
return torch.cat([torch.sin(coords), torch.cos(coords)], dim=-1)
- def forward(self, size: Tuple[int, int, int]) -> torch.torch.Tensor:
+ def forward(self, size: tuple[int, int, int]) -> torch.torch.Tensor:
"""Generate positional encoding for a grid of the specified size."""
h, w, d = size
device: Any = self.positional_encoding_gaussian_matrix.device
@@ -906,7 +907,7 @@ def forward(self, size: Tuple[int, int, int]) -> torch.torch.Tensor:
return pe.permute(3, 0, 1, 2)
def forward_with_coords(
- self, coords_input: torch.torch.Tensor, image_size: Tuple[int, int, int]
+ self, coords_input: torch.torch.Tensor, image_size: tuple[int, int, int]
) -> torch.torch.Tensor:
"""Positionally encode points that are not normalized to [0,1]."""
coords = coords_input.clone()
diff --git a/monai/networks/nets/vqvae.py b/monai/networks/nets/vqvae.py
index f198bfbb2b..43ba48585c 100644
--- a/monai/networks/nets/vqvae.py
+++ b/monai/networks/nets/vqvae.py
@@ -12,7 +12,6 @@
from __future__ import annotations
from collections.abc import Sequence
-from typing import Tuple
import torch
import torch.nn as nn
@@ -107,7 +106,7 @@ def __init__(
channels: Sequence[int],
num_res_layers: int,
num_res_channels: Sequence[int],
- downsample_parameters: Sequence[Tuple[int, int, int, int]],
+ downsample_parameters: Sequence[tuple[int, int, int, int]],
dropout: float,
act: tuple | str | None,
) -> None:
@@ -198,7 +197,7 @@ def __init__(
channels: Sequence[int],
num_res_layers: int,
num_res_channels: Sequence[int],
- upsample_parameters: Sequence[Tuple[int, int, int, int, int]],
+ upsample_parameters: Sequence[tuple[int, int, int, int, int]],
dropout: float,
act: tuple | str | None,
output_act: tuple | str | None,
@@ -312,12 +311,12 @@ def __init__(
channels: Sequence[int] = (96, 96, 192),
num_res_layers: int = 3,
num_res_channels: Sequence[int] | int = (96, 96, 192),
- downsample_parameters: Sequence[Tuple[int, int, int, int]] | Tuple[int, int, int, int] = (
+ downsample_parameters: Sequence[tuple[int, int, int, int]] | tuple[int, int, int, int] = (
(2, 4, 1, 1),
(2, 4, 1, 1),
(2, 4, 1, 1),
),
- upsample_parameters: Sequence[Tuple[int, int, int, int, int]] | Tuple[int, int, int, int, int] = (
+ upsample_parameters: Sequence[tuple[int, int, int, int, int]] | tuple[int, int, int, int, int] = (
(2, 4, 1, 1, 0),
(2, 4, 1, 1, 0),
(2, 4, 1, 1, 0),
diff --git a/monai/networks/schedulers/rectified_flow.py b/monai/networks/schedulers/rectified_flow.py
index e660a1abb6..bd0a715f2b 100644
--- a/monai/networks/schedulers/rectified_flow.py
+++ b/monai/networks/schedulers/rectified_flow.py
@@ -28,7 +28,6 @@
from __future__ import annotations
-from typing import Union
import numpy as np
import torch
@@ -283,7 +282,7 @@ def sample_timesteps(self, x_start):
return t
def step(
- self, model_output: torch.Tensor, timestep: int, sample: torch.Tensor, next_timestep: Union[int, None] = None
+ self, model_output: torch.Tensor, timestep: int, sample: torch.Tensor, next_timestep: int | None = None
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Predicts the next sample in the diffusion process.
diff --git a/monai/networks/trt_compiler.py b/monai/networks/trt_compiler.py
index d96b712003..2df7189ad4 100644
--- a/monai/networks/trt_compiler.py
+++ b/monai/networks/trt_compiler.py
@@ -18,7 +18,7 @@
from collections import OrderedDict
from pathlib import Path
from types import MethodType
-from typing import Any, Dict, List, Tuple, Union
+from typing import Any
import torch
@@ -242,8 +242,8 @@ def unroll_input(input_names, input_example):
def parse_groups(
- ret: List[torch.Tensor], output_lists: List[List[int]]
-) -> Tuple[Union[torch.Tensor, List[torch.Tensor]], ...]:
+ ret: list[torch.Tensor], output_lists: list[list[int]]
+) -> tuple[torch.Tensor | list[torch.Tensor], ...]:
"""
Implements parsing of 'output_lists' arg of trt_compile().
@@ -261,7 +261,7 @@ def parse_groups(
Tuple of Union[torch.Tensor, List[torch.Tensor]], according to the grouping in output_lists
"""
- groups: Tuple[Union[torch.Tensor, List[torch.Tensor]], ...] = tuple()
+ groups: tuple[torch.Tensor | list[torch.Tensor], ...] = tuple()
cur = 0
for l in range(len(output_lists)):
gl = output_lists[l]
@@ -273,7 +273,7 @@ def parse_groups(
groups = (*groups, ret[cur : cur + gl[0]])
cur = cur + gl[0]
elif gl[0] == -1:
- rev_groups: Tuple[Union[torch.Tensor, List[torch.Tensor]], ...] = tuple()
+ rev_groups: tuple[torch.Tensor | list[torch.Tensor], ...] = tuple()
rcur = len(ret)
for rl in range(len(output_lists) - 1, l, -1):
rgl = output_lists[rl]
@@ -601,8 +601,8 @@ def trt_forward(self, *argv, **kwargs):
def trt_compile(
model: torch.nn.Module,
base_path: str,
- args: Dict[str, Any] | None = None,
- submodule: Union[str, List[str]] | None = None,
+ args: dict[str, Any] | None = None,
+ submodule: str | list[str] | None = None,
logger: Any | None = None,
) -> torch.nn.Module:
"""
@@ -625,7 +625,7 @@ def trt_compile(
Always returns same model passed in as argument. This is for ease of use in configs.
"""
- default_args: Dict[str, Any] = {
+ default_args: dict[str, Any] = {
"method": "onnx",
"precision": "fp16",
"build_args": {"builder_optimization_level": 5, "precision_constraints": "obey"},
diff --git a/monai/networks/utils.py b/monai/networks/utils.py
index df91c84bdf..4f444bb100 100644
--- a/monai/networks/utils.py
+++ b/monai/networks/utils.py
@@ -22,7 +22,8 @@
from collections.abc import Callable, Mapping, Sequence
from contextlib import contextmanager
from copy import deepcopy
-from typing import Any, Iterable
+from typing import Any
+from collections.abc import Iterable
import numpy as np
import torch
From bcd347166ed8f008a994ace07e807d15aafafe7f Mon Sep 17 00:00:00 2001
From: jirka
Date: Sun, 9 Nov 2025 10:40:58 +0100
Subject: [PATCH 24/61] ./runtests.sh --autofix
Signed-off-by: jirka
---
monai/losses/ds_loss.py | 1 -
monai/losses/perceptual.py | 4 +++-
monai/metrics/utils.py | 2 +-
monai/networks/blocks/attention_utils.py | 1 -
monai/networks/blocks/crossattention.py | 1 -
monai/networks/blocks/mlp.py | 1 -
monai/networks/blocks/selfattention.py | 1 -
monai/networks/blocks/spatialattention.py | 1 -
monai/networks/blocks/transformerblock.py | 1 -
monai/networks/layers/simplelayers.py | 2 +-
monai/networks/layers/utils.py | 1 -
monai/networks/nets/dynunet.py | 2 +-
monai/networks/nets/vista3d.py | 2 +-
monai/networks/schedulers/rectified_flow.py | 1 -
monai/networks/utils.py | 3 +--
15 files changed, 8 insertions(+), 16 deletions(-)
diff --git a/monai/losses/ds_loss.py b/monai/losses/ds_loss.py
index ef359bcfd0..7e6fcf7e3c 100644
--- a/monai/losses/ds_loss.py
+++ b/monai/losses/ds_loss.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-
import torch
import torch.nn.functional as F
from torch.nn.modules.loss import _Loss
diff --git a/monai/losses/perceptual.py b/monai/losses/perceptual.py
index 7774c39f10..aed1f26b86 100644
--- a/monai/losses/perceptual.py
+++ b/monai/losses/perceptual.py
@@ -95,7 +95,9 @@ def __init__(
if network_type.lower() not in list(PercetualNetworkType):
raise ValueError(
- "Unrecognised criterion entered for Perceptual Loss. Must be one in: {}".format(", ".join(PercetualNetworkType))
+ "Unrecognised criterion entered for Perceptual Loss. Must be one in: {}".format(
+ ", ".join(PercetualNetworkType)
+ )
)
if cache_dir:
diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py
index 606a54669b..a451b1a770 100644
--- a/monai/metrics/utils.py
+++ b/monai/metrics/utils.py
@@ -13,7 +13,7 @@
import warnings
from collections.abc import Iterable, Sequence
-from functools import partial, cache
+from functools import cache, partial
from types import ModuleType
from typing import Any
diff --git a/monai/networks/blocks/attention_utils.py b/monai/networks/blocks/attention_utils.py
index c5666d7728..a8dfcd7df3 100644
--- a/monai/networks/blocks/attention_utils.py
+++ b/monai/networks/blocks/attention_utils.py
@@ -9,7 +9,6 @@
from __future__ import annotations
-
import torch
import torch.nn.functional as F
from torch import nn
diff --git a/monai/networks/blocks/crossattention.py b/monai/networks/blocks/crossattention.py
index e00ff406b6..baaa21ed1f 100644
--- a/monai/networks/blocks/crossattention.py
+++ b/monai/networks/blocks/crossattention.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-
import torch
import torch.nn as nn
diff --git a/monai/networks/blocks/mlp.py b/monai/networks/blocks/mlp.py
index aee76846e3..84c8065531 100644
--- a/monai/networks/blocks/mlp.py
+++ b/monai/networks/blocks/mlp.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-
import torch.nn as nn
from monai.networks.layers import get_act_layer
diff --git a/monai/networks/blocks/selfattention.py b/monai/networks/blocks/selfattention.py
index 0bca6a24bd..2791d2fb00 100644
--- a/monai/networks/blocks/selfattention.py
+++ b/monai/networks/blocks/selfattention.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-
import torch
import torch.nn as nn
import torch.nn.functional as F
diff --git a/monai/networks/blocks/spatialattention.py b/monai/networks/blocks/spatialattention.py
index 52513c961b..40fb36160b 100644
--- a/monai/networks/blocks/spatialattention.py
+++ b/monai/networks/blocks/spatialattention.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-
import torch
import torch.nn as nn
diff --git a/monai/networks/blocks/transformerblock.py b/monai/networks/blocks/transformerblock.py
index 0fd656b2b6..b93d81bdef 100644
--- a/monai/networks/blocks/transformerblock.py
+++ b/monai/networks/blocks/transformerblock.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-
import torch
import torch.nn as nn
diff --git a/monai/networks/layers/simplelayers.py b/monai/networks/layers/simplelayers.py
index ab5cad2b92..56f7192e4d 100644
--- a/monai/networks/layers/simplelayers.py
+++ b/monai/networks/layers/simplelayers.py
@@ -12,8 +12,8 @@
from __future__ import annotations
import math
-from copy import deepcopy
from collections.abc import Sequence
+from copy import deepcopy
import torch
import torch.nn.functional as F
diff --git a/monai/networks/layers/utils.py b/monai/networks/layers/utils.py
index c76eab934b..4f8cfb225e 100644
--- a/monai/networks/layers/utils.py
+++ b/monai/networks/layers/utils.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-
import torch.nn
from monai.networks.layers.factories import Act, Dropout, Norm, Pool, RelPosEmbedding, split_args
diff --git a/monai/networks/nets/dynunet.py b/monai/networks/nets/dynunet.py
index c5b7efd22c..ec418469bb 100644
--- a/monai/networks/nets/dynunet.py
+++ b/monai/networks/nets/dynunet.py
@@ -11,8 +11,8 @@
# isort: dont-add-import: from __future__ import annotations
-from typing import Optional, Union
from collections.abc import Sequence
+from typing import Optional, Union
import torch
import torch.nn as nn
diff --git a/monai/networks/nets/vista3d.py b/monai/networks/nets/vista3d.py
index 55d84afe28..93bdb34a76 100644
--- a/monai/networks/nets/vista3d.py
+++ b/monai/networks/nets/vista3d.py
@@ -12,8 +12,8 @@
from __future__ import annotations
import math
-from typing import Any, Callable
from collections.abc import Sequence
+from typing import Any, Callable
import numpy as np
import torch
diff --git a/monai/networks/schedulers/rectified_flow.py b/monai/networks/schedulers/rectified_flow.py
index bd0a715f2b..f090ebc1e8 100644
--- a/monai/networks/schedulers/rectified_flow.py
+++ b/monai/networks/schedulers/rectified_flow.py
@@ -28,7 +28,6 @@
from __future__ import annotations
-
import numpy as np
import torch
from torch.distributions import LogisticNormal
diff --git a/monai/networks/utils.py b/monai/networks/utils.py
index 4f444bb100..a4a006f97c 100644
--- a/monai/networks/utils.py
+++ b/monai/networks/utils.py
@@ -19,11 +19,10 @@
import tempfile
import warnings
from collections import OrderedDict
-from collections.abc import Callable, Mapping, Sequence
+from collections.abc import Callable, Iterable, Mapping, Sequence
from contextlib import contextmanager
from copy import deepcopy
from typing import Any
-from collections.abc import Iterable
import numpy as np
import torch
From 0e7a2b5dfa2e16af81fb2a1eb80265ef75bede1e Mon Sep 17 00:00:00 2001
From: ytl0623
Date: Tue, 11 Nov 2025 22:55:34 +0800
Subject: [PATCH 25/61] timestep scheduling with np.linspace (#8623)
Fixes #8600
### Description
The `np.linspace` approach generates a descending array that starts
exactly at 999 and ends exactly at 0 (after rounding), ensuring the
scheduler samples the entire intended trajectory.
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Breaking change (fix or new feature that would cause existing
functionality to change).
- [ ] New tests added to cover the changes.
- [ ] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [ ] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [ ] In-line docstrings updated.
- [ ] Documentation updated, tested `make html` command in the `docs/`
folder.
---------
Signed-off-by: ytl0623
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Signed-off-by: jirka
---
monai/networks/schedulers/ddim.py | 20 ++++++++------------
monai/networks/schedulers/ddpm.py | 9 +++------
2 files changed, 11 insertions(+), 18 deletions(-)
diff --git a/monai/networks/schedulers/ddim.py b/monai/networks/schedulers/ddim.py
index 50a680336d..9d843c6898 100644
--- a/monai/networks/schedulers/ddim.py
+++ b/monai/networks/schedulers/ddim.py
@@ -117,18 +117,14 @@ def set_timesteps(self, num_inference_steps: int, device: str | torch.device | N
)
self.num_inference_steps = num_inference_steps
- step_ratio = self.num_train_timesteps // self.num_inference_steps
- if self.steps_offset >= step_ratio:
- raise ValueError(
- f"`steps_offset`: {self.steps_offset} cannot be greater than or equal to "
- f"`num_train_timesteps // num_inference_steps : {step_ratio}` as this will cause timesteps to exceed"
- f" the max train timestep."
- )
-
- # creates integer timesteps by multiplying by ratio
- # casting to int to avoid issues when num_inference_step is power of 3
- timesteps = (np.arange(0, num_inference_steps) * step_ratio).round()[::-1].copy().astype(np.int64)
- self.timesteps = torch.from_numpy(timesteps).to(device)
+ if self.steps_offset < 0 or self.steps_offset >= self.num_train_timesteps:
+ raise ValueError(f"`steps_offset`: {self.steps_offset} must be in range [0, {self.num_train_timesteps}).")
+
+ self.timesteps = (
+ torch.linspace((self.num_train_timesteps - 1) - self.steps_offset, 0, num_inference_steps, device=device)
+ .round()
+ .long()
+ )
self.timesteps += self.steps_offset
def _get_variance(self, timestep: int, prev_timestep: int) -> torch.Tensor:
diff --git a/monai/networks/schedulers/ddpm.py b/monai/networks/schedulers/ddpm.py
index e2b7ab55f5..73480346b0 100644
--- a/monai/networks/schedulers/ddpm.py
+++ b/monai/networks/schedulers/ddpm.py
@@ -31,7 +31,6 @@
from __future__ import annotations
-import numpy as np
import torch
from monai.utils import StrEnum
@@ -122,11 +121,9 @@ def set_timesteps(self, num_inference_steps: int, device: str | torch.device | N
)
self.num_inference_steps = num_inference_steps
- step_ratio = self.num_train_timesteps // self.num_inference_steps
- # creates integer timesteps by multiplying by ratio
- # casting to int to avoid issues when num_inference_step is power of 3
- timesteps = (np.arange(0, num_inference_steps) * step_ratio).round()[::-1].astype(np.int64)
- self.timesteps = torch.from_numpy(timesteps).to(device)
+ self.timesteps = (
+ torch.linspace(self.num_train_timesteps - 1, 0, self.num_inference_steps, device=device).round().long()
+ )
def _get_mean(self, timestep: int, x_0: torch.Tensor, x_t: torch.Tensor) -> torch.Tensor:
"""
From b93479c3ba4f3cdddc67ce52cd80b33b6d60b5ec Mon Sep 17 00:00:00 2001
From: Iyassou Shimels
Date: Thu, 13 Nov 2025 05:26:37 +0300
Subject: [PATCH 26/61] Correct H&E stain ordering heuristic in ExtractHEStains
(#8551)
### Description
I noticed when attempting to extract the hematoxylin stain from an H&E
stained image that the previous heuristic for ordering hematoxylin first
and eosin second incorrectly assumed hematoxylin has a higher red
channel value, when the opposite is typically true. I've corrected this
and implemented a more robust heuristic that compares red-blue ratios
instead, since:
- Hematoxylin (nuclear, blue): lower red/blue ratio
- Eosin (cytoplasm, pink): higher red/blue ratio
I've updated the tests to reflect the corrected stain ordering, and the
output now accurately reflects the documented behaviour (first column
hematoxylin, second eosin).
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [x] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [x] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
---------
Signed-off-by: Iyassou Shimels
Signed-off-by: Bruce Hashemian <3968947+bhashemian@users.noreply.github.com>
Co-authored-by: Bruce Hashemian <3968947+bhashemian@users.noreply.github.com>
Signed-off-by: jirka
---
monai/apps/pathology/transforms/stain/array.py | 7 ++++++-
.../transforms/test_pathology_he_stain.py | 16 ++++++++--------
.../transforms/test_pathology_he_stain_dict.py | 16 ++++++++--------
3 files changed, 22 insertions(+), 17 deletions(-)
diff --git a/monai/apps/pathology/transforms/stain/array.py b/monai/apps/pathology/transforms/stain/array.py
index 5df9ad7ef3..266f5a74b2 100644
--- a/monai/apps/pathology/transforms/stain/array.py
+++ b/monai/apps/pathology/transforms/stain/array.py
@@ -85,7 +85,12 @@ def _deconvolution_extract_stain(self, image: np.ndarray) -> np.ndarray:
v_max = eigvecs[:, 1:3].dot(np.array([(np.cos(max_phi), np.sin(max_phi))], dtype=np.float32).T)
# a heuristic to make the vector corresponding to hematoxylin first and the one corresponding to eosin second
- if v_min[0] > v_max[0]:
+ # Hematoxylin: high blue, lower red (low R/B ratio)
+ # Eosin: high red, lower blue (high R/B ratio)
+ eps = np.finfo(np.float32).eps
+ v_min_rb_ratio = v_min[0] / (v_min[2] + eps)
+ v_max_rb_ratio = v_max[0] / (v_max[2] + eps)
+ if v_min_rb_ratio < v_max_rb_ratio:
he = np.array((v_min[:, 0], v_max[:, 0]), dtype=np.float32).T
else:
he = np.array((v_max[:, 0], v_min[:, 0]), dtype=np.float32).T
diff --git a/tests/apps/pathology/transforms/test_pathology_he_stain.py b/tests/apps/pathology/transforms/test_pathology_he_stain.py
index 26941c6abb..e40a6921c7 100644
--- a/tests/apps/pathology/transforms/test_pathology_he_stain.py
+++ b/tests/apps/pathology/transforms/test_pathology_he_stain.py
@@ -48,7 +48,7 @@
# input pixels not uniformly filled, leading to two different stains extracted
EXTRACT_STAINS_TEST_CASE_5 = [
np.array([[[100, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]),
- np.array([[0.70710677, 0.18696113], [0.0, 0.0], [0.70710677, 0.98236734]]),
+ np.array([[0.18696113, 0.70710677], [0.0, 0.0], [0.98236734, 0.70710677]]),
]
# input pixels all transparent and below the beta absorbance threshold
@@ -68,7 +68,7 @@
NORMALIZE_STAINS_TEST_CASE_4 = [
{"target_he": np.full((3, 2), 1)},
np.array([[[100, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]),
- np.array([[[87, 87, 87], [33, 33, 33]], [[33, 33, 33], [33, 33, 33]], [[33, 33, 33], [33, 33, 33]]]),
+ np.array([[[31, 31, 31], [85, 85, 85]], [[85, 85, 85], [85, 85, 85]], [[85, 85, 85], [85, 85, 85]]]),
]
@@ -135,7 +135,7 @@ def test_result_value(self, image, expected_data):
[[0.18696113],[0],[0.98236734]] and
[[0.70710677],[0],[0.70710677]] respectively
- the resulting extracted stain should be
- [[0.70710677,0.18696113],[0,0],[0.70710677,0.98236734]]
+ [[0.18696113,0.70710677],[0,0],[0.98236734,0.70710677]]
"""
if image is None:
with self.assertRaises(TypeError):
@@ -206,17 +206,17 @@ def test_result_value(self, arguments, image, expected_data):
For test case 4:
- For this non-uniformly filled image, the stain extracted should be
- [[0.70710677,0.18696113],[0,0],[0.70710677,0.98236734]], as validated for the
+ [[0.18696113,0.70710677],[0,0],[0.98236734,0.70710677]], as validated for the
ExtractHEStains class. Solving the linear least squares problem (since
absorbance matrix = stain matrix * concentration matrix), we obtain the concentration
- matrix that should be [[-0.3101, 7.7508, 7.7508, 7.7508, 7.7508, 7.7508],
- [5.8022, 0, 0, 0, 0, 0]]
+ matrix that should be [[5.8022, 0, 0, 0, 0, 0],
+ [-0.3101, 7.7508, 7.7508, 7.7508, 7.7508, 7.7508]]
- Normalizing the concentration matrix, taking the matrix product of the
target stain matrix and the concentration matrix, using the inverse
Beer-Lambert transform to obtain the RGB image from the absorbance
image, and finally converting to uint8, we get that the stain normalized
- image should be [[[87, 87, 87], [33, 33, 33]], [[33, 33, 33], [33, 33, 33]],
- [[33, 33, 33], [33, 33, 33]]]
+ image should be [[[31, 31, 31], [85, 85, 85]], [[85, 85, 85], [85, 85, 85]],
+ [[85, 85, 85], [85, 85, 85]]]
"""
if image is None:
with self.assertRaises(TypeError):
diff --git a/tests/apps/pathology/transforms/test_pathology_he_stain_dict.py b/tests/apps/pathology/transforms/test_pathology_he_stain_dict.py
index 975dc4ffb8..973fe9075f 100644
--- a/tests/apps/pathology/transforms/test_pathology_he_stain_dict.py
+++ b/tests/apps/pathology/transforms/test_pathology_he_stain_dict.py
@@ -42,7 +42,7 @@
# input pixels not uniformly filled, leading to two different stains extracted
EXTRACT_STAINS_TEST_CASE_5 = [
np.array([[[100, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]),
- np.array([[0.70710677, 0.18696113], [0.0, 0.0], [0.70710677, 0.98236734]]),
+ np.array([[0.18696113, 0.70710677], [0.0, 0.0], [0.98236734, 0.70710677]]),
]
# input pixels all transparent and below the beta absorbance threshold
@@ -62,7 +62,7 @@
NORMALIZE_STAINS_TEST_CASE_4 = [
{"target_he": np.full((3, 2), 1)},
np.array([[[100, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]),
- np.array([[[87, 87, 87], [33, 33, 33]], [[33, 33, 33], [33, 33, 33]], [[33, 33, 33], [33, 33, 33]]]),
+ np.array([[[31, 31, 31], [85, 85, 85]], [[85, 85, 85], [85, 85, 85]], [[85, 85, 85], [85, 85, 85]]]),
]
@@ -129,7 +129,7 @@ def test_result_value(self, image, expected_data):
[[0.18696113],[0],[0.98236734]] and
[[0.70710677],[0],[0.70710677]] respectively
- the resulting extracted stain should be
- [[0.70710677,0.18696113],[0,0],[0.70710677,0.98236734]]
+ [[0.18696113,0.70710677],[0,0],[0.98236734,0.70710677]]
"""
key = "image"
if image is None:
@@ -200,17 +200,17 @@ def test_result_value(self, arguments, image, expected_data):
For test case 4:
- For this non-uniformly filled image, the stain extracted should be
- [[0.70710677,0.18696113],[0,0],[0.70710677,0.98236734]], as validated for the
+ [[0.18696113,0.70710677],[0,0],[0.98236734,0.70710677]], as validated for the
ExtractHEStains class. Solving the linear least squares problem (since
absorbance matrix = stain matrix * concentration matrix), we obtain the concentration
- matrix that should be [[-0.3101, 7.7508, 7.7508, 7.7508, 7.7508, 7.7508],
- [5.8022, 0, 0, 0, 0, 0]]
+ matrix that should be [[5.8022, 0, 0, 0, 0, 0],
+ [-0.3101, 7.7508, 7.7508, 7.7508, 7.7508, 7.7508]]
- Normalizing the concentration matrix, taking the matrix product of the
target stain matrix and the concentration matrix, using the inverse
Beer-Lambert transform to obtain the RGB image from the absorbance
image, and finally converting to uint8, we get that the stain normalized
- image should be [[[87, 87, 87], [33, 33, 33]], [[33, 33, 33], [33, 33, 33]],
- [[33, 33, 33], [33, 33, 33]]]
+ image should be [[[31, 31, 31], [85, 85, 85]], [[85, 85, 85], [85, 85, 85]],
+ [[85, 85, 85], [85, 85, 85]]]
"""
key = "image"
if image is None:
From 5dae9c79cddc4c35a238a6163ba098394749bb67 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=A1bio=20S=2E=20Ferreira?=
Date: Fri, 14 Nov 2025 19:18:43 +0000
Subject: [PATCH 27/61] feat: add activation checkpointing to unet (#8554)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
### Description
Introduces an optional `use_checkpointing` flag in the `UNet`
implementation. When enabled, intermediate activations in the
encoder–decoder blocks are recomputed during the backward pass instead
of being stored in memory.
- Implemented via a lightweight `_ActivationCheckpointWrapper` wrapper
around sub-blocks.
- Checkpointing is only applied during training to avoid overhead at
inference.
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Breaking change (fix or new feature that would cause existing
functionality to change).
- [x] New tests added to cover the changes.
- [x] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [x] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [x] In-line docstrings updated.
- [ ] Documentation updated, tested `make html` command in the `docs/`
folder.
---------
Signed-off-by: Fábio S. Ferreira
Signed-off-by: Fabio Ferreira
Co-authored-by: Fabio Ferreira
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: YunLiu <55491388+KumoLiu@users.noreply.github.com>
Signed-off-by: jirka
---
.../blocks/activation_checkpointing.py | 41 ++++
monai/networks/nets/unet.py | 28 ++-
tests/networks/nets/test_checkpointunet.py | 186 ++++++++++++++++++
3 files changed, 254 insertions(+), 1 deletion(-)
create mode 100644 monai/networks/blocks/activation_checkpointing.py
create mode 100644 tests/networks/nets/test_checkpointunet.py
diff --git a/monai/networks/blocks/activation_checkpointing.py b/monai/networks/blocks/activation_checkpointing.py
new file mode 100644
index 0000000000..283bcd19e1
--- /dev/null
+++ b/monai/networks/blocks/activation_checkpointing.py
@@ -0,0 +1,41 @@
+# Copyright (c) MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import annotations
+
+from typing import cast
+
+import torch
+import torch.nn as nn
+from torch.utils.checkpoint import checkpoint
+
+
+class ActivationCheckpointWrapper(nn.Module):
+ """Wrapper applying activation checkpointing to a module during training.
+
+ Args:
+ module: The module to wrap with activation checkpointing.
+ """
+
+ def __init__(self, module: nn.Module) -> None:
+ super().__init__()
+ self.module = module
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ """Forward pass with optional activation checkpointing.
+
+ Args:
+ x: Input tensor.
+
+ Returns:
+ Output tensor from the wrapped module.
+ """
+ return cast(torch.Tensor, checkpoint(self.module, x, use_reentrant=False))
diff --git a/monai/networks/nets/unet.py b/monai/networks/nets/unet.py
index eac0ddab39..a4995ef701 100644
--- a/monai/networks/nets/unet.py
+++ b/monai/networks/nets/unet.py
@@ -17,11 +17,12 @@
import torch
import torch.nn as nn
+from monai.networks.blocks.activation_checkpointing import ActivationCheckpointWrapper
from monai.networks.blocks.convolutions import Convolution, ResidualUnit
from monai.networks.layers.factories import Act, Norm
from monai.networks.layers.simplelayers import SkipConnection
-__all__ = ["UNet", "Unet"]
+__all__ = ["UNet", "Unet", "CheckpointUNet"]
class UNet(nn.Module):
@@ -298,4 +299,29 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
return x
+class CheckpointUNet(UNet):
+ """UNet variant that wraps internal connection blocks with activation checkpointing.
+
+ See `UNet` for constructor arguments. During training with gradients enabled,
+ intermediate activations inside encoder-decoder connections are recomputed in
+ the backward pass to reduce peak memory usage at the cost of extra compute.
+ """
+
+ def _get_connection_block(self, down_path: nn.Module, up_path: nn.Module, subblock: nn.Module) -> nn.Module:
+ """Returns connection block with activation checkpointing applied to all components.
+
+ Args:
+ down_path: encoding half of the layer (will be wrapped with checkpointing).
+ up_path: decoding half of the layer (will be wrapped with checkpointing).
+ subblock: block defining the next layer (will be wrapped with checkpointing).
+
+ Returns:
+ Connection block with all components wrapped for activation checkpointing.
+ """
+ subblock = ActivationCheckpointWrapper(subblock)
+ down_path = ActivationCheckpointWrapper(down_path)
+ up_path = ActivationCheckpointWrapper(up_path)
+ return super()._get_connection_block(down_path, up_path, subblock)
+
+
Unet = UNet
diff --git a/tests/networks/nets/test_checkpointunet.py b/tests/networks/nets/test_checkpointunet.py
new file mode 100644
index 0000000000..cb1cf44b6a
--- /dev/null
+++ b/tests/networks/nets/test_checkpointunet.py
@@ -0,0 +1,186 @@
+# Copyright (c) MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import annotations
+
+import unittest
+
+import torch
+from parameterized import parameterized
+
+from monai.networks import eval_mode
+from monai.networks.layers import Act, Norm
+from monai.networks.nets.unet import CheckpointUNet, UNet
+
+device = "cuda" if torch.cuda.is_available() else "cpu"
+
+TEST_CASE_0 = [ # single channel 2D, batch 16, no residual
+ {
+ "spatial_dims": 2,
+ "in_channels": 1,
+ "out_channels": 3,
+ "channels": (16, 32, 64),
+ "strides": (2, 2),
+ "num_res_units": 0,
+ },
+ (16, 1, 32, 32),
+ (16, 3, 32, 32),
+]
+
+TEST_CASE_1 = [ # single channel 2D, batch 16
+ {
+ "spatial_dims": 2,
+ "in_channels": 1,
+ "out_channels": 3,
+ "channels": (16, 32, 64),
+ "strides": (2, 2),
+ "num_res_units": 1,
+ },
+ (16, 1, 32, 32),
+ (16, 3, 32, 32),
+]
+
+TEST_CASE_2 = [ # single channel 3D, batch 16
+ {
+ "spatial_dims": 3,
+ "in_channels": 1,
+ "out_channels": 3,
+ "channels": (16, 32, 64),
+ "strides": (2, 2),
+ "num_res_units": 1,
+ },
+ (16, 1, 32, 24, 48),
+ (16, 3, 32, 24, 48),
+]
+
+TEST_CASE_3 = [ # 4-channel 3D, batch 16
+ {
+ "spatial_dims": 3,
+ "in_channels": 4,
+ "out_channels": 3,
+ "channels": (16, 32, 64),
+ "strides": (2, 2),
+ "num_res_units": 1,
+ },
+ (16, 4, 32, 64, 48),
+ (16, 3, 32, 64, 48),
+]
+
+TEST_CASE_4 = [ # 4-channel 3D, batch 16, batch normalization
+ {
+ "spatial_dims": 3,
+ "in_channels": 4,
+ "out_channels": 3,
+ "channels": (16, 32, 64),
+ "strides": (2, 2),
+ "num_res_units": 1,
+ "norm": Norm.BATCH,
+ },
+ (16, 4, 32, 64, 48),
+ (16, 3, 32, 64, 48),
+]
+
+TEST_CASE_5 = [ # 4-channel 3D, batch 16, LeakyReLU activation
+ {
+ "spatial_dims": 3,
+ "in_channels": 4,
+ "out_channels": 3,
+ "channels": (16, 32, 64),
+ "strides": (2, 2),
+ "num_res_units": 1,
+ "act": (Act.LEAKYRELU, {"negative_slope": 0.2}),
+ "adn_ordering": "NA",
+ },
+ (16, 4, 32, 64, 48),
+ (16, 3, 32, 64, 48),
+]
+
+TEST_CASE_6 = [ # 4-channel 3D, batch 16, LeakyReLU activation explicit
+ {
+ "spatial_dims": 3,
+ "in_channels": 4,
+ "out_channels": 3,
+ "channels": (16, 32, 64),
+ "strides": (2, 2),
+ "num_res_units": 1,
+ "act": (torch.nn.LeakyReLU, {"negative_slope": 0.2}),
+ },
+ (16, 4, 32, 64, 48),
+ (16, 3, 32, 64, 48),
+]
+
+CASES = [TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6]
+
+
+class TestCheckpointUNet(unittest.TestCase):
+ @parameterized.expand(CASES)
+ def test_shape(self, input_param, input_shape, expected_shape):
+ """Validate CheckpointUNet output shapes across configurations.
+
+ Args:
+ input_param: Dictionary of UNet constructor arguments.
+ input_shape: Tuple specifying input tensor dimensions.
+ expected_shape: Tuple specifying expected output tensor dimensions.
+ """
+ net = CheckpointUNet(**input_param).to(device)
+ with eval_mode(net):
+ result = net.forward(torch.randn(input_shape).to(device))
+ self.assertEqual(result.shape, expected_shape)
+
+ def test_checkpointing_equivalence_eval(self):
+ """Confirm eval parity when checkpointing is inactive."""
+ params = dict(
+ spatial_dims=2, in_channels=1, out_channels=2, channels=(8, 16, 32), strides=(2, 2), num_res_units=1
+ )
+
+ x = torch.randn(2, 1, 32, 32, device=device)
+
+ torch.manual_seed(42)
+ net_plain = UNet(**params).to(device)
+
+ torch.manual_seed(42)
+ net_ckpt = CheckpointUNet(**params).to(device)
+
+ # Both in eval mode disables checkpointing logic
+ with eval_mode(net_ckpt), eval_mode(net_plain):
+ y_ckpt = net_ckpt(x)
+ y_plain = net_plain(x)
+
+ # Check shape equality
+ self.assertEqual(y_ckpt.shape, y_plain.shape)
+
+ # Check numerical equivalence
+ self.assertTrue(
+ torch.allclose(y_ckpt, y_plain, atol=1e-6, rtol=1e-5),
+ f"Eval-mode outputs differ: max abs diff={torch.max(torch.abs(y_ckpt - y_plain)).item():.2e}",
+ )
+
+ def test_checkpointing_activates_training(self):
+ """Verify checkpointing recomputes activations during training."""
+ params = dict(
+ spatial_dims=2, in_channels=1, out_channels=1, channels=(8, 16, 32), strides=(2, 2), num_res_units=1
+ )
+
+ net = CheckpointUNet(**params).to(device)
+ net.train()
+
+ x = torch.randn(2, 1, 32, 32, device=device, requires_grad=True)
+ y = net(x)
+ loss = y.mean()
+ loss.backward()
+
+ # gradient flow check
+ grad_norm = sum(p.grad.abs().sum() for p in net.parameters() if p.grad is not None)
+ self.assertGreater(grad_norm.item(), 0.0)
+
+
+if __name__ == "__main__":
+ unittest.main()
From 3291f48b3b0d93e2c6c4d5aa76de037ef372a618 Mon Sep 17 00:00:00 2001
From: John Zielke
Date: Tue, 18 Nov 2025 01:29:11 -0500
Subject: [PATCH 28/61] Fix index using tuple for image cropping operation
(#8633)
I'm getting a warning at this line:
"Using a non-tuple sequence for multidimensional indexing is deprecated
and will be changed in pytorch 2.9; use x instead of x. In pytorch 2.9
this will be interpreted as _tensor.py:1654
tensor index, x, which will result either in an error or a different
result".
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Breaking change (fix or new feature that would cause existing
functionality to change).
- [ ] New tests added to cover the changes.
- [ ] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [ ] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [ ] In-line docstrings updated.
- [ ] Documentation updated, tested `make html` command in the `docs/`
folder.
Signed-off-by: John Zielke
Signed-off-by: jirka
---
monai/transforms/croppad/functional.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/monai/transforms/croppad/functional.py b/monai/transforms/croppad/functional.py
index 361ec48dcd..653db43bc5 100644
--- a/monai/transforms/croppad/functional.py
+++ b/monai/transforms/croppad/functional.py
@@ -144,7 +144,7 @@ def crop_or_pad_nd(img: torch.Tensor, translation_mat, spatial_size: tuple[int,
_mode = _convert_pt_pad_mode(mode)
img = pad_nd(img, to_pad, mode=_mode, **kwargs)
if do_crop:
- img = img[to_crop]
+ img = img[tuple(to_crop)]
return img
From e6f2e1fb3e4436e256eba2c12b1decec54706897 Mon Sep 17 00:00:00 2001
From: "Mason C. Cleveland" <104479423+mccle@users.noreply.github.com>
Date: Wed, 19 Nov 2025 06:38:35 -0500
Subject: [PATCH 29/61] Fix #8599: Add track_meta and weights_only arguments to
PersistentDataset for MetaTensor support. (#8628)
Fixes #8599.
### Description
`PersistentDataset` currently casts all `MetaTensor` objects to
`torch.Tensor` objects and forces the use of `torch.load` with
`weights_only=True`. This makes it impossible to save or load metadata
to cached files, which may be necessary for accurate post-transform
operations.
To address this, this PR introduces the `track_meta` and `weights_only`
arguments directly to `PersistentDataset`. They are internally passed to
`convert_to_tensor` and `torch.load`, respectively. A `ValueError` is
raised when `track_meta=True` and `weights_only=True`, since
`MetaTensor` objects cannot be loaded with `weights_only=True` and the
cached files would be continually deleted and rewritten.
These changes restore the ability to cache `MetaTensor` objects by
allowing explicit control over data casting and `torch.load` behavior.
The default values of `track_meta=False` and `weights_only=True` will
preserve the current behavior of `PersistentDataset`.
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Breaking change (fix or new feature that would cause existing
functionality to change).
- [x] New tests added to cover the changes.
- [ ] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [x] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [x] In-line docstrings updated.
- [x] Documentation updated, tested `make html` command in the `docs/`
folder.
---------
Signed-off-by: Mason Cleveland
Signed-off-by: Mason C. Cleveland <104479423+mccle@users.noreply.github.com>
Signed-off-by: mccle
Co-authored-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
Co-authored-by: YunLiu <55491388+KumoLiu@users.noreply.github.com>
Signed-off-by: jirka
---
monai/data/dataset.py | 25 +++++++++++++++---
tests/data/test_persistentdataset.py | 38 +++++++++++++++++++++++++---
2 files changed, 57 insertions(+), 6 deletions(-)
diff --git a/monai/data/dataset.py b/monai/data/dataset.py
index d63ff32293..066cec41b7 100644
--- a/monai/data/dataset.py
+++ b/monai/data/dataset.py
@@ -230,6 +230,8 @@ def __init__(
pickle_protocol: int = DEFAULT_PROTOCOL,
hash_transform: Callable[..., bytes] | None = None,
reset_ops_id: bool = True,
+ track_meta: bool = False,
+ weights_only: bool = True,
) -> None:
"""
Args:
@@ -264,7 +266,17 @@ def __init__(
When this is enabled, the traced transform instance IDs will be removed from the cached MetaTensors.
This is useful for skipping the transform instance checks when inverting applied operations
using the cached content and with re-created transform instances.
-
+ track_meta: whether to track the meta information, if `True`, will convert to `MetaTensor`.
+ default to `False`. Cannot be used with `weights_only=True`.
+ weights_only: keyword argument passed to `torch.load` when reading cached files.
+ default to `True`. When set to `True`, `torch.load` restricts loading to tensors and
+ other safe objects. Setting this to `False` is required for loading `MetaTensor`
+ objects saved with `track_meta=True`, however this creates the possibility of remote
+ code execution through `torch.load` so be aware of the security implications of doing so.
+
+ Raises:
+ ValueError: When both `track_meta=True` and `weights_only=True`, since this combination
+ prevents cached MetaTensors from being reloaded and causes perpetual cache regeneration.
"""
super().__init__(data=data, transform=transform)
self.cache_dir = Path(cache_dir) if cache_dir is not None else None
@@ -280,6 +292,13 @@ def __init__(
if hash_transform is not None:
self.set_transform_hash(hash_transform)
self.reset_ops_id = reset_ops_id
+ if track_meta and weights_only:
+ raise ValueError(
+ "Invalid argument combination: `track_meta=True` cannot be used with `weights_only=True`. "
+ "To cache and reload MetaTensors, set `track_meta=True` and `weights_only=False`."
+ )
+ self.track_meta = track_meta
+ self.weights_only = weights_only
def set_transform_hash(self, hash_xform_func: Callable[..., bytes]):
"""Get hashable transforms, and then hash them. Hashable transforms
@@ -377,7 +396,7 @@ def _cachecheck(self, item_transformed):
if hashfile is not None and hashfile.is_file(): # cache hit
try:
- return torch.load(hashfile, weights_only=True)
+ return torch.load(hashfile, weights_only=self.weights_only)
except PermissionError as e:
if sys.platform != "win32":
raise e
@@ -398,7 +417,7 @@ def _cachecheck(self, item_transformed):
with tempfile.TemporaryDirectory() as tmpdirname:
temp_hash_file = Path(tmpdirname) / hashfile.name
torch.save(
- obj=convert_to_tensor(_item_transformed, convert_numeric=False),
+ obj=convert_to_tensor(_item_transformed, convert_numeric=False, track_meta=self.track_meta),
f=temp_hash_file,
pickle_module=look_up_option(self.pickle_module, SUPPORTED_PICKLE_MOD),
pickle_protocol=self.pickle_protocol,
diff --git a/tests/data/test_persistentdataset.py b/tests/data/test_persistentdataset.py
index 7bf1245592..ca62cdb184 100644
--- a/tests/data/test_persistentdataset.py
+++ b/tests/data/test_persistentdataset.py
@@ -11,6 +11,7 @@
from __future__ import annotations
+import contextlib
import os
import tempfile
import unittest
@@ -20,7 +21,7 @@
import torch
from parameterized import parameterized
-from monai.data import PersistentDataset, json_hashing
+from monai.data import MetaTensor, PersistentDataset, json_hashing
from monai.transforms import Compose, Flip, Identity, LoadImaged, SimulateDelayd, Transform
TEST_CASE_1 = [
@@ -43,9 +44,16 @@
TEST_CASE_3 = [None, (128, 128, 128)]
+TEST_CASE_4 = [True, False, False, MetaTensor]
+
+TEST_CASE_5 = [True, True, True, None]
+
+TEST_CASE_6 = [False, False, False, torch.Tensor]
+
+TEST_CASE_7 = [False, True, False, torch.Tensor]
-class _InplaceXform(Transform):
+class _InplaceXform(Transform):
def __call__(self, data):
if data:
data[0] = data[0] + np.pi
@@ -55,7 +63,6 @@ def __call__(self, data):
class TestDataset(unittest.TestCase):
-
def test_cache(self):
"""testing no inplace change to the hashed item"""
items = [[list(range(i))] for i in range(5)]
@@ -168,6 +175,31 @@ def test_different_transforms(self):
l2 = ((im1 - im2) ** 2).sum() ** 0.5
self.assertGreater(l2, 1)
+ @parameterized.expand([TEST_CASE_4, TEST_CASE_5, TEST_CASE_6, TEST_CASE_7])
+ def test_track_meta_and_weights_only(self, track_meta, weights_only, expected_error, expected_type):
+ """
+ Ensure expected behavior for all combinations of `track_meta` and `weights_only`.
+ """
+ test_image = nib.Nifti1Image(np.random.randint(0, 2, size=[128, 128, 128]).astype(float), np.eye(4))
+ with tempfile.TemporaryDirectory() as tempdir:
+ nib.save(test_image, os.path.join(tempdir, "test_image.nii.gz"))
+ test_data = [{"image": os.path.join(tempdir, "test_image.nii.gz")}]
+ transform = Compose([LoadImaged(keys=["image"])])
+ cache_dir = os.path.join(os.path.join(tempdir, "cache"), "data")
+
+ cm = self.assertRaises(ValueError) if expected_error else contextlib.nullcontext()
+ with cm:
+ test_dataset = PersistentDataset(
+ data=test_data,
+ transform=transform,
+ cache_dir=cache_dir,
+ track_meta=track_meta,
+ weights_only=weights_only,
+ )
+
+ im = test_dataset[0]["image"]
+ self.assertIsInstance(im, expected_type)
+
if __name__ == "__main__":
unittest.main()
From 6cf36b6616aede29ae606e140559c2139dd4ae13 Mon Sep 17 00:00:00 2001
From: YunLiu <55491388+KumoLiu@users.noreply.github.com>
Date: Fri, 21 Nov 2025 11:18:12 +0800
Subject: [PATCH 30/61] Update documentation links (#8637)
Fixes #8598 .
### Description
Update links to:
Website: https://project-monai.github.io/
Core Docs: https://monai.readthedocs.io/en/stable/
Label Docs: https://monai.readthedocs.io/projects/label/en/latest/
Deploy SDK Docs:
https://monai.readthedocs.io/projects/monai-deploy-app-sdk/en/stable/
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Breaking change (fix or new feature that would cause existing
functionality to change).
- [ ] New tests added to cover the changes.
- [ ] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [ ] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [ ] In-line docstrings updated.
- [ ] Documentation updated, tested `make html` command in the `docs/`
folder.
Signed-off-by: Yun Liu
Signed-off-by: jirka
---
CITATION.cff | 2 +-
CONTRIBUTING.md | 2 +-
README.md | 14 +++++++-------
docs/source/applications.md | 6 +++---
docs/source/config_syntax.md | 2 +-
docs/source/index.rst | 8 ++++----
docs/source/modules.md | 18 +++++++++---------
docs/source/whatsnew_0_6.md | 4 ++--
docs/source/whatsnew_0_8.md | 2 +-
docs/source/whatsnew_0_9.md | 2 +-
docs/source/whatsnew_1_0.md | 8 ++++----
docs/source/whatsnew_1_1.md | 2 +-
docs/source/whatsnew_1_2.md | 2 +-
monai/apps/auto3dseg/auto_runner.py | 6 +++---
monai/apps/auto3dseg/ensemble_builder.py | 6 +++---
.../maisi/networks/autoencoderkl_maisi.py | 2 +-
monai/bundle/workflows.py | 6 +++---
monai/config/deviceconfig.py | 2 +-
monai/networks/nets/segresnet_ds.py | 2 +-
monai/transforms/io/array.py | 4 ++--
monai/transforms/spatial/array.py | 8 ++++----
monai/utils/module.py | 4 ++--
monai/utils/tf32.py | 2 +-
setup.cfg | 4 ++--
tests/profile_subclass/README.md | 2 +-
25 files changed, 60 insertions(+), 60 deletions(-)
diff --git a/CITATION.cff b/CITATION.cff
index c406f3db1e..c51c8d0ea6 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -14,7 +14,7 @@ identifiers:
value: "10.5281/zenodo.4323058"
license: "Apache-2.0"
repository-code: "https://github.com/Project-MONAI/MONAI"
-url: "https://monai.io"
+url: "https://project-monai.github.io/"
cff-version: "1.2.0"
message: "If you use this software, please cite it using these metadata."
preferred-citation:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e87804a3e3..df7f5e336c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -198,7 +198,7 @@ The first line of the comment must be `/black` so that it will be interpreted by
#### Adding new optional dependencies
In addition to the minimal requirements of PyTorch and Numpy, MONAI's core modules are built optionally based on 3rd-party packages.
-The current set of dependencies is listed in [installing dependencies](https://docs.monai.io/en/stable/installation.html#installing-the-recommended-dependencies).
+The current set of dependencies is listed in [installing dependencies](https://monai.readthedocs.io/en/stable/installation.html#installing-the-recommended-dependencies).
To allow for flexible integration of MONAI with other systems and environments,
the optional dependency APIs are always invoked lazily. For example,
diff --git a/README.md b/README.md
index e4bcdbb815..c327846d8e 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
[](https://github.com/Project-MONAI/MONAI/actions/workflows/pythonapp.yml)
[](https://github.com/Project-MONAI/MONAI/actions?query=branch%3Adev)
-[](https://docs.monai.io/en/latest/)
+[](https://monai.readthedocs.io/en/latest/)
[](https://codecov.io/gh/Project-MONAI/MONAI)
[](https://piptrends.com/package/monai)
@@ -26,7 +26,7 @@ Its ambitions are as follows:
## Features
-> _Please see [the technical highlights](https://docs.monai.io/en/latest/highlights.html) and [What's New](https://docs.monai.io/en/latest/whatsnew.html) of the milestone releases._
+> _Please see [the technical highlights](https://monai.readthedocs.io/en/latest/highlights.html) and [What's New](https://monai.readthedocs.io/en/latest/whatsnew.html) of the milestone releases._
- flexible pre-processing for multi-dimensional medical imaging data;
- compositional & portable APIs for ease of integration in existing workflows;
@@ -51,7 +51,7 @@ To install [the current release](https://pypi.org/project/monai/), you can simpl
pip install monai
```
-Please refer to [the installation guide](https://docs.monai.io/en/latest/installation.html) for other installation options.
+Please refer to [the installation guide](https://monai.readthedocs.io/en/latest/installation.html) for other installation options.
## Getting Started
@@ -68,7 +68,7 @@ If you have used MONAI in your research, please cite us! The citation can be exp
## Model Zoo
[The MONAI Model Zoo](https://github.com/Project-MONAI/model-zoo) is a place for researchers and data scientists to share the latest and great models from the community.
-Utilizing [the MONAI Bundle format](https://docs.monai.io/en/latest/bundle_intro.html) makes it easy to [get started](https://github.com/Project-MONAI/tutorials/tree/main/model_zoo) building workflows with MONAI.
+Utilizing [the MONAI Bundle format](https://monai.readthedocs.io/en/latest/bundle_intro.html) makes it easy to [get started](https://github.com/Project-MONAI/tutorials/tree/main/model_zoo) building workflows with MONAI.
## Contributing
@@ -82,9 +82,9 @@ Ask and answer questions over on [MONAI's GitHub Discussions tab](https://github
## Links
-- Website:
-- API documentation (milestone):
-- API documentation (latest dev):
+- Website:
+- API documentation (milestone):
+- API documentation (latest dev):
- Code:
- Project tracker:
- Issue tracker:
diff --git a/docs/source/applications.md b/docs/source/applications.md
index c77cb4065c..44fb9bbf14 100644
--- a/docs/source/applications.md
+++ b/docs/source/applications.md
@@ -1,20 +1,20 @@
# Research and Application Highlights
### COPLE-Net for COVID-19 Pneumonia Lesion Segmentation
-[A reimplementation](https://monai.io/research/coplenet-pneumonia-lesion-segmentation) of the COPLE-Net originally proposed by:
+[A reimplementation](https://project-monai.github.io/research/coplenet-pneumonia-lesion-segmentation.html) of the COPLE-Net originally proposed by:
G. Wang, X. Liu, C. Li, Z. Xu, J. Ruan, H. Zhu, T. Meng, K. Li, N. Huang, S. Zhang. (2020) "A Noise-robust Framework for Automatic Segmentation of COVID-19 Pneumonia Lesions from CT Images." IEEE Transactions on Medical Imaging. 2020. [DOI: 10.1109/TMI.2020.3000314](https://doi.org/10.1109/TMI.2020.3000314)

### LAMP: Large Deep Nets with Automated Model Parallelism for Image Segmentation
-[A reimplementation](https://monai.io/research/lamp-automated-model-parallelism) of the LAMP system originally proposed by:
+[A reimplementation](https://project-monai.github.io/research/lamp-automated-model-parallelism.html) of the LAMP system originally proposed by:
Wentao Zhu, Can Zhao, Wenqi Li, Holger Roth, Ziyue Xu, and Daguang Xu (2020) "LAMP: Large Deep Nets with Automated Model Parallelism for Image Segmentation." MICCAI 2020 (Early Accept, paper link: https://arxiv.org/abs/2006.12575)

### DiNTS: Differentiable Neural Network Topology Search for 3D Medical Image Segmentation
-MONAI integrated the `DiNTS` module to support more flexible topologies and joint two-level search. It provides a topology guaranteed discretization algorithm and a discretization aware topology loss for the search stage to minimize the discretization gap, and a cost usage aware search method which can search 3D networks with different GPU memory requirements. For more details, please check the [DiNTS tutorial](https://monai.io/research/dints.html).
+MONAI integrated the `DiNTS` module to support more flexible topologies and joint two-level search. It provides a topology guaranteed discretization algorithm and a discretization aware topology loss for the search stage to minimize the discretization gap, and a cost usage aware search method which can search 3D networks with different GPU memory requirements. For more details, please check the [DiNTS tutorial](https://project-monai.github.io/research/dints.html).

diff --git a/docs/source/config_syntax.md b/docs/source/config_syntax.md
index 742841acca..4b24415d2f 100644
--- a/docs/source/config_syntax.md
+++ b/docs/source/config_syntax.md
@@ -68,7 +68,7 @@ or additionally, tune the input parameters then instantiate the component:
BasicUNet features: (32, 32, 32, 64, 64, 64).
```
-For more details on the `ConfigParser` API, please see [`monai.bundle.ConfigParser`](https://docs.monai.io/en/latest/bundle.html#config-parser).
+For more details on the `ConfigParser` API, please see [`monai.bundle.ConfigParser`](https://monai.readthedocs.io/en/latest/bundle.html#config-parser).
## Syntax examples explained
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 85adee7e44..4279e522d3 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -85,15 +85,15 @@ Model Zoo
---------
`The MONAI Model Zoo `_ is a place for researchers and data scientists to share the latest and great models from the community.
-Utilizing `the MONAI Bundle format `_ makes it easy to `get started `_ building workflows with MONAI.
+Utilizing `the MONAI Bundle format `_ makes it easy to `get started `_ building workflows with MONAI.
Links
-----
-- Website: https://monai.io/
-- API documentation (milestone): https://docs.monai.io/
-- API documentation (latest dev): https://docs.monai.io/en/latest/
+- Website: https://project-monai.github.io/
+- API documentation (milestone): https://monai.readthedocs.io/
+- API documentation (latest dev): https://monai.readthedocs.io/en/latest/
- Code: https://github.com/Project-MONAI/MONAI
- Project tracker: https://github.com/Project-MONAI/MONAI/projects
- Issue tracker: https://github.com/Project-MONAI/MONAI/issues
diff --git a/docs/source/modules.md b/docs/source/modules.md
index ea8362083c..b2e95658bf 100644
--- a/docs/source/modules.md
+++ b/docs/source/modules.md
@@ -240,7 +240,7 @@ users and programs to understand how the model is used and for what purpose. A b
single network as a pickled state dictionary plus optionally a Torchscript object and/or an ONNX object. Additional JSON
files are included to store metadata about the model, information for constructing training, inference, and
post-processing transform sequences, plain-text description, legal information, and other data the model creator wishes
-to include. More details are available at [bundle specification](https://docs.monai.io/en/latest/mb_specification.html).
+to include. More details are available at [bundle specification](https://monai.readthedocs.io/en/latest/mb_specification.html).
The key benefits of bundle are to define the model package and support building Python-based workflows via structured configurations:
- Self-contained model package include all the necessary information.
@@ -262,26 +262,26 @@ A typical bundle example can include:
┣━ *README.md
┗━ *license.txt
```
-Details about the bundle config definition and syntax & examples are at [config syntax](https://docs.monai.io/en/latest/config_syntax.html).
+Details about the bundle config definition and syntax & examples are at [config syntax](https://monai.readthedocs.io/en/latest/config_syntax.html).
A step-by-step [get started](https://github.com/Project-MONAI/tutorials/blob/main/bundle/README.md) tutorial notebook can help users quickly set up a bundle. [[bundle examples](https://github.com/Project-MONAI/tutorials/tree/main/bundle), [model-zoo](https://github.com/Project-MONAI/model-zoo)]
## Federated Learning

-Using the MONAI bundle configurations, we can use MONAI's [`MonaiAlgo`](https://docs.monai.io/en/latest/fl.html#monai.fl.client.MonaiAlgo)
-class, an implementation of the abstract [`ClientAlgo`](https://docs.monai.io/en/latest/fl.html#clientalgo) class for federated learning (FL),
+Using the MONAI bundle configurations, we can use MONAI's [`MonaiAlgo`](https://monai.readthedocs.io/en/latest/fl.html#monai.fl.client.MonaiAlgo)
+class, an implementation of the abstract [`ClientAlgo`](https://monai.readthedocs.io/en/latest/fl.html#clientalgo) class for federated learning (FL),
to execute bundles from the [MONAI model zoo](https://github.com/Project-MONAI/model-zoo).
-Note that [`ClientAlgo`](https://docs.monai.io/en/latest/fl.html#clientalgo) is provided as an abstract base class for
+Note that [`ClientAlgo`](https://monai.readthedocs.io/en/latest/fl.html#clientalgo) is provided as an abstract base class for
defining an algorithm to be run on any federated learning platform.
-[`MonaiAlgo`](https://docs.monai.io/en/latest/fl.html#monai.fl.client.MonaiAlgo) implements the main functionalities needed
+[`MonaiAlgo`](https://monai.readthedocs.io/en/latest/fl.html#monai.fl.client.MonaiAlgo) implements the main functionalities needed
to run federated learning experiments, namely `train()`, `get_weights()`, and `evaluate()`, that can be run using single- or multi-GPU training.
On top, it provides implementations for life-cycle management of the component such as `initialize()`, `abort()`, and `finalize()`.
The MONAI FL client also allows computing summary data statistics (e.g., intensity histograms) on the datasets defined in the bundle configs
-using the [`MonaiAlgoStats`](https://docs.monai.io/en/latest/fl.html#monai.fl.client.MonaiAlgoStats) class.
+using the [`MonaiAlgoStats`](https://monai.readthedocs.io/en/latest/fl.html#monai.fl.client.MonaiAlgoStats) class.
These statistics can be shared and visualized on the FL server.
[NVIDIA FLARE](https://github.com/NVIDIA/NVFlare), the federated learning platform developed by NVIDIA, has already built [the integration piece](https://github.com/NVIDIA/NVFlare/tree/2.2/integration/monai)
-with [`ClientAlgo`](https://docs.monai.io/en/latest/fl.html#clientalgo) to allow easy experimentation with MONAI bundles within their federated environment.
+with [`ClientAlgo`](https://monai.readthedocs.io/en/latest/fl.html#clientalgo) to allow easy experimentation with MONAI bundles within their federated environment.
Our [[federated learning tutorials]](https://github.com/Project-MONAI/tutorials/tree/main/federated_learning/nvflare) shows
examples of single- & multi-GPU training and federated statistics workflows.
@@ -289,7 +289,7 @@ examples of single- & multi-GPU training and federated statistics workflows.

-[Auto3DSeg](https://monai.io/apps/auto3dseg.html) is a comprehensive solution for large-scale 3D medical image segmentation.
+[Auto3DSeg](https://project-monai.github.io/apps/auto3dseg.html) is a comprehensive solution for large-scale 3D medical image segmentation.
It leverages the latest advances in MONAI
and GPUs to efficiently develop and deploy algorithms with state-of-the-art performance.
It first analyzes the global information such as intensity, dimensionality, and resolution of the dataset,
diff --git a/docs/source/whatsnew_0_6.md b/docs/source/whatsnew_0_6.md
index 8df0503142..0efe9847cb 100644
--- a/docs/source/whatsnew_0_6.md
+++ b/docs/source/whatsnew_0_6.md
@@ -42,7 +42,7 @@ The following illustrates target body organs that are segmentation in this tutor

Please visit UNETR repository for more details:
-https://monai.io/research/unetr-btcv-multi-organ-segmentation
+https://project-monai.github.io/research/unetr-btcv-multi-organ-segmentation
## Pythonic APIs to load the pretrained models from Clara Train MMARs
[The MMAR (Medical Model ARchive)](https://docs.nvidia.com/clara/clara-train-sdk/pt/mmar.html)
@@ -93,4 +93,4 @@ MONAI Label enables application developers to build labeling apps in a serverles
where custom labeling apps are exposed as a service through the MONAI Label Server.
Please visit MONAILabel documentation website for details:
-https://docs.monai.io/projects/label/en/latest/
+https://monai.readthedocs.io/projects/label/en/latest/
diff --git a/docs/source/whatsnew_0_8.md b/docs/source/whatsnew_0_8.md
index 3eb4bea167..ac63c8dbd0 100644
--- a/docs/source/whatsnew_0_8.md
+++ b/docs/source/whatsnew_0_8.md
@@ -15,7 +15,7 @@ It provides a topology guaranteed discretization algorithm and a
discretization-aware topology loss for the search stage to minimize the
discretization gap. The module is memory usage aware and is able to search 3D
networks with different GPU memory requirements. For more details, please check out the
-[DiNTS tutorial](https://monai.io/research/dints.html).
+[DiNTS tutorial](https://project-monai.github.io/research/dints.html).

diff --git a/docs/source/whatsnew_0_9.md b/docs/source/whatsnew_0_9.md
index 357dc01b35..4b884ebb78 100644
--- a/docs/source/whatsnew_0_9.md
+++ b/docs/source/whatsnew_0_9.md
@@ -7,7 +7,7 @@
- MetaTensor API preview
## MONAI Bundle
-MONAI Bundle format defines portable described of deep learning models ([docs](https://docs.monai.io/en/latest/bundle_intro.html)).
+MONAI Bundle format defines portable described of deep learning models ([docs](https://monai.readthedocs.io/en/latest/bundle_intro.html)).
A bundle includes the critical information necessary during a model development life cycle,
and allows users and programs to understand the purpose and usage of the models.
The key benefits of Bundle and the `monai.bundle` APIs are:
diff --git a/docs/source/whatsnew_1_0.md b/docs/source/whatsnew_1_0.md
index 7e347780bf..91ce13351c 100644
--- a/docs/source/whatsnew_1_0.md
+++ b/docs/source/whatsnew_1_0.md
@@ -17,7 +17,7 @@ For more details about how to use the models, please see [the tutorials](https:/
## Auto3DSeg

-[Auto3DSeg](https://monai.io/apps/auto3dseg.html) is a comprehensive solution for large-scale 3D medical image segmentation.
+[Auto3DSeg](https://project-monai.github.io/apps/auto3dseg.html) is a comprehensive solution for large-scale 3D medical image segmentation.
It leverages the latest advances in MONAI
and GPUs to efficiently develop and deploy algorithms with state-of-the-art performance.
It first analyzes the global information such as intensity, dimensionality, and resolution of the dataset,
@@ -35,7 +35,7 @@ MONAI now includes the federated learning (FL) client algorithm APIs that are ex
for defining an algorithm to be run on any federated learning platform.
[NVIDIA FLARE](https://github.com/NVIDIA/NVFlare), the federated learning platform developed by [NVIDIA](https://www.nvidia.com/en-us/),
has already built [the integration piece](https://github.com/NVIDIA/NVFlare/tree/dev/integration/monai) with these new APIs.
-With [the new federated learning APIs](https://docs.monai.io/en/latest/fl.html), MONAI bundles can seamlessly be extended to a federated paradigm
+With [the new federated learning APIs](https://monai.readthedocs.io/en/latest/fl.html), MONAI bundles can seamlessly be extended to a federated paradigm
and executed using single- or multi-GPU training.
The MONAI FL client also allows computing summary data statistics (e.g., intensity histograms) on the datasets defined in the bundle configs.
These can be shared and visualized on the FL server, for example, using NVIDIA FLARE's federated statistics operators,
@@ -60,8 +60,8 @@ examples](https://github.com/Project-MONAI/tutorials/tree/main/pathology).

This release includes initial components for various popular accelerated MRI reconstruction workflows.
-Many of them are general-purpose tools, for example the [`SSIMLoss`](https://docs.monai.io/en/latest/losses.html?highlight=ssimloss#ssimloss) function.
-Some new functionalities are task-specific, for example [`FastMRIReader`](https://docs.monai.io/en/latest/data.html?highlight=fastmri#monai.apps.reconstruction.fastmri_reader.FastMRIReader).
+Many of them are general-purpose tools, for example the [`SSIMLoss`](https://monai.readthedocs.io/en/latest/losses.html?highlight=ssimloss#ssimloss) function.
+Some new functionalities are task-specific, for example [`FastMRIReader`](https://monai.readthedocs.io/en/latest/data.html?highlight=fastmri#monai.apps.reconstruction.fastmri_reader.FastMRIReader).
For more details, please see [this tutorial](https://github.com/Project-MONAI/tutorials/tree/main/reconstruction/MRI_reconstruction/unet_demo) for using a baseline model for this task,
and [this tutorial](https://github.com/Project-MONAI/tutorials/tree/main/reconstruction/MRI_reconstruction/varnet_demo) for using a state-of-the-art model.
diff --git a/docs/source/whatsnew_1_1.md b/docs/source/whatsnew_1_1.md
index 71e1951d64..b4b2f4026e 100644
--- a/docs/source/whatsnew_1_1.md
+++ b/docs/source/whatsnew_1_1.md
@@ -52,7 +52,7 @@ data in sliding-window inference. For more details about how to enable it, pleas
## New models in MONAI Model Zoo
-New pretrained models are being created and released [in the Model Zoo](https://monai.io/model-zoo.html).
+New pretrained models are being created and released [in the Model Zoo](https://project-monai.github.io/model-zoo.html).
Notably,
- The `mednist_reg` model demonstrates how to build image registration workflows in MONAI bundle
diff --git a/docs/source/whatsnew_1_2.md b/docs/source/whatsnew_1_2.md
index 618eac95ec..a5ea7d3165 100644
--- a/docs/source/whatsnew_1_2.md
+++ b/docs/source/whatsnew_1_2.md
@@ -73,5 +73,5 @@ cropping transforms into a single operation. This allows MONAI to reduce the num
Lazy Resampling pipelines can use a mixture of MONAI and non-MONAI transforms, so
should work with almost all existing pipelines simply by setting `lazy=True`
on MONAI `Compose` instances. See the
-[Lazy Resampling topic](https://docs.monai.io/en/stable/lazy_resampling.html)
+[Lazy Resampling topic](https://monai.readthedocs.io/en/stable/lazy_resampling.html)
in the documentation for more details.
diff --git a/monai/apps/auto3dseg/auto_runner.py b/monai/apps/auto3dseg/auto_runner.py
index 28ba2a88f9..d06effcd1a 100644
--- a/monai/apps/auto3dseg/auto_runner.py
+++ b/monai/apps/auto3dseg/auto_runner.py
@@ -87,7 +87,7 @@ class AutoRunner:
tracking Server; MLflow runs will be recorded locally in algorithms' model folder if the value is None.
mlflow_experiment_name: the name of the experiment in MLflow server.
kwargs: image writing parameters for the ensemble inference. The kwargs format follows the SaveImage
- transform. For more information, check https://docs.monai.io/en/stable/transforms.html#saveimage.
+ transform. For more information, check https://monai.readthedocs.io/en/stable/transforms.html#saveimage.
Examples:
@@ -621,7 +621,7 @@ def set_image_save_transform(self, **kwargs: Any) -> AutoRunner:
Args:
kwargs: image writing parameters for the ensemble inference. The kwargs format follows SaveImage
- transform. For more information, check https://docs.monai.io/en/stable/transforms.html#saveimage.
+ transform. For more information, check https://monai.readthedocs.io/en/stable/transforms.html#saveimage.
"""
@@ -631,7 +631,7 @@ def set_image_save_transform(self, **kwargs: Any) -> AutoRunner:
else:
raise ValueError(
f"{extra_args} are not supported in monai.transforms.SaveImage,"
- "Check https://docs.monai.io/en/stable/transforms.html#saveimage for more information."
+ "Check https://monai.readthedocs.io/en/stable/transforms.html#saveimage for more information."
)
return self
diff --git a/monai/apps/auto3dseg/ensemble_builder.py b/monai/apps/auto3dseg/ensemble_builder.py
index b2bea806de..e574baf7c8 100644
--- a/monai/apps/auto3dseg/ensemble_builder.py
+++ b/monai/apps/auto3dseg/ensemble_builder.py
@@ -477,7 +477,7 @@ def _pop_kwargs_to_get_image_save_transform(self, **kwargs):
Args:
kwargs: image writing parameters for the ensemble inference. The kwargs format follows SaveImage
- transform. For more information, check https://docs.monai.io/en/stable/transforms.html#saveimage .
+ transform. For more information, check https://monai.readthedocs.io/en/stable/transforms.html#saveimage .
Returns:
save_image: a dictionary that can be used to instantiate a SaveImage class in ConfigParser.
@@ -525,7 +525,7 @@ def set_image_save_transform(self, **kwargs: Any) -> None:
Args:
kwargs: image writing parameters for the ensemble inference. The kwargs format follows SaveImage
- transform. For more information, check https://docs.monai.io/en/stable/transforms.html#saveimage .
+ transform. For more information, check https://monai.readthedocs.io/en/stable/transforms.html#saveimage .
"""
are_all_args_present, extra_args = check_kwargs_exist_in_class_init(SaveImage, kwargs)
@@ -534,7 +534,7 @@ def set_image_save_transform(self, **kwargs: Any) -> None:
else:
raise ValueError(
f"{extra_args} are not supported in monai.transforms.SaveImage,"
- "Check https://docs.monai.io/en/stable/transforms.html#saveimage for more information."
+ "Check https://monai.readthedocs.io/en/stable/transforms.html#saveimage for more information."
)
def set_num_fold(self, num_fold: int = 5) -> None:
diff --git a/monai/apps/generation/maisi/networks/autoencoderkl_maisi.py b/monai/apps/generation/maisi/networks/autoencoderkl_maisi.py
index 53985cc174..16697204c5 100644
--- a/monai/apps/generation/maisi/networks/autoencoderkl_maisi.py
+++ b/monai/apps/generation/maisi/networks/autoencoderkl_maisi.py
@@ -128,7 +128,7 @@ class MaisiConvolution(nn.Module):
print_info: Whether to print information.
save_mem: Whether to clean CUDA cache in order to save GPU memory, default to `True`.
Additional arguments for the convolution operation.
- https://docs.monai.io/en/stable/networks.html#convolution
+ https://monai.readthedocs.io/en/stable/networks.html#convolution
"""
def __init__(
diff --git a/monai/bundle/workflows.py b/monai/bundle/workflows.py
index 9526eceddb..5b95441d51 100644
--- a/monai/bundle/workflows.py
+++ b/monai/bundle/workflows.py
@@ -78,7 +78,7 @@ def __init__(
if isinstance(meta_file, str) and not os.path.isfile(meta_file):
logger.error(
f"Cannot find the metadata config file: {meta_file}. "
- "Please see: https://docs.monai.io/en/stable/mb_specification.html"
+ "Please see: https://monai.readthedocs.io/en/stable/mb_specification.html"
)
meta_file = None
if isinstance(meta_file, list):
@@ -86,7 +86,7 @@ def __init__(
if not os.path.isfile(f):
logger.error(
f"Cannot find the metadata config file: {f}. "
- "Please see: https://docs.monai.io/en/stable/mb_specification.html"
+ "Please see: https://monai.readthedocs.io/en/stable/mb_specification.html"
)
meta_file = None
@@ -363,7 +363,7 @@ class ConfigWorkflow(BundleWorkflow):
Specification for the config-based bundle workflow.
Standardized the `initialize`, `run`, `finalize` behavior in a config-based training, evaluation, or inference.
Before `run`, we add bundle root directory to Python search directories automatically.
- For more information: https://docs.monai.io/en/latest/mb_specification.html.
+ For more information: https://monai.readthedocs.io/en/latest/mb_specification.html.
Args:
config_file: filepath of the config file, if this is a list of file paths, their contents will be merged in order.
diff --git a/monai/config/deviceconfig.py b/monai/config/deviceconfig.py
index 05842245ce..aa1f2a0b53 100644
--- a/monai/config/deviceconfig.py
+++ b/monai/config/deviceconfig.py
@@ -111,7 +111,7 @@ def print_config(file=sys.stdout):
print(f"{k} version: {v}", file=file, flush=True)
print("\nFor details about installing the optional dependencies, please visit:", file=file, flush=True)
print(
- " https://docs.monai.io/en/latest/installation.html#installing-the-recommended-dependencies\n",
+ " https://monai.readthedocs.io/en/latest/installation.html#installing-the-recommended-dependencies\n",
file=file,
flush=True,
)
diff --git a/monai/networks/nets/segresnet_ds.py b/monai/networks/nets/segresnet_ds.py
index 9dda6821d5..36afe71c5c 100644
--- a/monai/networks/nets/segresnet_ds.py
+++ b/monai/networks/nets/segresnet_ds.py
@@ -235,7 +235,7 @@ class SegResNetDS(nn.Module):
"""
SegResNetDS based on `3D MRI brain tumor segmentation using autoencoder regularization
`_.
- It is similar to https://docs.monai.io/en/stable/networks.html#segresnet, with several
+ It is similar to https://monai.readthedocs.io/en/stable/networks.html#segresnet, with several
improvements including deep supervision and non-isotropic kernel support.
Args:
diff --git a/monai/transforms/io/array.py b/monai/transforms/io/array.py
index cae2d3cd1a..0628a7fbc4 100644
--- a/monai/transforms/io/array.py
+++ b/monai/transforms/io/array.py
@@ -282,7 +282,7 @@ def __call__(self, filename: Sequence[PathLike] | PathLike, reader: ImageReader
raise RuntimeError(
f"{self.__class__.__name__} cannot find a suitable reader for file: {filename}.\n"
" Please install the reader libraries, see also the installation instructions:\n"
- " https://docs.monai.io/en/latest/installation.html#installing-the-recommended-dependencies.\n"
+ " https://monai.readthedocs.io/en/latest/installation.html#installing-the-recommended-dependencies.\n"
f" The current registered: {self.readers}.\n{msg}"
)
img_array: NdarrayOrTensor
@@ -519,7 +519,7 @@ def __call__(
raise RuntimeError(
f"{self.__class__.__name__} cannot find a suitable writer for {filename}.\n"
" Please install the writer libraries, see also the installation instructions:\n"
- " https://docs.monai.io/en/latest/installation.html#installing-the-recommended-dependencies.\n"
+ " https://monai.readthedocs.io/en/latest/installation.html#installing-the-recommended-dependencies.\n"
f" The current registered writers for {self.output_ext}: {self.writers}.\n{msg}"
)
diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py
index 0c1e484739..1208a339dc 100644
--- a/monai/transforms/spatial/array.py
+++ b/monai/transforms/spatial/array.py
@@ -2023,7 +2023,7 @@ def __init__(
See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html
When `USE_COMPILED` is `True`, this argument uses
``"nearest"``, ``"bilinear"``, ``"bicubic"`` to indicate 0, 1, 3 order interpolations.
- See also: https://docs.monai.io/en/stable/networks.html#grid-pull (experimental).
+ See also: https://monai.readthedocs.io/en/stable/networks.html#grid-pull (experimental).
When it's an integer, the numpy (cpu tensor)/cupy (cuda tensor) backends will be used
and the value represents the order of the spline interpolation.
See also: https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html
@@ -2031,7 +2031,7 @@ def __init__(
Padding mode for outside grid values. Defaults to ``"border"``.
See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html
When `USE_COMPILED` is `True`, this argument uses an integer to represent the padding mode.
- See also: https://docs.monai.io/en/stable/networks.html#grid-pull (experimental).
+ See also: https://monai.readthedocs.io/en/stable/networks.html#grid-pull (experimental).
When `mode` is an integer, using numpy/cupy backends, this argument accepts
{'reflect', 'grid-mirror', 'constant', 'grid-constant', 'nearest', 'mirror', 'grid-wrap', 'wrap'}.
See also: https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html
@@ -2075,7 +2075,7 @@ def __call__(
See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html
When `USE_COMPILED` is `True`, this argument uses
``"nearest"``, ``"bilinear"``, ``"bicubic"`` to indicate 0, 1, 3 order interpolations.
- See also: https://docs.monai.io/en/stable/networks.html#grid-pull (experimental).
+ See also: https://monai.readthedocs.io/en/stable/networks.html#grid-pull (experimental).
When it's an integer, the numpy (cpu tensor)/cupy (cuda tensor) backends will be used
and the value represents the order of the spline interpolation.
See also: https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html
@@ -2083,7 +2083,7 @@ def __call__(
Padding mode for outside grid values. Defaults to ``self.padding_mode``.
See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html
When `USE_COMPILED` is `True`, this argument uses an integer to represent the padding mode.
- See also: https://docs.monai.io/en/stable/networks.html#grid-pull (experimental).
+ See also: https://monai.readthedocs.io/en/stable/networks.html#grid-pull (experimental).
When `mode` is an integer, using numpy/cupy backends, this argument accepts
{'reflect', 'grid-mirror', 'constant', 'grid-constant', 'nearest', 'mirror', 'grid-wrap', 'wrap'}.
See also: https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html
diff --git a/monai/utils/module.py b/monai/utils/module.py
index 7bbbb4ab1e..a64f73cd6b 100644
--- a/monai/utils/module.py
+++ b/monai/utils/module.py
@@ -195,7 +195,7 @@ def load_submodules(
except ImportError as e:
msg = (
"\nMultiple versions of MONAI may have been installed?\n"
- "Please see the installation guide: https://docs.monai.io/en/stable/installation.html\n"
+ "Please see the installation guide: https://monai.readthedocs.io/en/stable/installation.html\n"
) # issue project-monai/monai#5193
raise type(e)(f"{e}\n{msg}").with_traceback(e.__traceback__) from e # raise with modified message
@@ -405,7 +405,7 @@ def __init__(self, *_args, **_kwargs):
_default_msg = (
f"{msg}."
+ "\n\nFor details about installing the optional dependencies, please visit:"
- + "\n https://docs.monai.io/en/latest/installation.html#installing-the-recommended-dependencies"
+ + "\n https://monai.readthedocs.io/en/latest/installation.html#installing-the-recommended-dependencies"
)
if tb is None:
self._exception = OptionalImportError(_default_msg)
diff --git a/monai/utils/tf32.py b/monai/utils/tf32.py
index 81f56477bb..ad5918a34a 100644
--- a/monai/utils/tf32.py
+++ b/monai/utils/tf32.py
@@ -66,7 +66,7 @@ def detect_default_tf32() -> bool:
warnings.warn(
f"Environment variable `{name} = {override_val}` is set.\n"
f" This environment variable may enable TF32 mode accidentally and affect precision.\n"
- f" See https://docs.monai.io/en/latest/precision_accelerating.html#precision-and-accelerating"
+ f" See https://monai.readthedocs.io/en/latest/precision_accelerating.html#precision-and-accelerating"
)
may_enable_tf32 = True
diff --git a/setup.cfg b/setup.cfg
index b3949213c2..ab03b906c1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -2,7 +2,7 @@
name = monai
author = MONAI Consortium
author_email = monai.contact@gmail.com
-url = https://monai.io/
+url = https://project-monai.github.io/
description = AI Toolkit for Healthcare Imaging
long_description = file:README.md
long_description_content_type = text/markdown; charset=UTF-8
@@ -11,7 +11,7 @@ license = Apache License 2.0
license_files =
LICENSE
project_urls =
- Documentation=https://docs.monai.io/
+ Documentation=https://monai.readthedocs.io/
Bug Tracker=https://github.com/Project-MONAI/MONAI/issues
Source Code=https://github.com/Project-MONAI/MONAI
classifiers =
diff --git a/tests/profile_subclass/README.md b/tests/profile_subclass/README.md
index de16ef2d91..45d97d3f7e 100644
--- a/tests/profile_subclass/README.md
+++ b/tests/profile_subclass/README.md
@@ -12,7 +12,7 @@ pip install snakeviz # for viewing the cProfile results
```
./runtests.sh --build # from monai's root directory
```
-or follow the installation guide (https://docs.monai.io/en/latest/installation.html)
+or follow the installation guide (https://monai.readthedocs.io/en/latest/installation.html)
### Profiling the task of adding two MetaTensors
```bash
From a0e48895f64e2bb15491cdd990a2ac7ebd3aa047 Mon Sep 17 00:00:00 2001
From: Mohamed Salah
Date: Mon, 24 Nov 2025 06:08:54 +0200
Subject: [PATCH 31/61] Fix #8350: Clarify LocalNormalizedCrossCorrelationLoss
docstring (#8639)
## Description
This PR improves the docstring for `LocalNormalizedCrossCorrelationLoss`
to address the ambiguities identified in
#8350.
## Problem
The current docstring does not clearly document:
- The range of returned loss values
- Whether the loss should be minimized or maximized
- How to interpret high vs. low loss values
## Solution
Enhanced the class docstring with comprehensive documentation including
Returns section (value range, optimization
direction) and Note section (implementation details, interpretation
guidelines).
## Changes
- Added `Returns` section with explicit value range and optimization
direction
- Added `Note` section explaining transformations and interpretation
- Reorganized Args to class level for better discoverability
- Followed MONAI formatting conventions
## Testing
- [x] Verified docstring syntax is correct
- [x] Confirmed technical accuracy by analyzing implementation
- [x] Validated all three issue requirements are addressed
Signed-off-by: Mohamed Salah
Signed-off-by: jirka
---
monai/losses/image_dissimilarity.py | 45 +++++++++++++++++++----------
1 file changed, 30 insertions(+), 15 deletions(-)
diff --git a/monai/losses/image_dissimilarity.py b/monai/losses/image_dissimilarity.py
index dd132770ec..8ddfbd000f 100644
--- a/monai/losses/image_dissimilarity.py
+++ b/monai/losses/image_dissimilarity.py
@@ -51,6 +51,7 @@ def make_gaussian_kernel(kernel_size: int) -> torch.Tensor:
class LocalNormalizedCrossCorrelationLoss(_Loss):
"""
Local squared zero-normalized cross-correlation.
+
The loss is based on a moving kernel/window over the y_true/y_pred,
within the window the square of zncc is calculated.
The kernel can be a rectangular / triangular / gaussian window.
@@ -59,6 +60,35 @@ class LocalNormalizedCrossCorrelationLoss(_Loss):
Adapted from:
https://github.com/voxelmorph/voxelmorph/blob/legacy/src/losses.py
DeepReg (https://github.com/DeepRegNet/DeepReg)
+
+ Args:
+ spatial_dims: number of spatial dimensions, {``1``, ``2``, ``3``}. Defaults to 3.
+ kernel_size: kernel spatial size, must be odd.
+ kernel_type: {``"rectangular"``, ``"triangular"``, ``"gaussian"``}. Defaults to ``"rectangular"``.
+ reduction: {``"none"``, ``"mean"``, ``"sum"``}
+ Specifies the reduction to apply to the output. Defaults to ``"mean"``.
+
+ - ``"none"``: no reduction will be applied.
+ - ``"mean"``: the sum of the output will be divided by the number of elements in the output.
+ - ``"sum"``: the output will be summed.
+ smooth_nr: a small constant added to the numerator to avoid nan.
+ smooth_dr: a small constant added to the denominator to avoid nan.
+
+ Returns:
+ torch.Tensor: The computed loss value. The output range is approximately [-1, 0], where:
+ - Values closer to -1 indicate higher correlation (better match)
+ - Values closer to 0 indicate lower correlation (worse match)
+ - This loss should be **minimized** during optimization
+
+ Note:
+ The implementation computes the squared normalized cross-correlation coefficient
+ and then negates it, transforming the correlation maximization problem into a
+ loss minimization problem suitable for standard PyTorch optimizers.
+
+ Interpretation:
+ - Loss ≈ -1: Perfect correlation between images
+ - Loss ≈ 0: No correlation between images
+ - Lower (more negative) values indicate better alignment
"""
def __init__(
@@ -70,21 +100,6 @@ def __init__(
smooth_nr: float = 0.0,
smooth_dr: float = 1e-5,
) -> None:
- """
- Args:
- spatial_dims: number of spatial dimensions, {``1``, ``2``, ``3``}. Defaults to 3.
- kernel_size: kernel spatial size, must be odd.
- kernel_type: {``"rectangular"``, ``"triangular"``, ``"gaussian"``}. Defaults to ``"rectangular"``.
- reduction: {``"none"``, ``"mean"``, ``"sum"``}
- Specifies the reduction to apply to the output. Defaults to ``"mean"``.
-
- - ``"none"``: no reduction will be applied.
- - ``"mean"``: the sum of the output will be divided by the number of elements in the output.
- - ``"sum"``: the output will be summed.
- smooth_nr: a small constant added to the numerator to avoid nan.
- smooth_dr: a small constant added to the denominator to avoid nan.
-
- """
super().__init__(reduction=LossReduction(reduction).value)
self.ndim = spatial_dims
From 935f1cc27eac80c87862315d49420129c0c2f3ff Mon Sep 17 00:00:00 2001
From: Rafael Garcia-Dias
Date: Tue, 25 Nov 2025 04:17:17 +0000
Subject: [PATCH 32/61] 8620 modulenotfounderror no module named onnxscript in
test py3x 311 pipeline (#8638)
Fixes #8620 .
### Description
Adds `onnxscript` as an explicit dependency.
I have tried to find where this onnxscript package was coming from
before. For that, I tried all Python versions from 3.9 to 3.12, all
versions of onnxruntime and onnx_graphsurgeon, and all versions later
than 1.13.0 of onnx.
None of these would include `onnxscript`.
I suppose that this was a requirement of another library and was removed
in some new version.
I don't think it is worth the trouble of further investigating to find
which package it was, since we wouldn't want to freeze a package version
for this reason. So, instead, I propose we just add onnxscript as a
dependency.
### Potential issue
I am not sure if this will trigger the running of the ONNX tests in
Python < 3.10 and how it will impact those tests.
A few sentences describing the changes proposed in this pull request.
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Breaking change (fix or new feature that would cause existing
functionality to change).
- [ ] New tests added to cover the changes.
- [ ] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [ ] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [ ] In-line docstrings updated.
- [ ] Documentation updated, tested `make html` command in the `docs/`
folder.
---------
Signed-off-by: R. Garcia-Dias
Signed-off-by: Rafael Garcia-Dias
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Signed-off-by: jirka
---
requirements-dev.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/requirements-dev.txt b/requirements-dev.txt
index ff234c856e..1dc2141cf6 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -51,7 +51,8 @@ h5py
nni==2.10.1; platform_system == "Linux" and "arm" not in platform_machine and "aarch" not in platform_machine
optuna
git+https://github.com/Project-MONAI/MetricsReloaded@monai-support#egg=MetricsReloaded
-onnx>=1.13.0, <1.19.1
+onnx>=1.13.0
+onnxscript
onnxruntime; python_version <= '3.10'
typeguard<3 # https://github.com/microsoft/nni/issues/5457
filelock<3.12.0 # https://github.com/microsoft/nni/issues/5523
From 34f93b7395c53ce0837bf11dc48b02599f0acca2 Mon Sep 17 00:00:00 2001
From: sewon jeon
Date: Tue, 25 Nov 2025 16:02:59 +0900
Subject: [PATCH 33/61] Generate heatmap transforms (#8579)
Fixes #3328 .
### Description
A few sentences describing the changes proposed in this pull request.
This pull request introduces `GenerateHeatmap` and `GenerateHeatmapd`
transforms for creating Gaussian heatmaps from landmark coordinates.
The input points are currently expected in ZYX order, but this can be
changed to support XYZ if preferred.
The transforms ~support both batched (B, N, D) and~ only non-batched (N,
D) inputs.
Example notebooks are included for demonstration and will be removed
before the PR is merged.
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Breaking change (fix or new feature that would cause existing
functionality to change).
- [x] New tests added to cover the changes.
- [x] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [x] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [ ] In-line docstrings updated.
- [ ] Documentation updated, tested `make html` command in the `docs/`
folder.
---------
Signed-off-by: sewon.jeon
Signed-off-by: sewon jeon
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
Co-authored-by: YunLiu <55491388+KumoLiu@users.noreply.github.com>
Signed-off-by: jirka
---
monai/transforms/__init__.py | 4 +
monai/transforms/post/array.py | 158 ++++++++++++-
monai/transforms/post/dictionary.py | 207 ++++++++++++++++
tests/transforms/test_generate_heatmap.py | 261 +++++++++++++++++++++
tests/transforms/test_generate_heatmapd.py | 225 ++++++++++++++++++
5 files changed, 854 insertions(+), 1 deletion(-)
create mode 100644 tests/transforms/test_generate_heatmap.py
create mode 100644 tests/transforms/test_generate_heatmapd.py
diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py
index 0ab9fe63d5..3fd33b76da 100644
--- a/monai/transforms/__init__.py
+++ b/monai/transforms/__init__.py
@@ -293,6 +293,7 @@
AsDiscrete,
DistanceTransformEDT,
FillHoles,
+ GenerateHeatmap,
Invert,
KeepLargestConnectedComponent,
LabelFilter,
@@ -319,6 +320,9 @@
FillHolesD,
FillHolesd,
FillHolesDict,
+ GenerateHeatmapd,
+ GenerateHeatmapD,
+ GenerateHeatmapDict,
InvertD,
Invertd,
InvertDict,
diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py
index 2e733c4f6c..47623b748d 100644
--- a/monai/transforms/post/array.py
+++ b/monai/transforms/post/array.py
@@ -38,7 +38,14 @@
remove_small_objects,
)
from monai.transforms.utils_pytorch_numpy_unification import unravel_index
-from monai.utils import TransformBackends, convert_data_type, convert_to_tensor, ensure_tuple, look_up_option
+from monai.utils import (
+ TransformBackends,
+ convert_data_type,
+ convert_to_tensor,
+ ensure_tuple,
+ get_equivalent_dtype,
+ look_up_option,
+)
from monai.utils.type_conversion import convert_to_dst_type
__all__ = [
@@ -54,6 +61,7 @@
"SobelGradients",
"VoteEnsemble",
"Invert",
+ "GenerateHeatmap",
"DistanceTransformEDT",
]
@@ -742,6 +750,154 @@ def __call__(self, img: Sequence[NdarrayOrTensor] | NdarrayOrTensor) -> NdarrayO
return self.post_convert(out_pt, img)
+class GenerateHeatmap(Transform):
+ """
+ Generate per-landmark Gaussian heatmaps for 2D or 3D coordinates.
+
+ Notes:
+ - Coordinates are interpreted in voxel units and expected in (Y, X) for 2D or (Z, Y, X) for 3D.
+ - Target spatial_shape is (Y, X) for 2D and (Z, Y, X) for 3D.
+ - Output layout uses channel-first convention with one channel per landmark.
+ - Input points shape: (N, D) where N is number of landmarks, D is spatial dimensions (2 or 3).
+ - Output heatmap shape: (N, Y, X) for 2D or (N, Z, Y, X) for 3D.
+ - Each channel index corresponds to one landmark.
+
+ Args:
+ sigma: gaussian standard deviation. A single value is broadcast across all spatial dimensions.
+ spatial_shape: optional fallback spatial shape. If ``None`` it must be provided when calling the transform.
+ truncated: extent, in multiples of ``sigma``, used to crop the gaussian support window.
+ normalize: normalize every heatmap channel to ``[0, 1]`` when ``True``.
+ dtype: target dtype for the generated heatmaps (accepts numpy or torch dtypes).
+
+ Raises:
+ ValueError: when ``sigma`` is non-positive or ``spatial_shape`` cannot be resolved.
+
+ """
+
+ backend = [TransformBackends.NUMPY, TransformBackends.TORCH]
+
+ def __init__(
+ self,
+ sigma: Sequence[float] | float = 5.0,
+ spatial_shape: Sequence[int] | None = None,
+ truncated: float = 4.0,
+ normalize: bool = True,
+ dtype: np.dtype | torch.dtype | type = np.float32,
+ ) -> None:
+ if isinstance(sigma, Sequence) and not isinstance(sigma, (str, bytes)):
+ if any(s <= 0 for s in sigma):
+ raise ValueError("Argument `sigma` values must be positive.")
+ self._sigma = tuple(float(s) for s in sigma)
+ else:
+ if float(sigma) <= 0:
+ raise ValueError("Argument `sigma` must be positive.")
+ self._sigma = (float(sigma),)
+ if truncated <= 0:
+ raise ValueError("Argument `truncated` must be positive.")
+ self.truncated = float(truncated)
+ self.normalize = normalize
+ self.torch_dtype = get_equivalent_dtype(dtype, torch.Tensor)
+ self.numpy_dtype = get_equivalent_dtype(dtype, np.ndarray)
+ # Validate that dtype is floating-point for meaningful Gaussian values
+ if not self.torch_dtype.is_floating_point:
+ raise ValueError(f"Argument `dtype` must be a floating-point type, got {self.torch_dtype}")
+ self.spatial_shape = None if spatial_shape is None else tuple(int(s) for s in spatial_shape)
+
+ def __call__(self, points: NdarrayOrTensor, spatial_shape: Sequence[int] | None = None) -> NdarrayOrTensor:
+ """
+ Args:
+ points: landmark coordinates as ndarray/Tensor with shape (N, D),
+ ordered as (Y, X) for 2D or (Z, Y, X) for 3D, where N is the number
+ of landmarks and D is the spatial dimensionality.
+ spatial_shape: spatial size as a sequence. If None, uses the value provided at construction.
+
+ Returns:
+ Heatmaps with shape (N, *spatial), one channel per landmark.
+
+ Raises:
+ ValueError: if points shape/dimension or spatial_shape is invalid.
+ """
+ original_points = points
+ points_t = convert_to_tensor(points, dtype=torch.float32, track_meta=False)
+
+ if points_t.ndim != 2:
+ raise ValueError(
+ f"Argument `points` must be a 2D array with shape (num_points, spatial_dims), got shape {points_t.shape}."
+ )
+
+ if points_t.shape[-1] not in (2, 3):
+ raise ValueError("GenerateHeatmap only supports 2D or 3D landmarks.")
+
+ device = points_t.device
+ num_points, spatial_dims = points_t.shape
+
+ target_shape = self._resolve_spatial_shape(spatial_shape, spatial_dims)
+ sigma = self._resolve_sigma(spatial_dims)
+
+ # Create sparse image with impulses at landmark locations
+ heatmap = torch.zeros((num_points, *target_shape), dtype=self.torch_dtype, device=device)
+ bounds_t = torch.as_tensor(target_shape, device=device, dtype=points_t.dtype)
+
+ for idx, center in enumerate(points_t):
+ if not torch.isfinite(center).all():
+ continue
+ if not ((center >= 0).all() and (center < bounds_t).all()):
+ continue
+ # Round to nearest integer for impulse placement, then clamp to valid index range
+ center_int = center.round().long()
+ # Clamp indices to [0, size-1] to avoid out-of-bounds (e.g., 9.7 rounds to 10 in size-10 array)
+ bounds_max = (bounds_t - 1).long()
+ center_int = torch.minimum(torch.maximum(center_int, torch.zeros_like(center_int)), bounds_max)
+ # Place impulse (use maximum in case of overlapping landmarks)
+ current_val = heatmap[idx][tuple(center_int)]
+ heatmap[idx][tuple(center_int)] = torch.maximum(
+ current_val, torch.tensor(1.0, dtype=self.torch_dtype, device=device)
+ )
+
+ # Apply Gaussian blur using GaussianFilter
+ # Reshape to (num_points, 1, *spatial) for per-channel filtering
+ heatmap_input = heatmap.unsqueeze(1) # Add channel dimension
+
+ gaussian_filter = GaussianFilter(
+ spatial_dims=spatial_dims, sigma=sigma, truncated=self.truncated, approx="erf", requires_grad=False
+ ).to(device=device, dtype=self.torch_dtype)
+
+ heatmap_blurred = gaussian_filter(heatmap_input)
+ heatmap = heatmap_blurred.squeeze(1) # Remove channel dimension
+
+ # Normalize per channel if requested
+ if self.normalize:
+ for idx in range(num_points):
+ peak = heatmap[idx].amax()
+ if peak > 0:
+ heatmap[idx].div_(peak)
+
+ target_dtype = self.torch_dtype if isinstance(original_points, (torch.Tensor, MetaTensor)) else self.numpy_dtype
+ converted, _, _ = convert_to_dst_type(heatmap, original_points, dtype=target_dtype)
+ return converted
+
+ def _resolve_spatial_shape(self, call_shape: Sequence[int] | None, spatial_dims: int) -> tuple[int, ...]:
+ shape = call_shape if call_shape is not None else self.spatial_shape
+ if shape is None:
+ raise ValueError("Argument `spatial_shape` must be provided either at construction time or call time.")
+ shape_tuple = ensure_tuple(shape)
+ if len(shape_tuple) != spatial_dims:
+ if len(shape_tuple) == 1:
+ shape_tuple = shape_tuple * spatial_dims # type: ignore
+ else:
+ raise ValueError(
+ "Argument `spatial_shape` length must match the landmarks' spatial dims (or pass a single int to broadcast)."
+ )
+ return tuple(int(s) for s in shape_tuple)
+
+ def _resolve_sigma(self, spatial_dims: int) -> tuple[float, ...]:
+ if len(self._sigma) == spatial_dims:
+ return self._sigma
+ if len(self._sigma) == 1:
+ return self._sigma * spatial_dims
+ raise ValueError("Argument `sigma` sequence length must equal the number of spatial dimensions.")
+
+
class ProbNMS(Transform):
"""
Performs probability based non-maximum suppression (NMS) on the probabilities map via
diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py
index 7e1e074f71..65fdd22b22 100644
--- a/monai/transforms/post/dictionary.py
+++ b/monai/transforms/post/dictionary.py
@@ -35,6 +35,7 @@
AsDiscrete,
DistanceTransformEDT,
FillHoles,
+ GenerateHeatmap,
KeepLargestConnectedComponent,
LabelFilter,
LabelToContour,
@@ -48,6 +49,7 @@
from monai.transforms.utility.array import ToTensor
from monai.transforms.utils import allow_missing_keys_mode, convert_applied_interp_mode
from monai.utils import PostFix, convert_to_tensor, ensure_tuple, ensure_tuple_rep
+from monai.utils.type_conversion import convert_to_dst_type
__all__ = [
"ActivationsD",
@@ -95,6 +97,9 @@
"DistanceTransformEDTd",
"DistanceTransformEDTD",
"DistanceTransformEDTDict",
+ "GenerateHeatmapd",
+ "GenerateHeatmapD",
+ "GenerateHeatmapDict",
]
DEFAULT_POST_FIX = PostFix.meta()
@@ -508,6 +513,208 @@ def __init__(self, keys: KeysCollection, output_key: str | None = None, num_clas
super().__init__(keys, ensemble, output_key)
+class GenerateHeatmapd(MapTransform):
+ """
+ Dictionary-based wrapper of :py:class:`monai.transforms.GenerateHeatmap`.
+ Converts landmark coordinates into gaussian heatmaps and optionally copies metadata from a reference image.
+
+ Args:
+ keys: keys of the corresponding items in the dictionary, where each key references a tensor
+ of landmark point coordinates with shape (N, D), where N is the number of landmarks
+ and D is the spatial dimensionality (2 or 3).
+ sigma: standard deviation for the Gaussian kernel. Can be a single value or a sequence matching the number
+ of spatial dimensions.
+ heatmap_keys: keys to store output heatmaps. Default: "{key}_heatmap" for each key.
+ ref_image_keys: keys of reference images to inherit spatial metadata from. When provided, heatmaps will
+ have the same shape, affine, and spatial metadata as the reference images.
+ spatial_shape: spatial dimensions of output heatmaps. Can be:
+ - Single shape (tuple): applied to all keys
+ - List of shapes: one per key (must match keys length)
+ truncated: truncation distance for Gaussian kernel computation (in sigmas).
+ normalize: if True, normalize each heatmap's peak value to 1.0.
+ dtype: output data type for heatmaps. Defaults to np.float32.
+ allow_missing_keys: if True, don't raise error if some keys are missing in data.
+
+ Returns:
+ Dictionary with original data plus generated heatmaps at specified keys.
+
+ Raises:
+ ValueError: If heatmap_keys/ref_image_keys length doesn't match keys length.
+ ValueError: If no spatial shape can be determined (need spatial_shape or ref_image_keys).
+ ValueError: If input points have invalid shape (must be 2D array with shape (N, D)).
+
+ Example:
+ .. code-block:: python
+
+ import numpy as np
+ from monai.transforms import GenerateHeatmapd
+
+ # Create sample data with landmark points and a reference image
+ data = {
+ "landmarks": np.array([[10.0, 15.0], [20.0, 25.0]]), # 2 points in 2D
+ "image": np.zeros((32, 32)) # reference image
+ }
+
+ # Transform with reference image
+ transform = GenerateHeatmapd(
+ keys="landmarks",
+ sigma=2.0,
+ ref_image_keys="image"
+ )
+ result = transform(data)
+ # result["landmarks_heatmap"] has shape (2, 32, 32) - one channel per landmark
+
+ # Or with explicit spatial_shape
+ transform = GenerateHeatmapd(
+ keys="landmarks",
+ sigma=2.0,
+ spatial_shape=(64, 64)
+ )
+ result = transform(data)
+ # result["landmarks_heatmap"] has shape (2, 64, 64)
+
+ Notes:
+ - Default heatmap_keys are generated as "{key}_heatmap" for each input key
+ - Shape inference precedence: static spatial_shape > ref_image
+ - Input points shape: (N, D) where N is number of landmarks, D is spatial dimensions
+ - Output heatmap shape: (N, H, W) for 2D or (N, H, W, D) for 3D
+ - When using ref_image_keys, heatmaps inherit affine and spatial metadata from reference
+ """
+
+ backend = GenerateHeatmap.backend
+
+ # Error messages
+ _ERR_HEATMAP_KEYS_LEN = "Argument `heatmap_keys` length must match keys length."
+ _ERR_REF_KEYS_LEN = "Argument `ref_image_keys` length must match keys length when provided."
+ _ERR_SHAPE_LEN = "Argument `spatial_shape` length must match keys length when providing per-key shapes."
+ _ERR_NO_SHAPE = "Unable to determine spatial shape for GenerateHeatmapd. Provide spatial_shape or ref_image_keys."
+ _ERR_INVALID_POINTS = "Landmark arrays must be 2D with shape (N, D)."
+ _ERR_REF_NO_SHAPE = "Reference data must define a shape attribute."
+
+ def __init__(
+ self,
+ keys: KeysCollection,
+ sigma: Sequence[float] | float = 5.0,
+ heatmap_keys: KeysCollection | None = None,
+ ref_image_keys: KeysCollection | None = None,
+ spatial_shape: Sequence[int] | Sequence[Sequence[int]] | None = None,
+ truncated: float = 4.0,
+ normalize: bool = True,
+ dtype: np.dtype | torch.dtype | type = np.float32,
+ allow_missing_keys: bool = False,
+ ) -> None:
+ super().__init__(keys, allow_missing_keys)
+ self.heatmap_keys = self._prepare_heatmap_keys(heatmap_keys)
+ self.ref_image_keys = self._prepare_optional_keys(ref_image_keys)
+ self.static_shapes = self._prepare_shapes(spatial_shape)
+ self.generator = GenerateHeatmap(
+ sigma=sigma, spatial_shape=None, truncated=truncated, normalize=normalize, dtype=dtype
+ )
+
+ def __call__(self, data: Mapping[Hashable, Any]) -> dict[Hashable, Any]:
+ d = dict(data)
+ for key, out_key, ref_key, static_shape in self.key_iterator(
+ d, self.heatmap_keys, self.ref_image_keys, self.static_shapes
+ ):
+ points = d[key]
+ shape = self._determine_shape(points, static_shape, d, ref_key)
+ # The GenerateHeatmap transform will handle type conversion based on input points
+ heatmap = self.generator(points, spatial_shape=shape)
+ # If there's a reference image and we need to match its type/device
+ reference = d.get(ref_key) if ref_key is not None and ref_key in d else None
+ if reference is not None and isinstance(reference, (torch.Tensor, np.ndarray)):
+ # Convert to match reference type and device while preserving heatmap's dtype
+ heatmap, _, _ = convert_to_dst_type(
+ heatmap, reference, dtype=heatmap.dtype, device=getattr(reference, "device", None)
+ )
+ # Copy metadata if reference is MetaTensor
+ if isinstance(reference, MetaTensor) and isinstance(heatmap, MetaTensor):
+ heatmap.affine = reference.affine
+ self._update_spatial_metadata(heatmap, shape)
+ d[out_key] = heatmap
+ return d
+
+ def _prepare_heatmap_keys(self, heatmap_keys: KeysCollection | None) -> tuple[Hashable, ...]:
+ if heatmap_keys is None:
+ return tuple(f"{key}_heatmap" for key in self.keys)
+ keys_tuple = ensure_tuple(heatmap_keys)
+ if len(keys_tuple) == 1 and len(self.keys) > 1:
+ keys_tuple = keys_tuple * len(self.keys)
+ if len(keys_tuple) != len(self.keys):
+ raise ValueError(self._ERR_HEATMAP_KEYS_LEN)
+ return keys_tuple
+
+ def _prepare_optional_keys(self, maybe_keys: KeysCollection | None) -> tuple[Hashable | None, ...]:
+ if maybe_keys is None:
+ return (None,) * len(self.keys)
+ keys_tuple = ensure_tuple(maybe_keys)
+ if len(keys_tuple) == 1 and len(self.keys) > 1:
+ keys_tuple = keys_tuple * len(self.keys)
+ if len(keys_tuple) != len(self.keys):
+ raise ValueError(self._ERR_REF_KEYS_LEN)
+ return tuple(keys_tuple)
+
+ def _prepare_shapes(
+ self, spatial_shape: Sequence[int] | Sequence[Sequence[int]] | None
+ ) -> tuple[tuple[int, ...] | None, ...]:
+ if spatial_shape is None:
+ return (None,) * len(self.keys)
+ shape_tuple = ensure_tuple(spatial_shape)
+ if shape_tuple and all(isinstance(v, (int, np.integer)) for v in shape_tuple):
+ shape = tuple(int(v) for v in shape_tuple)
+ return (shape,) * len(self.keys)
+ if len(shape_tuple) == 1 and len(self.keys) > 1:
+ shape_tuple = shape_tuple * len(self.keys)
+ if len(shape_tuple) != len(self.keys):
+ raise ValueError(self._ERR_SHAPE_LEN)
+ prepared: list[tuple[int, ...] | None] = []
+ for item in shape_tuple:
+ if item is None:
+ prepared.append(None)
+ else:
+ dims = ensure_tuple(item)
+ prepared.append(tuple(int(v) for v in dims))
+ return tuple(prepared)
+
+ def _determine_shape(
+ self, points: Any, static_shape: tuple[int, ...] | None, data: Mapping[Hashable, Any], ref_key: Hashable | None
+ ) -> tuple[int, ...]:
+ points_t = convert_to_tensor(points, dtype=torch.float32, track_meta=False)
+ if points_t.ndim != 2:
+ raise ValueError(f"{self._ERR_INVALID_POINTS} Got {points_t.ndim}D tensor.")
+ spatial_dims = int(points_t.shape[-1])
+ if static_shape is not None:
+ if len(static_shape) == 1 and spatial_dims > 1:
+ static_shape = tuple([static_shape[0]] * spatial_dims)
+ if len(static_shape) != spatial_dims:
+ raise ValueError(
+ f"Provided static spatial_shape has {len(static_shape)} dims; expected {spatial_dims}."
+ )
+ return static_shape
+ if ref_key is not None and ref_key in data:
+ return self._shape_from_reference(data[ref_key], spatial_dims)
+ raise ValueError(self._ERR_NO_SHAPE)
+
+ def _shape_from_reference(self, reference: Any, spatial_dims: int) -> tuple[int, ...]:
+ if isinstance(reference, MetaTensor):
+ meta_shape = reference.meta.get("spatial_shape")
+ if meta_shape is not None:
+ dims = ensure_tuple(meta_shape)
+ if len(dims) == spatial_dims:
+ return tuple(int(v) for v in dims)
+ return tuple(int(v) for v in reference.shape[-spatial_dims:])
+ if hasattr(reference, "shape"):
+ return tuple(int(v) for v in reference.shape[-spatial_dims:])
+ raise ValueError(self._ERR_REF_NO_SHAPE)
+
+ def _update_spatial_metadata(self, heatmap: MetaTensor, spatial_shape: tuple[int, ...]) -> None:
+ """Set spatial_shape explicitly from resolved shape."""
+ heatmap.meta["spatial_shape"] = tuple(int(v) for v in spatial_shape)
+
+
+GenerateHeatmapD = GenerateHeatmapDict = GenerateHeatmapd
+
+
class ProbNMSd(MapTransform):
"""
Performs probability based non-maximum suppression (NMS) on the probabilities map via
diff --git a/tests/transforms/test_generate_heatmap.py b/tests/transforms/test_generate_heatmap.py
new file mode 100644
index 0000000000..0dd108429d
--- /dev/null
+++ b/tests/transforms/test_generate_heatmap.py
@@ -0,0 +1,261 @@
+# Copyright (c) MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import annotations
+
+import unittest
+
+import numpy as np
+import torch
+from parameterized import parameterized
+
+from monai.transforms.post.array import GenerateHeatmap
+from tests.test_utils import TEST_NDARRAYS
+
+
+def _argmax_nd(x) -> np.ndarray:
+ """argmax for N-D array → returns coordinate vector (z,y,x) or (y,x)."""
+ if isinstance(x, torch.Tensor):
+ x = x.cpu().numpy()
+ return np.asarray(np.unravel_index(np.argmax(x), x.shape))
+
+
+# Test cases for 2D array inputs with different data types
+TEST_CASES_2D = [
+ [
+ f"2d_basic_type{idx}",
+ p(np.array([[4.2, 7.8], [12.3, 3.6]], dtype=np.float32)),
+ {"sigma": 1.5, "spatial_shape": (16, 16)},
+ (2, 16, 16),
+ ]
+ for idx, p in enumerate(TEST_NDARRAYS)
+]
+
+# Test cases for 3D torch outputs with explicit dtype
+TEST_CASES_3D_TORCH = [
+ [
+ f"3d_torch_{str(dtype).replace('torch.', '')}",
+ torch.tensor([[1.5, 2.5, 3.5]], dtype=torch.float32),
+ {"sigma": 1.0, "spatial_shape": (8, 8, 8), "dtype": dtype},
+ (1, 8, 8, 8),
+ dtype,
+ ]
+ for dtype in [torch.float32, torch.float64]
+]
+
+# Test cases for 3D numpy outputs with explicit dtype
+TEST_CASES_3D_NUMPY = [
+ [
+ f"3d_numpy_{dtype_obj.__name__}",
+ np.array([[1.5, 2.5, 3.5]], dtype=np.float32),
+ {"sigma": 1.0, "spatial_shape": (8, 8, 8), "dtype": dtype_obj},
+ (1, 8, 8, 8),
+ dtype_obj,
+ ]
+ for dtype_obj in [np.float32, np.float64]
+]
+
+# Test cases for different sigma values
+TEST_CASES_SIGMA = [
+ [
+ f"sigma_{sigma}",
+ np.array([[8.0, 8.0]], dtype=np.float32),
+ {"sigma": sigma, "spatial_shape": (16, 16)},
+ (1, 16, 16),
+ ]
+ for sigma in [0.5, 1.0, 2.0, 3.0]
+]
+
+# Test cases for truncated parameter
+TEST_CASES_TRUNCATED = [
+ [
+ f"truncated_{truncated}",
+ np.array([[8.0, 8.0]], dtype=np.float32),
+ {"sigma": 2.0, "spatial_shape": (32, 32), "truncated": truncated},
+ (1, 32, 32),
+ ]
+ for truncated in [2.0, 4.0, 6.0]
+]
+
+# Test cases for device and dtype propagation (torch only)
+test_device = "cuda:0" if torch.cuda.is_available() else "cpu"
+test_dtypes = [torch.float32, torch.float64]
+if torch.cuda.is_available():
+ test_dtypes.append(torch.float16)
+
+TEST_CASES_DEVICE_DTYPE = [
+ [
+ f"{test_device.split(':')[0]}_{str(dtype).replace('torch.', '')}",
+ torch.tensor([[3.0, 4.0, 5.0]], dtype=torch.float32, device=test_device),
+ {"sigma": 1.2, "spatial_shape": (10, 10, 10), "dtype": dtype},
+ (1, 10, 10, 10),
+ dtype,
+ test_device,
+ ]
+ for dtype in test_dtypes
+]
+
+
+class TestGenerateHeatmap(unittest.TestCase):
+ @parameterized.expand(TEST_CASES_2D)
+ def test_array_2d(self, _, points, params, expected_shape):
+ transform = GenerateHeatmap(**params)
+ heatmap = transform(points)
+
+ # Check output type matches input type
+ if isinstance(points, torch.Tensor):
+ self.assertIsInstance(heatmap, torch.Tensor)
+ self.assertEqual(heatmap.dtype, torch.float32) # Default dtype for torch
+ heatmap_np = heatmap.cpu().numpy()
+ points_np = points.cpu().numpy()
+ else:
+ self.assertIsInstance(heatmap, np.ndarray)
+ self.assertEqual(heatmap.dtype, np.float32) # Default dtype for numpy
+ heatmap_np = heatmap
+ points_np = points
+
+ self.assertEqual(heatmap.shape, expected_shape)
+ np.testing.assert_allclose(heatmap_np.max(axis=(1, 2)), np.ones(expected_shape[0]), rtol=1e-5, atol=1e-5)
+
+ # peak should be close to original point location (<= 1px tolerance due to discretization)
+ for idx in range(expected_shape[0]):
+ peak = _argmax_nd(heatmap_np[idx])
+ self.assertTrue(np.all(np.abs(peak - points_np[idx]) <= 1.0), msg=f"peak={peak}, point={points_np[idx]}")
+ self.assertLess(heatmap_np[idx, 0, 0], 1e-3)
+
+ @parameterized.expand(TEST_CASES_3D_TORCH)
+ def test_array_3d_torch_output(self, _, points, params, expected_shape, expected_dtype):
+ transform = GenerateHeatmap(**params)
+ heatmap = transform(points)
+
+ self.assertIsInstance(heatmap, torch.Tensor)
+ self.assertEqual(heatmap.device, points.device)
+ self.assertEqual(tuple(heatmap.shape), expected_shape)
+ self.assertEqual(heatmap.dtype, expected_dtype)
+ self.assertTrue(torch.isclose(heatmap.max(), torch.tensor(1.0, dtype=heatmap.dtype, device=heatmap.device)))
+
+ @parameterized.expand(TEST_CASES_3D_NUMPY)
+ def test_array_3d_numpy_output(self, _, points, params, expected_shape, expected_dtype):
+ transform = GenerateHeatmap(**params)
+ heatmap = transform(points)
+
+ self.assertIsInstance(heatmap, np.ndarray)
+ self.assertEqual(heatmap.shape, expected_shape)
+ self.assertEqual(heatmap.dtype, expected_dtype)
+ np.testing.assert_allclose(heatmap.max(), 1.0, rtol=1e-5)
+
+ @parameterized.expand(TEST_CASES_DEVICE_DTYPE)
+ def test_array_torch_device_and_dtype_propagation(
+ self, _, pts, params, expected_shape, expected_dtype, expected_device
+ ):
+ tr = GenerateHeatmap(**params)
+ hm = tr(pts)
+
+ self.assertIsInstance(hm, torch.Tensor)
+ self.assertEqual(str(hm.device).split(":")[0], expected_device.split(":")[0])
+ self.assertEqual(hm.dtype, expected_dtype)
+ self.assertEqual(tuple(hm.shape), expected_shape)
+ self.assertTrue(torch.all(hm >= 0))
+
+ def test_array_channel_order_identity(self):
+ # ensure the order of channels follows the order of input points
+ pts = np.array([[2.0, 2.0], [12.0, 2.0], [2.0, 12.0]], dtype=np.float32) # point A # point B # point C
+ hm = GenerateHeatmap(sigma=1.2, spatial_shape=(16, 16))(pts)
+
+ self.assertIsInstance(hm, np.ndarray)
+ self.assertEqual(hm.shape, (3, 16, 16))
+
+ peaks = np.vstack([_argmax_nd(hm[i]) for i in range(3)])
+ # y,x close to points
+ np.testing.assert_allclose(peaks, pts, atol=1.0)
+
+ def test_array_points_out_of_bounds(self):
+ # points outside spatial domain: heatmap should still be valid (no NaN/Inf) and not all-zeros
+ pts = np.array(
+ [[-5.0, -5.0], [100.0, 100.0], [8.0, 8.0]], # outside top-left # outside bottom-right # inside
+ dtype=np.float32,
+ )
+ hm = GenerateHeatmap(sigma=2.0, spatial_shape=(16, 16))(pts)
+
+ self.assertIsInstance(hm, np.ndarray)
+ self.assertEqual(hm.shape, (3, 16, 16))
+ self.assertFalse(np.isnan(hm).any() or np.isinf(hm).any())
+
+ # inside point channel should have max≈1; others may clip at border (≤1)
+ self.assertGreater(hm[2].max(), 0.9)
+
+ @parameterized.expand(TEST_CASES_SIGMA)
+ def test_array_sigma_scaling_effect(self, _, pt, params, expected_shape):
+ heatmap = GenerateHeatmap(**params)(pt)[0]
+ self.assertEqual(heatmap.shape, expected_shape[1:])
+
+ # All should have peak normalized to 1.0
+ np.testing.assert_allclose(heatmap.max(), 1.0, rtol=1e-5)
+
+ # Verify heatmap is valid
+ self.assertFalse(np.isnan(heatmap).any() or np.isinf(heatmap).any())
+
+ def test_invalid_points_shape_raises(self):
+ # points must be (N, D) with D in {2,3}
+ tr = GenerateHeatmap(sigma=1.0, spatial_shape=(8, 8))
+ with self.assertRaises((ValueError, AssertionError, IndexError, RuntimeError)):
+ tr(np.zeros((2,), dtype=np.float32)) # wrong rank
+
+ with self.assertRaises((ValueError, AssertionError, IndexError, RuntimeError)):
+ tr(np.zeros((2, 4), dtype=np.float32)) # D=4 unsupported
+
+ @parameterized.expand(TEST_CASES_TRUNCATED)
+ def test_truncated_parameter(self, _, pt, params, expected_shape):
+ heatmap = GenerateHeatmap(**params)(pt)[0]
+
+ # All should have same peak value (normalized to 1.0)
+ np.testing.assert_allclose(heatmap.max(), 1.0, rtol=1e-5)
+
+ # Verify shape and no NaN/Inf
+ self.assertEqual(heatmap.shape, expected_shape[1:])
+ self.assertFalse(np.isnan(heatmap).any() or np.isinf(heatmap).any())
+
+ def test_torch_to_torch_type_preservation(self):
+ """Test that torch input produces torch output"""
+ pts = torch.tensor([[4.0, 4.0]], dtype=torch.float32)
+ hm = GenerateHeatmap(sigma=1.0, spatial_shape=(8, 8))(pts)
+
+ self.assertIsInstance(hm, torch.Tensor)
+ self.assertEqual(hm.dtype, torch.float32)
+ self.assertEqual(hm.device, pts.device)
+
+ def test_numpy_to_numpy_type_preservation(self):
+ """Test that numpy input produces numpy output"""
+ pts = np.array([[4.0, 4.0]], dtype=np.float32)
+ hm = GenerateHeatmap(sigma=1.0, spatial_shape=(8, 8))(pts)
+
+ self.assertIsInstance(hm, np.ndarray)
+ self.assertEqual(hm.dtype, np.float32)
+
+ def test_dtype_override_torch(self):
+ """Test dtype parameter works with torch tensors"""
+ pts = torch.tensor([[4.0, 4.0, 4.0]], dtype=torch.float32)
+ hm = GenerateHeatmap(sigma=1.0, spatial_shape=(8, 8, 8), dtype=torch.float64)(pts)
+
+ self.assertIsInstance(hm, torch.Tensor)
+ self.assertEqual(hm.dtype, torch.float64)
+
+ def test_dtype_override_numpy(self):
+ """Test dtype parameter works with numpy arrays"""
+ pts = np.array([[4.0, 4.0, 4.0]], dtype=np.float32)
+ hm = GenerateHeatmap(sigma=1.0, spatial_shape=(8, 8, 8), dtype=np.float64)(pts)
+
+ self.assertIsInstance(hm, np.ndarray)
+ self.assertEqual(hm.dtype, np.float64)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/transforms/test_generate_heatmapd.py b/tests/transforms/test_generate_heatmapd.py
new file mode 100644
index 0000000000..0867a959e5
--- /dev/null
+++ b/tests/transforms/test_generate_heatmapd.py
@@ -0,0 +1,225 @@
+# Copyright (c) MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import annotations
+
+import unittest
+
+import numpy as np
+import torch
+from parameterized import parameterized
+
+from monai.data import MetaTensor
+from monai.transforms.post.dictionary import GenerateHeatmapd
+from tests.test_utils import assert_allclose
+
+# Test cases for dictionary transforms with reference image
+# Only test with non-MetaTensor types to avoid affine conflicts
+TEST_CASES_WITH_REF = [
+ [
+ "dict_with_ref_3d_numpy",
+ np.array([[2.5, 2.5, 3.0], [5.0, 5.0, 4.0]], dtype=np.float32),
+ {"sigma": 2.0},
+ (2, 8, 8, 8),
+ torch.float32,
+ True, # uses reference image
+ ],
+ [
+ "dict_with_ref_3d_torch",
+ torch.tensor([[2.5, 2.5, 3.0], [5.0, 5.0, 4.0]], dtype=torch.float32),
+ {"sigma": 2.0},
+ (2, 8, 8, 8),
+ torch.float32,
+ True, # uses reference image
+ ],
+]
+
+# Test cases for dictionary transforms with static spatial shape
+TEST_CASES_STATIC_SHAPE = [
+ [
+ f"dict_static_shape_{len(shape)}d",
+ np.array([[1.0] * len(shape)], dtype=np.float32),
+ {"spatial_shape": shape},
+ (1, *shape),
+ np.float32,
+ ]
+ for shape in [(6, 6), (8, 8, 8), (10, 10, 10)]
+]
+
+# Test cases for dtype control
+TEST_CASES_DTYPE = [
+ [
+ f"dict_dtype_{str(dtype).replace('torch.', '')}",
+ np.array([[2.0, 3.0, 4.0]], dtype=np.float32),
+ {"sigma": 1.4, "dtype": dtype},
+ (1, 10, 10, 10),
+ dtype,
+ ]
+ for dtype in [torch.float16, torch.float32, torch.float64]
+]
+
+# Test cases for various sigma values
+TEST_CASES_SIGMA_VALUES = [
+ [
+ f"dict_sigma_{sigma}",
+ np.array([[4.0, 4.0, 4.0]], dtype=np.float32),
+ {"sigma": sigma, "spatial_shape": (8, 8, 8)},
+ (1, 8, 8, 8),
+ ]
+ for sigma in [0.5, 1.0, 2.0, 3.0]
+]
+
+
+class TestGenerateHeatmapd(unittest.TestCase):
+ @parameterized.expand(TEST_CASES_WITH_REF)
+ def test_dict_with_reference_meta(self, _, points, params, expected_shape, *_unused):
+ affine = torch.eye(4)
+ image = MetaTensor(torch.zeros((1, 8, 8, 8), dtype=torch.float32), affine=affine)
+ image.meta["spatial_shape"] = (8, 8, 8)
+ data = {"points": points, "image": image}
+
+ transform = GenerateHeatmapd(keys="points", heatmap_keys="heatmap", ref_image_keys="image", **params)
+ result = transform(data)
+ heatmap = result["heatmap"]
+
+ self.assertIsInstance(heatmap, MetaTensor)
+ self.assertEqual(tuple(heatmap.shape), expected_shape)
+ self.assertEqual(heatmap.meta["spatial_shape"], (8, 8, 8))
+ # The heatmap should inherit the reference image's affine
+ assert_allclose(heatmap.affine, image.affine, type_test=False)
+
+ # Check max values are normalized to 1.0
+ max_vals = heatmap.cpu().numpy().max(axis=tuple(range(1, len(expected_shape))))
+ np.testing.assert_allclose(max_vals, np.ones(expected_shape[0]), rtol=1e-5, atol=1e-5)
+
+ @parameterized.expand(TEST_CASES_STATIC_SHAPE)
+ def test_dict_static_shape(self, _, points, params, expected_shape, expected_dtype):
+ transform = GenerateHeatmapd(keys="points", heatmap_keys="heatmap", **params)
+ result = transform({"points": points})
+ heatmap = result["heatmap"]
+
+ self.assertIsInstance(heatmap, np.ndarray)
+ self.assertEqual(heatmap.shape, expected_shape)
+ self.assertEqual(heatmap.dtype, expected_dtype)
+
+ # Verify no NaN or Inf values
+ self.assertFalse(np.isnan(heatmap).any() or np.isinf(heatmap).any())
+
+ # Verify max value is 1.0 for normalized heatmaps
+ np.testing.assert_allclose(heatmap.max(), 1.0, rtol=1e-5)
+
+ def test_dict_missing_shape_raises(self):
+ # Without ref image or explicit spatial_shape, must raise
+ transform = GenerateHeatmapd(keys="points", heatmap_keys="heatmap")
+ with self.assertRaisesRegex(ValueError, "spatial_shape|ref_image_keys"):
+ transform({"points": np.zeros((1, 2), dtype=np.float32)})
+
+ @parameterized.expand(TEST_CASES_DTYPE)
+ def test_dict_dtype_control(self, _, points, params, expected_shape, expected_dtype):
+ ref = MetaTensor(torch.zeros((1, 10, 10, 10), dtype=torch.float32), affine=torch.eye(4))
+ d = {"pts": points, "img": ref}
+
+ tr = GenerateHeatmapd(keys="pts", heatmap_keys="hm", ref_image_keys="img", **params)
+ out = tr(d)
+ hm = out["hm"]
+
+ self.assertIsInstance(hm, MetaTensor)
+ self.assertEqual(tuple(hm.shape), expected_shape)
+ self.assertEqual(hm.dtype, expected_dtype)
+
+ @parameterized.expand(TEST_CASES_SIGMA_VALUES)
+ def test_dict_various_sigma(self, _, points, params, expected_shape):
+ transform = GenerateHeatmapd(keys="points", heatmap_keys="heatmap", **params)
+ result = transform({"points": points})
+ heatmap = result["heatmap"]
+
+ self.assertEqual(heatmap.shape, expected_shape)
+ # Verify heatmap is normalized
+ np.testing.assert_allclose(heatmap.max(), 1.0, rtol=1e-5)
+ # Verify no NaN or Inf
+ self.assertFalse(np.isnan(heatmap).any() or np.isinf(heatmap).any())
+
+ def test_dict_multiple_keys(self):
+ """Test dictionary transform with multiple input/output keys"""
+ points1 = np.array([[2.0, 2.0]], dtype=np.float32)
+ points2 = np.array([[4.0, 4.0]], dtype=np.float32)
+
+ data = {"pts1": points1, "pts2": points2}
+ transform = GenerateHeatmapd(
+ keys=["pts1", "pts2"], heatmap_keys=["hm1", "hm2"], spatial_shape=(8, 8), sigma=1.0
+ )
+
+ result = transform(data)
+
+ self.assertIn("hm1", result)
+ self.assertIn("hm2", result)
+ self.assertEqual(result["hm1"].shape, (1, 8, 8))
+ self.assertEqual(result["hm2"].shape, (1, 8, 8))
+
+ # Verify peaks are at different locations
+ self.assertNotEqual(np.argmax(result["hm1"]), np.argmax(result["hm2"]))
+
+ def test_dict_mismatched_heatmap_keys_length(self):
+ """Test ValueError when heatmap_keys length doesn't match keys"""
+ with self.assertRaises(ValueError):
+ GenerateHeatmapd(
+ keys=["pts1", "pts2"],
+ heatmap_keys=["hm1", "hm2", "hm3"], # Mismatch: 3 heatmap keys for 2 input keys
+ spatial_shape=(8, 8),
+ )
+
+ def test_dict_mismatched_ref_image_keys_length(self):
+ """Test ValueError when ref_image_keys length doesn't match keys"""
+ with self.assertRaises(ValueError):
+ GenerateHeatmapd(
+ keys=["pts1", "pts2"],
+ heatmap_keys=["hm1", "hm2"],
+ ref_image_keys=["img1", "img2", "img3"], # Mismatch: 3 ref keys for 2 input keys
+ spatial_shape=(8, 8),
+ )
+
+ def test_dict_per_key_spatial_shape_mismatch(self):
+ """Test ValueError when per-key spatial_shape length doesn't match keys"""
+ with self.assertRaises(ValueError):
+ GenerateHeatmapd(
+ keys=["pts1", "pts2"],
+ heatmap_keys=["hm1", "hm2"],
+ spatial_shape=[(8, 8), (8, 8), (8, 8)], # Mismatch: 3 shapes for 2 keys
+ sigma=1.0,
+ )
+
+ def test_metatensor_points_with_ref(self):
+ """Test MetaTensor points with reference image - documents current behavior"""
+ from monai.data import MetaTensor
+
+ # Create MetaTensor points with non-identity affine
+ points_affine = torch.tensor([[2.0, 0, 0, 0], [0, 2.0, 0, 0], [0, 0, 2.0, 0], [0, 0, 0, 1.0]])
+ points = MetaTensor(torch.tensor([[2.5, 2.5, 3.0], [5.0, 5.0, 4.0]], dtype=torch.float32), affine=points_affine)
+
+ # Reference image with identity affine
+ ref_affine = torch.eye(4)
+ image = MetaTensor(torch.zeros((1, 8, 8, 8), dtype=torch.float32), affine=ref_affine)
+ image.meta["spatial_shape"] = (8, 8, 8)
+
+ data = {"points": points, "image": image}
+ transform = GenerateHeatmapd(keys="points", heatmap_keys="heatmap", ref_image_keys="image", sigma=2.0)
+ result = transform(data)
+ heatmap = result["heatmap"]
+
+ self.assertIsInstance(heatmap, MetaTensor)
+ self.assertEqual(tuple(heatmap.shape), (2, 8, 8, 8))
+
+ # Heatmap should inherit affine from the reference image
+ assert_allclose(heatmap.affine, image.affine, type_test=False)
+
+
+if __name__ == "__main__":
+ unittest.main()
From 02c5e0db8433125793e3389ae2a83b94daadf8ed Mon Sep 17 00:00:00 2001
From: ytl0623
Date: Thu, 27 Nov 2025 18:24:38 +0800
Subject: [PATCH 34/61] =?UTF-8?q?Added=20an=20optional=5Fimport=20check=20?=
=?UTF-8?q?for=20onnxruntime=20and=20applied=20the=20@unitt=E2=80=A6=20(#8?=
=?UTF-8?q?641)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
…est.skipUnless decorator to the test_onnx method.
Fixes #8533 .
### Description
Added an optional_import check for onnxruntime and applied the
@unittest.skipUnless decorator to the test_onnx method. This ensures the
ONNX tests are automatically skipped if the onnxruntime dependency is
missing, preventing the test suite from hanging.
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Breaking change (fix or new feature that would cause existing
functionality to change).
- [ ] New tests added to cover the changes.
- [ ] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [ ] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [ ] In-line docstrings updated.
- [ ] Documentation updated, tested `make html` command in the `docs/`
folder.
---------
Signed-off-by: ytl0623
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: YunLiu <55491388+KumoLiu@users.noreply.github.com>
Signed-off-by: jirka
---
tests/apps/detection/networks/test_retinanet.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/apps/detection/networks/test_retinanet.py b/tests/apps/detection/networks/test_retinanet.py
index cb4129f7ed..3f4721a755 100644
--- a/tests/apps/detection/networks/test_retinanet.py
+++ b/tests/apps/detection/networks/test_retinanet.py
@@ -23,6 +23,7 @@
from tests.test_utils import dict_product, skip_if_quick, test_onnx_save, test_script_save
_, has_torchvision = optional_import("torchvision")
+_, has_onnxruntime = optional_import("onnxruntime")
device = "cuda" if torch.cuda.is_available() else "cpu"
num_anchors = 7
@@ -169,6 +170,7 @@ def test_script(self, model, input_param, input_shape):
test_script_save(net, data)
@parameterized.expand(TEST_CASES_TS)
+ @unittest.skipUnless(has_onnxruntime, "onnxruntime not installed")
def test_onnx(self, model, input_param, input_shape):
try:
idx = int(self.id().split("test_onnx_")[-1])
From 3bfcc43daed15cb4b857ccd80d332b72509267e0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 1 Dec 2025 11:00:38 +0000
Subject: [PATCH 35/61] Bump peter-evans/slash-command-dispatch from 4.0.0 to
5.0.0 (#8650)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps
[peter-evans/slash-command-dispatch](https://github.com/peter-evans/slash-command-dispatch)
from 4.0.0 to 5.0.0.
Release notes
Sourced from peter-evans/slash-command-dispatch's
releases.
Slash Command Dispatch v5.0.0
What's new in v5
What's Changed
New Contributors
Full Changelog: https://github.com/peter-evans/slash-command-dispatch/compare/v4.0.0...v5.0.0
Commits
e1b4e26
v5 (#431)
9a73d03
build(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 (#432)
9296fe3
build(deps): bump the github-actions group with 3 updates (#430)
59ebb73
ci: update dependabot config
cf4bfd0
build(deps): bump peter-evans/create-or-update-comment from 4 to 5 (#428)
a94182a
build(deps-dev): bump @vercel/ncc from 0.38.3 to 0.38.4
(#426)
bff1f2d
build(deps): bump actions/setup-node from 4 to 5 (#423)
2498e8f
build(deps): bump actions/checkout from 4 to 5 (#421)
f6a8811
build(deps-dev): bump eslint-plugin-prettier from 5.5.3 to 5.5.4 (#420)
9e6060d
build(deps): bump actions/download-artifact from 4 to 5 (#419)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Signed-off-by: jirka
---
.github/workflows/chatops.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/chatops.yml b/.github/workflows/chatops.yml
index 6f3b1c293d..d633c483ca 100644
--- a/.github/workflows/chatops.yml
+++ b/.github/workflows/chatops.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: dispatch
- uses: peter-evans/slash-command-dispatch@v4.0.0
+ uses: peter-evans/slash-command-dispatch@v5.0.0
with:
token: ${{ secrets.PR_MAINTAIN }}
reaction-token: ${{ secrets.GITHUB_TOKEN }}
From 1bb3e63ee2487291b33e3690db9d3d4603ac21f3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Dec 2025 04:53:45 +0000
Subject: [PATCH 36/61] Bump al-cheb/configure-pagefile-action from 1.4 to 1.5
(#8648)
Bumps
[al-cheb/configure-pagefile-action](https://github.com/al-cheb/configure-pagefile-action)
from 1.4 to 1.5.
Release notes
Sourced from al-cheb/configure-pagefile-action's
releases.
v1.5: Update task node version to 24
configure-pagefile-action
This action is intended to configure Pagefile size and location for
Windows images in GitHub Actions.
Available parameters
| Argument |
Description |
Format |
Default value |
minimum-size |
Set minimum size of Pagefile |
2048MB, 4GB, 8GB and etc |
8GB |
maximum-size |
Set maximum size of Pagefile |
The same like minimum-size |
minimum-size |
disk-root |
Set disk root where Pagefile will be located |
C: or D: |
D: |
Usage
name: CI
on: [push]
jobs:
build:
runs-on: windows-latest
steps:
- name: configure Pagefile
uses: al-cheb/configure-pagefile-action@v1.5
with:
minimum-size: 8
- name: configure Pagefile
uses: al-cheb/configure-pagefile-action@v1.5
with:
minimum-size: 8
maximum-size: 16
disk-root: "D:"
License
The scripts and documentation in this project are released under the
MIT
License
Commits
9b6da52
Merge pull request #24
from Goooler/node24
1a17a0f
Update action runtime to node 24
06a0aef
Merge pull request #23
from al-cheb/dependabot/npm_and_yarn/undici-5.29.0
ae10127
Bump undici from 5.28.4 to 5.29.0
93eb937
Merge pull request #22
from al-cheb/dependabot/npm_and_yarn/undici-5.28.4
f82ad0c
Bump undici from 5.28.3 to 5.28.4
b1b70e9
Merge pull request #21
from al-cheb/dependabot/npm_and_yarn/undici-5.28.3
9b7998f
Bump undici from 5.28.2 to 5.28.3
- See full diff in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Signed-off-by: jirka
---
.github/workflows/conda.yml | 2 +-
.github/workflows/pythonapp.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/conda.yml b/.github/workflows/conda.yml
index fd680e14c8..ec753adace 100644
--- a/.github/workflows/conda.yml
+++ b/.github/workflows/conda.yml
@@ -27,7 +27,7 @@ jobs:
steps:
- if: runner.os == 'windows'
name: Config pagefile (Windows only)
- uses: al-cheb/configure-pagefile-action@v1.4
+ uses: al-cheb/configure-pagefile-action@v1.5
with:
minimum-size: 8GB
maximum-size: 16GB
diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml
index 535ba187a8..ddc42b8975 100644
--- a/.github/workflows/pythonapp.yml
+++ b/.github/workflows/pythonapp.yml
@@ -65,7 +65,7 @@ jobs:
steps:
- if: runner.os == 'windows'
name: Config pagefile (Windows only)
- uses: al-cheb/configure-pagefile-action@v1.4
+ uses: al-cheb/configure-pagefile-action@v1.5
with:
minimum-size: 8GB
maximum-size: 16GB
From 3beefbd93d941ff96dc94f855a6317b17f68f317 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Dec 2025 07:11:49 +0000
Subject: [PATCH 37/61] Bump actions/checkout from 4 to 6 (#8649)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to
6.
Release notes
Sourced from actions/checkout's
releases.
v6.0.0
What's Changed
Full Changelog: https://github.com/actions/checkout/compare/v5.0.0...v6.0.0
v6-beta
What's Changed
Updated persist-credentials to store the credentials under
$RUNNER_TEMP instead of directly in the local git
config.
This requires a minimum Actions Runner version of v2.329.0
to access the persisted credentials for Docker
container action scenarios.
v5.0.1
What's Changed
Full Changelog: https://github.com/actions/checkout/compare/v5...v5.0.1
v5.0.0
What's Changed
⚠️ Minimum Compatible Runner Version
v2.327.1
Release
Notes
Make sure your runner is updated to this version or newer to use this
release.
Full Changelog: https://github.com/actions/checkout/compare/v4...v5.0.0
v4.3.1
What's Changed
Full Changelog: https://github.com/actions/checkout/compare/v4...v4.3.1
v4.3.0
What's Changed
... (truncated)
Changelog
Sourced from actions/checkout's
changelog.
Changelog
V6.0.0
V5.0.1
V5.0.0
V4.3.1
V4.3.0
v4.2.2
v4.2.1
v4.2.0
v4.1.7
v4.1.6
v4.1.5
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
---------
Signed-off-by: dependabot[bot]
Signed-off-by: Yun Liu
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Yun Liu
Signed-off-by: jirka
---
.github/workflows/codeql-analysis.yml | 2 +-
.github/workflows/conda.yml | 2 +-
.github/workflows/cron-ngc-bundle.yml | 2 +-
.github/workflows/cron.yml | 8 ++++----
.github/workflows/docker.yml | 4 ++--
.github/workflows/integration.yml | 4 ++--
.github/workflows/pythonapp-gpu.yml | 2 +-
.github/workflows/pythonapp-min.yml | 6 +++---
.github/workflows/pythonapp.yml | 8 ++++----
.github/workflows/release.yml | 6 +++---
.github/workflows/setupapp.yml | 6 +++---
.github/workflows/weekly-preview.yml | 4 ++--
12 files changed, 27 insertions(+), 27 deletions(-)
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 10ca752a5e..5166517cbe 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
diff --git a/.github/workflows/conda.yml b/.github/workflows/conda.yml
index ec753adace..6103e30ae6 100644
--- a/.github/workflows/conda.yml
+++ b/.github/workflows/conda.yml
@@ -32,7 +32,7 @@ jobs:
minimum-size: 8GB
maximum-size: 16GB
disk-root: "D:"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Clean up disk space
run: |
find /opt/hostedtoolcache/* -maxdepth 0 ! -name 'Python' -exec rm -rf {} \;
diff --git a/.github/workflows/cron-ngc-bundle.yml b/.github/workflows/cron-ngc-bundle.yml
index c9bebc7bff..3bdebd5730 100644
--- a/.github/workflows/cron-ngc-bundle.yml
+++ b/.github/workflows/cron-ngc-bundle.yml
@@ -17,7 +17,7 @@ jobs:
if: github.repository == 'Project-MONAI/MONAI'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python 3.9
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml
index f33faf8c1a..dca48dd9c8 100644
--- a/.github/workflows/cron.yml
+++ b/.github/workflows/cron.yml
@@ -32,7 +32,7 @@ jobs:
options: "--gpus all"
runs-on: [self-hosted, linux, x64, common]
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: apt install
run: |
apt-get update
@@ -82,7 +82,7 @@ jobs:
options: "--gpus all"
runs-on: [self-hosted, linux, x64, integration]
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Install APT dependencies
run: |
apt-get update
@@ -131,7 +131,7 @@ jobs:
options: "--gpus all"
runs-on: [self-hosted, linux, x64, integration]
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install the dependencies
@@ -233,7 +233,7 @@ jobs:
options: "--gpus all --ipc=host"
runs-on: [self-hosted, linux, x64, integration]
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Install MONAI
id: monai-install
run: |
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 4741dca858..25cffc09c3 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -21,7 +21,7 @@ jobs:
if: ${{ false }} # disable docker build job project-monai/monai#7450
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
# full history so that we can git describe
with:
ref: dev
@@ -53,7 +53,7 @@ jobs:
needs: versioning_dev
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
ref: dev
- name: Download version
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 597f59f767..54b5a3f381 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: [self-hosted, linux, x64, command]
steps:
# checkout the pull request branch
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
token: ${{ secrets.PR_MAINTAIN }}
repository: ${{ github.event.client_payload.pull_request.head.repo.full_name }}
@@ -89,7 +89,7 @@ jobs:
runs-on: [self-hosted, linux, x64, command1]
steps:
# checkout the pull request branch
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
token: ${{ secrets.PR_MAINTAIN }}
repository: ${{ github.event.client_payload.pull_request.head.repo.full_name }}
diff --git a/.github/workflows/pythonapp-gpu.yml b/.github/workflows/pythonapp-gpu.yml
index 347b49bba3..036ef50f41 100644
--- a/.github/workflows/pythonapp-gpu.yml
+++ b/.github/workflows/pythonapp-gpu.yml
@@ -44,7 +44,7 @@ jobs:
options: --gpus all --env NVIDIA_DISABLE_REQUIRE=true # workaround for unsatisfied condition: cuda>=11.6
runs-on: [self-hosted, linux, x64, common]
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: apt install
if: github.event.pull_request.merged != true
run: |
diff --git a/.github/workflows/pythonapp-min.yml b/.github/workflows/pythonapp-min.yml
index 34a328672a..844d8c844e 100644
--- a/.github/workflows/pythonapp-min.yml
+++ b/.github/workflows/pythonapp-min.yml
@@ -30,7 +30,7 @@ jobs:
os: [windows-latest, macOS-latest, ubuntu-latest]
timeout-minutes: 40
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python 3.9
uses: actions/setup-python@v6
with:
@@ -79,7 +79,7 @@ jobs:
python-version: ['3.9', '3.10', '3.11', '3.12']
timeout-minutes: 40
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
@@ -128,7 +128,7 @@ jobs:
pytorch-version: ['2.5.1', '2.6.0', '2.7.1', '2.8.0']
timeout-minutes: 40
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python 3.9
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml
index ddc42b8975..fca9136763 100644
--- a/.github/workflows/pythonapp.yml
+++ b/.github/workflows/pythonapp.yml
@@ -28,7 +28,7 @@ jobs:
matrix:
opt: ["codeformat", "pytype", "mypy"]
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python 3.9
uses: actions/setup-python@v6
with:
@@ -70,7 +70,7 @@ jobs:
minimum-size: 8GB
maximum-size: 16GB
disk-root: "D:"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python 3.9
uses: actions/setup-python@v6
with:
@@ -129,7 +129,7 @@ jobs:
QUICKTEST: True
shell: bash
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python 3.9
@@ -213,7 +213,7 @@ jobs:
build-docs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python 3.9
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3a8e8ffdf6..fe1f8ea411 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -15,7 +15,7 @@ jobs:
matrix:
python-version: ['3.9', '3.10', '3.11']
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
@@ -93,7 +93,7 @@ jobs:
needs: packaging
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
# full history so that we can git describe
with:
fetch-depth: 0
@@ -125,7 +125,7 @@ jobs:
needs: versioning
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Download version
uses: actions/download-artifact@v6
with:
diff --git a/.github/workflows/setupapp.yml b/.github/workflows/setupapp.yml
index acd72eaf6f..46e02592df 100644
--- a/.github/workflows/setupapp.yml
+++ b/.github/workflows/setupapp.yml
@@ -28,7 +28,7 @@ jobs:
options: --gpus all
runs-on: [self-hosted, linux, x64, integration]
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: cache weekly timestamp
id: pip-cache
run: |
@@ -83,7 +83,7 @@ jobs:
matrix:
python-version: ['3.9', '3.10', '3.11']
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
@@ -159,7 +159,7 @@ jobs:
python -c 'import monai; monai.config.print_config()'
- name: Get the test cases (dev branch only)
if: github.ref == 'refs/heads/dev'
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
ref: dev
- name: Quick test installed (dev branch only)
diff --git a/.github/workflows/weekly-preview.yml b/.github/workflows/weekly-preview.yml
index 93d6f67f09..44618b58d8 100644
--- a/.github/workflows/weekly-preview.yml
+++ b/.github/workflows/weekly-preview.yml
@@ -11,7 +11,7 @@ jobs:
matrix:
opt: ["codeformat", "pytype", "mypy"]
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python 3.9
uses: actions/setup-python@v6
with:
@@ -42,7 +42,7 @@ jobs:
if: github.repository == 'Project-MONAI/MONAI'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
ref: dev
fetch-depth: 0
From c8bb696541fce54dfc9a83580033cbcad4e78d38 Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Tue, 30 Dec 2025 12:14:52 +0100
Subject: [PATCH 38/61] Update monai/losses/perceptual.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Signed-off-by: jirka
---
monai/losses/perceptual.py | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/monai/losses/perceptual.py b/monai/losses/perceptual.py
index aed1f26b86..720f93a1f4 100644
--- a/monai/losses/perceptual.py
+++ b/monai/losses/perceptual.py
@@ -95,11 +95,8 @@ def __init__(
if network_type.lower() not in list(PercetualNetworkType):
raise ValueError(
- "Unrecognised criterion entered for Perceptual Loss. Must be one in: {}".format(
- ", ".join(PercetualNetworkType)
- )
+ f"Unrecognised criterion entered for Perceptual Loss. Must be one in: {', '.join(PercetualNetworkType)}"
)
-
if cache_dir:
torch.hub.set_dir(cache_dir)
# raise a warning that this may change the default cache dir for all torch.hub calls
From 9b0464710879585078d6e44a943a2e38d79f594e Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Mon, 5 Jan 2026 03:00:35 +0100
Subject: [PATCH 39/61] resolves CI's drive-out-of-space by prune caching and
use torch fro CPU (#8673)
This pull request simplifies the caching setup for pip dependencies in
the GitHub Actions workflow and ensures that all pip installs use the
PyTorch CPU wheel index. The main changes involve replacing custom cache
steps with the built-in `cache: 'pip'` option in `actions/setup-python`,
and adding the `--extra-index-url` flag to all relevant pip install
commands for consistent dependency resolution.
**Workflow caching improvements:**
* Replaced custom pip cache steps (using `actions/cache` and manual
timestamp keys) with the built-in `cache: 'pip'` option in
`actions/setup-python`, streamlining cache management across all jobs.
[[1]](diffhunk://#diff-cbf97851bdfebccf2fcdd8848c6a93adb8d8affd5c6b1faf00238d528c3ef6cbL36-R36)
[[2]](diffhunk://#diff-cbf97851bdfebccf2fcdd8848c6a93adb8d8affd5c6b1faf00238d528c3ef6cbR69-L93)
[[3]](diffhunk://#diff-cbf97851bdfebccf2fcdd8848c6a93adb8d8affd5c6b1faf00238d528c3ef6cbL139-R127)
[[4]](diffhunk://#diff-cbf97851bdfebccf2fcdd8848c6a93adb8d8affd5c6b1faf00238d528c3ef6cbL221-R194)
**Dependency installation updates:**
* Updated all `pip install` commands to include `--extra-index-url
https://download.pytorch.org/whl/cpu`, ensuring that PyTorch and related
packages are consistently installed from the CPU wheel index. This
affects installation from wheels, tarballs, requirements files, and
direct package installs.
[[1]](diffhunk://#diff-cbf97851bdfebccf2fcdd8848c6a93adb8d8affd5c6b1faf00238d528c3ef6cbL139-R127)
[[2]](diffhunk://#diff-cbf97851bdfebccf2fcdd8848c6a93adb8d8affd5c6b1faf00238d528c3ef6cbL187-R156)
[[3]](diffhunk://#diff-cbf97851bdfebccf2fcdd8848c6a93adb8d8affd5c6b1faf00238d528c3ef6cbL198-R167)
[[4]](diffhunk://#diff-cbf97851bdfebccf2fcdd8848c6a93adb8d8affd5c6b1faf00238d528c3ef6cbL208-R177)
[[5]](diffhunk://#diff-cbf97851bdfebccf2fcdd8848c6a93adb8d8affd5c6b1faf00238d528c3ef6cbL221-R194)
---------
Signed-off-by: jirka
---
.github/workflows/pythonapp.yml | 60 +++++----------------------------
setup.py | 2 +-
2 files changed, 10 insertions(+), 52 deletions(-)
diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml
index fca9136763..5fdec1e11e 100644
--- a/.github/workflows/pythonapp.yml
+++ b/.github/workflows/pythonapp.yml
@@ -33,16 +33,7 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: '3.9'
- - name: cache weekly timestamp
- id: pip-cache
- run: |
- echo "datew=$(date '+%Y-%V')" >> $GITHUB_OUTPUT
- - name: cache for pip
- uses: actions/cache@v4
- id: cache
- with:
- path: ~/.cache/pip
- key: ${{ runner.os }}-pip-${{ steps.pip-cache.outputs.datew }}
+ cache: 'pip'
- name: Install dependencies
run: |
find /opt/hostedtoolcache/* -maxdepth 0 ! -name 'Python' -exec rm -rf {} \;
@@ -75,22 +66,11 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: '3.9'
+ cache: 'pip'
- name: Prepare pip wheel
run: |
which python
python -m pip install --upgrade pip wheel
- - name: cache weekly timestamp
- id: pip-cache
- run: |
- echo "datew=$(date '+%Y-%V')" >> $GITHUB_OUTPUT
- echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- shell: bash
- - name: cache for pip
- uses: actions/cache@v4
- id: cache
- with:
- path: ${{ steps.pip-cache.outputs.dir }}
- key: ${{ matrix.os }}-latest-pip-${{ steps.pip-cache.outputs.datew }}
- if: runner.os == 'windows'
name: Install torch cpu from pytorch.org (Windows only)
run: |
@@ -136,18 +116,7 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: '3.9'
- - name: cache weekly timestamp
- id: pip-cache
- run: |
- echo "datew=$(date '+%Y-%V')" >> $GITHUB_OUTPUT
- - name: cache for pip
- uses: actions/cache@v4
- id: cache
- with:
- path: |
- ~/.cache/pip
- ~/.cache/torch
- key: ${{ runner.os }}-pip-${{ steps.pip-cache.outputs.datew }}
+ cache: 'pip'
- name: Install dependencies
run: |
find /opt/hostedtoolcache/* -maxdepth 0 ! -name 'Python' -exec rm -rf {} \;
@@ -155,7 +124,7 @@ jobs:
# install the latest pytorch for testing
# however, "pip install monai*.tar.gz" will build cpp/cuda with an isolated
# fresh torch installation according to pyproject.toml
- python -m pip install torch>=2.5.1 torchvision
+ python -m pip install torch>=2.5.1 torchvision --extra-index-url https://download.pytorch.org/whl/cpu
- name: Check packages
run: |
pip uninstall monai
@@ -184,7 +153,7 @@ jobs:
working-directory: ${{ steps.mktemp.outputs.tmp_dir }}
run: |
# install from wheel
- python -m pip install monai*.whl
+ python -m pip install monai*.whl --extra-index-url https://download.pytorch.org/whl/cpu
python -c 'import monai; monai.config.print_config()' 2>&1 | grep -iv "unknown"
python -c 'import monai; print(monai.__file__)'
python -m pip uninstall -y monai
@@ -195,7 +164,7 @@ jobs:
# install from tar.gz
name=$(ls *.tar.gz | head -n1)
echo $name
- python -m pip install $name[all]
+ python -m pip install $name[all] --extra-index-url https://download.pytorch.org/whl/cpu
python -c 'import monai; monai.config.print_config()' 2>&1 | grep -iv "unknown"
python -c 'import monai; print(monai.__file__)'
- name: Quick test
@@ -205,7 +174,7 @@ jobs:
cp ${{ steps.root.outputs.pwd }}/requirements*.txt .
cp -r ${{ steps.root.outputs.pwd }}/tests .
ls -al
- python -m pip install -r requirements-dev.txt
+ python -m pip install -r requirements-dev.txt --extra-index-url https://download.pytorch.org/whl/cpu
python -m unittest -v
env:
PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION: python # https://github.com/Project-MONAI/MONAI/issues/4354
@@ -218,22 +187,11 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: '3.9'
- - name: cache weekly timestamp
- id: pip-cache
- run: |
- echo "datew=$(date '+%Y-%V')" >> $GITHUB_OUTPUT
- - name: cache for pip
- uses: actions/cache@v4
- id: cache
- with:
- path: |
- ~/.cache/pip
- ~/.cache/torch
- key: ${{ runner.os }}-pip-${{ steps.pip-cache.outputs.datew }}
+ cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip wheel
- python -m pip install -r docs/requirements.txt
+ python -m pip install -r docs/requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu
- name: Make html
run: |
cd docs/
diff --git a/setup.py b/setup.py
index 576743c1f7..df05908bc2 100644
--- a/setup.py
+++ b/setup.py
@@ -146,6 +146,6 @@ def get_cmds():
cmdclass=get_cmds(),
packages=find_packages(exclude=("docs", "examples", "tests")),
zip_safe=False,
- package_data={"monai": ["py.typed", *jit_extension_source]},
+ package_data={"monai": ["py.typed", *jit_extension_source]}, # type: ignore[arg-type]
ext_modules=get_extensions(),
)
From 21d2289a7ca1cfb5e387c8e34d1600507199772d Mon Sep 17 00:00:00 2001
From: jirka
Date: Mon, 5 Jan 2026 10:39:31 +0100
Subject: [PATCH 40/61] fix linter
Signed-off-by: jirka
---
runtests.sh | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/runtests.sh b/runtests.sh
index fd7df79722..fc53bdb2ac 100755
--- a/runtests.sh
+++ b/runtests.sh
@@ -606,9 +606,9 @@ then
if [ $doRuffFix = true ]
then
- ruff check --fix "$homedir"
+ ruff check --fix --unsafe-fixes --exclude versioneer.py --exclude "^monai/_version.py" "$homedir"
else
- ruff check "$homedir"
+ ruff check --exclude versioneer.py --exclude "^monai/_version.py" "$homedir"
fi
ruff_status=$?
From 93497a41654027a56253c9480c758c0082527178 Mon Sep 17 00:00:00 2001
From: jirka
Date: Mon, 5 Jan 2026 10:39:38 +0100
Subject: [PATCH 41/61] linting
Signed-off-by: jirka
---
monai/apps/auto3dseg/bundle_gen.py | 2 +-
monai/apps/detection/networks/retinanet_detector.py | 2 +-
monai/apps/detection/utils/detector_utils.py | 2 +-
tests/data/test_video_datasets.py | 2 +-
tests/transforms/test_load_image.py | 8 ++++----
tests/transforms/test_load_imaged.py | 4 ++--
tests/transforms/test_resample_to_match.py | 4 ++--
tests/transforms/test_resample_to_matchd.py | 4 ++--
tests/transforms/test_transform.py | 4 ++--
tests/utils/misc/test_monai_env_vars.py | 4 ++--
10 files changed, 18 insertions(+), 18 deletions(-)
diff --git a/monai/apps/auto3dseg/bundle_gen.py b/monai/apps/auto3dseg/bundle_gen.py
index 8a54d18be7..87a612cae2 100644
--- a/monai/apps/auto3dseg/bundle_gen.py
+++ b/monai/apps/auto3dseg/bundle_gen.py
@@ -396,7 +396,7 @@ def _download_algos_url(url: str, at_path: str) -> dict[str, dict[str, str]]:
try:
download_and_extract(url=url, filepath=algo_compressed_file, output_dir=os.path.dirname(at_path))
except Exception as e:
- msg = f"Download and extract of {url} failed, attempt {i+1}/{download_attempts}."
+ msg = f"Download and extract of {url} failed, attempt {i + 1}/{download_attempts}."
if i < download_attempts - 1:
warnings.warn(msg)
time.sleep(i)
diff --git a/monai/apps/detection/networks/retinanet_detector.py b/monai/apps/detection/networks/retinanet_detector.py
index 3220f80587..17e70d1371 100644
--- a/monai/apps/detection/networks/retinanet_detector.py
+++ b/monai/apps/detection/networks/retinanet_detector.py
@@ -787,7 +787,7 @@ def compute_anchor_matched_idxs(
)
if self.debug:
- print(f"Max box overlap between anchors and gt boxes: {torch.max(match_quality_matrix,dim=1)[0]}.")
+ print(f"Max box overlap between anchors and gt boxes: {torch.max(match_quality_matrix, dim=1)[0]}.")
if torch.max(matched_idxs_per_image) < 0:
warnings.warn(
diff --git a/monai/apps/detection/utils/detector_utils.py b/monai/apps/detection/utils/detector_utils.py
index a687476996..e2b0a1d305 100644
--- a/monai/apps/detection/utils/detector_utils.py
+++ b/monai/apps/detection/utils/detector_utils.py
@@ -91,7 +91,7 @@ def check_training_targets(
if boxes.numel() == 0:
warnings.warn(
f"Warning: Given target boxes has shape of {boxes.shape}. "
- f"The detector reshaped it with boxes = torch.reshape(boxes, [0, {2* spatial_dims}])."
+ f"The detector reshaped it with boxes = torch.reshape(boxes, [0, {2 * spatial_dims}])."
)
else:
raise ValueError(
diff --git a/tests/data/test_video_datasets.py b/tests/data/test_video_datasets.py
index b338885511..51615081eb 100644
--- a/tests/data/test_video_datasets.py
+++ b/tests/data/test_video_datasets.py
@@ -99,7 +99,7 @@ class TestVideoFileDataset(Base.TestVideoDataset):
@classmethod
def setUpClass(cls):
- super(__class__, cls).setUpClass()
+ super().setUpClass()
codecs = VideoFileDataset.get_available_codecs()
if ".mp4" in codecs.values():
fname = "endo.mp4"
diff --git a/tests/transforms/test_load_image.py b/tests/transforms/test_load_image.py
index 930a18f2ee..031e38272e 100644
--- a/tests/transforms/test_load_image.py
+++ b/tests/transforms/test_load_image.py
@@ -188,7 +188,7 @@ def get_data(self, _obj):
class TestLoadImage(unittest.TestCase):
@classmethod
def setUpClass(cls):
- super(__class__, cls).setUpClass()
+ super().setUpClass()
with skip_if_downloading_fails():
cls.tmpdir = tempfile.mkdtemp()
key = "DICOM_single"
@@ -203,7 +203,7 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.tmpdir)
- super(__class__, cls).tearDownClass()
+ super().tearDownClass()
@parameterized.expand(
[TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_3_1, TEST_CASE_4, TEST_CASE_4_1, TEST_CASE_5]
@@ -471,7 +471,7 @@ def test_channel_dim(self, input_param, filename, expected_shape):
class TestLoadImageMeta(unittest.TestCase):
@classmethod
def setUpClass(cls):
- super(__class__, cls).setUpClass()
+ super().setUpClass()
cls.tmpdir = tempfile.mkdtemp()
test_image = nib.Nifti1Image(np.random.rand(128, 128, 128), np.eye(4))
nib.save(test_image, os.path.join(cls.tmpdir, "im.nii.gz"))
@@ -480,7 +480,7 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.tmpdir)
- super(__class__, cls).tearDownClass()
+ super().tearDownClass()
@parameterized.expand(TESTS_META)
def test_correct(self, input_param, expected_shape, track_meta):
diff --git a/tests/transforms/test_load_imaged.py b/tests/transforms/test_load_imaged.py
index 27ed993022..9b21e89e49 100644
--- a/tests/transforms/test_load_imaged.py
+++ b/tests/transforms/test_load_imaged.py
@@ -157,7 +157,7 @@ def test_png(self):
class TestLoadImagedMeta(unittest.TestCase):
@classmethod
def setUpClass(cls):
- super(__class__, cls).setUpClass()
+ super().setUpClass()
cls.tmpdir = tempfile.mkdtemp()
test_image = nib.Nifti1Image(np.random.rand(128, 128, 128), np.eye(4))
cls.test_data = {}
@@ -168,7 +168,7 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.tmpdir)
- super(__class__, cls).tearDownClass()
+ super().tearDownClass()
@parameterized.expand(TESTS_META)
def test_correct(self, input_p, expected_shape, track_meta):
diff --git a/tests/transforms/test_resample_to_match.py b/tests/transforms/test_resample_to_match.py
index 4b0f10898c..27fd1cf525 100644
--- a/tests/transforms/test_resample_to_match.py
+++ b/tests/transforms/test_resample_to_match.py
@@ -48,7 +48,7 @@ def get_rand_fname(len=10, suffix=".nii.gz"):
class TestResampleToMatch(unittest.TestCase):
@classmethod
def setUpClass(cls):
- super(__class__, cls).setUpClass()
+ super().setUpClass()
cls.fnames = []
cls.tmpdir = tempfile.mkdtemp()
for key in ("0000_t2_tse_tra_4", "0000_ep2d_diff_tra_7"):
@@ -62,7 +62,7 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.tmpdir)
- super(__class__, cls).tearDownClass()
+ super().tearDownClass()
@parameterized.expand(itertools.product([NibabelReader, ITKReader], ["monai.data.NibabelWriter", ITKWriter]))
def test_correct(self, reader, writer):
diff --git a/tests/transforms/test_resample_to_matchd.py b/tests/transforms/test_resample_to_matchd.py
index 936d336a1f..43a5101882 100644
--- a/tests/transforms/test_resample_to_matchd.py
+++ b/tests/transforms/test_resample_to_matchd.py
@@ -38,7 +38,7 @@ def update_fname(d):
class TestResampleToMatchd(unittest.TestCase):
@classmethod
def setUpClass(cls):
- super(__class__, cls).setUpClass()
+ super().setUpClass()
cls.fnames = []
cls.tmpdir = tempfile.mkdtemp()
for key in ("0000_t2_tse_tra_4", "0000_ep2d_diff_tra_7"):
@@ -52,7 +52,7 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.tmpdir)
- super(__class__, cls).tearDownClass()
+ super().tearDownClass()
def test_correct(self):
transforms = Compose(
diff --git a/tests/transforms/test_transform.py b/tests/transforms/test_transform.py
index 9b05133391..34ac5141f0 100644
--- a/tests/transforms/test_transform.py
+++ b/tests/transforms/test_transform.py
@@ -33,7 +33,7 @@ class TestTransform(unittest.TestCase):
@classmethod
def setUpClass(cls):
- super(__class__, cls).setUpClass()
+ super().setUpClass()
cls.orig_value = str(MONAIEnvVars.debug())
@classmethod
@@ -42,7 +42,7 @@ def tearDownClass(cls):
os.environ["MONAI_DEBUG"] = cls.orig_value
else:
os.environ.pop("MONAI_DEBUG")
- super(__class__, cls).tearDownClass()
+ super().tearDownClass()
def test_raise(self):
for transform in (FaultyTransform(), mt.Lambda(faulty_lambda)):
diff --git a/tests/utils/misc/test_monai_env_vars.py b/tests/utils/misc/test_monai_env_vars.py
index f5ef28a0ac..4b1eb659dd 100644
--- a/tests/utils/misc/test_monai_env_vars.py
+++ b/tests/utils/misc/test_monai_env_vars.py
@@ -21,7 +21,7 @@ class TestMONAIEnvVars(unittest.TestCase):
@classmethod
def setUpClass(cls):
- super(__class__, cls).setUpClass()
+ super().setUpClass()
cls.orig_value = str(MONAIEnvVars.debug())
@classmethod
@@ -31,7 +31,7 @@ def tearDownClass(cls):
else:
os.environ.pop("MONAI_DEBUG")
print("MONAI debug value:", str(MONAIEnvVars.debug()))
- super(__class__, cls).tearDownClass()
+ super().tearDownClass()
def test_monai_env_vars(self):
for debug in (False, True):
From 8c08d3ebc2e97c5dfbf4b410ef398e99b524b96a Mon Sep 17 00:00:00 2001
From: jirka
Date: Mon, 5 Jan 2026 11:07:41 +0100
Subject: [PATCH 42/61] linting
Signed-off-by: jirka
---
monai/networks/nets/ahnet.py | 2 +-
monai/networks/nets/autoencoder.py | 6 +++---
monai/networks/nets/densenet.py | 2 +-
runtests.sh | 4 ++--
4 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/monai/networks/nets/ahnet.py b/monai/networks/nets/ahnet.py
index bdacfd23a0..fe2adac461 100644
--- a/monai/networks/nets/ahnet.py
+++ b/monai/networks/nets/ahnet.py
@@ -115,7 +115,7 @@ def __init__(
layer = Pseudo3DLayer(
spatial_dims, num_input_features + i * growth_rate, growth_rate, bn_size, dropout_prob
)
- self.add_module("denselayer%d" % (i + 1), layer)
+ self.add_module(f"denselayer{i + 1}", layer)
class UpTransition(nn.Sequential):
diff --git a/monai/networks/nets/autoencoder.py b/monai/networks/nets/autoencoder.py
index fa7003746b..d7de0a3e98 100644
--- a/monai/networks/nets/autoencoder.py
+++ b/monai/networks/nets/autoencoder.py
@@ -148,7 +148,7 @@ def _get_encode_module(
for i, (c, s) in enumerate(zip(channels, strides)):
layer = self._get_encode_layer(layer_channels, c, s, False)
- encode.add_module("encode_%i" % i, layer)
+ encode.add_module(f"encode_{i}", layer)
layer_channels = c
return encode, layer_channels
@@ -199,7 +199,7 @@ def _get_intermediate_module(self, in_channels: int, num_inter_units: int) -> tu
padding=self.padding,
)
- intermediate.add_module("inter_%i" % i, unit)
+ intermediate.add_module(f"inter_{i}", unit)
layer_channels = dc
return intermediate, layer_channels
@@ -215,7 +215,7 @@ def _get_decode_module(
for i, (c, s) in enumerate(zip(channels, strides)):
layer = self._get_decode_layer(layer_channels, c, s, i == (len(strides) - 1))
- decode.add_module("decode_%i" % i, layer)
+ decode.add_module(f"decode_{i}", layer)
layer_channels = c
return decode, layer_channels
diff --git a/monai/networks/nets/densenet.py b/monai/networks/nets/densenet.py
index 5ccb429c91..42463b2493 100644
--- a/monai/networks/nets/densenet.py
+++ b/monai/networks/nets/densenet.py
@@ -117,7 +117,7 @@ def __init__(
for i in range(layers):
layer = _DenseLayer(spatial_dims, in_channels, growth_rate, bn_size, dropout_prob, act=act, norm=norm)
in_channels += growth_rate
- self.add_module("denselayer%d" % (i + 1), layer)
+ self.add_module(f"denselayer{i + 1}", layer)
class _Transition(nn.Sequential):
diff --git a/runtests.sh b/runtests.sh
index fc53bdb2ac..18cb0ab73a 100755
--- a/runtests.sh
+++ b/runtests.sh
@@ -606,9 +606,9 @@ then
if [ $doRuffFix = true ]
then
- ruff check --fix --unsafe-fixes --exclude versioneer.py --exclude "^monai/_version.py" "$homedir"
+ ruff check --fix --unsafe-fixes --exclude versioneer.py --exclude "monai/_version.py" "$homedir"
else
- ruff check --exclude versioneer.py --exclude "^monai/_version.py" "$homedir"
+ ruff check --exclude versioneer.py --exclude "monai/_version.py" "$homedir"
fi
ruff_status=$?
From 03a0a4a98b08dc29151ed8762dff9e26da44f296 Mon Sep 17 00:00:00 2001
From: jirka
Date: Mon, 5 Jan 2026 11:12:31 +0100
Subject: [PATCH 43/61] linting
Signed-off-by: jirka
---
monai/networks/nets/fullyconnectednet.py | 6 +++---
monai/networks/nets/generator.py | 2 +-
monai/networks/nets/hovernet.py | 2 +-
monai/networks/nets/patchgan_discriminator.py | 4 ++--
monai/networks/nets/regressor.py | 2 +-
monai/networks/nets/spade_network.py | 4 ++--
monai/utils/profiling.py | 2 +-
7 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/monai/networks/nets/fullyconnectednet.py b/monai/networks/nets/fullyconnectednet.py
index fe9a8a0cd6..be179e5b59 100644
--- a/monai/networks/nets/fullyconnectednet.py
+++ b/monai/networks/nets/fullyconnectednet.py
@@ -76,7 +76,7 @@ def __init__(
prev_channels = self.in_channels
for i, c in enumerate(hidden_channels):
- self.add_module("hidden_%i" % i, self._get_layer(prev_channels, c, bias))
+ self.add_module(f"hidden_{i}", self._get_layer(prev_channels, c, bias))
prev_channels = c
self.add_module("output", nn.Linear(prev_channels, out_channels, bias))
@@ -136,7 +136,7 @@ def __init__(
prev_channels = self.in_channels
for i, c in enumerate(encode_channels):
- self.encode.add_module("encode_%i" % i, self._get_layer(prev_channels, c, bias))
+ self.encode.add_module(f"encode_{i}", self._get_layer(prev_channels, c, bias))
prev_channels = c
self.mu = nn.Linear(prev_channels, self.latent_size)
@@ -144,7 +144,7 @@ def __init__(
self.decodeL = nn.Linear(self.latent_size, prev_channels)
for i, c in enumerate(decode_channels):
- self.decode.add_module("decode%i" % i, self._get_layer(prev_channels, c, bias))
+ self.decode.add_module(f"decode{i}", self._get_layer(prev_channels, c, bias))
prev_channels = c
self.decode.add_module("final", nn.Linear(prev_channels, out_channels, bias))
diff --git a/monai/networks/nets/generator.py b/monai/networks/nets/generator.py
index 32428b2696..312069a063 100644
--- a/monai/networks/nets/generator.py
+++ b/monai/networks/nets/generator.py
@@ -97,7 +97,7 @@ def __init__(
for i, (c, s) in enumerate(zip(channels, strides)):
is_last = i == len(channels) - 1
layer = self._get_layer(echannel, c, s, is_last)
- self.conv.add_module("layer_%i" % i, layer)
+ self.conv.add_module(f"layer_{i}", layer)
echannel = c
def _get_layer(
diff --git a/monai/networks/nets/hovernet.py b/monai/networks/nets/hovernet.py
index b773af91d4..f0cb5ab74d 100644
--- a/monai/networks/nets/hovernet.py
+++ b/monai/networks/nets/hovernet.py
@@ -153,7 +153,7 @@ def __init__(
padding=padding,
)
_in_channels += out_channels
- self.add_module("denselayerdecoder%d" % (i + 1), layer)
+ self.add_module(f"denselayerdecoder{i + 1}", layer)
trans = _Transition(_in_channels, act=act, norm=norm)
self.add_module("bna_block", trans)
diff --git a/monai/networks/nets/patchgan_discriminator.py b/monai/networks/nets/patchgan_discriminator.py
index 74da917694..7327e8b94e 100644
--- a/monai/networks/nets/patchgan_discriminator.py
+++ b/monai/networks/nets/patchgan_discriminator.py
@@ -91,7 +91,7 @@ def __init__(
last_conv_kernel_size=last_conv_kernel_size,
)
- self.add_module("discriminator_%d" % i_, subnet_d)
+ self.add_module(f"discriminator_{i_}", subnet_d)
def forward(self, i: torch.Tensor) -> tuple[list[torch.Tensor], list[list[torch.Tensor]]]:
"""
@@ -192,7 +192,7 @@ def __init__(
padding=padding,
strides=stride,
)
- self.add_module("%d" % l_, layer)
+ self.add_module(f"{l_}", layer)
input_channels = output_channels
output_channels = output_channels * 2
diff --git a/monai/networks/nets/regressor.py b/monai/networks/nets/regressor.py
index a14d8f9345..9a6c05c267 100644
--- a/monai/networks/nets/regressor.py
+++ b/monai/networks/nets/regressor.py
@@ -96,7 +96,7 @@ def __init__(
for i, (c, s) in enumerate(zip(self.channels, self.strides)):
layer = self._get_layer(echannel, c, s, i == len(channels) - 1)
echannel = c # use the output channel number as the input for the next loop
- self.net.add_module("layer_%i" % i, layer)
+ self.net.add_module(f"layer_{i}", layer)
self.final_size = calculate_out_shape(self.final_size, kernel_size, s, padding) # type: ignore
self.final = self._get_final_layer((echannel,) + self.final_size)
diff --git a/monai/networks/nets/spade_network.py b/monai/networks/nets/spade_network.py
index a4707e89eb..20d2cc2ec1 100644
--- a/monai/networks/nets/spade_network.py
+++ b/monai/networks/nets/spade_network.py
@@ -161,7 +161,7 @@ def __init__(
if s_ / (2 ** len(channels)) != s_ // (2 ** len(channels)):
raise ValueError(
"Each dimension of your input must be divisible by 2 ** (autoencoder depth)."
- "The shape in position %d, %d is not divisible by %d. " % (s_ind, s_, len(channels))
+ f"The shape in position {s_ind}, {s_} is not divisible by {len(channels)}. "
)
self.input_shape = input_shape
self.latent_spatial_shape = [s_ // (2 ** len(self.channels)) for s_ in self.input_shape]
@@ -260,7 +260,7 @@ def __init__(
if s_ / (2 ** len(channels)) != s_ // (2 ** len(channels)):
raise ValueError(
"Each dimension of your input must be divisible by 2 ** (autoencoder depth)."
- "The shape in position %d, %d is not divisible by %d. " % (s_ind, s_, len(channels))
+ f"The shape in position {s_ind}, {s_} is not divisible by {len(channels)}. "
)
self.latent_spatial_shape = [s_ // (2 ** len(self.num_channels)) for s_ in input_shape]
diff --git a/monai/utils/profiling.py b/monai/utils/profiling.py
index 5c880bbe1f..5eda00459e 100644
--- a/monai/utils/profiling.py
+++ b/monai/utils/profiling.py
@@ -337,7 +337,7 @@ def profile_iter(self, name, iterable):
class _Iterable:
- def __iter__(_self): # noqa: B902, N805 pylint: disable=E0213
+ def __iter__(_self): # noqa: N805 pylint: disable=E0213
do_iter = True
orig_iter = iter(iterable)
caller = getframeinfo(stack()[1][0])
From 02f2b1a3dd28c3d038441afb58c51083f6334028 Mon Sep 17 00:00:00 2001
From: jirka
Date: Mon, 5 Jan 2026 12:37:47 +0100
Subject: [PATCH 44/61] linting
Signed-off-by: jirka
---
monai/losses/sure_loss.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/monai/losses/sure_loss.py b/monai/losses/sure_loss.py
index 30aed2a4dc..816decee8d 100644
--- a/monai/losses/sure_loss.py
+++ b/monai/losses/sure_loss.py
@@ -195,6 +195,7 @@ def forward(
)
# compute loss
- loss = sure_loss_function(operator, x, y_pseudo_gt, y_ref, self.eps, self.perturb_noise, complex_input)
+ eps = self.eps if self.eps is not None else -1.0
+ loss = sure_loss_function(operator, x, y_pseudo_gt, y_ref, eps, self.perturb_noise, complex_input)
return loss
From 4b759abbed6463ec05a9625cc772cf5ddb7bda4d Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Mon, 5 Jan 2026 11:33:14 +0100
Subject: [PATCH 45/61] B902
Signed-off-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Signed-off-by: jirka
---
monai/utils/profiling.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/monai/utils/profiling.py b/monai/utils/profiling.py
index 5eda00459e..5c880bbe1f 100644
--- a/monai/utils/profiling.py
+++ b/monai/utils/profiling.py
@@ -337,7 +337,7 @@ def profile_iter(self, name, iterable):
class _Iterable:
- def __iter__(_self): # noqa: N805 pylint: disable=E0213
+ def __iter__(_self): # noqa: B902, N805 pylint: disable=E0213
do_iter = True
orig_iter = iter(iterable)
caller = getframeinfo(stack()[1][0])
From 0ba2bf60b465c9c1e38695a85cecd9014a84381a Mon Sep 17 00:00:00 2001
From: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
Date: Mon, 5 Jan 2026 05:58:46 -0500
Subject: [PATCH 46/61] Consolidating Version Bumps (#8681)
### Description
This consolidates the version bumps from #8675, #8676, #8677, #8678, and
#8679 into one PR for faster CICD and review.
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Breaking change (fix or new feature that would cause existing
functionality to change).
- [ ] New tests added to cover the changes.
- [ ] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [ ] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [ ] In-line docstrings updated.
- [ ] Documentation updated, tested `make html` command in the `docs/`
folder.
---------
Signed-off-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
Signed-off-by: Yun Liu
Co-authored-by: Yun Liu
Signed-off-by: jirka
---
.github/workflows/chatops.yml | 2 +-
.github/workflows/cron-ngc-bundle.yml | 2 +-
.github/workflows/docker.yml | 2 +-
.github/workflows/integration.yml | 4 ++--
.github/workflows/pythonapp-min.yml | 6 +++---
.github/workflows/release.yml | 4 ++--
.github/workflows/setupapp.yml | 6 +++---
.github/workflows/weekly-preview.yml | 2 +-
8 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/.github/workflows/chatops.yml b/.github/workflows/chatops.yml
index d633c483ca..41b7a2e158 100644
--- a/.github/workflows/chatops.yml
+++ b/.github/workflows/chatops.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: dispatch
- uses: peter-evans/slash-command-dispatch@v5.0.0
+ uses: peter-evans/slash-command-dispatch@v5.0.2
with:
token: ${{ secrets.PR_MAINTAIN }}
reaction-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/cron-ngc-bundle.yml b/.github/workflows/cron-ngc-bundle.yml
index 3bdebd5730..766e5372b8 100644
--- a/.github/workflows/cron-ngc-bundle.yml
+++ b/.github/workflows/cron-ngc-bundle.yml
@@ -26,7 +26,7 @@ jobs:
id: pip-cache
run: echo "datew=$(date '+%Y-%V')" >> $GITHUB_OUTPUT
- name: cache for pip
- uses: actions/cache@v4
+ uses: actions/cache@v5
id: cache
with:
path: ~/.cache/pip
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 25cffc09c3..86ac1bf733 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -37,7 +37,7 @@ jobs:
python setup.py build
cat build/lib/monai/_version.py
- name: Upload version
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: _version.py
path: build/lib/monai/_version.py
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 54b5a3f381..56d015e190 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -22,7 +22,7 @@ jobs:
id: pip-cache
run: echo "datew=$(date '+%Y-%V')" >> $GITHUB_OUTPUT
- name: cache for pip
- uses: actions/cache@v4
+ uses: actions/cache@v5
id: cache
with:
path: |
@@ -98,7 +98,7 @@ jobs:
id: pip-cache
run: echo "datew=$(date '+%Y-%V')" >> $GITHUB_OUTPUT
- name: cache for pip
- uses: actions/cache@v4
+ uses: actions/cache@v5
id: cache
with:
path: |
diff --git a/.github/workflows/pythonapp-min.yml b/.github/workflows/pythonapp-min.yml
index 844d8c844e..84edbd9283 100644
--- a/.github/workflows/pythonapp-min.yml
+++ b/.github/workflows/pythonapp-min.yml
@@ -46,7 +46,7 @@ jobs:
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
shell: bash
- name: cache for pip
- uses: actions/cache@v4
+ uses: actions/cache@v5
id: cache
with:
path: ${{ steps.pip-cache.outputs.dir }}
@@ -96,7 +96,7 @@ jobs:
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
shell: bash
- name: cache for pip
- uses: actions/cache@v4
+ uses: actions/cache@v5
id: cache
with:
path: ${{ steps.pip-cache.outputs.dir }}
@@ -145,7 +145,7 @@ jobs:
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
shell: bash
- name: cache for pip
- uses: actions/cache@v4
+ uses: actions/cache@v5
id: cache
with:
path: ${{ steps.pip-cache.outputs.dir }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index fe1f8ea411..5d20d280e1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -66,7 +66,7 @@ jobs:
- if: matrix.python-version == '3.9' && startsWith(github.ref, 'refs/tags/')
name: Upload artifacts
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: dist
path: dist/
@@ -109,7 +109,7 @@ jobs:
python setup.py build
cat build/lib/monai/_version.py
- name: Upload version
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: _version.py
path: build/lib/monai/_version.py
diff --git a/.github/workflows/setupapp.yml b/.github/workflows/setupapp.yml
index 46e02592df..c12c424321 100644
--- a/.github/workflows/setupapp.yml
+++ b/.github/workflows/setupapp.yml
@@ -35,7 +35,7 @@ jobs:
echo "datew=$(date '+%Y-%V')" >> $GITHUB_OUTPUT
- name: cache for pip
if: ${{ startsWith(github.ref, 'refs/heads/dev') }}
- uses: actions/cache@v4
+ uses: actions/cache@v5
id: cache
with:
path: |
@@ -95,7 +95,7 @@ jobs:
run: |
echo "datew=$(date '+%Y-%V')" >> $GITHUB_OUTPUT
- name: cache for pip
- uses: actions/cache@v4
+ uses: actions/cache@v5
id: cache
with:
path: |
@@ -136,7 +136,7 @@ jobs:
run: |
echo "datew=$(date '+%Y-%V')" >> $GITHUB_OUTPUT
- name: cache for pip
- uses: actions/cache@v4
+ uses: actions/cache@v5
id: cache
with:
path: |
diff --git a/.github/workflows/weekly-preview.yml b/.github/workflows/weekly-preview.yml
index 44618b58d8..9f63c5bc90 100644
--- a/.github/workflows/weekly-preview.yml
+++ b/.github/workflows/weekly-preview.yml
@@ -21,7 +21,7 @@ jobs:
run: |
echo "datew=$(date '+%Y-%V')" >> $GITHUB_OUTPUT
- name: cache for pip
- uses: actions/cache@v4
+ uses: actions/cache@v5
id: cache
with:
path: ~/.cache/pip
From 8af4f6d95ce2ad31e49e814b9b269a0e12d402fc Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Mon, 5 Jan 2026 13:16:20 +0100
Subject: [PATCH 47/61] Update monai/losses/sure_loss.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Signed-off-by: jirka
---
monai/losses/sure_loss.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/monai/losses/sure_loss.py b/monai/losses/sure_loss.py
index 816decee8d..cc02317f72 100644
--- a/monai/losses/sure_loss.py
+++ b/monai/losses/sure_loss.py
@@ -45,7 +45,7 @@ def sure_loss_function(
y_ref: torch.Tensor | None = None,
eps: float = -1.0,
perturb_noise: torch.Tensor | None = None,
- complex_input: bool = False,
+ complex_input: bool | None = False,
) -> torch.Tensor:
"""
Args:
From 7bf51b4c76f9dd79fa829395591e81ab89a4903f Mon Sep 17 00:00:00 2001
From: "Yue (Knox) Liu" <64764840+yueyueL@users.noreply.github.com>
Date: Tue, 6 Jan 2026 01:00:22 +0800
Subject: [PATCH 48/61] Fix Zip Slip vulnerability in NGC private bundle
download (#8682)
Replaced the unsafe `zipfile.extractall()` in
`_download_from_ngc_private` with MONAI's safe extraction utility.
Prevents path traversal via crafted zip member paths (CWE-22).
### Description
This changes _download_from_ngc_private() to use the same safe zip
extraction path as the other bundle download sources. The previous code
used ZipFile.extractall() directly, which could allow Zip Slip path
traversal if a malicious archive is downloaded. Now extraction validates
member paths and keeps writes within the target directory.
---------
Signed-off-by: Yue (Knox) Liu <64764840+yueyueL@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: YunLiu <55491388+KumoLiu@users.noreply.github.com>
Signed-off-by: jirka
---
monai/bundle/scripts.py | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py
index 2046a6242a..9fdee6acd0 100644
--- a/monai/bundle/scripts.py
+++ b/monai/bundle/scripts.py
@@ -17,7 +17,6 @@
import re
import urllib
import warnings
-import zipfile
from collections.abc import Mapping, Sequence
from functools import partial
from pathlib import Path
@@ -30,7 +29,7 @@
from torch.cuda import is_available
from monai._version import get_versions
-from monai.apps.utils import _basename, download_url, extractall, get_logger
+from monai.apps.utils import _basename, _extract_zip, download_url, extractall, get_logger
from monai.bundle.config_parser import ConfigParser
from monai.bundle.utils import DEFAULT_INFERENCE, DEFAULT_METADATA, merge_kv
from monai.bundle.workflows import BundleWorkflow, ConfigWorkflow
@@ -288,9 +287,8 @@ def _download_from_ngc_private(
if remove_prefix:
filename = _remove_ngc_prefix(filename, prefix=remove_prefix)
extract_path = download_path / f"{filename}"
- with zipfile.ZipFile(zip_path, "r") as z:
- z.extractall(extract_path)
- logger.info(f"Writing into directory: {extract_path}.")
+ _extract_zip(zip_path, extract_path)
+ logger.info(f"Writing into directory: {extract_path}.")
def _get_ngc_token(api_key, retry=0):
From 5e23203421ceef04576552962cb2cb9b82f01250 Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Tue, 6 Jan 2026 14:13:15 +0100
Subject: [PATCH 49/61] lru_cache(maxsize=None)
Signed-off-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Signed-off-by: jirka
---
monai/metrics/utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py
index a451b1a770..98edabfaa6 100644
--- a/monai/metrics/utils.py
+++ b/monai/metrics/utils.py
@@ -465,7 +465,7 @@ def prepare_spacing(
ENCODING_KERNEL = {2: [[8, 4], [2, 1]], 3: [[[128, 64], [32, 16]], [[8, 4], [2, 1]]]}
-@cache
+@lru_cache(maxsize=None)
def _get_neighbour_code_to_normals_table(device=None):
"""
returns a lookup table. For every binary neighbour code (2x2x2 neighbourhood = 8 neighbours = 8 bits = 256 codes)
From ce7cfe5e451fa0e7ea7a4275cddb5a388673e2d3 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 6 Jan 2026 13:13:33 +0000
Subject: [PATCH 50/61] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
Signed-off-by: jirka
---
monai/metrics/utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py
index 98edabfaa6..45d0efe113 100644
--- a/monai/metrics/utils.py
+++ b/monai/metrics/utils.py
@@ -13,7 +13,7 @@
import warnings
from collections.abc import Iterable, Sequence
-from functools import cache, partial
+from functools import partial
from types import ModuleType
from typing import Any
From 3392955b853c6bc853e27b321a539aef4e4afa63 Mon Sep 17 00:00:00 2001
From: jirka
Date: Tue, 6 Jan 2026 14:34:02 +0100
Subject: [PATCH 51/61] yesqa
Signed-off-by: jirka
---
.pre-commit-config.yaml | 32 +++++++++----------
monai/apps/nnunet/nnunetv2_runner.py | 4 +--
monai/metrics/utils.py | 4 +--
monai/transforms/compose.py | 5 +--
monai/utils/profiling.py | 2 +-
pyproject.toml | 1 +
.../test_integration_lazy_samples.py | 2 +-
tests/integration/test_loader_semaphore.py | 1 -
tests/profile_subclass/pyspy_profiling.py | 2 --
tests/runner.py | 4 +--
tests/transforms/compose/test_compose.py | 2 +-
11 files changed, 27 insertions(+), 32 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index db2d0f7534..9c583bb518 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -37,22 +37,22 @@ repos:
^monai/_version.py
)
- - repo: https://github.com/asottile/yesqa
- rev: v1.5.0
- hooks:
- - id: yesqa
- name: Unused noqa
- additional_dependencies:
- - flake8>=3.8.1
- - flake8-bugbear<=24.2.6
- - flake8-comprehensions
- - pep8-naming
- exclude: |
- (?x)^(
- monai/__init__.py|
- docs/source/conf.py|
- tests/utils.py
- )$
+# - repo: https://github.com/asottile/yesqa
+# rev: v1.5.0
+# hooks:
+# - id: yesqa
+# name: Unused noqa
+# additional_dependencies:
+# - flake8>=3.8.1
+# - flake8-bugbear<=24.2.6
+# - flake8-comprehensions
+# - pep8-naming
+# exclude: |
+# (?x)^(
+# monai/__init__.py|
+# docs/source/conf.py|
+# tests/utils.py
+# )$
- repo: https://github.com/hadialqattan/pycln
rev: v2.5.0
diff --git a/monai/apps/nnunet/nnunetv2_runner.py b/monai/apps/nnunet/nnunetv2_runner.py
index 8a10849904..e2f2262793 100644
--- a/monai/apps/nnunet/nnunetv2_runner.py
+++ b/monai/apps/nnunet/nnunetv2_runner.py
@@ -18,7 +18,7 @@
from typing import Any
import monai
-from monai.apps.nnunet.utils import NNUNETMode as M # noqa: N814
+from monai.apps.nnunet.utils import NNUNETMode as M
from monai.apps.nnunet.utils import analyze_data, create_new_data_copy, create_new_dataset_json
from monai.bundle import ConfigParser
from monai.utils import ensure_tuple, optional_import
@@ -34,7 +34,7 @@
__all__ = ["nnUNetV2Runner"]
-class nnUNetV2Runner: # noqa: N801
+class nnUNetV2Runner:
"""
``nnUNetV2Runner`` provides an interface in MONAI to use `nnU-Net` V2 library to analyze, train, and evaluate
neural networks for medical image segmentation tasks.
diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py
index 45d0efe113..1cae02c3a7 100644
--- a/monai/metrics/utils.py
+++ b/monai/metrics/utils.py
@@ -13,7 +13,7 @@
import warnings
from collections.abc import Iterable, Sequence
-from functools import partial
+from functools import partial, lru_cache
from types import ModuleType
from typing import Any
@@ -465,7 +465,7 @@ def prepare_spacing(
ENCODING_KERNEL = {2: [[8, 4], [2, 1]], 3: [[[128, 64], [32, 16]], [[8, 4], [2, 1]]]}
-@lru_cache(maxsize=None)
+@lru_cache(maxsize=None) # noqa: UP033
def _get_neighbour_code_to_normals_table(device=None):
"""
returns a lookup table. For every binary neighbour code (2x2x2 neighbourhood = 8 neighbours = 8 bits = 256 codes)
diff --git a/monai/transforms/compose.py b/monai/transforms/compose.py
index e984c4f26a..dd11246324 100644
--- a/monai/transforms/compose.py
+++ b/monai/transforms/compose.py
@@ -29,12 +29,9 @@
# For backwards compatibility (so this still works: from monai.transforms.compose import MapTransform)
from monai.transforms.lazy.functional import apply_pending_transforms
from monai.transforms.traits import ThreadUnsafe
-from monai.transforms.transform import ( # noqa: F401
+from monai.transforms.transform import (
LazyTransform,
- MapTransform,
Randomizable,
- RandomizableTransform,
- Transform,
apply_transform,
)
from monai.utils import MAX_SEED, TraceKeys, TraceStatusKeys, ensure_tuple, get_seed
diff --git a/monai/utils/profiling.py b/monai/utils/profiling.py
index 5c880bbe1f..f9e64902ce 100644
--- a/monai/utils/profiling.py
+++ b/monai/utils/profiling.py
@@ -337,7 +337,7 @@ def profile_iter(self, name, iterable):
class _Iterable:
- def __iter__(_self): # noqa: B902, N805 pylint: disable=E0213
+ def __iter__(_self):
do_iter = True
orig_iter = iter(iterable)
caller = getframeinfo(stack()[1][0])
diff --git a/pyproject.toml b/pyproject.toml
index ef85068ad7..b23e1de95a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -47,6 +47,7 @@ select = [
"E", "F", "W", # flake8
"NPY", # NumPy specific rules
"UP", # pyupgrade
+ "RUF100", # aka yesqa
]
extend-ignore = [
"E741", # ambiguous variable name
diff --git a/tests/integration/test_integration_lazy_samples.py b/tests/integration/test_integration_lazy_samples.py
index 63d6b8f9d9..0d9dcd44f5 100644
--- a/tests/integration/test_integration_lazy_samples.py
+++ b/tests/integration/test_integration_lazy_samples.py
@@ -137,7 +137,7 @@ def run_training_test(root_dir, device="cuda:0", cachedataset=0, readers=(None,
ops = [0]
if len(item.applied_operations) > 1:
found = False
- for idx, n in enumerate(item.applied_operations): # noqa
+ for idx, n in enumerate(item.applied_operations):
if n["class"] == "RandCropByPosNegLabel":
found = True
break
diff --git a/tests/integration/test_loader_semaphore.py b/tests/integration/test_loader_semaphore.py
index 83557d830d..df1ba1c74a 100644
--- a/tests/integration/test_loader_semaphore.py
+++ b/tests/integration/test_loader_semaphore.py
@@ -15,7 +15,6 @@
import multiprocessing as mp
import unittest
-import monai # noqa
def w():
diff --git a/tests/profile_subclass/pyspy_profiling.py b/tests/profile_subclass/pyspy_profiling.py
index c1b0963ba9..c69eddec2f 100644
--- a/tests/profile_subclass/pyspy_profiling.py
+++ b/tests/profile_subclass/pyspy_profiling.py
@@ -17,9 +17,7 @@
import argparse
import torch
-from min_classes import SubTensor, SubWithTorchFunc # noqa: F401
-from monai.data import MetaTensor # noqa: F401
Tensor = torch.Tensor
diff --git a/tests/runner.py b/tests/runner.py
index 8079a26091..6b6e2c092a 100644
--- a/tests/runner.py
+++ b/tests/runner.py
@@ -32,14 +32,14 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.timed_tests = {}
- def startTest(self, test): # noqa: N802
+ def startTest(self, test):
"""Start timer, print test name, do normal test."""
self.start_time = time.time()
name = self.getDescription(test)
self.stream.write(f"Starting test: {name}...\n")
super().startTest(test)
- def stopTest(self, test): # noqa: N802
+ def stopTest(self, test):
"""On test end, get time, print, store and do normal behaviour."""
elapsed = time.time() - self.start_time
name = self.getDescription(test)
diff --git a/tests/transforms/compose/test_compose.py b/tests/transforms/compose/test_compose.py
index 12547f9ec2..96c6d4606f 100644
--- a/tests/transforms/compose/test_compose.py
+++ b/tests/transforms/compose/test_compose.py
@@ -280,7 +280,7 @@ def test_flatten_and_len(self):
self.assertEqual(len(t1), 8)
def test_backwards_compatible_imports(self):
- from monai.transforms.transform import MapTransform, RandomizableTransform, Transform # noqa: F401
+ pass
def test_list_extend_multi_sample_trait(self):
center_crop = mt.CenterSpatialCrop([128, 128])
From ecd6bb2aba5d794001ef6391019606150b20a74c Mon Sep 17 00:00:00 2001
From: jirka
Date: Tue, 6 Jan 2026 14:47:36 +0100
Subject: [PATCH 52/61] linting
Signed-off-by: jirka
---
monai/metrics/utils.py | 2 +-
monai/transforms/compose.py | 6 +-----
tests/integration/test_loader_semaphore.py | 1 -
tests/profile_subclass/pyspy_profiling.py | 1 -
4 files changed, 2 insertions(+), 8 deletions(-)
diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py
index 1cae02c3a7..17eb80cb64 100644
--- a/monai/metrics/utils.py
+++ b/monai/metrics/utils.py
@@ -13,7 +13,7 @@
import warnings
from collections.abc import Iterable, Sequence
-from functools import partial, lru_cache
+from functools import lru_cache, partial
from types import ModuleType
from typing import Any
diff --git a/monai/transforms/compose.py b/monai/transforms/compose.py
index dd11246324..95653ffbd4 100644
--- a/monai/transforms/compose.py
+++ b/monai/transforms/compose.py
@@ -29,11 +29,7 @@
# For backwards compatibility (so this still works: from monai.transforms.compose import MapTransform)
from monai.transforms.lazy.functional import apply_pending_transforms
from monai.transforms.traits import ThreadUnsafe
-from monai.transforms.transform import (
- LazyTransform,
- Randomizable,
- apply_transform,
-)
+from monai.transforms.transform import LazyTransform, Randomizable, apply_transform
from monai.utils import MAX_SEED, TraceKeys, TraceStatusKeys, ensure_tuple, get_seed
logger = get_logger(__name__)
diff --git a/tests/integration/test_loader_semaphore.py b/tests/integration/test_loader_semaphore.py
index df1ba1c74a..78baedc264 100644
--- a/tests/integration/test_loader_semaphore.py
+++ b/tests/integration/test_loader_semaphore.py
@@ -16,7 +16,6 @@
import unittest
-
def w():
pass
diff --git a/tests/profile_subclass/pyspy_profiling.py b/tests/profile_subclass/pyspy_profiling.py
index c69eddec2f..fac425f577 100644
--- a/tests/profile_subclass/pyspy_profiling.py
+++ b/tests/profile_subclass/pyspy_profiling.py
@@ -18,7 +18,6 @@
import torch
-
Tensor = torch.Tensor
NUM_REPEATS = 1000000
From 11f6d094c4c0b35daf8825adfceef372755e4dce Mon Sep 17 00:00:00 2001
From: Alexander Jaus
Date: Tue, 6 Jan 2026 06:54:31 +0100
Subject: [PATCH 53/61] Fix channel-first indices buffer for
distance_transform_edt (return_indices=True) #8656 (#8657)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fixes #8656
Distance_transform_edt indices preallocation to use channel-first (C,
spatial_dims, ...) layout for both torch/cuCIM and NumPy/SciPy paths,
resolving “indices array has wrong shape” errors when
return_indices=True.
### Description
```
import torch
from monai.transforms.utils import distance_transform_edt
img = torch.tensor([[[0, 0, 1],
[0, 1, 1],
[1, 1, 1]]], dtype=torch.float32) # shape (1, 3, 3)
# Previously raised: RuntimeError: indices array has wrong shape
indices = distance_transform_edt(img, return_distances=False, return_indices=True)
print(indices.shape) # now: (1, 2, 3, 3)
```
### Types of changes
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Breaking change (fix or new feature that would cause existing
functionality to change).
- [ ] New tests added to cover the changes.
- [x] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [x] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [ ] In-line docstrings updated.
- [ ] Documentation updated, tested `make html` command in the `docs/`
folder.
---------
Signed-off-by: alexanderjaus
Co-authored-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
Signed-off-by: jirka
---
monai/transforms/utils.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/monai/transforms/utils.py b/monai/transforms/utils.py
index b50508962f..4ad60483fd 100644
--- a/monai/transforms/utils.py
+++ b/monai/transforms/utils.py
@@ -2498,7 +2498,7 @@ def distance_transform_edt(
if return_indices:
dtype = torch.int32
if indices is None:
- indices = torch.zeros((img.dim(),) + img.shape, dtype=dtype) # type: ignore
+ indices = torch.zeros((img.shape[0],) + (img.dim() - 1,) + img.shape[1:], dtype=dtype) # type: ignore
else:
if not isinstance(indices, torch.Tensor) and indices.device != img.device:
raise TypeError("indices must be a torch.Tensor on the same device as img")
@@ -2532,7 +2532,7 @@ def distance_transform_edt(
raise TypeError("distances must be a numpy.ndarray of dtype float64")
if return_indices:
if indices is None:
- indices = np.zeros((img_.ndim,) + img_.shape, dtype=np.int32)
+ indices = np.zeros((img_.shape[0],) + (img_.ndim - 1,) + img_.shape[1:], dtype=np.int32)
else:
if not isinstance(indices, np.ndarray):
raise TypeError("indices must be a numpy.ndarray")
From f64575edfe14605401c140943e552b430dfca55d Mon Sep 17 00:00:00 2001
From: jirka
Date: Tue, 6 Jan 2026 15:13:14 +0100
Subject: [PATCH 54/61] linting
Signed-off-by: jirka
---
monai/apps/detection/utils/anchor_utils.py | 4 ++--
monai/apps/detection/utils/detector_utils.py | 2 +-
monai/apps/nnunet/nnunetv2_runner.py | 4 ++--
monai/auto3dseg/analyzer.py | 10 +++++-----
monai/data/wsi_reader.py | 2 +-
monai/losses/unified_focal_loss.py | 2 +-
monai/metrics/meandice.py | 2 +-
monai/networks/blocks/patchembedding.py | 2 +-
monai/networks/layers/factories.py | 2 +-
monai/transforms/croppad/array.py | 2 +-
monai/transforms/regularization/array.py | 2 +-
monai/utils/profiling.py | 2 +-
tests/integration/test_integration_lazy_samples.py | 5 +----
tests/runner.py | 4 ++--
14 files changed, 21 insertions(+), 24 deletions(-)
diff --git a/monai/apps/detection/utils/anchor_utils.py b/monai/apps/detection/utils/anchor_utils.py
index b750fe0de8..20f6fc6025 100644
--- a/monai/apps/detection/utils/anchor_utils.py
+++ b/monai/apps/detection/utils/anchor_utils.py
@@ -174,13 +174,13 @@ def generate_anchors(
if (self.spatial_dims >= 3) and (len(aspect_ratios_t.shape) != 2):
raise ValueError(
f"In {self.spatial_dims}-D image, aspect_ratios for each level should be \
- {len(aspect_ratios_t.shape)-1}-D. But got aspect_ratios with shape {aspect_ratios_t.shape}."
+ {len(aspect_ratios_t.shape) - 1}-D. But got aspect_ratios with shape {aspect_ratios_t.shape}."
)
if (self.spatial_dims >= 3) and (aspect_ratios_t.shape[1] != self.spatial_dims - 1):
raise ValueError(
f"In {self.spatial_dims}-D image, aspect_ratios for each level should has \
- shape (_,{self.spatial_dims-1}). But got aspect_ratios with shape {aspect_ratios_t.shape}."
+ shape (_,{self.spatial_dims - 1}). But got aspect_ratios with shape {aspect_ratios_t.shape}."
)
# if 2d, w:h = 1:aspect_ratios
diff --git a/monai/apps/detection/utils/detector_utils.py b/monai/apps/detection/utils/detector_utils.py
index e2b0a1d305..c22df38be1 100644
--- a/monai/apps/detection/utils/detector_utils.py
+++ b/monai/apps/detection/utils/detector_utils.py
@@ -95,7 +95,7 @@ def check_training_targets(
)
else:
raise ValueError(
- f"Expected target boxes to be a tensor of shape [N, {2* spatial_dims}], got {boxes.shape}.)."
+ f"Expected target boxes to be a tensor of shape [N, {2 * spatial_dims}], got {boxes.shape}.)."
)
if not torch.is_floating_point(boxes):
raise ValueError(f"Expected target boxes to be a float tensor, got {boxes.dtype}.")
diff --git a/monai/apps/nnunet/nnunetv2_runner.py b/monai/apps/nnunet/nnunetv2_runner.py
index e2f2262793..8a10849904 100644
--- a/monai/apps/nnunet/nnunetv2_runner.py
+++ b/monai/apps/nnunet/nnunetv2_runner.py
@@ -18,7 +18,7 @@
from typing import Any
import monai
-from monai.apps.nnunet.utils import NNUNETMode as M
+from monai.apps.nnunet.utils import NNUNETMode as M # noqa: N814
from monai.apps.nnunet.utils import analyze_data, create_new_data_copy, create_new_dataset_json
from monai.bundle import ConfigParser
from monai.utils import ensure_tuple, optional_import
@@ -34,7 +34,7 @@
__all__ = ["nnUNetV2Runner"]
-class nnUNetV2Runner:
+class nnUNetV2Runner: # noqa: N801
"""
``nnUNetV2Runner`` provides an interface in MONAI to use `nnU-Net` V2 library to analyze, train, and evaluate
neural networks for medical image segmentation tasks.
diff --git a/monai/auto3dseg/analyzer.py b/monai/auto3dseg/analyzer.py
index f3f52d3a95..be3ffb7ae7 100644
--- a/monai/auto3dseg/analyzer.py
+++ b/monai/auto3dseg/analyzer.py
@@ -285,7 +285,7 @@ def __call__(self, data):
d[self.stats_name] = report
torch.set_grad_enabled(restore_grad_state)
- logger.debug(f"Get image stats spent {time.time()-start}")
+ logger.debug(f"Get image stats spent {time.time() - start}")
return d
@@ -366,7 +366,7 @@ def __call__(self, data: Mapping) -> dict:
d[self.stats_name] = report
torch.set_grad_enabled(restore_grad_state)
- logger.debug(f"Get foreground image stats spent {time.time()-start}")
+ logger.debug(f"Get foreground image stats spent {time.time() - start}")
return d
@@ -535,7 +535,7 @@ def __call__(self, data: Mapping[Hashable, MetaTensor]) -> dict[Hashable, MetaTe
d[self.stats_name] = report # type: ignore[assignment]
torch.set_grad_enabled(restore_grad_state)
- logger.debug(f"Get label stats spent {time.time()-start}")
+ logger.debug(f"Get label stats spent {time.time() - start}")
return d # type: ignore[return-value]
@@ -913,9 +913,9 @@ def __init__(
for i, hist_params in enumerate(zip(self.hist_bins, self.hist_range)):
_hist_bins, _hist_range = hist_params
if not isinstance(_hist_bins, int) or _hist_bins < 0:
- raise ValueError(f"Expected {i+1}. hist_bins value to be positive integer but got {_hist_bins}")
+ raise ValueError(f"Expected {i + 1}. hist_bins value to be positive integer but got {_hist_bins}")
if not isinstance(_hist_range, list) or len(_hist_range) != 2:
- raise ValueError(f"Expected {i+1}. hist_range values to be list of length 2 but received {_hist_range}")
+ raise ValueError(f"Expected {i + 1}. hist_range values to be list of length 2 but received {_hist_range}")
def __call__(self, data: dict) -> dict:
"""
diff --git a/monai/data/wsi_reader.py b/monai/data/wsi_reader.py
index 2a4fe9f7a8..c66a2a4ceb 100644
--- a/monai/data/wsi_reader.py
+++ b/monai/data/wsi_reader.py
@@ -210,7 +210,7 @@ def get_valid_level(
# Set the default value if no resolution parameter is provided.
level = 0
if level >= n_levels:
- raise ValueError(f"The maximum level of this image is {n_levels-1} while level={level} is requested)!")
+ raise ValueError(f"The maximum level of this image is {n_levels - 1} while level={level} is requested)!")
return level
diff --git a/monai/losses/unified_focal_loss.py b/monai/losses/unified_focal_loss.py
index 8484eb67ed..06704c0104 100644
--- a/monai/losses/unified_focal_loss.py
+++ b/monai/losses/unified_focal_loss.py
@@ -217,7 +217,7 @@ def forward(self, y_pred: torch.Tensor, y_true: torch.Tensor) -> torch.Tensor:
y_true = one_hot(y_true, num_classes=self.num_classes)
if torch.max(y_true) != self.num_classes - 1:
- raise ValueError(f"Please make sure the number of classes is {self.num_classes-1}")
+ raise ValueError(f"Please make sure the number of classes is {self.num_classes - 1}")
n_pred_ch = y_pred.shape[1]
if self.to_onehot_y:
diff --git a/monai/metrics/meandice.py b/monai/metrics/meandice.py
index 0802cc3364..fedd94fb93 100644
--- a/monai/metrics/meandice.py
+++ b/monai/metrics/meandice.py
@@ -160,7 +160,7 @@ def aggregate(
_f = {}
if isinstance(self.return_with_label, bool):
for i, v in enumerate(f):
- _label_key = f"label_{i+1}" if not self.include_background else f"label_{i}"
+ _label_key = f"label_{i + 1}" if not self.include_background else f"label_{i}"
_f[_label_key] = round(v.item(), 4)
else:
for key, v in zip(self.return_with_label, f):
diff --git a/monai/networks/blocks/patchembedding.py b/monai/networks/blocks/patchembedding.py
index 2a05ef964c..a4caae68be 100644
--- a/monai/networks/blocks/patchembedding.py
+++ b/monai/networks/blocks/patchembedding.py
@@ -101,7 +101,7 @@ def __init__(
chars = (("h", "p1"), ("w", "p2"), ("d", "p3"))[:spatial_dims]
from_chars = "b c " + " ".join(f"({k} {v})" for k, v in chars)
to_chars = f"b ({' '.join([c[0] for c in chars])}) ({' '.join([c[1] for c in chars])} c)"
- axes_len = {f"p{i+1}": p for i, p in enumerate(patch_size)}
+ axes_len = {f"p{i + 1}": p for i, p in enumerate(patch_size)}
self.patch_embeddings = nn.Sequential(
Rearrange(f"{from_chars} -> {to_chars}", **axes_len), nn.Linear(self.patch_dim, hidden_size)
)
diff --git a/monai/networks/layers/factories.py b/monai/networks/layers/factories.py
index 29b72a4f37..9ea181974a 100644
--- a/monai/networks/layers/factories.py
+++ b/monai/networks/layers/factories.py
@@ -95,7 +95,7 @@ def add_factory_callable(self, name: str, func: Callable, desc: str | None = Non
self.add(name.upper(), description, func)
# append name to the docstring
assert self.__doc__ is not None
- self.__doc__ += f"{', ' if len(self.names)>1 else ' '}``{name}``"
+ self.__doc__ += f"{', ' if len(self.names) > 1 else ' '}``{name}``"
def add_factory_class(self, name: str, cls: type, desc: str | None = None) -> None:
"""
diff --git a/monai/transforms/croppad/array.py b/monai/transforms/croppad/array.py
index fc0bafbcc4..b23fbac7d9 100644
--- a/monai/transforms/croppad/array.py
+++ b/monai/transforms/croppad/array.py
@@ -290,7 +290,7 @@ def compute_pad_width(self, spatial_shape: Sequence[int]) -> tuple[tuple[int, in
else:
raise ValueError(
f"Unsupported spatial_border length: {len(spatial_border)}, available options are "
- f"[1, len(spatial_shape)={len(spatial_shape)}, 2*len(spatial_shape)={2*len(spatial_shape)}]."
+ f"[1, len(spatial_shape)={len(spatial_shape)}, 2*len(spatial_shape)={2 * len(spatial_shape)}]."
)
return tuple([(0, 0)] + data_pad_width) # type: ignore
diff --git a/monai/transforms/regularization/array.py b/monai/transforms/regularization/array.py
index 66a5116c1a..445a9340f2 100644
--- a/monai/transforms/regularization/array.py
+++ b/monai/transforms/regularization/array.py
@@ -41,7 +41,7 @@ def __init__(self, batch_size: int, alpha: float = 1.0) -> None:
"""
super().__init__()
if alpha <= 0:
- raise ValueError(f"Expected positive number, but got {alpha = }")
+ raise ValueError(f"Expected positive number, but got {alpha=}")
self.alpha = alpha
self.batch_size = batch_size
diff --git a/monai/utils/profiling.py b/monai/utils/profiling.py
index f9e64902ce..7bcaa80ad3 100644
--- a/monai/utils/profiling.py
+++ b/monai/utils/profiling.py
@@ -337,7 +337,7 @@ def profile_iter(self, name, iterable):
class _Iterable:
- def __iter__(_self):
+ def __iter__(self):
do_iter = True
orig_iter = iter(iterable)
caller = getframeinfo(stack()[1][0])
diff --git a/tests/integration/test_integration_lazy_samples.py b/tests/integration/test_integration_lazy_samples.py
index 0d9dcd44f5..601495bde4 100644
--- a/tests/integration/test_integration_lazy_samples.py
+++ b/tests/integration/test_integration_lazy_samples.py
@@ -136,13 +136,10 @@ def run_training_test(root_dir, device="cuda:0", cachedataset=0, readers=(None,
np.testing.assert_array_equal(in_seg.pending_operations, [])
ops = [0]
if len(item.applied_operations) > 1:
- found = False
for idx, n in enumerate(item.applied_operations):
if n["class"] == "RandCropByPosNegLabel":
- found = True
+ ops = item.applied_operations[idx]["extra_info"]["extra_info"]["cropped"]
break
- if found:
- ops = item.applied_operations[idx]["extra_info"]["extra_info"]["cropped"]
img_name = os.path.basename(item.meta["filename_or_obj"])
coords = f"{img_name} - {ops}"
print(coords)
diff --git a/tests/runner.py b/tests/runner.py
index 6b6e2c092a..28f0539d30 100644
--- a/tests/runner.py
+++ b/tests/runner.py
@@ -32,14 +32,14 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.timed_tests = {}
- def startTest(self, test):
+ def startTest(self, test): # NOQA: N802
"""Start timer, print test name, do normal test."""
self.start_time = time.time()
name = self.getDescription(test)
self.stream.write(f"Starting test: {name}...\n")
super().startTest(test)
- def stopTest(self, test):
+ def stopTest(self, test): # NOQA: N802
"""On test end, get time, print, store and do normal behaviour."""
elapsed = time.time() - self.start_time
name = self.getDescription(test)
From 93861bda14ac03ee2362e78da570509570986d53 Mon Sep 17 00:00:00 2001
From: jirka
Date: Tue, 6 Jan 2026 15:14:54 +0100
Subject: [PATCH 55/61] linting
Signed-off-by: jirka
---
.pre-commit-config.yaml | 32 ++++++++++++++++----------------
1 file changed, 16 insertions(+), 16 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9c583bb518..db2d0f7534 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -37,22 +37,22 @@ repos:
^monai/_version.py
)
-# - repo: https://github.com/asottile/yesqa
-# rev: v1.5.0
-# hooks:
-# - id: yesqa
-# name: Unused noqa
-# additional_dependencies:
-# - flake8>=3.8.1
-# - flake8-bugbear<=24.2.6
-# - flake8-comprehensions
-# - pep8-naming
-# exclude: |
-# (?x)^(
-# monai/__init__.py|
-# docs/source/conf.py|
-# tests/utils.py
-# )$
+ - repo: https://github.com/asottile/yesqa
+ rev: v1.5.0
+ hooks:
+ - id: yesqa
+ name: Unused noqa
+ additional_dependencies:
+ - flake8>=3.8.1
+ - flake8-bugbear<=24.2.6
+ - flake8-comprehensions
+ - pep8-naming
+ exclude: |
+ (?x)^(
+ monai/__init__.py|
+ docs/source/conf.py|
+ tests/utils.py
+ )$
- repo: https://github.com/hadialqattan/pycln
rev: v2.5.0
From 8372011d8cd7bccd53078911b1f30fd71fd67af3 Mon Sep 17 00:00:00 2001
From: jirka
Date: Tue, 6 Jan 2026 15:18:12 +0100
Subject: [PATCH 56/61] linting
Signed-off-by: jirka
---
monai/apps/nnunet/nnunetv2_runner.py | 4 ++--
monai/metrics/utils.py | 2 +-
tests/runner.py | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/monai/apps/nnunet/nnunetv2_runner.py b/monai/apps/nnunet/nnunetv2_runner.py
index 8a10849904..e2f2262793 100644
--- a/monai/apps/nnunet/nnunetv2_runner.py
+++ b/monai/apps/nnunet/nnunetv2_runner.py
@@ -18,7 +18,7 @@
from typing import Any
import monai
-from monai.apps.nnunet.utils import NNUNETMode as M # noqa: N814
+from monai.apps.nnunet.utils import NNUNETMode as M
from monai.apps.nnunet.utils import analyze_data, create_new_data_copy, create_new_dataset_json
from monai.bundle import ConfigParser
from monai.utils import ensure_tuple, optional_import
@@ -34,7 +34,7 @@
__all__ = ["nnUNetV2Runner"]
-class nnUNetV2Runner: # noqa: N801
+class nnUNetV2Runner:
"""
``nnUNetV2Runner`` provides an interface in MONAI to use `nnU-Net` V2 library to analyze, train, and evaluate
neural networks for medical image segmentation tasks.
diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py
index 17eb80cb64..972ec0061e 100644
--- a/monai/metrics/utils.py
+++ b/monai/metrics/utils.py
@@ -465,7 +465,7 @@ def prepare_spacing(
ENCODING_KERNEL = {2: [[8, 4], [2, 1]], 3: [[[128, 64], [32, 16]], [[8, 4], [2, 1]]]}
-@lru_cache(maxsize=None) # noqa: UP033
+@lru_cache(maxsize=None)
def _get_neighbour_code_to_normals_table(device=None):
"""
returns a lookup table. For every binary neighbour code (2x2x2 neighbourhood = 8 neighbours = 8 bits = 256 codes)
diff --git a/tests/runner.py b/tests/runner.py
index 28f0539d30..6b6e2c092a 100644
--- a/tests/runner.py
+++ b/tests/runner.py
@@ -32,14 +32,14 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.timed_tests = {}
- def startTest(self, test): # NOQA: N802
+ def startTest(self, test):
"""Start timer, print test name, do normal test."""
self.start_time = time.time()
name = self.getDescription(test)
self.stream.write(f"Starting test: {name}...\n")
super().startTest(test)
- def stopTest(self, test): # NOQA: N802
+ def stopTest(self, test):
"""On test end, get time, print, store and do normal behaviour."""
elapsed = time.time() - self.start_time
name = self.getDescription(test)
From 2d0014ac60a54f51873c144e6f0d61127b2d42b2 Mon Sep 17 00:00:00 2001
From: jirka
Date: Tue, 6 Jan 2026 15:18:36 +0100
Subject: [PATCH 57/61] Revert "[pre-commit.ci] auto fixes from pre-commit.com
hooks"
This reverts commit 0b4c68631ccbcc0f3ab68079bf95b595d972dde1.
Signed-off-by: jirka
---
monai/apps/nnunet/nnunetv2_runner.py | 4 ++--
tests/runner.py | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/monai/apps/nnunet/nnunetv2_runner.py b/monai/apps/nnunet/nnunetv2_runner.py
index e2f2262793..8a10849904 100644
--- a/monai/apps/nnunet/nnunetv2_runner.py
+++ b/monai/apps/nnunet/nnunetv2_runner.py
@@ -18,7 +18,7 @@
from typing import Any
import monai
-from monai.apps.nnunet.utils import NNUNETMode as M
+from monai.apps.nnunet.utils import NNUNETMode as M # noqa: N814
from monai.apps.nnunet.utils import analyze_data, create_new_data_copy, create_new_dataset_json
from monai.bundle import ConfigParser
from monai.utils import ensure_tuple, optional_import
@@ -34,7 +34,7 @@
__all__ = ["nnUNetV2Runner"]
-class nnUNetV2Runner:
+class nnUNetV2Runner: # noqa: N801
"""
``nnUNetV2Runner`` provides an interface in MONAI to use `nnU-Net` V2 library to analyze, train, and evaluate
neural networks for medical image segmentation tasks.
diff --git a/tests/runner.py b/tests/runner.py
index 6b6e2c092a..28f0539d30 100644
--- a/tests/runner.py
+++ b/tests/runner.py
@@ -32,14 +32,14 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.timed_tests = {}
- def startTest(self, test):
+ def startTest(self, test): # NOQA: N802
"""Start timer, print test name, do normal test."""
self.start_time = time.time()
name = self.getDescription(test)
self.stream.write(f"Starting test: {name}...\n")
super().startTest(test)
- def stopTest(self, test):
+ def stopTest(self, test): # NOQA: N802
"""On test end, get time, print, store and do normal behaviour."""
elapsed = time.time() - self.start_time
name = self.getDescription(test)
From 6e7cf2c123f65c111a9ed2d2cb19935fc798570e Mon Sep 17 00:00:00 2001
From: jirka
Date: Tue, 6 Jan 2026 15:29:29 +0100
Subject: [PATCH 58/61] functools.cache (added in Python 3.9) is a convenience
alias for functools.lru_cache(maxsize=None). Functionally they are the same
when lru_cache is used with maxsize=None.
Signed-off-by: jirka
---
monai/metrics/utils.py | 4 ++--
pyproject.toml | 2 +-
tests/runner.py | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py
index 972ec0061e..606a54669b 100644
--- a/monai/metrics/utils.py
+++ b/monai/metrics/utils.py
@@ -13,7 +13,7 @@
import warnings
from collections.abc import Iterable, Sequence
-from functools import lru_cache, partial
+from functools import partial, cache
from types import ModuleType
from typing import Any
@@ -465,7 +465,7 @@ def prepare_spacing(
ENCODING_KERNEL = {2: [[8, 4], [2, 1]], 3: [[[128, 64], [32, 16]], [[8, 4], [2, 1]]]}
-@lru_cache(maxsize=None)
+@cache
def _get_neighbour_code_to_normals_table(device=None):
"""
returns a lookup table. For every binary neighbour code (2x2x2 neighbourhood = 8 neighbours = 8 bits = 256 codes)
diff --git a/pyproject.toml b/pyproject.toml
index b23e1de95a..f9fab1141a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -47,7 +47,7 @@ select = [
"E", "F", "W", # flake8
"NPY", # NumPy specific rules
"UP", # pyupgrade
- "RUF100", # aka yesqa
+ # "RUF100", # aka yesqa
]
extend-ignore = [
"E741", # ambiguous variable name
diff --git a/tests/runner.py b/tests/runner.py
index 28f0539d30..8079a26091 100644
--- a/tests/runner.py
+++ b/tests/runner.py
@@ -32,14 +32,14 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.timed_tests = {}
- def startTest(self, test): # NOQA: N802
+ def startTest(self, test): # noqa: N802
"""Start timer, print test name, do normal test."""
self.start_time = time.time()
name = self.getDescription(test)
self.stream.write(f"Starting test: {name}...\n")
super().startTest(test)
- def stopTest(self, test): # NOQA: N802
+ def stopTest(self, test): # noqa: N802
"""On test end, get time, print, store and do normal behaviour."""
elapsed = time.time() - self.start_time
name = self.getDescription(test)
From 5387c5ef4de5713ffe40df9aa70698e92878ba29 Mon Sep 17 00:00:00 2001
From: jirka
Date: Tue, 6 Jan 2026 15:38:32 +0100
Subject: [PATCH 59/61] linting
Signed-off-by: jirka
---
monai/networks/blocks/patchembedding.py | 3 +--
monai/networks/blocks/pos_embed_utils.py | 5 ++--
tests/transforms/compose/test_compose.py | 34 ------------------------
3 files changed, 3 insertions(+), 39 deletions(-)
diff --git a/monai/networks/blocks/patchembedding.py b/monai/networks/blocks/patchembedding.py
index 4e8a6a0463..2a05ef964c 100644
--- a/monai/networks/blocks/patchembedding.py
+++ b/monai/networks/blocks/patchembedding.py
@@ -12,7 +12,6 @@
from __future__ import annotations
from collections.abc import Sequence
-from typing import Optional
import numpy as np
import torch
@@ -54,7 +53,7 @@ def __init__(
pos_embed_type: str = "learnable",
dropout_rate: float = 0.0,
spatial_dims: int = 3,
- pos_embed_kwargs: Optional[dict] = None,
+ pos_embed_kwargs: dict | None = None,
) -> None:
"""
Args:
diff --git a/monai/networks/blocks/pos_embed_utils.py b/monai/networks/blocks/pos_embed_utils.py
index 266be5e28c..0612a02c28 100644
--- a/monai/networks/blocks/pos_embed_utils.py
+++ b/monai/networks/blocks/pos_embed_utils.py
@@ -13,7 +13,6 @@
import collections.abc
from itertools import repeat
-from typing import List, Union
import torch
import torch.nn as nn
@@ -33,7 +32,7 @@ def parse(x):
def build_fourier_position_embedding(
- grid_size: Union[int, List[int]], embed_dim: int, spatial_dims: int = 3, scales: Union[float, List[float]] = 1.0
+ grid_size: int | list[int], embed_dim: int, spatial_dims: int = 3, scales: float | list[float] = 1.0
) -> torch.nn.Parameter:
"""
Builds a (Anistropic) Fourier feature position embedding based on the given grid size, embed dimension,
@@ -86,7 +85,7 @@ def build_fourier_position_embedding(
def build_sincos_position_embedding(
- grid_size: Union[int, List[int]], embed_dim: int, spatial_dims: int = 3, temperature: float = 10000.0
+ grid_size: int | list[int], embed_dim: int, spatial_dims: int = 3, temperature: float = 10000.0
) -> torch.nn.Parameter:
"""
Builds a sin-cos position embedding based on the given grid size, embed dimension, spatial dimensions, and temperature.
diff --git a/tests/transforms/compose/test_compose.py b/tests/transforms/compose/test_compose.py
index 7258b3387e..96c6d4606f 100644
--- a/tests/transforms/compose/test_compose.py
+++ b/tests/transforms/compose/test_compose.py
@@ -316,40 +316,6 @@ def test_multi_sample_trait_cardinality(self):
for r in res:
self.assertEqual(r.shape, torch.Size([1, 32, 32]))
- def test_list_extend_multi_sample_trait(self):
- center_crop = mt.CenterSpatialCrop([128, 128])
- multi_sample_transform = mt.RandSpatialCropSamples([64, 64], 1)
- flatten_sequence_transform = mt.FlattenSequence()
-
- img = torch.zeros([1, 512, 512])
-
- self.assertEqual(execute_compose(img, [center_crop]).shape, torch.Size([1, 128, 128]))
- single_multi_sample_trait_result = execute_compose(
- img, [multi_sample_transform, center_crop, flatten_sequence_transform]
- )
- self.assertIsInstance(single_multi_sample_trait_result, list)
- self.assertEqual(len(single_multi_sample_trait_result), 1)
- self.assertEqual(single_multi_sample_trait_result[0].shape, torch.Size([1, 64, 64]))
-
- double_multi_sample_trait_result = execute_compose(
- img, [multi_sample_transform, multi_sample_transform, flatten_sequence_transform, center_crop]
- )
- self.assertIsInstance(double_multi_sample_trait_result, list)
- self.assertEqual(len(double_multi_sample_trait_result), 1)
- self.assertEqual(double_multi_sample_trait_result[0].shape, torch.Size([1, 64, 64]))
-
- def test_multi_sample_trait_cardinality(self):
- img = torch.zeros([1, 128, 128])
- t2 = mt.RandSpatialCropSamples([32, 32], num_samples=2)
- flatten_sequence_transform = mt.FlattenSequence()
-
- # chaining should multiply counts: 2 x 2 = 4, flattened
- res = execute_compose(img, [t2, t2, flatten_sequence_transform])
- self.assertIsInstance(res, list)
- self.assertEqual(len(res), 4)
- for r in res:
- self.assertEqual(r.shape, torch.Size([1, 32, 32]))
-
TEST_COMPOSE_EXECUTE_TEST_CASES = [
[None, tuple()],
From ad5f385afc32412d84ff7fd9f4c6bb9ae355dbc4 Mon Sep 17 00:00:00 2001
From: jirka
Date: Tue, 6 Jan 2026 16:05:02 +0100
Subject: [PATCH 60/61] fix
Signed-off-by: jirka
---
monai/utils/profiling.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/monai/utils/profiling.py b/monai/utils/profiling.py
index 7bcaa80ad3..5c880bbe1f 100644
--- a/monai/utils/profiling.py
+++ b/monai/utils/profiling.py
@@ -337,7 +337,7 @@ def profile_iter(self, name, iterable):
class _Iterable:
- def __iter__(self):
+ def __iter__(_self): # noqa: B902, N805 pylint: disable=E0213
do_iter = True
orig_iter = iter(iterable)
caller = getframeinfo(stack()[1][0])
From 83f351b3fdc186a2106160dc87331cf1d1d48af2 Mon Sep 17 00:00:00 2001
From: jirka
Date: Tue, 6 Jan 2026 16:30:33 +0100
Subject: [PATCH 61/61] linting
Signed-off-by: jirka
---
monai/auto3dseg/analyzer.py | 4 +++-
monai/data/wsi_reader.py | 4 +++-
monai/metrics/utils.py | 2 +-
3 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/monai/auto3dseg/analyzer.py b/monai/auto3dseg/analyzer.py
index be3ffb7ae7..8d662df83d 100644
--- a/monai/auto3dseg/analyzer.py
+++ b/monai/auto3dseg/analyzer.py
@@ -915,7 +915,9 @@ def __init__(
if not isinstance(_hist_bins, int) or _hist_bins < 0:
raise ValueError(f"Expected {i + 1}. hist_bins value to be positive integer but got {_hist_bins}")
if not isinstance(_hist_range, list) or len(_hist_range) != 2:
- raise ValueError(f"Expected {i + 1}. hist_range values to be list of length 2 but received {_hist_range}")
+ raise ValueError(
+ f"Expected {i + 1}. hist_range values to be list of length 2 but received {_hist_range}"
+ )
def __call__(self, data: dict) -> dict:
"""
diff --git a/monai/data/wsi_reader.py b/monai/data/wsi_reader.py
index c66a2a4ceb..62081d61d1 100644
--- a/monai/data/wsi_reader.py
+++ b/monai/data/wsi_reader.py
@@ -210,7 +210,9 @@ def get_valid_level(
# Set the default value if no resolution parameter is provided.
level = 0
if level >= n_levels:
- raise ValueError(f"The maximum level of this image is {n_levels - 1} while level={level} is requested)!")
+ raise ValueError(
+ f"The maximum level of this image is {n_levels - 1} while level={level} is requested)!"
+ )
return level
diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py
index 606a54669b..a451b1a770 100644
--- a/monai/metrics/utils.py
+++ b/monai/metrics/utils.py
@@ -13,7 +13,7 @@
import warnings
from collections.abc import Iterable, Sequence
-from functools import partial, cache
+from functools import cache, partial
from types import ModuleType
from typing import Any