diff --git a/.github/actions/max_disk_space/action.yaml b/.github/actions/max_disk_space/action.yaml new file mode 100644 index 00000000..759e53af --- /dev/null +++ b/.github/actions/max_disk_space/action.yaml @@ -0,0 +1,13 @@ +name: 'Maximize disk space' +description: 'Maximize available disk space by removing unwanted software' + +runs: + using: 'composite' + steps: + - name: Maximize available disk space + uses: AdityaGarg8/remove-unwanted-software@v5 + with: + remove-android: 'true' + remove-dotnet: 'true' + remove-haskell: 'true' + remove-codeql: 'true' diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5874d7e4..8b16fe53 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -8,7 +8,7 @@ on: workflow_dispatch: inputs: force-publish: - description: 'Force publish even if no version change detected' + description: 'Force publish even if no version change was detected' required: false type: choice options: @@ -41,6 +41,8 @@ jobs: lookup-only: true - uses: actions/checkout@v4 if: steps.look-up.outputs.cache-hit != 'true' + - uses: ./.github/actions/max_disk_space + if: steps.look-up.outputs.cache-hit != 'true' - uses: actions/cache@v4 if: steps.look-up.outputs.cache-hit != 'true' with: @@ -84,6 +86,7 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: ./.github/actions/max_disk_space - uses: actions/setup-python@v6 with: python-version: ${{matrix.python-version}} @@ -190,34 +193,6 @@ jobs: env: BIOIMAGEIO_CACHE_PATH: bioimageio_cache - docs: - needs: [coverage, test] - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: coverage-summary - path: dist - - uses: actions/setup-python@v6 - with: - python-version: '3.12' - cache: 'pip' - - run: pip install -e .[dev,partners] - - name: Generate developer docs - run: ./scripts/pdoc/run.sh - - run: cp README.md ./dist/README.md - - name: copy rendered presentations - run: | - mkdir ./dist/presentations - cp -r ./presentations/*.html ./dist/presentations/ - - name: Deploy to gh-pages 🚀 - uses: JamesIves/github-pages-deploy-action@v4 - with: - branch: gh-pages - folder: dist - build: runs-on: ubuntu-latest steps: @@ -235,21 +210,36 @@ jobs: path: dist/ name: dist - publish: - needs: [test, build, conda-build, docs] + docs: + needs: [build, conda-build, coverage, test] runs-on: ubuntu-latest - environment: - name: release - url: https://pypi.org/project/bioimageio.core/ permissions: contents: write # required for tag creation - id-token: write # required for pypi publish action + outputs: + new-version: ${{ steps.get-new-version.outputs.new-version }} steps: - - name: Check out the repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: - fetch-depth: 2 + fetch-depth: 0 fetch-tags: true + - uses: ./.github/actions/max_disk_space + - uses: actions/download-artifact@v4 + with: + name: coverage-summary + path: dist + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + cache: 'pip' + - run: pip install -e .[dev,docs,partners] + - name: Check doc scripts + run: pyright scripts/generate_api_doc_pages.py + - name: Get branch name to deploy to + id: get_branch + shell: bash + run: | + if [[ -n '${{ github.event.pull_request.head.ref }}' ]]; then branch=gh-pages-${{ github.event.pull_request.head.ref }}; else branch=gh-pages; fi + echo "::set-output name=branch::$branch" - name: Get parent commit if: inputs.force-publish != 'true' id: get-parent-commit @@ -258,7 +248,6 @@ jobs: - id: get-existing-tag if: inputs.force-publish == 'true' run: echo "existing-tag=$(git tag --points-at HEAD 'v[0-9]*.[0-9]*.[0-9]*')" >> $GITHUB_OUTPUT - - name: Detect new version from last commit and create tag id: tag-version if: github.ref == 'refs/heads/main' && steps.get-parent-commit.outputs.sha && inputs.force-publish != 'true' @@ -273,8 +262,6 @@ jobs: import os from pathlib import Path - - if "${{ inputs.force-publish }}" == "true": existing_tag = "${{ steps.get-existing-tag.outputs.existing-tag }}" valid = existing_tag.count("v") == 1 and existing_tag.count(".") == 2 and all(part.isdigit() for part in existing_tag.lstrip("v").split(".")) @@ -291,23 +278,52 @@ jobs: with open(os.environ['GITHUB_OUTPUT'], 'a') as f: print(f"new-version={new_version}", file=f) + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - name: Generate developer docs + run: mike deploy --push --branch ${{ steps.get_branch.outputs.branch }} --update-aliases ${{ steps.get-new-version.outputs.new-version || 'dev'}} ${{ steps.get-new-version.outputs.new-version && 'latest' || ' '}} + - name: copy rendered presentations + run: | + mkdir ./dist/presentations + cp -r ./presentations/*.html ./dist/presentations/ + - name: Deploy to gh-pages 🚀 + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: dist + clean: true + clean-exclude: | + .nojekyll + index.html + versions.json + latest/ + dev/ + v0.*/ + publish: + needs: [test, coverage, build, conda-build, docs] + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' && needs.docs.outputs.new-version + environment: + name: release + url: https://pypi.org/project/bioimageio.core/ + permissions: + contents: write # required to create a github release (release drafter) + id-token: write # required for pypi publish action + steps: - uses: actions/download-artifact@v4 - if: github.ref == 'refs/heads/main' && steps.get-new-version.outputs.new-version with: name: dist path: dist - name: Publish package on PyPI - if: github.ref == 'refs/heads/main' && steps.get-new-version.outputs.new-version uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist/ - - name: Publish the release notes - if: github.ref == 'refs/heads/main' uses: release-drafter/release-drafter@v6.0.0 with: - publish: "${{ steps.get-new-version.outputs.new-version != '' }}" - tag: '${{ steps.get-new-version.outputs.new-version }}' + tag: '${{ needs.docs.outputs.new-version }}' env: GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.gitignore b/.gitignore index 688e4a88..3cb4fc7f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,12 +6,13 @@ __pycache__/ *.egg-info/ *.pyc **/tmp +bioimageio_cache/ bioimageio_unzipped_tf_weights/ build/ cache coverage.xml dist/ -docs/ dogfood/ +pkgs/ +site/ typings/pooch/ -bioimageio_cache/ diff --git a/README.md b/README.md index da957da3..dc1f4b98 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,7 @@ bioimage.core has to offer: 1. test a model ```console - $ bioimageio test powerful-chipmunk - ... + bioimageio test powerful-chipmunk ```
@@ -65,8 +64,7 @@ bioimage.core has to offer: or ```console - $ bioimageio test impartial-shrimp - ... + bioimageio test impartial-shrimp ```
(Click to expand output) @@ -144,8 +142,7 @@ bioimage.core has to offer: - display the `bioimageio-predict` command help to get an overview: ```console - $ bioimageio predict --help - ... + bioimageio predict --help ```
@@ -233,8 +230,7 @@ bioimage.core has to offer: - create an example and run prediction locally! ```console - $ bioimageio predict impartial-shrimp --example=True - ... + bioimageio predict impartial-shrimp --example=True ```
diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..a38b0a23 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,5 @@ +--- +title: Changelog +--- + +--8<-- "changelog.md" diff --git a/docs/compatibility.md b/docs/compatibility.md new file mode 100644 index 00000000..5143266e --- /dev/null +++ b/docs/compatibility.md @@ -0,0 +1,7 @@ +# Compatibility with bioimage.io resources + +bioimageio.core is used on [bioimage.io](https://bioimage.io) to test resources during and after the upload process. +Results are reported as "Test reports" (bioimageio.core deployed in a generic Python environment) +as well as the bioimageio.core tool compatibility (testing a resource with bioimageio.core in a dedicated Python environment). + +An overview of the latter is available [as part of the collection documentation](https://bioimage-io.github.io/collection/latest/reports_overview/). diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..612c7a5e --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +--8<-- "README.md" diff --git a/mkdocs.yaml b/mkdocs.yaml new file mode 100644 index 00000000..ed97e53b --- /dev/null +++ b/mkdocs.yaml @@ -0,0 +1,175 @@ +site_name: 'bioimageio.core' +site_url: 'https://bioimage-io.github.io/core-bioimage-io-python' +site_author: Fynn Beuttenmüller +site_description: 'Python specific core utilities for bioimage.io resources (in particular DL models).' + +repo_name: bioimage-io/core-bioimage-io-python +repo_url: https://github.com/bioimage-io/core-bioimage-io-python +edit_uri: edit/main/docs/ + +theme: + name: material + language: en + features: + - announce.dismiss + - content.action.edit + - content.action.view + - content.code.annotate + - content.code.copy + - content.code.select + - content.footnote.tooltips + - content.tabs.link + - content.tooltips + - header.autohide + - navigation.expand + - navigation.footer + - navigation.indexes + - navigation.instant + - navigation.instant.prefetch + - navigation.instant.preview + - navigation.instant.progress + - navigation.path + - navigation.prune + - navigation.sections + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - navigation.tracking + - search.highlight + - search.share + - search.suggest + - toc.follow + + palette: + - media: '(prefers-color-scheme)' + primary: 'indigo' + accent: 'orange' + toggle: + icon: material/brightness-auto + name: 'Switch to light mode' + - media: '(prefers-color-scheme: light)' + scheme: default + primary: 'indigo' + accent: 'orange' + toggle: + icon: material/brightness-7 + name: 'Switch to dark mode' + - media: '(prefers-color-scheme: dark)' + scheme: slate + primary: 'indigo' + accent: 'orange' + toggle: + icon: material/brightness-4 + name: 'Switch to system preference' + + font: + text: Roboto + code: Roboto Mono + + logo: images/bioimage-io-icon.png + favicon: images/favicon.ico + +plugins: + - autorefs + - coverage: + html_report_dir: dist/coverage + - markdown-exec + - mkdocstrings: + enable_inventory: true + default_handler: python + locale: en + handlers: + python: + inventories: + - https://docs.pydantic.dev/latest/objects.inv + - https://bioimage-io.github.io/spec-bioimage-io/latest/objects.inv + - https://bioimage-io.github.io/spec-bioimage-io/dev/objects.inv + options: + annotations_path: source + backlinks: tree + docstring_options: + ignore_init_summary: true + returns_multiple_items: false + returns_named_value: false + trim_doctest_flags: true + # docstring_section_style: spacy + docstring_style: google + filters: public + heading_level: 1 + inherited_members: true + merge_init_into_class: true + parameter_headings: true + preload_modules: [pydantic, bioimageio.spec] + scoped_crossrefs: true + separate_signature: true + show_docstring_examples: true + show_if_no_docstring: true + show_inheritance_diagram: true + show_root_full_path: false + show_root_heading: true + show_signature_annotations: true + show_source: true + show_submodules: true + show_symbol_type_heading: true + show_symbol_type_toc: true + signature_crossrefs: true + summary: true + unwrap_annotated: false + extensions: + - griffe_pydantic: + schema: true + - griffe_inherited_docstrings + - griffe_public_redundant_aliases + - mike: + alias_type: symlink + canonical_version: latest + version_selector: true + - literate-nav: + nav_file: SUMMARY.md + - search + - section-index + +markdown_extensions: + - attr_list + - admonition + - callouts: + strip_period: false + - footnotes + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.highlight: + pygments_lang_class: true + - pymdownx.magiclink + - pymdownx.snippets: + base_path: [!relative $config_dir] + check_paths: true + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde + - toc: + permalink: '¤' + permalink_title: Anchor link to this section for reference + toc_depth: 2 + +nav: + - Home: + - index.md + - Compatibility: compatibility.md + - API Reference: reference/ + - Changelog: changelog.md + - Coverage report: coverage.md + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/bioimage-io + version: + provider: mike diff --git a/pyproject.toml b/pyproject.toml index ed7e9aa3..c5352c6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,30 +49,44 @@ partners = [ # "stardist", # for model testing and stardist postprocessing # TODO: add updated stardist to partners env ] dev = [ - "cellpose", # for model testing + "cellpose", # for model testing "crick", + "griffe-pydantic", + "griffe-inherited-docstrings", + "griffe-public-redundant-aliases", "httpx", "jupyter", "keras>=3.0,<4", "matplotlib", - "monai", # for model testing + "monai", # for model testing "numpy", "onnx", "onnxruntime", "onnxscript", "packaging>=17.0", - "pdoc", "pre-commit", "pyright==1.1.407", "pytest-cov", "pytest", "python-dotenv", - "segment-anything", # for model testing + "segment-anything", # for model testing "tensorflow", - "timm", # for model testing + "timm", # for model testing "torch>=1.6,<3", "torchvision>=0.21", ] +docs = [ + "markdown-callouts", + "markdown-exec", + "markdown-pycon", + "mike", + "mkdocs-api-autonav", + "mkdocs-coverage", + "mkdocs-gen-files", + "mkdocs-literate-nav", + "mkdocs-material", + "mkdocs-section-index", +] [build-system] requires = ["pip", "setuptools>=61.0"] @@ -90,8 +104,7 @@ exclude = [ "**/node_modules", "dogfood", "presentations", - "scripts/pdoc/original.py", - "scripts/pdoc/patched.py", + "scripts/generate_api_doc_pages.py", "tests/old_*", ] include = ["src", "scripts", "tests"] @@ -135,5 +148,8 @@ exclude = [ [tool.ruff.lint] select = ["NPY201"] +[tool.ruff.lint.isort] +known-first-party = ["bioimageio"] + [tool.coverage.report] exclude_also = ["if TYPE_CHECKING:", "assert_never\\("] diff --git a/scripts/generate_api_doc_pages.py b/scripts/generate_api_doc_pages.py new file mode 100644 index 00000000..872b9b29 --- /dev/null +++ b/scripts/generate_api_doc_pages.py @@ -0,0 +1,78 @@ +"""Generate the code reference pages. +(adapted from https://mkdocstrings.github.io/recipes/#bind-pages-to-sections-themselves) +""" + +from pathlib import Path + +import mkdocs_gen_files + +nav = mkdocs_gen_files.nav.Nav() + +root = Path(__file__).parent.parent +src = root / "src" + +# Track flat nav entries we have added +added_nav_labels: set[str] = set() + +for path in sorted(src.rglob("*.py")): + module_path = path.relative_to(src).with_suffix("") + doc_path = path.relative_to(src).with_suffix(".md") + full_doc_path = Path("reference", doc_path) + + parts = tuple(module_path.parts) + + # Skip if this is just the bioimageio namespace package + if parts == ("bioimageio",): + continue + + # Skip private submodules prefixed with '_' + if any( + part.startswith("_") and part not in ("__init__", "__main__") for part in parts + ): + continue + + if parts[-1] == "__init__": + parts = parts[:-1] + doc_path = doc_path.with_name("index.md") + full_doc_path = full_doc_path.with_name("index.md") + elif parts[-1] == "__main__": + continue + + if not parts: # Skip if parts is empty + continue + + # Build a flat nav for API Reference: one entry for bioimageio.core and + # one entry per top-level submodule under bioimageio.core. No subsections. + if parts[0:2] == ("bioimageio", "core"): + if len(parts) == 2: + # Landing page for bioimageio.core at reference/index.md + full_doc_path = Path("reference", "index.md") + doc_path = Path("index.md") + if "bioimageio.core" not in added_nav_labels: + nav[("bioimageio.core",)] = doc_path.as_posix() + added_nav_labels.add("bioimageio.core") + else: + # Top-level submodule/package directly under bioimageio.core + top = parts[2] + if top not in added_nav_labels: + pkg_init = src / "bioimageio" / "core" / top / "__init__.py" + if pkg_init.exists(): + nav_target = Path("bioimageio") / "core" / top / "index.md" + else: + nav_target = Path("bioimageio") / "core" / f"{top}.md" + + nav[(top,)] = nav_target.as_posix() + added_nav_labels.add(top) + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + # Reconstruct the full identifier from the original module_path + ident = ".".join(module_path.parts) + if ident.endswith(".__init__"): + ident = ident[:-9] # Remove .__init__ + fd.write(f"::: {ident}") + print(f"Written {full_doc_path}") + + mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root)) + +with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: + nav_file.writelines(nav.build_literate_nav()) diff --git a/scripts/pdoc/create_pydantic_patch.sh b/scripts/pdoc/create_pydantic_patch.sh deleted file mode 100755 index 05b6da6b..00000000 --- a/scripts/pdoc/create_pydantic_patch.sh +++ /dev/null @@ -1,25 +0,0 @@ -pydantic_root=$(python -c "import pydantic;from pathlib import Path;print(Path(pydantic.__file__).parent)") -main=$pydantic_root'/main.py' -original="$(dirname "$0")/original.py" -patched="$(dirname "$0")/patched.py" - -if [ -e $original ] -then - echo "found existing $original" -else - cp --verbose $main $original -fi - -if [ -e $patched ] -then - echo "found existing $patched" -else - cp --verbose $main $patched - echo "Please update $patched, then press enter to continue" - read -fi - -patch_file="$(dirname "$0")/mark_pydantic_attrs_private.patch" -diff -au $original $patched > $patch_file -echo "content of $patch_file:" -cat $patch_file diff --git a/scripts/pdoc/mark_pydantic_attrs_private.patch b/scripts/pdoc/mark_pydantic_attrs_private.patch deleted file mode 100644 index 722d4fbb..00000000 --- a/scripts/pdoc/mark_pydantic_attrs_private.patch +++ /dev/null @@ -1,28 +0,0 @@ ---- ./original.py 2024-11-08 15:18:37.493768700 +0100 -+++ ./patched.py 2024-11-08 15:13:54.288887700 +0100 -@@ -121,14 +121,14 @@ - # `GenerateSchema.model_schema` to work for a plain `BaseModel` annotation. - - model_config: ClassVar[ConfigDict] = ConfigDict() -- """ -+ """@private - Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict]. - """ - - # Because `dict` is in the local namespace of the `BaseModel` class, we use `Dict` for annotations. - # TODO v3 fallback to `dict` when the deprecated `dict` method gets removed. - model_fields: ClassVar[Dict[str, FieldInfo]] = {} # noqa: UP006 -- """ -+ """@private - Metadata about the fields defined on the model, - mapping of field names to [`FieldInfo`][pydantic.fields.FieldInfo] objects. - -@@ -136,7 +136,7 @@ - """ - - model_computed_fields: ClassVar[Dict[str, ComputedFieldInfo]] = {} # noqa: UP006 -- """A dictionary of computed field names and their corresponding `ComputedFieldInfo` objects.""" -+ """@private A dictionary of computed field names and their corresponding `ComputedFieldInfo` objects.""" - - __class_vars__: ClassVar[set[str]] - """The names of the class variables defined on the model.""" diff --git a/scripts/pdoc/run.sh b/scripts/pdoc/run.sh deleted file mode 100755 index 74981aa5..00000000 --- a/scripts/pdoc/run.sh +++ /dev/null @@ -1,16 +0,0 @@ -cd "$(dirname "$0")" # cd to folder this script is in - -# patch pydantic to hide pydantic attributes that somehow show up in the docs -# (not even as inherited, but as if the documented class itself would define them) -pydantic_main=$(python -c "import pydantic;from pathlib import Path;print(Path(pydantic.__file__).parent / 'main.py')") - -patch --verbose --forward -p1 $pydantic_main < mark_pydantic_attrs_private.patch - -cd ../.. # cd to repo root -pdoc \ - --docformat google \ - --logo "https://bioimage.io/static/img/bioimage-io-logo.svg" \ - --logo-link "https://bioimage.io/" \ - --favicon "https://bioimage.io/static/img/bioimage-io-icon-small.svg" \ - --footer-text "bioimageio.core $(python -c 'import bioimageio.core;print(bioimageio.core.__version__)')" \ - -o ./dist bioimageio.core bioimageio.spec # generate bioimageio.spec as well for references diff --git a/src/bioimageio/core/__init__.py b/src/bioimageio/core/__init__.py index ac51907d..bab4a098 100644 --- a/src/bioimageio/core/__init__.py +++ b/src/bioimageio/core/__init__.py @@ -1,5 +1,18 @@ -""" -.. include:: ../../README.md +"""bioimageio.core --- core functionality for BioImage.IO resources + +The main focus on this library is to provide functionality to run prediction with +BioImage.IO models, including standardized pre- and postprocessing operations. +The BioImage.IO models (and other resources) are described by---and can be loaded with---the bioimageio.spec package. + +See `predict` and `predict_many` for straight-forward model inference +and `create_prediction_pipeline` for finer control of the inference process. + +Other notable bioimageio.core functionalities include: +- Testing BioImage.IO resources beyond format validation, e.g. by generating model outputs from test inputs. + See `test_model` or for arbitrary resource types `test_description`. +- Extending available model weight formats by converting existing ones, see `add_weights`. +- Creating and manipulating `Sample`s consisting of tensors with associated statistics. +- Computing statistics on datasets (represented as sequences of samples), see `compute_dataset_measures`. """ # ruff: noqa: E402 diff --git a/src/bioimageio/core/_prediction_pipeline.py b/src/bioimageio/core/_prediction_pipeline.py index 0b7717aa..0cad757e 100644 --- a/src/bioimageio/core/_prediction_pipeline.py +++ b/src/bioimageio/core/_prediction_pipeline.py @@ -66,7 +66,7 @@ def __init__( default_blocksize_parameter: BlocksizeParameter = 10, default_batch_size: int = 1, ) -> None: - """Use `create_prediction_pipeline` to create a `PredictionPipeline`""" + """Consider using `create_prediction_pipeline` to create a `PredictionPipeline` with sensible defaults.""" super().__init__() default_blocksize_parameter = default_ns or default_blocksize_parameter if default_ns is not None: diff --git a/src/bioimageio/core/_resource_tests.py b/src/bioimageio/core/_resource_tests.py index c4572929..c581d9ee 100644 --- a/src/bioimageio/core/_resource_tests.py +++ b/src/bioimageio/core/_resource_tests.py @@ -24,6 +24,10 @@ ) import numpy as np +from loguru import logger +from numpy.typing import NDArray +from typing_extensions import NotRequired, TypedDict, Unpack, assert_never, get_args + from bioimageio.spec import ( AnyDatasetDescr, AnyModelDescr, @@ -61,18 +65,14 @@ ValidationSummary, WarningEntry, ) -from loguru import logger -from numpy.typing import NDArray -from typing_extensions import NotRequired, TypedDict, Unpack, assert_never, get_args - -from bioimageio.core import __version__ -from bioimageio.core.io import save_tensor +from . import __version__ from ._prediction_pipeline import create_prediction_pipeline from ._settings import settings from .axis import AxisId, BatchSize from .common import MemberId, SupportedWeightsFormat from .digest_spec import get_test_input_sample, get_test_output_sample +from .io import save_tensor from .sample import Sample CONDA_CMD = "conda.bat" if platform.system() == "Windows" else "conda" @@ -710,7 +710,7 @@ def _get_tolerance( if wf == weights_format: applicable = v0_5.ReproducibilityTolerance( relative_tolerance=test_kwargs.get("relative_tolerance", 1e-3), - absolute_tolerance=test_kwargs.get("absolute_tolerance", 1e-4), + absolute_tolerance=test_kwargs.get("absolute_tolerance", 1e-3), ) break @@ -739,7 +739,7 @@ def _get_tolerance( mismatched_tol = 0 else: # use given (deprecated) test kwargs - atol = deprecated.get("absolute_tolerance", 1e-5) + atol = deprecated.get("absolute_tolerance", 1e-3) rtol = deprecated.get("relative_tolerance", 1e-3) mismatched_tol = 0 @@ -874,10 +874,10 @@ def add_warning_entry(msg: str): f"Output '{m}' disagrees with {mismatched_elements} of" + f" {expected_np.size} expected values" + f" ({mismatched_ppm:.1f} ppm)." - + f"\n Max relative difference: {r_max:.2e}" + + f"\n Max relative difference not accounted for by absolute tolerance ({atol:.2e}): {r_max:.2e}" + rf" (= \|{r_actual:.2e} - {r_expected:.2e}\|/\|{r_expected:.2e} + 1e-6\|)" + f" at {dict(zip(dims, r_max_idx))}" - + f"\n Max absolute difference not accounted for by relative tolerance: {a_max:.2e}" + + f"\n Max absolute difference not accounted for by relative tolerance ({rtol:.2e}): {a_max:.2e}" + rf" (= \|{a_actual:.7e} - {a_expected:.7e}\|) at {dict(zip(dims, a_max_idx))}" + f"\n Saved actual output to {actual_output_path}." ) diff --git a/src/bioimageio/core/cli.py b/src/bioimageio/core/cli.py index ff24f1ec..d0e09c84 100644 --- a/src/bioimageio/core/cli.py +++ b/src/bioimageio/core/cli.py @@ -16,6 +16,7 @@ from pathlib import Path from pprint import pformat, pprint from typing import ( + Annotated, Any, Dict, Iterable, @@ -30,24 +31,8 @@ Union, ) -import rich.markdown -from loguru import logger -from pydantic import AliasChoices, BaseModel, Field, model_validator -from pydantic_settings import ( - BaseSettings, - CliPositionalArg, - CliSettingsSource, - CliSubCommand, - JsonConfigSettingsSource, - PydanticBaseSettingsSource, - SettingsConfigDict, - YamlConfigSettingsSource, -) -from tqdm import tqdm -from typing_extensions import assert_never - import bioimageio.spec -from bioimageio.core import __version__ +import rich.markdown from bioimageio.spec import ( AnyModelDescr, InvalidDescr, @@ -65,6 +50,22 @@ from bioimageio.spec.model import ModelDescr, v0_4, v0_5 from bioimageio.spec.notebook import NotebookDescr from bioimageio.spec.utils import ensure_description_is_model, get_reader, write_yaml +from loguru import logger +from pydantic import AliasChoices, BaseModel, Field, PlainSerializer, model_validator +from pydantic_settings import ( + BaseSettings, + CliPositionalArg, + CliSettingsSource, + CliSubCommand, + JsonConfigSettingsSource, + PydanticBaseSettingsSource, + SettingsConfigDict, + YamlConfigSettingsSource, +) +from tqdm import tqdm +from typing_extensions import assert_never + +from bioimageio.core import __version__ from .commands import WeightFormatArgAll, WeightFormatArgAny, package, test from .common import MemberId, SampleId, SupportedWeightsFormat @@ -450,7 +451,9 @@ class PredictCmd(CmdBase, WithSource): blockwise: bool = False """process inputs blockwise""" - stats: Path = Path("dataset_statistics.json") + stats: Annotated[Path, PlainSerializer(lambda p: p.as_posix())] = Path( + "dataset_statistics.json" + ) """path to dataset statistics (will be written if it does not exist, but the model requires statistical dataset measures) diff --git a/src/bioimageio/core/commands.py b/src/bioimageio/core/commands.py index 61d0bd4b..1a391f17 100644 --- a/src/bioimageio/core/commands.py +++ b/src/bioimageio/core/commands.py @@ -4,8 +4,6 @@ from pathlib import Path from typing import Optional, Sequence, Union -from typing_extensions import Literal - from bioimageio.spec import ( InvalidDescr, ResourceDescr, @@ -13,6 +11,7 @@ save_bioimageio_package_as_folder, ) from bioimageio.spec._internal.types import FormatVersionPlaceholder +from typing_extensions import Literal from ._resource_tests import test_description @@ -102,7 +101,7 @@ def package( Args: descr: a bioimageio resource description path: output path - weight-format: include only this single weight-format (if not 'all'). + weight_format: include only this single weight-format (if not 'all'). """ if isinstance(descr, InvalidDescr): logged = descr.validation_summary.save() diff --git a/src/bioimageio/core/tensor.py b/src/bioimageio/core/tensor.py index 17358b00..c49469f7 100644 --- a/src/bioimageio/core/tensor.py +++ b/src/bioimageio/core/tensor.py @@ -177,11 +177,11 @@ def from_numpy( Args: array: the nd numpy array - axes: A description of the array's axes, + dims: A description of the array's axes, if None axes are guessed (which might fail and raise a ValueError.) Raises: - ValueError: if `axes` is None and axes guessing fails. + ValueError: if `dims` is None and dims guessing fails. """ if dims is None: diff --git a/src/bioimageio/core/weight_converters/_add_weights.py b/src/bioimageio/core/weight_converters/_add_weights.py index cc915619..255aa7b2 100644 --- a/src/bioimageio/core/weight_converters/_add_weights.py +++ b/src/bioimageio/core/weight_converters/_add_weights.py @@ -30,8 +30,8 @@ def add_weights( Default: choose automatically from any available. target_format: convert to a specific weights format. Default: attempt to convert to any missing format. - devices: Devices that may be used during conversion. verbose: log more (error) output + allow_tracing: allow conversion to torchscript by tracing if scripting fails. Returns: A (potentially invalid) model copy stored at `output_path` with added weights if any conversion was possible. diff --git a/tests/test_add_weights.py b/tests/test_add_weights.py index 836353c7..225257d6 100644 --- a/tests/test_add_weights.py +++ b/tests/test_add_weights.py @@ -1,48 +1,45 @@ -# TODO: update add weights tests -# import os - - -# def _test_add_weights(model, tmp_path, base_weights, added_weights, **kwargs): -# from bioimageio.core.build_spec import add_weights - -# rdf = load_raw_resource_description(model) -# assert base_weights in rdf.weights -# assert added_weights in rdf.weights - -# weight_path = load_description(model).weights[added_weights].source -# assert weight_path.exists() - -# drop_weights = set(rdf.weights.keys()) - {base_weights} -# for drop in drop_weights: -# rdf.weights.pop(drop) -# assert tuple(rdf.weights.keys()) == (base_weights,) - -# in_path = tmp_path / "model1.zip" -# export_resource_package(rdf, output_path=in_path) - -# out_path = tmp_path / "model2.zip" -# add_weights(in_path, weight_path, weight_type=added_weights, output_path=out_path, **kwargs) - -# assert out_path.exists() -# new_rdf = load_description(out_path) -# assert set(new_rdf.weights.keys()) == {base_weights, added_weights} -# for weight in new_rdf.weights.values(): -# assert weight.source.exists() - -# test_res = _test_model(out_path, added_weights) -# failed = [s for s in test_res if s["status"] != "passed"] -# assert not failed, failed -# test_res = _test_model(out_path) -# failed = [s for s in test_res if s["status"] != "passed"] -# assert not failed, failed - -# # make sure the weights were cleaned from the cwd -# assert not os.path.exists(os.path.split(weight_path)[1]) - - -# def test_add_torchscript(unet2d_nuclei_broad_model, tmp_path): -# _test_add_weights(unet2d_nuclei_broad_model, tmp_path, "pytorch_state_dict", "torchscript") - - -# def test_add_onnx(unet2d_nuclei_broad_model, tmp_path): -# _test_add_weights(unet2d_nuclei_broad_model, tmp_path, "pytorch_state_dict", "onnx", opset_version=12) +from pathlib import Path + +import pytest +from bioimageio.spec import InvalidDescr +from bioimageio.spec.model.v0_5 import WeightsFormat + +from bioimageio.core import add_weights, load_model_description + + +@pytest.mark.parametrize( + ("model_fixture", "source_format", "target_format"), + [ + ("unet2d_nuclei_broad_model", "pytorch_state_dict", "torchscript"), + ("unet2d_nuclei_broad_model", "pytorch_state_dict", "onnx"), + ("unet2d_nuclei_broad_model", "torchscript", "onnx"), + ], +) +def test_add_weights( + model_fixture: str, + source_format: WeightsFormat, + target_format: WeightsFormat, + tmp_path: Path, + request: pytest.FixtureRequest, +): + model_source = request.getfixturevalue(model_fixture) + + model = load_model_description(model_source, format_version="latest") + assert source_format in model.weights.available_formats, ( + "source format not found in model" + ) + if target_format in model.weights.available_formats: + setattr(model.weights, target_format, None) + + out_path = tmp_path / "converted.zip" + converted = add_weights( + model, + output_path=out_path, + source_format=source_format, + target_format=target_format, + ) + assert not isinstance(converted, InvalidDescr), ( + "conversion resulted in invalid descr", + converted.validation_summary.display(), + ) + assert target_format in converted.weights.available_formats