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:

Now both methods are consistent:

Migration Guide

✅ No Action Needed If:
⚠️ 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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=4&new-version=5)](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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=4&new-version=5)](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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=5&new-version=6)](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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=peter-evans/create-or-update-comment&package-manager=github_actions&previous-version=4&new-version=5)](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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=5&new-version=6)](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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=4&new-version=5)](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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action&package-manager=github_actions&previous-version=3&new-version=4)](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 @@

- project-monai +project-monai

**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 @@ [![premerge](https://github.com/Project-MONAI/MONAI/actions/workflows/pythonapp.yml/badge.svg?branch=dev)](https://github.com/Project-MONAI/MONAI/actions/workflows/pythonapp.yml) [![postmerge](https://img.shields.io/github/checks-status/project-monai/monai/dev?label=postmerge)](https://github.com/Project-MONAI/MONAI/actions?query=branch%3Adev) -[![Documentation Status](https://readthedocs.org/projects/monai/badge/?version=latest)](https://docs.monai.io/en/latest/) +[![Documentation Status](https://readthedocs.org/projects/monai/badge/?version=latest)](https://monai.readthedocs.io/en/latest/) [![codecov](https://codecov.io/gh/Project-MONAI/MONAI/branch/dev/graph/badge.svg?token=6FTC7U1JJ4)](https://codecov.io/gh/Project-MONAI/MONAI) [![monai Downloads Last Month](https://assets.piptrends.com/get-last-month-downloads-badge/monai.svg 'monai Downloads Last Month by pip Trends')](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) ![coplenet](../images/coplenet.png) ### 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) ![LAMP UNet](../images/unet-pipe.png) ### 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). ![DiNTS](../images/dints-overview.png) 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 ![federated-learning](../images/federated.svg) -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](../images/auto3dseg.png) -[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 ![BTCV_organs](../images/BTCV_organs.png) 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). ![DiNTS](../images/dints-overview.png) 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](../images/auto3dseg.png) -[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). ![MRI-reconstruction](../images/mri_recon.png) 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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=peter-evans/slash-command-dispatch&package-manager=github_actions&previous-version=4.0.0&new-version=5.0.0)](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: &quot;D:&quot;

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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=al-cheb/configure-pagefile-action&package-manager=github_actions&previous-version=1.4&new-version=1.5)](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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=4&new-version=6)](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