From 6cff2ee9d33a12710120fd08d5d5ec21b578ee53 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 00:55:49 +0200 Subject: [PATCH 01/28] docs: drop the stale reference to the 2026-07 refactoring report --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 929d06dd..8c8169b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -177,7 +177,7 @@ Conventional Commits only took hold at `v1.5.9`, and rendering further back emit - **The YAML model builder is the trusted/untrusted boundary**: only registry types, and module names contain no `.`. - **`konfai-apps` is a separate package**; `apps/` is excluded from the `konfai` wheel. Core must never import `konfai_apps` **at module level**. Known exception: `data/transform/inference.py` `KonfAIInference.infer_entry` does a - lazy, guarded import: a layering inversion pending an owner decision (see `REFACTORING.md` §C); do not add more. + lazy, guarded import: a layering inversion pending an owner decision; do not add more. - **The pretrained bridge fills every target tensor or raises**; never report a partial load as success. - **The config write is atomic** (temp + `os.replace`); a reader must never see a truncated config and bind all-defaults. From b4e00f9b30d71154931c4e4c4c296ff7ddb82ff7 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:05:59 +0200 Subject: [PATCH 02/28] fix(config): refuse wrong-shaped YAML instead of binding it silently A scalar where an object block is expected bound the object to None (optimizer: AdamW trained without ever stepping), a block or list under a str parameter bound its Python repr, list elements were never validated, and an explicit null reactivated the default it was written to suppress. Every case now refuses with the dotted path, except the None spelling. The unreachable interactive machinery is deleted, torch no longer loads with the binder, and a refused strict block leaves the file untouched. --- konfai/utils/config.py | 195 ++++++++++++++++++++------------------ tests/unit/test_config.py | 109 +++++++++++++++++---- 2 files changed, 194 insertions(+), 110 deletions(-) diff --git a/konfai/utils/config.py b/konfai/utils/config.py index 971166ec..6e308856 100755 --- a/konfai/utils/config.py +++ b/konfai/utils/config.py @@ -22,6 +22,7 @@ import inspect import logging import os +import sys import time import types import typing @@ -35,7 +36,6 @@ from typing import Any, Literal, Union, get_args, get_origin import ruamel.yaml -import torch from konfai.utils.errors import ConfigError @@ -239,16 +239,19 @@ def strict_config(root: str, refuse: bool = True) -> Iterator[None]: _ledgers.append(ledger) if shared is not None: _shared_trees.append(shared) + unknown: list[str] = [] try: yield finally: _ledgers.remove(ledger) + unknown = ledger.unknown(root) if shared is not None: _shared_trees.remove(shared) - # Written whatever ended the block: what the contexts bound is on disk, as when each - # context wrote its own level. - shared.flush() - if unknown := ledger.unknown(root): + # Written whatever ended the block, EXCEPT when the block is about to refuse the config: + # a refused run must leave the user's file exactly as it was. + if not (refuse and unknown): + shared.flush() + if unknown: _report( refuse, f"Unknown key(s) in the {root} configuration: nothing reads them.", @@ -285,23 +288,26 @@ def __init__(self, key: str) -> None: def __enter__(self): if not self.filename.exists(): - mode = os.environ.get("KONFAI_CONFIG_MODE", "Done") - if mode in {"default", "interactive", "Import"}: + if os.environ.get("KONFAI_CONFIG_MODE") == "Import": self.filename.parent.mkdir(parents=True, exist_ok=True) self.filename.touch() else: raise ConfigError( f"Config file '{self.filename.resolve()}' does not exist.", - f"Active config mode: KONFAI_CONFIG_MODE={mode}.", - "Run `konfai TRAINING -c Config.yml` to generate a default config, " - "or set KONFAI_CONFIG_MODE=default.", + "Generate a resolved default with `konfai --init` " + "(e.g. `konfai TRAIN --init -c Config.yml`), or point -c at an existing file.", ) self._shared = _shared_tree(self.filename) self.data = self._shared.tree if self._shared is not None else _load_tree(self.filename) self.config = self.data - for key in self.keys: + for index, key in enumerate(self.keys): + if self.config is not None and not isinstance(self.config, collections.abc.Mapping): + raise ConfigError( + f"'{'.'.join(self.keys[:index])}' holds the value '{self.config}' where a block is expected.", + f"Nest '{key}:' under it as a mapping, or remove the value.", + ) if self.config is None or key not in self.config: self.config = {key: {}} @@ -328,47 +334,12 @@ def __exit__(self, exc_type, value, traceback) -> None: _write_tree(self.filename, data) @staticmethod - def _get_input(name: str, default: str) -> str: - try: - options = ",".join(default.split(":")[1:]) if ":" in default else "" - return input(f"{name} [{options}]: ") - except (EOFError, KeyboardInterrupt): - # Interactive editing is optional; when stdin is unavailable we - # degrade to default materialization instead of aborting the run. - os.environ["KONFAI_CONFIG_MODE"] = "default" - return default.split("|")[1] if len(default.split("|")) > 1 else default - - @staticmethod - def _get_input_default( - name: str, - default: str | None, - is_list: bool = False, - ) -> list[str | None] | str | None: - # ``default|value`` is KonfAI's marker for "materialize this default if - # the user/config did not provide a concrete value". - if isinstance(default, str) and ( - default == "default" or (len(default.split("|")) > 1 and default.split("|")[0] == "default") - ): - if os.environ["KONFAI_CONFIG_MODE"] == "interactive": - if is_list: - list_tmp: list[str | None] = [] - key_tmp = "OK" - while key_tmp != "!" and key_tmp != " " and os.environ["KONFAI_CONFIG_MODE"] == "interactive": - key_tmp = Config._get_input(name, default) - if key_tmp != "!" and key_tmp != " ": - if key_tmp == "": - key_tmp = default.split("|")[1] if len(default.split("|")) > 1 else default - list_tmp.append(key_tmp) - return list_tmp - else: - value = Config._get_input(name, default) - if value == "": - return default.split("|")[1] if len(default.split("|")) > 1 else default - else: - return value - else: - default = default.split("|")[1] if len(default.split("|")) > 1 else default - return [default] if is_list else default + def _default_value(default): + """Resolve the ``default|value`` marker ("materialize this when the config holds nothing").""" + if isinstance(default, str) and default.split("|")[0] == "default": + parts = default.split("|") + return parts[1] if len(parts) > 1 else default + return default def get_value(self, name, default) -> object: if not isinstance(self.config, collections.abc.MutableMapping): @@ -376,45 +347,29 @@ def get_value(self, name, default) -> object: for ledger in _ledgers: ledger.read(tuple(self.keys), name) - if name in self.config and self.config[name] is not None: - value = self.config[name] + if name in self.config: + # An explicit null (`name:` empty or `name: null`) is the disabled spelling, exactly like + # the string "None": substituting the default here would silently reactivate the very + # thing the line was written to suppress. + value = self.config[name] if self.config[name] is not None else "None" value_config = value else: - value = Config._get_input_default( - name, - default if default != inspect._empty else None, - ) + value = Config._default_value(default if default != inspect._empty else None) value_config = value if isinstance(value_config, tuple): value_config = list(value) if isinstance(value_config, list): - list_tmp = [] - for key in value_config: - res = Config._get_input_default(name, key, is_list=True) - if isinstance(res, list): - list_tmp.extend(res) - else: - list_tmp.append(str(res)) - - value = list_tmp - value_config = list_tmp + value = value_config = [Config._default_value(key) for key in value_config] if isinstance(value, dict): - key_tmp = [] - value_config = {} dict_value = {} for key in value: - res = Config._get_input_default(name, key, is_list=True) - if isinstance(res, list): - key_tmp.extend(res) - else: - key_tmp.append(str(res)) - for key in key_tmp: - if key in value: - value_tmp = value[key] + resolved = str(Config._default_value(key)) + if resolved in value: + value_tmp = value[resolved] else: value_tmp = next(v for k, v in value.items() if "default" in k) @@ -422,8 +377,8 @@ def get_value(self, name, default) -> object: # so a None placeholder is correct; primitive entries have no such pass, so they # must be persisted here or the write-back collapses the whole dict to ``{}`` # (empty on the next run, silently dropping the defaults). - value_config[key] = value_tmp if isinstance(value_tmp, int | float | str | bool) else None - dict_value[key] = value_tmp + value_config[resolved] = value_tmp if isinstance(value_tmp, int | float | str | bool) else None + dict_value[resolved] = value_tmp value = dict_value self.config[name] = _recordable(value_config) if value_config is not None else "None" if value == "None": @@ -458,8 +413,20 @@ def decorator(function): str, bool, float, - torch.Tensor, } + + +def _tensor_type() -> type | None: + """``torch.Tensor`` when torch is already imported, else None. + + torch is never imported here: an annotation can only mention Tensor if its declaring module + already paid the import, and keeping config.py torch-free keeps the light-import contract of + ``konfai/__init__`` honest for consumers like the Slicer-facing konfai-apps helpers (measured: + 634 of this module's 676 ms import was torch). + """ + return getattr(sys.modules.get("torch"), "Tensor", None) + + _CONFIG_SUPPORTED_TYPES_MESSAGE = ( "Config: The config only supports types : config(Object), int, str, " "bool, float, list[int], list[str], list[bool], list[float], " @@ -514,9 +481,9 @@ def _resolve_annotation(function, annotation): "int": int, "list": list, "str": str, - "torch": torch, "tuple": tuple, "typing": typing, + **({"torch": sys.modules["torch"]} if "torch" in sys.modules else {}), }, ) except Exception: @@ -585,10 +552,10 @@ def _convert_union_sequence_value( continue if not isinstance(candidate_type, type): continue - current_value = ( - torch.tensor(value) if candidate_type == torch.Tensor and not isinstance(value, torch.Tensor) else value - ) - converted = current_value if candidate_type == torch.Tensor else candidate_type(current_value) + if candidate_type is _tensor_type(): + converted = value if isinstance(value, candidate_type) else sys.modules["torch"].tensor(value) + else: + converted = candidate_type(value) break except Exception as exc: last_error = exc @@ -646,6 +613,14 @@ def _parse_bool(value: object) -> bool: def _bind_primitive(config: Config, param: inspect.Parameter, annotation, section_key: str) -> object: value = config.get_value(param.name, param.default) if annotation in {int, float, bool, str} and value is not None: + if isinstance(value, Mapping | list | tuple): + # `str` never fails to coerce, so without this a nested block or list binds as its + # Python repr and fails far downstream, on whatever that text then selects. + shape = "a nested block" if isinstance(value, Mapping) else "a list" + raise ConfigError( + f"Parameter '{section_key}.{param.name}' was given {shape}, but it takes a {annotation.__name__}.", + f"Write it as a single value ('{param.name}: <{annotation.__name__}>').", + ) try: value = _parse_bool(value) if annotation is bool else annotation(value) except (ValueError, TypeError) as exc: @@ -673,7 +648,7 @@ def _bind_path(config: Config, param: inspect.Parameter) -> Path | None: return path -def _bind_sequence(config: Config, param: inspect.Parameter, annotation) -> object: +def _bind_sequence(config: Config, param: inspect.Parameter, annotation, section_key: str) -> object: values: Any = config.get_value(param.name, param.default) if values is None: return None @@ -681,15 +656,41 @@ def _bind_sequence(config: Config, param: inspect.Parameter, annotation) -> obje elem_type = args_annotation[0] if args_annotation else Any if get_origin(elem_type) in {Union, types.UnionType}: return [_convert_union_sequence_value(value, get_args(elem_type), param.name) for value in values] - if elem_type in {int, str, bool, float, torch.Tensor, Any}: + if elem_type is Any or elem_type is _tensor_type(): return values - raise ConfigError(_CONFIG_SUPPORTED_TYPES_MESSAGE) + if isinstance(elem_type, type) and elem_type in {int, str, bool, float}: + if not isinstance(values, list | tuple): + raise ConfigError( + f"Parameter '{section_key}.{param.name}' expects a list of {elem_type.__name__}, got '{values}'.", + f"Spell it as a YAML list ('{param.name}: [a, b]' or one '- item' per line).", + ) + converted = [] + for index, value in enumerate(values): + if value is None or isinstance(value, Mapping | list | tuple): + raise ConfigError( + f"Element {index} of '{section_key}.{param.name}' is not a {elem_type.__name__}: '{value}'." + ) + try: + converted.append(_parse_bool(value) if elem_type is bool else elem_type(value)) + except (TypeError, ValueError) as exc: + raise ConfigError( + f"Element {index} of '{section_key}.{param.name}' is not a {elem_type.__name__}: '{value}'." + ) from exc + return converted + raise ConfigError( + f"Parameter '{section_key}.{param.name}' is annotated {annotation}, which the config cannot bind.", + _CONFIG_SUPPORTED_TYPES_MESSAGE, + ) def _bind_dict(config: Config, param: inspect.Parameter, annotation, section_key: str) -> object: key_type, value_type = get_args(annotation) if key_type is not str: - raise ConfigError(_CONFIG_SUPPORTED_TYPES_MESSAGE) + raise ConfigError( + f"Parameter '{section_key}.{param.name}' is annotated {annotation}, which the config cannot bind" + " (dict keys must be str).", + _CONFIG_SUPPORTED_TYPES_MESSAGE, + ) values: Any = config.get_value(param.name, param.default) if values is None or value_type in {int, str, bool, float, Any}: return values @@ -699,7 +700,7 @@ def _bind_dict(config: Config, param: inspect.Parameter, annotation, section_key for value in values } except Exception as exc: - raise ConfigError(f"{values} {exc}") from exc + raise ConfigError(f"Failed to build an entry of '{section_key}.{param.name}': {exc}") from exc def _bind_config_object(config: Config, param: inspect.Parameter, annotation, is_optional: bool, section_key: str): @@ -742,7 +743,7 @@ def _bind_parameter(function, config: Config, param: inspect.Parameter, section_ origin = get_origin(annotation) if origin in {list, tuple, Sequence, collections.abc.Sequence}: - return _bind_sequence(config, param, annotation) + return _bind_sequence(config, param, annotation, section_key) if origin is dict: return _bind_dict(config, param, annotation, section_key) @@ -780,7 +781,15 @@ def new_function(*args, **kwargs): try: with Config(key_tmp) as config: if not isinstance(config.config, collections.abc.Mapping): - return None + if config.config in (None, "None"): + return None + # `optimizer: AdamW` where a block is expected would otherwise bind the + # whole object to None and the run would proceed without it, silently. + raise ConfigError( + f"'{key_tmp}' holds the value '{config.config}' where a block is expected.", + f"Nest its settings under '{key_tmp.rsplit('.', 1)[-1]}:' as a mapping" + " (or write 'None' to disable it).", + ) for ledger in _ledgers: # a parameter the caller supplies itself is a read one ledger.read(tuple(config.keys), *without) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 73b5c131..cea55045 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -57,31 +57,27 @@ def test_config_missing_file_raises_clear_error_without_prompt( with Config("Trainer"): pass - # The error must name the file, the mode, and hint at the fix. + # The error must name the file and hint at the real fix (a command that exists). msg = str(exc_info.value) assert "missing.yml" in msg assert "does not exist" in msg - assert "KONFAI_CONFIG_MODE=Done" in msg - assert "konfai TRAINING" in msg + assert "--init" in msg -def test_config_default_mode_materializes_missing_file( +def test_config_missing_file_refuses_whatever_the_mode( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: + """Generation is `konfai --init` (which creates the file first); no mode materializes one.""" config_path = tmp_path / "generated.yml" monkeypatch.setenv("KONFAI_config_file", str(config_path)) monkeypatch.setenv("KONFAI_CONFIG_MODE", "default") monkeypatch.setattr("builtins.input", _fail_input) - with Config("Trainer") as config_obj: - value = config_obj.get_value("train_name", "default|SMOKE") - - assert config_path.exists() - assert value == "SMOKE" - content = config_path.read_text(encoding="utf-8") - assert "Trainer:" in content - assert "train_name: SMOKE" in content + with pytest.raises(ConfigError): + with Config("Trainer"): + pass + assert not config_path.exists() def test_config_missing_env_var_raises( @@ -709,6 +705,74 @@ def test_union_with_literal_member_does_not_crash() -> None: assert _convert_union_sequence_value("beta", (Literal["alpha", "beta"], str), "p") == "beta" +# -------------------------------------------------------------------------------------- +# Shape refusals: wrong-shaped YAML fails at the key that caused it, never silently +# -------------------------------------------------------------------------------------- + + +class _Engine: + def __init__(self, rate: float = 0.5) -> None: + self.rate = rate + + +class _ShapeRoot: + def __init__(self, name: str = "run", sizes: list[int] = [1, 2], engine: "_Engine | None" = None) -> None: + self.name, self.sizes, self.engine = name, sizes, engine + + +def test_a_scalar_where_an_object_block_is_expected_refuses(write_config) -> None: + # `Engine: AdamW` instead of a block once bound the object to None: the run then proceeded + # without it (an optimizer that never steps), silently. + write_config("Root:\n Engine: AdamW\n") + with pytest.raises(ConfigError, match="where a block is expected"): + apply_config("Root.Engine")(_Engine)() + + +def test_an_explicit_none_at_an_object_key_still_binds_none(write_config) -> None: + write_config("Root:\n Engine: None\n") + assert apply_config("Root.Engine")(_Engine)() is None + write_config("Root:\n Engine: null\n") + assert apply_config("Root.Engine")(_Engine)() is None + + +def test_a_scalar_at_an_intermediate_level_refuses_with_the_path(write_config) -> None: + write_config("Root: 5\n") + with pytest.raises(ConfigError, match="'Root' holds the value '5'"): + apply_config("Root.Engine")(_Engine)() + + +def test_a_block_under_a_str_parameter_refuses_instead_of_binding_its_repr(write_config) -> None: + write_config("Root:\n name:\n foo: bar\n") + with pytest.raises(ConfigError, match="nested block"): + apply_config("Root")(_ShapeRoot)() + + +def test_a_list_under_a_str_parameter_refuses_instead_of_binding_its_repr(write_config) -> None: + write_config("Root:\n name: [a, b]\n") + with pytest.raises(ConfigError, match="takes a str"): + apply_config("Root")(_ShapeRoot)() + + +def test_list_elements_are_coerced_to_the_declared_type(write_config) -> None: + write_config("Root:\n sizes: ['3', '4']\n") + assert apply_config("Root")(_ShapeRoot)().sizes == [3, 4] + + +def test_a_wrong_shaped_list_element_refuses_with_its_index(write_config) -> None: + write_config("Root:\n sizes: [3, {a: 1}]\n") + with pytest.raises(ConfigError, match="Element 1"): + apply_config("Root")(_ShapeRoot)() + + +def test_an_explicit_null_binds_none_instead_of_reactivating_the_default(write_config) -> None: + # `name: null` (or an empty value) is the disabled spelling: materializing the default here + # silently reactivated the very thing the line was written to suppress. + config_path = write_config("Root:\n name: null\n sizes: [1]\n") + root = apply_config("Root")(_ShapeRoot)() + assert root.name is None + assert "name: None" in config_path.read_text(encoding="utf-8") + + # -------------------------------------------------------------------------------------- # strict_config: a key nothing reads is refused, whoever would have read it # -------------------------------------------------------------------------------------- @@ -942,12 +1006,21 @@ def test_a_strict_block_reads_once_writes_once_and_the_bytes_a_write_per_context ) -def test_a_strict_block_that_refuses_still_leaves_what_it_bound_on_disk(write_config) -> None: - """A context wrote its level before the block could report an unknown key; the block writes - the same resolved file before it reports.""" +def test_a_strict_block_that_refuses_leaves_the_file_untouched(write_config) -> None: + """A refused config is the user's to fix: the resolved tree is dropped with the refusal, so the + file still reads exactly as they wrote it (the typo'd key included).""" config_path = write_config("Root:\n kep: 2\n") + before = config_path.read_text(encoding="utf-8") with pytest.raises(ConfigError, match="Unknown key"), strict_config("Root"): apply_config("Root")(_StrictRoot)() + assert config_path.read_text(encoding="utf-8") == before + + +def test_a_warning_strict_block_still_writes_the_resolved_file(write_config) -> None: + """refuse=False (TRAIN/PREDICTION/EVALUATION): the run proceeds, so the resolved file is kept.""" + config_path = write_config("Root:\n kep: 2\n") + with pytest.warns(UserWarning, match="Unknown key"), strict_config("Root", refuse=False): + apply_config("Root")(_StrictRoot)() written = ruamel.yaml.YAML().load(config_path.read_text(encoding="utf-8")) assert written["Root"] == {"kep": 2, "depth": 1, "kept": 0, "Nested": {"width": 2}} @@ -962,11 +1035,13 @@ def test_a_strict_block_does_not_create_a_file_a_context_refused(tmp_path: Path, def test_a_strict_block_writes_the_file_a_context_materialized(tmp_path: Path, monkeypatch) -> None: + """An existing empty file (what `--init` creates) resolves to the full default block on disk.""" config_path = tmp_path / "generated.yml" + config_path.touch() monkeypatch.setenv("KONFAI_config_file", str(config_path)) - monkeypatch.setenv("KONFAI_CONFIG_MODE", "default") + monkeypatch.setenv("KONFAI_CONFIG_MODE", "Done") monkeypatch.setattr("builtins.input", _fail_input) - with strict_config("Root"): + with strict_config("Root", refuse=False): root = apply_config("Root")(_StrictRoot)() assert root.kept == 0 written = ruamel.yaml.YAML().load(config_path.read_text(encoding="utf-8")) From 246ae3ac8ab46c2ae2798a2cb99b05a7245083f6 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:06:17 +0200 Subject: [PATCH 03/28] feat(cli): --init generates a resolved default config The documented generation modes were unreachable (every builder forces Done before any read) and the missing-config error advised a command that does not exist. konfai --init now seeds the file with its root key, binds the workflow once so every default resolves onto disk, and exits without running. The dead --resubmit flag is removed (it never requeued), the --cpu help no longer promises GPU auto-detection that never existed, prog names match the real binaries, and python -m konfai.main works. --- docs/source/reference/cli.md | 1 - konfai/main.py | 69 +++++++++++++++++++++++++---- konfai/utils/runtime/distributed.py | 12 ++--- konfai/utils/runtime/environment.py | 1 - tests/unit/test_runtime.py | 12 ++--- 5 files changed, 69 insertions(+), 26 deletions(-) diff --git a/docs/source/reference/cli.md b/docs/source/reference/cli.md index 44592575..d19e00ce 100644 --- a/docs/source/reference/cli.md +++ b/docs/source/reference/cli.md @@ -244,7 +244,6 @@ the optional `cluster` extra. | `--num-nodes` | `1` | Nodes to request. | | `--memory` | `16` | Memory per node, in GB. | | `--time-limit` | `1440` | Wall-clock limit, in minutes. | -| `--resubmit` | off | Accepted, but **not implemented**: the run warns and does not requeue. | Otherwise `konfai-cluster` takes the same subcommands and arguments as `konfai`. **The cluster options come before the subcommand**: they sit on the top-level diff --git a/konfai/main.py b/konfai/main.py index c197e036..96be18b4 100644 --- a/konfai/main.py +++ b/konfai/main.py @@ -80,10 +80,15 @@ def _add_common_args(parser: argparse.ArgumentParser) -> None: "--cpu", type=_positive_int, default=None, - help="Run on CPU using N worker processes/cores. If omitted, uses GPU when available.", + help="Number of CPU worker processes when no --gpu is given; the run stays on CPU unless --gpu is passed.", ) parser.add_argument("-q", "--quiet", action="store_true", help="Suppress console output for a quieter execution") parser.add_argument("-tb", "--tensorboard", action="store_true", help="Launch TensorBoard.") + parser.add_argument( + "--init", + action="store_true", + help="Create the config file if missing, resolve every default into it, and exit without running.", + ) def _add_dir_argument(parser: argparse.ArgumentParser, name: str, help_text: str) -> None: @@ -183,6 +188,11 @@ def _add_transform(subparsers: argparse._SubParsersAction) -> None: " takes back what the probe created (the entry, and the store when it did not exist). The" " plan is printed even with -q.", ) + parser.add_argument( + "--init", + action="store_true", + help="Create the config file if missing, resolve every default into it, and exit without running.", + ) _add_dir_argument( parser, "transforms", "Directory where run logs are written; --plan prints and writes nothing there" ) @@ -198,6 +208,44 @@ def _add_transform(subparsers: argparse._SubParsersAction) -> None: str(State.TRANSFORM): ("konfai.transformer", "transform", "transform_file"), } +# Command -> (default config filename, root key, pure build function beside the entrypoint). +_INIT_TARGETS: dict[str, tuple[str, str, str]] = { + str(State.TRAIN): ("Config.yml", "Trainer", "build_train"), + str(State.RESUME): ("Config.yml", "Trainer", "build_train"), + str(State.PREDICTION): ("Prediction.yml", "Predictor", "build_predict"), + str(State.EVALUATION): ("Evaluation.yml", "Evaluator", "build_evaluate"), + str(State.TRANSFORM): ("Transform.yml", "Transformer", "build_transform"), +} + + +def _run_init(args: dict[str, Any]) -> None: + """``--init``: bind the workflow once so every default resolves and lands in the file, then exit. + + The file is created seeded with its root key when missing (an empty tree would be refused as + holding no root). The build itself never runs anything; a build error after partial binding + still leaves what resolved on disk (the strict block flushes on exceptional exit too). + """ + import inspect + from pathlib import Path + + command = args["command"] + module_name, _, config_key = _COMMANDS[command] + default_name, root, builder_name = _INIT_TARGETS[command] + config_path = Path(args.get(config_key) or args.get("config") or default_name) + if not config_path.exists(): + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text(f"{root}:\n", encoding="utf-8") + args[config_key] = config_path + builder = getattr(importlib.import_module(module_name), builder_name) + accepted = inspect.signature(builder).parameters + try: + builder(**{name: value for name, value in args.items() if name in accepted}) + except Exception as error: + print(f"[KonfAI] Wrote what resolved before the error to '{config_path}'.") + print(f"[KonfAI] {error}") + sys.exit(1) + print(f"[KonfAI] Resolved default configuration written to '{config_path}'.") + def _check_gpu_ids(parser: argparse.ArgumentParser, gpu: list[int]) -> None: """The ``--gpu`` choices, checked after parsing: resolving them imports torch, which @@ -224,11 +272,15 @@ def _dispatch(parser: argparse.ArgumentParser, args: dict[str, Any]) -> None: del args["config"] # the entrypoint's own default config filename applies elif config_key != "config": args[config_key] = args.pop("config") + if args.pop("init", False): + # --init must SHORT-CIRCUIT for the same reason --plan does just below. + _run_init(args) + return if args.pop("plan", False): # --plan must SHORT-CIRCUIT here: the distributed wrapper filters kwargs by the entrypoint's # signature, so a 'plan' passed through would be silently dropped and the run would proceed # as if the flag had never been given. - if "resubmit" in args: + if "num_nodes" in args: parser.error("--plan is a dry run on this machine and submits nothing: use `konfai TRANSFORM --plan`.") # plan_transform declares the TRANSFORM flags and nothing else; the command name is not one. del args["command"] @@ -252,7 +304,7 @@ def _run(parser: argparse.ArgumentParser) -> None: def main(): """Entry point for the ``konfai`` command-line interface.""" parser = argparse.ArgumentParser( - prog="konfAI", description="KonfAI - Deep learning framework for Medical AI Models", allow_abbrev=False + prog="konfai", description="KonfAI - Deep learning framework for Medical AI Models", allow_abbrev=False ) _run(parser) @@ -260,7 +312,7 @@ def main(): def cluster(): """Entry point for the ``konfai-cluster`` CLI: the standard commands plus SLURM job arguments.""" parser = argparse.ArgumentParser( - prog="konfAI", description="KonfAI - Deep learning framework for Medical AI Models", allow_abbrev=False + prog="konfai-cluster", description="KonfAI - Deep learning framework for Medical AI Models", allow_abbrev=False ) cluster_args = parser.add_argument_group("Cluster manager arguments") cluster_args.add_argument("--name", type=str, help="Task name", required=True) @@ -273,9 +325,8 @@ def cluster(): default=1440, help="Job time limit in minute", ) - cluster_args.add_argument( - "--resubmit", - action="store_true", - help="Automatically resubmit job just before timout", - ) _run(parser) + + +if __name__ == "__main__": + main() diff --git a/konfai/utils/runtime/distributed.py b/konfai/utils/runtime/distributed.py index c95d0303..cc28c7ca 100644 --- a/konfai/utils/runtime/distributed.py +++ b/konfai/utils/runtime/distributed.py @@ -229,7 +229,9 @@ def wrapper(*args: Any, **kwargs: Any) -> None: bound = sig.bind_partial(*args, **kwargs_fun) bound.apply_defaults() - is_cluster = "resubmit" in kwargs + # The cluster CLI always parses --name (required) and the workflow signatures never declare + # it, so its presence in the RAW kwargs is what tells a submission from a local run. + is_cluster = "name" in kwargs # The auto memory budget is a NODE budget, but build-time sizing (the evaluation auto-patch) # runs while ``func(...)`` constructs the workflow: before the spawn where world_size exists. # The launcher therefore leaves the per-node rank count in the environment, and restores it @@ -253,7 +255,6 @@ def wrapper(*args: Any, **kwargs: Any) -> None: "memory": kwargs["memory"], "num_nodes": kwargs["num_nodes"], "time_limit": kwargs["time_limit"], - "resubmit": bool(kwargs.get("resubmit", False)), } if is_cluster else None @@ -365,13 +366,6 @@ def execute_distributed_object( if configured_object.manual_seed is not None: seed_all(configured_object.manual_seed) if cluster_config is not None: - if cluster_config["resubmit"]: - # Auto-requeue is not implemented; warn instead of silently dropping the flag. - print( - "[KonfAI] WARNING: --resubmit is not implemented yet; this job will NOT " - "auto-requeue at the time limit. Relaunch manually with the RESUME command " - "pointing at the latest checkpoint to continue training." - ) with clock.phase("setup"): configured_object.setup(len(gpu_ids) * cluster_config["num_nodes"]) clock.launch() diff --git a/konfai/utils/runtime/environment.py b/konfai/utils/runtime/environment.py index cd4ad96c..50e3f494 100644 --- a/konfai/utils/runtime/environment.py +++ b/konfai/utils/runtime/environment.py @@ -47,7 +47,6 @@ class ClusterKwargs(TypedDict): memory: int num_nodes: int time_limit: int - resubmit: bool def description(model, model_ema=None, show_memory: bool = True, train: bool = True) -> str: diff --git a/tests/unit/test_runtime.py b/tests/unit/test_runtime.py index fa9f0d96..f9ca1de1 100644 --- a/tests/unit/test_runtime.py +++ b/tests/unit/test_runtime.py @@ -155,11 +155,11 @@ def fake_spawn(fn, nprocs: int, *args, **kwargs) -> None: assert spawn_calls["nprocs"] == 2 -def test_cluster_resubmit_flag_warns_that_auto_requeue_is_not_wired( +def test_cluster_kwargs_route_the_run_through_submitit_instead_of_spawning( monkeypatch: pytest.MonkeyPatch, - capsys: pytest.CaptureFixture[str], ) -> None: monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising=False) + submitted = [] class DummyContext: def __init__(self, *_args, **_kwargs) -> None: @@ -178,8 +178,8 @@ def __init__(self, *_args, **_kwargs) -> None: def update_parameters(self, *_args, **_kwargs) -> None: pass - def submit(self, *_args, **_kwargs) -> None: - pass + def submit(self, *args, **_kwargs) -> None: + submitted.append(args) class DummyDistributed(DistributedObject): def __init__(self) -> None: @@ -195,10 +195,10 @@ def run_process(self, world_size, global_rank, local_rank, dataloaders): monkeypatch.setattr("konfai.utils.runtime.distributed.TensorBoard", DummyContext) monkeypatch.setitem(sys.modules, "submitit", SimpleNamespace(AutoExecutor=DummyExecutor)) - cluster_kwargs = {"name": "job", "memory": 8, "num_nodes": 1, "time_limit": 60, "resubmit": True} + cluster_kwargs = {"name": "job", "memory": 8, "num_nodes": 1, "time_limit": 60} execute_distributed_object(DummyDistributed(), gpu=[0], cpu=1, quiet=True, cluster_kwargs=cluster_kwargs) - assert "--resubmit is not implemented" in capsys.readouterr().out + assert len(submitted) == 1 def test_get_available_devices_maps_visible_env_ids_to_local_torch_indices( From 104178d4ddb789a9384d1cef45059a04b24e9574 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:06:24 +0200 Subject: [PATCH 04/28] fix(api): normalize predict models and refuse unspellable sweep trees predict(models="best.pt") iterated the string per character; a dict config with numpy scalars died as a raw ruamel RepresenterError instead of the api contract's named refusal. --- konfai/api.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/konfai/api.py b/konfai/api.py index da32e7be..9b34ffec 100644 --- a/konfai/api.py +++ b/konfai/api.py @@ -452,7 +452,9 @@ def _config_copy(config: "Mapping[str, object] | Path | str") -> "dict[str, obje scratch copy instead (removed at exit, like :func:`_materialized_config`'s). """ if isinstance(config, Mapping): - return dict(config) + # Through _yaml_safe so the documented sweep idiom (np.float64 learning rates from + # np.logspace, Path values) fails as a named refusal here, not a raw ruamel error at dump. + return _yaml_safe(dict(config), "config") # type: ignore[return-value] source = Path(config) scratch = Path(tempfile.mkdtemp(prefix="konfai_config_")) atexit.register(shutil.rmtree, scratch, ignore_errors=True) @@ -463,7 +465,7 @@ def _config_copy(config: "Mapping[str, object] | Path | str") -> "dict[str, obje def predict( - models: Sequence[Path | str], + models: Path | str | Sequence[Path | str], config: Mapping[str, object] | Path | str, *, gpu: Sequence[int] | None = None, @@ -479,6 +481,10 @@ def predict( """ from konfai.predictor import build_predict + # A bare str IS a Sequence[str]: without this, "best.pt" expands per character into + # [Path('b'), Path('e'), ...] and fails far downstream as missing models. + if isinstance(models, (str, Path)): + models = [models] return _launch( len(gpu or []) or cpu, lambda: build_predict( From 275fd1def197129a8938315ac7baf4c9ef13e586 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:06:43 +0200 Subject: [PATCH 05/28] fix(reduce): probe the run's first fold and replay the stat pass's regions A fold with a post-Reduce GLOBAL_STAT stage ran its entire stat pass at full planned height unmeasured, so the kernel-OOM-kill protection fired only on the second traversal, after the tallest allocations had already happened -- and on the kept-folds route never at all. The stat pass now carries the probe, keeps each region beside its fold (a mid-pass refit changes slab_rows, so re-derived regions would misalign), and the write pass replays them; the short probe is also skipped when no budget is declared. The bisection now re-prices only the height-dependent read fields per probe instead of rebuilding the whole plan (refusal walks, filesystem stats and channel maps are height-independent). --- konfai/data/case_reduction.py | 49 ++++++++++++++++--------------- tests/unit/test_case_reduction.py | 34 +++++++++++++++++++++ 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/konfai/data/case_reduction.py b/konfai/data/case_reduction.py index c20305fc..0782f15f 100644 --- a/konfai/data/case_reduction.py +++ b/konfai/data/case_reduction.py @@ -31,7 +31,7 @@ import contextlib from collections.abc import Iterator -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace import numpy as np import torch @@ -375,9 +375,9 @@ def fit_budget(self, budget_bytes: float | None, cap: int | None = None) -> None allowance = budget_share("regions", budget_bytes) or 0.0 if self.keeps_folds(plan): allowance -= self._folded_output_bytes(plan) - self.slab_rows = self._tallest_affordable(ceiling, allowance) + self.slab_rows = self._tallest_affordable(plan, ceiling, allowance) - def _tallest_affordable(self, ceiling: int, allowance: float) -> int: + def _tallest_affordable(self, plan: ReductionPlan, ceiling: int, allowance: float) -> int: """The tallest region up to ``ceiling`` whose PRICED plan fits ``allowance``. Bisected on the price itself rather than extrapolated from one height, because none of what @@ -391,25 +391,31 @@ def _tallest_affordable(self, ceiling: int, allowance: float) -> int: refuses, which is the only honest answer, there being no whole-volume path to fall back to. """ ceiling = max(1, int(ceiling)) - if self._priced_peak(ceiling) <= allowance: + if self._priced_peak(plan, ceiling) <= allowance: return ceiling low, high = 1, ceiling while low < high: middle = (low + high + 1) // 2 - if self._priced_peak(middle) <= allowance: + if self._priced_peak(plan, middle) <= allowance: low = middle else: high = middle - 1 return low - def _priced_peak(self, rows: int) -> int: - """What the plan prices at ``rows``, leaving the height the sizing is working from alone.""" + def _priced_peak(self, plan: ReductionPlan, rows: int) -> int: + """What ``plan`` prices at ``rows``, leaving the height the sizing is working from alone. + + Only the read fields depend on the height, so only they are recomputed per probe: the rest + of the plan (refusal walks, filesystem stats, channel maps) is height-independent, and a + bisection re-deriving all of it once put 96% of plan time into the members' geometry walks. + """ held = self.slab_rows try: self.slab_rows = rows - return self.plan().peak_bytes + pull_bytes, read_bytes = self._member_read_bytes(int(plan.source_channels)) finally: self.slab_rows = held + return replace(plan, slab_rows=rows, pull_bytes=pull_bytes, read_bytes=read_bytes).peak_bytes def keeps_folds(self, plan: ReductionPlan) -> bool: """Whether the stat pass hands its folds to the write pass instead of re-folding them. @@ -601,13 +607,6 @@ def _member_read_bytes(self, channels: int) -> tuple[int, int]: # --------------------------------------------------------------- execution - def _regions(self, spatial: list[int]) -> list[tuple[slice, ...]]: - """The output's regions: slabs along the first spatial axis, whole in the others.""" - return [ - (slice(start, min(start + self.slab_rows, spatial[0])), *(slice(0, extent) for extent in spatial[1:])) - for start in range(0, spatial[0], self.slab_rows) - ] - def _fold(self, region: tuple[slice, ...]) -> torch.Tensor: """One region of the reduced volume: every case reads that region, the operator folds them. @@ -665,7 +664,9 @@ def _folds(self, spatial: list[int], measure: bool = False): made, and the first region has already been folded at the planned height: this can correct an optimistic price, never spend a budget the sizing declined to spend. """ - start, refitted = 0, not measure + # A refit needs a declared budget to judge against; without one the short probe would only + # shorten the first region for a no-op. + start, refitted = 0, not (measure and self._budget_bytes) while start < int(spatial[0]): # The probe is SHORT. Every later region is cut against what it measured; the probe # itself is cut against nothing, so it is sized so that its own overshoot cannot @@ -764,10 +765,14 @@ def _output_attributes(self, plan: ReductionPlan) -> Attribute: # subtracted them by), and the write pass then only applies the post stages. Otherwise # the second pass re-folds, as before: correctness never depends on the keep. self._kept_folds = [] if self.keeps_folds(plan) else None - for _region, folded in self._folds(plan.spatial): + # The run's FIRST region happens here, so the probe must too: an unmeasured full-height + # stat pass is exactly the unbounded first allocation _PROBE_SHARE exists to prevent. + # The region is kept beside its fold because a mid-pass refit changes slab_rows: regions + # re-derived from the final height would misalign with folds cut at the earlier one. + for region, folded in self._folds(plan.spatial, measure=True): statistics.update(folded) if self._kept_folds is not None: - self._kept_folds.append(folded.cpu()) + self._kept_folds.append((region, folded.cpu())) statistics.write_into(attribute) return attribute @@ -856,11 +861,9 @@ def _write_folds(self, plan: ReductionPlan) -> None: # they are; otherwise every region is folded here, once. kept = self._kept_folds self._kept_folds = None - folds = ( - ((region, kept[index]) for index, region in enumerate(self._regions(spatial))) - if kept is not None - else self._folds(spatial, measure=True) - ) + # A stat pass already probed and refit these regions; measuring again would only reset the + # high-water mark the run's closing line reports. + folds = iter(kept) if kept is not None else self._folds(spatial, measure=not plan.stat_pass) writer = RegionWriter(lambda _key, array, header: self._open_stream(spatial, array, header)) try: for region, folded in folds: diff --git a/tests/unit/test_case_reduction.py b/tests/unit/test_case_reduction.py index 8dda475d..cc1168e6 100644 --- a/tests/unit/test_case_reduction.py +++ b/tests/unit/test_case_reduction.py @@ -1140,6 +1140,40 @@ def test_the_folds_a_stat_pass_keeps_come_out_of_the_regions_share(tmp_path: Pat assert not engine.keeps_folds(engine.plan()) +def test_the_stat_pass_is_the_measured_pass_and_the_write_pass_replays_kept_folds(tmp_path: Path) -> None: + """The run's FIRST region is the probe wherever it happens. + + A stat pass at full height was exactly the unbounded first allocation _PROBE_SHARE exists to + prevent (90 GiB resident on a 122 GiB host, kernel kill, nothing measured). And the regions a + stat pass keeps must travel beside their folds: a mid-pass refit changes ``slab_rows``, so + regions re-derived at the final height would misalign with folds cut at the earlier one. + """ + from konfai.data.transform import Standardize + + engine, _destination, _volumes = _run(tmp_path, [], Reduce(operator="Mean", output="t"), [Standardize()]) + plan = engine.plan() + assert plan.stat_pass, "a GLOBAL_STAT stage after the fold is what makes the second pass" + output = engine._folded_output_bytes(plan) + engine.fit_budget(output / (BUDGET_SHARES["regions"] * _KEPT_FOLDS_SHARE_OF_REGIONS * 0.99)) + plan = engine.plan() + assert engine.keeps_folds(plan) + engine.slab_rows = 4 + + meters: list[int] = [] + engine._open_meter = lambda: meters.append(1) or HeldMeter(lambda: 1, 0) # type: ignore[method-assign] + folded_regions: list[tuple[slice, ...]] = [] + original_fold = engine._fold + engine._fold = lambda region: folded_regions.append(region) or original_fold(region) # type: ignore[method-assign] + + engine._write_folds(plan) + + assert meters == [1], "the stat pass runs the run's first region, so it is the one measured pass" + heights = [region[0].stop - region[0].start for region in folded_regions] + assert heights[0] == 1, "the run's first region is the probe: short" + assert heights[1] == 4, "and the rest walk the planned height" + assert sum(heights) == plan.spatial[0], "every row folded exactly once: the write pass replays kept folds" + + def test_a_value_neutral_stage_does_not_decide_the_region_height(tmp_path: Path) -> None: """The sizing must follow the data, not the shape of the stage list. From 0d8ba9bdb645559c773b9aa3d0fe6e08e08e4169 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:06:51 +0200 Subject: [PATCH 06/28] fix(patching): key the sweep pricing to the segment it prices The sizing lived on the manager and read manager state: the WHOLE declared chain's channel folds and the RAW source's read granularity. A segment past a Save boundary was priced with another segment's facts (channel folds double-applied onto a cache that already holds them, the wrong store's chunk grid) and an Expand copy's draws were priced at zero. The pricing engine moves to SegmentSizer, constructed per segment from explicit inputs; the manager keeps thin whole-chain delegators for the single-segment case, and AugmentedStage now declares what a draw allocates (a REGRID draw builds grid_sample's coordinate grid). --- konfai/data/materialize.py | 4 +- konfai/data/patching/manager.py | 399 +++++++------------------------- konfai/data/patching/sizer.py | 344 +++++++++++++++++++++++++++ konfai/data/patching/stage.py | 16 ++ konfai/data/patching/sweep.py | 4 +- 5 files changed, 446 insertions(+), 321 deletions(-) create mode 100644 konfai/data/patching/sizer.py diff --git a/konfai/data/materialize.py b/konfai/data/materialize.py index f487d4f7..96aa0adf 100644 --- a/konfai/data/materialize.py +++ b/konfai/data/materialize.py @@ -393,7 +393,7 @@ def sub_cap_sweep(self) -> bool: if not any( extent < segment.landing[axis] for segment in self.manager.sweep_segments() or [] - for axis, extent in enumerate(manager._sweep_tile(segment.landing, segment.channels, segment.plans)) + for axis, extent in enumerate(manager.sizer_for(segment).sweep_tile()) ): return False for stage in manager.chain_stages(0): @@ -472,7 +472,7 @@ def predicted_stream_read_factor(self, a: int = 0, apply_augmentations: bool = F def _segment_read_factor(self, segment: SweepSegment) -> float: """One segment's reads over its source's voxels, block by block through the plan's own pulls.""" - tile = self.manager._sweep_tile(segment.landing, segment.channels, segment.plans) + tile = self.manager.sizer_for(segment).sweep_tile() targets = list(_sweep_targets(segment.landing, tile)) # A source that is not on disk yet is a Save cache this run sweeps first, onto a store that # serves region writes, and every such store serves bounded reads: priced as bounded, not diff --git a/konfai/data/patching/manager.py b/konfai/data/patching/manager.py index c2768a72..640d9f2f 100644 --- a/konfai/data/patching/manager.py +++ b/konfai/data/patching/manager.py @@ -27,18 +27,14 @@ import torch from konfai.data.augmentation import DataAugmentationsList -from konfai.data.patching import budget -from konfai.data.patching import sweep as sweep_module from konfai.data.patching.budget import ( _PLATEAU_READ_MARGIN, _STREAM_STAT_KEYS, _STREAM_STATS, - _SWEEP_ELEMENT_BYTES, - _SWEEP_SLAB_ROWS_DEVICE, - _SWEEP_TILE_MARGIN, _UNRESOLVED, ) from konfai.data.patching.grid import DatasetPatch +from konfai.data.patching.sizer import SegmentSizer from konfai.data.patching.stage import ( _MAX_HALO_FRACTION, AugmentedStage, @@ -57,19 +53,15 @@ RegionWriter, SweepSegment, _channel_first_block, - _cubic_tile, _HostLanding, _open_sweep_stream, _PatchStreamSource, _PendingSweep, _plateau_rows, - _pull_block_spans, _ReadAhead, _shares_h5_file, - _span_voxels, _stage_failure, _sweep_header, - _sweep_resident_regions, _sweep_targets, _SweepMember, _WriteBehind, @@ -85,9 +77,7 @@ split_expand, stat_seed_valid, ) -from konfai.utils.budget import format_bytes from konfai.utils.dataset import Attribute, Dataset -from konfai.utils.dataset import chunk_hull_voxels as _chunk_hull_voxels from konfai.utils.errors import DatasetManagerError, PatchError from konfai.utils.utils import env_flag @@ -205,8 +195,12 @@ def __init__( self._sweep_budget_bytes: float | None = None # The store's own read granularity, resolved on first use (None is an answer, not a miss). self._read_granularity: object = _UNRESOLVED - #: One walk of a decomposition per (decomposition, plans), keyed with the plans held - #: beside the answer so no identity under the key can be reused (:meth:`block_reads`). + # Per-segment store grains, keyed by (store, group, entry): a segment past a Save boundary + # reads its own store, never the raw source's (SegmentSizer's whole reason to exist). + self._granularities: dict[tuple[str, str, str], tuple[int, ...] | None] = {} + #: One walk of a decomposition per (decomposition, plans), shared by every sizer this + #: manager builds, keyed with the plans held beside the answer so no identity under the + #: key can be reused (:meth:`SegmentSizer.block_reads`). self._block_reads: dict[tuple, tuple[tuple, BlockReads]] = {} self._chain_device: torch.device | None = None self._disk_statistics: dict[tuple[Dataset, str, str, tuple[int, ...] | None], dict[str, float]] = {} @@ -891,6 +885,7 @@ def sweep_segments(self, a: int = 0, apply_augmentations: bool = False) -> list[ [int(extent) for extent in sweep.source_shape], list(sweep.out_spatial), sweep.stage_plans, + tuple(sweep.stages), ) for sweep in source.pending_sweeps ] @@ -903,6 +898,7 @@ def sweep_segments(self, a: int = 0, apply_augmentations: bool = False) -> list[ list(source.shape), list(source.stage_plans[-1].out_shape), source.stage_plans, + tuple(source.stages), ) ) return segments @@ -923,7 +919,7 @@ def stream_refusal(self, a: int = 0, apply_augmentations: bool = True) -> str | return self._stream_refusals.get((a, apply_augmentations), "the chain cannot stream.") try: for segment in segments: - self._sweep_tile(segment.landing, segment.channels, segment.plans) + self.sizer_for(segment).sweep_tile() except DatasetManagerError as refusal: return str(refusal.args[0]) return None @@ -1128,9 +1124,17 @@ def _sweep( the pass failed, why: every stream is then aborted; an interrupt is re-raised.""" spatial = list(reference.out_spatial) channels = int(reference.source_shape[0]) - tile = self._sweep_tile(spatial, channels, source.stage_plans) + # Keyed to the segment being swept: ITS stages (the re-planned source's) and ITS store. + sizer = self._sizer( + spatial, + channels, + source.stage_plans, + tuple(source.stages), + self._entry_granularity(source.dataset, source.group, source.entry), + ) + tile = sizer.sweep_tile() targets = list(_sweep_targets(spatial, tile)) - depth = self._sweep_depth(spatial, channels, source.stage_plans, tile) + depth = sizer.sweep_depth(tile) if any(_shares_h5_file(source.dataset, member.sweep.destination) for member in members): # The h5 backend holds a per-file lock for a stream's whole life, on the thread that # opened it: a read of that file from any other thread waits for the close that the @@ -1244,42 +1248,8 @@ def _sweep_failed_because(self, sweep: _PendingSweep, reason: str) -> bool: def _sweep_depth( self, spatial: list[int], channels: int, plans: Sequence["_ReadStagePlan"], tile: list[int] ) -> int: - """How many blocks to keep in flight, raised only while that changes nothing but the clock. - - A deeper queue absorbs the jitter between stages of uneven cost, and it is paid in resident - blocks, which the sizing takes out of the block. Raised only while the block it allows is - still ``tile``: a smaller block is a different decomposition, which re-chunks the output - (the tile IS the store's chunk shape) and, on a map that does not factorise, moves the - written values. Where the block is bounded by something other than the budget, the extra - blocks are free, and the cap is what bounds them: on a 513x1331x1776 sweep in 40 blocks, a - second block in flight recovers 0.5 s of a 6.7 s run and a third recovers none. - """ - depth = sweep_module._sweep_pipeline_depth() - # DOWN BEFORE UP. `tile` may be the one the sizing found only after giving the queue up - # (:meth:`_sweep_tile`), and a run that kept the queue anyway would hold what the sizing was - # never told about -- the budget's whole promise, lost to a default nobody revisited. - while depth and not self._keeps_the_block(spatial, channels, plans, tile, depth): - depth -= 1 - while ( - depth - and depth < budget._SWEEP_MAX_DEPTH - and self._keeps_the_block(spatial, channels, plans, tile, depth + 1) - ): - depth += 1 - return depth - - def _keeps_the_block( - self, spatial: list[int], channels: int, plans: Sequence["_ReadStagePlan"], tile: list[int], depth: int - ) -> bool: - """Whether a queue of ``depth`` both affords ``tile`` and still picks it. - - Asked of the search and not of :meth:`_sweep_tile`, which falls back to no queue at all: a - depth that cannot hold the block would come back holding it, and every depth would look - affordable. - """ - budget = self._sweep_budget_bytes - found, held = self._tile_within(spatial, channels, plans, depth, budget) - return found == tile and (not budget or budget <= 0 or held <= budget) + """:meth:`SegmentSizer.sweep_depth` of the whole declared chain against the raw source.""" + return self._chain_sizer(spatial, channels, plans).sweep_depth(tile) def read_granularity(self) -> tuple[int, ...] | None: """The stored block this case's source reads are served in, spatial axes only, or ``None`` @@ -1289,37 +1259,6 @@ def read_granularity(self) -> tuple[int, ...] | None: self._read_granularity = None if granularity is None else tuple(granularity[1:]) return cast(tuple[int, ...] | None, self._read_granularity) - def _sweep_rows( - self, - spatial: list[int], - channels: int, - plans: Sequence["_ReadStagePlan"] = (), - depth: int | None = None, - ) -> int: - """The tallest region the sweep will cut whatever the budget: ``budget.SWEEP_SLAB_ROWS`` on a CPU, - taller on a GPU as its free memory allows. What the budget then affords is - :meth:`_sweep_tile`'s. - - The device's share is held to the SAME price as everything else (:meth:`sweep_block_bytes`), - which counts the source a region pulls and what the widest stage allocates on top of it: a - region counted as one landed plane raised the cap by the pull ratio of a GPU ``Resample``, - and nothing else bounded it where no host budget was declared. - """ - cap = max(1, int(budget.SWEEP_SLAB_ROWS)) - # Never below the store's own block: a region shorter than one reads it whole regardless - # (the hull is what a chunked read decodes), so cutting under it buys no memory back and - # only reads the same bytes again for the next region. - granularity = self.read_granularity() - if granularity is not None: - cap = max(cap, int(granularity[0])) - if self._chain_device is not None and self._chain_device.type == "cuda": - # On a GPU the transfers and launches per region are the cost: taller regions, as far as - # a quarter of the free device memory allows (measured +10-20 % at 500^3 over 64 rows). - free_bytes, _total = torch.cuda.mem_get_info(self._chain_device) - affordable = self._rows_within(spatial, channels, plans, depth, free_bytes * 0.25, _SWEEP_SLAB_ROWS_DEVICE) - cap = max(cap, affordable) - return cap - def read_plateau_rows(self, spatial: list[int], tolerance: float = _PLATEAU_READ_MARGIN, a: int = 0) -> int | None: """The shortest region height whose decomposition already reads what the tallest one reads, within ``tolerance``: the point past which taller regions buy no fewer source voxels. @@ -1341,17 +1280,10 @@ def read_plateau_rows(self, spatial: list[int], tolerance: float = _PLATEAU_READ plateau = _plateau_rows(spatial, segment.plans, tolerance) if plateau is None: return None - floor = self._sweep_rows(spatial, segment.channels, segment.plans) + sizer = self.sizer_for(segment._replace(landing=[int(extent) for extent in spatial])) + floor = sizer.sweep_rows() return max(plateau, min(floor, int(spatial[0]))) - @staticmethod - def _source_extents(spatial: Sequence[int], plans: Sequence["_ReadStagePlan"]) -> list[int]: - """The extents a chain's pull spans live in: the first stage's own input, which is the - stored volume. The landing is a different grid, and a hull capped against it is under-charged - wherever the source is the larger of the two -- a resample onto a coarser reference reads a - window the landing has no extent for.""" - return [int(extent) for extent in plans[0].in_shape] if plans else [int(extent) for extent in spatial] - def region_reads(self, rows: int, a: int = 0) -> "BlockReads | None": """What a decomposition into ``rows``-row regions costs this chain in source voxels. ``None`` when the chain cannot stream. @@ -1371,187 +1303,21 @@ def region_reads(self, rows: int, a: int = 0) -> "BlockReads | None": segment = segments[-1] spatial = [int(extent) for extent in segment.landing] tile = [max(1, min(int(rows), spatial[0])), *spatial[1:]] - return self.block_reads(spatial, tile, segment.plans) - - def _grid_rows(self, cap: int) -> list[int]: - """The heights that land on the store's block grid, up to ``cap``. - - A decomposition aligned to the grid reads each stored block exactly once; one that straddles - reads both blocks it touches, for every region, and holds the larger hull. There are only a - handful of such heights under any cap, so they are worth trying outright rather than hoping - a search over every height finds them. - """ - granularity = self.read_granularity() - if granularity is None: - return [] - block = max(1, int(granularity[0])) - # A grain of one row is met by every height, so there is no shortlist to try: a store banded - # along its leading axis (a memmap) says its grain on the axes BELOW, and enumerating every - # height here would hand the search the whole range one at a time. - if block <= 1: - return [] - return list(range(block, int(cap) + 1, block)) - - def _best_tile( - self, - spatial: list[int], - channels: int, - plans: Sequence["_ReadStagePlan"], - depth: int, - budget: float, - candidates: Sequence[int], - ) -> list[int]: - """The affordable candidate whose decomposition reads the least, the first one otherwise. - - The search below bisects on the height, which asks the price to rise with it. It does not: - a stored block is decoded whole, so the price steps rather than climbs, and the shape rule - (:meth:`_sweep_shape`) may answer a cube at one height and a slab at the next. Bisection - lands somewhere affordable, not on the best region the budget buys. - - Judged on reads and not on landed voxels, because that is what the sweep spends: a region - that lands a few more rows by straddling the store's grid reads both blocks it touches, for - every region of the case. Ties go to the taller block, which pays the per-region costs fewer - times. - """ - best: list[int] | None = None - best_reads = 0 - for rows in candidates: - tile = self._sweep_shape(spatial, plans, rows) - if self.sweep_block_bytes(spatial, channels, plans, tile, depth) > budget: - continue - reads = self._decomposition_reads(spatial, tile, plans) - taller = best is not None and np.prod(tile, dtype=np.int64) > np.prod(best, dtype=np.int64) - if best is None or reads < best_reads or (reads == best_reads and taller): - best, best_reads = tile, reads - return best if best is not None else self._sweep_shape(spatial, plans, candidates[0]) - - def _rows_within( - self, - spatial: list[int], - channels: int, - plans: Sequence["_ReadStagePlan"], - depth: int | None, - budget: float, - cap: int, - ) -> int: - """The tallest region up to ``cap`` rows whose priced block holds inside ``budget``, ``1`` - when none does: the one search both ceilings (the rank's budget, the device's free memory) - are answered by.""" - depth = sweep_module._sweep_pipeline_depth() if depth is None else depth - low, high = 1, max(1, int(cap)) - while low < high: - middle = (low + high + 1) // 2 - tile = self._sweep_shape(spatial, plans, middle) - if self.sweep_block_bytes(spatial, channels, plans, tile, depth) <= budget: - low = middle - else: - high = middle - 1 - return low - - def _sweep_shape(self, spatial: list[int], plans: Sequence["_ReadStagePlan"], rows: int) -> list[int]: - """The block ``rows`` rows of the landing become: the slab itself, or the cube of the same - volume where that pulls less. - - A region pulls the BOUNDING BOX of its own image under the chain's maps, so a slab spanning - the trailing plane pays that plane's extent for every degree of shear where a cube pays its - side: 1.79x the image against 1.09x on a 513x1331x1776 rigid+affine. Both are priced against - the plans' own pull maps (:func:`_pull_block_voxels`), and the cube wins only by - ``_SWEEP_TILE_MARGIN``: the decomposition is also the shape a store gets chunked in. Without - plans, the slab. - """ - from konfai.utils.ome_zarr import CHUNK_SPATIAL_TILE - - slab = [min(int(rows), int(spatial[0])), *(int(extent) for extent in spatial[1:])] - voxels = int(rows) * int(np.prod(spatial[1:], dtype=np.int64)) - cube = _cubic_tile(spatial, voxels, CHUNK_SPATIAL_TILE) - if cube == slab or not plans: - return slab - cheaper = self._decomposition_reads(spatial, cube, plans) <= ( - self._decomposition_reads(spatial, slab, plans) * _SWEEP_TILE_MARGIN - ) - return cube if cheaper else slab - - def _decomposition_reads(self, spatial: list[int], tile: Sequence[int], plans: Sequence["_ReadStagePlan"]) -> int: - """What sweeping ``spatial`` in ``tile`` reads from the store, all blocks together. - - The store's own currency: a chunked backend decodes whole blocks, so what a decomposition - reads is the sum of its blocks' hulls, and a shape is judged on the same figure it is later - priced with (:meth:`sweep_block_bytes`). Two currencies here and there is how a shape gets - chosen for pulling little and then costs what its hull costs. - """ - return self.block_reads(spatial, tile, plans).total - - def block_reads(self, spatial: list[int], tile: Sequence[int], plans: Sequence["_ReadStagePlan"]) -> BlockReads: - """What a decomposition of ``spatial`` into ``tile`` reads, walked once and kept. - - The sizing asks the same question of the same decomposition many times over -- the shape - search prices each candidate and then judges its reads, the height search bisects, and the - plateau walks a ladder -- and every one of those goes through the chain's pull maps, which - for a ``Resample`` is real geometry per block. Keyed by the decomposition AND by the plans - that map it, whose tuple is held here so no identity is reused under the key. - """ - granularity = self.read_granularity() - key = (tuple(spatial), tuple(tile), tuple(id(plan) for plan in plans), granularity) - held = self._block_reads.get(key) - if held is not None: - return held[1] - extents = self._source_extents(spatial, plans) if granularity is not None else [] - widest_pull = widest_hull = total = 0 - for span in _pull_block_spans(list(spatial), tile, plans): - pull = _span_voxels(span) - hull = pull if granularity is None else _chunk_hull_voxels(span, granularity, extents) - widest_pull, widest_hull, total = max(widest_pull, pull), max(widest_hull, hull), total + hull - reads = BlockReads(widest_pull, widest_hull, total) - self._block_reads[key] = (tuple(plans), reads) - return reads + return self.sizer_for(segment).block_reads(tile) def sweep_block_bytes( self, spatial: list[int], channels: int, plans: Sequence["_ReadStagePlan"], tile: list[int], depth: int ) -> int: - """What a sweep decomposed into ``tile`` holds at its peak: the source regions it has pulled - and the blocks it has landed, both counted by :func:`_sweep_resident_regions`, plus what the - widest stage of the chain allocates on top of the largest of them - (``Transform.working_multiple``). Each term is counted on the channels it actually holds - (:meth:`_chain_channels`), at ``_SWEEP_ELEMENT_BYTES`` each: the source pulls the source's, - the block lands the chain's. Beside this, and outside it, a streamed case holds - ``SWEEP_ENGINE_FLOOR_BYTES`` the decomposition cannot lower. + """:meth:`SegmentSizer.sweep_block_bytes` of the whole declared chain against the raw source. Public because the sizing holds this figure to the budget and a caller sizing a budget for a decomposition asks for it: one price, not two that drift apart. """ - pulled, landed = _sweep_resident_regions(depth) - block = int(np.prod(tile, dtype=np.int64)) - reads = self.block_reads(spatial, tile, plans) - pull = reads.widest_pull or block - source, landed_channels, _peak, working = self._chain_channels(channels) - held = pulled * pull * source + landed * block * landed_channels + working * max(pull, block) - # A chunked store serves a window by decoding the block-aligned hull that covers it, and - # assembles the window out of that: one read is in flight at a time, so the hull is resident - # ONCE, and the window is the part of it the chain keeps. What a straddling region costs is - # exactly this term, and it does not fall when the region does -- below one stored block a - # shorter region reads the same bytes and only reads them more often. - held += reads.widest_excess * source - return int(held * _SWEEP_ELEMENT_BYTES) - - def _chain_channels(self, channels: int) -> tuple[int, int, int, float]: - """What the channel axis costs along the chain, for the plan's arithmetic: the channels the - source pulls, the channels a block lands with, the widest the chain ever holds, and the - volumes-worth its widest stage allocates, that one counted on the channels that stage is - handed (``Transform.case_working_multiple``). - - Identity for a chain that keeps the axis, where all three counts are the source's and the - last is the widest declaration times it. ``OneHot`` is the stage that widens it, and a - block priced at the source's would be short by its class count. - """ - source = held = landed = peak = max(1, int(channels)) - working = 0.0 - for stage in self.transforms: - if not isinstance(stage, Transform): - continue - working = max(working, float(stage.case_working_multiple(self.name)) * held) - held = landed = max(1, int(stage.output_channels(held))) - peak = max(peak, held) - return source, landed, peak, working + return self._chain_sizer(spatial, channels, plans).sweep_block_bytes(tile, depth) + + def _sweep_shape(self, spatial: list[int], plans: Sequence["_ReadStagePlan"], rows: int) -> list[int]: + """:meth:`SegmentSizer.sweep_shape` of the whole declared chain against the raw source.""" + return self._chain_sizer(spatial, 1, plans).sweep_shape(rows) def working_multiple(self) -> float: """What this chain allocates beyond what it is handed, in volumes-worth: the largest a stage @@ -1569,63 +1335,60 @@ def working_multiple(self) -> float: def _sweep_tile( self, spatial: list[int], channels: int, plans: Sequence["_ReadStagePlan"] = (), depth: int | None = None ) -> list[int]: - """The block one sweep region covers: the tallest the cap allows that still holds inside the - budget, in the shape that pulls the least (:meth:`_sweep_shape`). - - The budget is what a sweep may HOLD, so it is the priced block (:meth:`sweep_block_bytes`) - that is held to it, never the landed rows alone: a REGRID pulling eight source voxels per - landed one, or a stage declaring eight volumes-worth of buffers, costs what it costs. The - search is over the height, because that is the one free parameter of the decomposition. - """ - depth = sweep_module._sweep_pipeline_depth() if depth is None else depth - budget = self._sweep_budget_bytes - tile, held = self._tile_within(spatial, channels, plans, depth, budget) - if not budget or budget <= 0 or held <= budget: - return tile - # THE READ-AHEAD IS THE ONE PART OF THE PRICE THE SIZING CHOSE. Everything else in the block - # is what the chain must hold to run at all; the queue is bought, and what it buys is wall - # clock (_sweep_depth: half a second of a 6.7 s run). A sweep about to refuse has no clock to - # buy, so it gives the queue up and asks once more. Three source regions resident become one, - # which is a quarter to a third of the block on a chain whose stage buffers dominate -- a - # narrow band, and inside it the difference is running against not running. - serial = None - if depth > 0: - candidate, serial = self._tile_within(spatial, channels, plans, 0, budget) - if serial <= budget: - return candidate - raise DatasetManagerError( - f"'{self.name}': no region of '{self.group_src}' fits the per-rank memory budget" - f" ({format_bytes(budget)}): the smallest one this chain can sweep holds" - f" {format_bytes(held)}" - + (f", and {format_bytes(serial)} with the read-ahead given up" if serial is not None else "") - + ".", - "Raise 'memory_budget'.", - ) + """:meth:`SegmentSizer.sweep_tile` of the whole declared chain against the raw source.""" + return self._chain_sizer(spatial, channels, plans).sweep_tile(depth) - def _tile_within( + def _sizer( self, - spatial: list[int], + spatial: Sequence[int], channels: int, plans: Sequence["_ReadStagePlan"], - depth: int, - budget: float | None, - ) -> tuple[list[int], int]: - """The best block a sweep of ``depth`` can afford, and what it holds: the search alone. - - No refusal and no fallback, because two callers ask it two different questions -- whether a - deeper queue still buys the same block (:meth:`_keeps_the_block`) and what to do when none - of them fits (:meth:`_sweep_tile`) -- and a search that answered either for them would - answer the other one wrong. - """ - cap = self._sweep_rows(spatial, channels, plans, depth) - if not budget or budget <= 0: - return self._sweep_shape(spatial, plans, cap), 0 - # The bisection never takes one row as affordable: the caller answers for it. What it finds - # is then judged against the store's own heights, because the price steps rather than climbs - # and bisection lands somewhere affordable, not on the best region the budget buys. - low = self._rows_within(spatial, channels, plans, depth, budget, cap) - tile = self._best_tile(spatial, channels, plans, depth, budget, [low, *self._grid_rows(cap)]) - return tile, self.sweep_block_bytes(spatial, channels, plans, tile, depth) + stages: Sequence[Stage], + granularity: tuple[int, ...] | None, + ) -> SegmentSizer: + return SegmentSizer( + spatial=[int(extent) for extent in spatial], + channels=int(channels), + plans=tuple(plans), + stages=tuple(stages), + granularity=granularity, + case=self.name, + group=self.group_src, + budget_bytes=self._sweep_budget_bytes, + device=self._chain_device, + block_reads_memo=self._block_reads, + ) + + def _chain_sizer(self, spatial: Sequence[int], channels: int, plans: Sequence["_ReadStagePlan"]) -> SegmentSizer: + """The single-segment view: the whole declared chain reading the raw source. Right whenever + the chain has no ``Save`` boundary; a boundary's segments must go through :meth:`sizer_for`.""" + return self._sizer(spatial, channels, plans, tuple(self.transforms), self.read_granularity()) + + def sizer_for(self, segment: SweepSegment) -> SegmentSizer: + """The pricing view keyed to ``segment``: its own stages, and its OWN store's grain.""" + return self._sizer( + segment.landing, + segment.channels, + segment.plans, + segment.stages, + self._entry_granularity(segment.dataset, segment.group, segment.entry), + ) + + def _entry_granularity(self, dataset: Dataset, group: str, entry: str) -> tuple[int, ...] | None: + # The raw source keeps its own resolved-once slot (read_granularity), which is also the one + # knob tests and callers already reset; the memo below is for the other segment sources. + if dataset is self.dataset and group == self.group_src: + return self.read_granularity() + key = (str(dataset.filename), group, entry) + if key not in self._granularities: + granularity = None + # A cache this run has still to write has no metadata to ask; its chunks will be the + # very tile being sized, so its reads align by construction and None is its honest grain. + if dataset.is_dataset_exist(group, entry): + stored = dataset.read_granularity(group, entry) + granularity = None if stored is None else tuple(stored[1:]) + self._granularities[key] = granularity + return self._granularities[key] def _get_streamed_data( self, diff --git a/konfai/data/patching/sizer.py b/konfai/data/patching/sizer.py new file mode 100644 index 00000000..c392f12a --- /dev/null +++ b/konfai/data/patching/sizer.py @@ -0,0 +1,344 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""The sweep's pricing engine, keyed to the segment it prices. + +The sizing once lived on the :class:`~konfai.data.patching.manager.DatasetManager` and read manager +state: the WHOLE declared chain's channel folds and the RAW source's read granularity. A segment +past a ``Save`` boundary was therefore priced with another segment's facts -- channel folds applied +twice onto a cache that already holds them, the wrong store's chunk grid, and a copy's draws priced +at zero. A :class:`SegmentSizer` is constructed per segment from explicit inputs, so the price can +only read the segment's own facts -- and it needs no dataset fixture to be tested. +""" + +from collections.abc import Sequence +from dataclasses import dataclass, field + +import numpy as np +import torch + +from konfai.data.patching import budget +from konfai.data.patching import sweep as sweep_module +from konfai.data.patching.budget import _SWEEP_ELEMENT_BYTES, _SWEEP_SLAB_ROWS_DEVICE, _SWEEP_TILE_MARGIN +from konfai.data.patching.stage import Stage, _ReadStagePlan +from konfai.data.patching.sweep import ( + BlockReads, + _cubic_tile, + _pull_block_spans, + _span_voxels, + _sweep_resident_regions, +) +from konfai.utils.budget import format_bytes +from konfai.utils.dataset import chunk_hull_voxels as _chunk_hull_voxels +from konfai.utils.errors import DatasetManagerError + + +@dataclass +class SegmentSizer: + """Prices one sweep segment: what a decomposition reads, what a block holds, what fits. + + ``spatial``/``channels``/``plans``/``stages`` are the segment's own landing, source channels, + region plans and stage list; ``granularity`` is the segment's OWN store's decode grain (spatial + axes, ``None`` when a read costs what it asks for -- including a cache this run has still to + write, whose chunks will be the very tile being sized, so its reads align by construction). + ``block_reads_memo`` is shared across sizers by the owning manager: the geometry walk is the + expensive part and its key already carries everything a sizer varies. + """ + + spatial: list[int] + channels: int + plans: tuple[_ReadStagePlan, ...] + stages: tuple[Stage, ...] + granularity: tuple[int, ...] | None + case: str + group: str + budget_bytes: float | None + device: torch.device | None + block_reads_memo: dict[tuple, tuple[tuple, BlockReads]] = field(default_factory=dict, repr=False) + + # ------------------------------------------------------------------ chain facts + + def chain_channels(self) -> tuple[int, int, int, float]: + """What the channel axis costs along the SEGMENT's stages: the channels the source pulls, + the channels a block lands with, the widest the segment ever holds, and the volumes-worth + its widest stage allocates, that one counted on the channels that stage is handed. + + Identity for a segment that keeps the axis. ``OneHot`` is the stage that widens it, and a + block priced at the source's would be short by its class count. Every stage of the segment + answers -- a copy's draws included: priced at zero, an Expand copy swept under a budget + that never heard of its ``grid_sample`` buffers. + """ + source = held = landed = peak = max(1, int(self.channels)) + working = 0.0 + for stage in self.stages: + multiple = getattr(stage, "case_working_multiple", None) + fold = getattr(stage, "output_channels", None) + if multiple is None or fold is None: + continue + working = max(working, float(multiple(self.case)) * held) + held = landed = max(1, int(fold(held))) + peak = max(peak, held) + return source, landed, peak, working + + # ------------------------------------------------------------------ reads + + def _source_extents(self) -> list[int]: + """The extents the pull spans live in: the first stage's own input, which is the stored + volume. The landing is a different grid, and a hull capped against it is under-charged + wherever the source is the larger of the two.""" + if self.plans: + return [int(extent) for extent in self.plans[0].in_shape] + return [int(extent) for extent in self.spatial] + + def block_reads(self, tile: Sequence[int]) -> BlockReads: + """What a decomposition of the landing into ``tile`` reads, walked once and kept. + + The sizing asks the same question of the same decomposition many times over -- the shape + search prices each candidate and then judges its reads, the height search bisects, and the + plateau walks a ladder -- and every one of those goes through the chain's pull maps, which + for a ``Resample`` is real geometry per block. Keyed by the decomposition AND by the plans + that map it, whose tuple is held so no identity is reused under the key. + """ + key = (tuple(self.spatial), tuple(tile), tuple(id(plan) for plan in self.plans), self.granularity) + held = self.block_reads_memo.get(key) + if held is not None: + return held[1] + extents = self._source_extents() if self.granularity is not None else [] + widest_pull = widest_hull = total = 0 + for span in _pull_block_spans(list(self.spatial), tile, self.plans): + pull = _span_voxels(span) + hull = pull if self.granularity is None else _chunk_hull_voxels(span, self.granularity, extents) + widest_pull, widest_hull, total = max(widest_pull, pull), max(widest_hull, hull), total + hull + reads = BlockReads(widest_pull, widest_hull, total) + self.block_reads_memo[key] = (tuple(self.plans), reads) + return reads + + def decomposition_reads(self, tile: Sequence[int]) -> int: + """What sweeping the landing in ``tile`` reads from the store, all blocks together. + + The store's own currency: a chunked backend decodes whole blocks, so what a decomposition + reads is the sum of its blocks' hulls, and a shape is judged on the same figure it is later + priced with (:meth:`sweep_block_bytes`). Two currencies here and there is how a shape gets + chosen for pulling little and then costs what its hull costs. + """ + return self.block_reads(tile).total + + def sweep_block_bytes(self, tile: list[int], depth: int) -> int: + """What a sweep decomposed into ``tile`` holds at its peak: the source regions it has pulled + and the blocks it has landed, both counted by :func:`_sweep_resident_regions`, plus what the + widest stage of the segment allocates on top of the largest of them. Each term is counted on + the channels it actually holds (:meth:`chain_channels`), at ``_SWEEP_ELEMENT_BYTES`` each. + Beside this, and outside it, a streamed case holds ``SWEEP_ENGINE_FLOOR_BYTES`` the + decomposition cannot lower. + """ + pulled, landed = _sweep_resident_regions(depth) + block = int(np.prod(tile, dtype=np.int64)) + reads = self.block_reads(tile) + pull = reads.widest_pull or block + source, landed_channels, _peak, working = self.chain_channels() + held = pulled * pull * source + landed * block * landed_channels + working * max(pull, block) + # A chunked store serves a window by decoding the block-aligned hull that covers it, and + # assembles the window out of that: one read is in flight at a time, so the hull is resident + # ONCE, and the window is the part of it the chain keeps. What a straddling region costs is + # exactly this term, and it does not fall when the region does -- below one stored block a + # shorter region reads the same bytes and only reads them more often. + held += reads.widest_excess * source + return int(held * _SWEEP_ELEMENT_BYTES) + + # ------------------------------------------------------------------ the search + + def sweep_shape(self, rows: int) -> list[int]: + """The block ``rows`` rows of the landing become: the slab itself, or the cube of the same + volume where that pulls less. + + A region pulls the BOUNDING BOX of its own image under the chain's maps, so a slab spanning + the trailing plane pays that plane's extent for every degree of shear where a cube pays its + side: 1.79x the image against 1.09x on a 513x1331x1776 rigid+affine. Both are priced against + the plans' own pull maps, and the cube wins only by ``_SWEEP_TILE_MARGIN``: the decomposition + is also the shape a store gets chunked in. Without plans, the slab. + """ + from konfai.utils.ome_zarr import CHUNK_SPATIAL_TILE + + spatial = self.spatial + slab = [min(int(rows), int(spatial[0])), *(int(extent) for extent in spatial[1:])] + voxels = int(rows) * int(np.prod(spatial[1:], dtype=np.int64)) + cube = _cubic_tile(spatial, voxels, CHUNK_SPATIAL_TILE) + if cube == slab or not self.plans: + return slab + cheaper = self.decomposition_reads(cube) <= (self.decomposition_reads(slab) * _SWEEP_TILE_MARGIN) + return cube if cheaper else slab + + def grid_rows(self, cap: int) -> list[int]: + """The heights that land on the store's block grid, up to ``cap``. + + A decomposition aligned to the grid reads each stored block exactly once; one that straddles + reads both blocks it touches, for every region, and holds the larger hull. There are only a + handful of such heights under any cap, so they are worth trying outright rather than hoping + a search over every height finds them. + """ + if self.granularity is None: + return [] + block = max(1, int(self.granularity[0])) + # A grain of one row is met by every height, so there is no shortlist to try: a store banded + # along its leading axis (a memmap) says its grain on the axes BELOW, and enumerating every + # height here would hand the search the whole range one at a time. + if block <= 1: + return [] + return list(range(block, int(cap) + 1, block)) + + def rows_within(self, depth: int | None, budget_bytes: float, cap: int) -> int: + """The tallest region up to ``cap`` rows whose priced block holds inside ``budget_bytes``, + ``1`` when none does: the one search both ceilings (the rank's budget, the device's free + memory) are answered by.""" + depth = sweep_module._sweep_pipeline_depth() if depth is None else depth + low, high = 1, max(1, int(cap)) + while low < high: + middle = (low + high + 1) // 2 + if self.sweep_block_bytes(self.sweep_shape(middle), depth) <= budget_bytes: + low = middle + else: + high = middle - 1 + return low + + def best_tile(self, depth: int, budget_bytes: float, candidates: Sequence[int]) -> list[int]: + """The affordable candidate whose decomposition reads the least, the first one otherwise. + + The search bisects on the height, which asks the price to rise with it. It does not: a + stored block is decoded whole, so the price steps rather than climbs, and the shape rule + may answer a cube at one height and a slab at the next. Bisection lands somewhere + affordable, not on the best region the budget buys. + + Judged on reads and not on landed voxels, because that is what the sweep spends: a region + that lands a few more rows by straddling the store's grid reads both blocks it touches, for + every region of the case. Ties go to the taller block, which pays the per-region costs + fewer times. + """ + best: list[int] | None = None + best_reads = 0 + for rows in candidates: + tile = self.sweep_shape(rows) + if self.sweep_block_bytes(tile, depth) > budget_bytes: + continue + reads = self.decomposition_reads(tile) + taller = best is not None and np.prod(tile, dtype=np.int64) > np.prod(best, dtype=np.int64) + if best is None or reads < best_reads or (reads == best_reads and taller): + best, best_reads = tile, reads + return best if best is not None else self.sweep_shape(candidates[0]) + + def sweep_rows(self, depth: int | None = None) -> int: + """The tallest region the sweep will cut whatever the budget: ``budget.SWEEP_SLAB_ROWS`` on + a CPU, taller on a GPU as its free memory allows. What the budget then affords is + :meth:`sweep_tile`'s. + + The device's share is held to the SAME price as everything else (:meth:`sweep_block_bytes`), + which counts the source a region pulls and what the widest stage allocates on top of it. + """ + cap = max(1, int(budget.SWEEP_SLAB_ROWS)) + # Never below the store's own block: a region shorter than one reads it whole regardless + # (the hull is what a chunked read decodes), so cutting under it buys no memory back and + # only reads the same bytes again for the next region. + if self.granularity is not None: + cap = max(cap, int(self.granularity[0])) + if self.device is not None and self.device.type == "cuda": + # On a GPU the transfers and launches per region are the cost: taller regions, as far as + # a quarter of the free device memory allows (measured +10-20 % at 500^3 over 64 rows). + free_bytes, _total = torch.cuda.mem_get_info(self.device) + cap = max(cap, self.rows_within(depth, free_bytes * 0.25, _SWEEP_SLAB_ROWS_DEVICE)) + return cap + + def tile_within(self, depth: int, budget_bytes: float | None) -> tuple[list[int], int]: + """The best block a sweep of ``depth`` can afford, and what it holds: the search alone. + + No refusal and no fallback, because two callers ask it two different questions -- whether a + deeper queue still buys the same block (:meth:`keeps_the_block`) and what to do when none + of them fits (:meth:`sweep_tile`) -- and a search that answered either for them would + answer the other one wrong. + """ + cap = self.sweep_rows(depth) + if not budget_bytes or budget_bytes <= 0: + return self.sweep_shape(cap), 0 + # The bisection never takes one row as affordable: the caller answers for it. What it finds + # is then judged against the store's own heights, because the price steps rather than climbs + # and bisection lands somewhere affordable, not on the best region the budget buys. + low = self.rows_within(depth, budget_bytes, cap) + tile = self.best_tile(depth, budget_bytes, [low, *self.grid_rows(cap)]) + return tile, self.sweep_block_bytes(tile, depth) + + def keeps_the_block(self, tile: list[int], depth: int) -> bool: + """Whether a queue of ``depth`` both affords ``tile`` and still picks it. + + Asked of the search and not of :meth:`sweep_tile`, which falls back to no queue at all: a + depth that cannot hold the block would come back holding it, and every depth would look + affordable. + """ + budget_bytes = self.budget_bytes + found, held = self.tile_within(depth, budget_bytes) + return found == tile and (not budget_bytes or budget_bytes <= 0 or held <= budget_bytes) + + def sweep_depth(self, tile: list[int]) -> int: + """How many blocks to keep in flight, raised only while that changes nothing but the clock. + + A deeper queue absorbs the jitter between stages of uneven cost, and it is paid in resident + blocks, which the sizing takes out of the block. Raised only while the block it allows is + still ``tile``: a smaller block is a different decomposition, which re-chunks the output + (the tile IS the store's chunk shape) and, on a map that does not factorise, moves the + written values. Where the block is bounded by something other than the budget, the extra + blocks are free, and the cap is what bounds them: on a 513x1331x1776 sweep in 40 blocks, a + second block in flight recovers 0.5 s of a 6.7 s run and a third recovers none. + """ + depth = sweep_module._sweep_pipeline_depth() + # DOWN BEFORE UP. `tile` may be the one the sizing found only after giving the queue up + # (:meth:`sweep_tile`), and a run that kept the queue anyway would hold what the sizing was + # never told about -- the budget's whole promise, lost to a default nobody revisited. + while depth and not self.keeps_the_block(tile, depth): + depth -= 1 + while depth and depth < budget._SWEEP_MAX_DEPTH and self.keeps_the_block(tile, depth + 1): + depth += 1 + return depth + + def sweep_tile(self, depth: int | None = None) -> list[int]: + """The block one sweep region covers: the tallest the cap allows that still holds inside the + budget, in the shape that pulls the least (:meth:`sweep_shape`). + + The budget is what a sweep may HOLD, so it is the priced block (:meth:`sweep_block_bytes`) + that is held to it, never the landed rows alone: a REGRID pulling eight source voxels per + landed one, or a stage declaring eight volumes-worth of buffers, costs what it costs. The + search is over the height, because that is the one free parameter of the decomposition. + """ + depth = sweep_module._sweep_pipeline_depth() if depth is None else depth + budget_bytes = self.budget_bytes + tile, held = self.tile_within(depth, budget_bytes) + if not budget_bytes or budget_bytes <= 0 or held <= budget_bytes: + return tile + # THE READ-AHEAD IS THE ONE PART OF THE PRICE THE SIZING CHOSE. Everything else in the block + # is what the chain must hold to run at all; the queue is bought, and what it buys is wall + # clock (sweep_depth: half a second of a 6.7 s run). A sweep about to refuse has no clock to + # buy, so it gives the queue up and asks once more. Three source regions resident become one, + # which is a quarter to a third of the block on a chain whose stage buffers dominate -- a + # narrow band, and inside it the difference is running against not running. + serial = None + if depth > 0: + candidate, serial = self.tile_within(0, budget_bytes) + if serial <= budget_bytes: + return candidate + raise DatasetManagerError( + f"'{self.case}': no region of '{self.group}' fits the per-rank memory budget" + f" ({format_bytes(budget_bytes)}): the smallest one this chain can sweep holds" + f" {format_bytes(held)}" + + (f", and {format_bytes(serial)} with the read-ahead given up" if serial is not None else "") + + ".", + "Raise 'memory_budget'.", + ) diff --git a/konfai/data/patching/stage.py b/konfai/data/patching/stage.py index 1e9e0a3e..d96dd52f 100644 --- a/konfai/data/patching/stage.py +++ b/konfai/data/patching/stage.py @@ -133,6 +133,22 @@ class AugmentedStage: def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: return self.augmentation.patch_locality(self.index, self.a, cache_attribute) + def output_channels(self, channels: int) -> int: + """Only Mask/Permute reshape, and neither folds the channel axis: a draw keeps it.""" + return channels + + def case_working_multiple(self, name: str) -> float: + """What this copy's draw allocates beyond its block, in volumes-worth of it. + + From the draw's locality: a REGRID draw resamples through ``grid_sample``, which builds the + pull box's coordinate grid (one volume per spatial axis) beside the landed block; any other + draw returns a fresh tensor or a view over a field of its own (one volume). Priced at zero, + an Expand copy's draws swept under a budget that never heard of their buffers. + """ + del name + kind = self.patch_locality(Attribute()).kind + return 4.0 if kind is LocalityKind.REGRID else 1.0 + def stream_region_source( self, name: str, diff --git a/konfai/data/patching/sweep.py b/konfai/data/patching/sweep.py index 4586acb2..4b3c1b8e 100644 --- a/konfai/data/patching/sweep.py +++ b/konfai/data/patching/sweep.py @@ -312,7 +312,8 @@ def _sweep_resident_regions(depth: int) -> tuple[int, int]: class SweepSegment(NamedTuple): - """One segment the streamed route sweeps: where it reads from, what it lands, how it pulls.""" + """One segment the streamed route sweeps: where it reads from, what it lands, how it pulls, + and the stages that run on it (the pricing is keyed to them, never to the whole chain).""" dataset: Dataset group: str @@ -320,6 +321,7 @@ class SweepSegment(NamedTuple): source_shape: list[int] landing: list[int] plans: tuple["_ReadStagePlan", ...] + stages: tuple[Stage, ...] = () @property def channels(self) -> int: From b161fbabd526a6ccd344b8dabdcdb4a9c5ec61ce Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:07:21 +0200 Subject: [PATCH 07/28] fix(data): refuse disagreeing grids, balance one-pass shards, cut copies Destination groups whose chains land on different grids failed as an IndexError deep in a DataLoader worker or silently under-covered the epoch: the patch mapping was counted on the last group alone. Counts are now compared across groups at prepare() and refused with both groups' folded shapes. One-pass DDP sharding reuses the greedy least-loaded partitioner the workers already had instead of contiguous equal-count slices ([1000,10,10,10] on 2 ranks was 1010/20). A one-pass singleton batch travels as a view instead of a stack copy (training keeps the copy: patches alias the cache). The validation: key now resolves through Subset's selector grammar (one grammar, ~ exclusion and negative slices included) instead of its own 60-line parser, and the identity TrainSubset subclass is gone. --- konfai/data/data_manager/__init__.py | 2 - konfai/data/data_manager/order.py | 38 +++-- konfai/data/data_manager/samples.py | 23 ++- konfai/data/data_manager/sources.py | 133 +++++++-------- konfai/data/data_manager/subset.py | 61 +++---- konfai/data/sampling.py | 3 - tests/unit/test_data_manager.py | 243 ++++++++++++++++++++++++++- 7 files changed, 374 insertions(+), 129 deletions(-) diff --git a/konfai/data/data_manager/__init__.py b/konfai/data/data_manager/__init__.py index 0c26da44..e75eec82 100644 --- a/konfai/data/data_manager/__init__.py +++ b/konfai/data/data_manager/__init__.py @@ -46,7 +46,6 @@ from konfai.data.data_manager.sources import DataTransform as DataTransform from konfai.data.data_manager.subset import PredictionSubset as PredictionSubset from konfai.data.data_manager.subset import Subset as Subset -from konfai.data.data_manager.subset import TrainSubset as TrainSubset __all__ = [ "BatchDataItem", @@ -69,7 +68,6 @@ "PredictionSubset", "Sample", "Subset", - "TrainSubset", "WindowedCaseSampler", "collate_konfai", ] diff --git a/konfai/data/data_manager/order.py b/konfai/data/data_manager/order.py index 8ddab034..95138a21 100644 --- a/konfai/data/data_manager/order.py +++ b/konfai/data/data_manager/order.py @@ -17,7 +17,7 @@ """The order patches are read in, and the sampler that walks it per rank.""" -from collections.abc import Iterator +from collections.abc import Iterator, Mapping from typing import TYPE_CHECKING, cast import torch @@ -28,6 +28,23 @@ from konfai.data.patching import DatasetPatch +def _balanced_case_partitions(case_loads: Mapping[int, int], bins: int) -> list[list[int]]: + """Whole cases dealt into ``bins``, greedy least-loaded by patch count, largest first. + + A case is never split (a rank's writes and a worker's buffer both hold whole cases), so an + equal COUNT of cases leaves the patch loads as uneven as the cases are: give the next case to + whoever holds the fewest patches so far. Deterministic: the sort is stable, so equal loads keep + ``case_loads``'s own order, and ties on load go to the first bin. + """ + loads = [0] * bins + partitions: list[list[int]] = [[] for _ in range(bins)] + for case in sorted(case_loads, key=lambda case: -case_loads[case]): + bin_index = min(range(bins), key=lambda bin_index: loads[bin_index]) + partitions[bin_index].append(case) + loads[bin_index] += case_loads[case] + return partitions + + def _interleaved_case_entries(patches: list["DatasetPatch"], entries: list[tuple[int, int]]) -> list[tuple[int, int]]: """One case's ``(copy, patch)`` entries ordered so the copies advance together along the slab axis. @@ -153,19 +170,14 @@ def __init__( def _partitions(self) -> list[list[int]]: """The cases each worker walks, balanced by the patches they hold. - A worker is handed whole cases, because a case is what its buffer keeps resident. Handing out - an equal COUNT of them leaves the patch counts as uneven as the cases are, and it is patches - that are walked: the workers then run out at different times, and the batches of whoever is - left shift onto the workers that finished: a case landing on two of them, each reading the - volume. Give the next case to whoever holds the fewest patches so far, largest first. + A worker is handed whole cases, because a case is what its buffer keeps resident. It is + patches that are walked, so an uneven deal runs the workers out at different times, and the + batches of whoever is left shift onto the workers that finished: a case landing on two of + them, each reading the volume. """ - loads = [0] * self.num_workers - partitions: list[list[int]] = [[] for _ in range(self.num_workers)] - for case in sorted(self.case_entries, key=lambda case: -len(self.case_entries[case])): - worker = min(range(self.num_workers), key=lambda worker: loads[worker]) - partitions[worker].append(case) - loads[worker] += len(self.case_entries[case]) - return partitions + return _balanced_case_partitions( + {case: len(entries) for case, entries in self.case_entries.items()}, self.num_workers + ) def _windowed_order(self) -> list[int]: generator = torch.Generator().manual_seed(int(torch.randint(0, 2**31 - 1, (1,)).item())) diff --git a/konfai/data/data_manager/samples.py b/konfai/data/data_manager/samples.py index 89877a24..871c321e 100644 --- a/konfai/data/data_manager/samples.py +++ b/konfai/data/data_manager/samples.py @@ -61,6 +61,10 @@ class DataItem: a: int p: int is_input: bool + #: Whether ``tensor`` may alias a tensor the loader keeps and reads again (the training cache, + #: re-read every epoch): the collate must then batch a COPY, never a view a downstream in-place + #: op could write through. One-pass loaders clear it, and their singletons batch as views. + aliases_cache: bool = True @dataclass(frozen=True) @@ -89,13 +93,25 @@ def pin_memory(self) -> "BatchDataItem": BatchSample: TypeAlias = dict[str, BatchDataItem] +def _batch_tensor(items: list[DataItem]) -> torch.Tensor: + """The batch tensor: a view for a singleton that aliases no re-read cache, a stacked copy else. + + ``torch.stack`` copies, and on the ``batch_size=1`` evaluation path that copy is a whole volume + per case, outside the memory budget's sizing. The view stays in the main process: a worker's + batch travels by STORAGE, and a patch-view's storage is the whole resident case. + """ + if len(items) == 1 and not items[0].aliases_cache and data.get_worker_info() is None: + return items[0].tensor.unsqueeze(0) + return torch.stack([it.tensor for it in items], dim=0) + + def collate_konfai(batch: list[Sample]) -> BatchSample: """Collate KonfAI samples into the batch structure expected by the workflows.""" batch_sample: BatchSample = {} for k in batch[0].keys(): items = [b[k] for b in batch] batch_sample[k] = BatchDataItem( - tensor=torch.stack([it.tensor for it in items], dim=0), + tensor=_batch_tensor(items), x=[it.x for it in items], a=[it.a for it in items], p=[it.p for it in items], @@ -123,10 +139,14 @@ def __init__( apply_augmentations: bool = True, use_cache=True, batch_size: int = 1, + single_pass: bool = False, ) -> None: self.rank = rank self.data = data self.mapping = mapping + # A one-pass workflow never re-reads what a sample's tensor could alias, so its items may + # batch as views; a training loader's items may alias the epoch-spanning cache and may not. + self.single_pass = single_pass self.patch_size = patch_size self.overlap = overlap self.groups_src = groups_src @@ -302,5 +322,6 @@ def __getitem__(self, index: int) -> Sample: a, p, chain.is_input, + aliases_cache=not self.single_pass, ) return sample diff --git a/konfai/data/data_manager/sources.py b/konfai/data/data_manager/sources.py index 4e44a5eb..255375f8 100644 --- a/konfai/data/data_manager/sources.py +++ b/konfai/data/data_manager/sources.py @@ -33,9 +33,9 @@ from konfai import konfai_state from konfai.data.augmentation import DataAugmentation, DataAugmentationsList from konfai.data.data_manager.groups import Group, GroupMetric, GroupOut, _chains -from konfai.data.data_manager.order import WindowedCaseSampler, _interleaved_case_entries +from konfai.data.data_manager.order import WindowedCaseSampler, _balanced_case_partitions, _interleaved_case_entries from konfai.data.data_manager.samples import _CACHE_ELEMENT_BYTES, DatasetIter, collate_konfai -from konfai.data.data_manager.subset import PredictionSubset, Subset, TrainSubset +from konfai.data.data_manager.subset import PredictionSubset, Subset from konfai.data.patching import DatasetManager, DatasetPatch from konfai.data.transform import ( Expand, @@ -405,21 +405,6 @@ def _groups_require_single_process_loading(cls, groups_src: Mapping[str, Group | return True return False - @staticmethod - def _read_names_from_file(filename: str) -> list[str]: - with open(filename) as f: - return [name.strip() for name in f if name.strip()] - - @classmethod - def _resolve_name_selectors(cls, selectors: list[str]) -> set[str]: - resolved_names: set[str] = set() - for selector in selectors: - if os.path.exists(selector): - resolved_names.update(cls._read_names_from_file(selector)) - else: - resolved_names.add(selector) - return resolved_names - @abstractmethod def __init__( self, @@ -484,6 +469,7 @@ def _configure_data_loading(self, use_cache: bool) -> None: buffer_size=self._buffer_size, use_cache=use_cache, batch_size=self.batch_size, + single_pass=self._reads_each_case_once, ) resolved_num_workers = self._num_workers if self.requires_single_process_loading: @@ -748,6 +734,38 @@ def _patch_counts(managers: dict[str, list[DatasetManager]], nb_augmentation: in last = next(reversed(managers.values()), []) return [[manager.get_size(a) for a in range(nb_augmentation)] for manager in last] + @staticmethod + def _check_cross_group_patch_counts(managers: dict[str, list[DatasetManager]], nb_augmentation: int) -> None: + """Refuse destination groups whose grids disagree, before a single patch is read. + + The mapping is counted on ONE group (``_patch_counts``) and every group is then read with + the same patch index: a group with more patches never has its tail enumerated, one with + fewer raises an IndexError deep in a loader worker. + """ + grouped = list(managers.items()) + if len(grouped) < 2: + return + reference_group, reference_managers = grouped[0] + for group, group_managers in grouped[1:]: + for reference, manager in zip(reference_managers, group_managers, strict=True): + for a in range(nb_augmentation): + if reference.get_size(a) == manager.get_size(a): + continue + chains = { + name: ", ".join(type(stage).__name__ for stage in m.transforms) or "no transforms" + for name, m in ((reference_group, reference), (group, manager)) + } + raise DatasetManagerError( + f"Case '{reference.name}': destination groups '{reference_group}' and '{group}' disagree" + f" on the patch grid of copy {a}: {reference.get_size(a)} patches over shape" + f" {reference.shapes[a]} vs {manager.get_size(a)} over {manager.shapes[a]}.", + f"'{reference_group}' folds its case through [{chains[reference_group]}]," + f" '{group}' through [{chains[group]}].", + "Every destination group is read with the same patch index, so their grids must" + " agree: align the chains (the stage that changes the shape on one group must" + " change it identically on the other) or the stored resolutions.", + ) + def _case_entry_counts(self, managers: dict[str, list[DatasetManager]]) -> list[int]: """Per case, its ``(copy, patch)`` entries over the training draws: what the float split shares out.""" nb_augmentation = self._get_nb_augmentation(self._get_data_augmentations(True)) @@ -758,7 +776,8 @@ def _resolve_validation_indices( subset_names: list[str], case_entry_counts: list[int] | None = None, ) -> list[int]: - index: list[int] = [] + if self.validation is None: + return [] if isinstance(self.validation, float): if self.validation <= 0 or self.validation >= 1: raise DatasetManagerError( @@ -773,47 +792,24 @@ def _resolve_validation_indices( for dataset_index, count in enumerate(case_entry_counts): cumulative += count if cumulative > threshold: - index = list(range(dataset_index, len(subset_names))) - break - elif isinstance(self.validation, str): - if ":" in self.validation: - index = list(range(int(self.validation.split(":")[0]), int(self.validation.split(":")[1]))) - elif os.path.exists(self.validation): - validation_names = [] - with open(self.validation) as f: - for name in f: - validation_names.append(name.strip()) - index = [i for i, n in enumerate(subset_names) if n in validation_names] - else: - raise DatasetManagerError( - f"Invalid string value for 'validation': '{self.validation}'", - "Expected one of the following formats:", - "\t• A slice string like '0:10'", - "\t• A path to a text file listing validation sample names (e.g., './val.txt')", - "\t• A list of text files listing validation sample names", - "\t• A float between 0 and 1 (e.g., 0.2)", - "\t• A list of sample names or indices", - "The provided value is neither a valid slice nor a readable file.", - "Please fix your 'validation' setting in the configuration.", - ) - elif isinstance(self.validation, list): - if len(self.validation) == 0: - index = [] - elif all(isinstance(item, int) for item in self.validation): - index = cast(list[int], self.validation) - elif all(isinstance(item, str) for item in self.validation): - validation_name_set = self._resolve_name_selectors(cast(list[str], self.validation)) - index = [i for i, n in enumerate(subset_names) if n in validation_name_set] - else: - element_types = sorted({type(item).__name__ for item in self.validation}) - raise DatasetManagerError( - f"Invalid list type for 'validation': elements of type {element_types} are not supported.", - "Supported list element types are:", - "\t• int → list of indices (e.g., [0, 1, 2])", - "\t• str → list of sample names or file paths", - f"Received list: {self.validation}", - ) - return index + return list(range(dataset_index, len(subset_names))) + return [] + if isinstance(self.validation, list) and all(isinstance(item, int) for item in self.validation): + return cast(list[int], self.validation) + selectors = self.validation if isinstance(self.validation, list) else [self.validation] + unsupported = sorted({type(item).__name__ for item in selectors if not isinstance(item, str | int)}) + if unsupported: + raise DatasetManagerError( + f"Invalid list type for 'validation': elements of type {unsupported} are not supported.", + "Supported list element types are:", + "\t• int → list of indices (e.g., [0, 1, 2])", + "\t• str → list of sample names, file paths, slices ('0:10') or '~' exclusions", + f"Received list: {self.validation}", + ) + # One selector grammar for 'subset:' and 'validation:': names, case-list files, slices + # (negative ends included) and '~' exclusions all resolve through the subset's machinery, + # against the RUN-ORDER names, so a slice keeps its positional meaning. + return self.subset._resolve_selectors(cast("list[str | int]", selectors), subset_names) def _split_train_validation_names( self, @@ -843,6 +839,8 @@ def _split_train_validation_names( f"Dataset size: {dataset_size}", f"Validation setting: {self.validation}", "Please increase the validation size, increase the dataset, or disable validation.", + "'validation' accepts the same selector spellings as 'subset': a float share, indices," + " case names, case-list files, slices like '0:10', and '~' exclusions.", ) return train_names, validation_names @@ -860,6 +858,7 @@ def _get_datasets( if managers is None: managers = self._build_managers(names, dataset_name, self.patch, data_augmentations_list, index_offset) nb_augmentation = self._get_nb_augmentation(data_augmentations_list) + self._check_cross_group_patch_counts(managers, nb_augmentation) mapping: list[tuple[int, int, int]] = [] # PREDICTION walks the mapping in order, and the copies of a TTA case must advance together # along the slab axis for the streamed write to hold a bounded window (see @@ -886,12 +885,14 @@ def _split(mapping: list[tuple[int, int, int]], world_size: int) -> list[list[tu mapping_by_index: dict[int, list[tuple[int, int, int]]] = {} for entry in mapping: mapping_by_index.setdefault(entry[0], []).append(entry) - unique_index = np.asarray(sorted(mapping_by_index)) - for shard in np.array_split(unique_index, world_size): - shard_mapping: list[tuple[int, int, int]] = [] - for dataset_index in shard.tolist(): - shard_mapping.extend(mapping_by_index[int(dataset_index)]) - mappings.append(shard_mapping) + # Balanced by patch LOAD, not case count: an equal-count contiguous split lands a + # [1000, 10, 10, 10]-patch cohort as 1010 against 20 on two ranks, and every rank waits + # for the slowest at the end-of-run barrier. Deterministic (sorted cases, stable greedy), + # so a restart shards identically; within a shard each case keeps its entries in mapping + # order, walked in ascending case order. + case_loads = {case: len(mapping_by_index[case]) for case in sorted(mapping_by_index)} + for shard in _balanced_case_partitions(case_loads, world_size): + mappings.append([entry for case in sorted(shard) for entry in mapping_by_index[case]]) else: size = len(mapping) for rank in range(world_size): @@ -1008,7 +1009,7 @@ def __init__( inline_augmentations: bool = False, patch: DatasetPatch | None = DatasetPatch(), memory_budget: str | float | None = None, - subset: TrainSubset = TrainSubset(), + subset: Subset = Subset(), batch_size: int = 1, validation: float | str | list[int] | list[str] | None = 0.2, validation_augmentations: bool = True, diff --git a/konfai/data/data_manager/subset.py b/konfai/data/data_manager/subset.py index 067ebe90..d3d7ca35 100644 --- a/konfai/data/data_manager/subset.py +++ b/konfai/data/data_manager/subset.py @@ -18,6 +18,7 @@ """Which cases a run reads.""" import os +from collections.abc import Sequence import numpy as np @@ -70,21 +71,29 @@ def _resolve_selector(self, subset: str | int, names: list[str]) -> tuple[set[in return {i for i, name in enumerate(names) if name in selected_names}, False if self._is_slice_selector(subset): start, _, end = subset.partition(":") - r = np.clip( - np.asarray([int(start), int(end)]), - 0, - size, - ) + # Negative bounds count from the end, Python-slice style: '0:-2' keeps all but the last two. + bounds = [int(bound) + size if int(bound) < 0 else int(bound) for bound in (start, end)] + r = np.clip(np.asarray(bounds), 0, size) return set(range(int(r[0]), int(r[1]))), False if subset in name_to_index: return {name_to_index[subset]}, False return set(), False - def _get_index(self, subset: str | int, names: list[str]) -> list[int]: - index, is_exclusion = self._resolve_selector(subset, names) - if is_exclusion: - return [i for i in range(len(names)) if i not in index] - return sorted(index) + def _resolve_selectors(self, selectors: Sequence[str | int], names: list[str]) -> list[int]: + """The positions ``selectors`` keep in ``names``: inclusions united, exclusions subtracted, + and a list of only exclusions defined against the full list.""" + include_index: set[int] = set() + exclude_index: set[int] = set() + has_include = False + for selector in selectors: + resolved_index, is_exclusion = self._resolve_selector(selector, names) + if is_exclusion: + exclude_index.update(resolved_index) + else: + include_index.update(resolved_index) + has_include = True + index_set = include_index if has_include else set(range(len(names))) + return sorted(index_set.difference(exclude_index)) @staticmethod def _excludes(selector: str | int) -> bool: @@ -117,44 +126,20 @@ def required_names(self) -> set[str] | None: def __call__(self, names: list[str], infos: dict[str, tuple[list[int], Attribute]]) -> set[str]: names = sorted(names) - size = len(names) if self.subset is None: - index = list(range(0, size)) + index = list(range(0, len(names))) elif isinstance(self.subset, list): - if len(self.subset) == 0: - index = [] - else: - include_index: set[int] = set() - exclude_index: set[int] = set() - has_include = False - for s in self.subset: - resolved_index, is_exclusion = self._resolve_selector(s, names) - if is_exclusion: - exclude_index.update(resolved_index) - else: - include_index.update(resolved_index) - has_include = True - index_set = include_index if has_include else set(range(size)) - index = sorted(index_set.difference(exclude_index)) + # An empty list selects nothing: only a list of ONLY exclusions reads as "everything but". + index = self._resolve_selectors(self.subset, names) if self.subset else [] else: - index = self._get_index(self.subset, names) + index = self._resolve_selectors([self.subset], names) return {names[i] for i in index} def __str__(self): return f"Subset : {self.subset} shuffle : {self.shuffle} shuffle_window : {self.shuffle_window}" -class TrainSubset(Subset): - def __init__( - self, - subset: str | list[int] | list[str] | None = None, - shuffle: bool = True, - shuffle_window: int | None = None, - ) -> None: - super().__init__(subset, shuffle, shuffle_window) - - class PredictionSubset(Subset): def __init__(self, subset: str | list[int] | list[str] | None = None) -> None: super().__init__(subset, False, None) diff --git a/konfai/data/sampling.py b/konfai/data/sampling.py index 64eada28..223aa90d 100644 --- a/konfai/data/sampling.py +++ b/konfai/data/sampling.py @@ -325,9 +325,6 @@ def source_index( """ rank = target_grid.rank rows_total = int(target_grid.size_zyx[0]) - plane = 1 - for extent in target_grid.size_zyx[1:]: - plane *= int(extent) # The walk holds several float64 tensors per voxel at once (world, index, per-tap weights and # positions, the corner gather), so a large region's TRANSIENTS dwarf its result: ~30 GB beside # a 3.6 GB answer, measured on an ExaSPIM slab. Slabbing the leading array axis bounds them diff --git a/tests/unit/test_data_manager.py b/tests/unit/test_data_manager.py index 753fee5b..c6e34102 100644 --- a/tests/unit/test_data_manager.py +++ b/tests/unit/test_data_manager.py @@ -33,6 +33,7 @@ from konfai.data.data_manager import ( BatchDataItem, Data, + DataItem, DataPrediction, DatasetIter, DataTrain, @@ -40,14 +41,16 @@ GroupTransform, PatchReadOrder, PredictionSubset, - TrainSubset, + Subset, WindowedCaseSampler, _cache_worker_count, + collate_konfai, ) from konfai.data.patching import DatasetManager, DatasetPatch from konfai.data.transform import Gradient, TensorCast, Transform, TransformLoader from konfai.utils.clock import restart_startup_clock from konfai.utils.dataset import Attribute, Dataset +from konfai.utils.errors import DatasetManagerError from konfai.utils.runtime import State from konfai.utils.utils import split_path_spec from oracle_support import geometry @@ -142,14 +145,53 @@ def test_data_split_prediction_keeps_case_patches_together_and_allows_empty_shar 4, ) + # Whole cases dealt largest-first onto the least-loaded rank: the two 2-patch cases land on the + # first two ranks, the 1-patch case on the third, and the spare rank stays empty. assert shards == [ [(0, 0, 0), (0, 0, 1)], - [(1, 0, 0)], [(2, 0, 0), (2, 0, 1)], + [(1, 0, 0)], [], ] +def test_one_pass_split_balances_ranks_by_patch_load_and_restarts_identically( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """An equal-count contiguous split lands a [1000, 10, 10, 10]-patch cohort as 1010 vs 20 on two + ranks and idles one GPU behind the other; balancing by patch load lands 1000 vs 30.""" + monkeypatch.setenv("KONFAI_STATE", str(State.PREDICTION)) + counts = [1000, 10, 10, 10] + mapping = [(case, 0, patch) for case, count in enumerate(counts) for patch in range(count)] + + shards = Data._split(mapping, 2) + + assert sorted(len(shard) for shard in shards) == [30, 1000] + owner: dict[int, int] = {} + for rank, shard in enumerate(shards): + for entry in shard: + assert owner.setdefault(entry[0], rank) == rank, f"case {entry[0]} split across ranks" + # Within a shard each case keeps its entries in mapping order (read order == write order). + for case in {entry[0] for entry in shard}: + assert [entry for entry in shard if entry[0] == case] == [entry for entry in mapping if entry[0] == case] + # Deterministic: a restart shards identically. + assert Data._split(mapping, 2) == shards + + +def test_train_split_stays_contiguous_and_untouched_by_the_one_pass_balancer( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # TRAIN shuffles the mapping anyway and its DDP contract is equal-LENGTH shards: the balanced + # per-case deal is a one-pass-only change. + monkeypatch.setenv("KONFAI_STATE", str(State.TRAIN)) + counts = [1000, 10, 10, 10] + mapping = [(case, 0, patch) for case, count in enumerate(counts) for patch in range(count)] + + shards = Data._split(mapping, 2) + + assert shards == [mapping[:515], mapping[515:]] + + def test_data_remap_dataset_indices_compacts_sparse_mapping_indices() -> None: indices, remapped = Data._remap_dataset_indices([(3, 0, 0), (3, 0, 1), (8, 1, 0), (3, 1, 2)]) @@ -237,6 +279,61 @@ def test_data_train_validation_accepts_mixed_case_names_and_case_files(tmp_path: assert validation_names == ["CASE_001", "CASE_002", "CASE_003"] +@pytest.mark.parametrize( + "selector", + [ + "1:3", + "0:-2", + "CASE_001", + ["CASE_001", "CASE_002"], + ["~CASE_001"], + [0, "CASE_002"], + "file", + ["file", "CASE_002"], + ], + ids=["slice", "negative-slice", "name", "names", "exclusion", "mixed", "file", "file-and-name"], +) +def test_subset_and_validation_accept_the_same_selector_spellings(tmp_path: Path, selector) -> None: + """'validation:' resolves through the same selector grammar as 'subset:': one set of spellings + to learn, and every fix or extension lands on both keys at once.""" + names = ["CASE_000", "CASE_001", "CASE_002", "CASE_003"] + fold = tmp_path / "fold.txt" + fold.write_text("CASE_001\nCASE_003\n", encoding="utf-8") + + def resolve(spelling): + return str(fold) if spelling == "file" else spelling + + selector = [resolve(s) for s in selector] if isinstance(selector, list) else resolve(selector) + + kept_by_subset = Subset(selector)(list(names), {}) + dataset = DataTrain(augmentations=None, validation=selector) + train_names, validation_names = dataset._split_train_validation_names(list(names)) + + assert set(validation_names) == kept_by_subset + assert sorted(train_names + validation_names) == names + + +def test_a_negative_slice_end_counts_from_the_end_python_style() -> None: + # '0:-2' once clipped to an empty range and blamed the subset as "too restrictive". + names = [f"CASE_{index:03d}" for index in range(5)] + assert Subset("0:-2")(names, {}) == {"CASE_000", "CASE_001", "CASE_002"} + assert Subset("-2:5")(names, {}) == {"CASE_003", "CASE_004"} + + +def test_an_unresolvable_validation_selector_is_refused_with_the_accepted_spellings() -> None: + dataset = DataTrain(augmentations=None, validation="no_such_case_or_file") + + with pytest.raises(DatasetManagerError, match="same selector spellings as 'subset'"): + dataset._split_train_validation_names(["CASE_000", "CASE_001"]) + + +def test_a_validation_list_with_an_unsupported_element_type_is_refused() -> None: + dataset = DataTrain(augmentations=None, validation=[0.5, "CASE_000"]) + + with pytest.raises(DatasetManagerError, match="Invalid list type"): + dataset._split_train_validation_names(["CASE_000", "CASE_001"]) + + def test_data_train_validation_none_keeps_full_dataset_for_training() -> None: dataset = DataTrain( augmentations=None, @@ -325,7 +422,7 @@ def __init__(self, index: int, group_src: str, group_dest: str, name: str, *args }, augmentations={"DataAugmentation_0": augmentations}, patch=None, - subset=TrainSubset(shuffle=False), + subset=Subset(shuffle=False), validation=0.2, validation_augmentations=validation_augmentations, ) @@ -639,6 +736,66 @@ def test_reset_augmentation_shares_one_draw_across_destination_groups() -> None: assert manager_a.patch.get_size(1) == manager_b.patch.get_size(1) +# -------------------------------------------------------------------------------------- +# Destination groups must agree on the patch grid: the mapping is counted on ONE of them +# -------------------------------------------------------------------------------------- + + +def _plain_manager(group_dest: str, array: np.ndarray) -> DatasetManager: + return DatasetManager( + index=0, + group_src="src", + group_dest=group_dest, + name="case_000", + dataset=cast(Dataset, _DummyDataset(array)), + patch=DatasetPatch([4, 4]), + transforms=[], + data_augmentations_list=[], + ) + + +def test_cross_group_patch_count_check_names_the_case_the_groups_and_their_shapes() -> None: + agreeing = { + "A": [_plain_manager("A", np.zeros((1, 8, 8), np.float32))], + "B": [_plain_manager("B", np.zeros((1, 8, 8), np.float32))], + } + Data._check_cross_group_patch_counts(agreeing, 1) # same grids: no refusal + + disagreeing = { + "A": [_plain_manager("A", np.zeros((1, 8, 8), np.float32))], + "B": [_plain_manager("B", np.zeros((1, 8, 4), np.float32))], + } + with pytest.raises(DatasetManagerError) as refusal: + Data._check_cross_group_patch_counts(disagreeing, 1) + + message = str(refusal.value) + assert "case_000" in message and "'A'" in message and "'B'" in message + assert "[8, 8]" in message and "[8, 4]" in message + + +def test_destination_groups_with_disagreeing_grids_are_refused_at_prepare(tmp_path: Path) -> None: + """Two chains folding a case to different grids used to surface as an IndexError deep in a + loader worker (last group counted larger) or as silently unenumerated patches (smaller): the + disagreement is a config error and must be refused before a single patch is read.""" + pytest.importorskip("SimpleITK") + store = Dataset(tmp_path / "Dataset", "mha") + store.write("CT", "CASE_000", np.zeros((1, 8, 8), np.float32), _image_attributes([0.0, 0.0], [1.0, 1.0])) + store.write("SEG", "CASE_000", np.zeros((1, 8, 4), np.float32), _image_attributes([0.0, 0.0], [1.0, 1.0])) + dataset = DataPrediction( + augmentations=None, + dataset_filenames=[f"{tmp_path / 'Dataset'}:mha"], + groups_src={ + group: Group(groups_dest={group: GroupTransform(transforms=None, patch_transforms=None)}) + for group in ("CT", "SEG") + }, + patch=DatasetPatch(patch_size=[4, 4], overlap=None), + subset=PredictionSubset(), + ) + + with pytest.raises(DatasetManagerError, match="disagree on the patch grid"): + dataset.prepare() + + # -------------------------------------------------------------------------------------- # WindowedCaseSampler - locality-aware training order, worker sharding, buffer hit rate # -------------------------------------------------------------------------------------- @@ -950,11 +1107,11 @@ def test_prediction_subset_order_stays_case_major_and_unwindowed() -> None: assert list(iter(windowed)) == list(range(len(mapping))) -def test_train_subset_exposes_shuffle_window_knob() -> None: +def test_subset_exposes_shuffle_window_knob() -> None: # The knob is a plain constructor argument so the reflection config engine can bind it. - default = TrainSubset() + default = Subset() assert default.shuffle_window is None - configured = TrainSubset(shuffle_window=4) + configured = Subset(shuffle_window=4) assert configured.shuffle_window == 4 assert configured.shuffle is True @@ -1154,6 +1311,80 @@ def pin_memory(self, *args: object, **kwargs: object) -> "Recording": assert batch["CT"].is_input is True +# -------------------------------------------------------------------------------------- +# collate_konfai: a one-pass singleton batches as a view, a training singleton as a copy +# -------------------------------------------------------------------------------------- + + +def _singleton_sample(tensor: torch.Tensor, aliases_cache: bool) -> dict[str, DataItem]: + return {"CT": DataItem("case", tensor, Attribute(), 0, 0, 0, True, aliases_cache=aliases_cache)} + + +def test_collate_batches_a_one_pass_singleton_as_a_view_and_a_training_singleton_as_a_copy() -> None: + """The stack copy is a whole volume per case on the batch_size=1 evaluation path, outside the + memory budget's sizing; a training item may alias the epoch-spanning cache, so its copy is what + protects the cache from any downstream in-place op.""" + tensor = torch.arange(4.0).reshape(1, 2, 2) + + view = collate_konfai([_singleton_sample(tensor, aliases_cache=False)])["CT"].tensor + assert view.data_ptr() == tensor.data_ptr(), "the one-pass singleton must be a view, not a copy" + assert view.shape == (1, 1, 2, 2) + + copy = collate_konfai([_singleton_sample(tensor, aliases_cache=True)])["CT"].tensor + assert copy.data_ptr() != tensor.data_ptr(), "a cache-aliasing singleton must be copied" + assert torch.equal(copy[0], tensor) + + +def test_collate_still_copies_inside_a_dataloader_worker(monkeypatch: pytest.MonkeyPatch) -> None: + # A worker's batch travels by STORAGE, and a patch-view's storage is the whole resident case. + monkeypatch.setattr(torch.utils.data, "get_worker_info", lambda: SimpleNamespace(id=0, num_workers=2)) + tensor = torch.arange(4.0).reshape(1, 2, 2) + + batched = collate_konfai([_singleton_sample(tensor, aliases_cache=False)])["CT"].tensor + + assert batched.data_ptr() != tensor.data_ptr() + + +def test_one_pass_loaders_mark_their_samples_as_cache_free() -> None: + # The flag rides the DatasetIter factory: one-pass workflows read each case once, so their + # items alias no tensor that is read again; training items may alias the cache. + from konfai.data.data_manager import DataMetric + + assert DataPrediction(augmentations=None).datasetIter.keywords["single_pass"] is True + assert DataMetric().datasetIter.keywords["single_pass"] is True + assert DataTrain(augmentations=None).datasetIter.keywords["single_pass"] is False + + +def test_dataset_iter_marks_items_from_its_single_pass_flag() -> None: + def dataset_iter(single_pass: bool) -> DatasetIter: + manager = DatasetManager( + index=0, + group_src="src", + group_dest="dest", + name="case_000", + dataset=cast(Dataset, _DummyDataset(np.zeros((1, 2, 2), np.float32))), + patch=None, + transforms=[_WholeVolumeTransform()], + data_augmentations_list=[], + ) + return DatasetIter( + rank=0, + data={"dest": [manager]}, + mapping=[(0, 0, 0)], + groups_src={"src": Group(groups_dest={"dest": GroupTransform(transforms=None, patch_transforms=None)})}, + inline_augmentations=False, + data_augmentations_list=[], + patch_size=None, + overlap=None, + buffer_size=1, + use_cache=False, + single_pass=single_pass, + ) + + assert dataset_iter(single_pass=True)[0]["dest"].aliases_cache is False + assert dataset_iter(single_pass=False)[0]["dest"].aliases_cache is True + + # -------------------------------------------------------------------------------------- # PredictionSubset: case selection and common-name resolution # -------------------------------------------------------------------------------------- From f8b54eb5eebe733737d2ccea938200b567472e50 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:12:21 +0200 Subject: [PATCH 08/28] style(apps): fix the lint findings surfaced by wiring apps into ruff Late-binding loop closure bound by default arg, zip strict, exception chaining, percent formats; the bundles now hold to the same lint gate as every other package. --- .../impact_reg_konfai/models/convexadam.py | 8 ++++--- .../impact_reg_konfai/models/elastix.py | 6 ++--- .../models/elastix_engine.py | 2 +- .../models/elastix_install.py | 6 ++--- .../impact_reg_konfai/models/fireants.py | 4 ++-- .../tests/unit/test_fireants_distances.py | 3 +-- konfai/data/patching/manager.py | 6 +++-- konfai/utils/budget.py | 24 ++++++++++++++++--- 8 files changed, 40 insertions(+), 19 deletions(-) diff --git a/apps/impact_reg/impact_reg_konfai/models/convexadam.py b/apps/impact_reg/impact_reg_konfai/models/convexadam.py index cd1c50a1..e22deda0 100644 --- a/apps/impact_reg/impact_reg_konfai/models/convexadam.py +++ b/apps/impact_reg/impact_reg_konfai/models/convexadam.py @@ -235,7 +235,7 @@ def __init__( self._model_paths = self._download_models(models) # Built lazily and cached: constructing an itk.ModelConfiguration loads the TorchScript model # from disk in C++, so build the list once and reuse it across both stages and every case. - self._configurations: "list[itk.ModelConfiguration] | None" = None + self._configurations: list[itk.ModelConfiguration] | None = None self._voxel_sizes = voxel_sizes self._overlap = overlap self._layers_masks = layers_masks @@ -290,7 +290,9 @@ def _model_configurations(self) -> list["itk.ModelConfiguration"]: list(layers_mask), self._mixed_precision, ) - for path, voxel_size, layers_mask in zip(self._model_paths, self._voxel_sizes, self._layers_masks) + for path, voxel_size, layers_mask in zip( + self._model_paths, self._voxel_sizes, self._layers_masks, strict=True + ) ] return self._configurations @@ -410,7 +412,7 @@ def _run_stages(self, fixed: "itk.Image", moving: "itk.Image", device: str) -> " coarse-only app, ``['fine']`` a fine-only app (zero warm-start), and ``['coarse', 'fine']`` chains both (the composite). Returns None when no deformable stage runs (e.g. a linear-only chain). """ - field: "itk.Image | None" = None + field: itk.Image | None = None for stage in self._stages: if stage == "coarse": field = self._coarse(fixed, moving, device) diff --git a/apps/impact_reg/impact_reg_konfai/models/elastix.py b/apps/impact_reg/impact_reg_konfai/models/elastix.py index 18d84c4f..dd95bcb8 100644 --- a/apps/impact_reg/impact_reg_konfai/models/elastix.py +++ b/apps/impact_reg/impact_reg_konfai/models/elastix.py @@ -61,7 +61,7 @@ def registry_choices() -> list[str]: def _num(x: object) -> str: """Format a number the elastix way: no trailing '.0' (6.0 -> '6', 0.2 -> '0.2').""" - return "%g" % float(x) + return f"{float(x):g}" @dataclass @@ -220,7 +220,7 @@ def generate_impact_parameter_map(template_text: str, resolutions: dict, registr models = _sorted_specs(r.models) entries = [registry[_model_key(m.ref)] for m in models] - def row(stem: str, values: list[str]) -> None: + def row(stem: str, values: list[str], k: int = k) -> None: impact.append(f"(Impact{stem}{k} " + " ".join(values) + ")") # From the registry ONLY the 3 truly model-fixed props (Dimension, NumberOfChannels, PatchSize = the @@ -228,7 +228,7 @@ def row(stem: str, values: list[str]) -> None: row("ModelsPath", [f'"{_model_key(m.ref)}"' for m in models]) row("Dimension", [e["dimension"] for e in entries]) row("NumberOfChannels", [e["numberofchannels"] for e in entries]) - row("PatchSize", [_patch_size(mode_clean, e, m.layers_mask) for e, m in zip(entries, models)]) + row("PatchSize", [_patch_size(mode_clean, e, m.layers_mask) for e, m in zip(entries, models, strict=True)]) row("VoxelSize", [" ".join(_num(v) for v in m.voxel_size) for m in models]) row("LayersMask", [f'"{m.layers_mask}"' for m in models]) row("SubsetFeatures", [str(m.subset_features) for m in models]) diff --git a/apps/impact_reg/impact_reg_konfai/models/elastix_engine.py b/apps/impact_reg/impact_reg_konfai/models/elastix_engine.py index 5017dd5d..916a1705 100644 --- a/apps/impact_reg/impact_reg_konfai/models/elastix_engine.py +++ b/apps/impact_reg/impact_reg_konfai/models/elastix_engine.py @@ -34,10 +34,10 @@ import torch import tqdm from huggingface_hub import hf_hub_download -from .elastix_install import get_elastix_bin, install_elastix_impact, try_elastix from konfai.utils.dataset import Attribute, data_to_image, image_to_data from .elastix import _is_local_ref, _model_key, _sorted_specs, generate_impact_parameter_map, load_models_registry +from .elastix_install import get_elastix_bin, install_elastix_impact, try_elastix # Elastix + IMPACT binary is cached once here (heavy: binary + LibTorch) and reused across runs. # Set KONFAI_ELASTIX_DIR to point at an existing install and skip the download. diff --git a/apps/impact_reg/impact_reg_konfai/models/elastix_install.py b/apps/impact_reg/impact_reg_konfai/models/elastix_install.py index c078a3a1..da92f320 100644 --- a/apps/impact_reg/impact_reg_konfai/models/elastix_install.py +++ b/apps/impact_reg/impact_reg_konfai/models/elastix_install.py @@ -258,17 +258,17 @@ def try_elastix(install_path: Path) -> None: if e.stderr: msg += "Error output:\n" msg += e.stderr.strip() - raise NameError(msg) + raise NameError(msg) from e except OSError as e: msg = ( "Elastix could not be started.\n\n" "This is usually caused by missing shared libraries " "(e.g. LibTorch or CUDA runtime).\n\n" - f"System error:\n{str(e)}" + f"System error:\n{e!s}" ) - raise NameError(msg) + raise NameError(msg) from e def main() -> None: diff --git a/apps/impact_reg/impact_reg_konfai/models/fireants.py b/apps/impact_reg/impact_reg_konfai/models/fireants.py index 97564d79..c15ecf98 100644 --- a/apps/impact_reg/impact_reg_konfai/models/fireants.py +++ b/apps/impact_reg/impact_reg_konfai/models/fireants.py @@ -679,14 +679,14 @@ def register( # refinement at full resolution, where each patch sees only local anatomy, so a per-patch # rigid has no global meaning and neighbouring patches would each estimate a different one # and tear the blended field at the seams. - affine_matrix: "torch.Tensor | None" + affine_matrix: torch.Tensor | None if self._linear_method == "none": affine_matrix = None # the deformable stage builds its own identity init else: # The rigid's starting translation mirrors ANTs' ``-r [fixed,moving,N]``: "cof" is the # centre of FRAME (N=0), "com" the centre of MASS (N=1). "cof" aligns the image frames, # not the subjects, so a subject sitting off its frame centre starts the chain misplaced. - init_translation: "str | torch.Tensor" = "cof" + init_translation: str | torch.Tensor = "cof" if self._moments_init == "com": init_translation = self._center_of_mass_translation( fixed, diff --git a/apps/impact_reg/tests/unit/test_fireants_distances.py b/apps/impact_reg/tests/unit/test_fireants_distances.py index 87214432..2500fb01 100644 --- a/apps/impact_reg/tests/unit/test_fireants_distances.py +++ b/apps/impact_reg/tests/unit/test_fireants_distances.py @@ -23,11 +23,10 @@ import numpy as np import pytest import torch - from impact_reg_konfai.models.fireants import ( + _DISTANCES, _EPS, _CosineDistance, - _DISTANCES, _NCCDistance, _SoftDiceDistance, ) diff --git a/konfai/data/patching/manager.py b/konfai/data/patching/manager.py index 640d9f2f..61ed7661 100644 --- a/konfai/data/patching/manager.py +++ b/konfai/data/patching/manager.py @@ -1182,9 +1182,11 @@ def regions() -> Iterator[tuple[int, list[list[slice]], torch.Tensor, Attribute] Attribute(reference.base_attributes), Attribute(evolved) if index == 0 else None, ) - for member in members: + for position, member in enumerate(members): with SWEEP_CLOCK.phase("chain"): - member_tensor = tensor.clone() if len(members) > 1 else tensor + # The clone protects the shared block from a member's in-place tail; the + # LAST member is the last reader, so its clone would protect nothing. + member_tensor = tensor if position == len(members) - 1 else tensor.clone() scope = Attribute(region_attribute) # Dispatched exactly as the stages before the marker are, so a tail stage # reading a companion volume (Mask) or drawing from the voxel's place diff --git a/konfai/utils/budget.py b/konfai/utils/budget.py index 3bd015db..9027c62c 100644 --- a/konfai/utils/budget.py +++ b/konfai/utils/budget.py @@ -26,6 +26,7 @@ import math import os import re +import warnings from contextlib import suppress from dataclasses import dataclass from pathlib import Path @@ -39,6 +40,9 @@ # allocator slack. Caching runs with zero DataLoader workers, so a fifth of the node held back is ample. AUTO_MEMORY_SAFETY_FRACTION = 0.8 +#: The smallest declared budget the stack can honor (see the warning in resolve_memory_budget). +MINIMUM_DECLARED_BUDGET_BYTES = 256 << 20 + # Decimal (10^n) and binary (2^n) suffixes; "" / "b" are bytes. Case is folded before lookup. _MEMORY_UNIT_BYTES: dict[str, int] = { "": 1, "b": 1, @@ -458,6 +462,20 @@ def resolve_memory_budget(memory_budget: str | float | None) -> MemoryBudget: f"auto: {format_bytes(node_bytes)} {source} x {AUTO_MEMORY_SAFETY_FRACTION:.0%}", shared_across_ranks=True, ) - return MemoryBudget( - float(parse_memory_budget_bytes(memory_budget)), f"{memory_budget!r}", shared_across_ranks=False - ) + declared = parse_memory_budget_bytes(memory_budget) + if declared < MINIMUM_DECLARED_BUDGET_BYTES: + # Below this the declaration describes no process that can run: the interpreter with torch + # and one imaging backend was measured at 647 MiB resident before the first voxel, and the + # per-case engine floor, the statistics scan's blocks and one collate copy sit outside the + # sizing model (a smaller declaration held at 512 MiB and broke below 128 as an + # unattributable kill deep in a run). Warned rather than refused: tests and probes size + # tiny fixtures under tiny declarations on purpose. + warnings.warn( + f"memory_budget {memory_budget!r} is below the smallest supported declaration" + f" ({MINIMUM_DECLARED_BUDGET_BYTES >> 20} MiB): the process floor alone is several times" + " this figure, and what the sizing model cannot see may exceed it. Declare at least" + " 512 MiB, or 'auto' to size from the detected memory.", + UserWarning, + stacklevel=2, + ) + return MemoryBudget(float(declared), f"{memory_budget!r}", shared_across_ranks=False) From 3b39b2d11168eb234c7b44027679a4c530ddc0f0 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:16:20 +0200 Subject: [PATCH 09/28] fix(cli): seed --init with an empty mapping, not a null root --- konfai/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/konfai/main.py b/konfai/main.py index 96be18b4..db204ef6 100644 --- a/konfai/main.py +++ b/konfai/main.py @@ -234,7 +234,7 @@ def _run_init(args: dict[str, Any]) -> None: config_path = Path(args.get(config_key) or args.get("config") or default_name) if not config_path.exists(): config_path.parent.mkdir(parents=True, exist_ok=True) - config_path.write_text(f"{root}:\n", encoding="utf-8") + config_path.write_text(f"{root}: {{}}\n", encoding="utf-8") args[config_key] = config_path builder = getattr(importlib.import_module(module_name), builder_name) accepted = inspect.signature(builder).parameters From 537bc7776d01fc407ce83d0489f67b6f3a51eb95 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:19:33 +0200 Subject: [PATCH 10/28] feat(bench): tracked harness behind the documented performance claims bench_streaming.py proves the bounded-memory claim in one command (a synthetic volume larger than the declared budget, whole-tree peak RSS reported beside both figures); bench_hotpaths.py pins the residual-Add fold, the one-pass collate view and the deferred criterion readout against the alternatives they replaced. --- benchmarks/README.md | 27 ++++++ benchmarks/bench_hotpaths.py | 97 ++++++++++++++++++++++ benchmarks/bench_streaming.py | 149 ++++++++++++++++++++++++++++++++++ 3 files changed, 273 insertions(+) create mode 100644 benchmarks/README.md create mode 100644 benchmarks/bench_hotpaths.py create mode 100644 benchmarks/bench_streaming.py diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 00000000..6355eb86 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,27 @@ +# KonfAI benchmarks + +Runnable evidence for the performance claims the documentation makes. Every script pins what it +measures, prints the environment it ran in (versions, host, device), and emits the markdown row the +docs carry, so a published number is reproducible with one command. + +## Protocol + +- Wall time is the median of 3 runs after 1 warmup, on an otherwise idle machine. +- Host memory is the peak resident set of the whole process tree (`psutil`), sampled at 50 ms. +- Device memory is `torch.cuda.max_memory_allocated()` plus the NVML process figure when available. +- Every report line carries: konfai/torch/SimpleITK versions, CPU model, GPU model, and the input's + shape/dtype/checksum, so two numbers are only ever compared on the same footing. + +## Scripts + +| Script | Claim it evidences | +|---|---| +| `bench_streaming.py` | A volume larger than the declared memory budget is transformed with peak RAM bounded by the budget, not the volume (`--gib 16 --budget 1`). | +| `bench_hotpaths.py` | Framework-side hot paths hold their measured costs: the residual `Add` fold, the one-pass collate view, deferred criterion readout. | + +## App-level comparisons + +The published app tables (KonfAI-MRSegmentator / KonfAI-TotalSegmentator against the original +tools) are produced by each app's own benchmark entry, which needs the published weights and a +licensed case; see `apps/mrsegmentator/README.md` and `apps/totalsegmentator/README.md`. The +protocol above applies unchanged: same case, same weights, median of 3, whole-tree RSS. diff --git a/benchmarks/bench_hotpaths.py b/benchmarks/bench_hotpaths.py new file mode 100644 index 00000000..f112563b --- /dev/null +++ b/benchmarks/bench_hotpaths.py @@ -0,0 +1,97 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Micro-benchmarks of framework-side hot paths, each printed beside the alternative it replaced. + + python benchmarks/bench_hotpaths.py [--device cuda] + +These are the measurements behind three audit-driven changes: the residual ``Add`` fold (one fused +elementwise kernel against stack+reduce and its N-tensor transient), the one-pass collate view +(no volume copy at batch size 1), and the deferred criterion readout (no ``.item()`` host sync +inside forward). CPU numbers show the copies; the sync cost needs ``--device cuda``. +""" + +import argparse +import time + +import torch + + +def _timed(fn, repeats: int = 20, device: torch.device | None = None) -> float: + fn() # warmup + if device is not None and device.type == "cuda": + torch.cuda.synchronize(device) + start = time.perf_counter() + for _ in range(repeats): + fn() + if device is not None and device.type == "cuda": + torch.cuda.synchronize(device) + return (time.perf_counter() - start) / repeats + + +def bench_add(device: torch.device) -> None: + a = torch.randn(2, 32, 64, 64, 64, device=device) + b = torch.randn_like(a) + stacked = _timed(lambda: torch.sum(torch.stack([a, b]), dim=0), device=device) + folded = _timed(lambda: a + b, device=device) + transient = 2 * a.numel() * a.element_size() + print( + f"Add (residual sum, {list(a.shape)}): stack+sum {stacked * 1e3:.2f} ms" + f" (+{transient / 2**20:.0f} MiB transient) vs fold {folded * 1e3:.2f} ms" + ) + + +def bench_collate(device: torch.device) -> None: + volume = torch.randn(1, 256, 256, 256, device=device) + copy = _timed(lambda: torch.stack([volume], dim=0), device=device) + view = _timed(lambda: volume.unsqueeze(0), device=device) + print( + f"collate at batch=1 ({list(volume.shape)}): stack {copy * 1e3:.2f} ms" + f" ({volume.numel() * 4 / 2**20:.0f} MiB copied) vs view {view * 1e6:.1f} us" + ) + + +def bench_criterion_sync(device: torch.device) -> None: + if device.type != "cuda": + print("criterion sync: needs --device cuda (a CPU tensor has no queue to drain)") + return + output = torch.randn(2, 1, 64, 64, 64, device=device) + target = torch.randn_like(output) + + def synced() -> float: + return (output - target).abs().mean().item() + + def deferred() -> torch.Tensor: + return (output - target).abs().mean().detach() + + eager = _timed(synced, device=device) + lazy = _timed(deferred, device=device) + print(f"criterion readout: .item() in forward {eager * 1e3:.3f} ms vs deferred 0-d tensor {lazy * 1e3:.3f} ms") + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--device", default="cpu") + args = parser.parse_args() + device = torch.device(args.device) + print(f"torch {torch.__version__} on {device}") + bench_add(device) + bench_collate(device) + bench_criterion_sync(device) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/bench_streaming.py b/benchmarks/bench_streaming.py new file mode 100644 index 00000000..c0a32a59 --- /dev/null +++ b/benchmarks/bench_streaming.py @@ -0,0 +1,149 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""One-command proof of the bounded-memory claim. + +Synthesizes a volume of ``--gib`` GiB, runs a real TRANSFORM chain over it under a declared +``--budget`` GiB, and reports the whole-process-tree peak RSS beside both figures. The claim this +evidences: peak RAM tracks the BUDGET, not the volume. + + python benchmarks/bench_streaming.py --gib 16 --budget 1 + +Needs the ``imaging`` extra (SimpleITK + h5py) and free disk for the synthetic volume. +""" + +import argparse +import json +import platform +import tempfile +import threading +import time +from pathlib import Path + +import numpy as np +import psutil + + +def _tree_rss(process: psutil.Process) -> int: + total = 0 + for member in [process, *process.children(recursive=True)]: + try: + total += member.memory_info().rss + except psutil.NoSuchProcess: + continue + return total + + +class PeakSampler(threading.Thread): + """Whole-tree peak RSS, sampled at 50 ms: children (DataLoader workers, spawn ranks) count.""" + + def __init__(self) -> None: + super().__init__(daemon=True) + self._process = psutil.Process() + self._stop = threading.Event() + self.peak = 0 + + def run(self) -> None: + while not self._stop.is_set(): + self.peak = max(self.peak, _tree_rss(self._process)) + time.sleep(0.05) + + def stop(self) -> int: + self._stop.set() + self.join() + return self.peak + + +def synthesize(root: Path, gib: float) -> tuple[Path, list[int]]: + """A synthetic volume of ~``gib`` GiB in an h5 store, written slab by slab so the synthesis + itself never holds the volume (the bench must not be the thing that spends the RAM).""" + import h5py + + voxels = int(gib * 2**30 / 4) # float32 + side = max(64, int(round((voxels / 4) ** (1 / 3)))) # anisotropic: 4x taller than wide + shape = [4 * side, side, side] + store = root / "Dataset.h5" + rng = np.random.default_rng(0) + with h5py.File(store, "w") as file: + dataset = file.create_dataset( + "CT/CASE_000", shape=(1, *shape), dtype=np.float32, chunks=(1, 64, side, side) + ) + dataset.attrs["Origin"] = "[0. 0. 0.]" + dataset.attrs["Spacing"] = "[1. 1. 1.]" + dataset.attrs["Direction"] = "[1. 0. 0. 0. 1. 0. 0. 0. 1.]" + for start in range(0, shape[0], 64): + stop = min(start + 64, shape[0]) + dataset[0, start:stop] = rng.normal(0.0, 100.0, size=(stop - start, *shape[1:])).astype(np.float32) + return store, shape + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--gib", type=float, default=4.0, help="synthetic volume size in GiB (default 4)") + parser.add_argument("--budget", type=float, default=1.0, help="declared memory_budget in GiB (default 1)") + parser.add_argument("--keep", action="store_true", help="keep the scratch directory") + args = parser.parse_args() + + import konfai + + scratch = Path(tempfile.mkdtemp(prefix="konfai_bench_")) + print(f"[bench] scratch: {scratch}") + print(f"[bench] synthesizing ~{args.gib:g} GiB volume ...", flush=True) + store, shape = synthesize(scratch, args.gib) + + from konfai.data.transform import Normalize, Write + + sampler = PeakSampler() + sampler.start() + start = time.perf_counter() + result = konfai.transform( + "BENCH", + f"{store}:h5", + {"CT": {"CT": [Normalize(min_value=-1, max_value=1), Write(dataset=f"{scratch / 'Out'}:h5")]}}, + memory_budget=f"{args.budget}gib", + transforms_dir=scratch / "Transforms", + quiet=True, + ) + elapsed = time.perf_counter() - start + peak = sampler.stop() + + del result + report = { + "volume_gib": round(args.gib, 2), + "declared_budget_gib": round(args.budget, 2), + "peak_tree_rss_gib": round(peak / 2**30, 2), + "wall_s": round(elapsed, 1), + "shape_zyx": shape, + "versions": { + "konfai": getattr(konfai, "__version__", "dev"), + "numpy": np.__version__, + "python": platform.python_version(), + }, + "host": platform.node(), + } + print(json.dumps(report, indent=2)) + print( + f"| {args.gib:g} GiB volume | budget {args.budget:g} GiB " + f"| peak {peak / 2**30:.2f} GiB | {elapsed:.1f} s |" + ) + if not args.keep: + import shutil + + shutil.rmtree(scratch, ignore_errors=True) + + +if __name__ == "__main__": + main() From e345b9aa2304e184e25f3fc611b959d81f884297 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:19:34 +0200 Subject: [PATCH 11/28] fix(models): fused residual adds, SDPA attention, catalog atoms, real forwards Add.forward is a sequential fold (byte-identical for the 2-input sums every consumer uses; the N-tensor transient and the stacked ONNX pattern are gone). MultiHeadSelfAttention rides scaled_dot_product_attention (max diff vs MONAI parity 7e-7, no tolerance changed). The YAML builder gains the missing stateless atoms (SiLU/ELU/CELU/Mish/Softplus/ Hardswish/PixelShuffle/ConstantPad*/Detach). A smoke test now constructs and forwards every documented model class; it caught GeneratorV3 wiring that could never forward (fixed) and cStyleGan's broken mapping network (retained, construction-only, documented). Gan subnetworks are built per instance instead of shared class-level defaults. --- .../models/python/generation/diffusionGan.py | 7 +- konfai/models/python/generation/gan.py | 9 +- konfai/network/blocks.py | 13 +- konfai/utils/model_builder.py | 12 ++ tests/unit/test_model_builder.py | 57 ++++++++ tests/unit/test_models.py | 132 +++++++++++++++++- 6 files changed, 218 insertions(+), 12 deletions(-) diff --git a/konfai/models/python/generation/diffusionGan.py b/konfai/models/python/generation/diffusionGan.py index f623c3c7..f7e9ca8b 100755 --- a/konfai/models/python/generation/diffusionGan.py +++ b/konfai/models/python/generation/diffusionGan.py @@ -600,6 +600,9 @@ def __init__( dim: int, ) -> None: super().__init__() + # nb_class=0: no segmentation heads inside a GAN generator. UNet.UNetBlock has no + # internal X_0_* branches (that is NestedUNet's convention), so naming them here bound + # the Tanh head to the argmax label map (1-channel, long) instead of the feature map. self.add_module( "UNetBlock_0", UNet.UNetBlock( @@ -610,15 +613,13 @@ def __init__( upsample_mode=blocks.UpsampleMode[upsample_mode], attention=attention, block=blocks.ConvBlock if block_type == "Conv" else blocks.ResBlock, - nb_class=1, + nb_class=0, dim=dim, ), - out_branch=[f"X_0_{j + 1}" for j in range(len(channels) - 2)], ) self.add_module( "Head", GeneratorV3.NestedUNetHead(channels[:2], dim=dim), - in_branch=[f"X_0_{len(channels) - 2}"], ) def __init__( diff --git a/konfai/models/python/generation/gan.py b/konfai/models/python/generation/gan.py index 3fbbcdfb..0c2e0292 100755 --- a/konfai/models/python/generation/gan.py +++ b/konfai/models/python/generation/gan.py @@ -231,10 +231,15 @@ def get_name(self): class Gan(network.Network): def __init__( self, - generator: Generator = Generator(), - discriminator: Discriminator = Discriminator(), + # Annotations stay non-optional: the config binder builds a config-object parameter from + # its annotation and `X | None = None` would mean "only if configured". The None default + # gives each Gan() fresh sub-networks instead of instances shared through the signature. + generator: Generator = None, # type: ignore[assignment] + discriminator: Discriminator = None, # type: ignore[assignment] ) -> None: super().__init__() + generator = generator if generator is not None else Generator() + discriminator = discriminator if discriminator is not None else Discriminator() self.add_module( "Discriminator_B", discriminator, diff --git a/konfai/network/blocks.py b/konfai/network/blocks.py index fced092e..a568b662 100755 --- a/konfai/network/blocks.py +++ b/konfai/network/blocks.py @@ -688,7 +688,12 @@ def __init__(self) -> None: super().__init__() def forward(self, *tensor: torch.Tensor) -> torch.Tensor: - return torch.sum(torch.stack(tensor), dim=0) + # Sequential fold: same left-to-right sum as a stacked reduction, without materializing + # a contiguous copy of every input, and it exports as ONNX Add nodes. + output = tensor[0] + for other in tensor[1:]: + output = output + other + return output class Multiply(torch.nn.Module): @@ -909,7 +914,6 @@ def __init__(self, hidden_size: int, num_heads: int, qkv_bias: bool = False) -> raise ValueError(f"hidden_size ({hidden_size}) must be divisible by num_heads ({num_heads}).") self.num_heads = num_heads self.head_dim = hidden_size // num_heads - self.scale = self.head_dim**-0.5 self.qkv = torch.nn.Linear(hidden_size, hidden_size * 3, bias=qkv_bias) self.out_proj = torch.nn.Linear(hidden_size, hidden_size) @@ -917,8 +921,9 @@ def forward(self, tensor: torch.Tensor) -> torch.Tensor: batch, tokens, hidden = tensor.shape qkv = self.qkv(tensor).reshape(batch, tokens, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4) query, key, value = qkv[0], qkv[1], qkv[2] - attention = ((query @ key.transpose(-2, -1)) * self.scale).softmax(dim=-1) - output = (attention @ value).permute(0, 2, 1, 3).reshape(batch, tokens, hidden) + # SDPA's default scale is head_dim ** -0.5, the scale this block always used. + output = torch.nn.functional.scaled_dot_product_attention(query, key, value) + output = output.permute(0, 2, 1, 3).reshape(batch, tokens, hidden) return self.out_proj(output) def extra_repr(self) -> str: diff --git a/konfai/utils/model_builder.py b/konfai/utils/model_builder.py index 83ebbcdc..709161cb 100644 --- a/konfai/utils/model_builder.py +++ b/konfai/utils/model_builder.py @@ -58,6 +58,10 @@ def factory(*, dim: int, **kwargs: Any) -> torch.nn.Module: "AdaptiveAvgPool": _dimensional_factory("AdaptiveAvgPool"), "BatchNorm": _dimensional_factory("BatchNorm"), "InstanceNorm": _dimensional_factory("InstanceNorm"), + "ConstantPad": _dimensional_factory("ConstantPad"), + "ConstantPad1d": torch.nn.ConstantPad1d, + "ConstantPad2d": torch.nn.ConstantPad2d, + "ConstantPad3d": torch.nn.ConstantPad3d, "Conv1d": torch.nn.Conv1d, "Conv2d": torch.nn.Conv2d, "Conv3d": torch.nn.Conv3d, @@ -74,11 +78,19 @@ def factory(*, dim: int, **kwargs: Any) -> torch.nn.Module: "LeakyReLU": torch.nn.LeakyReLU, "PReLU": torch.nn.PReLU, "GELU": torch.nn.GELU, + "SiLU": torch.nn.SiLU, + "ELU": torch.nn.ELU, + "CELU": torch.nn.CELU, + "Mish": torch.nn.Mish, + "Softplus": torch.nn.Softplus, + "Hardswish": torch.nn.Hardswish, "Sigmoid": torch.nn.Sigmoid, "Tanh": torch.nn.Tanh, "Softmax": torch.nn.Softmax, "Identity": torch.nn.Identity, + "PixelShuffle": torch.nn.PixelShuffle, "Add": blocks.Add, + "Detach": blocks.Detach, "Multiply": blocks.Multiply, "ClipNormalize": blocks.ClipNormalize, "ArgMax": blocks.ArgMax, diff --git a/tests/unit/test_model_builder.py b/tests/unit/test_model_builder.py index b0c4f502..f31b32f9 100644 --- a/tests/unit/test_model_builder.py +++ b/tests/unit/test_model_builder.py @@ -36,17 +36,29 @@ BUILTIN_TYPES = [ "ArgMax", "AvgPool", + "CELU", "Concat", + "ConstantPad", + "ConstantPad1d", + "ConstantPad2d", + "ConstantPad3d", "Conv", "Conv1d", "Conv2d", "Conv3d", "ConvTranspose", "ConvBlock", + "Detach", + "ELU", + "Hardswish", "Identity", "MaxPool", + "Mish", + "PixelShuffle", "ResBlock", + "SiLU", "Softmax", + "Softplus", ] THREE_MODULE_YAML = """ @@ -70,6 +82,35 @@ SINGLE_IDENTITY_YAML = "modules:\n - type: Identity\n" +# Exercises the stateless torch.nn atoms added for post-2019 architectures: the ConstantPad +# dimensional factory, SiLU/Mish activations, PixelShuffle, and blocks.Detach. +MODERN_ATOMS_YAML = """ +name: ModernAtoms +modules: + - name: Pad + type: ConstantPad + args: + dim: 2 + padding: 1 + value: 0.0 + - name: Conv + type: Conv2d + args: + in_channels: 1 + out_channels: 4 + kernel_size: 3 + - name: SiLU + type: SiLU + - name: Shuffle + type: PixelShuffle + args: + upscale_factor: 2 + - name: Mish + type: Mish + - name: Stop + type: Detach +""" + ROUTED_GRAPH_YAML = """ name: RoutedGraph parameters: @@ -119,6 +160,22 @@ def test_three_module_head_runs_a_forward_pass(self) -> None: assert output.shape == (2, 1, 4, 4) + def test_modern_atoms_build_and_forward(self) -> None: + model = build_model_from_yaml(yaml_str=MODERN_ATOMS_YAML) + + children = dict(model.items()) + assert isinstance(children["Pad"], torch.nn.ConstantPad2d) + assert isinstance(children["SiLU"], torch.nn.SiLU) + assert isinstance(children["Shuffle"], torch.nn.PixelShuffle) + assert isinstance(children["Mish"], torch.nn.Mish) + assert isinstance(children["Stop"], model_builder.blocks.Detach) + + inputs = torch.randn(2, 1, 8, 8, requires_grad=True) + output = model.forward_tensor(inputs) + # pad 8->10, conv k3 10->8 with 4 channels, PixelShuffle(2) folds them into 1x16x16. + assert output.shape == (2, 1, 16, 16) + assert not output.requires_grad # the terminal Detach cut the graph + def test_single_identity_module_builds_one_child_network(self) -> None: model = build_model_from_yaml(yaml_str=SINGLE_IDENTITY_YAML) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 93636fbd..562044c3 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -18,13 +18,18 @@ import pytest import torch -from konfai.models.python.classification.convNeXt import LayerScaler +from konfai.models.python.classification.convNeXt import ConvNeXt, LayerScaler +from konfai.models.python.classification.resnet import ResNet +from konfai.models.python.features.mind import MIND +from konfai.models.python.generation import cStyleGan, diffusionGan, gan from konfai.models.python.generation.diffusionGan import CycleGanDiscriminator -from konfai.models.python.generation.vae import LinearVAE +from konfai.models.python.generation.vae import VAE, LinearVAE from konfai.models.python.registration.registration import VoxelMorph, rigid_affine -from konfai.models.python.representation.representation import Adaptation +from konfai.models.python.representation.representation import Adaptation, Representation from konfai.models.python.segmentation.NestedUNet import NestedUNet +from konfai.models.python.segmentation.residualencoderunet import ResidualEncoderUNet from konfai.models.python.segmentation.UNet import UNet +from konfai.models.python.segmentation.unetplusplus import UNetPlusPlus from konfai.utils.errors import ConfigError # -------------------------------------------------------------------------------------- @@ -169,3 +174,124 @@ def test_nested_unet_refuses_attention_it_does_not_have() -> None: # instead of silently building the plain model. with pytest.raises(ConfigError, match="attention"): NestedUNet(dim=2, channels=[1, 8, 16, 32], attention=True) + + +# -------------------------------------------------------------------------------------- +# The documented catalog: every model class listed in +# docs/source/reference/components/models.md constructs and runs a tiny forward. +# -------------------------------------------------------------------------------------- + +_SMALL = [1, 4, 8, 16] # channels small enough for a CPU forward, deep enough for real routing + +# id -> (builder, input shapes). Input shapes ``None`` = construction-only: the model's forward +# is broken by its own graph wiring, independent of the blocks API (see the entry's comment). +DOCUMENTED_MODEL_SPECS: dict = { + "UNet": (lambda: UNet(dim=2, channels=[1, 4, 8], nb_class=2), [(1, 1, 16, 16)]), + "NestedUNet": (lambda: NestedUNet(dim=2, channels=_SMALL, nb_class=2), [(1, 1, 16, 16)]), + "UNetPlusPlus": ( + lambda: UNetPlusPlus(dim=2, in_channels=3, classes=1, encoder_name="resnet18"), + [(1, 3, 64, 64)], + ), + "ResidualEncoderUNet": ( + lambda: ResidualEncoderUNet( + dim=2, + in_channels=1, + n_stages=3, + features_per_stage=[4, 8, 16], + strides=[1, 2, 2], + n_blocks_per_stage=[1, 1, 1], + num_classes=2, + ), + [(1, 1, 16, 16)], + ), + "ResNet": ( + lambda: ResNet(patch=None, dim=2, in_channels=1, widths=[4, 4, 8, 16, 32], num_classes=5), + [(1, 1, 32, 32)], + ), + "ConvNeXt": ( + lambda: ConvNeXt(patch=None, dim=2, in_channels=1, depths=[1, 1, 1, 1], widths=[4, 8, 16, 32], num_classes=[2]), + [(1, 1, 32, 32)], + ), + "VAE": (lambda: VAE(dim=2, channels=[1, 4, 8]), [(1, 1, 16, 16)]), + "LinearVAE": (lambda: LinearVAE(in_features=32, hidden_features=16, latent_dim=4), [(2, 32)]), + "gan.Generator": (lambda: gan.Generator(dim=2, patch=None), [(1, 1, 16, 16)]), + "gan.Discriminator": (lambda: gan.Discriminator(dim=2), [(1, 1, 32, 32)]), + "gan.Gan": ( + lambda: gan.Gan(gan.Generator(dim=2, patch=None), gan.Discriminator(dim=2)), + [(1, 1, 32, 32), (1, 1, 32, 32)], + ), + "DiffusionGan": ( + lambda: diffusionGan.DiffusionGan( + diffusionGan.GeneratorV1(dim=2, patch=None), diffusionGan.DiscriminatorADA(dim=2) + ), + [(1, 3, 32, 32), (1, 1, 32, 32)], + ), + "DiffusionGanV2": ( + lambda: diffusionGan.DiffusionGanV2( + diffusionGan.GeneratorV2(dim=2, channels=_SMALL), diffusionGan.Discriminator(dim=2) + ), + [(1, 1, 32, 32), (1, 1, 32, 32)], + ), + "DiffusionCycleGan": ( + lambda: diffusionGan.DiffusionCycleGan( + diffusionGan.CycleGanGeneratorV3(dim=2, channels=_SMALL), + diffusionGan.CycleGanDiscriminator(dim=2), + ), + [(1, 1, 32, 32), (1, 1, 32, 32)], + ), + "CycleGanDiscriminator": ( + lambda: CycleGanDiscriminator(dim=2), + [(1, 1, 32, 32), (1, 1, 32, 32)], + ), + "CycleGanGeneratorV1": ( + lambda: diffusionGan.CycleGanGeneratorV1(dim=2), + [(1, 3, 16, 16), (1, 3, 16, 16)], + ), + "CycleGanGeneratorV2": ( + lambda: diffusionGan.CycleGanGeneratorV2(dim=2, channels=_SMALL), + [(1, 1, 16, 16), (1, 1, 16, 16)], + ), + "CycleGanGeneratorV3": ( + lambda: diffusionGan.CycleGanGeneratorV3(dim=2, channels=_SMALL), + [(1, 1, 16, 16), (1, 1, 16, 16)], + ), + # Construction-only: MappingNetwork's first Linear reads branch 0 (the [z|c] concat) where it + # expects c alone, and NormalNoise emits a rank-1 draw that Concat(dim=1) cannot take; the + # forward has never run and fixing it means redesigning the mapping network. + "cStyleGan.Generator": ( + lambda: cStyleGan.Generator(patch=None, dim=2, channels=_SMALL, z_dim=8, c_dim=2, w_dim=8), + None, + ), + "VoxelMorph": (lambda: VoxelMorph(dim=2, shape=[32, 32]), [(1, 3, 32, 32), (1, 1, 32, 32)]), + "Representation": (lambda: Representation(), [(1, 1, 8, 8, 8)] * 3), + "MIND": (lambda: MIND(dim=2), [(1, 1, 16, 16)]), +} + + +@pytest.mark.parametrize("model_name", DOCUMENTED_MODEL_SPECS, ids=DOCUMENTED_MODEL_SPECS) +def test_documented_model_constructs_and_forwards(model_name: str) -> None: + builder, input_shapes = DOCUMENTED_MODEL_SPECS[model_name] + model = builder() + if input_shapes is None: + return + model.eval() + output = None + with torch.no_grad(): + for _, tensor in model.named_forward(*[torch.randn(*shape) for shape in input_shapes]): + output = tensor + assert output is not None + + +def test_gan_default_builds_fresh_unshared_subnetworks() -> None: + # The generator/discriminator defaults were class-level instances evaluated at import, so two + # Gan() shared (and co-trained) the same sub-networks. Defaults must build per instance, + # identically to an explicit Gan(Generator(), Discriminator()). + first, second = gan.Gan(), gan.Gan() + assert not {id(p) for p in first.parameters()} & {id(p) for p in second.parameters()} + + explicit = gan.Gan(gan.Generator(), gan.Discriminator()) + signature = [(name, tuple(p.shape)) for name, p in explicit.graph_parameters()] + assert [(name, tuple(p.shape)) for name, p in first.graph_parameters()] == signature + + # The shipped one-discriminator-three-roles aliasing survives inside each instance. + assert first["Discriminator_B"] is first["Discriminator_pB_detach"] is first["Discriminator_pB"] From 6a94ea56274ea25d0cfd3d1743e5f707a9fecde4 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:20:25 +0200 Subject: [PATCH 12/28] fix(metric): one typed criterion result, no mid-forward syncs, dead criteria out CriterionResult normalizes the four ad-hoc return shapes at the two consumers (a wrong shape is a named refusal, not a crash deep in a workflow). The MaskedLoss family, TRE, Variance/Mean and the IMPACT losses return detached 0-d tensors instead of .item(), and dict metrics defer (values, labels) into the existing batched per-device readout, so a training step no longer drains the CUDA queue inside forward. FID is deleted (crashed as a training metric, statistically invalid per case), with TripletLoss, L1LossRepresentation, MutualInformationLoss (a MONAI copy) and WGP (zero references anywhere; classpath routes documented). LPIPS scores the whole batch instead of sample 0 and loses its tqdm; FocalLoss drops its 5-class alpha default; the IMPACT uint8-mask sniff verifies {0,1} and refuses a mask that is the target; PSNR and SSIM share one 4095 CT range. huggingface_hub imports lazily. --- .../reference/components/losses-metrics.md | 26 +- konfai/evaluator.py | 21 +- konfai/metric/measure/__init__.py | 18 +- konfai/metric/measure/adversarial.py | 74 +---- konfai/metric/measure/base.py | 75 +++-- konfai/metric/measure/impact.py | 67 ++-- konfai/metric/measure/regression.py | 185 +++++------ konfai/metric/measure/segmentation.py | 22 +- konfai/metric/schedulers.py | 5 +- konfai/network/network/measure.py | 119 +++++-- konfai/predictor/ensemble.py | 7 +- tests/unit/test_measure.py | 303 +++++++++++++++--- 12 files changed, 586 insertions(+), 336 deletions(-) diff --git a/docs/source/reference/components/losses-metrics.md b/docs/source/reference/components/losses-metrics.md index 5a9d46a5..5ee01950 100644 --- a/docs/source/reference/components/losses-metrics.md +++ b/docs/source/reference/components/losses-metrics.md @@ -16,8 +16,10 @@ to the training loss; `is_loss: false` detaches it and only logs it. In an `Evaluation.yml`, every criterion is a metric. The return *shape* controls **logging**: a criterion may return a bare `Tensor`, -or a tuple `(tensor, scalar_or_dict)`. The tuple form is what lets `Dice`, `TRE`, -etc. log clean per-label values while still driving the gradient with the tensor. +or a tuple `(tensor, value)` where the value is a float, a 0-d tensor read back +lazily, a per-label dict, or a `(values, labels)` pair (also read back lazily). +The tuple form is what lets `Dice`, `TRE`, etc. log clean per-label values while +still driving the gradient with the tensor. ``` So most pixelwise criteria are **dual-use**: the same class is a training loss or @@ -78,7 +80,7 @@ groups act as a mask. | --- | --- | --- | --- | | `Dice` | `(Tensor, dict)` dual-use | Soft Dice per label; loss `= 1 − mean(dice)`, per-label dict logged. Resamples target to output (nearest). | `labels=None` (None → all present labels) | | `CrossEntropyLoss` | `Tensor` loss | Wraps `nn.CrossEntropyLoss` (squeezes the target channel). | `weight=None, reduction="mean"` | -| `FocalLoss` | `Tensor` loss | Multi-class focal loss. Note: `alpha` is a per-label weight list indexed by label id. | `gamma=2.0, alpha=[0.5,2.0,0.5,0.5,1], reduction="mean"` | +| `FocalLoss` | `Tensor` loss | Multi-class focal loss. `alpha` is an optional per-label weight list indexed by label id; `None` weights every class equally, and a list shorter than the class count is refused. | `gamma=2.0, alpha=None, reduction="mean"` | | `Accuracy` | `Tensor` metric | This batch's classification accuracy. It keeps no state: the logging window averages it over the batches and resets between train and validation, so one figure never blends epochs or splits. |: | | `DiceSaveMap` | 3-tuple | Dice + voxelwise error map (for a save-map consumer). | `labels=None, dataset=None, group=None` | @@ -88,7 +90,6 @@ groups act as a mask. | --- | --- | --- | | `BCE` | `BCEWithLogitsLoss` against a constant real/fake target. | `target=0` | | `PatchGanLoss` | LSGAN-style MSE against a constant target. | `target=0` | -| `WGP` | `mean((output−1)²)` WGAN-style penalty. |: | | `Gram` | Gram-matrix (style) loss. |: | | `PerceptualLoss` | Feature-space perceptual loss over a pretrained KonfAI `Network` (custom multi-model forward; requires a real `path_model` checkpoint). | `model_loader`, `path_model`, `modules`, `shape` | @@ -98,17 +99,16 @@ groups act as a mask. | --- | --- | --- | --- | | `TRE` | `(Tensor, dict)` metric | Target Registration Error between predicted/target landmark coordinates. |: | | `GradientImages` | `Tensor` loss | Image-gradient smoothness loss (2D/3D auto); regulariser, or gradient-difference if a target is given. |: | -| `MutualInformationLoss` | `Tensor` loss | Parzen-window Gaussian mutual information (returns `−MI`). | `num_bins=23, sigma_ratio=0.5` | +| `monai.losses:GlobalMutualInformationLoss` | `Tensor` loss | Parzen-window mutual information, by classpath (needs MONAI installed). | see MONAI | | `KLDivergence` | `Tensor` loss | VAE KL term. **Rewires the graph** on init, inserting a `LatentDistribution` block; computes closed-form KL from `mu`/`log_std`. | `shape` (**required**), `dim=100, mu=0, std=1` | ## Uncertainty / bookkeeping | Name | Role | Purpose | Key args | | --- | --- | --- | --- | -| `Variance` | `(Tensor, float)` metric | Channel-wise variance mean (ensemble/uncertainty). | `name="Variance"` | -| `Mean` | `(Tensor, float)` metric | Mean of the output tensor. | `name="Mean"` | -| `TripletLoss` | `Tensor` loss | `nn.TripletMarginLoss` over a 3-tuple output. |: | -| `L1LossRepresentation` | `Tensor` loss | L1 between two representations + variance-collapse regulariser. |: | +| `Variance` | `(Tensor, value)` metric | Channel-wise variance mean (ensemble/uncertainty). | `name="Variance"` | +| `Mean` | `(Tensor, value)` metric | Mean of the output tensor. | `name="Mean"` | +| `torch:nn:TripletMarginLoss` | `Tensor` loss | Triplet margin loss, by classpath. | see PyTorch | ## IMPACT feature-based criteria @@ -130,13 +130,13 @@ Imported lazily; a missing package raises a `MeasureError` with an install hint. | Name | Extra | Purpose | Key args | | --- | --- | --- | --- | -| `SSIM` | `konfai[ssim]` (scikit-image) | Masked structural similarity. Default `dynamic_range → 4024`. | `dynamic_range=None` | +| `SSIM` | `konfai[ssim]` (scikit-image) | Masked structural similarity. Default `dynamic_range → 4095`. | `dynamic_range=None` | | `LPIPS` | `konfai[lpips]` | Learned perceptual similarity (AlexNet by default), tiled over patches. | `model="alex"` | -| `FID` | `konfai[fid]` (scipy + torchvision) | Fréchet Inception Distance (InceptionV3). |: | +| `torchmetrics.image.fid:FrechetInceptionDistance` | `torchmetrics` | Fréchet Inception Distance, by classpath. FID is defined over dataset-level feature distributions, so compute it over the whole prediction set, never per case. | see torchmetrics | None of these pins a device. The `IMPACT*` sanity check probes its TorchScript -extractor on the CPU and discards the result; `LPIPS` and `FID` follow the device -of the tensor they are given: the rank's GPU under DDP, or the CPU. `SAM_Perceptual` +extractor on the CPU and discards the result; `LPIPS` follows the device +of the tensor it is given: the rank's GPU under DDP, or the CPU. `SAM_Perceptual` runs no sanity check at all. ## Next steps diff --git a/konfai/evaluator.py b/konfai/evaluator.py index 8e392235..06ec801f 100644 --- a/konfai/evaluator.py +++ b/konfai/evaluator.py @@ -30,6 +30,7 @@ from konfai import config_file, cuda_visible_devices, evaluations_directory, konfai_root from konfai.data.data_manager import BatchDataItem, BatchSample, DataMetric, DatasetIter from konfai.network.network import build_configured_criterions +from konfai.network.network.measure import CriterionResult from konfai.utils.budget import node_local_ranks, set_per_rank_budget from konfai.utils.clock import SweepClock from konfai.utils.config import apply_config, config, strict_config @@ -400,21 +401,20 @@ def update(self, batch_sample: BatchSample, statistics: Statistics) -> dict[str, ] name = batch_sample[output_group].name[0] for metric in self.metrics[output_group][target_group]: - with self._clock.phase(metric.get_name()), torch.no_grad(): + metric_name: str = metric.get_name() + with self._clock.phase(metric_name), torch.no_grad(): if getattr(metric, "accepts_attributes", False): loss = metric(output_tensor, *targets, attributes=target_attribute) else: loss = metric(output_tensor, *targets) - if isinstance(loss, tuple): - true_loss = loss[1] - if len(loss) == 3 and metric.dataset: - with self._clock.phase("map"): - self._write_map(metric, output_group, name, loss[2]) - else: - true_loss = loss.item() + outcome = CriterionResult.of(loss, metric_name) + true_loss = outcome.materialized() + if outcome.map is not None and getattr(metric, "dataset", None): + with self._clock.phase("map"): + self._write_map(metric, output_group, name, outcome.map) direction = "max" if getattr(metric, "maximize", False) else "min" - base_key = f"{output_group}:{target_group}:{metric.get_name()}" + base_key = f"{output_group}:{target_group}:{metric_name}" Evaluator._record_value(result, statistics, base_key, true_loss, direction) if len(self.metrics) > 0: statistics.add(result, name) @@ -584,8 +584,7 @@ def _flush_pending(self, statistics: Statistics) -> None: return result: dict[str, float] = {} for (output_group, target_group, _index), (metric, states) in self._pending.items(): - loss = metric.combine_metric(states) - true_loss = loss[1] if isinstance(loss, tuple) else float(loss.item()) + true_loss = CriterionResult.of(metric.combine_metric(states), metric.get_name()).materialized() direction = "max" if getattr(metric, "maximize", False) else "min" base_key = f"{output_group}:{target_group}:{metric.get_name()}" Evaluator._record_value(result, statistics, base_key, true_loss, direction) diff --git a/konfai/metric/measure/__init__.py b/konfai/metric/measure/__init__.py index 07772f83..c6f46992 100644 --- a/konfai/metric/measure/__init__.py +++ b/konfai/metric/measure/__init__.py @@ -17,14 +17,16 @@ """Criterion and metric implementations used by KonfAI workflows: a bare name in a config resolves here.""" -from konfai.metric.measure.adversarial import FID as FID -from konfai.metric.measure.adversarial import WGP as WGP from konfai.metric.measure.adversarial import Gram as Gram from konfai.metric.measure.adversarial import PatchGanLoss as PatchGanLoss from konfai.metric.measure.adversarial import PerceptualLoss as PerceptualLoss from konfai.metric.measure.base import Criterion as Criterion +from konfai.metric.measure.base import CriterionOutput as CriterionOutput +from konfai.metric.measure.base import CriterionResult as CriterionResult +from konfai.metric.measure.base import CriterionValue as CriterionValue from konfai.metric.measure.base import CriterionWithAttribute as CriterionWithAttribute from konfai.metric.measure.base import CriterionWithInit as CriterionWithInit +from konfai.metric.measure.base import LabelledValues as LabelledValues from konfai.metric.measure.base import MaskedLoss as MaskedLoss from konfai.metric.measure.base import _require_optional as _require_optional from konfai.metric.measure.base import models_register as models_register @@ -51,11 +53,8 @@ from konfai.metric.measure.regression import FocalLoss as FocalLoss from konfai.metric.measure.regression import GradientImages as GradientImages from konfai.metric.measure.regression import KLDivergence as KLDivergence -from konfai.metric.measure.regression import L1LossRepresentation as L1LossRepresentation from konfai.metric.measure.regression import MAESaveMap as MAESaveMap from konfai.metric.measure.regression import Mean as Mean -from konfai.metric.measure.regression import MutualInformationLoss as MutualInformationLoss -from konfai.metric.measure.regression import TripletLoss as TripletLoss from konfai.metric.measure.regression import Variance as Variance from konfai.metric.measure.segmentation import Dice as Dice from konfai.metric.measure.segmentation import DiceSaveMap as DiceSaveMap @@ -63,7 +62,6 @@ __all__ = [ "BCE", - "FID", "LPIPS", "MAE", "ME", @@ -71,9 +69,11 @@ "PSNR", "SSIM", "TRE", - "WGP", "Accuracy", "Criterion", + "CriterionOutput", + "CriterionResult", + "CriterionValue", "CriterionWithAttribute", "CriterionWithInit", "CrossEntropyLoss", @@ -86,16 +86,14 @@ "IMPACTSynth", "ImpactFeatureModel", "KLDivergence", - "L1LossRepresentation", "LabelSums", + "LabelledValues", "MAESaveMap", "MaskedLoss", "Mean", - "MutualInformationLoss", "PatchGanLoss", "PerceptualLoss", "SAM_Perceptual", - "TripletLoss", "Variance", "models_register", ] diff --git a/konfai/metric/measure/adversarial.py b/konfai/metric/measure/adversarial.py index 3c5d01fa..950291cc 100644 --- a/konfai/metric/measure/adversarial.py +++ b/konfai/metric/measure/adversarial.py @@ -23,7 +23,7 @@ import numpy as np import torch -from konfai.metric.measure.base import Criterion, _require_optional, models_register +from konfai.metric.measure.base import Criterion, models_register from konfai.network.network import ModelLoader, Network from konfai.utils.config import apply_config from konfai.utils.utils import get_module @@ -40,14 +40,6 @@ def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Tensor: return self.loss(output, (torch.ones_like(output) * target).to(output.device)) -class WGP(Criterion): - def __init__(self) -> None: - super().__init__() - - def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Tensor: - return torch.mean((output - 1) ** 2) - - class Gram(Criterion): @staticmethod def compute_gram(tensor: torch.Tensor): @@ -182,67 +174,3 @@ def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Tensor: else: loss = self._compute(output, *targets) return loss.to(output) - - -class FID(Criterion): - class InceptionV3(torch.nn.Module): - def __init__(self) -> None: - super().__init__() - - torchvision_models = _require_optional("torchvision.models", criterion="FID", extra="fid") - inception_v3 = torchvision_models.inception_v3 - Inception_V3_Weights = torchvision_models.Inception_V3_Weights - - self.model = inception_v3(weights=Inception_V3_Weights.DEFAULT, transform_input=False) - self.model.fc = torch.nn.Identity() - self.model.eval() - - def forward(self, x: torch.Tensor) -> torch.Tensor: - return self.model(x) - - def __init__(self) -> None: - super().__init__() - _require_optional("scipy.linalg", criterion="FID", extra="fid") - # Built on the CPU and moved to the evaluated tensor's device in forward: a hardcoded .cuda() - # crashes CPU-only hosts and pins every DDP rank to the same GPU. - self.inception_model = FID.InceptionV3() - - @staticmethod - def preprocess_images(image: torch.Tensor) -> torch.Tensor: - # resize/normalise-with-mean-std live in torchvision.transforms.functional, not torch.nn.functional - # (which has no ``resize`` and whose ``normalize`` takes no mean/std). - tvf = _require_optional("torchvision.transforms.functional", criterion="FID", extra="fid") - resized = tvf.resize(image, [299, 299]).repeat((1, 3, 1, 1)) - return tvf.normalize(resized, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) - - @staticmethod - def get_features(images: torch.Tensor, model: torch.nn.Module) -> np.ndarray: - with torch.no_grad(): - features = model(images).cpu().numpy() - return features - - @staticmethod - def calculate_fid(real_features: np.ndarray, generated_features: np.ndarray) -> float: - mu1 = np.mean(real_features, axis=0) - sigma1 = np.cov(real_features, rowvar=False) - mu2 = np.mean(generated_features, axis=0) - sigma2 = np.cov(generated_features, rowvar=False) - - diff = mu1 - mu2 - linalg = _require_optional("scipy.linalg", criterion="FID", extra="fid") - - covmean, _ = linalg.sqrtm(sigma1.dot(sigma2), disp=False) - if np.iscomplexobj(covmean): - covmean = covmean.real - - return diff.dot(diff) + np.trace(sigma1) + np.trace(sigma2) - 2 * np.trace(covmean) - - def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Tensor: - self.inception_model.to(output.device) - real_images = FID.preprocess_images(targets[0].to(output.device).squeeze(0).permute([1, 0, 2, 3])) - generated_images = FID.preprocess_images(output.squeeze(0).permute([1, 0, 2, 3])) - - real_features = FID.get_features(real_images, self.inception_model) - generated_features = FID.get_features(generated_images, self.inception_model) - - return FID.calculate_fid(real_features, generated_features) diff --git a/konfai/metric/measure/base.py b/konfai/metric/measure/base.py index 2e7d4463..9a9ac7f6 100644 --- a/konfai/metric/measure/base.py +++ b/konfai/metric/measure/base.py @@ -27,6 +27,10 @@ import torch from konfai.network.network import Network +from konfai.network.network.measure import CriterionOutput as CriterionOutput +from konfai.network.network.measure import CriterionResult as CriterionResult +from konfai.network.network.measure import CriterionValue as CriterionValue +from konfai.network.network.measure import LabelledValues as LabelledValues from konfai.utils.config import record_given_arguments from konfai.utils.dataset import Attribute from konfai.utils.errors import MeasureError @@ -37,7 +41,7 @@ def _require_optional(module: str, *, criterion: str, extra: str) -> ModuleType: """Import an optional criterion dependency or raise an actionable error. - Several criteria (LPIPS, FID) rely on heavyweight optional packages + Several criteria (LPIPS, the IMPACT family) rely on heavyweight optional packages that are not part of the base install. Importing them through this helper turns a missing dependency into a clear, install-ready message raised at criterion construction, instead of a raw ``ImportError`` surfacing mid-run. @@ -94,7 +98,9 @@ def combine_metric(self, states: list[Any]) -> Any: raise NotImplementedError(f"{self.get_name()} is not reducible: it cannot combine states.") @abstractmethod - def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Tensor: + def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> CriterionOutput: + """A loss ``Tensor``, or ``(loss, value)`` / ``(loss, value, map)``: every accepted shape + is normalized by ``CriterionResult.of`` at the consumers.""" raise NotImplementedError() @@ -116,9 +122,9 @@ def __init__(self) -> None: super().__init__() @abstractmethod - def forward( # type: ignore[override] + def forward( # type: ignore[override] # the added keyword is this subclass's contract self, output: torch.Tensor, *targets: torch.Tensor, attributes: list[list[Attribute]] - ) -> torch.Tensor: + ) -> CriterionOutput: raise NotImplementedError() @@ -143,11 +149,36 @@ def get_mask(targets: list[torch.Tensor]) -> torch.Tensor | None: return mask + def _kernel(self, output: torch.Tensor, target: torch.Tensor) -> torch.Tensor | None: + """Per-voxel contribution whose masked per-item sums reproduce ``self.loss`` through + ``_value``; ``None`` routes the masked forward through the generic per-item loop (a loss + that is not a pointwise reduction).""" + return None + + def _value(self, total: torch.Tensor, count: torch.Tensor) -> torch.Tensor: + """The per-item values ``self.loss`` returns from per-item (total, count): the batched + twin of ``_finish``.""" + raise NotImplementedError() + + def _masked_forward(self, kernel: torch.Tensor, mask: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """The masked loss batched: per-item masked sums over the flattened axes, finished by + ``_value``, averaged over the items whose mask holds a voxel. No host readout: an empty + item is neutralized on the device (its value multiplied by zero, never NaN, so backward + stays finite) and the reported value is NaN only when every item is empty.""" + items = kernel.shape[0] + per_item = (kernel * mask).reshape(items, -1).sum(1) + counts = mask.reshape(items, -1).sum(1) * (kernel[0].numel() // mask[0].numel()) + scored = counts > 0 + values = self._value(torch.where(scored, per_item, per_item.new_ones(())), counts.clamp(min=1)) + scored_nb = scored.sum() + loss = (values * scored).sum() / scored_nb.clamp(min=1) + return loss, torch.where(scored_nb > 0, loss, loss.new_tensor(float("nan"))).detach() + def forward( self, output: torch.Tensor, *targets: torch.Tensor, - ) -> tuple[torch.Tensor, float]: + ) -> CriterionOutput: if len(targets) == 0: raise ValueError("MaskedLoss expects at least one target tensor.") @@ -155,34 +186,37 @@ def forward( target = targets[0] mask = self.get_mask(list(targets[1:])) - loss = output.new_tensor(0.0) - true_nb = 0 - if mask is None: loss_b = self.loss( output.float(), target.to(device=output.device).float(), ) - return loss_b, loss_b.detach().item() + return loss_b, loss_b.detach() target = target.to(device=output.device) - mask = mask.to(device=output.device) + mask = mask.to(device=output.device) == 1 - for batch in range(output.shape[0]): - mask_b = mask[batch, ...] == 1 + kernel = None if self.mode_image_masked else self._kernel(output.float(), target.float()) + if kernel is not None: + return self._masked_forward(kernel, mask) - if not torch.any(mask_b): + # One readout for the whole batch, where a per-item ``torch.any`` was one sync each. + scored = torch.any(mask.reshape(mask.shape[0], -1), dim=1).tolist() + loss = output.new_tensor(0.0) + for batch in range(output.shape[0]): + if not scored[batch]: continue + mask_b = mask[batch, ...] output_b = output[batch, ...].float() target_b = target[batch, ...].float() if self.mode_image_masked: - mask_b = mask_b.to(dtype=output_b.dtype) + mask_f = mask_b.to(dtype=output_b.dtype) loss_b = self.loss( - output_b * mask_b, - target_b * mask_b, + output_b * mask_f, + target_b * mask_f, ) else: @@ -192,13 +226,13 @@ def forward( ) loss = loss + loss_b - true_nb += 1 + true_nb = sum(scored) if true_nb == 0: return loss, np.nan loss = loss / true_nb - return loss, loss.detach().item() + return loss, loss.detach() # . Streamed-evaluation hooks ------------------------------------------------------------------- # A subclass whose ``loss`` reduces to a running sum provides its sufficient statistic and its @@ -207,7 +241,10 @@ def forward( def _stat(self, x: torch.Tensor, y: torch.Tensor) -> float: """Sum-contribution of one (output, target) pair to this loss's running total.""" - raise NotImplementedError() + kernel = self._kernel(x, y) + if kernel is None: + raise NotImplementedError() + return float(kernel.sum().item()) def _finish(self, total: float, count: int) -> float: """The value ``self.loss`` would return from a running (total, count).""" diff --git a/konfai/metric/measure/impact.py b/konfai/metric/measure/impact.py index 7e39b97b..3af863ac 100644 --- a/konfai/metric/measure/impact.py +++ b/konfai/metric/measure/impact.py @@ -25,16 +25,41 @@ import numpy as np import torch import torch.nn.functional as F -from huggingface_hub import hf_hub_download from konfai.data.patching import ModelPatch from konfai.metric.measure.adversarial import Gram -from konfai.metric.measure.base import CriterionWithAttribute +from konfai.metric.measure.base import CriterionWithAttribute, _require_optional from konfai.utils.config import apply_config from konfai.utils.dataset import Attribute +from konfai.utils.errors import MeasureError from konfai.utils.utils import get_module +def _hf_hub_download(criterion: str): + """The ``hf_hub_download`` callable, imported at the call site: huggingface_hub is only needed + by the IMPACT criteria, never by the rest of the metric package.""" + return _require_optional("huggingface_hub", criterion=criterion, extra="all").hf_hub_download + + +def _sniffed_mask(targets: tuple[torch.Tensor, ...], candidate: torch.Tensor) -> torch.Tensor | None: + """The uint8-mask convention, checked: a target sniffed as a mask must be a {0, 1} map and a + tensor of its own, never the scored target itself (an 8-bit intensity target would otherwise be + consumed as a mask in silence).""" + if candidate.dtype != torch.uint8: + return None + if candidate is targets[0]: + raise MeasureError( + "The only target is uint8, so it would be read as both the scored target and its mask.", + "Pass the image target first and the {0, 1} uint8 mask last, or cast the image off uint8.", + ) + if bool(torch.any(candidate > 1)): + raise MeasureError( + "A uint8 target is read as a foreground mask, but it holds values above 1.", + "IMPACT masks are {0, 1} uint8 maps; cast an 8-bit intensity target to another dtype.", + ) + return candidate + + def _check_feature_model(model_path: str, in_channels: int, shape: list[int], nb_layer: int) -> None: """Probe a TorchScript feature extractor on the CPU: one output feature map per layer weight, or raise. @@ -124,13 +149,17 @@ def _masked_feature_loss( return loss, true_nb -def _feature_loss_mean(slices: Iterable[tuple[torch.Tensor, int]]) -> tuple[torch.Tensor, float]: - """The slice losses summed and divided by the number of scored patches, with the scalar to report. - No scored patch (a mask with no foreground) would divide by zero: the loss is then its zero seed, - returned as-is, and the scalar is NaN.""" +def _feature_loss_mean(slices: Iterable[tuple[torch.Tensor, int]]) -> tuple[torch.Tensor, float | torch.Tensor]: + """The slice losses summed and divided by the number of scored patches, with the value to report + as a detached 0-d tensor read off its device lazily (``Measure._materialize``). No scored patch + (a mask with no foreground) would divide by zero: the loss is then its zero seed, returned + as-is, and the value is NaN.""" losses, counts = zip(*slices, strict=True) loss, true_nb = reduce(torch.add, losses), sum(counts) - return (loss / true_nb if true_nb else loss), np.nan if true_nb == 0 else loss.item() / true_nb + if true_nb == 0: + return loss, np.nan + loss = loss / true_nb + return loss, loss.detach() def _denormalized(tensor: torch.Tensor, attributes: list[Attribute]) -> torch.Tensor: @@ -183,7 +212,8 @@ def download( ) -> "ImpactFeatureModel": """The model ``filename`` of the HuggingFace ``repo_id``, probed once on the CPU. ``shape`` is the tile, its length the dimension; an entry ``<= 0`` scores the whole tensor instead.""" - model_path = hf_hub_download(repo_id=repo_id, filename=filename, repo_type="model", revision=None) # nosec B615 + download = _hf_hub_download("IMPACT") + model_path = download(repo_id=repo_id, filename=filename, repo_type="model", revision=None) # nosec B615 tile = shape if all(s > 0 for s in shape) else None _check_feature_model(model_path, in_channels, tile or [224] * len(shape), len(weights)) return cls(model_path, in_channels, weights, tile, len(shape), denormalize) @@ -280,10 +310,10 @@ def _pca_project( projected_target.append(self._pca_transform(target_feature[b : b + 1], basis)) return torch.cat(projected_output), torch.cat(projected_target) - def forward( # type: ignore[override] + def forward( # type: ignore[override] # the added keyword is CriterionWithAttribute's contract self, output: torch.Tensor, *targets: torch.Tensor, attributes: list[list[Attribute]] - ) -> tuple[torch.Tensor, float]: - mask = targets[-1] if targets[-1].dtype == torch.uint8 else None + ) -> tuple[torch.Tensor, float | torch.Tensor]: + mask = _sniffed_mask(targets, targets[-1]) # The prediction and the target share the same intensity space, so a single target attribute # (single-group target such as ``CT``) is reused to normalize both output and target; a second # attribute set is honored when the target is multi-group. @@ -323,12 +353,12 @@ def __init__( self.content_loss = torch.nn.MSELoss() self.style_loss = Gram() - def forward( # type: ignore[override] + def forward( # type: ignore[override] # the added keyword is CriterionWithAttribute's contract self, output: torch.Tensor, *targets: torch.Tensor, attributes: list[list[Attribute]] - ) -> tuple[torch.Tensor, float]: + ) -> tuple[torch.Tensor, float | torch.Tensor]: if len(targets) < 2: raise ValueError("At least two target tensors are required.") - mask = targets[2] if len(targets) == 3 and targets[2].dtype == torch.uint8 else None + mask = _sniffed_mask(targets, targets[2]) if len(targets) == 3 else None return _feature_loss_mean( chain( self.content.slice_losses(output, attributes[0], targets[0], attributes[1], mask, self.content_loss), @@ -358,13 +388,14 @@ def __init__( repo_id, filename = "VBoussot/impact-torchscript-models", f"SAM2.1/{model_name}" else: repo_id, filename = "VBoussot/ImpactSynth", model_name - model_path = hf_hub_download(repo_id=repo_id, filename=filename, repo_type="model", revision=None) # nosec B615 + download = _hf_hub_download("SAM_Perceptual") + model_path = download(repo_id=repo_id, filename=filename, repo_type="model", revision=None) # nosec B615 self.model = ImpactFeatureModel(model_path, 3, [1.0] * 4 if weights is None else weights, [512, 512], 2) - def forward( # type: ignore[override] + def forward( # type: ignore[override] # the added keyword is CriterionWithAttribute's contract self, output: torch.Tensor, *targets: torch.Tensor, attributes: list[list[Attribute]] - ) -> tuple[torch.Tensor, float]: - mask = targets[-1] if targets[-1].dtype == torch.uint8 else None + ) -> tuple[torch.Tensor, float | torch.Tensor]: + mask = _sniffed_mask(targets, targets[-1]) # ``targets[0]`` is the reference (e.g. CT), normalized with its own stats; the same stats # normalize the prediction since both live in the same intensity space. return _feature_loss_mean( diff --git a/konfai/metric/measure/regression.py b/konfai/metric/measure/regression.py index 2b7a904a..2ca8775c 100644 --- a/konfai/metric/measure/regression.py +++ b/konfai/metric/measure/regression.py @@ -24,15 +24,17 @@ import numpy as np import torch import torch.nn.functional as F -from tqdm import tqdm from konfai.data.patching import ModelPatch -from konfai.metric.measure.base import Criterion, CriterionWithInit, MaskedLoss, _require_optional +from konfai.metric.measure.base import Criterion, CriterionWithInit, LabelledValues, MaskedLoss, _require_optional from konfai.metric.measure.segmentation import Dice from konfai.network.blocks import LatentDistribution from konfai.network.network import Network from konfai.utils.errors import MeasureError +#: The 12-bit CT convention, -1024..3071 HU: the one default dynamic range PSNR and SSIM share. +CT_DYNAMIC_RANGE = 4095.0 + class MSE(MaskedLoss): @staticmethod @@ -44,8 +46,11 @@ def __init__(self, reduction: str = "mean") -> None: self._reduction = reduction self.reducible = reduction in ("mean", "sum") - def _stat(self, x: torch.Tensor, y: torch.Tensor) -> float: - return float((x - y).pow(2).sum().item()) + def _kernel(self, output: torch.Tensor, target: torch.Tensor) -> torch.Tensor | None: + return (output - target).pow(2) if self.reducible else None + + def _value(self, total: torch.Tensor, count: torch.Tensor) -> torch.Tensor: + return total / count if self._reduction == "mean" else total def _finish(self, total: float, count: int) -> float: return total / count if self._reduction == "mean" else total @@ -61,8 +66,11 @@ def __init__(self, reduction: str = "mean") -> None: self._reduction = reduction self.reducible = reduction in ("mean", "sum") - def _stat(self, x: torch.Tensor, y: torch.Tensor) -> float: - return float((x - y).abs().sum().item()) + def _kernel(self, output: torch.Tensor, target: torch.Tensor) -> torch.Tensor | None: + return (output - target).abs() if self.reducible else None + + def _value(self, total: torch.Tensor, count: torch.Tensor) -> torch.Tensor: + return total / count if self._reduction == "mean" else total def _finish(self, total: float, count: int) -> float: return total / count if self._reduction == "mean" else total @@ -78,8 +86,11 @@ def _loss(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: def __init__(self) -> None: super().__init__(ME._loss, False) - def _stat(self, x: torch.Tensor, y: torch.Tensor) -> float: - return float((x - y).sum().item()) + def _kernel(self, output: torch.Tensor, target: torch.Tensor) -> torch.Tensor | None: + return output - target + + def _value(self, total: torch.Tensor, count: torch.Tensor) -> torch.Tensor: + return total / count def _finish(self, total: float, count: int) -> float: return total / count @@ -122,26 +133,32 @@ def partial_map(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Ten evaluation write it region by region instead of needing the whole case.""" return self._difference(output, *targets)[0].to(output.dtype).cpu() - def forward(self, output: torch.Tensor, *targets: torch.Tensor): # type: ignore[override] + def forward( + self, output: torch.Tensor, *targets: torch.Tensor + ) -> tuple[torch.Tensor, float | torch.Tensor, torch.Tensor]: difference, mask = self._difference(output, *targets) if mask is None: loss = self._reduce(difference, self._reduction) - return loss, loss.detach().item(), difference.to(output.dtype).cpu() + return loss, loss.detach(), difference.to(output.dtype).cpu() + map_ = difference.to(output.dtype).cpu() + if self.reducible: + # The difference is already zero outside the mask, so the batched masked forward + # reads the exact bits MAE reads: the scalar and the map agree by construction. + loss, value = self._masked_forward(difference, mask) + return loss, value, map_ # Per batch item over its masked voxels, averaged over the items that have any: the # structure of MaskedLoss.forward, read off the one difference buffer. + scored = torch.any(mask.reshape(mask.shape[0], -1), dim=1).tolist() loss = output.new_tensor(0.0) - true_nb = 0 for batch in range(output.shape[0]): - mask_b = mask[batch, ...] - if not torch.any(mask_b): + if not scored[batch]: continue - loss = loss + self._reduce(torch.masked_select(difference[batch, ...], mask_b), self._reduction) - true_nb += 1 - map_ = difference.to(output.dtype).cpu() + loss = loss + self._reduce(torch.masked_select(difference[batch, ...], mask[batch, ...]), self._reduction) + true_nb = sum(scored) if true_nb == 0: return loss, np.nan, map_ loss = loss / true_nb - return loss, loss.detach().item(), map_ + return loss, loss.detach(), map_ def get_name(self) -> str: return "MAE" @@ -158,12 +175,15 @@ def _loss(dynamic_range: float, x: torch.Tensor, y: torch.Tensor) -> torch.Tenso return psnr def __init__(self, dynamic_range: float | None = None) -> None: - dynamic_range = dynamic_range if dynamic_range else 1024 + 3071 + dynamic_range = CT_DYNAMIC_RANGE if dynamic_range is None else dynamic_range super().__init__(partial(PSNR._loss, dynamic_range), False) self._dynamic_range = float(dynamic_range) - def _stat(self, x: torch.Tensor, y: torch.Tensor) -> float: - return float((x - y).pow(2).sum().item()) + def _kernel(self, output: torch.Tensor, target: torch.Tensor) -> torch.Tensor | None: + return (output - target).pow(2) + + def _value(self, total: torch.Tensor, count: torch.Tensor) -> torch.Tensor: + return 10 * torch.log10(self._dynamic_range**2 / (total / count)) def _finish(self, total: float, count: int) -> float: # The log is a function of the RUNNING mean, applied once at the end, never per patch. @@ -195,7 +215,7 @@ class SSIM(MaskedLoss): slab_bytes = 8 << 20 def __init__(self, dynamic_range: float | None = None) -> None: - dynamic_range = dynamic_range if dynamic_range else 1024 + 3000 + dynamic_range = CT_DYNAMIC_RANGE if dynamic_range is None else dynamic_range super().__init__(partial(SSIM._loss, dynamic_range), True) self._dynamic_range = float(dynamic_range) @@ -349,37 +369,33 @@ def preprocessing(tensor: torch.Tensor) -> torch.Tensor: return tensor.repeat((1, 3, 1, 1)) @staticmethod - def _loss(loss_fn_alex, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: + def _loss(loss_fn_alex, dataset_patch: ModelPatch, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: # Follow the input's device (the DDP rank's GPU, or CPU) instead of a hardcoded device 0. loss_fn_alex = loss_fn_alex.to(x.device) - dataset_patch = ModelPatch([1, 320, 320]) dataset_patch.load(x.shape[2:]) - patch_iterator = dataset_patch.disassemble(LPIPS.normalize(x), LPIPS.normalize(y)) - loss = 0 - with tqdm( - iterable=enumerate(patch_iterator), - leave=False, - total=dataset_patch.get_size(0), - ) as batch_iter: - for _, patch_input in batch_iter: - real, fake = LPIPS.preprocessing(patch_input[0]), LPIPS.preprocessing(patch_input[1]) - loss += loss_fn_alex(real, fake).flatten()[0] + loss = x.new_tensor(0.0) + for patch_input in dataset_patch.disassemble(LPIPS.normalize(x), LPIPS.normalize(y)): + real, fake = LPIPS.preprocessing(patch_input[0]), LPIPS.preprocessing(patch_input[1]) + # One distance per batch sample: the mean scores them all, where ``.flatten()[0]`` + # silently kept only the first. + loss = loss + loss_fn_alex(real, fake).mean() return loss / dataset_patch.get_size(0) def __init__(self, model: str = "alex") -> None: lpips = _require_optional("lpips", criterion="LPIPS", extra="lpips") - super().__init__(partial(LPIPS._loss, lpips.LPIPS(net=model)), True) + super().__init__(partial(LPIPS._loss, lpips.LPIPS(net=model), ModelPatch([1, 320, 320])), True) class TRE(Criterion): def __init__(self) -> None: super().__init__() - def forward(self, output: torch.Tensor, *targets: torch.Tensor): + def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> tuple[torch.Tensor, LabelledValues]: loss = torch.linalg.norm(output - targets[0], dim=2) - return loss.mean(), {f"Landmarks_{i}": v.item() for i, v in enumerate(loss.mean(0))} + per_landmark = loss.mean(0).detach() + return loss.mean(), LabelledValues(per_landmark, [f"Landmarks_{i}" for i in range(per_landmark.shape[0])]) class GradientImages(Criterion): @@ -476,37 +492,20 @@ def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Tensor: return (predicted == targets[0]).float().mean() -class TripletLoss(Criterion): - def __init__(self) -> None: - super().__init__() - self.triplet_loss = torch.nn.TripletMarginLoss(margin=1.0, p=2, eps=1e-7) - - def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Tensor: - return self.triplet_loss(output[0], output[1], output[2]) - - -class L1LossRepresentation(Criterion): - def __init__(self) -> None: - super().__init__() - self.loss = torch.nn.L1Loss() - - def _variance(self, features: torch.Tensor) -> torch.Tensor: - return torch.mean(torch.clamp(1 - torch.var(features, dim=0), min=0)) - - def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Tensor: - return self.loss(output[0], output[1]) + self._variance(output[0]) + self._variance(output[1]) - - class FocalLoss(Criterion): def __init__( self, gamma: float = 2.0, - alpha: list[float] = [0.5, 2.0, 0.5, 0.5, 1], + alpha: list[float] | None = None, reduction: str = "mean", ): super().__init__() - raw_alpha = torch.tensor(alpha, dtype=torch.float32) - self.alpha = raw_alpha / raw_alpha.sum() * len(raw_alpha) + alpha_tensor = None + if alpha is not None: + raw_alpha = torch.tensor(alpha, dtype=torch.float32) + alpha_tensor = raw_alpha / raw_alpha.sum() * len(raw_alpha) + # A buffer, so ``criterion.to(device)`` moves it once instead of a per-call upload. + self.register_buffer("alpha", alpha_tensor) self.gamma = gamma self.reduction = reduction @@ -519,9 +518,16 @@ def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Tensor: logpt = logpt.gather(1, target) pt = pt.gather(1, target) - # alpha[target] is already [B, 1, *spatial] (matching pt/logpt); do not add an axis. - at = self.alpha.to(target.device)[target] - loss = -at * ((1 - pt) ** self.gamma) * logpt + loss = -((1 - pt) ** self.gamma) * logpt + alpha = self._buffers["alpha"] + if alpha is not None: + if output.shape[1] > alpha.numel(): + raise MeasureError( + f"FocalLoss got {output.shape[1]} output classes for {alpha.numel()} alpha weights.", + "Give `alpha:` one weight per class, or omit it to weight every class equally.", + ) + # alpha[target] is already [B, 1, *spatial] (matching pt/logpt); do not add an axis. + loss = alpha.to(target.device)[target] * loss if self.reduction == "mean": return loss.mean() @@ -530,51 +536,6 @@ def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Tensor: return loss -class MutualInformationLoss(Criterion): - def __init__( - self, - num_bins: int = 23, - sigma_ratio: float = 0.5, - smooth_nr: float = 1e-7, - smooth_dr: float = 1e-7, - ) -> None: - super().__init__() - bin_centers = torch.linspace(0.0, 1.0, num_bins) - sigma = torch.mean(bin_centers[1:] - bin_centers[:-1]) * sigma_ratio - self.num_bins = num_bins - self.preterm = 1 / (2 * sigma**2) - self.bin_centers = bin_centers[None, None, ...] - self.smooth_nr = float(smooth_nr) - self.smooth_dr = float(smooth_dr) - - def parzen_windowing( - self, pred: torch.Tensor, target: torch.Tensor - ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: - pred_weight, pred_probability = self.parzen_windowing_gaussian(pred) - target_weight, target_probability = self.parzen_windowing_gaussian(target) - return pred_weight, pred_probability, target_weight, target_probability - - def parzen_windowing_gaussian(self, img: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: - img = torch.clamp(img, 0, 1) - img = img.reshape(img.shape[0], -1, 1) # (batch, num_sample, 1) - weight = torch.exp( - -self.preterm.to(img) * (img - self.bin_centers.to(img)) ** 2 - ) # (batch, num_sample, num_bin) - weight = weight / torch.sum(weight, dim=-1, keepdim=True) # (batch, num_sample, num_bin) - probability = torch.mean(weight, dim=-2, keepdim=True) # (batch, 1, num_bin) - return weight, probability - - def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Tensor: - wa, pa, wb, pb = self.parzen_windowing(output, targets[0]) # (batch, num_sample, num_bin), (batch, 1, num_bin) - pab = torch.bmm(wa.permute(0, 2, 1), wb.to(wa)).div(wa.shape[1]) # (batch, num_bins, num_bins) - papb = torch.bmm(pa.permute(0, 2, 1), pb.to(pa)) # (batch, num_bins, num_bins) - mi = torch.sum( - pab * torch.log((pab + self.smooth_nr) / (papb + self.smooth_dr) + self.smooth_dr), - dim=(1, 2), - ) # (batch) - return torch.mean(mi).neg() # average over the batch and channel ndims - - class CrossEntropyLoss(Criterion): def __init__(self, weight: list[float] | None = None, reduction: str = "mean") -> None: super().__init__() @@ -592,13 +553,13 @@ def __init__(self, name: str = "Variance") -> None: def get_name(self): return self.name - def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Tensor: + def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: output = output.float() if output.shape[1] > 1: variance = output.var(1).mean() else: variance = torch.zeros((), device=output.device, dtype=output.dtype) - return variance, variance.item() + return variance, variance.detach() class Mean(Criterion): @@ -609,6 +570,6 @@ def __init__(self, name: str = "Mean") -> None: def get_name(self): return self.name - def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Tensor: + def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: loss = output.float().mean() - return loss, loss.item() + return loss, loss.detach() diff --git a/konfai/metric/measure/segmentation.py b/konfai/metric/measure/segmentation.py index f4ae824d..3c344814 100644 --- a/konfai/metric/measure/segmentation.py +++ b/konfai/metric/measure/segmentation.py @@ -24,7 +24,7 @@ import torch import torch.nn.functional as F -from konfai.metric.measure.base import Criterion, MaskedLoss +from konfai.metric.measure.base import Criterion, CriterionOutput, LabelledValues, MaskedLoss from konfai.utils.errors import MeasureError #: Per-label sums of one (output, reference) pair, what every Dice is computed from and the state a @@ -193,7 +193,7 @@ def _score(sums: LabelSums, labels: list[int]) -> tuple[float, dict[int, float]] @staticmethod def _soft_loss( labels: list[int] | None, output: torch.Tensor, target: torch.Tensor - ) -> tuple[torch.Tensor, dict[int, float]]: + ) -> tuple[torch.Tensor, dict[int, float] | LabelledValues]: """The differentiable soft Dice over a probability map's channels, one per label the reference holds, every label in one expression: their channels gathered in one slice, the reference one comparison against the label vector on an axis of its own, and each sum one @@ -208,8 +208,10 @@ def _soft_loss( where the loop peaked at one channel (6 -> 180 MiB), and ``_soft_sums`` is the frugal route. The reference's own mass is the voxel count ``_reference_counts`` already holds, the exact - integer the metric route's ``_score`` divides by. The readouts come back in one - ``.tolist()``: a ``.item()`` per label drained the CUDA queue mid-loss, twice per label. + integer the metric route's ``_score`` divides by. The per-label dices leave as a + ``LabelledValues`` read off the device lazily (``Measure._materialize``), never a + ``.tolist()`` in the forward: a ``.item()`` per label once drained the CUDA queue mid-loss, + twice per label. """ labels, reference = Dice._reference_counts(target, labels) held = [label for label, count in zip(labels, reference, strict=True) if count] @@ -230,14 +232,14 @@ def _soft_loss( intersection = (probabilities * on_reference).sum(summed) reference_mass = torch.tensor(held_counts, dtype=torch.float32, device=output.device) dices = (2.0 * intersection + 1e-6) / (probabilities.sum(summed) + reference_mass + 1e-6) - values = iter(dices.tolist()) - result = {label: next(values) if count else np.nan for label, count in zip(labels, reference, strict=True)} - return 1 - dices.mean(), result + values = dices.new_full((len(labels),), float("nan")) + values[[index for index, count in enumerate(reference) if count]] = dices.detach() + return 1 - dices.mean(), LabelledValues(values, list(labels)) @staticmethod def _loss( labels: list[int] | None, output: torch.Tensor, *targets: torch.Tensor - ) -> tuple[torch.Tensor, dict[int, float]]: + ) -> tuple[torch.Tensor, dict[int, float] | LabelledValues]: target = Dice.on_grid(output, targets[0]) if output.shape[1] > 1: return Dice._soft_loss(labels, output, target) @@ -263,7 +265,7 @@ def _masked(output: torch.Tensor, targets: tuple[torch.Tensor, ...]) -> tuple[to mask = mask == 1 return output * mask, targets[0] * mask - def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> tuple[torch.Tensor, dict[int, float]]: + def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> CriterionOutput: return self.loss(*self._masked(output, targets)) def partial_metric(self, output: torch.Tensor, *targets: torch.Tensor) -> Any: @@ -316,7 +318,7 @@ def partial_map(self, output: torch.Tensor, *targets: torch.Tensor) -> torch.Ten evaluation write it region by region instead of needing the whole case.""" return self._map(*self._masked(output, targets)) - def forward(self, output: torch.Tensor, *targets: torch.Tensor): # type: ignore[override] + def forward(self, output: torch.Tensor, *targets: torch.Tensor) -> CriterionOutput: output, target = self._masked(output, targets) loss, true_loss = self.loss(output, target) return loss, true_loss, self._map(output, target) diff --git a/konfai/metric/schedulers.py b/konfai/metric/schedulers.py index a246e0f2..59011e06 100644 --- a/konfai/metric/schedulers.py +++ b/konfai/metric/schedulers.py @@ -72,12 +72,11 @@ def __init__( optimizer: torch.optim.Optimizer, warmup_steps: int = 10, last_epoch=-1, - verbose="deprecated", ): - super().__init__(optimizer, partial(Warmup.warmup, warmup_steps), last_epoch, verbose) + super().__init__(optimizer, partial(Warmup.warmup, warmup_steps), last_epoch) -class PolyLRScheduler(torch.optim.lr_scheduler._LRScheduler): +class PolyLRScheduler(torch.optim.lr_scheduler.LRScheduler): def __init__( self, optimizer, diff --git a/konfai/network/network/measure.py b/konfai/network/network/measure.py index 0b5d652a..26f63bcd 100644 --- a/konfai/network/network/measure.py +++ b/konfai/network/network/measure.py @@ -21,6 +21,7 @@ from collections import deque from collections.abc import Iterator from itertools import islice +from typing import Any, NamedTuple, TypeAlias import numpy as np import torch @@ -32,6 +33,73 @@ from konfai.utils.errors import ConfigError, MeasureError +class LabelledValues(NamedTuple): + """A per-label metric before its host readout: one value per label (NaN for a label the + reference lacks) and the labels naming them. ``Measure._materialize`` reads the tensor in the + same batched transfer as the scalar losses; the evaluator turns it into a per-label dict.""" + + values: torch.Tensor + labels: list[Any] + + +#: The value a criterion reports beside its loss: a float, a 0-d tensor read lazily off its device, +#: a dict of per-label floats, or a :class:`LabelledValues` pair read lazily. +CriterionValue: TypeAlias = float | torch.Tensor | dict[Any, float] | LabelledValues + +#: Every shape ``Criterion.forward`` may return; ``CriterionResult.of`` normalizes them all. +CriterionOutput: TypeAlias = ( + torch.Tensor | tuple[torch.Tensor, CriterionValue] | tuple[torch.Tensor, CriterionValue, torch.Tensor] +) + + +class CriterionResult(NamedTuple): + """A criterion's forward, normalized: the loss tensor, the reported value, the optional + per-voxel map. ``of`` is the one place the accepted shapes are checked.""" + + loss: torch.Tensor + value: CriterionValue + map: torch.Tensor | None = None + + @classmethod + def of(cls, raw: CriterionOutput, criterion: str = "criterion") -> "CriterionResult": + if isinstance(raw, torch.Tensor): + return cls(raw, raw.detach(), None) + if not isinstance(raw, tuple) or not 2 <= len(raw) <= 3 or not isinstance(raw[0], torch.Tensor): + raise MeasureError( + f"'{criterion}' returned {type(raw).__name__} instead of a criterion result.", + "A criterion returns a loss Tensor, or (loss, value) with value a float, a 0-d " + "Tensor, a dict of floats or a (values, labels) pair, plus an optional per-voxel " + "map Tensor third.", + ) + loss, value = raw[0], raw[1] + map_ = raw[2] if len(raw) == 3 else None + if isinstance(value, np.generic): + value = float(value) + elif isinstance(value, bool | int): + value = float(value) + elif isinstance(value, tuple) and not isinstance(value, LabelledValues): + if len(value) == 2 and isinstance(value[0], torch.Tensor) and isinstance(value[1], list): + value = LabelledValues(value[0], value[1]) + if not isinstance(value, float | torch.Tensor | dict | LabelledValues) or ( + map_ is not None and not isinstance(map_, torch.Tensor) + ): + raise MeasureError( + f"'{criterion}' reported a {type(value).__name__} value.", + "The reported value is a float, a 0-d Tensor, a dict of floats or a " + "(values, labels) pair, and a map is a Tensor.", + ) + return cls(loss, value, map_) + + def materialized(self) -> float | dict[Any, float]: + """The reported value as plain floats, read off their device: for per-case consumers (the + evaluator records into JSON immediately, where a sync is the cadence anyway).""" + if isinstance(self.value, LabelledValues): + return dict(zip(self.value.labels, self.value.values.tolist(), strict=True)) + if isinstance(self.value, torch.Tensor): + return float(self.value.item()) + return self.value + + class _RunningNanMean: """The nan-aware mean of everything added, in O(1) per value: what ``np.nanmean`` over the whole history returns, up to summation order (measured at most 3.2e-14 relative over 5e5 values).""" @@ -85,10 +153,11 @@ def __init__( self._mean = _RunningNanMean() self._mean_weight = _RunningNanMean() self._recorded = 0 - # Values recorded but not yet in ``_values``: a loss is kept as its 0-d tensor, because - # reading it inside the forward stalls the CPU on the whole graph before backward is - # enqueued. The consumers read them in one transfer per device (``Measure._materialize``). - self._unread: list[float | torch.Tensor] = [] + # Values recorded but not yet in ``_values``: a loss is kept as its 0-d tensor (a + # per-label metric as its LabelledValues), because reading it inside the forward stalls + # the CPU on the whole graph before backward is enqueued. The consumers read them in one + # transfer per device (``Measure._materialize``). + self._unread: list[float | torch.Tensor | LabelledValues] = [] def reset_loss(self) -> None: self._loss.clear() @@ -119,21 +188,19 @@ def values_mean(self, n: int) -> float: def weights_mean(self, n: int) -> float: return float(np.nanmean(_tail(self._weight, n))) if n > 0 else self._mean_weight.mean() - def add(self, weight: float, value: torch.Tensor | tuple[torch.Tensor, float | dict[str, float]]) -> None: - true_value: float | torch.Tensor - if isinstance(value, tuple): - loss_value, true_value = value - if isinstance(true_value, dict): - # Per-label/landmark metrics (Dice, TRE) report a dict; the logging windows - # nan-mean ``_values``, so store a scalar summary here while the tensor still - # carries the metric. Absent labels are NaN and are ignored by the mean. - numeric = [v for v in true_value.values() if isinstance(v, int | float)] - true_value = float(np.nanmean(numeric)) if numeric else float("nan") + def add(self, weight: float, value: CriterionOutput) -> None: + result = CriterionResult.of(value, self.name) + true_value: float | torch.Tensor | LabelledValues + if isinstance(result.value, dict): + # Per-label dicts of plain floats (the hard-label Dice route); the logging windows + # nan-mean ``_values``, so store the scalar summary. Absent labels are NaN and are + # ignored by the mean. + numeric = [v for v in result.value.values() if isinstance(v, int | float)] + true_value = float(np.nanmean(numeric)) if numeric else float("nan") else: - loss_value = value - true_value = value.detach() + true_value = result.value - self._loss.append((weight, loss_value if self.is_loss else loss_value.detach())) + self._loss.append((weight, result.loss if self.is_loss else result.loss.detach())) self._unread.append(true_value) self._weight.append(weight) self._mean_weight.add(weight) @@ -320,17 +387,25 @@ def _records(self) -> Iterator[tuple[str, "Measure.Loss"]]: def _materialize(self) -> None: """Append every unread value to its record's window, in the order recorded, reading the - tensors off their device in one transfer per device.""" + tensors off their device in one transfer per device. A ``LabelledValues`` lands as its + NaN-skipping mean over the labels: what the eager per-label dict summarized before.""" records = [record for _, record in self._records() if record._unread] tensors: dict[torch.device, list[torch.Tensor]] = {} for record in records: for value in record._unread: - if isinstance(value, torch.Tensor): - tensors.setdefault(value.device, []).append(value.reshape(())) - read = {device: iter(torch.stack(batch).tolist()) for device, batch in tensors.items()} + tensor = value.values if isinstance(value, LabelledValues) else value + if isinstance(tensor, torch.Tensor): + tensors.setdefault(tensor.device, []).append(tensor.reshape(-1)) + read = {device: iter(torch.cat(batch).tolist()) for device, batch in tensors.items()} for record in records: for value in record._unread: - record._record(next(read[value.device]) if isinstance(value, torch.Tensor) else value) + if isinstance(value, LabelledValues): + values = list(islice(read[value.values.device], value.values.numel())) + record._record(float(np.nanmean(values)) if values else float("nan")) + elif isinstance(value, torch.Tensor): + record._record(next(read[value.device])) + else: + record._record(value) record._unread.clear() def set_window(self, n: int) -> None: diff --git a/konfai/predictor/ensemble.py b/konfai/predictor/ensemble.py index 657cc621..183aeb90 100644 --- a/konfai/predictor/ensemble.py +++ b/konfai/predictor/ensemble.py @@ -45,12 +45,7 @@ def _colocate_loaded_modules(model: torch.nn.Module) -> None: if target is None: return for sub in model.modules(): - # ModuleArgsDict overrides parameters()/buffers() without a ``recurse`` kwarg, so use the - # base nn.Module methods to read each module's own (non-recursive) tensors. - own = [ - *torch.nn.Module.parameters(sub, recurse=False), - *torch.nn.Module.buffers(sub, recurse=False), - ] + own = [*sub.parameters(recurse=False), *sub.buffers(recurse=False)] if own and all(t.device.type == "cpu" for t in own): sub.to(target) diff --git a/tests/unit/test_measure.py b/tests/unit/test_measure.py index 0342f272..06f4a1d4 100644 --- a/tests/unit/test_measure.py +++ b/tests/unit/test_measure.py @@ -21,7 +21,17 @@ import pytest import torch import torch.nn.functional as F -from konfai.metric.measure import SSIM, Dice, FocalLoss, KLDivergence, PerceptualLoss, Variance, _require_optional +from konfai.metric.measure import ( + SSIM, + CriterionResult, + Dice, + FocalLoss, + KLDivergence, + LabelledValues, + PerceptualLoss, + Variance, + _require_optional, +) from konfai.network.network import CriterionsAttr from konfai.utils.errors import MeasureError @@ -33,6 +43,20 @@ def _one_hot(target: torch.Tensor, nb_channels: int) -> torch.Tensor: return output +def _per_label(value) -> dict: + """A criterion's per-label report as a dict: the soft Dice route reports a LabelledValues pair + read lazily; the hard route (and combine_metric) reports a plain dict.""" + if isinstance(value, LabelledValues): + return dict(zip(value.labels, value.values.tolist(), strict=True)) + return value + + +def _scores(criterion, *args): + """(loss, per-label dict) of one forward, whatever pair shape the route reports.""" + loss, value = criterion(*args)[:2] + return loss, _per_label(value) + + class TestFocalLoss: def test_does_not_cross_pair_samples_for_batch_greater_than_one(self): # The alpha weighting must stay per-voxel: the per-element loss shape must match the gathered @@ -124,7 +148,7 @@ def test_loss_averages_over_present_labels_only(self): target[0, 0, 1, :] = 2 output = _one_hot(target, 4) - loss, per_label = Dice(labels=[1, 2, 3])(output, target) + loss, per_label = _scores(Dice(labels=[1, 2, 3]), output, target) # Labels 1 and 2 are perfectly predicted (Dice = 1), label 3 is absent: # mean Dice = (1 + 1) / 2 = 1, hence loss = 1 - 1 = 0. @@ -138,7 +162,7 @@ def test_loss_is_zero_when_no_requested_label_is_present(self): target[0, 0, 0, :] = 1 output = _one_hot(target, 6) - loss, per_label = Dice(labels=[5])(output, target) + loss, per_label = _scores(Dice(labels=[5]), output, target) assert loss.item() == 0.0 assert np.isnan(per_label[5]) @@ -150,7 +174,7 @@ def test_default_labels_exclude_background(self): output = torch.zeros(1, 1, 4, 4, dtype=torch.uint8) output[0, 0, 0, :2] = 1 # 2 of them predicted - loss, per_label = Dice(labels=None)(output, target) + loss, per_label = _scores(Dice(labels=None), output, target) # Dice(label 1) = 2 * 2 / (2 + 4) = 2/3; the background Dice (24/26) # must not enter the average. @@ -164,7 +188,7 @@ def test_default_labels_support_multichannel_output(self): target[0, 0, 1, :] = 2 output = _one_hot(target, 3) - loss, per_label = Dice(labels=None)(output, target) + loss, per_label = _scores(Dice(labels=None), output, target) assert set(per_label) == {1, 2} assert loss.item() == pytest.approx(0.0, abs=1e-6) @@ -178,7 +202,7 @@ def test_mask_preserves_float_probabilities(self): output[0, 0] = 1 - output[0, 1] mask = torch.ones(1, 1, 2, 2) - loss, per_label = Dice(labels=[1])(output, target, mask) + loss, per_label = _scores(Dice(labels=[1]), output, target, mask) # Soft Dice(label 1) = 2 * (0.9 + 0.9) / ((0.9 + 0.9 + 0.1 + 0.1) + 2) = 0.9. assert per_label[1] == pytest.approx(0.9, abs=1e-5) @@ -193,7 +217,7 @@ def test_mask_restricts_the_computation(self): mask = torch.zeros(1, 1, 2, 2) mask[0, 0, :, 0] = 1 # first column only - _, per_label = Dice(labels=[1])(output, target, mask) + _, per_label = _scores(Dice(labels=[1]), output, target, mask) # Inside the mask: prediction {(0,0),(1,0)}, target {(0,0)} -> # Dice = 2 * 1 / (2 + 1) = 2/3. @@ -225,12 +249,13 @@ def _dice_per_label_oracle(labels, output, target, mask=None): def _assert_same_scores(got, expected, abs_tol): - assert set(got[1]) == set(expected[1]) + got_labels = _per_label(got[1]) + assert set(got_labels) == set(expected[1]) for label, value in expected[1].items(): if np.isnan(value): - assert np.isnan(got[1][label]) + assert np.isnan(got_labels[label]) else: - assert got[1][label] == pytest.approx(value, abs=abs_tol) + assert got_labels[label] == pytest.approx(value, abs=abs_tol) assert got[0].item() == pytest.approx(expected[0].item(), abs=abs_tol) @@ -330,9 +355,9 @@ def test_soft_loss_of_a_reference_holding_no_label_scores_nothing(self): output = torch.softmax(torch.randn(1, 3, 4, 4), dim=1) target = torch.zeros(1, 1, 4, 4, dtype=torch.int64) - loss, per_label = Dice()(output, target) + loss, per_label = _scores(Dice(), output, target) assert loss.item() == 0.0 and per_label == {} - assert np.isnan(Dice(labels=[1, 2])(output, target)[1][2]) + assert np.isnan(_per_label(Dice(labels=[1, 2])(output, target)[1])[2]) def test_streamed_sums_carry_the_predicted_mass_of_a_label_the_patch_reference_lacks(self): # labels=None: a patch predicting label 2 where its reference has none must still count @@ -347,8 +372,8 @@ def test_streamed_sums_carry_the_predicted_mass_of_a_label_the_patch_reference_l states.append(metric.partial_metric(output[..., 2:, :], target[..., 2:, :])) combined = metric.combine_metric(states) - assert whole[1][2] == pytest.approx(2 * 8 / (16 + 8), abs=1e-6) - assert combined[1][2] == pytest.approx(whole[1][2], abs=1e-9) + assert _per_label(whole[1])[2] == pytest.approx(2 * 8 / (16 + 8), abs=1e-6) + assert combined[1][2] == pytest.approx(_per_label(whole[1])[2], abs=1e-9) assert set(combined[1]) == {2} # a label no reference holds is not reported @pytest.mark.parametrize("soft", [False, True]) @@ -434,7 +459,7 @@ def test_mae_with_an_empty_mask_is_nan_and_its_map_zero(self): _, value, map_ = MAESaveMap()(output, target, mask) - assert np.isnan(value) + assert np.isnan(float(value)) assert torch.equal(map_, torch.zeros(1, 1, 4, 4)) def test_dice_map_of_uint8_labels_does_not_wrap(self): @@ -623,7 +648,7 @@ def test_single_channel_reports_zero(self): assert not torch.isnan(variance) assert variance.item() == pytest.approx(0.0) - assert value == pytest.approx(0.0) + assert float(value) == pytest.approx(0.0) def test_multi_channel_uses_unbiased_variance(self): """With several samples the unbiased (N-1) variance is averaged.""" @@ -633,7 +658,16 @@ def test_multi_channel_uses_unbiased_variance(self): # Unbiased var of [1, 3] = ((1-2)^2 + (3-2)^2) / (2 - 1) = 2.0. assert variance.item() == pytest.approx(2.0) - assert value == pytest.approx(2.0) + assert float(value) == pytest.approx(2.0) + + def test_value_is_a_detached_tensor_read_lazily(self): + """The reported value defers its host readout: a 0-d detached tensor, never an eager + ``.item()`` that drains the CUDA queue mid-forward.""" + output = torch.tensor([1.0, 3.0], requires_grad=True).reshape(1, 2, 1, 1) + + _, value = Variance()(output) + + assert isinstance(value, torch.Tensor) and not value.requires_grad def test_perceptual_loss_forward_unpacks_targets() -> None: @@ -741,17 +775,6 @@ def test_accuracy_reports_per_batch_not_a_lifetime_running_fraction() -> None: assert all_wrong.item() == pytest.approx(0.0) # not blended with the previous batch -def test_fid_preprocess_images_runs() -> None: - # FID.preprocess_images must use torchvision.transforms.functional: torch.nn.functional has no - # resize / normalize(mean, std), so calling them there means the metric cannot execute. - pytest.importorskip("torchvision") - from konfai.metric.measure import FID - - out = FID.preprocess_images(torch.zeros(2, 1, 64, 64)) - - assert out.shape == (2, 3, 299, 299) - - def test_lpips_preprocessing_follows_input_device() -> None: # LPIPS.preprocessing must keep the input's device (the model is moved to it lazily in _loss): # a hardcoded .to(0) crashes a CPU-only host and pins every DDP rank to GPU 0. @@ -878,17 +901,6 @@ def test_accepts_init_flag_lives_on_the_criterion_not_the_attr() -> None: assert getattr(CriterionsAttr(), "accepts_init", False) is False -def test_fid_builds_on_cpu_and_follows_the_input_device(): - # A hardcoded .cuda() at construction crashes CPU-only hosts; the model must be built on the - # CPU and moved to the evaluated tensor's device in forward. - pytest.importorskip("torchvision") - pytest.importorskip("scipy") - from konfai.metric.measure import FID - - metric = FID() - assert next(metric.inception_model.parameters()).device.type == "cpu" - - class TestSSIMFromHaloPatches: """SSIM is reducible from patches read with the window's radius of halo, each scoring the map voxels centred in its own grid slot: the streamed sum equals the whole-volume sum to float64 @@ -955,3 +967,216 @@ def test_an_empty_mask_item_is_skipped_as_the_whole_volume_skips_it(self): assert metric.combine_metric(states)[1] == pytest.approx(metric(x, y, mask)[1], rel=1e-12) assert np.isnan(metric.combine_metric([metric.partial_metric(x, y, torch.zeros_like(mask))])[1]) + + +class TestCriterionResult: + """The one normalizer of every shape a criterion may return (the boundary a bare numpy float + once slipped through, crashing the training-time consumer).""" + + def test_a_bare_tensor_is_the_loss_and_its_detached_value(self): + loss = torch.tensor(0.5, requires_grad=True) * 2 + result = CriterionResult.of(loss) + + assert result.loss is loss and result.map is None + assert isinstance(result.value, torch.Tensor) and not result.value.requires_grad + + def test_numpy_and_integer_values_are_coerced_to_floats(self): + assert CriterionResult.of((torch.tensor(0.0), np.float64(3.0))).value == 3.0 + assert CriterionResult.of((torch.tensor(0.0), 3)).value == 3.0 + assert isinstance(CriterionResult.of((torch.tensor(0.0), np.float64(3.0))).value, float) + + def test_a_values_labels_pair_normalizes_to_labelled_values(self): + result = CriterionResult.of((torch.tensor(0.0), (torch.tensor([1.0, float("nan")]), [1, 2]))) + + assert isinstance(result.value, LabelledValues) + assert result.materialized()[1] == 1.0 and np.isnan(result.materialized()[2]) + + def test_a_three_tuple_carries_the_map(self): + map_ = torch.zeros(1, 1, 2, 2) + result = CriterionResult.of((torch.tensor(0.0), 0.5, map_)) + + assert result.map is map_ and result.materialized() == 0.5 + + def test_a_non_tensor_return_is_refused_naming_the_criterion(self): + with pytest.raises(MeasureError, match="FID"): + CriterionResult.of(np.float64(3.0), "FID") + with pytest.raises(MeasureError, match="MyLoss"): + CriterionResult.of((torch.tensor(0.0), object()), "MyLoss") + + def test_an_external_bare_tensor_criterion_flows_through_the_measure_record(self): + # torch:nn:* / monai.losses:* classpath criteria return bare Tensors: the record must keep + # accepting them, deferring the readout to the batched materialize. + from konfai.network.network import Measure + + record = Measure.Loss("L1Loss", "out", "target", 0, True, False) + expected = torch.nn.L1Loss()(torch.full((2, 3), 2.0), torch.zeros(2, 3)) + record.add(1.0, expected) + assert record.recorded == 1 and record._unread # deferred, not yet read + + measure = object.__new__(Measure) + measure._loss = {0: {"out:target:L1Loss": record}} + measure._materialize() + + assert record.values_mean(1) == pytest.approx(float(expected)) + + def test_a_labelled_metric_materializes_to_the_nan_mean_of_its_labels(self): + from konfai.network.network import Measure + + record = Measure.Loss("Dice", "out", "target", 0, False, False) + record.add(1.0, (torch.tensor(0.25), LabelledValues(torch.tensor([0.5, float("nan"), 1.0]), [1, 2, 3]))) + + measure = object.__new__(Measure) + measure._loss = {0: {"out:target:Dice": record}} + measure._materialize() + + assert record.values_mean(1) == pytest.approx(0.75) # the NaN label is skipped + + +class TestMaskedLossDeferredValues: + """The MaskedLoss family reports detached 0-d tensors, and its masked elementwise path runs + batched: no per-item host sync before backward is enqueued.""" + + def test_unmasked_value_is_the_detached_loss(self): + from konfai.metric.measure import MAE + + loss, value = MAE()(torch.rand(2, 1, 4, 4), torch.rand(2, 1, 4, 4)) + + assert isinstance(value, torch.Tensor) and not value.requires_grad + assert float(value) == pytest.approx(loss.item()) + + @pytest.mark.parametrize("metric_name", ["MAE", "MSE", "ME", "PSNR"]) + @pytest.mark.parametrize("reduction", ["mean", "sum"]) + def test_batched_masked_path_matches_the_per_item_loop(self, metric_name, reduction): + import konfai.metric.measure as measure + + torch.manual_seed(3) + cls = getattr(measure, metric_name) + metric = cls() if metric_name in ("ME", "PSNR") else cls(reduction) + output = torch.rand(3, 2, 5, 6) * 100 + target = output + torch.randn(3, 2, 5, 6) * 10 + mask = (torch.rand(3, 1, 5, 6) > 0.4).to(torch.uint8) + mask[1] = 0 # one empty item: skipped, exactly as the loop skipped it + + loss, value = metric(output, target, mask) + + expected = [] + for b in range(3): + keep = mask[b : b + 1] == 1 + if not bool(keep.any()): + continue + expected.append( + metric.loss( + torch.masked_select(output[b : b + 1].float(), keep), + torch.masked_select(target[b : b + 1].float(), keep), + ) + ) + assert float(value) == pytest.approx(loss.item(), rel=1e-6) + assert loss.item() == pytest.approx(torch.stack(expected).mean().item(), rel=1e-5) + + def test_an_all_empty_mask_reports_nan_and_a_finite_zero_loss(self): + from konfai.metric.measure import MAE + + output = torch.rand(2, 1, 4, 4, requires_grad=True) + loss, value = MAE()(output, torch.rand(2, 1, 4, 4), torch.zeros(2, 1, 4, 4, dtype=torch.uint8)) + + assert loss.item() == 0.0 + assert np.isnan(float(value)) + assert torch.isfinite(torch.autograd.grad(loss, output)[0]).all() # empty items never NaN the graph + + def test_an_empty_item_keeps_the_gradient_finite(self): + from konfai.metric.measure import MSE + + output = torch.rand(2, 1, 3, 3, requires_grad=True) + mask = torch.zeros(2, 1, 3, 3, dtype=torch.uint8) + mask[0] = 1 + loss, _ = MSE()(output, torch.rand(2, 1, 3, 3), mask) + + assert torch.isfinite(torch.autograd.grad(loss, output)[0]).all() + + +class TestLPIPSBatch: + class _StubLpips(torch.nn.Module): + """One distance per batch sample, [B, 1, 1, 1]: the shape the lpips package returns.""" + + def forward(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: + distance = (a - b).abs().reshape(a.shape[0], -1).mean(1) + return distance.reshape(-1, 1, 1, 1) + + def test_every_batch_item_is_scored(self): + # Sample 0 identical, sample 1 not: the old `.flatten()[0]` reported 0 and silently + # discarded every sample past the first; the mean scores them all. + from konfai.data.patching import ModelPatch + from konfai.metric.measure import LPIPS + + torch.manual_seed(0) + x = torch.rand(2, 1, 1, 16, 16) + x[0, 0, 0, 0, 0], x[0, 0, 0, 0, 1] = 0.0, 1.0 # both extremes in sample 0: one shared range + y = x.clone() + y[1] = (x[1] + 0.3).clamp(0.0, 1.0) + + value = LPIPS._loss(self._StubLpips(), ModelPatch([1, 16, 16]), x, y) + + nx, ny = LPIPS.preprocessing(LPIPS.normalize(x)[:, :, 0]), LPIPS.preprocessing(LPIPS.normalize(y)[:, :, 0]) + expected = self._StubLpips()(nx, ny).mean() + assert float(value) == pytest.approx(float(expected)) + assert float(value) > 0.0 # not just the first (identical) sample + + +class TestFocalLossAlpha: + def test_default_alpha_is_uniform_for_any_class_count(self): + # The old default was a 5-class weight vector from a past experiment: silently misweighted + # below 5 classes, indexed out of range above. + torch.manual_seed(1) + output = torch.randn(1, 7, 4, 4) + target = torch.randint(0, 7, (1, 1, 4, 4)).float() + + loss = FocalLoss()(output, target) + + log_pt = F.log_softmax(output, dim=1).gather(1, target.long()) + pt = torch.exp(log_pt) + expected = (-((1 - pt) ** 2.0) * log_pt).mean() + assert loss.item() == pytest.approx(expected.item()) + + def test_more_classes_than_alpha_weights_are_refused(self): + with pytest.raises(MeasureError, match="alpha"): + FocalLoss(alpha=[0.5, 2.0])(torch.randn(1, 3, 4, 4), torch.randint(0, 2, (1, 1, 4, 4)).float()) + + def test_a_given_alpha_lives_in_a_buffer(self): + focal = FocalLoss(alpha=[1.0, 1.0, 2.0]) + assert "alpha" in dict(focal.named_buffers()) + + +class TestImpactMaskSniffing: + def test_a_non_binary_uint8_target_is_refused(self): + from konfai.metric.measure.impact import _sniffed_mask + + image = torch.rand(1, 1, 4, 4) + eight_bit = (torch.rand(1, 1, 4, 4) * 255).to(torch.uint8) + + with pytest.raises(MeasureError, match="values above 1"): + _sniffed_mask((image, eight_bit), eight_bit) + + def test_a_binary_uint8_target_is_the_mask(self): + from konfai.metric.measure.impact import _sniffed_mask + + image = torch.rand(1, 1, 4, 4) + mask = (torch.rand(1, 1, 4, 4) > 0.5).to(torch.uint8) + + assert _sniffed_mask((image, mask), mask) is mask + assert _sniffed_mask((image, image), image) is None # not uint8: no mask + + def test_the_scored_target_itself_cannot_be_the_mask(self): + from konfai.metric.measure.impact import _sniffed_mask + + only = (torch.rand(1, 1, 4, 4) > 0.5).to(torch.uint8) + + with pytest.raises(MeasureError, match="both the scored target and its mask"): + _sniffed_mask((only,), only) + + +def test_psnr_and_ssim_share_the_ct_dynamic_range_default() -> None: + # 4095 (12-bit CT, -1024..3071) for both: two different defaults quietly scored the standard + # synthesis pair against two different ranges. + from konfai.metric.measure import PSNR + + assert PSNR()._dynamic_range == SSIM()._dynamic_range == 4095.0 From fab9d15b5818c2dd5adb83f8b5a5d04b89240a68 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:20:56 +0200 Subject: [PATCH 13/28] fix(dataset): first-class DICOM streaming, declarative backends, honest refusals DICOM declares its plane granularity and caches decoded planes under the budget's cache share, so overlapping regions decode each slice once instead of re-parsing every touched file per patch. Per-backend facts (single_store, concurrent_write_safe, case suffix, streaming) are class declarations consulted through one registry instead of ~9 format-name branches in core.py, and SitkFile stops stubbing NotImplementedError. Stepped slice reads return one physically-correct geometry record on every backend (shared region_geometry helper). H5 raises the designed DatasetManagerError for a missing group and sizes rdcc from the cache share; remote OME-Zarr reads memoize entry resolution (0 fs.info calls on repeated reads); a >=2-D sidecar value fails as a named refusal at the read door; missing SimpleITK refuses with the install hint at every touch point instead of AttributeError. --- konfai/utils/dataset/__init__.py | 7 ++ konfai/utils/dataset/abstract.py | 69 +++++++++++- konfai/utils/dataset/attribute.py | 44 +++++++- konfai/utils/dataset/backend.py | 32 ++++-- konfai/utils/dataset/core.py | 64 +++++++---- konfai/utils/dataset/dicom_file.py | 11 ++ konfai/utils/dataset/h5.py | 44 +++++++- konfai/utils/dataset/itk_transform_file.py | 18 +++ konfai/utils/dataset/ome_zarr_file.py | 63 +++++++++-- konfai/utils/dataset/raw_block.py | 28 +++-- konfai/utils/dataset/sitk_file.py | 76 ++++++++++--- konfai/utils/dicom.py | 124 +++++++++++++++++++-- konfai/utils/ome_zarr.py | 19 ++-- tests/unit/test_dataset.py | 110 +++++++++++++++++- tests/unit/test_imaging_formats.py | 50 +++++++++ tests/unit/test_perf_hot_paths.py | 2 + tests/unit/test_remote_dataset.py | 31 ++++++ 17 files changed, 686 insertions(+), 106 deletions(-) diff --git a/konfai/utils/dataset/__init__.py b/konfai/utils/dataset/__init__.py index 509785f9..d344ddcd 100644 --- a/konfai/utils/dataset/__init__.py +++ b/konfai/utils/dataset/__init__.py @@ -36,8 +36,11 @@ from konfai.utils.dataset.attribute import image_to_data as image_to_data from konfai.utils.dataset.attribute import is_an_image as is_an_image from konfai.utils.dataset.attribute import ome_zarr_attributes as ome_zarr_attributes +from konfai.utils.dataset.attribute import region_geometry as region_geometry from konfai.utils.dataset.attribute import sitk as sitk +from konfai.utils.dataset.backend import BACKENDS as BACKENDS from konfai.utils.dataset.backend import File as File +from konfai.utils.dataset.backend import backend_for as backend_for from konfai.utils.dataset.core import Dataset as Dataset from konfai.utils.dataset.core import _is_listed_name as _is_listed_name from konfai.utils.dataset.dicom_file import DicomFile as DicomFile @@ -59,6 +62,7 @@ from konfai.utils.dataset.landmarks import write_landmarks as write_landmarks from konfai.utils.dataset.ome_zarr_file import OmeZarrFile as OmeZarrFile from konfai.utils.dataset.ome_zarr_file import _divisor_tile as _divisor_tile +from konfai.utils.dataset.ome_zarr_file import _forget_resolved_paths as _forget_resolved_paths from konfai.utils.dataset.ome_zarr_file import _OmeZarrDataStream as _OmeZarrDataStream from konfai.utils.dataset.ome_zarr_file import _store_chunks as _store_chunks from konfai.utils.dataset.raw_block import _MHA_DTYPES as _MHA_DTYPES @@ -115,11 +119,13 @@ from konfai.utils.dataset.stream import _RawBlockStream as _RawBlockStream __all__ = [ + "BACKENDS", "DISPLACEMENT_FIELD_ATTRIBUTE", "Attribute", "DataStream", "Dataset", "as_channel_first", + "backend_for", "chunk_hull_voxels", "data_to_image", "data_to_transform", @@ -130,6 +136,7 @@ "is_staging_entry", "ome_zarr_attributes", "read_landmarks", + "region_geometry", "release_read_handles", "write_landmarks", ] diff --git a/konfai/utils/dataset/abstract.py b/konfai/utils/dataset/abstract.py index 9bcdc917..83a31723 100644 --- a/konfai/utils/dataset/abstract.py +++ b/konfai/utils/dataset/abstract.py @@ -36,10 +36,67 @@ class AbstractFile(ABC): + """One storage backend: how a ``(group, name)`` entry is read, written and enumerated. + + The per-backend FACTS live here as class-level declarations, so a new backend is one module + plus one :data:`~konfai.utils.dataset.backend.BACKENDS` entry: the dataset consults the class, + never a format-name branch of its own. + """ + + #: One store holds every case (a single ``.h5`` file); a directory backend keeps one file (or + #: store directory) per case, and the dataset walks the root itself. + single_store: bool = False + + #: Whether writes to different entries land in disjoint files, so a background writer may + #: flush one entry while another thread writes elsewhere in the dataset. A backend whose + #: entries share handles or metadata (one HDF5 file, a zarr hierarchy, a DICOM series) + #: declares False and stays serial. + concurrent_write_safe: bool = True + + #: The suffix a case file carries implicitly on disk (``.h5``), or ``None`` when the case path + #: is spelled as listed. + case_file_suffix: str | None = None + + #: Whether this backend reads a remote (URI) root; the rest open a local path. + reads_remote: bool = False + + #: Whether a written store can hold multiscale levels (``scale_factors``): only a format with + #: levels may be asked for a pyramid. + writes_pyramid: bool = False + + #: Whether a case is a directory of entries the backend itself enumerates (``get_group`` on + #: the case), rather than plain files the dataset walks. + lists_case_entries: bool = False + @abstractmethod - def __init__(self) -> None: + def __init__(self, filename: str, read: bool) -> None: pass + @classmethod + def open( + cls, + filename: str, + read: bool, + file_format: str, + level: int = 0, + scale_factors: list[int] | None = None, + downsample_method: str | None = None, + ) -> AbstractFile: + """This backend on ``filename``, built from the dispatch's full hand: each backend takes + the arguments its constructor actually needs and ignores the rest.""" + del file_format, level, scale_factors, downsample_method + return cls(filename, read) + + @classmethod + def can_stream(cls, file_format: str, attributes: Attribute) -> bool: + """Whether this backend can serve incremental region writes for ``file_format``. + + The base answers ``False``: a backend that cannot stream is written whole through + ``data_to_file``. + """ + del file_format, attributes + return False + @abstractmethod def __enter__(self): pass @@ -127,13 +184,15 @@ def open_data_stream( """Open ``name`` for incremental region writes; ``None`` when this backend cannot.""" return None - @abstractmethod def get_names(self, group: str) -> list[str]: - pass + """The cases of ``group`` this store holds. Only a backend that enumerates its own entries + answers (a single store, a store-per-case directory); a plain-file backend's cases are the + root's listing, which the dataset walks itself.""" + raise NotImplementedError(f"{type(self).__name__} keeps one file per entry; the dataset lists its root.") - @abstractmethod def get_group(self) -> list[str]: - pass + """The groups this store holds, under the same contract as :meth:`get_names`.""" + raise NotImplementedError(f"{type(self).__name__} keeps one file per entry; the dataset walks its root.") @abstractmethod def is_exist(self, group: str, name: str | None = None) -> bool: diff --git a/konfai/utils/dataset/attribute.py b/konfai/utils/dataset/attribute.py index a0b27a8b..3db13349 100644 --- a/konfai/utils/dataset/attribute.py +++ b/konfai/utils/dataset/attribute.py @@ -64,6 +64,27 @@ def _attribute_text(value: Any) -> str: return str(value).replace("\n", "") +def region_geometry( + origin: np.ndarray, + spacing: np.ndarray, + direction: np.ndarray, + spatial_slices: tuple[slice, ...], +) -> tuple[np.ndarray, np.ndarray]: + """The geometry record of the samples a normalized spatial slice keeps: the first sample's + world position as the origin, the spacing scaled by the step. + + THE region-geometry update, shared by every backend so a stepped read carries the same record + whatever format the volume is stored in. ``spatial_slices`` arrive array-ordered (``(Z)YX``, + ``slice.indices``-normalized); geometry is ``(x, y, z)``. + """ + origin = np.asarray(origin, dtype=np.float64) + spacing = np.asarray(spacing, dtype=np.float64) + matrix = np.asarray(direction, dtype=np.float64).reshape(len(spacing), len(spacing)) + start_xyz = np.asarray([item.start for item in reversed(spatial_slices)], dtype=np.float64) + step_xyz = np.asarray([item.step for item in reversed(spatial_slices)], dtype=np.float64) + return origin + matrix @ (start_xyz * spacing), spacing * step_xyz + + class Attribute(dict[str, Any]): """Metadata container storing repeated values with a stack-like naming scheme. @@ -137,14 +158,33 @@ def _parse_array(text: str) -> np.ndarray: """ return np.fromstring(text[1:-1].replace(",", " "), sep=" ", dtype=np.double) + @staticmethod + def _parsed_array(key: str, text: str) -> np.ndarray: + """:meth:`_parse_array`, refusing by name what does not parse back as a flat array. + + The sidecar stores any value as its print, so a >= 2-D array is accepted at write and its + nested print fails only here, far from the writer: ``np.fromstring`` used to surface it as + an anonymous ``ValueError`` deep in numpy. (``Crop`` deliberately records its 2-D ``box`` + and reads it back through its own parser, never this door, which is why the write door + cannot refuse the rank outright.) + """ + try: + return Attribute._parse_array(text) + except ValueError: + raise DatasetManagerError( + f"'{key}' does not parse back as a flat array: it holds '{text}'.", + "Only flat scalars and 1-D arrays round-trip through the sidecar:" + " flatten the value where it is recorded.", + ) from None + def get_np_array(self, key: str) -> np.ndarray: - return Attribute._parse_array(self[key]) + return Attribute._parsed_array(key, self[key]) def get_tensor(self, key: str) -> torch.Tensor: return torch.tensor(self.get_np_array(key)).to(torch.float32) def pop_np_array(self, key: str) -> np.ndarray: - return Attribute._parse_array(self.pop(key)) + return Attribute._parsed_array(key, self.pop(key)) def pop_tensor(self, key: str) -> torch.Tensor: return torch.tensor(self.pop_np_array(key)) diff --git a/konfai/utils/dataset/backend.py b/konfai/utils/dataset/backend.py index 45920918..97dd94e1 100644 --- a/konfai/utils/dataset/backend.py +++ b/konfai/utils/dataset/backend.py @@ -32,6 +32,22 @@ if TYPE_CHECKING: from konfai.utils.dataset.abstract import AbstractFile +#: The backend each format token dispatches to; every token not named here is a plain-file +#: extension SitkFile serves. THE token-to-class table: a new backend registers here and declares +#: its facts on the class (see ``AbstractFile``), and nothing else needs a format-name branch. +BACKENDS: dict[str, type[AbstractFile]] = { + "h5": H5File, + "omezarr": OmeZarrFile, + "dicom": DicomFile, + "itktransform": ItkTransformFile, +} + + +def backend_for(file_format: str) -> type[AbstractFile]: + """The backend class serving ``file_format``: where ``File.__enter__`` and ``Dataset`` read + the per-backend facts from.""" + return BACKENDS.get(file_format, SitkFile) + class File: def __init__( @@ -52,22 +68,16 @@ def __init__( self.downsample_method = downsample_method def __enter__(self) -> AbstractFile: - if self.file_format == "omezarr": - self.file = OmeZarrFile(self.filename, self.read, self.level, self.scale_factors, self.downsample_method) - elif uri.is_uri(self.filename): + backend = backend_for(self.file_format) + if uri.is_uri(self.filename) and not backend.reads_remote: # OME-Zarr addresses a store; every other backend opens a path. raise DatasetManagerError( f"'{self.filename}' is a remote root, which only ':omezarr' can read.", "Declare the root as ':omezarr', or copy the dataset locally first.", ) - elif self.file_format == "h5": - self.file = H5File(self.filename, self.read) - elif self.file_format == "dicom": - self.file = DicomFile(self.filename, self.read) - elif self.file_format == "itktransform": - self.file = ItkTransformFile(self.filename + "/", self.read) - else: - self.file = SitkFile(self.filename + "/", self.read, self.file_format) + self.file = backend.open( + self.filename, self.read, self.file_format, self.level, self.scale_factors, self.downsample_method + ) self.file.__enter__() return self.file diff --git a/konfai/utils/dataset/core.py b/konfai/utils/dataset/core.py index d9f6383d..f84548be 100644 --- a/konfai/utils/dataset/core.py +++ b/konfai/utils/dataset/core.py @@ -42,9 +42,9 @@ as_channel_first, data_to_image, data_to_transform, - is_an_image, ) from konfai.utils.dataset.backend import File as _File +from konfai.utils.dataset.backend import backend_for from konfai.utils.dataset.dicom_file import DicomFile from konfai.utils.dataset.h5 import H5File from konfai.utils.dataset.itk_transform_file import ItkTransformFile @@ -127,7 +127,7 @@ def __init__( # rather than ignored: only OME-NGFF has multiple levels, so a pyramid asked of an mha or an # h5 is a request the format cannot serve, and silently writing one level would leave the # consumer's ``@1`` resolving to a level that does not exist. - if scale_factors and self.file_format != "omezarr": + if scale_factors and not backend_for(self.file_format).writes_pyramid: raise DatasetManagerError( f"A pyramid was asked of a '{self.file_format}' destination, which has no levels.", "Only ':omezarr' stores levels. Drop scale_factors, or write to ':omezarr'.", @@ -136,6 +136,11 @@ def __init__( self.downsample_method = downsample_method self._names_cache: dict[str, list[str]] = {} self._infos_cache: dict[tuple[str, str], tuple[list[int], Attribute]] = {} + #: Root-existence and case-path probes are one round-trip each on a remote root, per entry + #: read without these: a root seen once is not re-probed (a vanished one fails loudly at + #: the read), and a case resolved once keeps its path until a write drops the caches. + self._root_seen = False + self._case_paths: dict[tuple[str, str], str] = {} #: Facts a stage derived from an entry's pixels (a Crop's foreground box), keyed by #: ``(group, name)``: computed once per volume, whatever the number of chains reading it. self.case_facts: dict[tuple[str, str], dict[str, Any]] = {} @@ -144,15 +149,20 @@ def _file(self, filename: str, read: bool) -> _File: """One entry's backing file, opened as this dataset's root is.""" return self.File(filename, read, self.file_format, self.level) + @property + def _backend(self) -> type[_AbstractFile]: + """The class serving this dataset's format: where the per-backend facts are declared.""" + return backend_for(self.file_format) + @staticmethod def _normalize_path(filename: str | Path, file_format: str) -> tuple[str, bool]: - # A single-store h5 is one file, every other backend a directory of cases: only the latter gets the + # A single store is one file, every other backend a directory of cases: only the latter gets the # trailing slash that marks ``is_directory``. Keep the two in lock-step so a path never ends up a # directory-flagged h5 (which would write the hidden dotfile ``/.h5``). ``as_posix`` keeps the # separator forward on every OS, so the stored filename (and the trailing-slash marker) is the same # on Windows, where ``prefix / name`` would otherwise render backslashes. path = uri.normalize(filename) - if file_format != "h5" and not path.endswith("/"): + if not backend_for(file_format).single_store and not path.endswith("/"): path += "/" return path, path.endswith("/") @@ -207,8 +217,9 @@ def store_root(self) -> str: view, for the callers that manipulate the path. """ root = self.filename - if self.file_format == "h5" and not root.endswith(".h5"): - return f"{root}.h5" + suffix = self._backend.case_file_suffix + if self._backend.single_store and suffix and not root.endswith(suffix): + return f"{root}{suffix}" return root @property @@ -225,12 +236,10 @@ def concurrent_write_safe(self) -> bool: """Whether writes to different entries land in disjoint files, so a background writer may flush one entry while another thread writes elsewhere in the dataset. - Mirrors the backend dispatch in ``File.__enter__``: everything that is not a single-store - backend is a :class:`SitkFile` directory, one image file per ``(group, name)``. A single - store (one HDF5 file, one zarr hierarchy, a DICOM series) shares handles and metadata across - entries and must stay serial. + The backend's own declaration: a store that shares handles or metadata across entries (one + HDF5 file, a zarr hierarchy, a DICOM series) says so and stays serial. """ - return self.file_format not in ("h5", "omezarr", "dicom") + return self._backend.concurrent_write_safe def _write_target(self, group: str, name: str) -> tuple[_File, str]: """The file a ``(group, name)`` write lands in and the entry name inside it, caches dropped. @@ -242,6 +251,7 @@ def _write_target(self, group: str, name: str) -> tuple[_File, str]: uri.refuse_write(self.filename) self._names_cache.clear() self._infos_cache.clear() + self._case_paths.clear() self.case_facts.clear() if self.is_directory: os.makedirs(self.filename, exist_ok=True) @@ -282,14 +292,11 @@ def write( def can_stream_data(self, attributes: Attribute) -> bool: """Whether ``open_data_stream`` can serve this dataset's write format. - H5 and OME-Zarr always can; MetaImage ``mha`` needs image geometry to write its header up - front; every other format only writes whole volumes (use ``write``). + The backend's own declaration: H5 and OME-Zarr always can; MetaImage/NIfTI need image + geometry to write their headers up front; every other format only writes whole volumes + (use ``write``). """ - if self.file_format in ("h5", "omezarr"): - return True - if self.file_format == "itktransform": - return is_an_image(attributes) - return self.file_format in ("mha", "nii") and is_an_image(attributes) + return self._backend.can_stream(self.file_format, attributes) def open_data_stream( self, @@ -330,11 +337,18 @@ def _case_path(self, sub_directory: str, name: str) -> str | None: """The file a directory dataset stores case ``name`` under, or ``None`` if absent on disk. The returned path omits the implicit ``.h5`` suffix h5 case files carry: ``H5File`` - re-appends it on open. + re-appends it on open. A case found once is not probed again (the probe is a round-trip on + a remote root, per entry read); an ABSENT case stays a fresh question, because a run may + produce it mid-read. """ + memo_key = (sub_directory, name) + memoised = self._case_paths.get(memo_key) + if memoised is not None: + return memoised path = f"{self.filename}{sub_directory}{name}" - on_disk = f"{path}{'.h5' if self.file_format == 'h5' else ''}" + on_disk = f"{path}{self._backend.case_file_suffix or ''}" if uri.exists(on_disk): + self._case_paths[memo_key] = path return path if uri.is_uri(on_disk): return None # no writer of a remote root, so no backup of one to recover @@ -360,11 +374,12 @@ def _resolve_entry(self, groups: str, name: str, action: Callable[[_AbstractFile path's last component, so the coordinates are ``("", group)`` there and ``(groups, name)`` on a single-file dataset. Raises ``DatasetManagerError`` when the dataset or the entry is missing. """ - if not self.exists_on_disk(): + if not self._root_seen and not self.exists_on_disk(): raise DatasetManagerError( f"The dataset '{self.filename}' does not exist.", "Check 'dataset_filenames' and the path it names.", ) + self._root_seen = True if self.is_directory: for sub_directory in self._get_sub_directories(groups): path = self._case_path(sub_directory, name) @@ -599,14 +614,15 @@ def _iter_names(self, groups: str) -> Generator[str, None, None]: yield from file.get_names(groups) return group = groups.split("/")[-1] + suffix = self._backend.case_file_suffix for sub_directory in self._get_sub_directories(groups): root = f"{self.filename}{sub_directory}" for name in uri.list_names(root): - if self.file_format == "h5" and uri.is_dir(f"{root}{name}"): + if suffix and uri.is_dir(f"{root}{name}"): continue with self._file(f"{root}{name}", True) as file: if file.is_exist(group): - yield name.replace(".h5", "") if self.file_format == "h5" else name + yield name.removesuffix(suffix) if suffix else name def get_names(self, groups: str, index: list[int] | None = None) -> list[str]: if index is None and groups in self._names_cache: @@ -643,7 +659,7 @@ def select_names(self, groups: str, requested: set[str] | None) -> list[str]: def get_group(self) -> list[str]: if self.is_directory: - if self.file_format in {"dicom", "omezarr"}: + if self._backend.lists_case_entries: groups_set = set() for case in uri.list_names(self.filename): case_path = uri.join(self.filename, case) diff --git a/konfai/utils/dataset/dicom_file.py b/konfai/utils/dataset/dicom_file.py index 5186e0db..f1655e0c 100644 --- a/konfai/utils/dataset/dicom_file.py +++ b/konfai/utils/dataset/dicom_file.py @@ -36,6 +36,9 @@ class DicomFile(AbstractFile): """DICOM series backend with header-only metadata and slice-level reads.""" + concurrent_write_safe = False # a series shares its directory and info memo across entries + lists_case_entries = True # a case is a directory of series this backend enumerates + def __init__(self, filename: str, read: bool) -> None: self.filename = filename if filename.endswith("/") else f"{filename}/" self.read = read @@ -72,6 +75,14 @@ def bounded_region_reads(self, name: str) -> bool: del name return True # one file per slice: a region decodes the slices it covers and nothing else + def read_granularity(self, name: str) -> tuple[int, ...] | None: + from konfai.utils.dicom import get_dicom_info + + # One file per z step, decoded as a whole plane: a window narrower than a plane costs the + # plane, exactly the band a memmapped volume declares. + shape = get_dicom_info(self._path(name))["shape"] + return (1, 1, int(shape[2]), int(shape[3])) + def file_to_data_slice(self, group: str, name: str, slices: tuple[slice, ...]) -> tuple[np.ndarray, Attribute]: from konfai.utils.dicom import get_dicom_info, read_dicom_series_slice diff --git a/konfai/utils/dataset/h5.py b/konfai/utils/dataset/h5.py index 17f755c9..e4e0cd61 100644 --- a/konfai/utils/dataset/h5.py +++ b/konfai/utils/dataset/h5.py @@ -38,6 +38,7 @@ except ImportError: sitk = None # type: ignore[assignment] from konfai import current_date +from konfai.utils.budget import budget_share from konfai.utils.dataset.abstract import AbstractFile from konfai.utils.dataset.attribute import Attribute, _encode_transform_leaves, image_to_data from konfai.utils.dataset.staging import _REPLACED_MARKER, _orphaned_backup_names, _replaced_name, is_staging_entry @@ -234,6 +235,15 @@ def _close(self, success: bool) -> None: class H5File(AbstractFile): + single_store = True # one .h5 file holds every case + concurrent_write_safe = False # entries share the file's handles and metadata + case_file_suffix = ".h5" # what a case file carries when a directory keeps one per case + + @classmethod + def can_stream(cls, file_format: str, attributes: Attribute) -> bool: + del file_format, attributes + return True # a dataset written by regions, chunked or contiguous + # Read-side HDF5 chunk cache, per opened dataset. The library default (1 MB) holds barely one # medical-imaging chunk, so overlapping patch reads on a chunked (compressed) store # re-decompress the same chunks once per patch. KonfAI writes its own h5 contiguous @@ -242,6 +252,17 @@ class H5File(AbstractFile): _READ_CHUNK_CACHE_BYTES = 128 * 1024 * 1024 _READ_CHUNK_CACHE_SLOTS = 100003 + @staticmethod + def _read_chunk_cache_bytes() -> int: + """What one pooled handle's HDF5 chunk cache may hold: the cache share of the declared + per-rank budget divided across the pool's handles, so the pool at capacity stays inside + the one share every decoded-block cache draws from; the fixed default when no budget was + declared.""" + share = budget_share("cache") + if share is None: + return H5File._READ_CHUNK_CACHE_BYTES + return max(1, int(share) // _H5ReadPool._MAX) + def __init__(self, filename: str, read: bool) -> None: if h5py is None: raise DatasetManagerError( @@ -267,7 +288,7 @@ def __enter__(self): if self.read: pooled = _h5_read_pool.get( self.filename, - rdcc_nbytes=self._READ_CHUNK_CACHE_BYTES, + rdcc_nbytes=self._read_chunk_cache_bytes(), rdcc_nslots=self._READ_CHUNK_CACHE_SLOTS, ) self.h5, self._sidecars = pooled.file, pooled.sidecars @@ -306,7 +327,7 @@ def _sidecar(self, dataset: h5py.Dataset) -> Attribute: return Attribute(sidecar) def file_to_data(self, groups: str, name: str) -> tuple[np.ndarray, Attribute]: - dataset = self._get_dataset(groups, name) + dataset = self._require_dataset(groups, name) data = np.zeros(dataset.shape, dataset.dtype) dataset.read_direct(data) return data, self._sidecar(dataset) @@ -318,7 +339,7 @@ def bounded_region_reads(self, name: str) -> bool: return True def file_to_data_slice(self, groups: str, name: str, slices: tuple[slice, ...]) -> tuple[np.ndarray, Attribute]: - dataset = self._get_dataset(groups, name) + dataset = self._require_dataset(groups, name) data = np.asarray(dataset[slices]) return data, self._sidecar(dataset) @@ -472,7 +493,20 @@ def _orphaned_entries(h5_group: h5py.Group, present: list[str]) -> list[str]: def get_group(self) -> list[str]: return list(self.h5.keys()) if self.h5 is not None else [] - def _get_dataset(self, groups: str, name: str, h5_group: h5py.Group = None) -> h5py.Dataset: + def _require_dataset(self, groups: str, name: str) -> h5py.Dataset: + """The entry, or the designed refusal: an absent group resolved ``None`` and every reader + dereferenced it, an anonymous ``AttributeError`` deep in numpy where the sibling backends + name the entry.""" + dataset = self._get_dataset(groups, name) + if dataset is None: + entry = f"{groups}/{name}" if groups else name + raise DatasetManagerError( + f"'{entry}' is not in '{self.filename}'.", + "Check the case name and the group it is looked up under.", + ) + return dataset + + def _get_dataset(self, groups: str, name: str, h5_group: h5py.Group = None) -> h5py.Dataset | None: if h5_group is None: h5_group = self.h5 if groups != "": @@ -498,5 +532,5 @@ def _get_dataset(self, groups: str, name: str, h5_group: h5py.Group = None) -> h return result def get_infos(self, groups: str, name: str) -> tuple[list[int], Attribute]: - dataset = self._get_dataset(groups, name) + dataset = self._require_dataset(groups, name) return dataset.shape, self._sidecar(dataset) diff --git a/konfai/utils/dataset/itk_transform_file.py b/konfai/utils/dataset/itk_transform_file.py index e64e9421..142987a0 100644 --- a/konfai/utils/dataset/itk_transform_file.py +++ b/konfai/utils/dataset/itk_transform_file.py @@ -143,6 +143,24 @@ def __init__(self, filename: str, read: bool) -> None: self.filename = filename self.read = read + @classmethod + def open( + cls, + filename: str, + read: bool, + file_format: str, + level: int = 0, + scale_factors: list[int] | None = None, + downsample_method: str | None = None, + ) -> ItkTransformFile: + del file_format, level, scale_factors, downsample_method + return cls(f"{filename}/", read) + + @classmethod + def can_stream(cls, file_format: str, attributes: Attribute) -> bool: + del file_format + return is_an_image(attributes) # only a displacement FIELD writes by regions + def __enter__(self): return self diff --git a/konfai/utils/dataset/ome_zarr_file.py b/konfai/utils/dataset/ome_zarr_file.py index 04d3b397..1028ed95 100644 --- a/konfai/utils/dataset/ome_zarr_file.py +++ b/konfai/utils/dataset/ome_zarr_file.py @@ -40,6 +40,7 @@ displacement_field_to_data, image_to_data, ome_zarr_attributes, + region_geometry, ) from konfai.utils.dataset.staging import _recover_orphaned_backup, _replaced_name, _retire_dead_debris from konfai.utils.dataset.stream import DataStream @@ -87,6 +88,18 @@ def _divisor_tile(extent: int, cap: int) -> int: return divisor if divisor * 4 >= cap else extent +#: Where each entry's store was resolved on disk, keyed by ``(root, entry)``: the store-suffix +#: probes are one ``fs.info`` round-trip each on a remote root, per patch without this. A write +#: through this backend forgets the memo (it may change the suffix the entry resolves under); a +#: store REPLACED at the same path keeps its resolution, so no other invalidation is owed. +_resolved_store_paths: dict[tuple[str, str], str] = {} + + +def _forget_resolved_paths() -> None: + """Drop the entry-path memo: what a write must call, being the one thing that moves a store.""" + _resolved_store_paths.clear() + + class _OmeZarrDataStream(DataStream): def __init__( self, @@ -144,6 +157,7 @@ def _close(self, success: bool) -> None: # points at a component that is no longer there. This path alone: the sources a cohort is # still reading are not what changed. clear_ome_zarr_cache(self._final_path) + _forget_resolved_paths() class OmeZarrFile(AbstractFile): @@ -158,6 +172,16 @@ class OmeZarrFile(AbstractFile): writes one and a consumer that asks for ``@1`` are two halves of the same contract. """ + concurrent_write_safe = False # a store shares metadata across its arrays + reads_remote = True # a store is addressed by key, so fsspec serves a URI root + writes_pyramid = True # the one format with levels + lists_case_entries = True # a case is a directory of stores this backend enumerates + + @classmethod + def can_stream(cls, file_format: str, attributes: Attribute) -> bool: + del file_format, attributes + return True # zarr chunks materialise as regions land + def __init__( self, filename: str, @@ -180,11 +204,20 @@ def __exit__(self, exc_type, value, traceback): def _path(self, name: str, *, writing: bool = False) -> str: """Where entry ``name``'s store sits: text, because a remote one is a URI and ``Path`` - eats the second slash of one.""" + eats the second slash of one. Resolved once per ``(root, entry)``: each suffix probe is a + round-trip on a remote root, and the store's location cannot change mid-run.""" base = uri.join(self.filename, name) if writing: uri.refuse_write(self.filename) return f"{base}.ome.zarr" + memo_key = (self.filename, name) + resolved = _resolved_store_paths.get(memo_key) + if resolved is not None: + return resolved + _resolved_store_paths[memo_key] = resolved = self._resolve_path(name, base) + return resolved + + def _resolve_path(self, name: str, base: str) -> str: # Every spelling is_store_name accepts, or a root whose first case names one of the # others is detected as omezarr at setup and then fails to resolve. candidates = [f"{base}{form}" for form in STORE_FORMS] + [base] @@ -245,12 +278,14 @@ def file_to_data_slice(self, group: str, name: str, slices: tuple[slice, ...]) - attributes = self._attributes(metadata) shape = metadata["shape"] normalized = tuple(slice(*item.indices(size)) for item, size in zip(slices, shape, strict=True)) - spacing = attributes.get_np_array("Spacing") - direction = attributes.get_np_array("Direction").reshape(len(spacing), len(spacing)) - start_xyz = np.asarray([item.start for item in reversed(normalized[1:])], dtype=np.float64) - step_xyz = np.asarray([item.step for item in reversed(normalized[1:])], dtype=np.float64) - attributes["Origin"] = attributes.get_np_array("Origin") + direction @ (start_xyz * spacing) - attributes["Spacing"] = spacing * step_xyz + origin, spacing = region_geometry( + attributes.get_np_array("Origin"), + attributes.get_np_array("Spacing"), + attributes.get_np_array("Direction"), + normalized[1:], + ) + attributes["Origin"] = origin + attributes["Spacing"] = spacing return data, attributes def bounded_region_reads(self, name: str) -> bool: @@ -326,6 +361,7 @@ def data_to_file( shutil.rmtree(replaced, ignore_errors=True) # The reader memoises decoded chunks by path, and this path now holds another store. clear_ome_zarr_cache(final) + _forget_resolved_paths() with contextlib.suppress(Exception): _retire_dead_debris(final) # housekeeping past the publish: it cannot fail the write @@ -363,6 +399,19 @@ def open_data_stream( # complete but whose coarser levels are not. return _OmeZarrDataStream(array, store_path, final_path, self.scale_factors, self.downsample_method) + @classmethod + def open( + cls, + filename: str, + read: bool, + file_format: str, + level: int = 0, + scale_factors: list[int] | None = None, + downsample_method: str | None = None, + ) -> OmeZarrFile: + del file_format + return cls(filename, read, level, scale_factors, downsample_method) + def get_names(self, group: str) -> list[str]: return self.get_group() diff --git a/konfai/utils/dataset/raw_block.py b/konfai/utils/dataset/raw_block.py index 3ef3a8ad..45bdcf57 100644 --- a/konfai/utils/dataset/raw_block.py +++ b/konfai/utils/dataset/raw_block.py @@ -31,7 +31,7 @@ import SimpleITK as sitk except ImportError: sitk = None # type: ignore[assignment] -from konfai.utils.dataset.attribute import Attribute, _attribute_text +from konfai.utils.dataset.attribute import Attribute, _attribute_text, region_geometry from konfai.utils.dataset.stream import _MHA_ELEMENT_TYPES, _NIFTI_DATATYPES #: NumPy dtype of each element type the raw-block route reads (the inverses of the writers' tables). @@ -240,20 +240,30 @@ def _pixel_block_region(block: _PixelBlock, path: str, normalized: tuple[slice, return np.array(region, dtype=block.dtype.newbyteorder("="), order="C") -def _pixel_block_attributes(block: _PixelBlock, index_xyz: list[int] | None) -> Attribute: - """The attributes ITK's route records: the header's keys, then the geometry, the origin being - the region's (at ``index_xyz``) as ITK's extract computes it, then the region's origin again as - the module computes it. ``None`` is the whole volume's record, as ``file_to_data`` returns it.""" +def _pixel_block_attributes(block: _PixelBlock, spatial_slices: tuple[slice, ...] | None) -> Attribute: + """The attributes ITK's route records for the region ``spatial_slices`` keeps (``None`` is the + whole volume's record, as ``file_to_data`` returns it). + + A unit-step region carries the record ITK's extract leaves: the region's origin as ITK computes + it, then the region's origin again as :func:`region_geometry` computes it. A stepped region + starts from the volume's record (what the ITK route reads, since ITK cannot extract a step) and + appends the region's shifted origin and step-scaled spacing, so the two routes stay + key-for-key identical. + """ attributes = Attribute(block.metadata) - if index_xyz is None: + stepped = spatial_slices is not None and any(item.step != 1 for item in spatial_slices) + if spatial_slices is None or stepped: attributes["Origin"] = block.geometry_text["Origin"] else: + index_xyz = [item.start for item in reversed(spatial_slices)] attributes["Origin"] = np.asarray(block.probe.TransformIndexToPhysicalPoint(index_xyz)) attributes["Spacing"] = block.geometry_text["Spacing"] attributes["Direction"] = block.geometry_text["Direction"] - if index_xyz is not None: - direction = block.direction.reshape(len(block.spacing), len(block.spacing)) - attributes["Origin"] = block.origin + direction @ (np.asarray(index_xyz, dtype=np.float64) * block.spacing) + if spatial_slices is not None: + origin, spacing = region_geometry(block.origin, block.spacing, block.direction, spatial_slices) + attributes["Origin"] = origin + if stepped: + attributes["Spacing"] = spacing return attributes diff --git a/konfai/utils/dataset/sitk_file.py b/konfai/utils/dataset/sitk_file.py index 2e570c58..a7c906ea 100644 --- a/konfai/utils/dataset/sitk_file.py +++ b/konfai/utils/dataset/sitk_file.py @@ -41,6 +41,7 @@ data_to_image, image_to_data, is_an_image, + region_geometry, ) from konfai.utils.dataset.landmarks import read_landmarks, write_landmarks from konfai.utils.dataset.raw_block import ( @@ -67,6 +68,17 @@ _unstreamed_formats_warned: set[str] = set() +def _require_sitk(path: str, action: str = "read") -> None: + """The structured refusal for a bare install, called at every SimpleITK touch point: guarding + the backend whole would refuse the npy/fcsv/xml/vtk entries it serves with no SimpleITK at + all.""" + if sitk is None: + raise DatasetManagerError( + f"SimpleITK is required to {action} '{path}'.", + "Install it with: pip install konfai[itk] (or konfai[imaging]).", + ) + + def _warn_unstreamed_region_read(path: str) -> None: """Warn that `path`'s format decodes the whole volume for every patch region read from it. @@ -92,6 +104,25 @@ def __init__(self, filename: str, read: bool, file_format: str) -> None: self.read = read self.file_format = file_format + @classmethod + def open( + cls, + filename: str, + read: bool, + file_format: str, + level: int = 0, + scale_factors: list[int] | None = None, + downsample_method: str | None = None, + ) -> SitkFile: + del level, scale_factors, downsample_method + return cls(f"{filename}/", read, file_format) + + @classmethod + def can_stream(cls, file_format: str, attributes: Attribute) -> bool: + # The region-writable formats are the region-readable ones: an uncompressed MetaImage or + # NIfTI is a fixed header plus a flat raw block, written through a memmap. + return file_format in ("mha", "nii") and is_an_image(attributes) + @staticmethod def _normalize_slices(slices: tuple[slice, ...], shape: list[int]) -> tuple[slice, ...]: if len(slices) != len(shape): @@ -119,6 +150,7 @@ def _supports_region_read(path: str) -> bool: Cached: the patch path asks this per read, and it opens the file to read a header. """ + _require_sitk(path) if _pixel_block(path) is not None: return True # a memmap of the raw block reads the region's pages and no other image_io = sitk.ImageFileReader.GetImageIOFromFileName(path) @@ -150,6 +182,8 @@ def read_granularity(self, name: str) -> tuple[int, ...] | None: volume is the cost and the streaming refusal already says so. """ path = self._resolve_data_path(name) + if path is not None: + _require_sitk(path) block = _pixel_block(path) if path is not None else None if block is None: return None @@ -186,12 +220,12 @@ def _resolve_data_path(self, name: str) -> str | None: return matches[0] if matches else None def _file_to_image_slice(self, name: str, path: str, slices: tuple[slice, ...]) -> tuple[np.ndarray, Attribute]: + _require_sitk(path) block = _pixel_block(path) if block is not None: # The region's bytes off the file, where ITK's streaming reader decodes them through # its pipeline: 3.5 ms against 0.09 ms for a 64^3 region of an uncompressed 256^3 - # .mha, the same bytes. The record ITK's route leaves is kept: the region's origin - # for a direct slice, the volume's for a stepped one, which ITK reads whole. + # .mha, the same bytes. The record ITK's route leaves is kept, key for key. normalized = self._normalize_slices(slices, list(block.shape)) if all(item.step > 0 for item in normalized): try: @@ -199,9 +233,7 @@ def _file_to_image_slice(self, name: str, path: str, slices: tuple[slice, ...]) except (OSError, ValueError): # replaced under the stat: ITK answers for it pass else: - index_xyz = [item.start for item in reversed(normalized[1:])] - direct = self._supports_direct_slice(normalized) - return data, _pixel_block_attributes(block, index_xyz if direct else None) + return data, _pixel_block_attributes(block, normalized[1:]) reader = sitk.ImageFileReader() reader.SetFileName(path) reader.ReadImageInformation() @@ -212,7 +244,19 @@ def _file_to_image_slice(self, name: str, path: str, slices: tuple[slice, ...]) normalized = self._normalize_slices(slices, data_shape) if not self._supports_direct_slice(normalized) or _nifti_extract_aborts(path): + # ITK reads the volume whole here; the record is still the REGION's, like every other + # backend's: the volume's own geometry, then the shifted origin (and, for a step, the + # step-scaled spacing) of the samples actually returned. data, attributes = self.file_to_data("", name) + origin, spacing = region_geometry( + attributes.get_np_array("Origin"), + attributes.get_np_array("Spacing"), + attributes.get_np_array("Direction"), + normalized[1:], + ) + attributes["Origin"] = origin + if any(item.step != 1 for item in normalized[1:]): + attributes["Spacing"] = spacing return data[normalized], attributes if not self._supports_region_read(path): @@ -225,10 +269,10 @@ def _file_to_image_slice(self, name: str, path: str, slices: tuple[slice, ...]) image = reader.Execute() data, attributes = image_to_data(image) - origin = np.asarray(reader.GetOrigin(), dtype=np.float64) - spacing = np.asarray(reader.GetSpacing(), dtype=np.float64) - direction = np.asarray(reader.GetDirection(), dtype=np.float64).reshape(len(spacing), len(spacing)) - attributes["Origin"] = origin + direction @ (np.asarray(extract_index_xyz, dtype=np.float64) * spacing) + origin, _spacing = region_geometry( + reader.GetOrigin(), reader.GetSpacing(), reader.GetDirection(), normalized[1:] + ) + attributes["Origin"] = origin return data[normalized[:1] + tuple(slice(None) for _ in normalized[1:])], attributes def file_to_data(self, group: str, name: str) -> tuple[np.ndarray, Attribute]: @@ -240,6 +284,7 @@ def file_to_data(self, group: str, name: str) -> tuple[np.ndarray, Attribute]: ) attributes = Attribute() if path.endswith(".itk.txt"): + _require_sitk(path) datas = _encode_transform_leaves(sitk.ReadTransform(path), name, attributes) max_len = max(len(v) for v in datas) data = np.array([np.pad(v, (0, max_len - len(v)), constant_values=np.nan) for v in datas]) @@ -270,6 +315,7 @@ def file_to_data(self, group: str, name: str) -> tuple[np.ndarray, Attribute]: elif path.endswith(".npy"): data = np.load(path) else: + _require_sitk(path) image = sitk.ReadImage(path) data, attributes_tmp = image_to_data(image) attributes.update(attributes_tmp) @@ -324,7 +370,7 @@ def data_to_file( if attributes is None: attributes = Attribute() os.makedirs(self.filename, exist_ok=True) - if isinstance(data, sitk.Image): + if sitk is not None and isinstance(data, sitk.Image): for k, v in attributes.items(): if v and len(v): data.SetMetaData(k, v) @@ -336,7 +382,7 @@ def data_to_file( os.replace(staging, final) with contextlib.suppress(Exception): _retire_dead_debris(Path(final)) # past the publish: housekeeping cannot fail the write - elif isinstance(data, sitk.Transform): + elif sitk is not None and isinstance(data, sitk.Transform): sitk.WriteTransform(data, f"{self.filename}{name}.itk.txt") elif self.is_vtk_polydata(data): import vtk @@ -346,6 +392,7 @@ def data_to_file( vtk_writer.SetInputData(data) vtk_writer.Write() elif is_an_image(attributes): + _require_sitk(f"{self.filename}{name}.{self.file_format}", "write") self.data_to_file(name, data_to_image(data, attributes), attributes) elif len(data.shape) == 2 and data.shape[1] == 3 and data.shape[0] > 0: data = np.round(data, 4) @@ -424,12 +471,6 @@ def is_exist(self, group: str, name: str | None = None) -> bool: _recover_orphaned_backup(Path(f"{base}.{ext}")) return any(os.path.exists(base + "." + ext) for ext in SUPPORTED_EXTENSIONS) - def get_names(self, group: str) -> list[str]: - raise NotImplementedError() - - def get_group(self) -> list[str]: - raise NotImplementedError() - def get_infos(self, group: str, name: str) -> tuple[list[int], Attribute]: attributes = Attribute() # Resolve the actual entry path (any image extension, not only the dataset's file_format): @@ -439,6 +480,7 @@ def get_infos(self, group: str, name: str) -> tuple[list[int], Attribute]: entry = f"{group if group is not None else ''}{name}" path = self._resolve_data_path(entry) if path is not None and not path.endswith((".itk.txt", ".fcsv", ".xml", ".vtk", ".npy")): + _require_sitk(path) file_reader = sitk.ImageFileReader() file_reader.SetFileName(path) file_reader.ReadImageInformation() diff --git a/konfai/utils/dicom.py b/konfai/utils/dicom.py index 9013371c..86b975b8 100644 --- a/konfai/utils/dicom.py +++ b/konfai/utils/dicom.py @@ -48,6 +48,8 @@ import os import re +import threading +from collections import OrderedDict from collections.abc import Sequence from datetime import datetime from functools import cache @@ -56,6 +58,7 @@ import numpy as np +from konfai.utils.budget import budget_share from konfai.utils.errors import DatasetManagerError # Zero-padded slice filenames produced by :func:`write_dicom_series` (e.g. ``000001.dcm``). @@ -374,6 +377,111 @@ def _decode_slices(datasets: list[DicomDataset], window: tuple[slice, slice], ap return volume +#: What the plane cache may hold when no budget is declared; a declared budget gives it the cache +#: share instead (:data:`~konfai.utils.budget.BUDGET_SHARES`), so one declaration is divided once. +_PLANE_CACHE_DEFAULT_BYTES = 256 << 20 + +#: Large data elements are left in the file until accessed: a plane read needs the pixels and the +#: rescale tags, not every private element parsed into memory. +_DCMREAD_DEFER_BYTES = 4096 + + +def _plane_cache_capacity() -> int: + """What the decoded-plane cache may hold: its share of the declared per-rank budget, the + default when none was declared.""" + share = budget_share("cache") + return int(share) if share is not None else _PLANE_CACHE_DEFAULT_BYTES + + +class _DecodedPlaneCache: + """Decoded DICOM slice planes with their rescale tags, evicted LRU under a byte cap. + + A series stores one file per plane, and every region read touching a z index decodes that + file's whole plane: overlapping regions of a sweep would parse and decode the same file once + per region (the same cliff the OME-Zarr route caps with its decoded-chunk cache). Keyed by + ``(path, mtime_ns, size)``, so a rewritten slice is a new entry and never served stale.""" + + def __init__(self) -> None: + self._entries: OrderedDict[tuple[str, int, int], tuple[np.ndarray, float, float]] = OrderedDict() + self._bytes = 0 + self._lock = threading.Lock() + + def get(self, key: tuple[str, int, int]) -> tuple[np.ndarray, float, float] | None: + with self._lock: + entry = self._entries.get(key) + if entry is not None: + self._entries.move_to_end(key) + return entry + + def put(self, key: tuple[str, int, int], plane: np.ndarray, slope: float, intercept: float) -> None: + capacity = _plane_cache_capacity() + if plane.nbytes > capacity: + return + with self._lock: + if key in self._entries: + return + self._entries[key] = (plane, slope, intercept) + self._bytes += plane.nbytes + while self._bytes > capacity and self._entries: + self._bytes -= self._entries.popitem(last=False)[1][0].nbytes + + def clear(self) -> None: + with self._lock: + self._entries.clear() + self._bytes = 0 + + +_plane_cache = _DecodedPlaneCache() + + +def _decoded_plane(path: Path) -> tuple[np.ndarray, float, float]: + """One slice file's whole decoded plane and its rescale tags, parsed and decoded once per file + per pass: the cache is what keeps an overlapping sweep from decoding it once per region.""" + stamp = os.stat(path) + key = (str(path), stamp.st_mtime_ns, stamp.st_size) + cached = _plane_cache.get(key) + if cached is not None: + return cached + ds = pydicom.dcmread(str(path), defer_size=_DCMREAD_DEFER_BYTES) + plane = ds.pixel_array + plane.flags.writeable = False # shared across every region that hits the cache + slope = float(getattr(ds, "RescaleSlope", 1.0)) + intercept = float(getattr(ds, "RescaleIntercept", 0.0)) + _plane_cache.put(key, plane, slope, intercept) + return plane, slope, intercept + + +def _decode_cached_planes(files: list[Path], window: tuple[slice, slice], apply_rescale: bool) -> np.ndarray: + """:func:`_decode_slices` off the plane cache: the same values, the same refusals, each plane + parsed and decoded at most once per pass instead of once per touching region.""" + volume: np.ndarray | None = None + expected_shape: tuple[int, ...] | None = None + for i, path in enumerate(files): + try: + plane, slope, intercept = _decoded_plane(path) + except Exception as exc: + raise DatasetManagerError( + f"Cannot read pixel data from DICOM slice {i}.", + f"Transfer syntax or compression may be unsupported: {exc}", + ) from exc + if expected_shape is None: + expected_shape = plane.shape + elif plane.shape != expected_shape: + raise DatasetManagerError( + f"Inconsistent slice shape at index {i}: expected {expected_shape}, got {plane.shape}.", + "All slices in a series must have the same rows and columns.", + ) + arr = plane[window].astype(np.float32) + if apply_rescale: + arr = arr * slope + intercept + if volume is None: + volume = np.empty((len(files), *arr.shape), dtype=np.float32) + volume[i] = arr + if volume is None: + raise DatasetManagerError("Series contains no readable slices.") + return volume + + @cache def get_dicom_info( directory: str | Path, @@ -434,17 +542,16 @@ def read_dicom_series_slice( raise DatasetManagerError("DICOM stores scalar data and supports only channel 0.") _require_pydicom() + from konfai.utils.dataset.attribute import region_geometry + # ``sorted_files`` is in slice order already: the selection is read in that order, not sorted again. z_indices = range(*normalized[1].indices(shape[1])) - datasets = [pydicom.dcmread(str(info["sorted_files"][index])) for index in z_indices] - volume = _decode_slices(datasets, (normalized[2], normalized[3]), apply_rescale)[np.newaxis][normalized[0]] + volume = _decode_cached_planes( + [info["sorted_files"][index] for index in z_indices], (normalized[2], normalized[3]), apply_rescale + )[np.newaxis][normalized[0]] - direction_matrix = np.asarray(info["direction"], dtype=np.float64).reshape(3, 3) - start_xyz = np.asarray([normalized[3].start, normalized[2].start, normalized[1].start], dtype=np.float64) - spacing = np.asarray(info["spacing"], dtype=np.float64) - origin = np.asarray(info["origin"], dtype=np.float64) + direction_matrix @ (start_xyz * spacing) - step_xyz = np.asarray([normalized[3].step, normalized[2].step, normalized[1].step], dtype=np.float64) - return volume, origin, spacing * step_xyz, np.asarray(info["direction"], dtype=np.float64) + origin, spacing = region_geometry(info["origin"], info["spacing"], info["direction"], normalized[1:]) + return volume, origin, spacing, np.asarray(info["direction"], dtype=np.float64) def _encode_pixels(data: np.ndarray) -> tuple[np.ndarray, float, float]: @@ -498,6 +605,7 @@ def write_dicom_series( root = Path(directory) root.mkdir(parents=True, exist_ok=True) get_dicom_info.cache_clear() # what this directory holds is about to change + _plane_cache.clear() # Remove only slices previously written by this function (its zero-padded NNNNNN.dcm # naming), never unrelated DICOM files that may share the directory. for existing in root.glob("*.dcm"): diff --git a/konfai/utils/ome_zarr.py b/konfai/utils/ome_zarr.py index 226a9e53..c4b22e48 100644 --- a/konfai/utils/ome_zarr.py +++ b/konfai/utils/ome_zarr.py @@ -966,20 +966,15 @@ def _grid_is_axis_aligned(attributes: dict[str, Any] | None) -> bool: grid can be declared a ``displacements`` transformation; an oriented one keeps the label-only layout, its Direction in the sidecar. - ``Attribute`` versions its keys (``Direction_0``, ``Direction_1``, ...), and the sidecar dict - arrives here verbatim: the LATEST version is the grid the store describes. + The sidecar dict is read through :class:`Attribute`, the one owner of the versioned-key stack + and the printed-array format: the LATEST ``Direction`` is the grid the store describes. """ - versions = { - key: value - for key, value in (attributes or {}).items() - if key == "Direction" or (key.startswith("Direction_") and key.removeprefix("Direction_").isdigit()) - } - if not versions: + from konfai.utils.dataset.attribute import Attribute + + record = Attribute(attributes or {}) + if "Direction" not in record: return True - value = versions[max(versions, key=lambda key: int(key.rsplit("_", 1)[-1]) if "_" in key else -1)] - flat = np.asarray( - str(value).replace("[", " ").replace("]", " ").split() if isinstance(value, str) else value, dtype=np.float64 - ).ravel() + flat = record.get_np_array("Direction") side = round(len(flat) ** 0.5) return side * side == len(flat) and bool(np.allclose(flat.reshape(side, side), np.eye(side))) diff --git a/tests/unit/test_dataset.py b/tests/unit/test_dataset.py index a8a50756..7a1449d5 100644 --- a/tests/unit/test_dataset.py +++ b/tests/unit/test_dataset.py @@ -103,11 +103,67 @@ def test_attribute_holding_a_long_array_round_trips_past_numpys_print_threshold( np.testing.assert_allclose(attribute.get_np_array("Long"), np.arange(2000, dtype=float)) +def test_attribute_names_the_key_whose_value_does_not_parse_back_flat() -> None: + """A >= 2-D value is stored as a nested print (Crop's ``box`` is read back through its own + parser, so the write door cannot refuse the rank), and reading it back as an array used to be + an anonymous ``ValueError`` deep in numpy: the refusal now names the key and the remedy.""" + attribute = Attribute() + attribute["MyMatrix"] = np.eye(3) + with pytest.raises(DatasetManagerError, match=r"'MyMatrix'.*flat"): + attribute.get_np_array("MyMatrix") + with pytest.raises(DatasetManagerError, match="flatten the value"): + attribute.get_tensor("MyMatrix") + assert attribute["MyMatrix_0"] == "[[1. 0. 0.] [0. 1. 0.] [0. 0. 1.]]", "the text door still serves it" + with pytest.raises(DatasetManagerError, match=r"'MyMatrix'"): + attribute.pop_np_array("MyMatrix") + attribute["Direction"] = np.eye(3).flatten() # the flat form parses back exactly + np.testing.assert_array_equal(attribute.get_np_array("Direction"), np.eye(3).flatten()) + + # -------------------------------------------------------------------------------------- # HDF5 backend: directories, modes, and per-file locking # -------------------------------------------------------------------------------------- +def test_h5_missing_group_raises_the_designed_refusal_not_attributeerror(tmp_path: Path, image_attributes) -> None: + """H5File._get_dataset answered None for an absent group and every reader dereferenced it: + ``AttributeError: 'NoneType' object has no attribute 'shape'`` deep in numpy, where every + sibling backend names the entry. The backend-level coordinates are the ones the dataset's + directory branch passes for a directory of case files: ``("", group)``.""" + h5py_module = pytest.importorskip("h5py") + del h5py_module + volume = np.arange(1 * 2 * 3 * 4, dtype=np.float32).reshape(1, 2, 3, 4) + Dataset(tmp_path / "Cases", "h5").write("ct", "CASE_000", volume, image_attributes([0.0] * 3, [1.0] * 3)) + + reader = Dataset.H5File(str(tmp_path / "Cases"), True) + with reader as _: + for read in ( + lambda: reader.file_to_data("", "missing_group"), + lambda: reader.get_infos("", "missing_group"), + lambda: reader.file_to_data_slice("", "missing_group", (slice(None),)), + lambda: reader.file_to_data("nope", "CASE_000"), + ): + with pytest.raises(DatasetManagerError, match="is not in"): + read() + + +def test_h5_read_chunk_cache_takes_its_slice_of_the_declared_budget() -> None: + """The HDF5 read pool's rdcc cache was the one decoded-block cache that ignored the declared + budget: 128 MiB per handle, up to 8 handles, whatever the declaration. Declared, the pool at + capacity now stays inside the same cache share every other decoded-block cache draws from.""" + pytest.importorskip("h5py") + from konfai.utils.budget import BUDGET_SHARES, set_per_rank_budget + from konfai.utils.dataset import _H5ReadPool + + try: + set_per_rank_budget(256 << 20) + expected = int((256 << 20) * BUDGET_SHARES["cache"]) // _H5ReadPool._MAX + assert Dataset.H5File._read_chunk_cache_bytes() == expected + finally: + set_per_rank_budget(None) + assert Dataset.H5File._read_chunk_cache_bytes() == Dataset.H5File._READ_CHUNK_CACHE_BYTES + + def test_h5_dataset_creates_nested_parent_directories(tmp_path: Path, image_attributes) -> None: # B19 - the parent directory is created with pathlib (nested paths, OS separators). dataset = Dataset(tmp_path / "runs" / "exp" / "Volumes", "h5") @@ -897,14 +953,16 @@ def test_a_region_off_the_raw_block_is_the_one_itk_decodes( np.testing.assert_array_equal(got, want) np.testing.assert_array_equal(got, data[region]) if data.shape[0] > 1 and path.suffix == ".nii": - # ITK aborts on a region of a vector NIfTI, so its route reads the volume whole and records - # the volume's origin; the block route records the region's, like every other format. + # ITK aborts on a region of a vector NIfTI, so its route reads the volume whole; both + # routes still record the REGION's origin (the shared region-geometry update), and only + # the first rung of the Origin stack differs: ITK's extract origin on the block route, + # the volume's on the whole-read one. index_xyz = np.asarray([item.start for item in reversed(region[1:])], dtype=np.float64) direction = want_attributes.get_np_array("Direction").reshape(3, 3) - expected = want_attributes.get_np_array("Origin") + direction @ ( - index_xyz * want_attributes.get_np_array("Spacing") - ) + volume_origin = Attribute._parse_array(dict(want_attributes)["Origin_0"]) + expected = volume_origin + direction @ (index_xyz * want_attributes.get_np_array("Spacing")) np.testing.assert_array_equal(attributes.get_np_array("Origin"), expected) + np.testing.assert_array_equal(want_attributes.get_np_array("Origin"), expected) rungs = ("Origin_0", "Origin_1") assert {k: v for k, v in attributes.items() if k not in rungs} == { k: v for k, v in want_attributes.items() if k not in rungs @@ -951,7 +1009,8 @@ def test_a_file_the_block_route_declines_is_still_read_by_itk(tmp_path: Path, ki def test_a_stepped_region_off_the_raw_block_reads_as_itk_reads_it_whole(tmp_path: Path, monkeypatch) -> None: """A step ITK cannot extract is served whole and sliced: the block serves the same values and - keeps the record ITK's route leaves, the volume's own geometry.""" + the same record as ITK's route — the volume's own geometry, then the region's shifted origin + and step-scaled spacing, the record every backend returns for the samples actually kept.""" path, data = _write_block_fixture(tmp_path, "vector.mha") region = (slice(0, 3, 2), slice(1, 12, 3), slice(0, 14, 2), slice(2, 16, 3)) @@ -963,6 +1022,45 @@ def test_a_stepped_region_off_the_raw_block_reads_as_itk_reads_it_whole(tmp_path np.testing.assert_array_equal(got, want) np.testing.assert_array_equal(got, data[region]) assert dict(attributes) == dict(want_attributes) + volume_origin = Attribute._parse_array(dict(attributes)["Origin_0"]) + volume_spacing = Attribute._parse_array(dict(attributes)["Spacing_0"]) + direction = attributes.get_np_array("Direction").reshape(3, 3) + start_xyz = np.asarray([2.0, 0.0, 1.0]) + np.testing.assert_array_equal( + attributes.get_np_array("Origin"), volume_origin + direction @ (start_xyz * volume_spacing) + ) + np.testing.assert_array_equal(attributes.get_np_array("Spacing"), volume_spacing * [3.0, 2.0, 3.0]) + + +def test_a_stepped_region_carries_the_same_geometry_record_whatever_the_backend(tmp_path: Path) -> None: + """The same stepped read of the same logical volume used to answer three different geometry + records depending on the file format it was stored in: SitkFile kept the volume's origin and + un-scaled spacing where OME-Zarr and DICOM returned the region's. One shared helper now + computes the record everywhere: the first kept sample's world position, the step-scaled + spacing.""" + pytest.importorskip("zarr") + pytest.importorskip("pydicom") + volume = np.arange(1 * 6 * 8 * 10, dtype=np.int16).reshape(1, 6, 8, 10) + origin = np.asarray([10.0, 20.0, 30.0]) + spacing = np.asarray([0.5, 1.5, 2.0]) + direction = np.asarray([[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]) + region = (slice(None), slice(1, 6, 2), slice(0, 8, 3), slice(2, 10, 2)) + start_xyz, step_xyz = np.asarray([2.0, 0.0, 1.0]), np.asarray([2.0, 3.0, 2.0]) + + for file_format in ("mha", "omezarr", "dicom"): + attributes = Attribute() + attributes["Origin"] = origin + attributes["Spacing"] = spacing + attributes["Direction"] = direction.flatten() + dataset = Dataset(tmp_path / file_format, file_format) + dataset.write("CT", "CASE_000", volume, attributes) + data, record = dataset.read_data_slice("CT", "CASE_000", region) + + np.testing.assert_array_equal(data, volume[region], err_msg=file_format) + np.testing.assert_allclose( + record.get_np_array("Origin"), origin + direction @ (start_xyz * spacing), err_msg=file_format + ) + np.testing.assert_allclose(record.get_np_array("Spacing"), spacing * step_xyz, err_msg=file_format) def test_the_raw_block_header_is_read_once_and_follows_a_rewrite(tmp_path: Path, monkeypatch) -> None: diff --git a/tests/unit/test_imaging_formats.py b/tests/unit/test_imaging_formats.py index 1456f151..25b24ed6 100644 --- a/tests/unit/test_imaging_formats.py +++ b/tests/unit/test_imaging_formats.py @@ -285,6 +285,56 @@ def counting(*args, **kwargs): np.testing.assert_array_equal(got, volume[:, 1:4:2]) assert sorts["count"] == 0 + def test_overlapping_region_reads_decode_each_plane_once(self, tmp_path: Path, monkeypatch) -> None: + """A series stores one file per plane, and every region read touching a z index used to + re-parse and re-decode that file whole: overlapping regions of a sweep paid one full + ``dcmread`` per touched slice per region. The plane cache decodes each file once per pass.""" + pydicom = pytest.importorskip("pydicom") + from konfai.utils import dicom + + root = tmp_path / "CT" + volume = np.arange(1 * 6 * 8 * 8, dtype=np.int16).reshape(1, 6, 8, 8) + dicom.write_dicom_series(root, volume, origin=(0.0,) * 3, spacing=(1.0,) * 3) + dicom.get_dicom_info(root) + reads: list[str] = [] + real_dcmread = pydicom.dcmread + + def counting(*args, **kwargs): + if not kwargs.get("stop_before_pixels", False): + reads.append(str(args[0])) + return real_dcmread(*args, **kwargs) + + monkeypatch.setattr(pydicom, "dcmread", counting) + first, *_ = dicom.read_dicom_series_slice(root, (slice(None), slice(0, 4), slice(1, 6), slice(0, 5))) + second, *_ = dicom.read_dicom_series_slice(root, (slice(None), slice(2, 6), slice(2, 8), slice(3, 8))) + + np.testing.assert_array_equal(first, volume[:, 0:4, 1:6, 0:5]) + np.testing.assert_array_equal(second, volume[:, 2:6, 2:8, 3:8]) + assert len(reads) == 6, "six distinct planes touched; the two overlapping slices decode once" + assert len(set(reads)) == 6 + + def test_the_plane_cache_takes_the_cache_share_of_a_declared_budget(self) -> None: + pytest.importorskip("pydicom") + from konfai.utils import dicom + from konfai.utils.budget import BUDGET_SHARES, set_per_rank_budget + + try: + set_per_rank_budget(128 << 20) + assert dicom._plane_cache_capacity() == int((128 << 20) * BUDGET_SHARES["cache"]) + finally: + set_per_rank_budget(None) + assert dicom._plane_cache_capacity() == dicom._PLANE_CACHE_DEFAULT_BYTES + + def test_a_dicom_series_declares_its_plane_as_the_read_grain(self, tmp_path: Path) -> None: + """One file per z step, decoded as a whole plane: the plan aligns and prices a sweep on the + plane exactly as it does a memmapped band, instead of trusting a silent None.""" + pytest.importorskip("pydicom") + volume = np.zeros((1, 3, 6, 5), dtype=np.int16) + dataset = Dataset(tmp_path / "DICOM", "dicom") + dataset.write("CT", "CASE_001", volume, _image_attributes()) + + assert dataset.read_granularity("CT", "CASE_001") == (1, 1, 6, 5) + def test_the_series_info_memo_is_unbounded_and_a_write_clears_it(self, tmp_path: Path) -> None: """A miss re-reads every slice header twice; a bound of 64 series missed on every patch of a cohort read in any order but case by case. A write of a series is what changes a directory.""" diff --git a/tests/unit/test_perf_hot_paths.py b/tests/unit/test_perf_hot_paths.py index e17e0c2e..1fcc3c90 100644 --- a/tests/unit/test_perf_hot_paths.py +++ b/tests/unit/test_perf_hot_paths.py @@ -89,6 +89,8 @@ def test_get_infos_is_memoized_and_returns_independent_copies(monkeypatch): ds.level = 0 ds._names_cache = {} ds._infos_cache = {} + ds._case_paths = {} + ds._root_seen = False monkeypatch.setattr(ds, "exists_on_disk", lambda: True) opens = {"n": 0} diff --git a/tests/unit/test_remote_dataset.py b/tests/unit/test_remote_dataset.py index 02691ae7..94729c09 100644 --- a/tests/unit/test_remote_dataset.py +++ b/tests/unit/test_remote_dataset.py @@ -38,10 +38,13 @@ @pytest.fixture def memory_root() -> str: """An empty ``memory://`` root, cleared of whatever a previous test left behind.""" + from konfai.utils.dataset.ome_zarr_file import _forget_resolved_paths + fs = fsspec.filesystem("memory") fs.store.clear() fs.pseudo_dirs.clear() fs.pseudo_dirs.append("") + _forget_resolved_paths() # the entry-path memo outlives the store this fixture just emptied return "memory://cohort" @@ -124,6 +127,34 @@ def test_a_remote_entry_is_read_region_by_region(memory_root: str) -> None: np.testing.assert_allclose(read, volume[region]) +def test_repeated_remote_region_reads_pay_no_path_resolution_round_trips( + memory_root: str, monkeypatch: pytest.MonkeyPatch +) -> None: + """Every region read paid several ``fs.info`` round-trips of re-resolution before touching + cached data: the root probe, the case probe, and the store-suffix probes. None of those + answers can change mid-run, so after the first read they are memoised and a patch read + touches only the caches.""" + volume = (np.random.default_rng(2).random((1, 6, 8, 10)) * 100).astype(np.float32) + _publish(memory_root, "CASE_000", "CT", volume) + dataset = Dataset(memory_root, "omezarr") + region = (slice(0, 1), slice(1, 4), slice(0, 8), slice(0, 10)) + dataset.read_data_slice("CT", "CASE_000", region) # resolves once, and memoises + + calls = {"info": 0} + real_info = fsspec.filesystem("memory").__class__.info + + def counting(self, *args, **kwargs): + calls["info"] += 1 + return real_info(self, *args, **kwargs) + + monkeypatch.setattr(fsspec.filesystem("memory").__class__, "info", counting) + for _ in range(3): + read, _ = dataset.read_data_slice("CT", "CASE_000", region) + np.testing.assert_allclose(read, volume[region]) + + assert calls["info"] == 0 + + def test_an_unreachable_remote_root_raises_instead_of_reporting_no_cases( memory_root: str, monkeypatch: pytest.MonkeyPatch ) -> None: From 0995cb1ffc21b2a21059d03206f0254a0b76bc89 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:21:18 +0200 Subject: [PATCH 14/28] ci: parallel suites, tested bundles, recoverable releases CI and test-fast run under pytest-xdist (installed everywhere, used only locally until now) with pip caching and CPU torch on Linux runners. The 628-test streamed oracle splits along its own axes so loadfile can spread it. The five published apps/* bundles' test suites finally run in CI and at publish, and apps joins the lint targets. konfai-mcp pins konfai/konfai-apps to the exact scm version like every sibling instead of floating over core internals; the 9-package upload gains skip-existing so a partial publish is completable by re-run; the studio wheel's built front is verified inside the artifact. Integration subprocesses share one run_workflow helper with a timeout, the duplicated API pipeline test is deleted, heavy imports skip instead of erroring at collection, and the SimpleITK NumPy-2.5 warning flood is filtered with its upstream note. --- .github/workflows/konfai_apps_ci.yml | 20 +- .github/workflows/konfai_ci.yml | 17 +- .github/workflows/konfai_mcp_ci.yml | 8 +- .github/workflows/konfai_studio_ci.yml | 20 +- .github/workflows/publish.yml | 36 +- konfai-mcp/pyproject.toml | 19 +- konfai-mcp/setup.py | 49 ++ pyproject.toml | 14 +- tests/integration/harness.py | 13 + .../test_konfai_auto_patch_prediction.py | 10 +- .../test_konfai_auto_patch_training.py | 10 +- tests/integration/test_konfai_chain_check.py | 10 +- .../integration/test_konfai_core_workflows.py | 94 +-- tests/integration/test_konfai_ensemble_tta.py | 10 +- tests/integration/test_konfai_resume.py | 22 +- .../test_konfai_streamed_evaluation.py | 11 +- .../test_konfai_streamed_prediction.py | 10 +- tests/unit/conftest.py | 10 + tests/unit/oracle_support.py | 153 ++++- tests/unit/test_streamed_oracle.py | 615 ------------------ .../test_streamed_oracle_decomposition.py | 94 +++ .../test_streamed_oracle_dtype_reduction.py | 176 +++++ tests/unit/test_streamed_oracle_expansion.py | 205 ++++++ tests/unit/test_streamed_oracle_geometry.py | 115 ++++ .../test_transform_materialize_contract.py | 4 +- tests/unit/test_write_pyramid.py | 4 + 26 files changed, 949 insertions(+), 800 deletions(-) create mode 100644 konfai-mcp/setup.py delete mode 100644 tests/unit/test_streamed_oracle.py create mode 100644 tests/unit/test_streamed_oracle_decomposition.py create mode 100644 tests/unit/test_streamed_oracle_dtype_reduction.py create mode 100644 tests/unit/test_streamed_oracle_expansion.py create mode 100644 tests/unit/test_streamed_oracle_geometry.py diff --git a/.github/workflows/konfai_apps_ci.yml b/.github/workflows/konfai_apps_ci.yml index 9443f9b4..a8c864f2 100644 --- a/.github/workflows/konfai_apps_ci.yml +++ b/.github/workflows/konfai_apps_ci.yml @@ -54,11 +54,15 @@ jobs: uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: ${{ matrix.python-version }} + cache: "pip" + cache-dependency-path: pyproject.toml - name: Install core package + # Linux adds the CPU torch index: the default PyPI torch wheel bundles the multi-GB CUDA + # stack a CPU-only runner never uses (macOS/Windows PyPI wheels are already CPU-only). run: | python -m pip install -U pip - pip install -e ".[dev]" + pip install -e ".[dev]" ${{ runner.os == 'Linux' && '--extra-index-url https://download.pytorch.org/whl/cpu' || '' }} - name: Restore Hugging Face cache uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 @@ -73,3 +77,17 @@ jobs: - name: Run apps tests run: | python -m pytest -q konfai-apps/tests + + - name: Install app bundles + # Editable, so each bundle's scm-derived `konfai==`/`konfai-apps==` pin resolves against + # the editable installs above (both sides derive the same version from this checkout). + shell: bash + run: | + for app in apps/*/; do pip install -e "./$app"; done + + - name: Run app bundle tests + # The five PyPI-published bundles. impact_reg's integration test gates itself + # (KONFAI_IMPACTREG_REPO + fireants + CUDA) and skips here; its unit tests stub the runtime. + shell: bash + run: | + for app in apps/*/; do python -m pytest -q "${app}tests"; done diff --git a/.github/workflows/konfai_ci.yml b/.github/workflows/konfai_ci.yml index 4f9fc6c7..eea69512 100644 --- a/.github/workflows/konfai_ci.yml +++ b/.github/workflows/konfai_ci.yml @@ -45,15 +45,20 @@ jobs: uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: ${{ matrix.python-version }} + cache: "pip" + cache-dependency-path: pyproject.toml - name: Install dependencies + # Linux adds the CPU torch index: the default PyPI torch wheel bundles the multi-GB CUDA + # stack a CPU-only runner never uses (macOS/Windows PyPI wheels are already CPU-only). run: | python -m pip install -U pip - pip install -e ".[dev]" + pip install -e ".[dev]" ${{ runner.os == 'Linux' && '--extra-index-url https://download.pytorch.org/whl/cpu' || '' }} - name: Run pytest + # Same xdist mode as the pixi `test` task; loadfile keeps per-file session fixtures on one worker. run: | - pytest -q tests + pytest -q -n auto --dist loadfile tests lint: runs-on: ubuntu-latest @@ -68,6 +73,8 @@ jobs: uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" + cache: "pip" + cache-dependency-path: pyproject.toml - name: Install ruff run: pip install ruff==0.15.2 @@ -88,6 +95,8 @@ jobs: uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" + cache: "pip" + cache-dependency-path: pyproject.toml - name: Install ruff run: pip install ruff==0.15.2 @@ -109,13 +118,15 @@ jobs: uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" + cache: "pip" + cache-dependency-path: pyproject.toml - name: Install dependencies # The dev extra carries pytest + build; setuptools/setuptools-scm/wheel are the declared # build requirements the wheel test needs for a --no-isolation build. run: | python -m pip install -U pip - pip install -e ".[dev]" setuptools setuptools-scm wheel + pip install -e ".[dev]" setuptools setuptools-scm wheel --extra-index-url https://download.pytorch.org/whl/cpu - name: Build sdist and wheel run: python -m build --sdist --wheel diff --git a/.github/workflows/konfai_mcp_ci.yml b/.github/workflows/konfai_mcp_ci.yml index cc0961c9..fca7423a 100644 --- a/.github/workflows/konfai_mcp_ci.yml +++ b/.github/workflows/konfai_mcp_ci.yml @@ -39,11 +39,17 @@ jobs: uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: ${{ matrix.python-version }} + cache: "pip" + cache-dependency-path: | + pyproject.toml + konfai-mcp/pyproject.toml - name: Install core package (with imaging backends) + # The CPU torch index: the default PyPI torch wheel bundles the multi-GB CUDA stack this + # CPU-only runner never uses. run: | python -m pip install -U pip - pip install -e ".[dev,imaging]" + pip install -e ".[dev,imaging]" --extra-index-url https://download.pytorch.org/whl/cpu - name: Install standalone apps package run: | diff --git a/.github/workflows/konfai_studio_ci.yml b/.github/workflows/konfai_studio_ci.yml index 356c2dd4..4d87dfc1 100644 --- a/.github/workflows/konfai_studio_ci.yml +++ b/.github/workflows/konfai_studio_ci.yml @@ -39,6 +39,10 @@ jobs: uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: ${{ matrix.python-version }} + cache: "pip" + cache-dependency-path: | + pyproject.toml + studio/pyproject.toml - name: Set up Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -55,9 +59,11 @@ jobs: test -f ../konfai_studio/web/index.html - name: Install core package + # The CPU torch index: the default PyPI torch wheel bundles the multi-GB CUDA stack this + # CPU-only runner never uses. run: | python -m pip install -U pip - pip install -e ".[dev]" + pip install -e ".[dev]" --extra-index-url https://download.pytorch.org/whl/cpu - name: Install standalone apps package run: | @@ -71,6 +77,18 @@ jobs: run: | pip install -e ./studio + - name: Build a throwaway studio wheel and verify it ships the front + # The wheel is the only artifact that carries web/ (git-ignored, swept in by package-data), + # so only inspecting a built wheel catches a packaging regression before release day. + run: | + python -m build --wheel studio + python -c " + import glob, sys, zipfile + names = zipfile.ZipFile(glob.glob('studio/dist/*.whl')[0]).namelist() + ok = 'konfai_studio/web/index.html' in names and any(n.startswith('konfai_studio/web/assets/') for n in names) + sys.exit(0 if ok else 'the studio wheel is missing konfai_studio/web/index.html or assets/') + " + - name: Lint and format check run: | ruff check studio/konfai_studio diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 10c26738..1b8a09d1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,11 +26,15 @@ jobs: uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" + cache: "pip" + cache-dependency-path: pyproject.toml - name: Install core package + # The CPU torch index: the default PyPI torch wheel bundles the multi-GB CUDA stack this + # CPU-only runner never uses. run: | python -m pip install -U pip - pip install -e ".[dev]" + pip install -e ".[dev]" --extra-index-url https://download.pytorch.org/whl/cpu - name: Install standalone apps package run: | @@ -42,12 +46,22 @@ jobs: - name: Run konfai tests run: | - pytest -q tests + pytest -q -n auto --dist loadfile tests - name: Run konfai-apps tests run: | python -m pytest -q konfai-apps/tests + - name: Install app bundles + run: | + for app in apps/*/; do pip install -e "./$app"; done + + - name: Run app bundle tests + # These are the five PyPI-published bundles of the build matrix below. impact_reg's + # integration test gates itself (KONFAI_IMPACTREG_REPO + fireants + CUDA) and skips here. + run: | + for app in apps/*/; do python -m pytest -q "${app}tests"; done + - name: Run konfai-mcp tests run: | python -m pytest -q konfai-mcp/tests @@ -115,6 +129,8 @@ jobs: - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.11" + cache: "pip" + cache-dependency-path: pyproject.toml - name: Set up Node if: matrix.name == 'konfai-studio' @@ -136,6 +152,18 @@ jobs: cd "${{ matrix.pkg }}" python -m build ${{ matrix.build_args }} + - name: Verify the studio wheel ships the built front + # web/ is non-Python payload swept in by package-data at build time: a setuptools or glob + # regression would publish a wheel whose UI is an empty directory, and nothing else fails. + if: matrix.name == 'konfai-studio' + run: | + python -c " + import glob, sys, zipfile + names = zipfile.ZipFile(glob.glob('studio/dist/*.whl')[0]).namelist() + ok = 'konfai_studio/web/index.html' in names and any(n.startswith('konfai_studio/web/assets/') for n in names) + sys.exit(0 if ok else 'the studio wheel is missing konfai_studio/web/index.html or assets/') + " + - name: Upload dist uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: @@ -163,6 +191,10 @@ jobs: uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 with: packages-dir: dist + # A transient failure can publish some of the nine packages and not others; every file + # here was built from the same tag in the same run, so skipping duplicates makes a + # re-run of this job complete the remaining packages instead of failing on the uploaded ones. + skip-existing: true github_release: needs: publish diff --git a/konfai-mcp/pyproject.toml b/konfai-mcp/pyproject.toml index 479a5c58..d9f3225e 100644 --- a/konfai-mcp/pyproject.toml +++ b/konfai-mcp/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "konfai-mcp" -dynamic = ["version"] +dynamic = ["version", "dependencies"] description = "Standalone MCP server package for KonfAI experimentation workflows" readme = "README.md" license = "Apache-2.0" @@ -14,21 +14,8 @@ authors = [ { name = "Valentin Boussot", email = "boussot.v@gmail.com" } ] -# konfai carries a bound because runner.py imports konfai.transformer at module scope: a 1.7.0 -# satisfies a bare name, and the server then dies on import -- all of it, not just the transform -# tools. Spelled `>1.7.0` and not `>=1.8.0` so a source checkout satisfies it: setuptools_scm builds -# the unreleased tree as 1.7.1.devN, which PEP 440 sorts BELOW 1.7.1 and above 1.7.0. -# konfai-apps carries the same bound: its imports are lazy, so an older one does not break the server on -# import, it breaks one app tool at the call with an ImportError naming a symbol instead of a version. -dependencies = [ - "konfai>1.7.0", - "konfai-apps>1.7.0", - # >= 2.10.2 for stateless streamable HTTP (stateless_http/json_response run - # kwargs and the FASTMCP_STATELESS_HTTP setting) per MCP spec rev 2025-03-26. - "fastmcp>=2.10.2", - "ruamel.yaml", - "numpy" -] +# Dependencies live in setup.py: konfai and konfai-apps are pinned to the exact scm-derived release +# version there, like every other member of the lockstep family. [project.urls] Homepage = "https://github.com/fideus-labs/KonfAI" diff --git a/konfai-mcp/setup.py b/konfai-mcp/setup.py new file mode 100644 index 00000000..f0fed3a0 --- /dev/null +++ b/konfai-mcp/setup.py @@ -0,0 +1,49 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +from email import message_from_string +from pathlib import Path + +from setuptools import setup + +_ROOT = Path(__file__).resolve().parents[1] + + +def _release_version() -> str: + pkg_info = Path(__file__).with_name("PKG-INFO") + if pkg_info.exists(): + return message_from_string(pkg_info.read_text())["Version"] + from setuptools_scm import get_version + + return get_version(root=str(_ROOT), tag_regex=r"^v(?P.*)$", local_scheme="no-local-version") + + +# konfai and konfai-apps are pinned to the exact release version: runner.py imports core internals +# (konfai.transformer, konfai.network.network) whose layout moves between minor releases, so the +# family ships in lockstep, like the apps/* bundles pin konfai and studio pins konfai-mcp. +_version = _release_version() + +setup( + install_requires=[ + f"konfai=={_version}", + f"konfai-apps=={_version}", + # >= 2.10.2 for stateless streamable HTTP (stateless_http/json_response run + # kwargs and the FASTMCP_STATELESS_HTTP setting) per MCP spec rev 2025-03-26. + "fastmcp>=2.10.2", + "ruamel.yaml", + "numpy", + ] +) diff --git a/pyproject.toml b/pyproject.toml index a83971e8..607f37c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -176,12 +176,12 @@ scikit-image = "*" [tool.pixi.feature.dev.tasks] test = { cmd = "pytest -q -n auto --dist loadfile tests/", description = "Run the test suite" } -test-fast = { cmd = 'pytest -q -m "not slow and not integration" tests/', description = "Run the test suite without slow oracle and integration tests" } +test-fast = { cmd = 'pytest -q -n auto --dist loadfile -m "not slow and not integration" tests/', description = "Run the test suite without slow oracle and integration tests" } test-apps = { cmd = "pytest -q konfai-apps/tests", description = "Run the konfai-apps test suite" } test-cov = { cmd = "pytest -n auto --dist loadfile --cov=konfai --cov-report=term-missing tests/", description = "Run tests with coverage" } -lint = { cmd = "ruff check konfai konfai-apps/konfai_apps konfai-mcp/konfai_mcp studio/konfai_studio tests", description = "Lint source code" } -format = { cmd = "ruff format konfai konfai-apps/konfai_apps konfai-mcp/konfai_mcp studio/konfai_studio tests", description = "Format source code" } -format-check = { cmd = "ruff format --check konfai konfai-apps/konfai_apps konfai-mcp/konfai_mcp studio/konfai_studio tests", description = "Check formatting without modifying files" } +lint = { cmd = "ruff check konfai konfai-apps/konfai_apps konfai-mcp/konfai_mcp studio/konfai_studio apps tests", description = "Lint source code" } +format = { cmd = "ruff format konfai konfai-apps/konfai_apps konfai-mcp/konfai_mcp studio/konfai_studio apps tests", description = "Format source code" } +format-check = { cmd = "ruff format --check konfai konfai-apps/konfai_apps konfai-mcp/konfai_mcp studio/konfai_studio apps tests", description = "Check formatting without modifying files" } typecheck = { cmd = "python -m mypy konfai konfai-apps/konfai_apps konfai-mcp/konfai_mcp studio/konfai_studio --ignore-missing-imports --no-site-packages", description = "Type-check all packages" } build = { cmd = "python -m build", description = "Build sdist and wheel" } check = { depends-on = ["lint", "format-check", "test", "test-apps"], description = "Run all quality checks" } @@ -244,6 +244,12 @@ markers = [ "slow: marks tests as slow (deselect with -m 'not slow')", "integration: marks integration tests", ] +filterwarnings = [ + # SimpleITK's GetImageFromArray/GetArrayViewFromImage assigns array.shape in place + # (SimpleITK/extra.py), which NumPy 2.5 deprecated: ~1000 copies per heavy file run bury every + # other warning. Drop this once the SimpleITK floor ships a reshape-based fix. + "ignore:Setting the shape on a NumPy array has been deprecated:DeprecationWarning:SimpleITK", +] # --------------------------------------------------------------------------- # Mypy diff --git a/tests/integration/harness.py b/tests/integration/harness.py index 5b77b66a..ed19abb1 100644 --- a/tests/integration/harness.py +++ b/tests/integration/harness.py @@ -18,7 +18,9 @@ import os import shutil +import subprocess import sys +from collections.abc import Sequence from pathlib import Path import numpy as np @@ -138,6 +140,17 @@ def subprocess_env() -> dict[str, str]: return env +def run_workflow( + cmd: Sequence[str], cwd: Path, timeout: float = 600, check: bool = True, **kwargs +) -> subprocess.CompletedProcess: + """Run one child workflow with the shared environment and a hang guard. + + The children traverse ``run_distributed_app``/``mp.spawn``, where a deadlock would otherwise + hold the CI job to the runner limit; the timeout turns it into a failure with a traceback. + """ + return subprocess.run(list(cmd), cwd=cwd, env=subprocess_env(), check=check, timeout=timeout, **kwargs) + + def konfai_cli_command() -> list[str]: cli = shutil.which("konfai") if cli is not None: diff --git a/tests/integration/test_konfai_auto_patch_prediction.py b/tests/integration/test_konfai_auto_patch_prediction.py index 28de41e5..a33f4411 100644 --- a/tests/integration/test_konfai_auto_patch_prediction.py +++ b/tests/integration/test_konfai_auto_patch_prediction.py @@ -19,13 +19,12 @@ whole-volume run. The model is pointwise (1x1 conv) and the overlap 0, so patched == whole holds exactly and any grid/mapping/accumulation mistake shows up as a voxel difference.""" -import subprocess import sys from pathlib import Path import numpy as np import pytest -from harness import prepare_experiment_dir, replace_once, subprocess_env +from harness import prepare_experiment_dir, replace_once, run_workflow pytestmark = pytest.mark.integration @@ -123,12 +122,7 @@ def auto_patch_experiment(tmp_path_factory: pytest.TempPathFactory) -> dict[str, runner_path = experiment_dir / "run_auto_patch_prediction.py" runner_path.write_text(RUNNER_SOURCE.replace("__TRAIN_NAME__", TRAIN_NAME), encoding="utf-8") - subprocess.run( - [sys.executable, str(runner_path)], - cwd=experiment_dir, - env=subprocess_env(), - check=True, - ) + run_workflow([sys.executable, str(runner_path)], experiment_dir) return { "dataset_dir": paths["dataset_dir"], "reference": experiment_dir / "Predictions_reference", diff --git a/tests/integration/test_konfai_auto_patch_training.py b/tests/integration/test_konfai_auto_patch_training.py index 325f5226..5c95facc 100644 --- a/tests/integration/test_konfai_auto_patch_training.py +++ b/tests/integration/test_konfai_auto_patch_training.py @@ -18,12 +18,11 @@ must shrink the free axes through the rank rendezvous, re-plan the grid, restart the run, and train to completion (checkpoints produced).""" -import subprocess import sys from pathlib import Path import pytest -from harness import prepare_experiment_dir, replace_once, subprocess_env +from harness import prepare_experiment_dir, replace_once, run_workflow pytestmark = pytest.mark.integration @@ -100,9 +99,4 @@ def test_auto_patch_training_restarts_and_completes(tmp_path: Path) -> None: runner_path = experiment_dir / "run_auto_patch_training.py" runner_path.write_text(RUNNER_SOURCE.replace("__TRAIN_NAME__", TRAIN_NAME), encoding="utf-8") - subprocess.run( - [sys.executable, str(runner_path)], - cwd=experiment_dir, - env=subprocess_env(), - check=True, - ) + run_workflow([sys.executable, str(runner_path)], experiment_dir) diff --git a/tests/integration/test_konfai_chain_check.py b/tests/integration/test_konfai_chain_check.py index 33e98bb7..90310cbb 100644 --- a/tests/integration/test_konfai_chain_check.py +++ b/tests/integration/test_konfai_chain_check.py @@ -18,12 +18,11 @@ chain it applies to a model input is not that one. The unit suite pins the comparison; this pins the wiring: the check runs on the checkpoint TRAIN actually wrote, and its lines reach the run's log.""" -import subprocess import sys from pathlib import Path import pytest -from harness import prepare_experiment_dir, replace_once, subprocess_env +from harness import prepare_experiment_dir, replace_once, run_workflow pytestmark = pytest.mark.integration @@ -91,12 +90,7 @@ def test_prediction_warns_in_its_log_when_a_model_input_drifts_from_training(tmp ) (experiment_dir / "run_chain_check.py").write_text(RUNNER_SOURCE, encoding="utf-8") - subprocess.run( - [sys.executable, "run_chain_check.py"], - cwd=experiment_dir, - env=subprocess_env(), - check=True, - ) + run_workflow([sys.executable, "run_chain_check.py"], experiment_dir) matching = (experiment_dir / "Predictions_Matching" / TRAIN_NAME / "log_0.txt").read_text(encoding="utf-8") mismatched = (experiment_dir / "Predictions_Mismatched" / TRAIN_NAME / "log_0.txt").read_text(encoding="utf-8") diff --git a/tests/integration/test_konfai_core_workflows.py b/tests/integration/test_konfai_core_workflows.py index 1136c4bc..35ff2e50 100644 --- a/tests/integration/test_konfai_core_workflows.py +++ b/tests/integration/test_konfai_core_workflows.py @@ -16,15 +16,13 @@ import json import os -import subprocess import sys -import textwrap from contextlib import contextmanager from pathlib import Path import numpy as np import pytest -from harness import konfai_cli_command, prepare_experiment_dir, subprocess_env, write_image +from harness import konfai_cli_command, prepare_experiment_dir, run_workflow, write_image from konfai.evaluator import build_evaluate from konfai.predictor import build_predict from konfai.trainer import build_train @@ -81,85 +79,13 @@ def _assert_experiment_outputs( assert all(np.isfinite(value) for value in case_values.values()), metric_name -def test_konfai_api_user_path(tmp_path: Path) -> None: - experiment_dir = tmp_path / "experiment_api" - train_name = "API" - paths = prepare_experiment_dir(experiment_dir, train_name) - - runner_path = experiment_dir / "run_api_workflow.py" - runner_path.write_text( - textwrap.dedent( - """ - from pathlib import Path - from konfai.evaluator import evaluate - from konfai.predictor import predict - from konfai.trainer import train - - def main() -> None: - root = Path.cwd() - train( - overwrite=True, - gpu=[], - cpu=1, - quiet=True, - tensorboard=False, - config=root / "Config.yml", - checkpoints_dir=root / "Checkpoints", - statistics_dir=root / "Statistics", - ) - checkpoints = sorted((root / "Checkpoints" / "__TRAIN_NAME__").glob("*.pt")) - if not checkpoints: - raise RuntimeError("no checkpoints produced") - predict( - models=checkpoints, - overwrite=True, - gpu=[], - cpu=1, - quiet=True, - tb=False, - prediction_file=root / "Prediction.yml", - predictions_dir=root / "Predictions", - ) - evaluate( - overwrite=True, - gpu=[], - cpu=1, - quiet=True, - tb=False, - evaluations_file=root / "Evaluation.yml", - evaluations_dir=root / "Evaluations", - ) - - - if __name__ == "__main__": - main() - """.replace("__TRAIN_NAME__", train_name) - ), - encoding="utf-8", - ) - - subprocess.run( - [sys.executable, str(runner_path)], - cwd=experiment_dir, - env=subprocess_env(), - check=True, - ) - _assert_experiment_outputs( - paths["dataset_dir"], - paths["checkpoints_dir"], - paths["predictions_dir"], - paths["evaluations_dir"], - train_name, - ) - - def test_konfai_cli_user_path(tmp_path: Path) -> None: experiment_dir = tmp_path / "experiment_cli" train_name = "CLI" paths = prepare_experiment_dir(experiment_dir, train_name) cli = konfai_cli_command() - subprocess.run( + run_workflow( [ *cli, "TRAIN", @@ -174,14 +100,12 @@ def test_konfai_cli_user_path(tmp_path: Path) -> None: "--statistics-dir", "Statistics", ], - cwd=experiment_dir, - env=subprocess_env(), - check=True, + experiment_dir, ) checkpoints = sorted((paths["checkpoints_dir"] / train_name).glob("*.pt")) assert checkpoints - subprocess.run( + run_workflow( [ *cli, "PREDICTION", @@ -196,11 +120,9 @@ def test_konfai_cli_user_path(tmp_path: Path) -> None: "--predictions-dir", "Predictions", ], - cwd=experiment_dir, - env=subprocess_env(), - check=True, + experiment_dir, ) - subprocess.run( + run_workflow( [ *cli, "EVALUATION", @@ -213,9 +135,7 @@ def test_konfai_cli_user_path(tmp_path: Path) -> None: "--evaluations-dir", "Evaluations", ], - cwd=experiment_dir, - env=subprocess_env(), - check=True, + experiment_dir, ) _assert_experiment_outputs( paths["dataset_dir"], diff --git a/tests/integration/test_konfai_ensemble_tta.py b/tests/integration/test_konfai_ensemble_tta.py index ba8e7b66..a0adab6d 100644 --- a/tests/integration/test_konfai_ensemble_tta.py +++ b/tests/integration/test_konfai_ensemble_tta.py @@ -28,13 +28,12 @@ with the ``Sum`` transform must yield ``A + B`` (one term per TTA branch). """ -import subprocess import sys from pathlib import Path import numpy as np import pytest -from harness import TTA_AUGMENTATIONS_BLOCK, prepare_experiment_dir, replace_once, subprocess_env +from harness import TTA_AUGMENTATIONS_BLOCK, prepare_experiment_dir, replace_once, run_workflow pytestmark = pytest.mark.integration @@ -139,12 +138,7 @@ def ensemble_experiment(tmp_path_factory: pytest.TempPathFactory) -> dict[str, P runner_path = experiment_dir / "run_ensemble_tta.py" runner_path.write_text(RUNNER_SOURCE.replace("__TRAIN_NAME__", TRAIN_NAME), encoding="utf-8") - subprocess.run( - [sys.executable, str(runner_path)], - cwd=experiment_dir, - env=subprocess_env(), - check=True, - ) + run_workflow([sys.executable, str(runner_path)], experiment_dir) return { "experiment_dir": experiment_dir, "dataset_dir": paths["dataset_dir"], diff --git a/tests/integration/test_konfai_resume.py b/tests/integration/test_konfai_resume.py index 268ec73e..015be7c9 100644 --- a/tests/integration/test_konfai_resume.py +++ b/tests/integration/test_konfai_resume.py @@ -24,14 +24,13 @@ resumed model still predicts finite values through the PREDICTION workflow. """ -import subprocess from pathlib import Path from typing import Any import numpy as np import pytest import torch -from harness import konfai_cli_command, prepare_experiment_dir, replace_once, subprocess_env +from harness import konfai_cli_command, prepare_experiment_dir, replace_once, run_workflow pytestmark = pytest.mark.integration @@ -95,12 +94,7 @@ def test_konfai_cli_resume_continues_training(tmp_path: Path) -> None: ) cli = konfai_cli_command() - subprocess.run( - [*cli, "TRAIN", "-y", "--cpu", "1", "-q", "-c", "Config.yml"], - cwd=experiment_dir, - env=subprocess_env(), - check=True, - ) + run_workflow([*cli, "TRAIN", "-y", "--cpu", "1", "-q", "-c", "Config.yml"], experiment_dir) checkpoints_dir = paths["checkpoints_dir"] / train_name initial_checkpoints = _read_checkpoints(checkpoints_dir) @@ -112,11 +106,9 @@ def test_konfai_cli_resume_continues_training(tmp_path: Path) -> None: assert it_end >= EPOCHS_INITIAL and it_end % EPOCHS_INITIAL == 0 its_per_epoch = it_end // EPOCHS_INITIAL - subprocess.run( + run_workflow( [*cli, "RESUME", "-y", "--cpu", "1", "-q", "-c", "ConfigResume.yml", "--model", str(last_checkpoint)], - cwd=experiment_dir, - env=subprocess_env(), - check=True, + experiment_dir, ) final_checkpoints = _read_checkpoints(checkpoints_dir) @@ -154,11 +146,9 @@ def test_konfai_cli_resume_continues_training(tmp_path: Path) -> None: assert any((weights_after[name] - weights_before[name]).abs().max().item() > 0 for name in float_names) # The resumed model is still usable end-to-end and predicts finite values. - subprocess.run( + run_workflow( [*cli, "PREDICTION", "-y", "--cpu", "1", "-q", "-c", "Prediction.yml", "--models", str(final_checkpoint)], - cwd=experiment_dir, - env=subprocess_env(), - check=True, + experiment_dir, ) expected_cases = sorted(path.name for path in paths["dataset_dir"].iterdir() if path.is_dir()) predicted = sorted((experiment_dir / "Predictions" / train_name / "Dataset").rglob("sCT.mha")) diff --git a/tests/integration/test_konfai_streamed_evaluation.py b/tests/integration/test_konfai_streamed_evaluation.py index 0f6065cc..efb117a1 100644 --- a/tests/integration/test_konfai_streamed_evaluation.py +++ b/tests/integration/test_konfai_streamed_evaluation.py @@ -23,7 +23,6 @@ """ import json -import subprocess import sys import textwrap from pathlib import Path @@ -33,7 +32,7 @@ SimpleITK = pytest.importorskip("SimpleITK") -from harness import subprocess_env, write_image # noqa: E402 (the harness imports SimpleITK itself) +from harness import run_workflow, write_image # noqa: E402 (the harness imports SimpleITK itself) pytestmark = pytest.mark.integration @@ -137,13 +136,7 @@ def main() -> None: ), encoding="utf-8", ) - completed = subprocess.run( - [sys.executable, str(runner)], - cwd=experiment_dir, - env=subprocess_env(), - capture_output=True, - text=True, - ) + completed = run_workflow([sys.executable, str(runner)], experiment_dir, check=False, capture_output=True, text=True) assert completed.returncode == 0, f"evaluation failed:\n{completed.stdout}\n{completed.stderr}" return completed.stdout + completed.stderr diff --git a/tests/integration/test_konfai_streamed_prediction.py b/tests/integration/test_konfai_streamed_prediction.py index 0448cacb..6ac47018 100644 --- a/tests/integration/test_konfai_streamed_prediction.py +++ b/tests/integration/test_konfai_streamed_prediction.py @@ -28,13 +28,12 @@ streams (each copy's window reduced slab by slab), while a slab-axis flip must refuse and complete whole-volume. Every variant is compared voxel for voxel against its own kill-switch reference.""" -import subprocess import sys from pathlib import Path import numpy as np import pytest -from harness import TTA_AUGMENTATIONS_BLOCK, prepare_experiment_dir, replace_once, subprocess_env +from harness import TTA_AUGMENTATIONS_BLOCK, prepare_experiment_dir, replace_once, run_workflow pytestmark = pytest.mark.integration @@ -220,12 +219,7 @@ def streamed_experiment(tmp_path_factory: pytest.TempPathFactory) -> dict[str, P runner_path = experiment_dir / "run_streamed_prediction.py" runner_path.write_text(RUNNER_SOURCE.replace("__TRAIN_NAME__", TRAIN_NAME), encoding="utf-8") - subprocess.run( - [sys.executable, str(runner_path)], - cwd=experiment_dir, - env=subprocess_env(), - check=True, - ) + run_workflow([sys.executable, str(runner_path)], experiment_dir) return { "dataset_dir": paths["dataset_dir"], "experiment_dir": experiment_dir, diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 65470c77..9c36bccb 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -111,6 +111,16 @@ def streaming_dataset_stub() -> type[StreamingDatasetStub]: return StreamingDatasetStub +@pytest.fixture(scope="session") +def oracle_cases(tmp_path_factory: pytest.TempPathFactory) -> dict[str, Dataset]: + """One case on disk per oracle geometry: built once, read by every streamed-oracle row that + runs on it (the ``test_streamed_oracle_*`` family).""" + from oracle_support import GEOMETRIES, build_case + + root = tmp_path_factory.mktemp("oracle") + return {name: build_case(root / name, geometry) for name, geometry in GEOMETRIES.items()} + + _TTA_SHAPE = [6, 4, 3] _TTA_PATCH_SIZE = [2, 4, 3] _TTA_OVERLAP = 1 diff --git a/tests/unit/oracle_support.py b/tests/unit/oracle_support.py index 42c4b4d8..5d9cd728 100644 --- a/tests/unit/oracle_support.py +++ b/tests/unit/oracle_support.py @@ -18,7 +18,7 @@ A stage must behave as if it had seen the whole volume: proven on the READ side, patch by patch (``test_transform_locality_contract``), and on the WRITE side, region by region over dtype, rank and -budget (``test_streamed_oracle``). Both need the same two things, so both read them here: the +budget (the ``test_streamed_oracle_*`` family). Both need the same two things, so both read them here: the enumeration of the built-ins with one representative configuration each, and a case on disk holding one volume per input kind those configurations consume. @@ -29,17 +29,19 @@ """ import inspect -from collections.abc import Sequence +from collections.abc import Callable, Sequence from dataclasses import dataclass from pathlib import Path import numpy as np +import pytest import torch from konfai.data import augmentation as augmentation_module from konfai.data import transform as transform_module from konfai.data.augmentation import DataAugmentation from konfai.data.augmentation import Flip as FlipAugmentation -from konfai.data.patching import DatasetManager, DatasetPatch +from konfai.data.materialize import CaseMaterializer, Verdict +from konfai.data.patching import DatasetManager, DatasetPatch, SweepSegment from konfai.data.transform import ( Argmax, Canonical, @@ -73,7 +75,7 @@ Write, ) from konfai.utils.dataset import Attribute, Dataset -from konfai.utils.errors import TransformError +from konfai.utils.errors import DatasetManagerError, TransformError CASE_NAME = "CASE_000" @@ -748,3 +750,146 @@ def builtin_augmentations() -> list[type[DataAugmentation]]: and cls.__module__ == augmentation_module.__name__ and not inspect.isabstract(cls) ] + + +# -------------------------------------------------------------------------------------- +# The write-side sweep vocabulary of the streamed-oracle family (test_streamed_oracle_*): +# one property, one axis per file, and here what every file shares: the same case driven +# region by region (as the budget cuts it) and whole, then compared. +# -------------------------------------------------------------------------------------- + +#: The geometries the oracle matrix runs on, drawn from a FIXED seed list rather than per run: a +#: property that fails only on Tuesday's seed is not a property. Extents land in 16..40, which keeps +#: the family inside its time budget while leaving every axis room for several regions. +GEOMETRIES = { + "rank3-seed11": seeded_geometry(11, 3), + "rank3-seed23": seeded_geometry(23, 3), + "rank2-seed37": seeded_geometry(37, 2), +} +#: The one the tests that vary something OTHER than the geometry run on. +MAIN = "rank3-seed11" + + +@dataclass(frozen=True) +class Route: + """How the sweep is made to cut the case, as the height one region spans of its first axis.""" + + name: str + #: Rows per region, as a fraction of the case's first extent. ``None`` leaves the sweep its own + #: cap, which covers these extents whole. + height: float | None + + +#: One region, a handful, and one row each: the three decompositions of the same case. +ROUTES = (Route("one-region", None), Route("few-regions", 0.25), Route("row-regions", 0.0)) + + +def budget_for(manager: DatasetManager, route: Route) -> float | None: + """The smallest per-rank budget under which the sweep cuts regions of ``route``'s height. + + Found by bisecting the production sizing rule rather than by restating it: the test says how + tall a region should be, and ``_sweep_tile`` says what budget buys it. Asked with the landing + and the pull maps the sweep itself will use, because that is what the budget is spent on. + """ + if route.height is None: + return None + # Every copy the run will sweep, each with the landing and the pull maps of its own chain: a + # draw that samples through an affine pulls more than the shared prefix, and the budget the + # matrix asks for is the one that buys the height on all of them. + augmented = manager._expand is not None + copies = [0] if not augmented else list(range(1, int(manager._expand.nb) + 1)) + segments = {a: manager.sweep_segments(a, augmented) or [] for a in copies} + rows = max(1, int(route.height * int(manager.shapes[copies[0]][0]))) + low, high = 1.0, float(2**48) + for _ in range(64): + middle = (low + high) / 2 + manager.set_memory_budget(middle) + if min(_sweep_height(manager, sweeps) for sweeps in segments.values()) < rows: + low = middle + else: + high = middle + manager.set_memory_budget(None) + return high + + +def _sweep_height(manager: DatasetManager, segments: Sequence[SweepSegment]) -> int: + """The shortest region the sizing buys these segments under the budget the manager currently + carries; zero where one of them does not fit, which is below every height the routes ask for.""" + heights = [] + for segment in segments: + try: + heights.append(manager._sweep_tile(segment.landing, segment.channels, segment.plans)[0]) + except DatasetManagerError: + return 0 + return min(heights, default=0) + + +@dataclass(frozen=True) +class Written: + """One materialization's result: what landed, how it landed, and in how many pieces.""" + + array: np.ndarray + attribute: Attribute + verdict: Verdict + regions: int + + +def count_regions(monkeypatch: pytest.MonkeyPatch) -> Callable[[], int]: + """Count the regions a sweep reads, so a row cannot pass by never having been decomposed.""" + read = DatasetManager._read_streamed_region + counted = [0] + + def counting(self, *args, **kwargs): + counted[0] += 1 + return read(self, *args, **kwargs) + + monkeypatch.setattr(DatasetManager, "_read_streamed_region", counting) + return lambda: counted[0] + + +def sweep( + dataset: Dataset, group: str, stage: Transform, destination: Path, route: Route, monkeypatch: pytest.MonkeyPatch +) -> Written: + """Write ``stage`` over the case region by region, cut as ``route`` says, and read back what landed.""" + stage.set_datasets([dataset]) + case_manager = manager(dataset, [stage, Save(f"{destination}:h5")], group=group) + budget = budget_for(case_manager, route) + with monkeypatch.context() as context: + regions = count_regions(context) + verdict = CaseMaterializer(case_manager).materialize(fallback_budget_bytes=budget) + array, attribute = Dataset(destination, "h5").read_data(group, CASE_NAME) + return Written(array, attribute, verdict, regions()) + + +def whole_volume(dataset: Dataset, group: str, stage: Transform, destination: Path) -> Written: + """The reference: the same chain over the assembled case, which is what streaming must reproduce.""" + stage.set_datasets([dataset]) + case_manager = manager(dataset, [stage, Save(f"{destination}:h5")], group=group) + CaseMaterializer(case_manager)._assemble_and_write(0) + case_manager.unload() + array, attribute = Dataset(destination, "h5").read_data(group, CASE_NAME) + return Written(array, attribute, Verdict.WHOLE_VOLUME, 0) + + +def assert_same(got: Written, want: Written, atol: float, rtol: float = 0.0) -> None: + """Same voxels within the stated bound, same dtype, same geometry: streaming is invisible.""" + assert got.array.shape == want.array.shape + assert got.array.dtype == want.array.dtype + np.testing.assert_allclose(got.array, want.array, rtol=rtol, atol=atol) + for key in ("Origin", "Spacing", "Direction"): + np.testing.assert_allclose(got.attribute.get_np_array(key), want.attribute.get_np_array(key), rtol=0, atol=0) + + +def oracle_matrix(geometries: Sequence[str]) -> list[tuple[str, StageCase, Route]]: + """Every (geometry, built-in, decomposition) the property is proven on, for these geometries.""" + return [ + (geometry, case, route) + for geometry in geometries + for case in streamable_cases(GEOMETRIES[geometry]) + for route in ROUTES + ] + + +def identify(entry: tuple[str, StageCase, Route]) -> str: + geometry, case, route = entry + return f"{type(case.transform).__name__}-{case.group}-{geometry}-{route.name}" diff --git a/tests/unit/test_streamed_oracle.py b/tests/unit/test_streamed_oracle.py deleted file mode 100644 index 02953a74..00000000 --- a/tests/unit/test_streamed_oracle.py +++ /dev/null @@ -1,615 +0,0 @@ -# Copyright (c) 2025 Valentin Boussot -# -# 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. -# -# SPDX-License-Identifier: Apache-2.0 - -"""What a sweep writes region by region is what the whole-volume pass writes, over the whole matrix. - -One property, three files, one axis each, over the registry in ``oracle_support``: -``test_transform_locality_contract`` reads it patch by patch, ``test_transform_materialize_contract`` -writes it through every storage backend, and this one varies what decides how a case is CUT and what -arithmetic runs on it: - -* the DECOMPOSITION: the budget cuts the case into one region, a few, and one row each, which is the - axis a wrong region boundary shows on and the one no fixed slab height exercises; -* the RANK: a 2-D case and a 3-D one, from the same table of configurations; -* the GEOMETRY: seeded extents, anisotropic spacings, oblique and axis-permuting cosines; -* the DTYPE: what a store actually holds, refusals included; - -and the two cardinality changes the workflow owns: N cases folded into one (``Reduce``) and one case -expanded into copies (``Expand``). - -Bounds. Byte-identical is the contract everywhere except where an interpolation legitimately rounds -differently, and each such case carries the bound the locality contract measured and states its -reason (``oracle_support.REGRID_ATOL`` for a map that does not factorise, ``LSB_ATOL`` where an -integer store quantizes that difference, ``STAT_ATOL`` for a statistic seeded from the store rather -than recomputed). Nothing here relaxes a bound of its own. - -Vacuity. A fallback that quietly writes the whole volume would satisfy "the bytes agree" and prove -nothing, so every row asserts the ROUTE it took as well, and a decomposed row asserts that it really -was decomposed. What cannot take a route says why, in a refusal of its own. - -One file because it is one property: the same two routes compared, with the axes as parameters and -the vocabulary they share (the decompositions, the driver, the bounds) declared once at the top. -""" - -from collections.abc import Callable, Sequence -from dataclasses import dataclass -from pathlib import Path - -import numpy as np -import pytest -import torch -from konfai.data.augmentation import CutOUT, DataAugmentation, Elastix, Noise, Rotate, Scale -from konfai.data.augmentation import Flip as FlipDraw -from konfai.data.case_reduction import CaseReduction -from konfai.data.materialize import CaseMaterializer, Regime, Verdict -from konfai.data.patching import DatasetManager, SweepSegment -from konfai.data.transform import ( - Clip, - Crop, - Expand, - Flip, - Gradient, - Mask, - Reduce, - Resample, - Save, - Transform, - Write, -) -from konfai.utils.dataset import Attribute, Dataset -from konfai.utils.errors import DatasetManagerError, PatchError, TransformError -from oracle_support import ( - AUGMENTATION_ATOL, - CASE_NAME, - FIXED_GEOMETRY, - LSB_ATOL, - Geometry, - StageCase, - attributes, - build_case, - manager, - seeded_geometry, - streamable_cases, - volumes, -) - -pytest.importorskip("SimpleITK") - -#: The geometries the matrix runs on, drawn from a FIXED seed list rather than per run: a property -#: that fails only on Tuesday's seed is not a property. Extents land in 16..40, which keeps the file -#: inside its time budget while leaving every axis room for several regions. -GEOMETRIES = { - "rank3-seed11": seeded_geometry(11, 3), - "rank3-seed23": seeded_geometry(23, 3), - "rank2-seed37": seeded_geometry(37, 2), -} -#: The one the tests that vary something OTHER than the geometry run on. -MAIN = "rank3-seed11" - - -@dataclass(frozen=True) -class Route: - """How the sweep is made to cut the case, as the height one region spans of its first axis.""" - - name: str - #: Rows per region, as a fraction of the case's first extent. ``None`` leaves the sweep its own - #: cap, which covers these extents whole. - height: float | None - - -#: One region, a handful, and one row each: the three decompositions of the same case. -ROUTES = (Route("one-region", None), Route("few-regions", 0.25), Route("row-regions", 0.0)) - - -def _budget_for(manager: DatasetManager, route: Route) -> float | None: - """The smallest per-rank budget under which the sweep cuts regions of ``route``'s height. - - Found by bisecting the production sizing rule rather than by restating it: the test says how - tall a region should be, and ``_sweep_tile`` says what budget buys it. Asked with the landing - and the pull maps the sweep itself will use, because that is what the budget is spent on. - """ - if route.height is None: - return None - # Every copy the run will sweep, each with the landing and the pull maps of its own chain: a - # draw that samples through an affine pulls more than the shared prefix, and the budget the - # matrix asks for is the one that buys the height on all of them. - augmented = manager._expand is not None - copies = [0] if not augmented else list(range(1, int(manager._expand.nb) + 1)) - segments = {a: manager.sweep_segments(a, augmented) or [] for a in copies} - rows = max(1, int(route.height * int(manager.shapes[copies[0]][0]))) - low, high = 1.0, float(2**48) - for _ in range(64): - middle = (low + high) / 2 - manager.set_memory_budget(middle) - if min(_sweep_height(manager, sweeps) for sweeps in segments.values()) < rows: - low = middle - else: - high = middle - manager.set_memory_budget(None) - return high - - -def _sweep_height(manager: DatasetManager, segments: Sequence[SweepSegment]) -> int: - """The shortest region the sizing buys these segments under the budget the manager currently - carries; zero where one of them does not fit, which is below every height the routes ask for.""" - heights = [] - for segment in segments: - try: - heights.append(manager._sweep_tile(segment.landing, segment.channels, segment.plans)[0]) - except DatasetManagerError: - return 0 - return min(heights, default=0) - - -# ---------------------------------------------------------------- driving one case both ways - - -def _manager(dataset: Dataset, group: str, chain: list[Transform], name: str = CASE_NAME) -> DatasetManager: - return manager(dataset, chain, group=group, name=name) - - -@dataclass(frozen=True) -class Written: - """One materialization's result: what landed, how it landed, and in how many pieces.""" - - array: np.ndarray - attribute: Attribute - verdict: Verdict - regions: int - - -def _count_regions(monkeypatch: pytest.MonkeyPatch) -> Callable[[], int]: - """Count the regions a sweep reads, so a row cannot pass by never having been decomposed.""" - read = DatasetManager._read_streamed_region - counted = [0] - - def counting(self, *args, **kwargs): - counted[0] += 1 - return read(self, *args, **kwargs) - - monkeypatch.setattr(DatasetManager, "_read_streamed_region", counting) - return lambda: counted[0] - - -def _sweep( - dataset: Dataset, group: str, stage: Transform, destination: Path, route: Route, monkeypatch: pytest.MonkeyPatch -) -> Written: - """Write ``stage`` over the case region by region, cut as ``route`` says, and read back what landed.""" - stage.set_datasets([dataset]) - manager = _manager(dataset, group, [stage, Save(f"{destination}:h5")]) - budget = _budget_for(manager, route) - with monkeypatch.context() as context: - regions = _count_regions(context) - verdict = CaseMaterializer(manager).materialize(fallback_budget_bytes=budget) - array, attribute = Dataset(destination, "h5").read_data(group, CASE_NAME) - return Written(array, attribute, verdict, regions()) - - -def _whole_volume(dataset: Dataset, group: str, stage: Transform, destination: Path) -> Written: - """The reference: the same chain over the assembled case, which is what streaming must reproduce.""" - stage.set_datasets([dataset]) - manager = _manager(dataset, group, [stage, Save(f"{destination}:h5")]) - CaseMaterializer(manager)._assemble_and_write(0) - manager.unload() - array, attribute = Dataset(destination, "h5").read_data(group, CASE_NAME) - return Written(array, attribute, Verdict.WHOLE_VOLUME, 0) - - -def _assert_same(got: Written, want: Written, atol: float, rtol: float = 0.0) -> None: - """Same voxels within the stated bound, same dtype, same geometry: streaming is invisible.""" - assert got.array.shape == want.array.shape - assert got.array.dtype == want.array.dtype - np.testing.assert_allclose(got.array, want.array, rtol=rtol, atol=atol) - for key in ("Origin", "Spacing", "Direction"): - np.testing.assert_allclose(got.attribute.get_np_array(key), want.attribute.get_np_array(key), rtol=0, atol=0) - - -# ---------------------------------------------------------------- the cases the matrix runs - - -def _matrix() -> list[tuple[str, StageCase, Route]]: - """Every (geometry, built-in, decomposition) the property is proven on.""" - return [ - (geometry, case, route) - for geometry in GEOMETRIES - for case in streamable_cases(GEOMETRIES[geometry]) - for route in ROUTES - ] - - -def _identify(entry: tuple[str, StageCase, Route]) -> str: - geometry, case, route = entry - return f"{type(case.transform).__name__}-{case.group}-{geometry}-{route.name}" - - -@pytest.fixture(scope="session") -def cases(tmp_path_factory: pytest.TempPathFactory) -> dict[str, Dataset]: - """One case on disk per geometry: built once, read by every row that runs on it.""" - root = tmp_path_factory.mktemp("oracle") - return {name: build_case(root / name, geometry) for name, geometry in GEOMETRIES.items()} - - -# ---------------------------------------------------------------- what the fixture itself claims - - -@pytest.mark.parametrize("geometry", [*GEOMETRIES.values(), FIXED_GEOMETRY], ids=[*GEOMETRIES, "fixed"]) -def test_a_geometry_carries_what_the_property_leans_on(geometry: Geometry) -> None: - """The fixture's own claims, since every row of every contract file assumes them. - - Both directions are orthonormal (a stored volume has no other kind) and the permuting one really - permutes, so reorienting a case stored on it transposes extents and moves the grid the patches - are cut on. The reference grid starts inside the case and reaches past it on some axis: one - contained in its case would prove the sampler and never the boundary, which is the half that - differs between the streamed and the whole-volume routes. - """ - identity = np.eye(geometry.rank) - for direction in (geometry.oblique, geometry.permuting): - np.testing.assert_allclose(direction @ direction.T, identity, rtol=0, atol=1e-12) - assert not np.array_equal(geometry.permuting, identity) - - def world(extents: tuple[int, ...], spacing: tuple[float, ...]) -> np.ndarray: - return np.asarray(extents, dtype=np.float64)[::-1] * np.asarray(spacing) - - case = world(geometry.extents, geometry.spacing) - reference = world(geometry.reference_extents, geometry.reference_spacing) - start = np.asarray(geometry.reference_origin) - np.asarray(geometry.origin) - assert (start > 0).all(), "the reference grid starts outside the case" - assert (start + reference > case).any(), "the reference grid is nested inside the case" - - -# ---------------------------------------------------------------- the property - - -@pytest.mark.parametrize("entry", _matrix(), ids=_identify) -def test_a_swept_case_equals_the_whole_volume_case( - entry: tuple[str, StageCase, Route], - cases: dict[str, Dataset], - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """The matrix: one built-in, one geometry, one decomposition, both routes, the same bytes. - - The route is asserted before the values, so a stage that quietly stopped streaming fails here - rather than passing on a whole-volume comparison with itself. - """ - geometry, case, route = entry - dataset = cases[geometry] - streamed = _sweep(dataset, case.group, case.transform, tmp_path / "streamed", route, monkeypatch) - whole = _whole_volume(dataset, case.group, case.transform, tmp_path / "whole") - - assert streamed.verdict is Verdict.STREAM - # One row per region on a case of 16 rows or more is at least two regions: without this the row - # would pass on a sweep that never decomposed anything. - assert streamed.regions >= (2 if route.height == 0.0 else 1) - _assert_same(streamed, whole, case.atol, case.rtol) - - -# ---------------------------------------------------------------- the dtypes a store holds - - -def _dtype_cases() -> list[StageCase]: - """One stage per read-streamable kind, each a remap a store of ANY dtype can carry. - - The dtype axis must vary the dtype and nothing else, so a stage whose configuration only makes - sense in one numeric range (a clip at fixed Hounsfield bounds, a fill value no unsigned store - holds) would confound the two. - """ - return [ - StageCase(Flip("0")), # ORIENTATION - StageCase(Mask(path="Labels", value_outside=0)), # POINTWISE, reading a companion - StageCase(Gradient()), # HALO - StageCase(Resample(spacing=[2.0, 1.0, 3.0]), atol=LSB_ATOL), # REGRID - StageCase(Crop(), group="Boxed"), # CROP - StageCase(Clip("min", "max")), # GLOBAL_STAT - ] - - -#: What torch has kernels for, of what a store legitimately holds. ``uint16`` is what microscopy -#: writes and torch implements neither comparison nor arithmetic for it: its refusal is pinned -#: below rather than tolerated here, and ``bool`` never reaches a chain because the store refuses to -#: hold it. -DTYPES = (np.uint8, np.int16, np.int32, np.float32, np.float64) - - -@pytest.mark.parametrize("dtype", DTYPES, ids=lambda dtype: np.dtype(dtype).name) -@pytest.mark.parametrize("case", _dtype_cases(), ids=lambda case: type(case.transform).__name__) -@pytest.mark.parametrize("route", ROUTES, ids=lambda route: route.name) -def test_a_swept_case_equals_the_whole_volume_case_on_every_dtype( - case: StageCase, dtype: np.dtype, route: Route, tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """A dtype is not a detail of the storage: it decides whether the chain rounds, and where. - - An integer store quantizes an interpolation, so the two routes may land a least significant bit - apart (``LSB_ATOL``); every other stage here is an exact remap and must be byte-identical. - """ - dataset = build_case(tmp_path / "case", GEOMETRIES[MAIN], np.dtype(dtype)) - streamed = _sweep(dataset, case.group, case.transform, tmp_path / "streamed", route, monkeypatch) - whole = _whole_volume(dataset, case.group, case.transform, tmp_path / "whole") - - assert streamed.verdict is Verdict.STREAM - # The store's dtype survives a remap. Gradient hands back differences, which an integer store - # cannot hold: it widens those to float32 and leaves a floating dtype as it found it. - expected = np.dtype(dtype) - if isinstance(case.transform, Gradient) and not np.issubdtype(expected, np.floating): - expected = np.dtype(np.float32) - assert streamed.array.dtype == expected - _assert_same(streamed, whole, case.atol, case.rtol) - - -def test_a_dtype_torch_has_no_kernel_for_refuses_on_both_routes(tmp_path: Path) -> None: - """``uint16`` is a store's dtype, not a chain's: torch implements no comparison for it. - - The sweep gives up on it (a warning naming ``TensorCast``) and the whole-volume fallback then - raises with the same remedy: a refusal on one route and a result on the other would make the - decomposition decide whether a case runs at all. - """ - dataset = build_case(tmp_path / "case", GEOMETRIES[MAIN], np.dtype(np.uint16)) - manager = _manager(dataset, "Intensity", [Clip("min", "max"), Save(f"{tmp_path / 'out'}:h5")]) - with pytest.warns(UserWarning, match="TensorCast"), pytest.raises(TransformError, match="TensorCast"): - CaseMaterializer(manager).materialize() - - -def test_a_two_dimensional_stored_map_is_refused_before_any_route_runs( - cases: dict[str, Dataset], tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """The stored-map codec holds the 3-D rigid, affine and BSpline kinds; a 2-D map has no tag. - - Refused where the map is READ, which is before either route is chosen, and the message names - the type it found: a map applied on one route and refused on the other would be the worst of - both. This is why the two resample-through-a-stored-map cases leave the rank-2 matrix. - """ - stage = Resample(transforms={"transform": True}) - with pytest.raises(TransformError, match="Euler2DTransform"): - _sweep(cases["rank2-seed37"], "Intensity", stage, tmp_path / "out", ROUTES[0], monkeypatch) - - -def test_the_store_refuses_a_dtype_it_cannot_hold(tmp_path: Path) -> None: - """``bool`` is the one dtype in the list no case can be built on: the refusal is the contract.""" - geometry = GEOMETRIES[MAIN] - with pytest.raises(TypeError, match="bool"): - Dataset(tmp_path / "case", "mha").write( - "Intensity", - CASE_NAME, - volumes(geometry)["Labels"].astype(bool), - attributes(geometry, "Intensity"), - ) - - -# ---------------------------------------------------------------- N cases folded into one - - -def _cohort(root: Path, geometry: Geometry, count: int) -> tuple[Dataset, list[np.ndarray]]: - """``count`` cases on ONE grid, which is what a reduction requires of its members.""" - rng = np.random.default_rng(7) - dataset = Dataset(root, "h5") - written = [] - for index in range(count): - volume = (rng.random((1, *geometry.extents)) * 100.0).astype(np.float32) - dataset.write("CT", f"CASE_{index:03d}", volume, attributes(geometry, "Intensity")) - written.append(volume) - return dataset, written - - -@pytest.mark.parametrize("operator", ["Mean", "Median", "Std", "Vote", "Concat"]) -@pytest.mark.parametrize("count", [2, 3, 4, 5]) -@pytest.mark.parametrize("slab_rows", [1, 3, 64], ids=["row-regions", "few-regions", "one-region"]) -def test_a_streamed_reduction_equals_the_operator_on_the_whole_cohort( - operator: str, count: int, slab_rows: int, tmp_path: Path -) -> None: - """A reduction never assembles its members, so its regions are its only route to the answer. - - The reference is the SAME operator applied once to the whole volumes, in the layout both engines - hand it (``[1, C, *spatial]`` per case): what a region-wise fold must reproduce exactly, whatever - the count, whatever the region height. ``Median`` changes route at five members and ``Concat`` - changes the channel count, which is why both bounds of the count are run. - """ - geometry = GEOMETRIES["rank3-seed23"] - dataset, written = _cohort(tmp_path / "cohort", geometry, count) - destination = Dataset(tmp_path / "out", "h5") - reduce = Reduce(operator=operator, output="folded") - engine = CaseReduction( - managers=[_manager(dataset, "CT", [], f"CASE_{index:03d}") for index in range(count)], - reduce=reduce, - post=[], - destination=destination, - group="CT", - slab_rows=slab_rows, - ) - assert engine.materialize() is True - - got, _ = destination.read_data("CT", "folded") - expected = reduce.operator([torch.from_numpy(volume).unsqueeze(0) for volume in written]).squeeze(0).numpy() - assert got.shape == expected.shape - np.testing.assert_allclose(got, expected, rtol=0, atol=0) - - -# ---------------------------------------------------------------- one case expanded into copies - - -@dataclass(frozen=True) -class Draw: - """One draw, the regime its copies must take, and how far a copy of it may round. - - A per-voxel draw is exactly its own block, so its copies ride ONE read pass; a draw that reads - elsewhere than its target block cannot, and sweeps its own. Which one is not a detail: the - shared pass is the whole point of the regime, and a pass that fails falls back to solo passes - that write the same bytes, so only the regime says whether the optimisation still happens. - - A draw that resamples reaches its copy through grid_sample on coordinates normalised by the - region's own extent rather than the volume's, which is the deviation ``AUGMENTATION_ATOL`` - bounds (ulps of the phantom's step; measured here at 1.5e-4 on a 500-wide range, 3e-7 of it). - The exact remaps and the per-voxel fields are byte-identical at any region count. - """ - - build: Callable[[], DataAugmentation] - regime: Regime - atol: float = 0.0 - - -def _draws() -> dict[str, Draw]: - """One draw per way a copy is read: a per-voxel field, a box, two exact remaps, two pull maps. - Built per call, because a draw caches the parameters it drew for a case.""" - return { - "Noise": Draw(lambda: Noise(1.0), Regime.SHARED), - "CutOUT": Draw(lambda: CutOUT(1.0, 0.5, 0.0), Regime.SHARED), - "Flip": Draw(lambda: FlipDraw(f_prob=[1.0, 1.0, 1.0]), Regime.SOLO), - "QuarterRotate": Draw(lambda: Rotate(is_quarter=True), Regime.SOLO), - "Rotate": Draw(lambda: Rotate(a_min=10.0, a_max=10.0), Regime.SOLO, AUGMENTATION_ATOL), - "Scale": Draw(lambda: Scale(), Regime.SOLO, AUGMENTATION_ATOL), - } - - -def _expanded(dataset: Dataset, augmentation: DataAugmentation, copies: int, destination: Path) -> DatasetManager: - return _manager( - dataset, - "Intensity", - [ - Clip(-200.0, 300.0), - Expand(nb=copies, pattern="{name}_c{a:02d}"), - augmentation, - Write(f"{destination}:h5"), - ], - ) - - -@pytest.mark.parametrize("name", list(_draws()), ids=list(_draws())) -@pytest.mark.parametrize("copies", [2, 3]) -@pytest.mark.parametrize("route", ROUTES, ids=lambda route: route.name) -def test_a_streamed_copy_equals_the_whole_volume_copy(name: str, copies: int, route: Route, tmp_path: Path) -> None: - """Every copy of an ``Expand`` carries its own draw, and the decomposition must not change it. - - Pointwise is not place-independent: a noise field and a cutout box are functions of the voxel's - position, so a copy's stages must be told where their block sits exactly as the shared prefix's - are. Without that, the copies agreed with the whole volume on a case that fitted one region and - diverged over its whole extent on anything larger. - - Rank 3 only: the draws are declared three-dimensional (``Permute`` refuses anything else), so a - 2-D row would exercise that refusal rather than this property. - """ - dataset = build_case(tmp_path / "case", GEOMETRIES[MAIN]) - draw = _draws()[name] - augmentation = draw.build() - augmentation.load(1.0) - - streamed = _expanded(dataset, augmentation, copies, tmp_path / "streamed") - budget = _budget_for(streamed, route) - outcomes = CaseMaterializer(streamed).materialize_copies(list(range(1, copies + 1)), fallback_budget_bytes=budget) - whole = _expanded(dataset, augmentation, copies, tmp_path / "whole") - for a in range(1, copies + 1): - CaseMaterializer(whole)._assemble_and_write(a) - - assert set(outcomes.values()) == {(Verdict.STREAM, draw.regime)} - for a in range(1, copies + 1): - entry = f"{CASE_NAME}_c{a:02d}" - got, _ = Dataset(tmp_path / "streamed", "h5").read_data("Intensity", entry) - want, _ = Dataset(tmp_path / "whole", "h5").read_data("Intensity", entry) - np.testing.assert_allclose(got, want, rtol=0, atol=draw.atol) - - -@pytest.mark.parametrize("route", ROUTES, ids=lambda route: route.name) -def test_a_transform_after_the_marker_reads_its_companion_where_the_block_sits(route: Route, tmp_path: Path) -> None: - """A copy's tail is not only its draw: a pointwise TRANSFORM there reads a second volume. - - ``Mask`` takes its foreground from a companion aligned with the case, so it needs the block's - place as much as a noise field does, and it is the half of the fix whose failure is not silent: - handed a block as a whole volume it raises, the shared pass gives up, and the copies fall back - to a solo pass each that writes exactly the same bytes. Which is why the REGIME is what says - whether the shared pass still happens. - """ - dataset = build_case(tmp_path / "case", GEOMETRIES[MAIN]) - draw = _draws()["Noise"].build() - draw.load(1.0) - mask = Mask(path="Labels", value_outside=-7) - - def chain(destination: Path) -> list[Transform]: - return [ - Expand(nb=2, pattern="{name}_c{a:02d}"), - draw, - mask, - Write(f"{destination}:h5"), - ] - - mask.set_datasets([dataset]) - streamed = _manager(dataset, "Intensity", chain(tmp_path / "streamed")) - budget = _budget_for(streamed, route) - outcomes = CaseMaterializer(streamed).materialize_copies([1, 2], fallback_budget_bytes=budget) - assert set(outcomes.values()) == {(Verdict.STREAM, Regime.SHARED)} - - whole = _manager(dataset, "Intensity", chain(tmp_path / "whole")) - for a in (1, 2): - CaseMaterializer(whole)._assemble_and_write(a) - for a in (1, 2): - entry = f"{CASE_NAME}_c{a:02d}" - got, _ = Dataset(tmp_path / "streamed", "h5").read_data("Intensity", entry) - want, _ = Dataset(tmp_path / "whole", "h5").read_data("Intensity", entry) - np.testing.assert_array_equal(got, want) - assert (got == -7).any(), "the mask fell outside the copy: nothing was masked" - - -def test_a_copy_that_cannot_stream_is_refused_under_a_budget_its_whole_volume_exceeds(tmp_path: Path) -> None: - """``Elastix`` solves its field over the whole volume, so its copies take the whole-volume path. - - That path is priced, not free: under a budget the assembled case does not fit, the copies must - be refused with the working set named, and nothing written. Given room, the same copies land.""" - dataset = build_case(tmp_path / "case", GEOMETRIES[MAIN]) - draw = Elastix() - draw.load(1.0) - refused = _expanded(dataset, draw, 2, tmp_path / "refused") - with pytest.raises(PatchError, match="exceeds the per-rank budget"): - CaseMaterializer(refused).materialize_copies([1, 2], fallback_budget_bytes=1.0) - assert not (tmp_path / "refused").exists() - - written = _expanded(dataset, draw, 2, tmp_path / "written") - outcomes = CaseMaterializer(written).materialize_copies([1, 2]) - assert {verdict for verdict, _regime in outcomes.values()} == {Verdict.WHOLE_VOLUME} - assert Dataset(tmp_path / "written", "h5").is_dataset_exist("Intensity", f"{CASE_NAME}_c01") - - -def test_the_copies_of_a_case_are_not_the_same_copy(tmp_path: Path) -> None: - """The property above compares two routes of ONE draw, so it would hold if every copy were the - identity. The copies must differ from each other and from the source.""" - geometry = GEOMETRIES[MAIN] - dataset = build_case(tmp_path / "case", geometry) - augmentation = _draws()["Noise"].build() - augmentation.load(1.0) - CaseMaterializer(_expanded(dataset, augmentation, 2, tmp_path / "out")).materialize_copies([1, 2]) - - out = Dataset(tmp_path / "out", "h5") - first, _ = out.read_data("Intensity", f"{CASE_NAME}_c01") - second, _ = out.read_data("Intensity", f"{CASE_NAME}_c02") - source, _ = dataset.read_data("Intensity", CASE_NAME) - assert not np.array_equal(first, second) and not np.array_equal(first, source) - - -# ---------------------------------------------------------------- the budget is the decomposition - - -def test_the_budget_is_what_decides_how_many_regions_a_sweep_cuts( - cases: dict[str, Dataset], tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """The matrix's third axis, asserted where it can be seen: the same case, the same stage, three - budgets, three decompositions, and the same bytes out of all three.""" - dataset = cases[MAIN] - results = [ - _sweep(dataset, "Intensity", Clip(-200.0, 300.0), tmp_path / route.name, route, monkeypatch) for route in ROUTES - ] - rows = int(dataset.get_infos("Intensity", CASE_NAME)[0][1]) - counts = [result.regions for result in results] - assert [result.verdict for result in results] == [Verdict.STREAM] * len(ROUTES) - assert counts[0] == 1 - assert 1 < counts[1] < counts[2] == rows, f"the budgets gave {counts} regions for {rows} rows" - for result in results[1:]: - np.testing.assert_array_equal(result.array, results[0].array) diff --git a/tests/unit/test_streamed_oracle_decomposition.py b/tests/unit/test_streamed_oracle_decomposition.py new file mode 100644 index 00000000..826dea57 --- /dev/null +++ b/tests/unit/test_streamed_oracle_decomposition.py @@ -0,0 +1,94 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""What a sweep writes region by region is what the whole-volume pass writes: the DECOMPOSITION axis. + +One property, one axis per file, over the vocabulary ``oracle_support`` declares once (the routes, +the driver, the bounds): this file runs every streamable built-in on the main geometry under the +three decompositions the budget buys (one region, a few, one row each), which is the axis a wrong +region boundary shows on and the one no fixed slab height exercises. The siblings run the same +property over the geometry (``test_streamed_oracle_geometry``), the store's dtype and the N-to-one +fold (``test_streamed_oracle_dtype_reduction``), and the one-to-copies expansion +(``test_streamed_oracle_expansion``). + +Vacuity. A fallback that quietly writes the whole volume would satisfy "the bytes agree" and prove +nothing, so every row asserts the ROUTE it took as well, and a decomposed row asserts that it really +was decomposed. +""" + +from pathlib import Path + +import numpy as np +import pytest +from konfai.data.materialize import Verdict +from konfai.data.transform import Clip +from konfai.utils.dataset import Dataset +from oracle_support import ( + CASE_NAME, + MAIN, + ROUTES, + Route, + StageCase, + assert_same, + identify, + oracle_matrix, + sweep, + whole_volume, +) + +pytest.importorskip("SimpleITK") + + +@pytest.mark.parametrize("entry", oracle_matrix([MAIN]), ids=identify) +def test_a_swept_case_equals_the_whole_volume_case( + entry: tuple[str, StageCase, Route], + oracle_cases: dict[str, Dataset], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The matrix: one built-in, one decomposition, both routes, the same bytes. + + The route is asserted before the values, so a stage that quietly stopped streaming fails here + rather than passing on a whole-volume comparison with itself. + """ + geometry, case, route = entry + dataset = oracle_cases[geometry] + streamed = sweep(dataset, case.group, case.transform, tmp_path / "streamed", route, monkeypatch) + whole = whole_volume(dataset, case.group, case.transform, tmp_path / "whole") + + assert streamed.verdict is Verdict.STREAM + # One row per region on a case of 16 rows or more is at least two regions: without this the row + # would pass on a sweep that never decomposed anything. + assert streamed.regions >= (2 if route.height == 0.0 else 1) + assert_same(streamed, whole, case.atol, case.rtol) + + +def test_the_budget_is_what_decides_how_many_regions_a_sweep_cuts( + oracle_cases: dict[str, Dataset], tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The matrix's decomposition axis, asserted where it can be seen: the same case, the same + stage, three budgets, three decompositions, and the same bytes out of all three.""" + dataset = oracle_cases[MAIN] + results = [ + sweep(dataset, "Intensity", Clip(-200.0, 300.0), tmp_path / route.name, route, monkeypatch) for route in ROUTES + ] + rows = int(dataset.get_infos("Intensity", CASE_NAME)[0][1]) + counts = [result.regions for result in results] + assert [result.verdict for result in results] == [Verdict.STREAM] * len(ROUTES) + assert counts[0] == 1 + assert 1 < counts[1] < counts[2] == rows, f"the budgets gave {counts} regions for {rows} rows" + for result in results[1:]: + np.testing.assert_array_equal(result.array, results[0].array) diff --git a/tests/unit/test_streamed_oracle_dtype_reduction.py b/tests/unit/test_streamed_oracle_dtype_reduction.py new file mode 100644 index 00000000..3a463fa5 --- /dev/null +++ b/tests/unit/test_streamed_oracle_dtype_reduction.py @@ -0,0 +1,176 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""The streamed-oracle property over the DTYPE axis and the N-cases-to-one REDUCTION (see the +family note in ``test_streamed_oracle_decomposition``). + +The dtype axis varies what a store actually holds, refusals included: a dtype is not a detail of +the storage, it decides whether the chain rounds, and where. The reduction is one of the two +cardinality changes the workflow owns (``Reduce``, N cases folded into one); the other, one case +expanded into copies, lives in ``test_streamed_oracle_expansion``. +""" + +from pathlib import Path + +import numpy as np +import pytest +import torch +from konfai.data.case_reduction import CaseReduction +from konfai.data.materialize import CaseMaterializer, Verdict +from konfai.data.transform import Clip, Crop, Flip, Gradient, Mask, Reduce, Resample, Save +from konfai.utils.dataset import Dataset +from konfai.utils.errors import TransformError +from oracle_support import ( + CASE_NAME, + GEOMETRIES, + LSB_ATOL, + MAIN, + ROUTES, + Geometry, + Route, + StageCase, + assert_same, + attributes, + build_case, + manager, + sweep, + volumes, + whole_volume, +) + +pytest.importorskip("SimpleITK") + + +def _dtype_cases() -> list[StageCase]: + """One stage per read-streamable kind, each a remap a store of ANY dtype can carry. + + The dtype axis must vary the dtype and nothing else, so a stage whose configuration only makes + sense in one numeric range (a clip at fixed Hounsfield bounds, a fill value no unsigned store + holds) would confound the two. + """ + return [ + StageCase(Flip("0")), # ORIENTATION + StageCase(Mask(path="Labels", value_outside=0)), # POINTWISE, reading a companion + StageCase(Gradient()), # HALO + StageCase(Resample(spacing=[2.0, 1.0, 3.0]), atol=LSB_ATOL), # REGRID + StageCase(Crop(), group="Boxed"), # CROP + StageCase(Clip("min", "max")), # GLOBAL_STAT + ] + + +#: What torch has kernels for, of what a store legitimately holds. ``uint16`` is what microscopy +#: writes and torch implements neither comparison nor arithmetic for it: its refusal is pinned +#: below rather than tolerated here, and ``bool`` never reaches a chain because the store refuses to +#: hold it. +DTYPES = (np.uint8, np.int16, np.int32, np.float32, np.float64) + + +@pytest.mark.parametrize("dtype", DTYPES, ids=lambda dtype: np.dtype(dtype).name) +@pytest.mark.parametrize("case", _dtype_cases(), ids=lambda case: type(case.transform).__name__) +@pytest.mark.parametrize("route", ROUTES, ids=lambda route: route.name) +def test_a_swept_case_equals_the_whole_volume_case_on_every_dtype( + case: StageCase, dtype: np.dtype, route: Route, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A dtype is not a detail of the storage: it decides whether the chain rounds, and where. + + An integer store quantizes an interpolation, so the two routes may land a least significant bit + apart (``LSB_ATOL``); every other stage here is an exact remap and must be byte-identical. + """ + dataset = build_case(tmp_path / "case", GEOMETRIES[MAIN], np.dtype(dtype)) + streamed = sweep(dataset, case.group, case.transform, tmp_path / "streamed", route, monkeypatch) + whole = whole_volume(dataset, case.group, case.transform, tmp_path / "whole") + + assert streamed.verdict is Verdict.STREAM + # The store's dtype survives a remap. Gradient hands back differences, which an integer store + # cannot hold: it widens those to float32 and leaves a floating dtype as it found it. + expected = np.dtype(dtype) + if isinstance(case.transform, Gradient) and not np.issubdtype(expected, np.floating): + expected = np.dtype(np.float32) + assert streamed.array.dtype == expected + assert_same(streamed, whole, case.atol, case.rtol) + + +def test_a_dtype_torch_has_no_kernel_for_refuses_on_both_routes(tmp_path: Path) -> None: + """``uint16`` is a store's dtype, not a chain's: torch implements no comparison for it. + + The sweep gives up on it (a warning naming ``TensorCast``) and the whole-volume fallback then + raises with the same remedy: a refusal on one route and a result on the other would make the + decomposition decide whether a case runs at all. + """ + dataset = build_case(tmp_path / "case", GEOMETRIES[MAIN], np.dtype(np.uint16)) + case_manager = manager(dataset, [Clip("min", "max"), Save(f"{tmp_path / 'out'}:h5")], group="Intensity") + with pytest.warns(UserWarning, match="TensorCast"), pytest.raises(TransformError, match="TensorCast"): + CaseMaterializer(case_manager).materialize() + + +def test_the_store_refuses_a_dtype_it_cannot_hold(tmp_path: Path) -> None: + """``bool`` is the one dtype in the list no case can be built on: the refusal is the contract.""" + geometry = GEOMETRIES[MAIN] + with pytest.raises(TypeError, match="bool"): + Dataset(tmp_path / "case", "mha").write( + "Intensity", + CASE_NAME, + volumes(geometry)["Labels"].astype(bool), + attributes(geometry, "Intensity"), + ) + + +# ---------------------------------------------------------------- N cases folded into one + + +def _cohort(root: Path, geometry: Geometry, count: int) -> tuple[Dataset, list[np.ndarray]]: + """``count`` cases on ONE grid, which is what a reduction requires of its members.""" + rng = np.random.default_rng(7) + dataset = Dataset(root, "h5") + written = [] + for index in range(count): + volume = (rng.random((1, *geometry.extents)) * 100.0).astype(np.float32) + dataset.write("CT", f"CASE_{index:03d}", volume, attributes(geometry, "Intensity")) + written.append(volume) + return dataset, written + + +@pytest.mark.parametrize("operator", ["Mean", "Median", "Std", "Vote", "Concat"]) +@pytest.mark.parametrize("count", [2, 3, 4, 5]) +@pytest.mark.parametrize("slab_rows", [1, 3, 64], ids=["row-regions", "few-regions", "one-region"]) +def test_a_streamed_reduction_equals_the_operator_on_the_whole_cohort( + operator: str, count: int, slab_rows: int, tmp_path: Path +) -> None: + """A reduction never assembles its members, so its regions are its only route to the answer. + + The reference is the SAME operator applied once to the whole volumes, in the layout both engines + hand it (``[1, C, *spatial]`` per case): what a region-wise fold must reproduce exactly, whatever + the count, whatever the region height. ``Median`` changes route at five members and ``Concat`` + changes the channel count, which is why both bounds of the count are run. + """ + geometry = GEOMETRIES["rank3-seed23"] + dataset, written = _cohort(tmp_path / "cohort", geometry, count) + destination = Dataset(tmp_path / "out", "h5") + reduce = Reduce(operator=operator, output="folded") + engine = CaseReduction( + managers=[manager(dataset, [], name=f"CASE_{index:03d}") for index in range(count)], + reduce=reduce, + post=[], + destination=destination, + group="CT", + slab_rows=slab_rows, + ) + assert engine.materialize() is True + + got, _ = destination.read_data("CT", "folded") + expected = reduce.operator([torch.from_numpy(volume).unsqueeze(0) for volume in written]).squeeze(0).numpy() + assert got.shape == expected.shape + np.testing.assert_allclose(got, expected, rtol=0, atol=0) diff --git a/tests/unit/test_streamed_oracle_expansion.py b/tests/unit/test_streamed_oracle_expansion.py new file mode 100644 index 00000000..3459778f --- /dev/null +++ b/tests/unit/test_streamed_oracle_expansion.py @@ -0,0 +1,205 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""The streamed-oracle property over the EXPANSION axis: one case expanded into copies (see the +family note in ``test_streamed_oracle_decomposition``). + +Every copy of an ``Expand`` carries its own draw, and the decomposition must not change it: the +copies of a swept case must equal the copies of the whole-volume case, draw by draw, at every +region count, and take the regime (one shared read pass, or a solo pass each) their draw declares. +""" + +from collections.abc import Callable +from dataclasses import dataclass +from pathlib import Path + +import numpy as np +import pytest +from konfai.data.augmentation import CutOUT, DataAugmentation, Elastix, Noise, Rotate, Scale +from konfai.data.augmentation import Flip as FlipDraw +from konfai.data.materialize import CaseMaterializer, Regime, Verdict +from konfai.data.patching import DatasetManager +from konfai.data.transform import Clip, Expand, Mask, Transform, Write +from konfai.utils.dataset import Dataset +from konfai.utils.errors import PatchError +from oracle_support import ( + AUGMENTATION_ATOL, + CASE_NAME, + GEOMETRIES, + MAIN, + ROUTES, + Route, + budget_for, + build_case, + manager, +) + +pytest.importorskip("SimpleITK") + + +@dataclass(frozen=True) +class Draw: + """One draw, the regime its copies must take, and how far a copy of it may round. + + A per-voxel draw is exactly its own block, so its copies ride ONE read pass; a draw that reads + elsewhere than its target block cannot, and sweeps its own. Which one is not a detail: the + shared pass is the whole point of the regime, and a pass that fails falls back to solo passes + that write the same bytes, so only the regime says whether the optimisation still happens. + + A draw that resamples reaches its copy through grid_sample on coordinates normalised by the + region's own extent rather than the volume's, which is the deviation ``AUGMENTATION_ATOL`` + bounds (ulps of the phantom's step; measured here at 1.5e-4 on a 500-wide range, 3e-7 of it). + The exact remaps and the per-voxel fields are byte-identical at any region count. + """ + + build: Callable[[], DataAugmentation] + regime: Regime + atol: float = 0.0 + + +def _draws() -> dict[str, Draw]: + """One draw per way a copy is read: a per-voxel field, a box, two exact remaps, two pull maps. + Built per call, because a draw caches the parameters it drew for a case.""" + return { + "Noise": Draw(lambda: Noise(1.0), Regime.SHARED), + "CutOUT": Draw(lambda: CutOUT(1.0, 0.5, 0.0), Regime.SHARED), + "Flip": Draw(lambda: FlipDraw(f_prob=[1.0, 1.0, 1.0]), Regime.SOLO), + "QuarterRotate": Draw(lambda: Rotate(is_quarter=True), Regime.SOLO), + "Rotate": Draw(lambda: Rotate(a_min=10.0, a_max=10.0), Regime.SOLO, AUGMENTATION_ATOL), + "Scale": Draw(lambda: Scale(), Regime.SOLO, AUGMENTATION_ATOL), + } + + +def _expanded(dataset: Dataset, augmentation: DataAugmentation, copies: int, destination: Path) -> DatasetManager: + return manager( + dataset, + [ + Clip(-200.0, 300.0), + Expand(nb=copies, pattern="{name}_c{a:02d}"), + augmentation, + Write(f"{destination}:h5"), + ], + group="Intensity", + ) + + +@pytest.mark.parametrize("name", list(_draws()), ids=list(_draws())) +@pytest.mark.parametrize("copies", [2, 3]) +@pytest.mark.parametrize("route", ROUTES, ids=lambda route: route.name) +def test_a_streamed_copy_equals_the_whole_volume_copy(name: str, copies: int, route: Route, tmp_path: Path) -> None: + """Every copy of an ``Expand`` carries its own draw, and the decomposition must not change it. + + Pointwise is not place-independent: a noise field and a cutout box are functions of the voxel's + position, so a copy's stages must be told where their block sits exactly as the shared prefix's + are. Without that, the copies agreed with the whole volume on a case that fitted one region and + diverged over its whole extent on anything larger. + + Rank 3 only: the draws are declared three-dimensional (``Permute`` refuses anything else), so a + 2-D row would exercise that refusal rather than this property. + """ + dataset = build_case(tmp_path / "case", GEOMETRIES[MAIN]) + draw = _draws()[name] + augmentation = draw.build() + augmentation.load(1.0) + + streamed = _expanded(dataset, augmentation, copies, tmp_path / "streamed") + budget = budget_for(streamed, route) + outcomes = CaseMaterializer(streamed).materialize_copies(list(range(1, copies + 1)), fallback_budget_bytes=budget) + whole = _expanded(dataset, augmentation, copies, tmp_path / "whole") + for a in range(1, copies + 1): + CaseMaterializer(whole)._assemble_and_write(a) + + assert set(outcomes.values()) == {(Verdict.STREAM, draw.regime)} + for a in range(1, copies + 1): + entry = f"{CASE_NAME}_c{a:02d}" + got, _ = Dataset(tmp_path / "streamed", "h5").read_data("Intensity", entry) + want, _ = Dataset(tmp_path / "whole", "h5").read_data("Intensity", entry) + np.testing.assert_allclose(got, want, rtol=0, atol=draw.atol) + + +@pytest.mark.parametrize("route", ROUTES, ids=lambda route: route.name) +def test_a_transform_after_the_marker_reads_its_companion_where_the_block_sits(route: Route, tmp_path: Path) -> None: + """A copy's tail is not only its draw: a pointwise TRANSFORM there reads a second volume. + + ``Mask`` takes its foreground from a companion aligned with the case, so it needs the block's + place as much as a noise field does, and it is the half of the fix whose failure is not silent: + handed a block as a whole volume it raises, the shared pass gives up, and the copies fall back + to a solo pass each that writes exactly the same bytes. Which is why the REGIME is what says + whether the shared pass still happens. + """ + dataset = build_case(tmp_path / "case", GEOMETRIES[MAIN]) + draw = _draws()["Noise"].build() + draw.load(1.0) + mask = Mask(path="Labels", value_outside=-7) + + def chain(destination: Path) -> list[Transform]: + return [ + Expand(nb=2, pattern="{name}_c{a:02d}"), + draw, + mask, + Write(f"{destination}:h5"), + ] + + mask.set_datasets([dataset]) + streamed = manager(dataset, chain(tmp_path / "streamed"), group="Intensity") + budget = budget_for(streamed, route) + outcomes = CaseMaterializer(streamed).materialize_copies([1, 2], fallback_budget_bytes=budget) + assert set(outcomes.values()) == {(Verdict.STREAM, Regime.SHARED)} + + whole = manager(dataset, chain(tmp_path / "whole"), group="Intensity") + for a in (1, 2): + CaseMaterializer(whole)._assemble_and_write(a) + for a in (1, 2): + entry = f"{CASE_NAME}_c{a:02d}" + got, _ = Dataset(tmp_path / "streamed", "h5").read_data("Intensity", entry) + want, _ = Dataset(tmp_path / "whole", "h5").read_data("Intensity", entry) + np.testing.assert_array_equal(got, want) + assert (got == -7).any(), "the mask fell outside the copy: nothing was masked" + + +def test_a_copy_that_cannot_stream_is_refused_under_a_budget_its_whole_volume_exceeds(tmp_path: Path) -> None: + """``Elastix`` solves its field over the whole volume, so its copies take the whole-volume path. + + That path is priced, not free: under a budget the assembled case does not fit, the copies must + be refused with the working set named, and nothing written. Given room, the same copies land.""" + dataset = build_case(tmp_path / "case", GEOMETRIES[MAIN]) + draw = Elastix() + draw.load(1.0) + refused = _expanded(dataset, draw, 2, tmp_path / "refused") + with pytest.raises(PatchError, match="exceeds the per-rank budget"): + CaseMaterializer(refused).materialize_copies([1, 2], fallback_budget_bytes=1.0) + assert not (tmp_path / "refused").exists() + + written = _expanded(dataset, draw, 2, tmp_path / "written") + outcomes = CaseMaterializer(written).materialize_copies([1, 2]) + assert {verdict for verdict, _regime in outcomes.values()} == {Verdict.WHOLE_VOLUME} + assert Dataset(tmp_path / "written", "h5").is_dataset_exist("Intensity", f"{CASE_NAME}_c01") + + +def test_the_copies_of_a_case_are_not_the_same_copy(tmp_path: Path) -> None: + """The property above compares two routes of ONE draw, so it would hold if every copy were the + identity. The copies must differ from each other and from the source.""" + geometry = GEOMETRIES[MAIN] + dataset = build_case(tmp_path / "case", geometry) + augmentation = _draws()["Noise"].build() + augmentation.load(1.0) + CaseMaterializer(_expanded(dataset, augmentation, 2, tmp_path / "out")).materialize_copies([1, 2]) + + out = Dataset(tmp_path / "out", "h5") + first, _ = out.read_data("Intensity", f"{CASE_NAME}_c01") + second, _ = out.read_data("Intensity", f"{CASE_NAME}_c02") + source, _ = dataset.read_data("Intensity", CASE_NAME) + assert not np.array_equal(first, second) and not np.array_equal(first, source) diff --git a/tests/unit/test_streamed_oracle_geometry.py b/tests/unit/test_streamed_oracle_geometry.py new file mode 100644 index 00000000..22c01b38 --- /dev/null +++ b/tests/unit/test_streamed_oracle_geometry.py @@ -0,0 +1,115 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""The streamed-oracle property over the RANK and GEOMETRY axes (see the family note in +``test_streamed_oracle_decomposition``). + +This file varies what the case is stored ON: a second seeded 3-D geometry and a 2-D one, with +anisotropic spacings, oblique and axis-permuting cosines, drawn from a fixed seed list. It also +pins the fixture's own claims (what every row of every contract file assumes of a geometry) and the +one refusal the rank axis owns: a 2-D stored map has no codec tag. +""" + +from pathlib import Path + +import numpy as np +import pytest +from konfai.data.materialize import Verdict +from konfai.data.transform import Resample +from konfai.utils.dataset import Dataset +from konfai.utils.errors import TransformError +from oracle_support import ( + FIXED_GEOMETRY, + GEOMETRIES, + MAIN, + ROUTES, + Geometry, + Route, + StageCase, + assert_same, + identify, + oracle_matrix, + sweep, + whole_volume, +) + +pytest.importorskip("SimpleITK") + +#: The geometries this file owns: the decomposition sibling runs the same matrix on MAIN. +OTHERS = [name for name in GEOMETRIES if name != MAIN] + + +@pytest.mark.parametrize("geometry", [*GEOMETRIES.values(), FIXED_GEOMETRY], ids=[*GEOMETRIES, "fixed"]) +def test_a_geometry_carries_what_the_property_leans_on(geometry: Geometry) -> None: + """The fixture's own claims, since every row of every contract file assumes them. + + Both directions are orthonormal (a stored volume has no other kind) and the permuting one really + permutes, so reorienting a case stored on it transposes extents and moves the grid the patches + are cut on. The reference grid starts inside the case and reaches past it on some axis: one + contained in its case would prove the sampler and never the boundary, which is the half that + differs between the streamed and the whole-volume routes. + """ + identity = np.eye(geometry.rank) + for direction in (geometry.oblique, geometry.permuting): + np.testing.assert_allclose(direction @ direction.T, identity, rtol=0, atol=1e-12) + assert not np.array_equal(geometry.permuting, identity) + + def world(extents: tuple[int, ...], spacing: tuple[float, ...]) -> np.ndarray: + return np.asarray(extents, dtype=np.float64)[::-1] * np.asarray(spacing) + + case = world(geometry.extents, geometry.spacing) + reference = world(geometry.reference_extents, geometry.reference_spacing) + start = np.asarray(geometry.reference_origin) - np.asarray(geometry.origin) + assert (start > 0).all(), "the reference grid starts outside the case" + assert (start + reference > case).any(), "the reference grid is nested inside the case" + + +@pytest.mark.parametrize("entry", oracle_matrix(OTHERS), ids=identify) +def test_a_swept_case_equals_the_whole_volume_case( + entry: tuple[str, StageCase, Route], + oracle_cases: dict[str, Dataset], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The matrix: one built-in, one geometry, one decomposition, both routes, the same bytes. + + The route is asserted before the values, so a stage that quietly stopped streaming fails here + rather than passing on a whole-volume comparison with itself. + """ + geometry, case, route = entry + dataset = oracle_cases[geometry] + streamed = sweep(dataset, case.group, case.transform, tmp_path / "streamed", route, monkeypatch) + whole = whole_volume(dataset, case.group, case.transform, tmp_path / "whole") + + assert streamed.verdict is Verdict.STREAM + # One row per region on a case of 16 rows or more is at least two regions: without this the row + # would pass on a sweep that never decomposed anything. + assert streamed.regions >= (2 if route.height == 0.0 else 1) + assert_same(streamed, whole, case.atol, case.rtol) + + +def test_a_two_dimensional_stored_map_is_refused_before_any_route_runs( + oracle_cases: dict[str, Dataset], tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The stored-map codec holds the 3-D rigid, affine and BSpline kinds; a 2-D map has no tag. + + Refused where the map is READ, which is before either route is chosen, and the message names + the type it found: a map applied on one route and refused on the other would be the worst of + both. This is why the two resample-through-a-stored-map cases leave the rank-2 matrix. + """ + stage = Resample(transforms={"transform": True}) + with pytest.raises(TransformError, match="Euler2DTransform"): + sweep(oracle_cases["rank2-seed37"], "Intensity", stage, tmp_path / "out", ROUTES[0], monkeypatch) diff --git a/tests/unit/test_transform_materialize_contract.py b/tests/unit/test_transform_materialize_contract.py index 1f8b39fa..14ec91cc 100644 --- a/tests/unit/test_transform_materialize_contract.py +++ b/tests/unit/test_transform_materialize_contract.py @@ -34,7 +34,6 @@ import numpy as np import pytest -import SimpleITK as sitk import torch from konfai.data.materialize import CaseMaterializer, Verdict from konfai.data.patching import DatasetManager @@ -51,6 +50,9 @@ volumes, ) +pytest.importorskip("SimpleITK") +import SimpleITK as sitk + @pytest.fixture(scope="session") def dataset(tmp_path_factory: pytest.TempPathFactory) -> Dataset: diff --git a/tests/unit/test_write_pyramid.py b/tests/unit/test_write_pyramid.py index cbe4ced2..c2867f76 100644 --- a/tests/unit/test_write_pyramid.py +++ b/tests/unit/test_write_pyramid.py @@ -25,6 +25,10 @@ import numpy as np import pytest + +pytest.importorskip("zarr") +pytest.importorskip("ngff_zarr") + import zarr from konfai.utils.dataset import Attribute, Dataset from konfai.utils.errors import DatasetManagerError From 8966fade7108aac1e5bb9691ea1a88b026a69777 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:24:40 +0200 Subject: [PATCH 15/28] build: one dev-dependency list, one mypy config, py.typed everywhere The pixi dev environment now installs the [dev] extra instead of restating it (the drift had already diverged: onnx export tests ran in CI and silently skipped under pixi run check). mypy reads [tool.mypy] alone -- the --no-site-packages that made types-requests dead weight in the very task it was installed for is gone, and real stubs lower the baseline (206 -> 193 advisory errors). PEP 561 markers ship in konfai, konfai-apps and konfai-mcp so downstream type checkers and IDEs finally see the annotations; the wheel test pins the marker. --- .pre-commit-config.yaml | 3 +- konfai-apps/konfai_apps/py.typed | 0 konfai-apps/pyproject.toml | 3 + konfai-mcp/konfai_mcp/py.typed | 0 konfai-mcp/pyproject.toml | 3 + konfai/py.typed | 0 pixi.lock | 1210 ++++++++++++++++++++++++------ pyproject.toml | 25 +- tests/unit/test_packaging.py | 3 + 9 files changed, 982 insertions(+), 265 deletions(-) create mode 100644 konfai-apps/konfai_apps/py.typed create mode 100644 konfai-mcp/konfai_mcp/py.typed create mode 100644 konfai/py.typed diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c9b9647..a8e4709e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,8 +44,7 @@ repos: additional_dependencies: - types-requests files: ^(konfai/|konfai-apps/konfai_apps/|konfai-mcp/konfai_mcp/|studio/konfai_studio/) - args: - - --ignore-missing-imports + - --check-untyped-defs - --install-types - --non-interactive diff --git a/konfai-apps/konfai_apps/py.typed b/konfai-apps/konfai_apps/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/konfai-apps/pyproject.toml b/konfai-apps/pyproject.toml index 549f7035..ba5bb922 100644 --- a/konfai-apps/pyproject.toml +++ b/konfai-apps/pyproject.toml @@ -31,3 +31,6 @@ local_scheme = "no-local-version" [tool.setuptools.packages.find] include = ["konfai_apps*"] + +[tool.setuptools.package-data] +"konfai_apps" = ["py.typed"] diff --git a/konfai-mcp/konfai_mcp/py.typed b/konfai-mcp/konfai_mcp/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/konfai-mcp/pyproject.toml b/konfai-mcp/pyproject.toml index d9f3225e..50ed3577 100644 --- a/konfai-mcp/pyproject.toml +++ b/konfai-mcp/pyproject.toml @@ -33,3 +33,6 @@ local_scheme = "no-local-version" [tool.setuptools.packages.find] include = ["konfai_mcp*"] + +[tool.setuptools.package-data] +"konfai_mcp" = ["py.typed"] diff --git a/konfai/py.typed b/konfai/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/pixi.lock b/pixi.lock index bd493e15..6150501f 100644 --- a/pixi.lock +++ b/pixi.lock @@ -17,205 +17,6 @@ platforms: - __archspec=0=x86_64 environments: default: - channels: - - url: https://conda.anaconda.org/conda-forge/ - indexes: - - https://pypi.org/simple - packages: - linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.2-hf4e2dac_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.14-h6add32d_100_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh145f28c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - - pypi: ./ - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/32/4e/8d7656ccaab3e81e97258b8a9bc5f0c8502513a92fb4ceb0a2cbfebc17bf/numpy-2.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e1/75/20bb8fe9c1ad6538cce8cd0391b51927ae5af0b17ed1eab44b8824465dc1/torch-2.12.1-cp313-cp313-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl - osx-arm64: - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh145f28c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.1-hf6b4638_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.2-h1ae2325_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.14-h448ec07_100_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - - pypi: ./ - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/85/4b/953118a730ee3b35e28645e0eb4cf9beec5bdbb954e1ac2f5fcefba6bbc3/numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl - win-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-h4c7d964_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh145f28c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.2-hf5d6505_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.14-h09917c8_100_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_39.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_39.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_39.conda - - pypi: ./ - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/53/491e1c47c55b62ccc6a63c1c5b8635c73fc2258dddeb9bda27cae4a0ae96/numpy-2.5.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d1/fa/824ddb662af55b2eabc0dbb7b57c7c0b1bcd93693754a2b8509ec4d16490/torch-2.12.1-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl - dev: channels: - url: https://conda.anaconda.org/conda-forge/ indexes: @@ -260,7 +61,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0b/35/1cceccc5fcb50fa2ed53e2aa278cd032f3902682a73e763fb1ac3be8e6fa/rich_argparse-1.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0d/1e/b47957057e729adc6cdf519a47f8be2562b7140e280f1418443eb4022192/grpcio-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl @@ -272,7 +72,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl @@ -289,19 +88,16 @@ environments: - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl @@ -315,7 +111,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl @@ -338,10 +133,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/50/dfce42eff2cecabcd5a9bbad5489449c87db3415f408d23ffee417ce01f6/coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl @@ -361,9 +154,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl @@ -387,13 +177,11 @@ environments: - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl @@ -430,8 +218,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/bb/711e1c2ebd18a21202c972dd5d5c8e09a921f2d3560e3a53d6350c808ab7/submitit-1.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl @@ -439,7 +225,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/f0/48/3d07340e3256b2cdf441b883da4fd2f7ab89d4894da94a4c627ba8b2f9eb/zarrista-0.1.0-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -449,7 +234,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh145f28c_0.conda @@ -506,18 +290,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/d8/f37480ebc669af91ebab0b01ab649413446bd5cce365f3f50b71d05be2af/zarrista-0.1.0-cp311-abi3-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl @@ -552,12 +333,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl @@ -578,8 +357,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl @@ -634,8 +411,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/bb/711e1c2ebd18a21202c972dd5d5c8e09a921f2d3560e3a53d6350c808ab7/submitit-1.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/6a/481561f1093834376ed493e4ca42a73e5be0d50031f2969c86593bdc7c96/onnx-1.22.0-cp312-abi3-macosx_12_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl @@ -643,14 +418,11 @@ environments: - pypi: https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f3/ab/a7653bce9a3b204be6a6931767a9e23595807bb84790ce6685e4d7e5bd08/nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh145f28c_0.conda @@ -707,10 +479,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/30/53/491e1c47c55b62ccc6a63c1c5b8635c73fc2258dddeb9bda27cae4a0ae96/numpy-2.5.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl @@ -718,7 +488,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/ab/3a03de43173749171340ad974636105cb02c780f76055551c874c3df7fff/simpleitk-2.5.5-cp311-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl @@ -752,14 +521,11 @@ environments: - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/76/59ed932b0244acd7bbbd6449480053a6d958ea66357f022f932872e19287/onnxruntime-1.27.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/fa/2b5d684e3edf1e81bfd02d298c78c3e3da77ca1d8a2be3183a79544a7548/nh3-0.3.6-cp38-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl @@ -776,8 +542,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl @@ -825,7 +589,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl @@ -834,14 +597,11 @@ environments: - pypi: https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/bb/711e1c2ebd18a21202c972dd5d5c8e09a921f2d3560e3a53d6350c808ab7/submitit-1.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl @@ -849,8 +609,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl - docs: + dev: channels: - url: https://conda.anaconda.org/conda-forge/ indexes: @@ -882,18 +641,672 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/09/a6/bd32357e6cc1ecb473afd78193d7231724f284435d2db25696ecfaaa1503/onnx-1.22.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/35/1cceccc5fcb50fa2ed53e2aa278cd032f3902682a73e763fb1ac3be8e6fa/rich_argparse-1.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1e/b47957057e729adc6cdf519a47f8be2562b7140e280f1418443eb4022192/grpcio-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/4e/8d7656ccaab3e81e97258b8a9bc5f0c8502513a92fb4ceb0a2cbfebc17bf/numpy-2.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/50/dfce42eff2cecabcd5a9bbad5489449c87db3415f408d23ffee417ce01f6/coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9f/68/ed67a355a62848ee04bb4f01e89d3be871052c2c3ae6d5fc0fb2f6010979/simpleitk-2.5.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/b8/0d8eeb5a9fd7d34ba84f8a55753a0a3e2b5b51b2a5a0ade648a8db4a62f7/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/e6/d359fdd37498e74d26a167f7a51e54542e642ea47181eb4e643a69a066c3/numcodecs-0.16.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/19/14929c3c2fe0b79b41cce24463062bf3afa4cdd3c19dccf00319caa92bff/onnxruntime-1.27.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d7/8c/e9019a28e908214031310aefd78e4755221d02303190b54b2c85cb69573e/wasmtime-45.0.0-py3-none-manylinux1_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/75/20bb8fe9c1ad6538cce8cd0391b51927ae5af0b17ed1eab44b8824465dc1/torch-2.12.1-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ea/bb/711e1c2ebd18a21202c972dd5d5c8e09a921f2d3560e3a53d6350c808ab7/submitit-1.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f0/48/3d07340e3256b2cdf441b883da4fd2f7ab89d4894da94a4c627ba8b2f9eb/zarrista-0.1.0-cp311-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh145f28c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.1-hf6b4638_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.2-h1ae2325_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.14-h448ec07_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/35/1cceccc5fcb50fa2ed53e2aa278cd032f3902682a73e763fb1ac3be8e6fa/rich_argparse-1.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/d8/f37480ebc669af91ebab0b01ab649413446bd5cce365f3f50b71d05be2af/zarrista-0.1.0-cp311-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/0b/a81b5daf5adea482ecb68d9615f6a348486ab4d8e980a915d4420e57ee4d/wasmtime-45.0.0-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/34/4b3208b35dea488263a5c9f4a464ef20316f663e9e90d5de61349c31b327/simpleitk-2.5.5-cp311-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/4a/a36e03210183a8a7d4c80c3936acee679f4bd77d5861f369db47b2cc5f05/grpcio-1.81.1-cp313-cp313-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/4b/953118a730ee3b35e28645e0eb4cf9beec5bdbb954e1ac2f5fcefba6bbc3/numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/c0/5f84ba7525577c1b9909fc2d06ef11314825fc4ad4378f61d0e4c9883b4a/numcodecs-0.16.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ea/bb/711e1c2ebd18a21202c972dd5d5c8e09a921f2d3560e3a53d6350c808ab7/submitit-1.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/6a/481561f1093834376ed493e4ca42a73e5be0d50031f2969c86593bdc7c96/onnx-1.22.0-cp312-abi3-macosx_12_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f3/ab/a7653bce9a3b204be6a6931767a9e23595807bb84790ce6685e4d7e5bd08/nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl + - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh145f28c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.2-hf5d6505_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.14-h09917c8_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_39.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/35/1cceccc5fcb50fa2ed53e2aa278cd032f3902682a73e763fb1ac3be8e6fa/rich_argparse-1.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/20/3da8bb0d637feccdc3e1e419bb511ce93651ce7d54164f95de22cc0b8b34/grpcio-1.81.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/a8/734725bb703c5fabb687f79c79e51249475212b3eb37771ac4a4ac9b487f/onnx-1.22.0-cp312-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/71/8e31ea472ceb934e7261ac59a786e82cd82b4d4dcb7c870d498aa9c3c21e/wasmtime-45.0.0-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/27/72/6663cc0382ddbb866136c255c837bcb96cc7ce5e83562efec55e1b995941/numcodecs-0.16.5-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/53/491e1c47c55b62ccc6a63c1c5b8635c73fc2258dddeb9bda27cae4a0ae96/numpy-2.5.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/ab/3a03de43173749171340ad974636105cb02c780f76055551c874c3df7fff/simpleitk-2.5.5-cp311-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/e8/196ebc25d8f34c06d43a6e9c8513c9266ef8dbf3b5672beb1a00cf5e29fa/coverage-7.14.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/76/59ed932b0244acd7bbbd6449480053a6d958ea66357f022f932872e19287/onnxruntime-1.27.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/fa/2b5d684e3edf1e81bfd02d298c78c3e3da77ca1d8a2be3183a79544a7548/nh3-0.3.6-cp38-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/2e/59f3ab896f5a8c76a3d009c40015a32660b1b05b0ff055efcee3aa977caa/zarrista-0.1.0-cp311-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/fa/824ddb662af55b2eabc0dbb7b57c7c0b1bcd93693754a2b8509ec4d16490/torch-2.12.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ea/bb/711e1c2ebd18a21202c972dd5d5c8e09a921f2d3560e3a53d6350c808ab7/submitit-1.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl + docs: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.2-hf4e2dac_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.14-h6add32d_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh145f28c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/09/a6/bd32357e6cc1ecb473afd78193d7231724f284435d2db25696ecfaaa1503/onnx-1.22.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/35/1cceccc5fcb50fa2ed53e2aa278cd032f3902682a73e763fb1ac3be8e6fa/rich_argparse-1.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1e/b47957057e729adc6cdf519a47f8be2562b7140e280f1418443eb4022192/grpcio-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/4e/8d7656ccaab3e81e97258b8a9bc5f0c8502513a92fb4ceb0a2cbfebc17bf/numpy-2.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -901,64 +1314,154 @@ environments: - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/50/dfce42eff2cecabcd5a9bbad5489449c87db3415f408d23ffee417ce01f6/coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9f/68/ed67a355a62848ee04bb4f01e89d3be871052c2c3ae6d5fc0fb2f6010979/simpleitk-2.5.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/b8/0d8eeb5a9fd7d34ba84f8a55753a0a3e2b5b51b2a5a0ade648a8db4a62f7/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/e6/d359fdd37498e74d26a167f7a51e54542e642ea47181eb4e643a69a066c3/numcodecs-0.16.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/19/14929c3c2fe0b79b41cce24463062bf3afa4cdd3c19dccf00319caa92bff/onnxruntime-1.27.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d7/8c/e9019a28e908214031310aefd78e4755221d02303190b54b2c85cb69573e/wasmtime-45.0.0-py3-none-manylinux1_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/75/20bb8fe9c1ad6538cce8cd0391b51927ae5af0b17ed1eab44b8824465dc1/torch-2.12.1-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f0/48/3d07340e3256b2cdf441b883da4fd2f7ab89d4894da94a4c627ba8b2f9eb/zarrista-0.1.0-cp311-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f3/bd/d3348d62296e73a91420f0edf671450c9003ecd8704da40b1e3d9b868459/sphinxcontrib_mermaid-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda @@ -979,64 +1482,176 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/35/1cceccc5fcb50fa2ed53e2aa278cd032f3902682a73e763fb1ac3be8e6fa/rich_argparse-1.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/d8/f37480ebc669af91ebab0b01ab649413446bd5cce365f3f50b71d05be2af/zarrista-0.1.0-cp311-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/0b/a81b5daf5adea482ecb68d9615f6a348486ab4d8e980a915d4420e57ee4d/wasmtime-45.0.0-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/34/4b3208b35dea488263a5c9f4a464ef20316f663e9e90d5de61349c31b327/simpleitk-2.5.5-cp311-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/4a/a36e03210183a8a7d4c80c3936acee679f4bd77d5861f369db47b2cc5f05/grpcio-1.81.1-cp313-cp313-macosx_11_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/4b/953118a730ee3b35e28645e0eb4cf9beec5bdbb954e1ac2f5fcefba6bbc3/numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/c0/5f84ba7525577c1b9909fc2d06ef11314825fc4ad4378f61d0e4c9883b4a/numcodecs-0.16.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/6a/481561f1093834376ed493e4ca42a73e5be0d50031f2969c86593bdc7c96/onnx-1.22.0-cp312-abi3-macosx_12_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f3/bd/d3348d62296e73a91420f0edf671450c9003ecd8704da40b1e3d9b868459/sphinxcontrib_mermaid-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-h4c7d964_0.conda @@ -1058,66 +1673,173 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_39.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_39.conda - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/35/1cceccc5fcb50fa2ed53e2aa278cd032f3902682a73e763fb1ac3be8e6fa/rich_argparse-1.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/20/3da8bb0d637feccdc3e1e419bb511ce93651ce7d54164f95de22cc0b8b34/grpcio-1.81.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/a8/734725bb703c5fabb687f79c79e51249475212b3eb37771ac4a4ac9b487f/onnx-1.22.0-cp312-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/71/8e31ea472ceb934e7261ac59a786e82cd82b4d4dcb7c870d498aa9c3c21e/wasmtime-45.0.0-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/27/72/6663cc0382ddbb866136c255c837bcb96cc7ce5e83562efec55e1b995941/numcodecs-0.16.5-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/53/491e1c47c55b62ccc6a63c1c5b8635c73fc2258dddeb9bda27cae4a0ae96/numpy-2.5.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/ab/3a03de43173749171340ad974636105cb02c780f76055551c874c3df7fff/simpleitk-2.5.5-cp311-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/e8/196ebc25d8f34c06d43a6e9c8513c9266ef8dbf3b5672beb1a00cf5e29fa/coverage-7.14.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/76/59ed932b0244acd7bbbd6449480053a6d958ea66357f022f932872e19287/onnxruntime-1.27.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/2e/59f3ab896f5a8c76a3d009c40015a32660b1b05b0ff055efcee3aa977caa/zarrista-0.1.0-cp311-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/fa/824ddb662af55b2eabc0dbb7b57c7c0b1bcd93693754a2b8509ec4d16490/torch-2.12.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f3/bd/d3348d62296e73a91420f0edf671450c9003ecd8704da40b1e3d9b868459/sphinxcontrib_mermaid-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl lint: channels: - url: https://conda.anaconda.org/conda-forge/ diff --git a/pyproject.toml b/pyproject.toml index 607f37c1..bb1f667e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,7 +131,8 @@ include = ["konfai", "konfai.*"] exclude = ["apps*", "konfai-apps*"] [tool.setuptools.package-data] -# Ship the declarative model catalog (konfai/models/yaml/*.yml) in the wheel. +# Ship the declarative model catalog (konfai/models/yaml/*.yml) and the PEP 561 marker in the wheel. +"konfai" = ["py.typed"] "konfai.models.yaml" = ["*.yml"] [tool.pixi.workspace] @@ -139,7 +140,9 @@ channels = ["conda-forge"] platforms = ["linux-64", "osx-arm64", "win-64"] [tool.pixi.pypi-dependencies] -konfai = { path = ".", editable = true } +# The dev extra IS the dev environment: one list, so `pixi run check` and CI (`pip install -e +# ".[dev]"`) exercise the same dependencies. Only twine and submitit are pixi-only additions. +konfai = { path = ".", editable = true, extras = ["dev"] } [tool.pixi.dependencies] python = ">=3.11,<3.14" @@ -155,24 +158,8 @@ docs = { features = ["default", "docs"], solve-group = "default" } # Feature: dev # --------------------------------------------------------------------------- [tool.pixi.feature.dev.pypi-dependencies] -pytest = "*" -pytest-cov = "*" -pytest-xdist = "*" -ruff = "==0.15.2" -build = "*" twine = "*" -mypy = "*" -types-requests = "*" submitit = "*" -SimpleITK = ">=2.0" -h5py = "*" -tensorboard = "*" -nvidia-ml-py = "*" -pydicom = "*" -zarr = ">=3" -ngff-zarr = ">=0.45" -dask = "*" -scikit-image = "*" [tool.pixi.feature.dev.tasks] test = { cmd = "pytest -q -n auto --dist loadfile tests/", description = "Run the test suite" } @@ -182,7 +169,7 @@ test-cov = { cmd = "pytest -n auto --dist loadfile --cov=konfai --cov-report=ter lint = { cmd = "ruff check konfai konfai-apps/konfai_apps konfai-mcp/konfai_mcp studio/konfai_studio apps tests", description = "Lint source code" } format = { cmd = "ruff format konfai konfai-apps/konfai_apps konfai-mcp/konfai_mcp studio/konfai_studio apps tests", description = "Format source code" } format-check = { cmd = "ruff format --check konfai konfai-apps/konfai_apps konfai-mcp/konfai_mcp studio/konfai_studio apps tests", description = "Check formatting without modifying files" } -typecheck = { cmd = "python -m mypy konfai konfai-apps/konfai_apps konfai-mcp/konfai_mcp studio/konfai_studio --ignore-missing-imports --no-site-packages", description = "Type-check all packages" } +typecheck = { cmd = "python -m mypy konfai konfai-apps/konfai_apps konfai-mcp/konfai_mcp studio/konfai_studio", description = "Type-check all packages ([tool.mypy] governs; real stubs in use)" } build = { cmd = "python -m build", description = "Build sdist and wheel" } check = { depends-on = ["lint", "format-check", "test", "test-apps"], description = "Run all quality checks" } diff --git a/tests/unit/test_packaging.py b/tests/unit/test_packaging.py index a933ab85..ac79d5ef 100644 --- a/tests/unit/test_packaging.py +++ b/tests/unit/test_packaging.py @@ -258,6 +258,9 @@ def test_wheel_ships_model_zoo_and_catalog(built_wheel: Path) -> None: missing_yaml = sorted(expected_yaml - names) assert not missing_yaml, f"catalog .yml files missing from the wheel: {missing_yaml}" + # PEP 561: without the marker every downstream type checker treats konfai as untyped. + assert "konfai/py.typed" in names, "py.typed missing from the wheel" + forbidden_top_level = {"apps", "konfai-apps", "konfai_apps", "konfai-mcp", "konfai_mcp"} leaked = sorted(n for n in names if n.split("/", 1)[0] in forbidden_top_level) assert not leaked, f"sibling package leaked into the wheel: {leaked[:5]}" From 1b5ed1e042bbb0eeef8d4d1bc0e2d896783c6b7c Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:37:15 +0200 Subject: [PATCH 16/28] fix(network): honest torch protocol, identity dedup, opt-in head resize state_dict/named_parameters/apply carried torch-incompatible signatures, which forced the dynamo-only export note and made DDP/FSDP compatibility a per-torch-version accident: the KonfAI aggregates move to network_states/graph_parameters/graph_apply and the protocol names get torch-native signatures (still skipping nested Networks; checkpoint keys byte-identical, pinned by the writer-vs-reader round trip). Nested networks dedup by object identity and a distinct-objects name collision refuses (it could never checkpoint correctly). The silent head resize-and-overlap-copy on every load is opt-in (allow_head_resize, YAML-bindable); the default is the natural shape error. Alias remaps match segment-aligned and rewrite only the leading prefix. The dead torch._jit_internal import is gone, _LRScheduler is the public name, and every documented scheduler now smoke-instantiates in tests (Warmup crashed on torch 2.12). --- konfai/export.py | 4 +- konfai/network/network/__init__.py | 5 - konfai/network/network/loaders.py | 4 +- konfai/network/network/model.py | 16 +- konfai/network/network/network.py | 137 ++++++++++------- konfai/predictor/__init__.py | 10 -- konfai/predictor/output.py | 4 +- konfai/predictor/workflow.py | 5 +- konfai/trainer.py | 13 +- tests/unit/model_oracles.py | 4 +- tests/unit/test_network.py | 231 +++++++++++++++++++++++++++-- tests/unit/test_schedulers.py | 89 +++++++++++ tests/unit/test_trainer.py | 11 +- 13 files changed, 428 insertions(+), 105 deletions(-) create mode 100644 tests/unit/test_schedulers.py diff --git a/konfai/export.py b/konfai/export.py index aca98a61..34361125 100644 --- a/konfai/export.py +++ b/konfai/export.py @@ -19,8 +19,8 @@ A trained model becomes ``model.onnx`` (graph + weights, single file) plus ``manifest.json`` (patch geometry, input/output spec) for a Python-free runtime. Three constraints shape the code: -* KonfAI ``Network`` overrides ``state_dict()`` with a custom signature that breaks the - TorchScript exporter, so the **dynamo** exporter is used. +* ``Network.named_forward`` is a Python generator, which TorchScript cannot script, so the + **dynamo** exporter is used. * ``Network.forward`` returns per-output-group results (empty without ``init()``), so the graph is reached via ``named_forward`` and a named head is selected. * The dynamo exporter writes weights as external data; they are inlined so the ``.onnx`` is diff --git a/konfai/network/network/__init__.py b/konfai/network/network/__init__.py index f26cc667..dfb9bdb0 100644 --- a/konfai/network/network/__init__.py +++ b/konfai/network/network/__init__.py @@ -28,17 +28,12 @@ from konfai.network.network.loaders import TargetCriterionsLoader as TargetCriterionsLoader from konfai.network.network.loaders import build_configured_criterions as build_configured_criterions from konfai.network.network.measure import Measure as Measure -from konfai.network.network.measure import _RunningNanMean as _RunningNanMean -from konfai.network.network.measure import _tail as _tail from konfai.network.network.model import Model as Model from konfai.network.network.model import ModelLoader as ModelLoader from konfai.network.network.network import MinimalModel as MinimalModel from konfai.network.network.network import ModuleArgsDict as ModuleArgsDict from konfai.network.network.network import Network as Network from konfai.network.network.network import OutputsGroup as OutputsGroup -from konfai.network.network.network import _channels_last as _channels_last -from konfai.network.network.network import _flat_downsampling as _flat_downsampling -from konfai.network.network.network import _leaf_spatial_stride as _leaf_spatial_stride __all__ = [ "CriterionsAttr", diff --git a/konfai/network/network/loaders.py b/konfai/network/network/loaders.py index 34fe7009..b652a878 100644 --- a/konfai/network/network/loaders.py +++ b/konfai/network/network/loaders.py @@ -54,7 +54,7 @@ def __init__(self, nb_step: int = 0) -> None: def getschedulers( self, key: str, scheduler_classname: str, optimizer: torch.optim.Optimizer - ) -> torch.optim.lr_scheduler._LRScheduler: + ) -> torch.optim.lr_scheduler.LRScheduler: for m in ["torch.optim.lr_scheduler", "konfai.metric.schedulers"]: module, name = get_module(scheduler_classname, m) if hasattr(module, name): @@ -74,7 +74,7 @@ class LossSchedulersLoader: def __init__(self, nb_step: int = 0) -> None: self.nb_step = nb_step - def getschedulers(self, key: str, scheduler_classname: str) -> torch.optim.lr_scheduler._LRScheduler: + def getschedulers(self, key: str, scheduler_classname: str) -> Scheduler: return apply_config(f"{key}.{scheduler_classname}")( getattr(importlib.import_module("konfai.metric.schedulers"), scheduler_classname) )() diff --git a/konfai/network/network/model.py b/konfai/network/network/model.py index fad21504..9782b98a 100644 --- a/konfai/network/network/model.py +++ b/konfai/network/network/model.py @@ -38,8 +38,18 @@ class ModelLoader: """Instantiate the root model graph declared in the active configuration.""" - def __init__(self, classpath: str = "default|segmentation.UNet.UNet") -> None: + def __init__(self, classpath: str = "default|segmentation.UNet.UNet", allow_head_resize: bool = False) -> None: self.classpath = classpath + self.allow_head_resize = allow_head_resize + + def _apply_options(self, model: Network) -> Network: + # The loader can only ENABLE the head resize: a model class that opted in through its own + # constructor keeps it when the config default (False) says nothing. + if self.allow_head_resize: + for module in model.modules(): + if isinstance(module, Network): + module.allow_head_resize = True + return model def _yaml_path(self) -> Path | None: raw_path = self.classpath.split("|", maxsplit=1)[-1] @@ -110,7 +120,7 @@ def builder( ) model = apply_config(f"{konfai_args}.{name}")(builder)(konfai_without=konfai_without if not train else []) - return model + return self._apply_options(model) classpath = self.classpath # A config that references a built-in model by the absolute path konfai.models..: @@ -139,7 +149,7 @@ def builder( ) model.set_name(name) - return model + return self._apply_options(model) class Model: diff --git a/konfai/network/network/network.py b/konfai/network/network/network.py index 3ec1309f..24a6671b 100644 --- a/konfai/network/network/network.py +++ b/konfai/network/network/network.py @@ -18,6 +18,7 @@ """The routed module graph and the Network built on it.""" import inspect +import logging import os from abc import ABC from collections import OrderedDict @@ -27,7 +28,6 @@ from typing import Any, Self import torch -from torch._jit_internal import _copy_to_script_wrapper from torch.utils.checkpoint import checkpoint from konfai import konfai_root @@ -48,6 +48,8 @@ from konfai.utils.errors import ConfigError from konfai.utils.runtime import State, get_device, get_gpu_memory +_log = logging.getLogger(__name__) + def _leaf_spatial_stride(module: torch.nn.Module) -> list[int] | None: """Per-axis stride of a leaf that shrinks the grid (a ``Conv``, ``MaxPool`` or ``AvgPool``), else @@ -208,15 +210,12 @@ def __getitem__(self, key: str) -> torch.nn.Module: raise ValueError(f"Module '{key}' is None or missing in self._modules") return module - @_copy_to_script_wrapper def keys(self) -> Iterable[str]: return self._modules.keys() - @_copy_to_script_wrapper def items(self) -> Iterable[tuple[str, torch.nn.Module | None]]: return self._modules.items() - @_copy_to_script_wrapper def values(self) -> Iterable[torch.nn.Module | None]: return self._modules.values() @@ -250,6 +249,13 @@ def get_mapping(self): count = dict.fromkeys(set(module.get_mapping().values()), 0) if len(count): for k, v in module.get_mapping().items(): + if count[v] >= len(module_args.alias): + raise ConfigError( + f"Module '{name}' declares {len(module_args.alias)} alias(es) but its graph " + f"maps at least {count[v] + 1} entries onto '{v}'.", + "Alias lists are positional: one alias per mapped occurrence. Extend the " + "'alias' list of add_module to cover every occurrence.", + ) alias_name = module_args.alias[count[v]] if k == "": results.update({alias_name: name + "." + v}) @@ -397,13 +403,17 @@ def forward(self, *input: torch.Tensor) -> torch.Tensor: pass return _v - def named_parameters( - self, pretrained: bool = False, recurse=False - ) -> Iterator[tuple[str, torch.nn.parameter.Parameter]]: + def graph_parameters(self, pretrained: bool = False) -> Iterator[tuple[str, torch.nn.parameter.Parameter]]: + """The routed graph's trainable parameters, named by dotted module path. + + Unlike ``named_parameters`` (torch semantics, untouched), this walk honours the graph + metadata: a module gated off by ``training=False`` is skipped, and ``pretrained=True`` + keeps only the modules declared ``pretrained=False``. + """ for name, module_args in self._modulesArgs.items(): module = self[name] if isinstance(module, ModuleArgsDict): - for k, v in module.named_parameters(pretrained=pretrained): + for k, v in module.graph_parameters(pretrained=pretrained): yield name + "." + k, v elif isinstance(module, torch.nn.Module): if not pretrained or not module_args.pretrained: @@ -411,10 +421,6 @@ def named_parameters( for k, v in module.named_parameters(): yield name + "." + k, v - def parameters(self, pretrained: bool = False): - for _, v in self.named_parameters(pretrained=pretrained): - yield v - def named_module_args_dict(self) -> Iterator[tuple[str, Self, ModuleArgs]]: for name, module in self._modules.items(): yield name, module, self._modulesArgs[name] @@ -491,7 +497,7 @@ class Network(ModuleArgsDict, ABC): def _apply_network( self, name_function: Callable[[Self], str], - networks: list[str], + networks: dict[str, "Network"], key: str, function: Callable, *args, @@ -505,18 +511,29 @@ def _apply_network( results: dict[str, object] = {} for module in self.values(): if isinstance(module, Network): - if name_function(module) not in networks: - networks.append(name_function(module)) - for k, v in module._apply_network( - name_function, - networks, - key + "." + name_function(module), - function, - *args, - root=root, - **kwargs, - ).items(): - results.update({name_function(self) + "." + k: v}) + name = name_function(module) + known = networks.get(name) + if known is module: + # The same object under several module names (a GAN's shared discriminator) + # is visited once. + continue + if known is not None: + raise ConfigError( + f"Two distinct networks share the name '{name}' in the graph of '{name_function(root)}'.", + "The name is the checkpoint key: a collision cannot be saved or resumed " + "correctly. Give one of them its own name with set_name().", + ) + networks[name] = module + for k, v in module._apply_network( + name_function, + networks, + key + "." + name, + function, + *args, + root=root, + **kwargs, + ).items(): + results.update({name_function(self) + "." + k: v}) param_names = {param.name for param in inspect.signature(function).parameters.values()} if "key" in param_names: function = partial(function, key=key) @@ -531,7 +548,7 @@ def _function_network_d(function: Callable): def new_function(self: Self, *args, **kwargs) -> dict[str, object]: return self._apply_network( lambda network: network.get_name(), - [], + {}, self.get_name(), function, *args, @@ -553,6 +570,7 @@ def __init__( init_type: str = "normal", init_gain: float = 0.02, dim: int = 3, + allow_head_resize: bool = False, ) -> None: super().__init__() self.name = self.__class__.__name__ @@ -561,7 +579,7 @@ def __init__( self.optimizer: torch.optim.Optimizer | None = None self.lr_schedulers_loader = schedulers - self.schedulers: dict[torch.optim.lr_scheduler._LRScheduler, int] = {} + self.schedulers: dict[torch.optim.lr_scheduler.LRScheduler, int] = {} self.outputs_criterions_loader = outputs_criterions self.measure: Measure | None = None @@ -572,21 +590,34 @@ def __init__( self.init_type = init_type self.init_gain = init_gain self.dim = dim + #: Opt-in: a checkpoint head whose out-channels mismatch may be re-initialised and + #: overlap-copied instead of failing the load (transfer to a different label set). + self.allow_head_resize = allow_head_resize self._it = 0 self._nb_lr_update = 0 self.outputsGroup: list[OutputsGroup] = [] @_function_network() - def state_dict(self) -> dict[str, OrderedDict]: - destination: OrderedDict[str, Any] = OrderedDict() + def network_states(self) -> OrderedDict: + """Per-network flat state, keyed by ``get_name()`` (dotted for nested networks): the + checkpoint's ``Model`` entry. The decorated call returns ``dict[str, OrderedDict]``.""" + return self.state_dict() + + def state_dict( # type: ignore[override] + self, *, destination: OrderedDict | None = None, prefix: str = "", keep_vars: bool = False + ) -> OrderedDict: + """This network's own flat state under the torch signature, skipping nested ``Network`` + children: each owns its optimizer/state and is saved under its own ``network_states`` key.""" + if destination is None: + destination = OrderedDict() local_metadata = {"version": self._version} - self._save_to_state_dict(destination, "", False) + self._save_to_state_dict(destination, prefix, keep_vars) for name, module in self._modules.items(): if module is not None: if not isinstance(module, Network): - module.state_dict(destination=destination, prefix="" + name + ".", keep_vars=False) + module.state_dict(destination=destination, prefix=prefix + name + ".", keep_vars=keep_vars) for hook in self._state_dict_hooks.values(): - hook_result = hook(self, destination, "", local_metadata) + hook_result = hook(self, destination, prefix, local_metadata) if hook_result is not None: destination = hook_result return destination @@ -623,10 +654,15 @@ def load(module: torch.nn.Module, prefix=""): current_size = child.weight.shape[0] last_size = state_dict[weight_key].shape[0] - if current_size != last_size: - print( - f"Warning: The size of '{prefix + name}' has changed from {last_size}" - f" to {current_size}. Please check for potential impacts" + # Opt-in only: without allow_head_resize the mismatch falls through to the + # strict load below and raises, naming the tensor and both shapes. + if current_size != last_size and self.allow_head_resize: + _log.warning( + "The size of '%s' has changed from %s to %s: re-initialised and " + "overlap-copied (allow_head_resize).", + prefix + name, + last_size, + current_size, ) ModuleArgsDict.init_func(child, self.init_type, self.init_gain) @@ -666,13 +702,13 @@ def load(module: torch.nn.Module, prefix=""): f"Error(s) in loading state_dict for {self.__class__.__name__}:\n\t{formatted_errors}", ) - def apply(self, fn: Callable[[torch.nn.Module], None]) -> None: + def graph_apply(self, fn: Callable[[torch.nn.Module], None]) -> None: """ - Apply ``fn`` to each non-KonfAI child module and finally to ``self``. + Apply ``fn`` to each non-``Network`` child module and finally to ``self``. - This overrides ``torch.nn.Module.apply`` so the recursive traversal can - skip nested ``Network`` instances and keep KonfAI's graph semantics - intact. + Nested ``Network`` instances are skipped: each owns its own state (init, load), so a + fan-out over the graph applies ``fn`` per network, never twice through a parent. + ``torch.nn.Module.apply`` keeps its native signature and full recursion. """ for module in self.children(): if not isinstance(module, Network): @@ -693,7 +729,7 @@ def load( # `key` here, so a nested network resumes its own state instead of silently missing the bare-name key. state_key = key if key is not None else self.get_name() if init: - self.apply( + self.graph_apply( partial( ModuleArgsDict.init_func, init_type=self.init_type, @@ -714,16 +750,13 @@ def load( for alias in model_state_dict_tmp.keys(): prefix = ".".join(alias.split(".")[:-1]) - alias_list = [ - (".".join(prefix.split(".")[: len(i.split("."))]), v) - for i, v in modules_name.items() - if prefix.startswith(i) - ] + # Segment-aligned: alias 'layer1' must not claim a module 'layer10', and only the + # leading prefix is rewritten, never a later occurrence of the same substring. + alias_list = [(a, b) for a, b in modules_name.items() if prefix == a or prefix.startswith(a + ".")] if len(alias_list): - for a, b in alias_list: - model_state_dict[alias.replace(a, b)] = model_state_dict_tmp[alias] - break + a, b = alias_list[0] + model_state_dict[b + alias[len(a) :]] = model_state_dict_tmp[alias] else: model_state_dict[alias] = model_state_dict_tmp[alias] self.load_state_dict(model_state_dict) @@ -859,7 +892,9 @@ def init(self, autocast: bool, state: State, group_dest: list[str], key: str, ro if self.measure is not None: self.measure.scaler = self.scaler if self.optimizerLoader: - self.optimizer = self.optimizerLoader.get_optimizer(key, self.parameters(False)) + self.optimizer = self.optimizerLoader.get_optimizer( + key, (parameter for _, parameter in self.graph_parameters()) + ) self.optimizer.zero_grad() if self.lr_schedulers_loader and self.optimizer: diff --git a/konfai/predictor/__init__.py b/konfai/predictor/__init__.py index a87decc7..828da63d 100644 --- a/konfai/predictor/__init__.py +++ b/konfai/predictor/__init__.py @@ -25,20 +25,10 @@ from konfai.data.reduction import Median as Median from konfai.data.reduction import Reduction as Reduction from konfai.predictor.ensemble import ModelComposite as ModelComposite -from konfai.predictor.ensemble import _colocate_loaded_modules as _colocate_loaded_modules -from konfai.predictor.loop import _DESCRIPTION_EVERY as _DESCRIPTION_EVERY -from konfai.predictor.loop import _prediction_report as _prediction_report -from konfai.predictor.loop import _Predictor as _Predictor -from konfai.predictor.output import _STREAM_WORTH_MIN_FRACTION as _STREAM_WORTH_MIN_FRACTION from konfai.predictor.output import PREDICTION_CLOCK as PREDICTION_CLOCK from konfai.predictor.output import OutputDataset as OutputDataset from konfai.predictor.output import OutputDatasetLoader as OutputDatasetLoader from konfai.predictor.output import OutSameAsGroupDataset as OutSameAsGroupDataset -from konfai.predictor.output import _AsyncWriter as _AsyncWriter -from konfai.predictor.output import _FinalizeStage as _FinalizeStage -from konfai.predictor.output import _RegionState as _RegionState -from konfai.predictor.output import _slab_context as _slab_context -from konfai.predictor.output import _StreamPlan as _StreamPlan from konfai.predictor.workflow import Predictor as Predictor from konfai.predictor.workflow import build_predict as build_predict from konfai.predictor.workflow import predict as predict diff --git a/konfai/predictor/output.py b/konfai/predictor/output.py index a7ffed5b..487f7821 100644 --- a/konfai/predictor/output.py +++ b/konfai/predictor/output.py @@ -40,12 +40,10 @@ SlabAligner, SlabRegionStream, StreamingAccumulator, - _halo_radii, - _HaloPull, - _RemapPull, blend_axes, blend_overlap, ) +from konfai.data.patching.stage import _halo_radii, _HaloPull, _RemapPull from konfai.data.reduction import Mean, Median, Reduction from konfai.data.transform import ( LocalityKind, diff --git a/konfai/predictor/workflow.py b/konfai/predictor/workflow.py index b195f043..c888adf7 100644 --- a/konfai/predictor/workflow.py +++ b/konfai/predictor/workflow.py @@ -516,8 +516,9 @@ def predict( """ Build and execute the configured prediction workflow. - This compatibility wrapper preserves the historical CLI-facing API while - delegating the pure build step to :func:`build_predict`. + ``overwrite``/``gpu``/``cpu``/``quiet``/``tensorboard`` are load-bearing even though the body + drops them: :func:`run_distributed_app` reads them from the bound signature to drive the launch. + The pure build step is :func:`build_predict`. """ del overwrite, gpu, cpu, quiet, tensorboard return build_predict( diff --git a/konfai/trainer.py b/konfai/trainer.py index d823685b..8da5a753 100644 --- a/konfai/trainer.py +++ b/konfai/trainer.py @@ -661,11 +661,11 @@ def checkpoint_save(self, loss: float | None) -> None: "epoch": self.epoch, "it": self.it, "loss": checkpoint_loss, - "Model": self.model.module.state_dict(), + "Model": self.model.module.network_states(), } if self.model_ema is not None: - save_dict["Model_EMA"] = self.model_ema.module.state_dict() + save_dict["Model_EMA"] = self.model_ema.module.network_states() save_dict["Model_EMA_n_averaged"] = int(self.model_ema.n_averaged) save_dict.update( @@ -1234,8 +1234,9 @@ def train( """ Build and execute the configured training workflow. - This compatibility wrapper preserves the historical CLI-facing API while - delegating the pure build step to :func:`build_train`. + ``overwrite``/``gpu``/``cpu``/``quiet``/``tensorboard`` are load-bearing even though the body + drops them: :func:`run_distributed_app` reads them from the bound signature to drive the launch. + The pure build step is :func:`build_train`. """ del overwrite, gpu, cpu, quiet, tensorboard return build_train( @@ -1246,7 +1247,3 @@ def train( statistics_dir=statistics_dir, lr=lr, ) - - -if __name__ == "__main__": - train(State.TRAIN, False, None) diff --git a/tests/unit/model_oracles.py b/tests/unit/model_oracles.py index 6c34054e..85accb4f 100644 --- a/tests/unit/model_oracles.py +++ b/tests/unit/model_oracles.py @@ -34,8 +34,8 @@ def seeded_input(*shape: int, seed: int = 0) -> torch.Tensor: def flat_state_dict(net: Network) -> dict[str, torch.Tensor]: - """The network's own tensors (``Network.state_dict`` nests them under the network name).""" - return net.state_dict()[net.get_name()] + """The network's own tensors (``Network.network_states`` nests them under the network name).""" + return net.network_states()[net.get_name()] def terminal_output_paths(net: Network) -> list[str]: diff --git a/tests/unit/test_network.py b/tests/unit/test_network.py index 068576ce..18cb60a9 100644 --- a/tests/unit/test_network.py +++ b/tests/unit/test_network.py @@ -29,7 +29,8 @@ import torch from konfai.metric.schedulers import Constant, PolyLRScheduler from konfai.network.blocks import Add -from konfai.network.network import CriterionsAttr, Measure, ModuleArgsDict, Network, _channels_last +from konfai.network.network import CriterionsAttr, Measure, ModuleArgsDict, Network +from konfai.network.network.network import _channels_last from konfai.utils.dataset import Attribute from konfai.utils.errors import ConfigError, MeasureError @@ -186,12 +187,13 @@ def __init__(self, fc_out: int) -> None: self.add_module("head", torch.nn.Linear(4, 2)) old = _Net(fc_out=4) - # Network.state_dict() wraps the flat params under the network name; load_state_dict + # Network.network_states() wraps the flat params under the network name; load_state_dict # consumes that inner flat dict ("fc.weight", ...). - inner = next(iter(old.state_dict().values())) + inner = next(iter(old.network_states().values())) checkpoint = {key: value.clone() for key, value in inner.items()} new = _Net(fc_out=6) # fc output grows 4 -> 6 (resized); head is unchanged + new.allow_head_resize = True # the resize path is opt-in new.load_state_dict(checkpoint) # must not raise fc = new["fc"] @@ -203,6 +205,206 @@ def __init__(self, fc_out: int) -> None: assert torch.equal(head.bias, checkpoint["head.bias"]) +class _ResizableNet(Network): + def __init__(self, fc_out: int) -> None: + super().__init__(in_channels=1) + self.add_module("fc", torch.nn.Linear(4, fc_out)) + + +def test_load_state_dict_shape_mismatch_raises_without_opt_in() -> None: + """The overlap-copy resize is opt-in (``allow_head_resize``): by default a checkpoint whose + out-channels disagree with the model fails the load, naming the tensor and both shapes.""" + checkpoint = next(iter(_ResizableNet(fc_out=4).network_states().values())) + + with pytest.raises(RuntimeError) as excinfo: + _ResizableNet(fc_out=6).load_state_dict(checkpoint) + + message = str(excinfo.value) + assert "fc.weight" in message + assert "[4, 4]" in message and "[6, 4]" in message + + +def test_opt_in_head_resize_warns_through_the_logger(caplog: pytest.LogCaptureFixture) -> None: + import logging + + checkpoint = next(iter(_ResizableNet(fc_out=4).network_states().values())) + new = _ResizableNet(fc_out=6) + new.allow_head_resize = True + + with caplog.at_level(logging.WARNING, logger="konfai.network.network.network"): + new.load_state_dict(checkpoint) + + assert any("fc" in record.getMessage() for record in caplog.records) + assert torch.equal(new["fc"].weight[:4], checkpoint["fc.weight"]) + + +# -------------------------------------------------------------------------------------- +# _apply_network: dedup by object identity, refuse a genuine name collision +# -------------------------------------------------------------------------------------- + + +def test_two_distinct_networks_sharing_a_name_raise_config_error() -> None: + """Dedup by NAME silently dropped a second distinct same-named sibling from every fan-out + (init/backward/checkpoint). Dedup is by object identity; a genuine collision refuses, + because the name is the checkpoint key and could never save correctly.""" + + class Leaf(Network): + def __init__(self) -> None: + super().__init__(in_channels=1, dim=2) + self.add_module("Conv", torch.nn.Conv2d(1, 1, 1)) + + def get_name(self) -> str: + return "Gen" + + class Root(Network): + def __init__(self) -> None: + super().__init__(in_channels=1, dim=2) + self.add_module("A", Leaf()) + self.add_module("B", Leaf()) + + with pytest.raises(ConfigError, match="share the name 'Gen'"): + Root().get_networks() + + +def test_same_network_object_under_several_module_names_is_visited_once() -> None: + """The shipped Gan adds ONE discriminator under three module names: identity dedup keeps + that pattern working (one name, one visit, no collision).""" + + class Leaf(Network): + def __init__(self) -> None: + super().__init__(in_channels=1, dim=2) + self.add_module("Conv", torch.nn.Conv2d(1, 1, 1)) + + class Root(Network): + def __init__(self, leaf: Network) -> None: + super().__init__(in_channels=1, dim=2) + self.add_module("A", leaf) + self.add_module("B", leaf) + self.add_module("C", leaf) + + leaf = Leaf() + networks = Root(leaf).get_networks() + assert set(networks.keys()) == {"Root.Leaf", "Root"} + assert networks["Root.Leaf"] is leaf + + +# -------------------------------------------------------------------------------------- +# torch protocol names keep torch signatures; the KonfAI aggregates carry KonfAI names +# -------------------------------------------------------------------------------------- + + +def test_torch_protocol_signatures_are_honoured_and_state_dict_skips_nested_networks() -> None: + """``state_dict``/``named_parameters`` accept torch's kwargs (DDP/tooling pass + prefix/recurse/remove_duplicate); the aggregates live on ``network_states``/ + ``graph_parameters``. The flat ``state_dict`` still skips nested Networks: each owns its + own checkpoint entry.""" + + class Sub(Network): + def __init__(self) -> None: + super().__init__(in_channels=1, dim=2) + self.add_module("Conv", torch.nn.Conv2d(1, 1, 1)) + + class Root(Network): + def __init__(self) -> None: + super().__init__(in_channels=1, dim=2) + self.add_module("Head", torch.nn.Conv2d(1, 1, 1)) + self.add_module("Sub", Sub()) + + root = Root() + + flat = root.state_dict(prefix="m.", keep_vars=True) + assert set(flat) == {"m.Head.weight", "m.Head.bias"} # nested Sub excluded, prefix honoured + assert flat["m.Head.weight"].requires_grad # keep_vars honoured + + named = dict(root.named_parameters(prefix="p", recurse=True, remove_duplicate=True)) + assert "p.Head.weight" in named and "p.Sub.Conv.weight" in named + + aggregate = root.network_states() + assert set(aggregate) == {"Root", "Root.Sub"} + assert set(aggregate["Root"]) == {"Head.weight", "Head.bias"} + assert set(aggregate["Root.Sub"]) == {"Conv.weight", "Conv.bias"} + + assert [name for name, _ in root.graph_parameters()] == [ + "Head.weight", + "Head.bias", + "Sub.Conv.weight", + "Sub.Conv.bias", + ] + + +# -------------------------------------------------------------------------------------- +# Alias remapping (Network.load) and positional alias pairing (get_mapping) +# -------------------------------------------------------------------------------------- + + +def test_alias_remap_is_segment_aligned_and_rewrites_only_the_leading_prefix() -> None: + """Checkpoint keys are remapped by module alias: 'layer1' must not claim 'layer10.*' + (non-segment ``startswith`` did), and only the leading prefix is rewritten.""" + + class _Aliased(Network): + def __init__(self) -> None: + super().__init__(in_channels=1, dim=2) + self.add_module("A", torch.nn.Conv2d(1, 1, 1), alias=["layer1"]) + self.add_module("B", torch.nn.Conv2d(1, 1, 1), alias=["layer10"]) + + checkpoint = { + "Model": { + "_Aliased": { + "layer1.weight": torch.full((1, 1, 1, 1), 1.0), + "layer1.bias": torch.tensor([1.0]), + "layer10.weight": torch.full((1, 1, 1, 1), 2.0), + "layer10.bias": torch.tensor([2.0]), + } + } + } + net = _Aliased() + net.load(cast(Any, checkpoint), init=False) + + assert torch.equal(net["A"].weight, torch.full((1, 1, 1, 1), 1.0)) + assert torch.equal(net["B"].weight, torch.full((1, 1, 1, 1), 2.0)) + + +def test_model_loader_enables_head_resize_on_every_network_and_never_disables() -> None: + """``Model: allow_head_resize: true`` reaches every nested network; the loader default + (False) never overrides a model class that opted in through its own constructor.""" + from konfai.network.network import ModelLoader + + class Sub(Network): + def __init__(self) -> None: + super().__init__(in_channels=1, dim=2) + self.add_module("Conv", torch.nn.Conv2d(1, 1, 1)) + + class Root(Network): + def __init__(self) -> None: + super().__init__(in_channels=1, dim=2) + self.add_module("Sub", Sub()) + + enabled = ModelLoader(allow_head_resize=True)._apply_options(Root()) + assert all(net.allow_head_resize for net in [enabled, *enabled.get_networks().values()]) + + opted_in = Root() + opted_in.allow_head_resize = True + assert ModelLoader()._apply_options(opted_in).allow_head_resize + + +def test_get_mapping_alias_shortfall_raises_config_error_naming_the_module() -> None: + """Positional alias pairing raised a bare IndexError when a nested block maps more entries + than the alias list covers; the refusal names the module and the counts.""" + + class _Child(ModuleArgsDict): + def __init__(self) -> None: + super().__init__() + self.add_module("L", torch.nn.Conv2d(1, 1, 1), alias=["a1", "a2"]) + + class _Root(Network): + def __init__(self) -> None: + super().__init__(in_channels=1, dim=2) + self.add_module("Child", _Child(), alias=["p"]) + + with pytest.raises(ConfigError, match=r"Child.*1 alias"): + _Root().get_mapping() + + # -------------------------------------------------------------------------------------- # Measure.Loss: loss records feeding the gradient and the logging windows # -------------------------------------------------------------------------------------- @@ -282,29 +484,30 @@ def test_loss_add_does_not_read_a_loss_off_its_device() -> None: def test_measure_reads_every_unread_value_in_one_transfer(monkeypatch: pytest.MonkeyPatch) -> None: - # Two criteria over three batches, one of them handing a float in the middle: one stack per - # device reads the lot, each record keeps the order it recorded, and the floats equal `.item()`. + # Two criteria over three batches, one of them handing a float in the middle: one concatenated + # transfer per device reads the lot, each record keeps the order it recorded, and the floats + # equal `.item()`. loss = Measure.Loss("l", "out", "tgt", 0, is_loss=True, accumulation=False) metric = Measure.Loss("m", "out", "tgt", 0, is_loss=False, accumulation=False) for i in range(3): loss.add(1.0, torch.tensor(float(i))) metric.add(1.0, (torch.tensor([11.0]), 11.5) if i == 1 else torch.tensor([10.0 + i])) - stacked: list[int] = [] - original_stack = torch.stack + transfers: list[int] = [] + original_cat = torch.cat - def counting_stack(tensors, *args, **kwargs): - stacked.append(len(tensors)) - return original_stack(tensors, *args, **kwargs) + def counting_cat(tensors, *args, **kwargs): + transfers.append(len(tensors)) + return original_cat(tensors, *args, **kwargs) - monkeypatch.setattr(torch, "stack", counting_stack) + monkeypatch.setattr(torch, "cat", counting_cat) measure = _measure_of(loss, metric) assert measure.format_loss(True, 3) == {"l": (1.0, 1.0)} - assert stacked == [5] + assert transfers == [5] assert list(loss._values) == [0.0, 1.0, 2.0] assert list(metric._values) == [10.0, 11.5, 12.0] assert measure.get_last_values(3) == {"l": 1.0, "m": pytest.approx(33.5 / 3)} - assert stacked == [5] # nothing was left unread: no second transfer + assert transfers == [5] # nothing was left unread: no second transfer def test_whole_history_mean_is_a_running_mean_and_the_window_is_bounded() -> None: @@ -578,7 +781,7 @@ def with_optimizers(root: Network) -> Network: saved_sub._nb_lr_update = 7 # Mirror checkpoint_save: dotted get_networks() keys. - state_dict: dict = {"Model": source.state_dict()} + state_dict: dict = {"Model": source.network_states()} for name, net in source.get_networks().items(): if net.optimizer is not None: state_dict[f"{name}_optimizer_state_dict"] = net.optimizer.state_dict() diff --git a/tests/unit/test_schedulers.py b/tests/unit/test_schedulers.py new file mode 100644 index 00000000..8186207c --- /dev/null +++ b/tests/unit/test_schedulers.py @@ -0,0 +1,89 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Every scheduler documented in docs/source/reference/components/schedulers.md instantiates and +steps once: a torch signature change (LambdaLR dropped ``verbose``) otherwise ships as a +config-reachable crash that no test sees.""" + +import pytest +import torch +from konfai.metric.schedulers import Constant, CosineAnnealing, PolyLRScheduler, Warmup + + +def _optimizer() -> torch.optim.Optimizer: + return torch.optim.SGD([torch.nn.Parameter(torch.zeros(1))], lr=0.1) + + +# -------------------------------------------------------------------------------------- +# B. Learning-rate schedulers (built against a real optimizer) +# -------------------------------------------------------------------------------------- + + +def test_warmup_instantiates_and_steps() -> None: + optimizer = _optimizer() + scheduler = Warmup(optimizer, warmup_steps=4) + + assert optimizer.param_groups[0]["lr"] == pytest.approx(0.1 * 1 / 5) + optimizer.step() + scheduler.step() + assert optimizer.param_groups[0]["lr"] == pytest.approx(0.1 * 2 / 5) + + +def test_polylr_instantiates_and_steps() -> None: + optimizer = _optimizer() + scheduler = PolyLRScheduler(optimizer, initial_lr=0.1, max_steps=100) + + scheduler.step() + assert optimizer.param_groups[0]["lr"] == pytest.approx(0.1 * (1 - 1 / 100) ** 0.9) + + +@pytest.mark.parametrize( + ("name", "kwargs"), + [ + ("StepLR", {"step_size": 20, "gamma": 0.5}), + ("CosineAnnealingLR", {"T_max": 10}), + ("ReduceLROnPlateau", {}), + ], +) +def test_documented_torch_lr_schedulers_instantiate_and_step(name: str, kwargs: dict) -> None: + # The LR loader resolves against torch.optim.lr_scheduler first: the doc's torch examples + # must construct and step against a real optimizer on the installed torch. + optimizer = _optimizer() + scheduler = getattr(torch.optim.lr_scheduler, name)(optimizer, **kwargs) + optimizer.step() + if name == "ReduceLROnPlateau": + scheduler.step(0.5) + else: + scheduler.step() + assert optimizer.param_groups[0]["lr"] > 0.0 + + +# -------------------------------------------------------------------------------------- +# A. Criterion-weight schedulers (scalar, no optimizer) +# -------------------------------------------------------------------------------------- + + +def test_constant_steps_and_reports_its_value() -> None: + scheduler = Constant(value=2.0) + scheduler.step(5) + assert scheduler.get_value() == 2.0 + + +def test_cosine_annealing_steps_and_reports_the_annealed_value() -> None: + scheduler = CosineAnnealing(start_value=1.0, eta_min=0.0, t_max=100) + assert scheduler.get_value() == pytest.approx(1.0) + scheduler.step(50) + assert scheduler.get_value() == pytest.approx(0.5) diff --git a/tests/unit/test_trainer.py b/tests/unit/test_trainer.py index bf3aba2f..c606d9d1 100644 --- a/tests/unit/test_trainer.py +++ b/tests/unit/test_trainer.py @@ -47,7 +47,7 @@ def close(self) -> None: class _DummyModelModule: @staticmethod - def state_dict() -> dict[str, torch.Tensor]: + def network_states() -> dict[str, torch.Tensor]: return {"weight": torch.tensor([1.0])} @staticmethod @@ -219,7 +219,12 @@ def test_bootstrap_prefers_real_best_over_exit_checkpoint(tmp_path: Path, monkey def test_checkpoint_persists_ema_n_averaged(tmp_path: Path, monkeypatch) -> None: - base = nn.Linear(2, 2) + # checkpoint_save reads the EMA module through the Network contract (network_states). + class _Base(nn.Linear): + def network_states(self) -> dict[str, dict[str, torch.Tensor]]: + return {"Base": self.state_dict()} + + base = _Base(2, 2) ema = AveragedModel(base) ema.update_parameters(base) ema.update_parameters(base) @@ -248,7 +253,7 @@ def test_checkpoint_save_returns_before_the_file_lands_and_the_file_equals_the_l net(torch.ones(1, 3)).sum().backward() optimizer.step() network = SimpleNamespace(optimizer=optimizer, _it=4, _nb_lr_update=2, measure=None) - module = SimpleNamespace(state_dict=net.state_dict, get_networks=lambda: {"Net": network}) + module = SimpleNamespace(network_states=net.state_dict, get_networks=lambda: {"Net": network}) trainer = _build_trainer(tmp_path, monkeypatch, ["stamp"], model=SimpleNamespace(module=module)) gate = threading.Event() From 3517f39643188f874c7dabacd934ae727107bf63 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:39:37 +0200 Subject: [PATCH 17/28] test(metric): materialize deferred values in the reduction-contract helper The helper compared reported values as floats; they are 0-d tensors (or LabelledValues) since the deferred-readout change, and approx(nan) against a tensor crashed the assertion repr. Values were already equal. --- tests/unit/test_auto_patching.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_auto_patching.py b/tests/unit/test_auto_patching.py index 280247bd..158a2e7d 100644 --- a/tests/unit/test_auto_patching.py +++ b/tests/unit/test_auto_patching.py @@ -246,14 +246,24 @@ def _patches(shape, patch): return [(slice(None), slice(None), *sl) for sl in slices] @staticmethod - def _identity(metric, whole_args, patch_grids): - + def _materialized(value): + """A criterion's reported value as the evaluator records it: 0-d tensors read out to floats, + LabelledValues to a per-label dict (the deferred-readout contract).""" + from konfai.network.network.measure import LabelledValues + + if isinstance(value, LabelledValues): + return dict(zip(value.labels, value.values.tolist(), strict=True)) + if isinstance(value, torch.Tensor): + return float(value.item()) + return value + + def _identity(self, metric, whole_args, patch_grids): expected = metric(*whole_args) - expected_value = expected[1] if isinstance(expected, tuple) else expected.item() + expected_value = self._materialized(expected[1] if isinstance(expected, tuple) else expected.item()) for grid in patch_grids: states = [metric.partial_metric(*[t[sl] for t in whole_args]) for sl in grid] combined = metric.combine_metric(states) - got = combined[1] if isinstance(combined, tuple) else combined + got = self._materialized(combined[1] if isinstance(combined, tuple) else combined) if isinstance(expected_value, dict): assert set(got) == set(expected_value) for k, v in expected_value.items(): From 5aa2af4971bac99da8a76e4e3fc2df5000f88667 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 02:41:09 +0200 Subject: [PATCH 18/28] test(oracle): bisect the budget on the segment's own price The expansion routes derived their budget from the whole-chain sizer, which does not see a copy's draws; the run prices the segment (draws included) since the sizer went segment-keyed, so the bisected budget no longer bought the height it named and the sweep refused. The helper now asks the same rule the sweep spends by. --- tests/unit/oracle_support.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/oracle_support.py b/tests/unit/oracle_support.py index 5d9cd728..2ae329f2 100644 --- a/tests/unit/oracle_support.py +++ b/tests/unit/oracle_support.py @@ -818,7 +818,9 @@ def _sweep_height(manager: DatasetManager, segments: Sequence[SweepSegment]) -> heights = [] for segment in segments: try: - heights.append(manager._sweep_tile(segment.landing, segment.channels, segment.plans)[0]) + # The segment's own price (its stages, its store): the rule stream_refusal and the + # sweep spend the budget by -- a copy's draws included. + heights.append(manager.sizer_for(segment).sweep_tile()[0]) except DatasetManagerError: return 0 return min(heights, default=0) From 905a56926a21cf55be2795ab5f2ac50875a5f2e8 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 08:31:45 +0200 Subject: [PATCH 19/28] refactor: close the layering exceptions and shed three dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KonfAIInference moves to konfai-apps (core keeps a bare-name loader shim for published bundles; the AGENTS §7 exception clause is gone). Slicer's grep verdict keeps check_server/get_vram/get_ram in core, but their two GETs ride urllib, so requests leaves the dependency list with lxml (stdlib ElementTree serves the one XML sidecar, same trust boundary) and huggingface_hub (lazy, in the all extra; konfai-apps now declares its own). The 176 private re-exports across nine package __init__ files are deleted (internal modules and tests import the defining submodule; a guard test keeps the belt from regrowing). The run_distributed_app decorator raises on unknown kwargs instead of dropping them silently. konfai-mcp loses fine_tune_app (import_app + run_resume is the one fine-tune path; Studio prompts updated). AGENTS.md tells the truth again (registry table, dev extra, committed MCP fixtures); konfai-apps checkpoint fixtures build through network_states(), the aggregate state_dict() no longer is. --- .claude/skills/konfai-experiments/SKILL.md | 2 +- .../references/tool-reference.md | 16 +- .gitignore | 1 + AGENTS.md | 29 +- docs/scripts/generate_visual_gallery.py | 2 +- docs/source/concepts/datasets.md | 6 +- docs/source/examples/registration.md | 6 +- docs/source/getting-started/installation.md | 3 +- konfai-apps/konfai_apps/__init__.py | 4 + .../konfai_apps/transforms.py | 20 +- konfai-apps/setup.py | 14 +- .../test_konfai_app_client_remote.py | 2 +- .../tests/integration/test_konfai_apps.py | 4 +- .../tests/unit/test_app_server_helpers.py | 4 +- konfai-apps/tests/unit/test_bundle.py | 11 +- .../tests/unit/test_finetune_requires_loss.py | 18 +- konfai-apps/tests/unit/test_transforms.py | 202 +++++++++++ konfai-mcp/README.md | 6 +- konfai-mcp/konfai_mcp/capabilities.py | 4 +- konfai-mcp/konfai_mcp/dataset_inspection.py | 2 +- konfai-mcp/konfai_mcp/experiment_state.py | 5 +- konfai-mcp/konfai_mcp/guide.py | 34 +- konfai-mcp/konfai_mcp/runner.py | 48 --- konfai-mcp/konfai_mcp/server.py | 74 +--- konfai-mcp/konfai_mcp/server_apps.py | 83 +---- konfai-mcp/konfai_mcp/server_jobs.py | 5 - konfai-mcp/konfai_mcp/workflows.py | 5 +- konfai-mcp/tests/make_fixtures.py | 154 ++++++++ konfai-mcp/tests/test_experiment_state.py | 2 +- konfai-mcp/tests/test_live_parse.py | 25 ++ konfai-mcp/tests/test_mcp_server_apps.py | 88 +---- konfai-mcp/tests/test_mcp_server_cli.py | 4 +- .../tests/test_mcp_server_refine_loop.py | 9 - .../tests/test_mcp_server_tool_index.py | 1 - konfai-mcp/tests/test_workflow_registry.py | 2 +- konfai/__init__.py | 66 ++-- konfai/data/augmentation/__init__.py | 8 - konfai/data/case_reduction.py | 13 +- konfai/data/data_manager/__init__.py | 7 - konfai/data/materialize.py | 5 +- konfai/data/patching/__init__.py | 39 -- konfai/data/patching/sweep.py | 2 +- konfai/data/transform/__init__.py | 44 +-- konfai/evaluator.py | 5 +- konfai/metric/measure/__init__.py | 7 - konfai/transformer.py | 2 +- konfai/utils/dataset/__init__.py | 68 ---- konfai/utils/dataset/sitk_file.py | 16 +- konfai/utils/runtime/__init__.py | 13 - konfai/utils/runtime/distributed.py | 10 + konfai/utils/utils.py | 3 +- pixi.lock | 332 +----------------- pyproject.toml | 12 +- run_tests_here.py | 6 - studio/konfai_studio/agent.py | 6 +- studio/konfai_studio/workflow.py | 2 +- .../test_konfai_auto_patch_prediction.py | 5 +- tests/unit/test_case_expansion.py | 2 +- tests/unit/test_data_manager.py | 2 +- tests/unit/test_data_stream.py | 4 +- tests/unit/test_dataset.py | 23 +- tests/unit/test_dataset_backends.py | 6 +- tests/unit/test_dataset_statistics.py | 19 +- tests/unit/test_dataset_streaming.py | 8 +- tests/unit/test_imaging_formats.py | 2 +- tests/unit/test_itk_transform_backend.py | 4 +- tests/unit/test_measure.py | 6 +- tests/unit/test_mind_descriptor.py | 2 +- tests/unit/test_ome_zarr_data_surface.py | 7 +- tests/unit/test_package_exports.py | 44 +++ tests/unit/test_packaging.py | 3 +- tests/unit/test_predictor.py | 4 +- tests/unit/test_predictor_memory.py | 12 +- tests/unit/test_resample_to_reference.py | 2 +- tests/unit/test_resample_transform.py | 11 +- tests/unit/test_runtime.py | 20 ++ tests/unit/test_sampling.py | 2 +- tests/unit/test_save_streaming.py | 14 +- tests/unit/test_streamed_oracle_expansion.py | 10 +- tests/unit/test_streamed_tta.py | 3 +- tests/unit/test_streamed_write_dispatcher.py | 6 +- tests/unit/test_sweep_pipeline.py | 9 +- tests/unit/test_sweep_tiling.py | 18 +- tests/unit/test_transform_working_multiple.py | 1 - 84 files changed, 749 insertions(+), 1061 deletions(-) rename konfai/data/transform/inference.py => konfai-apps/konfai_apps/transforms.py (92%) create mode 100644 konfai-apps/tests/unit/test_transforms.py create mode 100644 konfai-mcp/tests/make_fixtures.py delete mode 100644 run_tests_here.py create mode 100644 tests/unit/test_package_exports.py diff --git a/.claude/skills/konfai-experiments/SKILL.md b/.claude/skills/konfai-experiments/SKILL.md index f383035c..356cfff0 100644 --- a/.claude/skills/konfai-experiments/SKILL.md +++ b/.claude/skills/konfai-experiments/SKILL.md @@ -34,7 +34,7 @@ This is the tool order verified by the segmentation and synthesis end-to-end tes the discovery steps only when the dataset and task are already understood. **Route first (cheapest fit wins)** -0. `list_apps` → `describe_app` → `run_app_infer`: when the user wants a RESULT, check whether a published app already solves it BEFORE authoring and training from scratch. `run_app_infer` / `run_app_evaluate` / `run_app_uncertainty` / `run_app_pipeline` run the app AS PUBLISHED (no config editing), and `fine_tune_app` adapts it to the user's dataset. `import_app` is the modify-then-run path: it copies the app into the session so it runs as a normal experiment (`run_prediction`, or `run_resume` with `weights_only=True` to fine-tune from its weights). `run_resume` (without `weights_only`) continues an interrupted session training. +0. `list_apps` → `describe_app` → `run_app_infer`: when the user wants a RESULT, check whether a published app already solves it BEFORE authoring and training from scratch. `run_app_infer` / `run_app_evaluate` / `run_app_uncertainty` / `run_app_pipeline` run the app AS PUBLISHED (no config editing). `import_app` is the modify-or-fine-tune path: it copies the app into the session so it runs as a normal experiment (`run_prediction`, or `run_resume` with `weights_only=True` to fine-tune from its weights on the user's dataset). `run_resume` (without `weights_only`) continues an interrupted session training. **Discover (dataset-driven)** 1. `browse_dataset` → `inspect_dataset`: choose the real dataset root, see groups + sampled stats (`include_stats=False` for a fast structural peek; `groups=[...]` when you need intensity ranges for normalization). diff --git a/.claude/skills/konfai-experiments/references/tool-reference.md b/.claude/skills/konfai-experiments/references/tool-reference.md index 04b7e3cf..2d934d4d 100644 --- a/.claude/skills/konfai-experiments/references/tool-reference.md +++ b/.claude/skills/konfai-experiments/references/tool-reference.md @@ -2,7 +2,7 @@ > GENERATED from the registry by `konfai-mcp/scripts/generate_tool_reference.py`: do not edit by hand. -62 tools, 4 prompts, 23 resources. The live equivalent is the `guide://tool-index` resource. +61 tools, 4 prompts, 23 resources. The live equivalent is the `guide://tool-index` resource. ## Tools @@ -36,7 +36,7 @@ Use when you want to remove the current session workspace. This deletes the work ### `describe_app` -Use to read one app's manifest so you can decide whether it matches the user's task: the app's free-text description is the primary signal, with the input/output modality confirming the fit. This resolves a single app and returns its app.json: display name, description, input and output modality (with volume types), inference/evaluation/uncertainty capabilities, checkpoints, and segmentation terminology. It is metadata-only and SAFE: it does not import the app's model code and does not pip-install its requirements (those happen only later, behind an explicit trust gate). Outputs: display_name, description, inputs, outputs, capabilities, checkpoints, terminology, next_actions. Next: run_app_infer / list_app_parameters / import_app / fine_tune_app when it fits (next_actions reflect the app's capabilities), or design_config_strategy if no app fits the task. +Use to read one app's manifest so you can decide whether it matches the user's task: the app's free-text description is the primary signal, with the input/output modality confirming the fit. This resolves a single app and returns its app.json: display name, description, input and output modality (with volume types), inference/evaluation/uncertainty capabilities, checkpoints, and segmentation terminology. It is metadata-only and SAFE: it does not import the app's model code and does not pip-install its requirements (those happen only later, behind an explicit trust gate). Outputs: display_name, description, inputs, outputs, capabilities, checkpoints, terminology, next_actions. Next: run_app_infer / list_app_parameters / import_app when it fits (next_actions reflect the app's capabilities), or design_config_strategy if no app fits the task. ### `describe_config_schema` @@ -70,10 +70,6 @@ Use to SAVE a HuggingFace / remote-cached app (optionally with tuned parameters) Use to EXPORT the full reproducibility record of one run: the job manifest (command, devices, environment snapshot with package versions and GPUs), the launch-time config snapshots' CONTENT, the post-run resolved config, every split's metrics, and a log tail: a Methods-section-grade record in one payload. It does not rerun anything. Caveat: resolved_config is read from the LIVE session config, which may have been rewritten since the run: the launch-time truth is config_snapshots. Outputs: job, manifest, config_snapshots (text), resolved_config, metrics per split, log_tail. Next: compare_runs or read_training_curves. -### `fine_tune_app` - -Use to TRAIN by starting from a published app instead of a blank slate: fine-tune an existing app's checkpoint(s) on the user's dataset, WITHOUT authoring or editing a config. This is the middle option between run_app_infer (use as-is, no training) and design_config_strategy (author a config and train from scratch); it is also the safer alternative to import_app + run_resume(weights_only=True), which needs the copied Config.yml to be edited by hand. It launches a tracked training job and writes a resolvable app bundle (config + code + fine-tuned checkpoint) to the output directory, which you can then run with run_app_infer. TRUST GATE: resolving the app imports its Python code and pip-installs its requirements, so pass allow_untrusted_code=True to confirm you trust the source. Local and HuggingFace apps only. It does not author a config or adapt the dataset layout for you. Training knobs are first-class parameters (epochs, it_validation, lr, batch_size); set_parameters is for the app's MODEL tunables (bare names) or any config key by its full dotted path. Outputs: a job payload (status, resources, next_actions) plus the bundle output path. Next: wait_for_job, then run_app_infer on the produced bundle (then run_app_evaluate to score and rank this fine-tune against other training trials via leaderboard / compare_runs). - ### `generate_folds` Use to SPLIT a dataset into K cross-validation folds: writes one case-list file per fold into the session workspace and returns the exact subset stanzas to paste into the configs. KonfAI's Dataset.subset accepts a case-list file ('folds/fold_0.txt' keeps those cases) and its '~file' negation (trains on every OTHER fold). Outputs: folds {fold_i: {cases, file, train_subset, eval_subset}}, how_to_use, next_actions. Next: write per-fold configs (distinct train_name each), then run_batch. @@ -88,7 +84,7 @@ Use to read the FULL evaluation metrics (per-case values + aggregates) of ONE na ### `import_app` -Use to RUN a published KonfAI app as a NORMAL experiment in this session: the single path to use a local or HuggingFace app. It copies the app's config(s), custom code, and .pt checkpoints into the session root and pip-installs its requirements, so predict / fine-tune / evaluate then go through the ordinary run_prediction / run_resume / run_evaluation tools (no app-specific wrapper, no extra sub-folder). The copied checkpoints are returned so run_prediction can pass them as models, and run_resume(weights_only=True) warm-starts a fine-tune from them. TRUST GATE: copying+running the app's Python code and installing its requirements is the trust boundary, so you MUST pass allow_untrusted_code=True to confirm you trust the source. Local/HuggingFace apps only: a remote server keeps its code remote and cannot be imported (drive a remote app with konfai-apps directly). Outputs: imported_to, files, checkpoints, configs, next_actions. Next: run_prediction (pass checkpoints as models) / run_resume (fine-tune) / run_evaluation. +Use to RUN a published KonfAI app as a NORMAL experiment in this session. Prefer run_app_* when the app is used exactly as published; import_app is the tier for everything else: editing the config, fine-tuning (run_resume with weights_only=True), or wiring the app into a larger experiment. It copies the app's config(s), custom code, and .pt checkpoints into the session root and pip-installs its requirements, so predict / fine-tune / evaluate then go through the ordinary run_prediction / run_resume / run_evaluation tools (no app-specific wrapper, no extra sub-folder). The copied checkpoints are returned so run_prediction can pass them as models, and run_resume(weights_only=True) warm-starts a fine-tune from them. TRUST GATE: copying+running the app's Python code and installing its requirements is the trust boundary, so you MUST pass allow_untrusted_code=True to confirm you trust the source. Local/HuggingFace apps only: a remote server keeps its code remote and cannot be imported (drive a remote app with konfai-apps directly). Outputs: imported_to, files, checkpoints, configs, next_actions. Next: run_prediction (pass checkpoints as models) / run_resume (fine-tune) / run_evaluation. ### `import_experiment` @@ -128,7 +124,7 @@ Use when you need the current job registry state. This lists jobs for the curren ### `package_app_from_session` -Use to PACKAGE a model trained in the current session (the train-from-scratch branch) into a resolvable KonfAI app bundle: the same endpoint fine_tune_app produces, so a from-scratch run can also finish as a reusable app. It gathers the session's checkpoints and a config, writes an app.json from the metadata you give, and assembles a bundle (app.json + config + checkpoint + optional Model.py/requirements) that describe_app / run_app_infer / import_app can consume. It does not train, and it does not upload the bundle anywhere. Outputs: bundle_path, the packaged checkpoints/configs, next_actions (and onnx path if requested). Next: describe_app or run_app_infer on the produced bundle. +Use to PACKAGE a model trained in the current session (the train-from-scratch branch) into a resolvable KonfAI app bundle, so a from-scratch run can also finish as a reusable app. It gathers the session's checkpoints and a config, writes an app.json from the metadata you give, and assembles a bundle (app.json + config + checkpoint + optional Model.py/requirements) that describe_app / run_app_infer / import_app can consume. It does not train, and it does not upload the bundle anywhere. Outputs: bundle_path, the packaged checkpoints/configs, next_actions (and onnx path if requested). Next: describe_app or run_app_infer on the produced bundle. ### `plan_transform` @@ -212,7 +208,7 @@ Use after prediction config review/validation and when a checkpoint exists. This ### `run_resume` -Use to RESUME an interrupted or crashed training run from a checkpoint: model, optimizer, scheduler, and epoch/iteration counters are restored (KonfAI's RESUME command). Set weights_only=True instead to WARM-START a fine-tune from an imported app: load only the checkpoint's model weights and restart epoch/optimizer from scratch; prefer fine_tune_app when the app is used as published, since it needs no config editing. This launches a resumed training job from the current session Config.yml. It does not pick between runs: by default it resumes from the newest checkpoint of the configured run (falling back to the newest in the session), avoiding cross-run contamination. It trains up to the LIVE config's epochs: if the run already completed them, raise epochs in Config.yml first or the resume finishes immediately without adding checkpoints. Outputs: job payload with resources and next_actions; or, when a prerequisite is missing (dataset path, checkpoint), a blocker payload {ok, blocked, error, missing_paths, next_actions} with no job_id/status. Next: wait_for_job or read_live_metrics. +Use to RESUME an interrupted or crashed training run from a checkpoint: model, optimizer, scheduler, and epoch/iteration counters are restored (KonfAI's RESUME command). Set weights_only=True instead to WARM-START a fine-tune from an imported app: load only the checkpoint's model weights and restart epoch/optimizer from scratch (import_app + run_resume(weights_only=True) is THE fine-tune path for published apps). This launches a resumed training job from the current session Config.yml. It does not pick between runs: by default it resumes from the newest checkpoint of the configured run (falling back to the newest in the session), avoiding cross-run contamination. It trains up to the LIVE config's epochs: if the run already completed them, raise epochs in Config.yml first or the resume finishes immediately without adding checkpoints. Outputs: job payload with resources and next_actions; or, when a prerequisite is missing (dataset path, checkpoint), a blocker payload {ok, blocked, error, missing_paths, next_actions} with no job_id/status. Next: wait_for_job or read_live_metrics. ### `run_train` @@ -220,7 +216,7 @@ Use after train config review and validation succeed. This launches a training j ### `run_transform` -Use to apply a transform chain to a dataset with NO model: read, transform, write. Read plan_transform first: this writes a dataset, and the plan is what says how much and how. This launches a transform job from the session Transform.yml and returns structured job resources. The run replans, stores the plan, and refuses outright when a case can neither stream nor fit memory_budget. A case whose output already exists is skipped, so an interrupted run resumes; pass overwrite to recompute. Outputs: job payload with resources and next_actions; or a blocker payload when a prerequisite is missing. Next: wait_for_job then inspect_dataset on what it wrote. +Use to batch-process a dataset with a transform chain: read, transform, write. Read plan_transform first: this writes a dataset, and the plan is what says how much and how. This launches a transform job from the session Transform.yml and returns structured job resources. Pass gpu to run the chain on a GPU (the resample and intensity stages move with it; every write still lands on the host). The run replans, stores the plan, and refuses outright when a case can neither stream nor fit memory_budget. A case whose output already exists is skipped, so an interrupted run resumes; pass overwrite to recompute. Outputs: job payload with resources and next_actions; or a blocker payload when a prerequisite is missing. Next: wait_for_job then inspect_dataset on what it wrote. ### `set_live_tunables` diff --git a/.gitignore b/.gitignore index e2ef16cf..48c6ddc5 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ docs/_build/ build/ mcp-workspace/ KonfAI_Workspaces/ +konfai-mcp/tests/fixtures/ diff --git a/AGENTS.md b/AGENTS.md index 8c8169b5..5471dabd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,11 +85,10 @@ A third independent package (depends only on KonfAI's public API) exposing a **F **Working on the MCP server, how to validate a change:** -- **Synthetic fixtures:** `pixi run --environment dev python audit/make_fixtures.py` (note: `audit/` is - currently local-only/untracked, so commit it or regenerate it before relying on this flow elsewhere) builds a segmentation - dataset, a registration pair with a known translation, a synthesis pair, a 3-level OME-Zarr store, and - corrupted/unsupported inputs under `audit/fixtures/` (procedural, no patient data). Reuse these, do not - invent ad-hoc data in `/tmp`. +- **Synthetic fixtures:** `pixi run --environment dev python konfai-mcp/tests/make_fixtures.py` builds a + segmentation dataset, a registration pair with a known translation, a synthesis pair, a 3-level OME-Zarr + store, and corrupted/unsupported inputs under the gitignored `konfai-mcp/tests/fixtures/` (procedural, no + patient data). Reuse these, do not invent ad-hoc data in `/tmp`. - **Drive it black-box first, not by tool name.** Formulate a real objective ("segment these CT volumes"), then exercise the loop through a `fastmcp.Client` exactly as `test_mcp_server_segmentation_pipeline.py` does. A new tool is not "done" because it returns without an exception. @@ -105,9 +104,10 @@ A third independent package (depends only on KonfAI's public API) exposing a **F spawn subprocess (`run_api_in_subprocess`) and gate it behind `allow_untrusted_code` where applicable; (4) document per-parameter meaning via `Annotated[..., Field(description=...)]`, not only prose; (5) add a pytest that inspects the output. -- **Adding a workflow kind touches ~7 registries** (WORKFLOWS, WORKFLOW_CONFIG_FILES/ROOT_KEYS, runner - command map, capabilities `_WORKFLOW_ROOTS`, `Job.kind` Literal + retry map, GUIDE). Prefer one descriptor - table consumed everywhere over editing each. +- **Adding a workflow kind is one `WorkflowSpec` entry** in `konfai-mcp/konfai_mcp/workflows.py` plus the + two `Literal` aliases beside it (and a `GUIDE`/tool description); every other map (config filename, root + key, runner command, capabilities class, retry tool) derives from that table, and + `tests/test_workflow_registry.py` pins the derivations to it. - **Safety invariants to preserve:** validation/smoke-tests never execute in the server process; only `read/write_session_file` are path-jailed (dataset tools read arbitrary host paths by design, so keep it that way only for the trusted-local deployment, and never widen writes). `cancel_job` now reaps the whole @@ -128,7 +128,7 @@ pip install -e ./konfai-apps && pixi run --environment dev python -m pytest konf pip install -e ./konfai-mcp && pixi run --environment dev python -m pytest konfai-mcp/tests # mcp suite (separate) ``` -The Pixi `dev` env carries the imaging extras; a bare `pip install .[dev]` does not. `pixi run test` does **not** run `konfai-apps/tests` or `konfai-mcp/tests`; install those packages first (they pull their own runtime deps), exactly as their CI does. Install runtime extras with `pip install konfai[]` (`itk`, `hdf5`, `dicom`, `omezarr`, `imaging`, `tensorboard`, `lpips`, `ssim`, `fid`, `cluster`, `export`, …). +The Pixi `dev` env and a bare `pip install .[dev]` carry the same dependency list, imaging extras included (the `dev` extra IS the dev environment). `pixi run test` does **not** run `konfai-apps/tests` or `konfai-mcp/tests`; install those packages first (they pull their own runtime deps), exactly as their CI does. Install runtime extras with `pip install konfai[]` (`itk`, `hdf5`, `dicom`, `omezarr`, `imaging`, `tensorboard`, `lpips`, `ssim`, `cluster`, `export`, …). ## 6b. Releasing @@ -175,9 +175,10 @@ Conventional Commits only took hold at `v1.5.9`, and rendering further back emit - **`outputs_criterions` keys equal a module's dotted path**; the `:`/`.` separators are load-bearing. - **`state_dict` load/save does not recurse into nested `Network`s** (each owns its optimizer/state); alias lists are positional. - **The YAML model builder is the trusted/untrusted boundary**: only registry types, and module names contain no `.`. -- **`konfai-apps` is a separate package**; `apps/` is excluded from the `konfai` wheel. Core must never import - `konfai_apps` **at module level**. Known exception: `data/transform/inference.py` `KonfAIInference.infer_entry` does a - lazy, guarded import: a layering inversion pending an owner decision; do not add more. +- **`konfai-apps` is a separate package**; `apps/` is excluded from the `konfai` wheel. Core never imports + `konfai_apps`. The one sanctioned edge is the loader shim in `data/transform/__init__.py`: the bare stage + name `KonfAIInference` (published bundles spell it) resolves to `konfai_apps.transforms` when konfai-apps + is installed and refuses with the install hint otherwise; do not add more. - **The pretrained bridge fills every target tensor or raises**; never report a partial load as success. - **The config write is atomic** (temp + `os.replace`); a reader must never see a truncated config and bind all-defaults. @@ -213,7 +214,9 @@ Three, and only three, places decide trust. Keep them honest: `level='train_step'` does a real forward+backward. - **`transform_shape()` must be exact**: patch planning trusts it, a wrong prediction corrupts reassembly. - **Reading a config mutates it**, so snapshot bytes before any validation that builds a workflow. -- **Adding a workflow kind touches ~12 registries + ~8 `Literal`s**; prefer one descriptor table over editing each. +- **Adding a workflow kind**: in konfai-mcp this is one `WorkflowSpec` entry + two `Literal` aliases + (drift-tested, see §5b); in core it still touches several maps (`main.py` `_COMMANDS`/`_INIT_TARGETS`, + `State`, api.py). Prefer extending the descriptor tables over scattering new registries. - **Union coercion in the config binder is declaration-order-sensitive**: `overlap: 0.25` once bound `0` (lossy `int` won). Fixed; pinned by `test_config.py::test_apply_config_union_keeps_the_value_type_over_lossy_coercion`. Any new union-typed diff --git a/docs/scripts/generate_visual_gallery.py b/docs/scripts/generate_visual_gallery.py index 7d7fb716..964ed2d6 100644 --- a/docs/scripts/generate_visual_gallery.py +++ b/docs/scripts/generate_visual_gallery.py @@ -186,7 +186,7 @@ def main() -> None: noise.load(0.55) noise.state_init(0, [list(augmentation_source.shape[1:])], [Attribute()]) noisy = noise("IMAGE", 0, [augmentation_source.clone()])[0] - cutout = apply_augmentation(CutOUT(c_prob=1, cutout_size=0.34, value=-1), augmentation_source, seed=13) + cutout = apply_augmentation(CutOUT(cutout_size=0.34, value=-1), augmentation_source, seed=13) save_images( [ ("source", medical_image(augmentation_source, (-1, 1))), diff --git a/docs/source/concepts/datasets.md b/docs/source/concepts/datasets.md index c822c5c7..e9a92441 100644 --- a/docs/source/concepts/datasets.md +++ b/docs/source/concepts/datasets.md @@ -179,11 +179,11 @@ Three semantics are worth remembering: - `subset: None` keeps the full dataset; - `validation: None` disables the split; -- `~` exclusion applies to `subset` but **not** to `validation`. +- `subset` and `validation` accept the same selector spellings (slices, names, + files, `~` exclusion): one grammar, implemented by `Subset`. The `subset` object is applied before validation splitting and can exclude or -include items. The exact logic is implemented by `TrainSubset` and -`PredictionSubset`. +include items. ## Caching, augmentation, and patching diff --git a/docs/source/examples/registration.md b/docs/source/examples/registration.md index 0be404a6..f519c299 100644 --- a/docs/source/examples/registration.md +++ b/docs/source/examples/registration.md @@ -20,12 +20,12 @@ KonfAI checkout, install the ITK reader/writer and TensorBoard support used by training: ```bash -python -m pip install -e ".[itk,tensorboard,fid]" +python -m pip install -e ".[itk,tensorboard]" scipy cd examples/Registration ``` -`fid` is there for `scipy`, which `make_dataset.py` uses to apply the -displacement field; it is the only extra that carries it. +`scipy` is there for `make_dataset.py`, which uses it to apply the +displacement field; no KonfAI extra carries it. The commands below show GPU 0; replace `--gpu 0` with `--cpu 1` for a CPU-only run, but note the shipped configuration is 400 epochs at `256 × 256`: about diff --git a/docs/source/getting-started/installation.md b/docs/source/getting-started/installation.md index ca331ba7..4b5f0788 100644 --- a/docs/source/getting-started/installation.md +++ b/docs/source/getting-started/installation.md @@ -44,11 +44,10 @@ everything, `[dev]` adds the test, lint and docs tooling. | `smp` | `segmentation-models-pytorch` | the SMP model bridge, **required by `examples/Synthesis`** | | `lpips` | `lpips` | the `LPIPS` metric | | `ssim` | `scikit-image` | the `SSIM` metric | -| `fid` | `scipy`, `torchvision` | the `FID` metric | | `vtk` | `vtk` | VTK rendering and mesh features | | `export` | `onnx`, `onnxruntime`, `onnxscript` | ONNX export, see {doc}`../reference/python-api` | | `cluster` | `submitit` | the `konfai-cluster` submitter | -| `all` | everything above | one shot | +| `all` | everything above, plus `huggingface_hub` | one shot; `huggingface_hub` serves the `IMPACT*` criteria's feature-extractor downloads | | `dev` | pytest, ruff, mypy, sphinx, … | working on KonfAI itself | ## Running packaged apps diff --git a/konfai-apps/konfai_apps/__init__.py b/konfai-apps/konfai_apps/__init__.py index e400b1ee..b6f69ec3 100644 --- a/konfai-apps/konfai_apps/__init__.py +++ b/konfai-apps/konfai_apps/__init__.py @@ -18,11 +18,15 @@ from .app import AbstractKonfAIApp, KonfAIApp, KonfAIAppClient, run_distributed_app, run_remote_job from .cli import add_common_konfai_apps, main_apps, main_apps_server +from .transforms import DEFAULT_INFERENCE_MODEL_NAME, DEFAULT_INFERENCE_REPO_ID, KonfAIInference __all__ = [ + "DEFAULT_INFERENCE_MODEL_NAME", + "DEFAULT_INFERENCE_REPO_ID", "AbstractKonfAIApp", "KonfAIApp", "KonfAIAppClient", + "KonfAIInference", "add_common_konfai_apps", "main_apps", "main_apps_server", diff --git a/konfai/data/transform/inference.py b/konfai-apps/konfai_apps/transforms.py similarity index 92% rename from konfai/data/transform/inference.py rename to konfai-apps/konfai_apps/transforms.py index 03e6f1dd..72ca44a7 100644 --- a/konfai/data/transform/inference.py +++ b/konfai-apps/konfai_apps/transforms.py @@ -15,19 +15,22 @@ # SPDX-License-Identifier: Apache-2.0 -"""A KonfAI app run as a chain stage.""" +"""A KonfAI app run as a chain stage. + +Published configs spell the stage by its bare name (``KonfAIInference:``); core's transform package +resolves that name to this class when ``konfai-apps`` is installed. +""" import os import tempfile from multiprocessing import current_process, get_context from pathlib import Path +import SimpleITK as sitk import torch - from konfai import cuda_visible_devices -from konfai.data.transform.base import Transform, sitk +from konfai.data.transform import Transform from konfai.utils.dataset import Attribute, data_to_image, image_to_data -from konfai.utils.ITK import _require_simpleitk # Published app used by KonfAIInference when the configuration leaves repo/model unset. DEFAULT_INFERENCE_REPO_ID = "VBoussot/MRSegmentator-KonfAI" @@ -79,13 +82,7 @@ def infer_entry(self, dataset_path: Path, output_path: Path, gpu: list[int]): # footprint fits, so it runs at its trained patch_size, not a shrunk one. setdefault so an # explicit caller setting still wins. os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") - try: - from konfai_apps import KonfAIApp - except ImportError as exc: # pragma: no cover - depends on optional install - raise RuntimeError( - "KonfAIInference requires the standalone 'konfai-apps' package. " - "Install it from the repository with 'pip install -e ./konfai-apps'." - ) from exc + from konfai_apps import KonfAIApp # Nested KonfAI runs must choose their own rendezvous ports instead of # inheriting the parent's already-bound distributed settings. @@ -111,7 +108,6 @@ def __call__(self, name: str, tensor: torch.Tensor, cache_attribute: Attribute) "KonfAIInference cannot run inside daemon DataLoader workers. " "Use 'Dataset.num_workers: 0' for pipelines that include this transform." ) - _require_simpleitk() with tempfile.TemporaryDirectory() as tmpdir: dataset_path = Path(tmpdir) / "Dataset" if self.per_channel: diff --git a/konfai-apps/setup.py b/konfai-apps/setup.py index f83c0d63..e42f2cd6 100644 --- a/konfai-apps/setup.py +++ b/konfai-apps/setup.py @@ -33,4 +33,16 @@ def _release_version() -> str: _version = _release_version() -setup(install_requires=[f"konfai=={_version}", "SimpleITK", "fastapi", "uvicorn", "python-multipart"]) +# ``requests`` and ``huggingface_hub`` are declared here, not inherited: konfai core no longer +# depends on either. +setup( + install_requires=[ + f"konfai=={_version}", + "SimpleITK", + "requests", + "huggingface_hub", + "fastapi", + "uvicorn", + "python-multipart", + ] +) diff --git a/konfai-apps/tests/integration/test_konfai_app_client_remote.py b/konfai-apps/tests/integration/test_konfai_app_client_remote.py index 5b52960b..85c8ac73 100644 --- a/konfai-apps/tests/integration/test_konfai_app_client_remote.py +++ b/konfai-apps/tests/integration/test_konfai_app_client_remote.py @@ -117,7 +117,7 @@ def _write_local_synthesis_app(app_dir: Path) -> None: module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) model = module.TinySynthNet() - torch.save({"Model": model.state_dict()}, app_dir / "tiny.pt") + torch.save({"Model": model.network_states()}, app_dir / "tiny.pt") def _subprocess_env(bin_dir: Path, token: str) -> dict[str, str]: diff --git a/konfai-apps/tests/integration/test_konfai_apps.py b/konfai-apps/tests/integration/test_konfai_apps.py index d156cad4..47c41fa0 100644 --- a/konfai-apps/tests/integration/test_konfai_apps.py +++ b/konfai-apps/tests/integration/test_konfai_apps.py @@ -245,7 +245,7 @@ def _write_local_synthesis_app(app_dir: Path) -> None: model.Projection.weight.fill_(1.0) model.Projection.bias.zero_() - checkpoint = {"Model": model.state_dict()} + checkpoint = {"Model": model.network_states()} torch.save(checkpoint, app_dir / "tiny_0.pt") torch.save(checkpoint, app_dir / "tiny_1.pt") @@ -369,7 +369,7 @@ def _write_local_finetune_app(app_dir: Path) -> None: "epoch": _PRETRAINED_EPOCH, "it": _PRETRAINED_IT, "loss": 0.0, - "Model": model.state_dict(), + "Model": model.network_states(), } torch.save(checkpoint, app_dir / "tiny_0.pt") torch.save(checkpoint, app_dir / "tiny_1.pt") diff --git a/konfai-apps/tests/unit/test_app_server_helpers.py b/konfai-apps/tests/unit/test_app_server_helpers.py index f22669b6..eb050346 100644 --- a/konfai-apps/tests/unit/test_app_server_helpers.py +++ b/konfai-apps/tests/unit/test_app_server_helpers.py @@ -736,9 +736,7 @@ def test_save_directory_volume_bounds_extracted_bytes_not_compressed(tmp_path: P upload = _make_zip_bomb_upload(64 * 1024 * 1024) # ~64MB extracted, a few KB compressed with pytest.raises(HTTPException) as exc: - app_server.save_uploads( - [upload], tmp_path / "inputs", max_file_bytes=1024 * 1024, max_total_bytes=1024 * 1024 - ) + app_server.save_uploads([upload], tmp_path / "inputs", max_file_bytes=1024 * 1024, max_total_bytes=1024 * 1024) assert exc.value.status_code == 413 # Nothing left behind: the partial extraction directory is removed on failure. assert not (tmp_path / "inputs" / "store").exists() diff --git a/konfai-apps/tests/unit/test_bundle.py b/konfai-apps/tests/unit/test_bundle.py index ffa64024..89b30179 100644 --- a/konfai-apps/tests/unit/test_bundle.py +++ b/konfai-apps/tests/unit/test_bundle.py @@ -234,7 +234,11 @@ def test_masked_tta_compiler_reads_the_config(): }, "augmentations": {"DA0": {"nb": 2, "data_augmentations": {"Flip": {"f_prob": [0, 0.5, 0.5]}}}}, }, - "outputs_dataset": {"H": {"OutputDataset": {"before_reduction_transforms": {"Mask": {"path": "MASK", "value_outside": -1024}}}}}, + "outputs_dataset": { + "H": { + "OutputDataset": {"before_reduction_transforms": {"Mask": {"path": "MASK", "value_outside": -1024}}} + } + }, } } passes = _tta_passes(config, "Predictor") @@ -243,7 +247,10 @@ def test_masked_tta_compiler_reads_the_config(): assert set(aux) == {"MASK"} and [op for op, _ in aux["MASK"]["ops"]] == ["resample", "dilate"] assert _mask_specs(config, "Predictor") == [{"group": "MASK", "value_outside": -1024.0}] - fold = {"preprocessing": [{"op": "resample", "inverse": True}], "postprocessing": [{"op": "cast", "dtype": "int16"}]} + fold = { + "preprocessing": [{"op": "resample", "inverse": True}], + "postprocessing": [{"op": "cast", "dtype": "int16"}], + } program = _assemble_masked_tta_program( [{"id": f"CV_{i}", "manifest": fold} for i in range(5)], passes, diff --git a/konfai-apps/tests/unit/test_finetune_requires_loss.py b/konfai-apps/tests/unit/test_finetune_requires_loss.py index 618c4f2a..cfdb1517 100644 --- a/konfai-apps/tests/unit/test_finetune_requires_loss.py +++ b/konfai-apps/tests/unit/test_finetune_requires_loss.py @@ -26,10 +26,14 @@ from konfai_apps.app import _finetune_target_has_loss _WITH_LOSS = { - "outputs_criterions": {"Head:Tanh": {"targets_criterions": {"CT": {"criterions_loader": {"MAE": {"is_loss": True}}}}}} + "outputs_criterions": { + "Head:Tanh": {"targets_criterions": {"CT": {"criterions_loader": {"MAE": {"is_loss": True}}}}} + } } _METRIC_ONLY = { - "outputs_criterions": {"Head:Tanh": {"targets_criterions": {"CT": {"criterions_loader": {"MAE": {"is_loss": False}}}}}} + "outputs_criterions": { + "Head:Tanh": {"targets_criterions": {"CT": {"criterions_loader": {"MAE": {"is_loss": False}}}}} + } } @@ -57,7 +61,11 @@ def test_has_loss_rejects_engine_placeholder() -> None: # The registration-engine default expands to a placeholder key that names no concrete loss. placeholder = { "outputs_criterions": { - "default": {"targets_criterions": {"Labels": {"criterions_loader": {"default|torch:nn:CrossEntropyLoss|Dice|NCC": {}}}}} + "default": { + "targets_criterions": { + "Labels": {"criterions_loader": {"default|torch:nn:CrossEntropyLoss|Dice|NCC": {}}} + } + } } } assert _finetune_target_has_loss({"classpath": "Reg", "Reg": placeholder}) is False @@ -106,7 +114,9 @@ def fake_train(*args, **kwargs): # type: ignore[no-untyped-def] return trained -_LOSSLESS = "Trainer:\n train_name: PLACEHOLDER\n Model:\n classpath: Net\n Net:\n outputs_criterions: None\n" +_LOSSLESS = ( + "Trainer:\n train_name: PLACEHOLDER\n Model:\n classpath: Net\n Net:\n outputs_criterions: None\n" +) _WITH_LOSS_CONFIG = ( "Trainer:\n train_name: PLACEHOLDER\n Model:\n classpath: Net\n Net:\n" " outputs_criterions:\n Head:\n targets_criterions:\n" diff --git a/konfai-apps/tests/unit/test_transforms.py b/konfai-apps/tests/unit/test_transforms.py new file mode 100644 index 00000000..964547ee --- /dev/null +++ b/konfai-apps/tests/unit/test_transforms.py @@ -0,0 +1,202 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for ``konfai_apps.transforms``: the KonfAIInference chain stage.""" + +import os +import sys +import types +from pathlib import Path + +import konfai_apps +import numpy as np +import pytest +import torch +from konfai.utils.dataset import Attribute +from konfai_apps.transforms import ( + DEFAULT_INFERENCE_MODEL_NAME, + DEFAULT_INFERENCE_REPO_ID, + KonfAIInference, +) + + +@pytest.fixture(autouse=True) +def _ambient_ports_survive(monkeypatch: pytest.MonkeyPatch): + """infer_entry pops both port vars from the real environment; registering them with monkeypatch + makes teardown put an ambient value back instead of leaking the deletion into the session.""" + monkeypatch.delenv("KONFAI_MASTER_PORT", raising=False) + monkeypatch.delenv("KONFAI_TENSORBOARD_PORT", raising=False) + + +def test_konfai_inference_reassembles_channels_in_sorted_order(tmp_path, monkeypatch): + """Per-channel outputs must be stacked in deterministic (sorted) case order.""" + sitk = pytest.importorskip("SimpleITK") + + output_dir = tmp_path / "Output" + files = [] + for i in range(3): + case_dir = output_dir / f"P{i:03d}" + case_dir.mkdir(parents=True) + array = np.full((2, 2, 2), float(i * 10), dtype=np.float32) + path = case_dir / "Volume.mha" + sitk.WriteImage(sitk.GetImageFromArray(array), str(path)) + files.append(path) + + # Simulate an arbitrary (here reversed) filesystem enumeration order. + scrambled = list(reversed(files)) + monkeypatch.setattr(Path, "rglob", lambda self, pattern: iter(scrambled)) + + result = KonfAIInference._reassemble_output(output_dir) + + assert list(result.shape) == [3, 2, 2, 2] + assert float(result[0].mean()) == 0.0 + assert float(result[1].mean()) == 10.0 + assert float(result[2].mean()) == 20.0 + + +def test_konfai_inference_default_repo_and_model_preserved(): + """Constructing without arguments keeps the current published repo/model default.""" + transform = KonfAIInference() + + assert transform.repo_id == DEFAULT_INFERENCE_REPO_ID + assert transform.model_name == DEFAULT_INFERENCE_MODEL_NAME + assert transform.repo_id == "VBoussot/MRSegmentator-KonfAI" + assert transform.model_name == "MRSegmentator" + + +def test_konfai_inference_forwards_configured_repo_and_model(monkeypatch): + """A custom repo/model is forwarded verbatim to the KonfAIApp spec, not the default.""" + captured = {} + + class _FakeKonfAIApp: + def __init__(self, spec, *args): + captured["spec"] = spec + + def infer(self, *args, **kwargs): + captured["infer"] = (args, kwargs) + + fake_module = types.ModuleType("konfai_apps") + fake_module.KonfAIApp = _FakeKonfAIApp + monkeypatch.setitem(sys.modules, "konfai_apps", fake_module) + + transform = KonfAIInference( + repo_id="acme/Custom-KonfAI", + model_name="CustomModel", + checkpoints_name=["fold_1"], + ) + transform.infer_entry(Path("dataset"), Path("output"), []) + + assert captured["spec"] == "acme/Custom-KonfAI:CustomModel" + + +def test_konfai_inference_raises_clear_error_inside_daemon_workers(monkeypatch: pytest.MonkeyPatch) -> None: + transform = KonfAIInference() + + class DaemonProcess: + daemon = True + + monkeypatch.setattr("konfai_apps.transforms.current_process", lambda: DaemonProcess()) + + with pytest.raises(RuntimeError, match=r"Dataset\.num_workers: 0"): + transform("CASE_000", torch.zeros(1, 4, 4), Attribute()) + + +def test_konfai_inference_forwards_config_overrides_to_the_nested_run(monkeypatch: pytest.MonkeyPatch) -> None: + # The nested run is tunable from the calling code via the generic --set mechanism (not for shrinking a + # trained patch_size (that hurts the result), but for any legitimate config knob). + recorded: dict[str, object] = {} + + class FakeKonfAIApp: + def __init__(self, ref: str, download: bool, force_update: bool) -> None: + recorded["ref"] = ref + + def infer(self, *args: object, **kwargs: object) -> None: + recorded["config_overrides"] = kwargs.get("config_overrides") + + monkeypatch.setattr(konfai_apps, "KonfAIApp", FakeKonfAIApp) + overrides = ["iterations=300"] + transform = KonfAIInference(repo_id="Org/Repo", model_name="tiny", config_overrides=overrides) + transform.infer_entry(Path("/tmp/in"), Path("/tmp/out"), [0]) + + assert recorded["ref"] == "Org/Repo:tiny" + assert recorded["config_overrides"] == overrides + + +def test_konfai_inference_defragments_the_nested_allocator(monkeypatch: pytest.MonkeyPatch) -> None: + # A heavy nested model (e.g. a 3D segmentation a metric relies on) can OOM on a large volume purely from + # allocator fragmentation; the nested run enables expandable segments so it fits without config changes. + class FakeKonfAIApp: + def __init__(self, ref: str, download: bool, force_update: bool) -> None: + pass + + def infer(self, *args: object, **kwargs: object) -> None: + pass + + monkeypatch.setattr(konfai_apps, "KonfAIApp", FakeKonfAIApp) + monkeypatch.delenv("PYTORCH_CUDA_ALLOC_CONF", raising=False) + + KonfAIInference(repo_id="Org/Repo", model_name="tiny").infer_entry(Path("/tmp/in"), Path("/tmp/out"), [0]) + assert "expandable_segments:True" in os.environ["PYTORCH_CUDA_ALLOC_CONF"] + + # An explicit caller setting must win (setdefault, not overwrite). + monkeypatch.setenv("PYTORCH_CUDA_ALLOC_CONF", "max_split_size_mb:128") + KonfAIInference(repo_id="Org/Repo", model_name="tiny").infer_entry(Path("/tmp/in"), Path("/tmp/out"), [0]) + assert os.environ["PYTORCH_CUDA_ALLOC_CONF"] == "max_split_size_mb:128" + + +def test_konfai_inference_targets_the_ranks_own_device(monkeypatch: pytest.MonkeyPatch) -> None: + """A TRANSFORM chain routes its tensors to the rank's device, and the nested inference + must run there too, not on every device the launch was given (a two-GPU prediction per rank).""" + import konfai_apps.transforms as transform_module + + pytest.importorskip("SimpleITK") + launched: dict[str, list[int]] = {} + + class _Process: + exitcode = 0 + + def __init__(self, target, args): + launched["gpu"] = list(args[2]) + + def start(self) -> None: + pass + + def join(self) -> None: + pass + + class _Context: + Process = _Process + + monkeypatch.setattr(transform_module, "get_context", lambda _method: _Context()) + monkeypatch.setattr(transform_module, "cuda_visible_devices", lambda: [4, 7]) + monkeypatch.setattr(torch.cuda, "is_available", lambda: True) + monkeypatch.setattr(torch.cuda, "empty_cache", lambda: None) + monkeypatch.setattr(torch.cuda, "current_device", lambda: 1) # local rank 1 + monkeypatch.setattr(KonfAIInference, "_reassemble_output", staticmethod(lambda _dir: torch.zeros(1, 2, 2, 2))) + attributes = Attribute() + attributes["Origin"] = np.zeros(3) + attributes["Spacing"] = np.ones(3) + attributes["Direction"] = np.eye(3).reshape(-1) + KonfAIInference()("case", torch.zeros(1, 2, 2, 2), attributes) + assert launched["gpu"] == [7], "the rank's own device, in the launch's numbering" + + +def test_konfai_inference_bare_name_resolves_through_core() -> None: + """Published bundles spell the bare name ``KonfAIInference:``; core's transform package must + keep resolving it to this class.""" + import konfai.data.transform as transform_package + + assert transform_package.KonfAIInference is KonfAIInference diff --git a/konfai-mcp/README.md b/konfai-mcp/README.md index f89154f6..f752098e 100644 --- a/konfai-mcp/README.md +++ b/konfai-mcp/README.md @@ -241,9 +241,9 @@ The `solve_task` prompt frames the entry decision as a three-way fork: app (config + code + checkpoints) into the session so it runs as a normal experiment through `run_prediction` / `run_resume` / `run_evaluation` 2. **Fine-tune an app**: start training from a published model rather than a - blank slate: `fine_tune_app` adapts it to the user's dataset and writes a - resolvable app bundle (or `import_app` + `run_resume(weights_only=True)` when - the training config has to be edited first). + blank slate: `import_app` + `run_resume(weights_only=True)` warm-starts a + training on the user's dataset, and `package_app_from_session` turns the + result into a resolvable app bundle. 3. **Train from scratch**: author a config (the loop above) and, when done, `package_app_from_session` turns the trained model into a bundle too. diff --git a/konfai-mcp/konfai_mcp/capabilities.py b/konfai-mcp/konfai_mcp/capabilities.py index 7e5c062d..1f5222a3 100644 --- a/konfai-mcp/konfai_mcp/capabilities.py +++ b/konfai-mcp/konfai_mcp/capabilities.py @@ -76,8 +76,8 @@ def describe_konfai_capabilities() -> dict[str, Any]: "use an app as-is, else fine-tune one, else train from scratch.", "use_dont_train": "list_apps -> describe_app -> list_app_parameters -> run_app_infer / run_app_pipeline " "(runs the app as published); import_app copies it into the session when it must be MODIFIED first", - "fine_tune": "fine_tune_app (weights-only warm start on the user's dataset -> runnable bundle), or " - "import_app -> run_resume(weights_only=True) when the training config must be edited first", + "fine_tune": "import_app -> run_resume(weights_only=True): a weights-only warm start on the user's " + "dataset; package_app_from_session then turns the result into a runnable bundle", "resume": "run_resume (true RESUME of an interrupted session training: optimizer/epoch restored)", "package": "package_app_from_session / export_app (turn a trained session or tuned app into a bundle)", }, diff --git a/konfai-mcp/konfai_mcp/dataset_inspection.py b/konfai-mcp/konfai_mcp/dataset_inspection.py index 76ef8030..bd1a250c 100644 --- a/konfai-mcp/konfai_mcp/dataset_inspection.py +++ b/konfai-mcp/konfai_mcp/dataset_inspection.py @@ -472,7 +472,7 @@ def _infer_dataset_structure_payload(self, dataset_dir: Path, *, discover_candid # as is, fine-tune a close one, or train from scratch. Only when nothing was found does the next # step remain "locate the dataset". payload["next_actions"] = ( - ["list_apps", "run_app_infer", "fine_tune_app", "run_train", "design_config_strategy"] + ["list_apps", "run_app_infer", "import_app", "run_train", "design_config_strategy"] if payload["groups"] else ["browse_dataset", "inspect_dataset", "design_config_strategy", "initialize_session"] ) diff --git a/konfai-mcp/konfai_mcp/experiment_state.py b/konfai-mcp/konfai_mcp/experiment_state.py index 039f5504..d6f23af4 100644 --- a/konfai-mcp/konfai_mcp/experiment_state.py +++ b/konfai-mcp/konfai_mcp/experiment_state.py @@ -101,8 +101,8 @@ # are candidates, not a script. STAGE_ACTIONS: dict[str, list[str]] = { "dataset_inspection": ["inspect_dataset", "browse_dataset", "design_config_strategy", "initialize_session"], - "action_selection": ["list_apps", "fine_tune_app", "design_config_strategy", "run_train"], - "app_selection": ["describe_app", "run_app_infer", "fine_tune_app", "list_app_parameters"], + "action_selection": ["list_apps", "import_app", "design_config_strategy", "run_train"], + "app_selection": ["describe_app", "run_app_infer", "import_app", "list_app_parameters"], "configuration": ["validate_config_semantics", "review_config_semantics", "run_train", "write_workflow_config"], "running": ["wait_for_job", "read_live_metrics", "get_job_status", "cancel_job"], "failed": ["read_job_log", "validate_config_semantics", "get_job_status"], @@ -252,7 +252,6 @@ def diagnose(text: str, *, status: str = "error") -> Diagnosis: # A finished job of this kind leaves the experiment at this stage: the successor step, not "done". _JOB_KIND_STAGE: dict[str, str] = { "train": "checkpoint_selection", - "finetune": "checkpoint_selection", "prediction": "prediction", "infer": "prediction", "evaluation": "evaluation", diff --git a/konfai-mcp/konfai_mcp/guide.py b/konfai-mcp/konfai_mcp/guide.py index b183e126..2a2706dd 100644 --- a/konfai-mcp/konfai_mcp/guide.py +++ b/konfai-mcp/konfai_mcp/guide.py @@ -147,7 +147,7 @@ "It is metadata-only and SAFE: it does not import the app's model code and does not pip-install its " "requirements (those happen only later, behind an explicit trust gate). " "Outputs: display_name, description, inputs, outputs, capabilities, checkpoints, terminology, next_actions. " - "Next: run_app_infer / list_app_parameters / import_app / fine_tune_app when it fits (next_actions reflect " + "Next: run_app_infer / list_app_parameters / import_app when it fits (next_actions reflect " "the app's capabilities), or design_config_strategy if no app fits the task." ), "list_app_parameters": ( @@ -169,8 +169,10 @@ "Outputs: exported_to, next_actions. Next: describe_app / run_app_infer / import_app / register_app_source." ), "import_app": ( - "Use to RUN a published KonfAI app as a NORMAL experiment in this session: the single path to use a local " - "or HuggingFace app. It copies the app's config(s), custom code, and .pt checkpoints into the session root " + "Use to RUN a published KonfAI app as a NORMAL experiment in this session. Prefer run_app_* when the app " + "is used exactly as published; import_app is the tier for everything else: editing the config, fine-tuning " + "(run_resume with weights_only=True), or wiring the app into a larger experiment. " + "It copies the app's config(s), custom code, and .pt checkpoints into the session root " "and pip-installs its requirements, so predict / fine-tune / evaluate then go through the ordinary " "run_prediction / run_resume / run_evaluation tools (no app-specific wrapper, no extra sub-folder). The " "copied checkpoints are returned so run_prediction can pass them as models, and run_resume(weights_only=True) " @@ -233,26 +235,9 @@ "allow_untrusted_code=True). Local and HuggingFace apps only. " "Outputs: a job payload plus the output directory. Next: wait_for_job, then inspect the output subdirectories." ), - "fine_tune_app": ( - "Use to TRAIN by starting from a published app instead of a blank slate: fine-tune an existing app's " - "checkpoint(s) on the user's dataset, WITHOUT authoring or editing a config. This is the middle option " - "between run_app_infer (use as-is, no training) and design_config_strategy (author a config and train from " - "scratch); it is also the safer alternative to import_app + run_resume(weights_only=True), which needs the " - "copied Config.yml to be edited by hand. It launches a tracked training job and writes a resolvable app " - "bundle (config + code + fine-tuned checkpoint) to the output directory, which you can then run with " - "run_app_infer. " - "TRUST GATE: resolving the app imports its Python code and pip-installs its requirements, so pass " - "allow_untrusted_code=True to confirm you trust the source. Local and HuggingFace apps only. " - "It does not author a config or adapt the dataset layout for you. " - "Training knobs are first-class parameters (epochs, it_validation, lr, batch_size); set_parameters is for " - "the app's MODEL tunables (bare names) or any config key by its full dotted path. " - "Outputs: a job payload (status, resources, next_actions) plus the bundle output path. " - "Next: wait_for_job, then run_app_infer on the produced bundle (then run_app_evaluate to score and rank " - "this fine-tune against other training trials via leaderboard / compare_runs)." - ), "package_app_from_session": ( "Use to PACKAGE a model trained in the current session (the train-from-scratch branch) into a resolvable " - "KonfAI app bundle: the same endpoint fine_tune_app produces, so a from-scratch run can also finish as a " + "KonfAI app bundle, so a from-scratch run can also finish as a " "reusable app. It gathers the session's checkpoints " "and a config, writes an app.json from the metadata you give, and assembles a bundle (app.json + config + " "checkpoint + optional Model.py/requirements) that describe_app / run_app_infer / import_app can consume. " @@ -470,7 +455,7 @@ "Use to RESUME an interrupted or crashed training run from a checkpoint: model, optimizer, scheduler, and " "epoch/iteration counters are restored (KonfAI's RESUME command). Set weights_only=True instead to WARM-START " "a fine-tune from an imported app: load only the checkpoint's model weights and restart epoch/optimizer " - "from scratch; prefer fine_tune_app when the app is used as published, since it needs no config editing. " + "from scratch (import_app + run_resume(weights_only=True) is THE fine-tune path for published apps). " "This launches a resumed training job from the current session Config.yml. " "It does not pick between runs: by default it resumes from the newest checkpoint of the configured run " "(falling back to the newest in the session), avoiding cross-run contamination. " @@ -593,8 +578,9 @@ "candidate. Judge fit from the app's own description first, confirmed by its declared " "inputs/outputs. If one clearly does the job, run it with run_app_infer (or run_app_pipeline to " "also score it): done. Use import_app instead only when the app must be MODIFIED before running.\n" - "2. FINE-TUNE FROM AN APP. If no app is usable as-is but one is a close starting point, train " - "from it with fine_tune_app on the user's dataset, producing a bundle you can then run.\n" + "2. FINE-TUNE FROM AN APP. If no app is usable as-is but one is a close starting point, " + "import_app it into the session and train from its weights with run_resume(weights_only=True) " + "on the user's dataset; package_app_from_session can then turn the result into a bundle.\n" "3. TRAIN FROM A BLANK SLATE. If no app is a useful starting point, author a config from scratch " "via design_config_strategy and the train loop.\n\n" "Prefer the earliest option that truly fits: do not train when an app already solves it, and do " diff --git a/konfai-mcp/konfai_mcp/runner.py b/konfai-mcp/konfai_mcp/runner.py index 59edd2ca..b90bd319 100644 --- a/konfai-mcp/konfai_mcp/runner.py +++ b/konfai-mcp/konfai_mcp/runner.py @@ -411,54 +411,6 @@ def _groups(value: list[list[str]]) -> list[list[Path]]: getattr(KonfAIApp(ref, download=True, force_update=force_update), action)(**call) -def run_finetune_api( - *, - ref: str, - dataset: str, - output: str, - name: str = "Finetune", - epochs: int = 10, - it_validation: int = 1000, - models: list[str] | None = None, - lr: float | None = None, - batch_size: int | None = None, - config_overrides: list[str] | None = None, - gpu: list[int] | None = None, - cpu: int | None = None, - config_file: str = "Config.yml", - force_update: bool = False, - quiet: bool = False, - cwd: str | None = None, -) -> None: - """Child entrypoint that fine-tunes a KonfAI app on the user's dataset, producing a bundle. - - Resolving the app imports its Python code and pip-installs its requirements (gated in the parent - tool). Local and HuggingFace apps only. - """ - with _runtime_context(cwd=Path(cwd).resolve() if cwd is not None else None): - _ensure_local_imports() - from konfai_apps.app import KonfAIApp - - app = KonfAIApp(ref, download=True, force_update=force_update) - common: dict[str, Any] = { - "dataset": Path(dataset).resolve(), - "output": Path(output).resolve(), - "name": name, - "epochs": epochs, - "it_validation": it_validation, - "models": models or [], - "lr": lr, - "batch_size": batch_size, - "config_file": config_file, - "quiet": quiet, - } - if gpu is not None: - common["gpu"] = gpu - if cpu is not None: - common["cpu"] = cpu - app.fine_tune(**common, config_overrides=config_overrides) - - def app_parameters_api(*, ref: str, force_update: bool = False) -> dict[str, Any]: """Child entrypoint that reads an app's tunable parameters (``{values, constraints}``). diff --git a/konfai-mcp/konfai_mcp/server.py b/konfai-mcp/konfai_mcp/server.py index f0801a10..18ec83c4 100644 --- a/konfai-mcp/konfai_mcp/server.py +++ b/konfai-mcp/konfai_mcp/server.py @@ -477,17 +477,17 @@ def _config_overrides(set_parameters: dict[str, Any] | None) -> list[str] | None def _launch_app_job(spec: dict[str, Any]) -> dict[str, Any]: - """Launch an app job (inference, evaluation, uncertainty, pipeline, fine-tune) from an AppService + """Launch an app job (inference, evaluation, uncertainty, pipeline) from an AppService spec via the shared job registry. Unlike workflow jobs, an app job has no session YAML: it auto-creates the session workspace, tracks the run under the spec's kind, and carries its own runner target and kwargs. """ - kind = cast(JobKind, spec.get("kind", "infer")) # an app kind: infer / evaluate / uncertainty / pipeline / finetune + kind = cast(JobKind, spec.get("kind", "infer")) # an app kind: infer / evaluate / uncertainty / pipeline workspace = WORKSPACE_LAYOUT.ensure_session_workspace() WORKSPACE_LAYOUT.jobs_dir().mkdir(parents=True, exist_ok=True) kwargs = dict(spec["kwargs"]) - # config_overrides live directly in kwargs (infer / finetune) or nested under extra (pipeline). Recording + # config_overrides live directly in kwargs (infer) or nested under extra (pipeline). Recording # them links this trial's tuned parameters to the score it produces and gates the refine next_actions. set_parameters = kwargs.get("config_overrides") or (kwargs.get("extra") or {}).get("config_overrides") job = JOB_REGISTRY.launch( @@ -1573,74 +1573,6 @@ def run_app_pipeline( ) -@mcp.tool(description=(TOOL_DESCRIPTIONS["fine_tune_app"])) -def fine_tune_app( - ref: Annotated[str, Field(description=_APP_REF_DESC)], - dataset: Annotated[str, Field(description="KonfAI-style dataset directory to fine-tune on (must exist).")], - output: Annotated[ - str | None, - Field( - description="Destination for the produced app bundle (default: a unique dir under the session workspace AppBundles/)." - ), - ] = None, - name: Annotated[str, Field(description="Run name of the fine-tune training (default 'Finetune').")] = "Finetune", - epochs: Annotated[int, Field(description="Number of training epochs (must be > 0; default 10).")] = 10, - it_validation: Annotated[ - int, Field(description="Iterations between validation/checkpoint steps (KonfAI it_validation; default 1000).") - ] = 1000, - models: Annotated[ - list[str] | None, - Field(description="Which app checkpoints to fine-tune (default: the app's first advertised checkpoint)."), - ] = None, - lr: Annotated[ - float | None, Field(description="Learning-rate override; omit to keep the app config's value.") - ] = None, - batch_size: Annotated[ - int | None, - Field( - description="Training batch-size override (written to Trainer.Dataset.batch_size); " - "omit to keep the app config's value." - ), - ] = None, - set_parameters: Annotated[ - dict[str, Any] | None, - Field( - description="NAME->VALUE overrides baked into the training config before fine-tuning. A bare NAME " - "is a model parameter (see list_app_parameters, e.g. {'iterations': 300}); any other config key " - "needs its full dotted path from the config root (e.g. {'Trainer.Dataset.num_workers': 2}). " - "For batch size, prefer the batch_size parameter." - ), - ] = None, - gpu: Annotated[list[int] | None, Field(description=_APP_GPU_DESC)] = None, - cpu: Annotated[int | None, Field(description=_APP_CPU_DESC)] = None, - config_file: Annotated[ - str, Field(description="Which train config of the app to use (default 'Config.yml').") - ] = "Config.yml", - allow_untrusted_code: Annotated[bool, Field(description=_APP_TRUST_DESC)] = False, - force_update: Annotated[bool, Field(description=_APP_FORCE_UPDATE_DESC)] = False, -) -> dict[str, Any]: - """Fine-tune a published KonfAI app on the user's dataset and produce a resolvable app bundle.""" - return _launch_app_job( - APP_SERVICE.prepare_finetune( - ref=ref, - dataset=dataset, - output=output, - name=name, - epochs=epochs, - it_validation=it_validation, - models=models, - lr=lr, - batch_size=batch_size, - config_overrides=_config_overrides(set_parameters), - gpu=gpu, - cpu=cpu, - config_file=config_file, - allow_untrusted_code=allow_untrusted_code, - force_update=force_update, - ) - ) - - @mcp.tool(description=(TOOL_DESCRIPTIONS["register_app_source"])) def register_app_source( ref: Annotated[ diff --git a/konfai-mcp/konfai_mcp/server_apps.py b/konfai-mcp/konfai_mcp/server_apps.py index 1ec0d5d7..2900882b 100644 --- a/konfai-mcp/konfai_mcp/server_apps.py +++ b/konfai-mcp/konfai_mcp/server_apps.py @@ -288,8 +288,7 @@ def describe_app(self, ref: str, force_update: bool = False) -> dict[str, Any]: # Route by what the app can actually do instead of dead-ending on describe/design. The run_app_* # tools run the app AS PUBLISHED; import_app is offered beside them for the case where the app has - # to be modified first. fine_tune_app is only offered when the app ships a train config to - # warm-start from, so an inference-only bundle never routes the agent to a tool it cannot use. + # to be modified first, and fine-tuning goes through import_app + run_resume(weights_only). source = _source_of(info) next_actions: list[str] = [] if not inference: @@ -304,8 +303,6 @@ def describe_app(self, ref: str, force_update: bool = False) -> dict[str, Any]: next_actions.append("run_app_evaluate") if uncertainty: next_actions.append("run_app_uncertainty") - if finetunable: - next_actions.append("fine_tune_app") payload: dict[str, Any] = { "ref": ref, @@ -804,77 +801,6 @@ def prepare_pipeline( extra=extra, ) - def prepare_finetune( - self, - ref: str, - dataset: str, - output: str | None = None, - name: str = "Finetune", - epochs: int = 10, - it_validation: int = 1000, - models: list[str] | None = None, - lr: float | None = None, - batch_size: int | None = None, - config_overrides: list[str] | None = None, - gpu: list[int] | None = None, - cpu: int | None = None, - config_file: str = "Config.yml", - allow_untrusted_code: bool = False, - force_update: bool = False, - ) -> dict[str, Any]: - """Validate a fine-tune request and build the job spec for ``runner.run_finetune_api``. - - Fine-tuning starts training from an existing app's checkpoint(s) on the user's dataset and - produces a resolvable app bundle in ``output``. Same trust gate as inference: resolving the app - imports its code and pip-installs its requirements. - """ - dataset_path = Path(dataset).expanduser().resolve() - if not dataset_path.is_dir(): - raise ValueError(f"dataset must be an existing directory: {dataset}") - if epochs <= 0: - raise ValueError("epochs must be a positive integer.") - if batch_size is not None and batch_size <= 0: - raise ValueError("batch_size must be a positive integer.") - self._require_local_app(ref, "Fine-tuning", allow_untrusted_code) - - if cpu is not None and gpu is None: - gpu = [] - - # First-class knobs join the label the same way --set overrides do, so two fine-tunes differing - # only by batch size still read apart on the leaderboard. - labelled_params = ([f"batch_size={batch_size}"] if batch_size is not None else []) + (config_overrides or []) - label = self.workspace_layout.sanitize_name( - f"finetune_{self._app_label(ref)}{self._param_label_suffix(labelled_params)}" - ) - resolved_output = ( - str(Path(output).expanduser().resolve()) if output else self._default_output("AppBundles", label) - ) - - kwargs: dict[str, Any] = { - "ref": ref, - "dataset": str(dataset_path), - "output": resolved_output, - "name": name, - "epochs": epochs, - "it_validation": it_validation, - "models": models or [], - "lr": lr, - "batch_size": batch_size, - "config_overrides": config_overrides, - "gpu": gpu, - "cpu": cpu, - "config_file": config_file, - "force_update": force_update, - } - return { - "kind": "finetune", - "run_name": label, - "target": "konfai_mcp.runner:run_finetune_api", - "command": ["konfai_mcp.runner:run_finetune_api", ref, "->", resolved_output], - "kwargs": kwargs, - "output": resolved_output, - } - #: packaging ------------------------------------------------------------------------------ def package_from_session( @@ -964,7 +890,7 @@ def package_from_session( if any(Path(path).name == "Config.yml" for path in resolved_configs): result["warnings"] = [ "Config.yml is copied from the session as-is: make sure it describes the SAME architecture " - "as the packaged checkpoints (fine_tune_app will train with it)." + "as the packaged checkpoints (a fine-tune via import_app + run_resume will train with it)." ] if onnx: # The ONNX export instantiates and traces the packaged model: it imports the bundle's @@ -1015,8 +941,9 @@ def _resolve_package_checkpoints(self, checkpoints: list[str] | None) -> list[st def _resolve_package_configs(self, configs: list[str] | None) -> list[str]: if configs is None: - # Bundle BOTH the prediction config (to run) and the train config (so fine_tune_app can warm-start - # from the bundle) when present: a Prediction.yml-only bundle cannot be fine-tuned. + # Bundle BOTH the prediction config (to run) and the train config (so a fine-tune via + # import_app + run_resume can warm-start from the bundle) when present: a + # Prediction.yml-only bundle cannot be fine-tuned. prediction = self.workspace_layout.config_path("prediction") train = self.workspace_layout.config_path("train") configs = [str(path) for path in (prediction, train) if path.exists()] diff --git a/konfai-mcp/konfai_mcp/server_jobs.py b/konfai-mcp/konfai_mcp/server_jobs.py index a6a1a23b..4641ad69 100644 --- a/konfai-mcp/konfai_mcp/server_jobs.py +++ b/konfai-mcp/konfai_mcp/server_jobs.py @@ -525,11 +525,6 @@ def payload(self, job: Job, isoformat: Callable[[float | None], str | None]) -> elif job.kind == "infer" and job.set_parameters: # Already tuning inference parameters: help close the loop toward a score. next_actions.extend(["run_app_evaluate", "run_app_pipeline", "compare_runs"]) - elif job.kind == "finetune": - # A fine-tune produces a bundle but keeps its training metrics out of it, so there is - # nothing to rank yet: use the bundle, then evaluate it; that evaluation lands where - # leaderboard/compare_runs can rank this fine-tune against other training trials. - next_actions.extend(["run_app_infer", "run_app_evaluate"]) else: # A finished workflow job is a step, not the end: point at the step that actually follows it # (a trained model is worth nothing until it has predicted, a prediction until it is scored), diff --git a/konfai-mcp/konfai_mcp/workflows.py b/konfai-mcp/konfai_mcp/workflows.py index bcad8b2f..2e0a965d 100644 --- a/konfai-mcp/konfai_mcp/workflows.py +++ b/konfai-mcp/konfai_mcp/workflows.py @@ -89,7 +89,6 @@ class WorkflowSpec: # konfai-apps job kinds (no session YAML of their own) -> the tool that relaunches them. APP_JOB_RETRY_TOOLS: dict[str, str] = { "infer": "run_app_infer", - "finetune": "fine_tune_app", "evaluate": "run_app_evaluate", "uncertainty": "run_app_uncertainty", "pipeline": "run_app_pipeline", @@ -127,6 +126,4 @@ def workflow_choice_description(action: str) -> str: # Static mirrors of the table for tool signatures; pinned to it by the drift test. WorkflowKind = Literal["train", "prediction", "evaluation", "transform"] -JobKind = Literal[ - "train", "prediction", "evaluation", "transform", "infer", "finetune", "evaluate", "uncertainty", "pipeline" -] +JobKind = Literal["train", "prediction", "evaluation", "transform", "infer", "evaluate", "uncertainty", "pipeline"] diff --git a/konfai-mcp/tests/make_fixtures.py b/konfai-mcp/tests/make_fixtures.py new file mode 100644 index 00000000..df785977 --- /dev/null +++ b/konfai-mcp/tests/make_fixtures.py @@ -0,0 +1,154 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +"""Generate small synthetic fixtures for practical konfai-mcp validation runs. + +All data is procedurally generated — no patient data. Shapes are tiny so CPU +train/predict/evaluate loops finish in seconds. Output lands in the gitignored +``konfai-mcp/tests/fixtures/`` directory. Run in the KonfAI dev env: + + pixi run --environment dev python konfai-mcp/tests/make_fixtures.py +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import numpy as np +import SimpleITK as sitk + +ROOT = Path(__file__).resolve().parent / "fixtures" + + +def _write(arr: np.ndarray, path: Path, spacing=(1.0, 1.0, 1.0), origin=(0.0, 0.0, 0.0), direction=None) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + img = sitk.GetImageFromArray(arr) # arr is (z, y, x) + img.SetSpacing(spacing) + img.SetOrigin(origin) + if direction is not None: + img.SetDirection(direction) + sitk.WriteImage(img, str(path)) + + +def _sphere(shape, center, radius) -> np.ndarray: + zz, yy, xx = np.ogrid[: shape[0], : shape[1], : shape[2]] + d2 = (zz - center[0]) ** 2 + (yy - center[1]) ** 2 + (xx - center[2]) ** 2 + return d2 <= radius**2 + + +def make_segmentation_dataset(n_cases=4, shape=(16, 32, 32)) -> Path: + """Dataset/CASE_xxx/{CT.mha, SEG.mha}. Two foreground labels (sphere=1, cube=2) on background=0.""" + base = ROOT / "seg_ds" / "Dataset" + rng = np.random.default_rng(0) + for i in range(n_cases): + c = (shape[0] // 2, shape[1] // 2 + (i - 2) * 2, shape[2] // 2) + seg = np.zeros(shape, dtype=np.int16) + seg[_sphere(shape, c, 6)] = 1 + seg[4:8, 4:12, 20:28] = 2 # a cube = label 2 + ct = (seg.astype(np.float32) * 300.0) + rng.normal(0, 20, shape).astype(np.float32) - 100.0 + case = base / f"CASE_{i:03d}" + _write(ct.astype(np.float32), case / "CT.mha") + _write(seg, case / "SEG.mha") + return base + + +def make_registration_pair(shape=(24, 48, 48), shift=(0, 5, 0)) -> Path: + """Fixed/moving pair with a KNOWN integer translation (moving = fixed shifted by `shift`).""" + base = ROOT / "reg_pair" + fixed = np.zeros(shape, dtype=np.float32) + fixed[_sphere(shape, (12, 24, 24), 8)] = 500.0 + fixed[6:10, 10:16, 30:38] = 300.0 # asymmetric feature so translation is recoverable + moving = np.roll(fixed, shift=shift, axis=(0, 1, 2)) + _write(fixed, base / "fixed.nii.gz") + _write(moving, base / "moving.nii.gz") + # Same content but different spacing/origin, to test geometry mismatches. + _write(fixed, base / "fixed_spacing2.nii.gz", spacing=(2.0, 2.0, 2.0), origin=(10.0, -5.0, 3.0)) + (base / "known_transform.json").write_text( + json.dumps({"type": "translation_voxels_zyx", "shift": list(shift)}), encoding="utf-8" + ) + return base + + +def make_synthesis_pair(n_cases=3, shape=(16, 32, 32)) -> Path: + """Dataset/CASE_xxx/{MR.mha, CT.mha} for an MR->CT synthesis task.""" + base = ROOT / "synth_ds" / "Dataset" + rng = np.random.default_rng(1) + for i in range(n_cases): + struct = np.zeros(shape, dtype=np.float32) + struct[_sphere(shape, (8, 16, 16), 7)] = 1.0 + mr = struct * 800.0 + rng.normal(0, 30, shape).astype(np.float32) + ct = struct * 200.0 - 100.0 + rng.normal(0, 10, shape).astype(np.float32) + case = base / f"CASE_{i:03d}" + _write(mr.astype(np.float32), case / "MR.mha") + _write(ct.astype(np.float32), case / "CT.mha") + return base + + +def make_nrrd_and_dicom_variants() -> None: + """A NRRD copy and an incompatible-shape image, to test format/geometry handling.""" + misc = ROOT / "misc" + a = np.zeros((10, 20, 20), dtype=np.float32) + a[_sphere((10, 20, 20), (5, 10, 10), 4)] = 1.0 + _write(a, misc / "shape_10x20x20.nrrd") + _write(a, misc / "shape_10x20x20.nii.gz") + b = np.zeros((8, 24, 24), dtype=np.float32) # different shape -> pair mismatch + _write(b, misc / "shape_8x24x24.nii.gz") + + +def make_bad_fixtures() -> None: + """Corrupted / unsupported / empty inputs for failure-handling scenarios.""" + bad = ROOT / "bad" + bad.mkdir(parents=True, exist_ok=True) + (bad / "corrupted.nii.gz").write_bytes(b"\x1f\x8b\x08\x00not-a-real-nifti-gzip-body") + (bad / "not_an_image.txt").write_text("this is plainly not a medical image\n", encoding="utf-8") + (bad / "empty.mha").write_bytes(b"") + + +def make_ome_zarr() -> None: + """A tiny 3-level multiscale OME-Zarr store, to probe large-image/streaming behavior.""" + try: + import zarr # noqa: F401 + except Exception as exc: # pragma: no cover + (ROOT / "omezarr_SKIPPED.txt").write_text(f"zarr not available: {exc}\n", encoding="utf-8") + return + try: + # Prefer ngff-zarr if present (KonfAI's OME-Zarr backend); else fall back to a hand-rolled store. + import ngff_zarr as nz + + arr = np.zeros((64, 128, 128), dtype=np.float32) + arr[_sphere((64, 128, 128), (32, 64, 64), 20)] = 1.0 + image = nz.to_ngff_image(arr, dims=["z", "y", "x"], scale={"z": 1.0, "y": 1.0, "x": 1.0}) + multiscales = nz.to_multiscales(image, scale_factors=[2, 4]) + nz.to_ngff_zarr(str(ROOT / "large.zarr"), multiscales) + (ROOT / "omezarr_backend.txt").write_text("ngff_zarr\n", encoding="utf-8") + except Exception as exc: # pragma: no cover + (ROOT / "omezarr_SKIPPED.txt").write_text(f"ngff_zarr failed: {exc}\n", encoding="utf-8") + + +if __name__ == "__main__": + seg = make_segmentation_dataset() + reg = make_registration_pair() + syn = make_synthesis_pair() + make_nrrd_and_dicom_variants() + make_bad_fixtures() + make_ome_zarr() + print("seg dataset:", seg) + print("reg pair:", reg) + print("synth dataset:", syn) + print("fixtures root:", ROOT) + for p in sorted(ROOT.rglob("*")): + if p.is_file(): + print(" ", p.relative_to(ROOT), p.stat().st_size, "bytes") diff --git a/konfai-mcp/tests/test_experiment_state.py b/konfai-mcp/tests/test_experiment_state.py index adedf7d8..7e5126c3 100644 --- a/konfai-mcp/tests/test_experiment_state.py +++ b/konfai-mcp/tests/test_experiment_state.py @@ -308,7 +308,7 @@ def test_stage_derivation_is_total() -> None: """Every derived stage is one the focus and action tables know about.""" from konfai_mcp.experiment_state import Facts - for kind in ("train", "prediction", "evaluation", "infer", "finetune", "pipeline", "uncertainty", ""): + for kind in ("train", "prediction", "evaluation", "infer", "pipeline", "uncertainty", ""): for status in ("queued", "running", "done", "error", "killed", ""): facts = Facts(job_kind=kind, job_status=status, checkpoints=["a"], predictions=["b"], metrics=["c"]) assert derive_stage(facts) in STAGES diff --git a/konfai-mcp/tests/test_live_parse.py b/konfai-mcp/tests/test_live_parse.py index e20a18dd..fea46c33 100644 --- a/konfai-mcp/tests/test_live_parse.py +++ b/konfai-mcp/tests/test_live_parse.py @@ -86,3 +86,28 @@ def test_process_memory_not_confused_with_gpu_memory() -> None: def test_blank_and_unrelated_lines_return_none() -> None: assert parse_live_metric_line("") is None assert parse_live_metric_line("[konfai-mcp] job started") is None + + +def test_core_host_stat_emitters_parse_as_host_stats() -> None: + """parse_host_stats matches the literal f-strings core emits (environment.get_memory_info / + get_cpu_info / gpu_info). A harmless rewording in core would pass every suite while silently + blanking Studio's RAM/GPU charts, so the real emitter output is fed through the parser here.""" + from konfai.utils.runtime import environment + + line = f"Caching Train: {environment.get_memory_info()} | {environment.get_cpu_info()}: 5% 3/60" + stats = parse_host_stats(line) + assert {"memory_gb", "memory_percent", "cpu_percent"} <= set(stats) + assert stats["memory_gb"] > 0 + + # gpu_info reads NVML, which the konfai runtime initialises before emitting; without a usable + # GPU the RAM/CPU contract above still holds. + try: + import pynvml + + pynvml.nvmlInit() + gpu_line = environment.gpu_info() + except Exception: + gpu_line = "" + if gpu_line: + gpu_stats = parse_host_stats(f"Training : Loss (x : 0.1) {gpu_line} | {environment.get_memory_info()}: 1/10") + assert {"memory_gpu_gb", "memory_gpu_percent"} <= set(gpu_stats) diff --git a/konfai-mcp/tests/test_mcp_server_apps.py b/konfai-mcp/tests/test_mcp_server_apps.py index 841f43c8..345107a2 100644 --- a/konfai-mcp/tests/test_mcp_server_apps.py +++ b/konfai-mcp/tests/test_mcp_server_apps.py @@ -82,12 +82,11 @@ def test_describe_app_reads_local_manifest(tmp_path: Path) -> None: assert payload["checkpoints_available"] == ["tiny.pt"] assert payload["patch_size"] == [1, 64, 64] assert payload["task"] == "synthesis" - # The bundle ships a Config.yml, so it is finetunable and offers fine_tune_app. + # The bundle ships a Config.yml, so it is finetunable (via import_app + run_resume). assert payload["finetunable"] is True # An inference-capable app routes forward to the run/tune tools instead of dead-ending. assert payload["next_actions"][0] == "run_app_infer" - assert "fine_tune_app" in payload["next_actions"] - assert "import_app" in payload["next_actions"] # the modify-then-run path stays offered + assert "import_app" in payload["next_actions"] # the modify/fine-tune path stays offered assert "run_app_evaluate" not in payload["next_actions"] @@ -490,7 +489,6 @@ def test_server_registers_app_tools(tmp_path: Path, monkeypatch: pytest.MonkeyPa "run_app_evaluate", "run_app_uncertainty", "run_app_pipeline", - "fine_tune_app", "package_app_from_session", ) import asyncio @@ -504,12 +502,11 @@ def test_server_registers_app_tools(tmp_path: Path, monkeypatch: pytest.MonkeyPa assert callable(runner.run_app_api) assert callable(runner.run_app_action_api) - assert callable(runner.run_finetune_api) assert "solve_task" in index["prompts"] solve = server.prompt_solve_task("segment the liver", "one CT group") content = solve[0]["content"] - for tool in ("run_app_infer", "fine_tune_app", "import_app", "design_config_strategy"): + for tool in ("run_app_infer", "run_resume", "import_app", "design_config_strategy"): assert tool in content app_dir = _write_local_app(tmp_path) @@ -569,14 +566,13 @@ def _dummy_inputs(tmp_path: Path) -> list[list[str]]: def test_describe_app_inference_only_is_not_finetunable(tmp_path: Path) -> None: - """An app with no train Config.yml must not advertise fine_tune_app (it would dead-end).""" + """An app with no train Config.yml must report finetunable=False (a fine-tune would dead-end).""" app_dir = _write_local_app(tmp_path) (app_dir / "Config.yml").unlink() payload = _service(tmp_path).describe_app(str(app_dir)) assert payload["finetunable"] is False - assert "fine_tune_app" not in payload["next_actions"] # Inference routing is unaffected. assert payload["next_actions"][0] == "run_app_infer" @@ -700,73 +696,6 @@ def test_prepare_pipeline_spec_and_overrides(tmp_path: Path) -> None: assert "pipeline_TinyLocalApp__iterations_300" in tuned["output"] -def test_prepare_finetune_gates_local_app(tmp_path: Path) -> None: - app_dir = _write_local_app(tmp_path) - dataset = tmp_path / "Dataset" - dataset.mkdir() - service = _service(tmp_path) - - with pytest.raises(ValueError, match="allow_untrusted_code=True"): - service.prepare_finetune(ref=str(app_dir), dataset=str(dataset)) - - spec = service.prepare_finetune(ref=str(app_dir), dataset=str(dataset), allow_untrusted_code=True, epochs=3) - assert spec["kind"] == "finetune" - assert spec["target"] == "konfai_mcp.runner:run_finetune_api" - assert spec["kwargs"]["dataset"] == str(dataset.resolve()) - assert spec["kwargs"]["epochs"] == 3 - assert "finetune_TinyLocalApp" in spec["output"] - - -def test_prepare_finetune_rejects_remote_and_bad_dataset(tmp_path: Path) -> None: - app_dir = _write_local_app(tmp_path) - service = _service(tmp_path) - dataset = tmp_path / "Dataset" - dataset.mkdir() - - with pytest.raises(ValueError, match="remote app server"): - service.prepare_finetune(ref="localhost:8000:MyApp", dataset=str(dataset), allow_untrusted_code=True) - - with pytest.raises(ValueError, match="dataset must be an existing directory"): - service.prepare_finetune(ref=str(app_dir), dataset=str(tmp_path / "missing"), allow_untrusted_code=True) - - with pytest.raises(ValueError, match="epochs must be a positive integer"): - service.prepare_finetune(ref=str(app_dir), dataset=str(dataset), epochs=0, allow_untrusted_code=True) - - -def test_prepare_finetune_bakes_set_parameters(tmp_path: Path) -> None: - app_dir = _write_local_app(tmp_path) - dataset = tmp_path / "Dataset" - dataset.mkdir() - - spec = _service(tmp_path).prepare_finetune( - ref=str(app_dir), - dataset=str(dataset), - allow_untrusted_code=True, - config_overrides=["iterations=300"], - ) - # The overrides reach the runner (so they bake into the training config) ... - assert spec["kwargs"]["config_overrides"] == ["iterations=300"] - # ... and the default output dir is param-legible, so the leaderboard metrics_path names the trial. - assert "finetune_TinyLocalApp__iterations_300" in spec["output"] - - -def test_prepare_finetune_carries_the_batch_size_as_a_training_knob(tmp_path: Path) -> None: - """batch_size is a first-class training knob like epochs, NOT an app tunable: routed through - set_parameters it was refused ('batch_size' is no model parameter) and the job died at launch.""" - app_dir = _write_local_app(tmp_path) - dataset = tmp_path / "Dataset" - dataset.mkdir() - service = _service(tmp_path) - - spec = service.prepare_finetune(ref=str(app_dir), dataset=str(dataset), allow_untrusted_code=True, batch_size=4) - assert spec["kwargs"]["batch_size"] == 4 - # The knob still labels the trial's output dir, so the leaderboard names what produced the score. - assert "finetune_TinyLocalApp__batch_size_4" in spec["output"] - - with pytest.raises(ValueError, match="batch_size must be a positive integer"): - service.prepare_finetune(ref=str(app_dir), dataset=str(dataset), allow_untrusted_code=True, batch_size=0) - - def test_app_tools_launch_tracked_app_jobs(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """The tool -> prepare_* -> job-registry wiring: kind, runner target, devices, manifest and the tuned parameters that gate the refine loop (the launch itself is stubbed: no subprocess).""" @@ -818,14 +747,6 @@ def fake_launch(**kwargs: object) -> Job: assert payload["kind"] == "infer" assert payload["output"] == captured["kwargs"]["output"] # type: ignore[index] assert payload["set_parameters"] == ["iterations=300"] - - dataset = tmp_path / "Dataset" - dataset.mkdir(exist_ok=True) - tuned = server.fine_tune_app(ref=str(app_dir), dataset=str(dataset), allow_untrusted_code=True, epochs=2, cpu=1) - assert captured["kind"] == "finetune" - assert captured["target"] == "konfai_mcp.runner:run_finetune_api" - assert captured["kwargs"]["epochs"] == 2 # type: ignore[index] - assert tuned["kind"] == "finetune" finally: sys.modules.pop("konfai_mcp.server", None) @@ -852,7 +773,6 @@ async def scenario() -> dict[str, dict]: "run_app_evaluate": {"ref", "inputs", "gt"}, "run_app_uncertainty": {"ref", "inputs"}, "run_app_pipeline": {"ref", "inputs", "gt"}, - "fine_tune_app": {"ref", "dataset", "output"}, } for name, required_params in expected.items(): assert name in schemas, f"{name} is not exposed to the client" diff --git a/konfai-mcp/tests/test_mcp_server_cli.py b/konfai-mcp/tests/test_mcp_server_cli.py index 60cff44f..4513d058 100644 --- a/konfai-mcp/tests/test_mcp_server_cli.py +++ b/konfai-mcp/tests/test_mcp_server_cli.py @@ -148,9 +148,7 @@ def test_parser_reads_stateless_http_from_environment(monkeypatch: pytest.Monkey @pytest.mark.parametrize("transport", ["stdio", "sse"]) -def test_cli_rejects_stateless_http_on_session_transports( - monkeypatch: pytest.MonkeyPatch, transport: str -) -> None: +def test_cli_rejects_stateless_http_on_session_transports(monkeypatch: pytest.MonkeyPatch, transport: str) -> None: monkeypatch.delenv("KONFAI_MCP_TRANSPORT", raising=False) monkeypatch.delenv("KONFAI_MCP_STATELESS_HTTP", raising=False) with pytest.raises(SystemExit): diff --git a/konfai-mcp/tests/test_mcp_server_refine_loop.py b/konfai-mcp/tests/test_mcp_server_refine_loop.py index 0f7aa687..18c0fbb4 100644 --- a/konfai-mcp/tests/test_mcp_server_refine_loop.py +++ b/konfai-mcp/tests/test_mcp_server_refine_loop.py @@ -63,12 +63,3 @@ def test_evaluate_and_pipeline_incite_ranking_and_reexport(tmp_path: Path) -> No assert "leaderboard" in actions assert "compare_runs" in actions assert "export_app" in actions - - -def test_finetune_points_at_use_then_evaluate_not_empty_leaderboard(tmp_path: Path) -> None: - """A fine-tune keeps its training metrics out of the bundle, so it points at use+score, not a leaderboard - that would have nothing to rank yet.""" - actions = _next_actions(_done_job(tmp_path, "finetune")) - assert "run_app_infer" in actions - assert "run_app_evaluate" in actions - assert "leaderboard" not in actions diff --git a/konfai-mcp/tests/test_mcp_server_tool_index.py b/konfai-mcp/tests/test_mcp_server_tool_index.py index 6889dccf..1de4452e 100644 --- a/konfai-mcp/tests/test_mcp_server_tool_index.py +++ b/konfai-mcp/tests/test_mcp_server_tool_index.py @@ -59,7 +59,6 @@ def test_job_payload_next_actions_are_registered_tools( "evaluation", "transform", "infer", - "finetune", "evaluate", "uncertainty", "pipeline", diff --git a/konfai-mcp/tests/test_workflow_registry.py b/konfai-mcp/tests/test_workflow_registry.py index bb351c3c..048c7a03 100644 --- a/konfai-mcp/tests/test_workflow_registry.py +++ b/konfai-mcp/tests/test_workflow_registry.py @@ -31,7 +31,7 @@ def test_literal_aliases_match_the_table() -> None: - # A job kind is either a workflow kind or a konfai-apps kind (run_app_* / fine_tune_app), never else. + # A job kind is either a workflow kind or a konfai-apps kind (run_app_*), never else. assert set(get_args(WorkflowKind)) == set(WORKFLOW_SPECS) assert set(get_args(JobKind)) == set(JOB_KINDS) assert set(JOB_KINDS) == set(WORKFLOW_SPECS) | set(APP_JOB_KINDS) diff --git a/konfai/__init__.py b/konfai/__init__.py index bb4eb4a3..51bc4049 100755 --- a/konfai/__init__.py +++ b/konfai/__init__.py @@ -30,9 +30,10 @@ except ImportError: _PYNVML_AVAILABLE = False -# ``requests`` (remote-server helpers only) and ``torch`` (device-name lookup only) are imported lazily -# at their point of use so that ``import konfai`` stays light: CLI paths that never touch a GPU -# (``--help``/``--version``, light apps helpers) avoid the ~1s torch import. +# ``torch`` (device-name lookup only) is imported lazily at its point of use so that +# ``import konfai`` stays light: CLI paths that never touch a GPU (``--help``/``--version``, +# light apps helpers) avoid the ~1s torch import. The remote-server helpers speak plain HTTP +# over the stdlib (urllib), so they cost no dependency at all. from konfai.utils.errors import KonfAIError try: @@ -104,14 +105,17 @@ def get_url(self) -> str: return f"http://{self.host}:{self.port}" def get_json(self, path: str, timeout_s: float, params: list[tuple[str, int]] | None = None) -> dict: - """The JSON body of ``GET /``; a failed status raises.""" - import requests + """The JSON body of ``GET /``; a failed status raises (``HTTPError``).""" + import json + from urllib.parse import urlencode + from urllib.request import Request, urlopen - response = requests.get( - f"{self.get_url()}/{path}", params=params, headers=self.get_headers(), timeout=timeout_s - ) - response.raise_for_status() - return response.json() + url = f"{self.get_url()}/{path}" + if params: + url += "?" + urlencode(params) + # The scheme is this class's own constant (get_url), never caller input: no file:// reach. + with urlopen(Request(url, headers=self.get_headers()), timeout=timeout_s) as response: # nosec B310 + return json.loads(response.read().decode("utf-8")) def cuda_visible_devices() -> list[int]: @@ -250,11 +254,8 @@ def _get_env(var: str) -> str: "psutil": "psutil", "tensorboard": "tensorboard", "SimpleITK": "SimpleITK", - "lxml": "lxml", # often used as lxml.etree "h5py": "h5py", "nvidia-ml-py": "pynvml", # IMPORTANT: pip != import - "requests": "requests", - "huggingface_hub": "huggingface_hub", } @@ -282,31 +283,36 @@ def check_server(remote_server: RemoteServer, timeout_s: float = 2.0) -> tuple[b tuple[bool, str] A boolean success flag and a human-readable status message. """ - import requests + import json + from urllib.error import HTTPError, URLError + from urllib.request import Request, urlopen try: - r = requests.get( - f"{remote_server.get_url()}/health", - headers=remote_server.get_headers(), - timeout=timeout_s, - ) + request = Request(f"{remote_server.get_url()}/health", headers=remote_server.get_headers()) + # The scheme comes from RemoteServer.get_url (its own constant), never caller input. + with urlopen(request, timeout=timeout_s) as response: # nosec B310 + if response.status != 200: + return False, f"HTTP {response.status}" + data = json.loads(response.read().decode("utf-8")) - if r.status_code == 401: - return False, "Unauthorized (invalid or missing token)" - if r.status_code == 403: - return False, "Forbidden" - if r.status_code != 200: - return False, f"HTTP {r.status_code}" - - data = r.json() if data.get("status") != "ok": return False, f"Unexpected response: {data}" return True, "OK" - except requests.exceptions.ConnectionError: - return False, "Connection refused" - except requests.exceptions.Timeout: + except HTTPError as error: + if error.code == 401: + return False, "Unauthorized (invalid or missing token)" + if error.code == 403: + return False, "Forbidden" + return False, f"HTTP {error.code}" + except URLError as error: + if isinstance(error.reason, TimeoutError): + return False, "Timeout" + if isinstance(error.reason, ConnectionRefusedError): + return False, "Connection refused" + return False, str(error.reason) + except TimeoutError: return False, "Timeout" except Exception as e: return False, str(e) diff --git a/konfai/data/augmentation/__init__.py b/konfai/data/augmentation/__init__.py index 010c484b..343709e5 100644 --- a/konfai/data/augmentation/__init__.py +++ b/konfai/data/augmentation/__init__.py @@ -21,14 +21,6 @@ from konfai.data.augmentation.base import DataAugmentationsList as DataAugmentationsList from konfai.data.augmentation.base import Foreign as Foreign from konfai.data.augmentation.base import Prob as Prob -from konfai.data.augmentation.base import _axis_rotation_matrix as _axis_rotation_matrix -from konfai.data.augmentation.base import _hashed_normal_field as _hashed_normal_field -from konfai.data.augmentation.base import _reflect_interval as _reflect_interval -from konfai.data.augmentation.base import _require_simpleitk as _require_simpleitk -from konfai.data.augmentation.base import _rotation_2d_matrix as _rotation_2d_matrix -from konfai.data.augmentation.base import _rotation_3d_matrix as _rotation_3d_matrix -from konfai.data.augmentation.base import _scale_matrix as _scale_matrix -from konfai.data.augmentation.base import _translate_matrix as _translate_matrix from konfai.data.augmentation.base import sitk as sitk from konfai.data.augmentation.color import HUE as HUE from konfai.data.augmentation.color import Brightness as Brightness diff --git a/konfai/data/case_reduction.py b/konfai/data/case_reduction.py index 0782f15f..aed7e30c 100644 --- a/konfai/data/case_reduction.py +++ b/konfai/data/case_reduction.py @@ -49,13 +49,8 @@ from konfai.data.reduction import Reduction from konfai.data.transform import LocalityKind, PatchLocality, Reduce, Save, Transform, stat_seed_valid from konfai.utils.budget import budget_share, format_bytes -from konfai.utils.dataset import ( - Attribute, - Dataset, - DataStream, - _finalize_running_statistics, - _update_running_statistics, -) +from konfai.utils.dataset import Attribute, Dataset, DataStream +from konfai.utils.dataset.statistics import _finalize_running_statistics, _update_running_statistics from konfai.utils.errors import ReductionError #: Geometry keys compared between cases under ``grid: strict``. Direction is in because a flipped @@ -234,7 +229,7 @@ def body_lines(self) -> list[str]: class _RunningStatistics: """Min/Max/Mean/Std accumulated over regions, so the volume is never resident. - The store-scan recurrence (:func:`konfai.utils.dataset._update_running_statistics`) is the one + The store-scan recurrence (:func:`konfai.utils.dataset.statistics._update_running_statistics`) is the one Welford kernel; this feeds it blocks and writes the keys in KonfAI's own spelling. """ @@ -596,7 +591,7 @@ def _member_read_bytes(self, channels: int) -> tuple[int, int]: that cannot answer) contributes nothing: the peak then says what it did before, which is what the run-time probe is there to correct. """ - from konfai.data.patching import _SWEEP_ELEMENT_BYTES + from konfai.data.patching.budget import _SWEEP_ELEMENT_BYTES reads = [manager.region_reads(self.slab_rows) for manager in self.managers] present = [read for read in reads if read is not None] diff --git a/konfai/data/data_manager/__init__.py b/konfai/data/data_manager/__init__.py index e75eec82..ceb1b9d8 100644 --- a/konfai/data/data_manager/__init__.py +++ b/konfai/data/data_manager/__init__.py @@ -23,20 +23,13 @@ from konfai.data.data_manager.groups import GroupTransform as GroupTransform from konfai.data.data_manager.groups import GroupTransformMetric as GroupTransformMetric from konfai.data.data_manager.groups import GroupTransformOut as GroupTransformOut -from konfai.data.data_manager.groups import _chains as _chains -from konfai.data.data_manager.groups import _check_patch_transform_invertible as _check_patch_transform_invertible -from konfai.data.data_manager.groups import _check_patch_transform_locality as _check_patch_transform_locality -from konfai.data.data_manager.groups import _check_patch_transform_shape as _check_patch_transform_shape from konfai.data.data_manager.order import PatchReadOrder as PatchReadOrder from konfai.data.data_manager.order import WindowedCaseSampler as WindowedCaseSampler -from konfai.data.data_manager.order import _interleaved_case_entries as _interleaved_case_entries -from konfai.data.data_manager.samples import _CACHE_ELEMENT_BYTES as _CACHE_ELEMENT_BYTES from konfai.data.data_manager.samples import BatchDataItem as BatchDataItem from konfai.data.data_manager.samples import BatchSample as BatchSample from konfai.data.data_manager.samples import DataItem as DataItem from konfai.data.data_manager.samples import DatasetIter as DatasetIter from konfai.data.data_manager.samples import Sample as Sample -from konfai.data.data_manager.samples import _cache_worker_count as _cache_worker_count from konfai.data.data_manager.samples import collate_konfai as collate_konfai from konfai.data.data_manager.sources import Data as Data from konfai.data.data_manager.sources import DataMetric as DataMetric diff --git a/konfai/data/materialize.py b/konfai/data/materialize.py index 96aa0adf..10ff1d1a 100644 --- a/konfai/data/materialize.py +++ b/konfai/data/materialize.py @@ -42,11 +42,12 @@ AugmentedStage, DatasetManager, SweepSegment, +) +from konfai.data.patching.stage import _ReadStagePlan, _stage_name +from konfai.data.patching.sweep import ( _PendingSweep, _pull_block_voxels, - _ReadStagePlan, _stage_failures_explained, - _stage_name, _sweep_targets, _SweepMember, ) diff --git a/konfai/data/patching/__init__.py b/konfai/data/patching/__init__.py index 41038f37..944fe6d3 100644 --- a/konfai/data/patching/__init__.py +++ b/konfai/data/patching/__init__.py @@ -28,14 +28,6 @@ from konfai.data.patching.blend import Trim as Trim from konfai.data.patching.blend import blend_axes as blend_axes from konfai.data.patching.blend import blend_overlap as blend_overlap -from konfai.data.patching.budget import _PLATEAU_READ_MARGIN as _PLATEAU_READ_MARGIN -from konfai.data.patching.budget import _STREAM_STAT_KEYS as _STREAM_STAT_KEYS -from konfai.data.patching.budget import _STREAM_STATS as _STREAM_STATS -from konfai.data.patching.budget import _SWEEP_ELEMENT_BYTES as _SWEEP_ELEMENT_BYTES -from konfai.data.patching.budget import _SWEEP_MAX_DEPTH as _SWEEP_MAX_DEPTH -from konfai.data.patching.budget import _SWEEP_SLAB_ROWS_DEVICE as _SWEEP_SLAB_ROWS_DEVICE -from konfai.data.patching.budget import _SWEEP_TILE_MARGIN as _SWEEP_TILE_MARGIN -from konfai.data.patching.budget import _UNRESOLVED as _UNRESOLVED from konfai.data.patching.budget import CASE_ELEMENT_BYTES as CASE_ELEMENT_BYTES from konfai.data.patching.budget import FALLBACK_INFLIGHT_FACTOR as FALLBACK_INFLIGHT_FACTOR from konfai.data.patching.budget import SWEEP_ENGINE_FLOOR_BYTES as SWEEP_ENGINE_FLOOR_BYTES @@ -46,45 +38,14 @@ from konfai.data.patching.grid import DatasetPatch as DatasetPatch from konfai.data.patching.grid import ModelPatch as ModelPatch from konfai.data.patching.grid import Patch as Patch -from konfai.data.patching.grid import _PatchGrid as _PatchGrid from konfai.data.patching.manager import DatasetManager as DatasetManager -from konfai.data.patching.stage import _MAX_HALO_FRACTION as _MAX_HALO_FRACTION from konfai.data.patching.stage import AugmentedStage as AugmentedStage from konfai.data.patching.stage import PatchReadPlan as PatchReadPlan from konfai.data.patching.stage import Stage as Stage -from konfai.data.patching.stage import _drawn_from as _drawn_from -from konfai.data.patching.stage import _halo_radii as _halo_radii -from konfai.data.patching.stage import _HaloPull as _HaloPull -from konfai.data.patching.stage import _is_draw as _is_draw -from konfai.data.patching.stage import _ReadStagePlan as _ReadStagePlan -from konfai.data.patching.stage import _RemapPull as _RemapPull -from konfai.data.patching.stage import _spatial as _spatial -from konfai.data.patching.stage import _stage_name as _stage_name from konfai.data.patching.sweep import SWEEP_CLOCK as SWEEP_CLOCK from konfai.data.patching.sweep import BlockReads as BlockReads from konfai.data.patching.sweep import RegionWriter as RegionWriter from konfai.data.patching.sweep import SweepSegment as SweepSegment -from konfai.data.patching.sweep import _channel_first_block as _channel_first_block -from konfai.data.patching.sweep import _cubic_tile as _cubic_tile -from konfai.data.patching.sweep import _HostLanding as _HostLanding -from konfai.data.patching.sweep import _open_sweep_stream as _open_sweep_stream -from konfai.data.patching.sweep import _PatchStreamSource as _PatchStreamSource -from konfai.data.patching.sweep import _PendingSweep as _PendingSweep -from konfai.data.patching.sweep import _plateau_rows as _plateau_rows -from konfai.data.patching.sweep import _pull_block_spans as _pull_block_spans -from konfai.data.patching.sweep import _pull_block_voxels as _pull_block_voxels -from konfai.data.patching.sweep import _ReadAhead as _ReadAhead -from konfai.data.patching.sweep import _shares_h5_file as _shares_h5_file -from konfai.data.patching.sweep import _span_voxels as _span_voxels -from konfai.data.patching.sweep import _stage_failure as _stage_failure -from konfai.data.patching.sweep import _stage_failures_explained as _stage_failures_explained -from konfai.data.patching.sweep import _sweep_header as _sweep_header -from konfai.data.patching.sweep import _sweep_pipeline_depth as _sweep_pipeline_depth -from konfai.data.patching.sweep import _sweep_resident_regions as _sweep_resident_regions -from konfai.data.patching.sweep import _sweep_targets as _sweep_targets -from konfai.data.patching.sweep import _SweepMember as _SweepMember -from konfai.data.patching.sweep import _torch_dtype_hint as _torch_dtype_hint -from konfai.data.patching.sweep import _WriteBehind as _WriteBehind from konfai.data.patching.sweep import save_destination as save_destination __all__ = [ diff --git a/konfai/data/patching/sweep.py b/konfai/data/patching/sweep.py index 4b3c1b8e..1fa0e320 100644 --- a/konfai/data/patching/sweep.py +++ b/konfai/data/patching/sweep.py @@ -201,7 +201,7 @@ def _cubic_tile(spatial: list[int], voxels: int, align: int) -> list[int]: """The block of at most ``voxels`` closest to a cube inside ``spatial``, aligned to ``align``. ``align`` keeps a block a whole number of store chunks wide, so a region write never becomes a - read-modify-write (:func:`konfai.utils.dataset._store_chunks`); an axis shorter than one step is + read-modify-write (:func:`konfai.utils.dataset.ome_zarr_file._store_chunks`); an axis shorter than one step is taken whole. Why a cube: :meth:`DatasetManager._sweep_tile`. """ tile = [max(1, int(extent)) for extent in spatial] diff --git a/konfai/data/transform/__init__.py b/konfai/data/transform/__init__.py index 6e43fffa..952dcba3 100644 --- a/konfai/data/transform/__init__.py +++ b/konfai/data/transform/__init__.py @@ -27,10 +27,8 @@ from konfai.data.transform.base import Transform as Transform from konfai.data.transform.base import TransformInverse as TransformInverse from konfai.data.transform.base import TransformLoader as TransformLoader -from konfai.data.transform.base import _is_augmentation as _is_augmentation from konfai.data.transform.base import sitk as sitk from konfai.data.transform.base import stat_seed_valid as stat_seed_valid -from konfai.data.transform.chain import _REDUCE_OWN_KEYS as _REDUCE_OWN_KEYS from konfai.data.transform.chain import Expand as Expand from konfai.data.transform.chain import Reduce as Reduce from konfai.data.transform.chain import resolve_operator as resolve_operator @@ -42,10 +40,6 @@ from konfai.data.transform.ensemble import SegmentationDisagreement as SegmentationDisagreement from konfai.data.transform.ensemble import StandardDeviation as StandardDeviation from konfai.data.transform.ensemble import Variance as Variance -from konfai.data.transform.ensemble import _MemberSpread as _MemberSpread -from konfai.data.transform.inference import DEFAULT_INFERENCE_MODEL_NAME as DEFAULT_INFERENCE_MODEL_NAME -from konfai.data.transform.inference import DEFAULT_INFERENCE_REPO_ID as DEFAULT_INFERENCE_REPO_ID -from konfai.data.transform.inference import KonfAIInference as KonfAIInference from konfai.data.transform.intensity import Clip as Clip from konfai.data.transform.intensity import HistogramMatching as HistogramMatching from konfai.data.transform.intensity import Normalize as Normalize @@ -53,7 +47,6 @@ from konfai.data.transform.intensity import Statistics as Statistics from konfai.data.transform.intensity import TensorCast as TensorCast from konfai.data.transform.intensity import UnNormalize as UnNormalize -from konfai.data.transform.intensity import _seeded_scalar as _seeded_scalar from konfai.data.transform.io import Save as Save from konfai.data.transform.io import Write as Write from konfai.data.transform.labels import Argmax as Argmax @@ -65,22 +58,7 @@ from konfai.data.transform.labels import SelectLabel as SelectLabel from konfai.data.transform.labels import Softmax as Softmax from konfai.data.transform.labels import Sum as Sum -from konfai.data.transform.labels import _axis_reduction_locality as _axis_reduction_locality -from konfai.data.transform.labels import _forget_model_channel_counts as _forget_model_channel_counts -from konfai.data.transform.resample import _FIELD_ELEMENT_BYTES as _FIELD_ELEMENT_BYTES -from konfai.data.transform.resample import _FIELD_WINDOW_COPIES as _FIELD_WINDOW_COPIES from konfai.data.transform.resample import Resample as Resample -from konfai.data.transform.resample import _DerivedGrid as _DerivedGrid -from konfai.data.transform.resample import _DisplacementSource as _DisplacementSource -from konfai.data.transform.resample import _optional_image_filler as _optional_image_filler -from konfai.data.transform.resample import _OwnGrid as _OwnGrid -from konfai.data.transform.resample import _ReferenceGrid as _ReferenceGrid -from konfai.data.transform.resample import _resample_with_sitk as _resample_with_sitk -from konfai.data.transform.resample import _set_image_from_array as _set_image_from_array -from konfai.data.transform.resample import _SitkInput as _SitkInput -from konfai.data.transform.resample import _stages_bytes as _stages_bytes -from konfai.data.transform.resample import _StoredMap as _StoredMap -from konfai.data.transform.resample import _TargetGrid as _TargetGrid from konfai.data.transform.shape import Canonical as Canonical from konfai.data.transform.shape import Crop as Crop from konfai.data.transform.shape import Flatten as Flatten @@ -89,11 +67,8 @@ from konfai.data.transform.shape import Padding as Padding from konfai.data.transform.shape import Permute as Permute from konfai.data.transform.shape import Squeeze as Squeeze -from konfai.utils.ITK import _require_simpleitk as _require_simpleitk __all__ = [ - "DEFAULT_INFERENCE_MODEL_NAME", - "DEFAULT_INFERENCE_REPO_ID", "Argmax", "Canonical", "Clip", @@ -107,7 +82,6 @@ "Gradient", "HistogramMatching", "InferenceStack", - "KonfAIInference", "LocalityKind", "Magnitude", "Mask", @@ -142,3 +116,21 @@ "split_expand", "stat_seed_valid", ] + + +def __getattr__(name: str): + # ``KonfAIInference`` lives in konfai-apps (it drives a nested app run), but published configs + # spell the bare name, which the TransformLoader resolves against this package: hand the class + # over when konfai-apps is installed, refuse with the install otherwise. + if name == "KonfAIInference": + try: + from konfai_apps.transforms import KonfAIInference + except ImportError as exc: + from konfai.utils.errors import TransformError + + raise TransformError( + "KonfAIInference requires the standalone 'konfai-apps' package.", + "Install it with 'pip install konfai-apps'.", + ) from exc + return KonfAIInference + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/konfai/evaluator.py b/konfai/evaluator.py index 06ec801f..42e3a0e1 100644 --- a/konfai/evaluator.py +++ b/konfai/evaluator.py @@ -740,8 +740,9 @@ def evaluate( """ Build and execute the configured evaluation workflow. - This compatibility wrapper preserves the historical CLI-facing API while - delegating the pure build step to :func:`build_evaluate`. + ``overwrite``/``gpu``/``cpu``/``quiet``/``tensorboard`` are load-bearing even though the body + drops them: :func:`run_distributed_app` reads them from the bound signature to drive the launch. + The pure build step is :func:`build_evaluate`. """ del overwrite, gpu, cpu, quiet, tensorboard return build_evaluate( diff --git a/konfai/metric/measure/__init__.py b/konfai/metric/measure/__init__.py index c6f46992..2e57df4d 100644 --- a/konfai/metric/measure/__init__.py +++ b/konfai/metric/measure/__init__.py @@ -28,18 +28,11 @@ from konfai.metric.measure.base import CriterionWithInit as CriterionWithInit from konfai.metric.measure.base import LabelledValues as LabelledValues from konfai.metric.measure.base import MaskedLoss as MaskedLoss -from konfai.metric.measure.base import _require_optional as _require_optional from konfai.metric.measure.base import models_register as models_register from konfai.metric.measure.impact import ImpactFeatureModel as ImpactFeatureModel from konfai.metric.measure.impact import IMPACTReg as IMPACTReg from konfai.metric.measure.impact import IMPACTSynth as IMPACTSynth from konfai.metric.measure.impact import SAM_Perceptual as SAM_Perceptual -from konfai.metric.measure.impact import _check_feature_model as _check_feature_model -from konfai.metric.measure.impact import _denormalized as _denormalized -from konfai.metric.measure.impact import _feature_loss_mean as _feature_loss_mean -from konfai.metric.measure.impact import _feature_mask as _feature_mask -from konfai.metric.measure.impact import _masked_feature_loss as _masked_feature_loss -from konfai.metric.measure.impact import _patch_views as _patch_views from konfai.metric.measure.regression import BCE as BCE from konfai.metric.measure.regression import LPIPS as LPIPS from konfai.metric.measure.regression import MAE as MAE diff --git a/konfai/transformer.py b/konfai/transformer.py index f26a28ca..cb8a8245 100644 --- a/konfai/transformer.py +++ b/konfai/transformer.py @@ -69,12 +69,12 @@ from konfai.utils.runtime import ( DistributedObject, State, - _materialized_config, configure_workflow_environment, get_device, record, run_distributed_app, ) +from konfai.utils.runtime.environment import _materialized_config _PROBE_ENTRY = "__konfai_plan_probe__" diff --git a/konfai/utils/dataset/__init__.py b/konfai/utils/dataset/__init__.py index d344ddcd..df36e6c2 100644 --- a/konfai/utils/dataset/__init__.py +++ b/konfai/utils/dataset/__init__.py @@ -23,11 +23,6 @@ from konfai.utils.dataset.abstract import AbstractFile as AbstractFile from konfai.utils.dataset.attribute import DISPLACEMENT_FIELD_ATTRIBUTE as DISPLACEMENT_FIELD_ATTRIBUTE from konfai.utils.dataset.attribute import Attribute as Attribute -from konfai.utils.dataset.attribute import _attribute_text as _attribute_text -from konfai.utils.dataset.attribute import _decode_transform as _decode_transform -from konfai.utils.dataset.attribute import _encode_transform_leaves as _encode_transform_leaves -from konfai.utils.dataset.attribute import _flatten_transforms as _flatten_transforms -from konfai.utils.dataset.attribute import _transform_codec as _transform_codec from konfai.utils.dataset.attribute import as_channel_first as as_channel_first from konfai.utils.dataset.attribute import data_to_image as data_to_image from konfai.utils.dataset.attribute import data_to_transform as data_to_transform @@ -42,81 +37,18 @@ from konfai.utils.dataset.backend import File as File from konfai.utils.dataset.backend import backend_for as backend_for from konfai.utils.dataset.core import Dataset as Dataset -from konfai.utils.dataset.core import _is_listed_name as _is_listed_name from konfai.utils.dataset.dicom_file import DicomFile as DicomFile from konfai.utils.dataset.h5 import H5File as H5File -from konfai.utils.dataset.h5 import _get_h5_file_lock as _get_h5_file_lock -from konfai.utils.dataset.h5 import _h5_file_locks as _h5_file_locks -from konfai.utils.dataset.h5 import _h5_file_locks_guard as _h5_file_locks_guard -from konfai.utils.dataset.h5 import _h5_read_pool as _h5_read_pool -from konfai.utils.dataset.h5 import _H5DataStream as _H5DataStream -from konfai.utils.dataset.h5 import _H5ReadPool as _H5ReadPool -from konfai.utils.dataset.h5 import _open_h5 as _open_h5 -from konfai.utils.dataset.h5 import _PooledRead as _PooledRead from konfai.utils.dataset.h5 import h5py as h5py from konfai.utils.dataset.h5 import release_read_handles as release_read_handles from konfai.utils.dataset.itk_transform_file import ItkTransformFile as ItkTransformFile -from konfai.utils.dataset.itk_transform_file import _create_itk_transform_file as _create_itk_transform_file -from konfai.utils.dataset.itk_transform_file import _ItkTransformDataStream as _ItkTransformDataStream from konfai.utils.dataset.landmarks import read_landmarks as read_landmarks from konfai.utils.dataset.landmarks import write_landmarks as write_landmarks from konfai.utils.dataset.ome_zarr_file import OmeZarrFile as OmeZarrFile -from konfai.utils.dataset.ome_zarr_file import _divisor_tile as _divisor_tile -from konfai.utils.dataset.ome_zarr_file import _forget_resolved_paths as _forget_resolved_paths -from konfai.utils.dataset.ome_zarr_file import _OmeZarrDataStream as _OmeZarrDataStream -from konfai.utils.dataset.ome_zarr_file import _store_chunks as _store_chunks -from konfai.utils.dataset.raw_block import _MHA_DTYPES as _MHA_DTYPES -from konfai.utils.dataset.raw_block import _MHA_HEADER_PROBE_BYTES as _MHA_HEADER_PROBE_BYTES -from konfai.utils.dataset.raw_block import _NIFTI_DTYPES as _NIFTI_DTYPES -from konfai.utils.dataset.raw_block import _mapped_band as _mapped_band -from konfai.utils.dataset.raw_block import _mha_raw_block as _mha_raw_block -from konfai.utils.dataset.raw_block import _nifti_extract_aborts as _nifti_extract_aborts -from konfai.utils.dataset.raw_block import _nifti_raw_block as _nifti_raw_block -from konfai.utils.dataset.raw_block import _pixel_block as _pixel_block -from konfai.utils.dataset.raw_block import _pixel_block_at as _pixel_block_at -from konfai.utils.dataset.raw_block import _pixel_block_attributes as _pixel_block_attributes -from konfai.utils.dataset.raw_block import _pixel_block_region as _pixel_block_region -from konfai.utils.dataset.raw_block import _PixelBlock as _PixelBlock -from konfai.utils.dataset.raw_block import _sitk_component_dtypes as _sitk_component_dtypes from konfai.utils.dataset.sitk_file import SitkFile as SitkFile -from konfai.utils.dataset.sitk_file import _unstreamed_formats_warned as _unstreamed_formats_warned -from konfai.utils.dataset.sitk_file import _warn_unstreamed_region_read as _warn_unstreamed_region_read -from konfai.utils.dataset.staging import _REPLACED_MARKER as _REPLACED_MARKER -from konfai.utils.dataset.staging import _STAGING_PID as _STAGING_PID -from konfai.utils.dataset.staging import _orphaned_backup_names as _orphaned_backup_names -from konfai.utils.dataset.staging import _recover_orphaned_backup as _recover_orphaned_backup -from konfai.utils.dataset.staging import _replaced_name as _replaced_name -from konfai.utils.dataset.staging import _retire_dead_debris as _retire_dead_debris -from konfai.utils.dataset.staging import _writer_is_dead as _writer_is_dead from konfai.utils.dataset.staging import is_staging_entry as is_staging_entry -from konfai.utils.dataset.statistics import _QUANTILE_BINS as _QUANTILE_BINS -from konfai.utils.dataset.statistics import _QUANTILE_COLLECT_CAP as _QUANTILE_COLLECT_CAP -from konfai.utils.dataset.statistics import _STATISTICS_BLOCKS_IN_FLIGHT as _STATISTICS_BLOCKS_IN_FLIGHT -from konfai.utils.dataset.statistics import _STATISTICS_CHUNK_ELEMENTS as _STATISTICS_CHUNK_ELEMENTS -from konfai.utils.dataset.statistics import _STATISTICS_ELEMENT_BYTES as _STATISTICS_ELEMENT_BYTES -from konfai.utils.dataset.statistics import _STATISTICS_UPDATE_ELEMENTS as _STATISTICS_UPDATE_ELEMENTS -from konfai.utils.dataset.statistics import _binned as _binned -from konfai.utils.dataset.statistics import _empty_statistics_state as _empty_statistics_state -from konfai.utils.dataset.statistics import _finalize_running_statistics as _finalize_running_statistics -from konfai.utils.dataset.statistics import _lerp_like_numpy as _lerp_like_numpy -from konfai.utils.dataset.statistics import _max_of as _max_of -from konfai.utils.dataset.statistics import _min_of as _min_of -from konfai.utils.dataset.statistics import _order_statistics as _order_statistics -from konfai.utils.dataset.statistics import _quantile_positions as _quantile_positions -from konfai.utils.dataset.statistics import _scan_block_on_the_store_grid as _scan_block_on_the_store_grid -from konfai.utils.dataset.statistics import _statistics_block_elements as _statistics_block_elements -from konfai.utils.dataset.statistics import _statistics_chunk_length as _statistics_chunk_length -from konfai.utils.dataset.statistics import _statistics_plane_elements as _statistics_plane_elements -from konfai.utils.dataset.statistics import _update_pieces as _update_pieces -from konfai.utils.dataset.statistics import _update_running_statistics as _update_running_statistics from konfai.utils.dataset.statistics import chunk_hull_voxels as chunk_hull_voxels -from konfai.utils.dataset.stream import _MADV_DONTNEED as _MADV_DONTNEED -from konfai.utils.dataset.stream import _MHA_ELEMENT_TYPES as _MHA_ELEMENT_TYPES -from konfai.utils.dataset.stream import _NIFTI_DATATYPES as _NIFTI_DATATYPES from konfai.utils.dataset.stream import DataStream as DataStream -from konfai.utils.dataset.stream import _MhaDataStream as _MhaDataStream -from konfai.utils.dataset.stream import _NiftiDataStream as _NiftiDataStream -from konfai.utils.dataset.stream import _RawBlockStream as _RawBlockStream __all__ = [ "BACKENDS", diff --git a/konfai/utils/dataset/sitk_file.py b/konfai/utils/dataset/sitk_file.py index a7c906ea..858780cb 100644 --- a/konfai/utils/dataset/sitk_file.py +++ b/konfai/utils/dataset/sitk_file.py @@ -25,10 +25,10 @@ import os import re import warnings +import xml.etree.ElementTree as ET # nosec B405 - the sidecar is the user's own dataset entry, same trust as lxml before from pathlib import Path import numpy as np -from lxml import etree # nosec B410 try: import SimpleITK as sitk @@ -292,7 +292,7 @@ def file_to_data(self, group: str, name: str) -> tuple[np.ndarray, Attribute]: data = read_landmarks(Path(path)) elif path.endswith(".xml"): with open(path, "rb") as xml_file: - root = etree.parse(xml_file, etree.XMLParser(remove_blank_text=True)).getroot() # nosec B320 + root = ET.parse(xml_file).getroot() # nosec B314 - user-owned sidecar node = root while len(node): node = node[-1] @@ -400,18 +400,17 @@ def data_to_file( elif "path" in attributes: if os.path.exists(f"{self.filename}{name}.xml"): with open(f"{self.filename}{name}.xml", "rb") as xml_file: - root = etree.parse(xml_file, etree.XMLParser(remove_blank_text=True)).getroot() # nosec B320 + root = ET.parse(xml_file).getroot() # nosec B314 - user-owned sidecar xml_file.close() else: - root = etree.Element(name) + root = ET.Element(name) node = root path = attributes["path"].split(":") for node_name in path: node_tmp = node.find(node_name) if node_tmp is None: - node_tmp = etree.SubElement(node, node_name) - node.append(node_tmp) + node_tmp = ET.SubElement(node, node_name) node = node_tmp if attributes is not None: for attribute_tmp in attributes.keys(): @@ -421,7 +420,10 @@ def data_to_file( if data.size > 0: node.text = ", ".join(map(str, data.flatten())) with open(f"{self.filename}{name}.xml", "wb") as f: - f.write(etree.tostring(root, pretty_print=True, encoding="utf-8")) + # ``ET.indent`` replaces whitespace-only text/tails, so a re-read file re-indents + # cleanly instead of accumulating blank lines. + ET.indent(root) + f.write(ET.tostring(root, encoding="utf-8")) f.close() else: np.save(f"{self.filename}{name}.npy", data) diff --git a/konfai/utils/runtime/__init__.py b/konfai/utils/runtime/__init__.py index e24562c2..6012f904 100644 --- a/konfai/utils/runtime/__init__.py +++ b/konfai/utils/runtime/__init__.py @@ -18,14 +18,7 @@ """Runtime helpers: the workflow environment, logging, and the distributed runtime.""" from konfai.utils import State as State -from konfai.utils.runtime.distributed import _T as _T from konfai.utils.runtime.distributed import DistributedObject as DistributedObject -from konfai.utils.runtime.distributed import _cpu_budget_applied as _cpu_budget_applied -from konfai.utils.runtime.distributed import _forget_rank_pool as _forget_rank_pool -from konfai.utils.runtime.distributed import _rank_pool as _rank_pool -from konfai.utils.runtime.distributed import _rank_pool_lock as _rank_pool_lock -from konfai.utils.runtime.distributed import _rank_pool_share as _rank_pool_share -from konfai.utils.runtime.distributed import _runs_inline as _runs_inline from konfai.utils.runtime.distributed import apply_cpu_thread_budget as apply_cpu_thread_budget from konfai.utils.runtime.distributed import cleanup as cleanup from konfai.utils.runtime.distributed import execute_distributed_object as execute_distributed_object @@ -41,7 +34,6 @@ from konfai.utils.runtime.distributed import synchronize_data as synchronize_data from konfai.utils.runtime.environment import ClusterKwargs as ClusterKwargs from konfai.utils.runtime.environment import NeedDevice as NeedDevice -from konfai.utils.runtime.environment import _materialized_config as _materialized_config from konfai.utils.runtime.environment import clear_directory_except_logs as clear_directory_except_logs from konfai.utils.runtime.environment import configure_workflow_environment as configure_workflow_environment from konfai.utils.runtime.environment import confirm_overwrite_or_raise as confirm_overwrite_or_raise @@ -60,11 +52,6 @@ from konfai.utils.runtime.logging import Log as Log from konfai.utils.runtime.logging import MinimalLog as MinimalLog from konfai.utils.runtime.logging import TensorBoard as TensorBoard -from konfai.utils.runtime.logging import _bar_key as _bar_key -from konfai.utils.runtime.logging import _log_image_format as _log_image_format -from konfai.utils.runtime.logging import _log_images_format as _log_images_format -from konfai.utils.runtime.logging import _log_signal_format as _log_signal_format -from konfai.utils.runtime.logging import _log_video_format as _log_video_format from konfai.utils.runtime.logging import record as record __all__ = [ diff --git a/konfai/utils/runtime/distributed.py b/konfai/utils/runtime/distributed.py index cc28c7ca..940a6f04 100644 --- a/konfai/utils/runtime/distributed.py +++ b/konfai/utils/runtime/distributed.py @@ -225,6 +225,16 @@ def run_distributed_app( @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> None: params = sig.parameters + # A kwarg the entrypoint does not declare must refuse, not vanish: silently dropping one + # already forced the --plan short-circuit in main.py. Tolerated beside the signature: the + # cluster kwargs (read from the raw kwargs below) and 'command', the CLI's subcommand + # discriminator, which only the TRAIN/RESUME entrypoint declares. + unknown = set(kwargs) - set(params) - {"name", "memory", "num_nodes", "time_limit", "command"} + if unknown: + raise ConfigError( + f"{func.__name__}() does not accept {sorted(unknown)}.", + f"It accepts {sorted(params)}; a cluster submission adds name/memory/num_nodes/time_limit.", + ) kwargs_fun = {k: v for k, v in kwargs.items() if k in params} bound = sig.bind_partial(*args, **kwargs_fun) diff --git a/konfai/utils/utils.py b/konfai/utils/utils.py index 3506a303..c5468731 100755 --- a/konfai/utils/utils.py +++ b/konfai/utils/utils.py @@ -14,7 +14,8 @@ # # SPDX-License-Identifier: Apache-2.0 -"""Compatibility facade for KonfAI utility helpers and lightweight array utilities.""" +"""The patch/overlap and storage-format grammar shared by every workflow: patch sizing and overlap +resolution, the supported-extension and ``path:spec`` vocabulary, and the classpath importer.""" import importlib import itertools diff --git a/pixi.lock b/pixi.lock index 6150501f..8e8dfd08 100644 --- a/pixi.lock +++ b/pixi.lock @@ -52,7 +52,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -71,12 +70,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl @@ -94,7 +91,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl @@ -105,7 +101,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl @@ -131,7 +126,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl @@ -168,12 +162,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -209,9 +201,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/75/20bb8fe9c1ad6538cce8cd0391b51927ae5af0b17ed1eab44b8824465dc1/torch-2.12.1-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl @@ -259,7 +249,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -275,26 +264,22 @@ environments: - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl @@ -305,7 +290,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl @@ -331,7 +315,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/4a/a36e03210183a8a7d4c80c3936acee679f4bd77d5861f369db47b2cc5f05/grpcio-1.81.1-cp313-cp313-macosx_11_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl @@ -368,12 +351,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl @@ -406,7 +387,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl @@ -447,7 +427,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl @@ -466,13 +445,11 @@ environments: - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/71/8e31ea472ceb934e7261ac59a786e82cd82b4d4dcb7c870d498aa9c3c21e/wasmtime-45.0.0-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/72/6663cc0382ddbb866136c255c837bcb96cc7ce5e83562efec55e1b995941/numcodecs-0.16.5-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl @@ -483,7 +460,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl @@ -495,7 +471,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl @@ -510,7 +485,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl @@ -519,7 +493,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/76/59ed932b0244acd7bbbd6449480053a6d958ea66357f022f932872e19287/onnxruntime-1.27.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl @@ -555,7 +528,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/2e/59f3ab896f5a8c76a3d009c40015a32660b1b05b0ff055efcee3aa977caa/zarrista-0.1.0-cp311-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl @@ -590,11 +562,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl @@ -645,7 +615,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -665,13 +634,11 @@ environments: - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl @@ -691,7 +658,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl @@ -703,7 +669,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl @@ -730,7 +695,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl @@ -772,12 +736,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -815,9 +777,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/75/20bb8fe9c1ad6538cce8cd0391b51927ae5af0b17ed1eab44b8824465dc1/torch-2.12.1-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl @@ -869,7 +829,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -885,20 +844,17 @@ environments: - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl @@ -906,7 +862,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl @@ -918,7 +873,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl @@ -944,7 +898,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/4a/a36e03210183a8a7d4c80c3936acee679f4bd77d5861f369db47b2cc5f05/grpcio-1.81.1-cp313-cp313-macosx_11_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl @@ -985,12 +938,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl @@ -1023,7 +974,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl @@ -1069,7 +1019,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl @@ -1088,13 +1037,11 @@ environments: - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/71/8e31ea472ceb934e7261ac59a786e82cd82b4d4dcb7c870d498aa9c3c21e/wasmtime-45.0.0-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/72/6663cc0382ddbb866136c255c837bcb96cc7ce5e83562efec55e1b995941/numcodecs-0.16.5-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl @@ -1107,7 +1054,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl @@ -1120,7 +1066,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl @@ -1135,7 +1080,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl @@ -1144,7 +1088,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/76/59ed932b0244acd7bbbd6449480053a6d958ea66357f022f932872e19287/onnxruntime-1.27.0-cp313-cp313-win_amd64.whl @@ -1185,7 +1128,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/2e/59f3ab896f5a8c76a3d009c40015a32660b1b05b0ff055efcee3aa977caa/zarrista-0.1.0-cp311-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl @@ -1221,11 +1163,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl @@ -1280,7 +1220,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -1299,12 +1238,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl @@ -1322,7 +1259,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl @@ -1333,7 +1269,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl @@ -1359,7 +1294,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl @@ -1396,12 +1330,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -1437,9 +1369,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/75/20bb8fe9c1ad6538cce8cd0391b51927ae5af0b17ed1eab44b8824465dc1/torch-2.12.1-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl @@ -1488,7 +1418,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -1504,26 +1433,22 @@ environments: - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl @@ -1534,7 +1459,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl @@ -1560,7 +1484,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/4a/a36e03210183a8a7d4c80c3936acee679f4bd77d5861f369db47b2cc5f05/grpcio-1.81.1-cp313-cp313-macosx_11_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl @@ -1597,12 +1520,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl @@ -1635,7 +1556,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl @@ -1677,7 +1597,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl @@ -1696,13 +1615,11 @@ environments: - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/71/8e31ea472ceb934e7261ac59a786e82cd82b4d4dcb7c870d498aa9c3c21e/wasmtime-45.0.0-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/72/6663cc0382ddbb866136c255c837bcb96cc7ce5e83562efec55e1b995941/numcodecs-0.16.5-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl @@ -1713,7 +1630,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl @@ -1725,7 +1641,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl @@ -1740,7 +1655,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl @@ -1749,7 +1663,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/76/59ed932b0244acd7bbbd6449480053a6d958ea66357f022f932872e19287/onnxruntime-1.27.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl @@ -1785,7 +1698,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/2e/59f3ab896f5a8c76a3d009c40015a32660b1b05b0ff055efcee3aa977caa/zarrista-0.1.0-cp311-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl @@ -1820,11 +1732,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl @@ -2713,9 +2623,6 @@ packages: - numpy - ruamel-yaml - psutil - - lxml - - requests - - huggingface-hub - simpleitk>=2.0 ; extra == 'itk' - h5py ; extra == 'hdf5' - nvidia-ml-py ; extra == 'monitoring' @@ -2730,8 +2637,6 @@ packages: - lpips ; extra == 'lpips' - segmentation-models-pytorch ; extra == 'smp' - scikit-image ; extra == 'ssim' - - scipy ; extra == 'fid' - - torchvision ; extra == 'fid' - submitit ; extra == 'cluster' - pydicom ; extra == 'dicom' - zarr>=3 ; extra == 'omezarr' @@ -2749,13 +2654,13 @@ packages: - konfai[vtk] ; extra == 'all' - konfai[lpips] ; extra == 'all' - konfai[ssim] ; extra == 'all' - - konfai[fid] ; extra == 'all' - konfai[cluster] ; extra == 'all' - konfai[dicom] ; extra == 'all' - konfai[omezarr] ; extra == 'all' - konfai[s3] ; extra == 'all' - konfai[export] ; extra == 'all' - konfai[smp] ; extra == 'all' + - huggingface-hub ; extra == 'all' - simpleitk>=2.0 ; extra == 'dev' - h5py ; extra == 'dev' - pydicom ; extra == 'dev' @@ -2890,11 +2795,6 @@ packages: - sphinx>=1.8,!=8.2.0,!=8.2.1 - traitlets>=5 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - name: h11 - version: 0.16.0 - sha256: 63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl name: roman-numerals version: 4.1.0 @@ -3133,11 +3033,6 @@ packages: - mypy>=1.11.2 ; extra == 'all' - pytest>=8.3.2 ; extra == 'all' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - name: annotated-doc - version: 0.0.4 - sha256: 571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl name: cryptography version: 49.0.0 @@ -3225,24 +3120,6 @@ packages: - pytest ; extra == 'test' - defusedxml>=0.7.1 ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - name: httpx - version: 0.28.1 - sha256: d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad - requires_dist: - - anyio - - certifi - - httpcore==1.* - - idna - - brotli ; platform_python_implementation == 'CPython' and extra == 'brotli' - - brotlicffi ; platform_python_implementation != 'CPython' and extra == 'brotli' - - click==8.* ; extra == 'cli' - - pygments==2.* ; extra == 'cli' - - rich>=10,<14 ; extra == 'cli' - - h2>=3,<5 ; extra == 'http2' - - socksio==1.* ; extra == 'socks' - - zstandard>=0.18.0 ; extra == 'zstd' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl name: virtualenv version: 21.5.1 @@ -3339,13 +3216,6 @@ packages: - sphinx>=5 ; extra == 'standalone' - pytest ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl - name: hf-xet - version: 1.5.1 - sha256: 94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e - requires_dist: - - pytest ; extra == 'tests' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl name: nbclient version: 0.11.0 @@ -3552,16 +3422,6 @@ packages: - trove-classifiers>=2024.10.12 ; extra == 'tests' - defusedxml ; extra == 'xmp' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - name: typer - version: 0.25.1 - sha256: 75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89 - requires_dist: - - click>=8.2.1 - - shellingham>=1.3.0 - - rich>=13.8.0 - - annotated-doc>=0.0.2 - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl name: ipython version: 9.15.0 @@ -3795,125 +3655,6 @@ packages: version: 3.1.1 sha256: 7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752 requires_python: '>=3.3' -- pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl - name: huggingface-hub - version: 1.21.0 - sha256: eadaa3678c512c82aea69e8675d90a184861e68de32f1105668628b4dce0e7cd - requires_dist: - - click>=8.4.0 - - filelock>=3.10.0 - - fsspec>=2023.5.0 - - hf-xet>=1.5.1,<2.0.0 ; platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64' - - httpx>=0.23.0,<1 - - packaging>=20.9 - - pyyaml>=5.1 - - tqdm>=4.42.1 - - typer>=0.20.0,<0.26.0 - - typing-extensions>=4.1.0 - - authlib>=1.3.2 ; extra == 'oauth' - - fastapi ; extra == 'oauth' - - httpx ; extra == 'oauth' - - itsdangerous ; extra == 'oauth' - - torch ; extra == 'torch' - - safetensors[torch] ; extra == 'torch' - - toml ; extra == 'fastai' - - fastai>=2.4 ; extra == 'fastai' - - fastcore>=1.3.27 ; extra == 'fastai' - - hf-xet>=1.5.1,<2.0.0 ; extra == 'hf-xet' - - mcp>=1.8.0 ; extra == 'mcp' - - authlib>=1.3.2 ; extra == 'testing' - - fastapi ; extra == 'testing' - - httpx ; extra == 'testing' - - itsdangerous ; extra == 'testing' - - jedi ; extra == 'testing' - - jinja2 ; extra == 'testing' - - pytest>=8.4.2 ; extra == 'testing' - - pytest-cov ; extra == 'testing' - - pytest-env ; extra == 'testing' - - pytest-xdist ; extra == 'testing' - - pytest-vcr ; extra == 'testing' - - pytest-asyncio ; extra == 'testing' - - pytest-rerunfailures<16.0 ; extra == 'testing' - - pytest-mock ; extra == 'testing' - - urllib3<2.0 ; extra == 'testing' - - soundfile ; extra == 'testing' - - pillow ; extra == 'testing' - - numpy ; extra == 'testing' - - duckdb ; extra == 'testing' - - fastapi ; extra == 'testing' - - gradio>=5.0.0 ; extra == 'gradio' - - requests ; extra == 'gradio' - - typing-extensions>=4.8.0 ; extra == 'typing' - - types-pyyaml ; extra == 'typing' - - types-simplejson ; extra == 'typing' - - types-toml ; extra == 'typing' - - types-tqdm ; extra == 'typing' - - types-urllib3 ; extra == 'typing' - - ruff>=0.9.0 ; extra == 'quality' - - mypy==1.15.0 ; extra == 'quality' - - libcst>=1.4.0 ; extra == 'quality' - - ty ; extra == 'quality' - - authlib>=1.3.2 ; extra == 'all' - - fastapi ; extra == 'all' - - httpx ; extra == 'all' - - itsdangerous ; extra == 'all' - - jedi ; extra == 'all' - - jinja2 ; extra == 'all' - - pytest>=8.4.2 ; extra == 'all' - - pytest-cov ; extra == 'all' - - pytest-env ; extra == 'all' - - pytest-xdist ; extra == 'all' - - pytest-vcr ; extra == 'all' - - pytest-asyncio ; extra == 'all' - - pytest-rerunfailures<16.0 ; extra == 'all' - - pytest-mock ; extra == 'all' - - urllib3<2.0 ; extra == 'all' - - soundfile ; extra == 'all' - - pillow ; extra == 'all' - - numpy ; extra == 'all' - - duckdb ; extra == 'all' - - fastapi ; extra == 'all' - - ruff>=0.9.0 ; extra == 'all' - - mypy==1.15.0 ; extra == 'all' - - libcst>=1.4.0 ; extra == 'all' - - ty ; extra == 'all' - - typing-extensions>=4.8.0 ; extra == 'all' - - types-pyyaml ; extra == 'all' - - types-simplejson ; extra == 'all' - - types-toml ; extra == 'all' - - types-tqdm ; extra == 'all' - - types-urllib3 ; extra == 'all' - - authlib>=1.3.2 ; extra == 'dev' - - fastapi ; extra == 'dev' - - httpx ; extra == 'dev' - - itsdangerous ; extra == 'dev' - - jedi ; extra == 'dev' - - jinja2 ; extra == 'dev' - - pytest>=8.4.2 ; extra == 'dev' - - pytest-cov ; extra == 'dev' - - pytest-env ; extra == 'dev' - - pytest-xdist ; extra == 'dev' - - pytest-vcr ; extra == 'dev' - - pytest-asyncio ; extra == 'dev' - - pytest-rerunfailures<16.0 ; extra == 'dev' - - pytest-mock ; extra == 'dev' - - urllib3<2.0 ; extra == 'dev' - - soundfile ; extra == 'dev' - - pillow ; extra == 'dev' - - numpy ; extra == 'dev' - - duckdb ; extra == 'dev' - - fastapi ; extra == 'dev' - - ruff>=0.9.0 ; extra == 'dev' - - mypy==1.15.0 ; extra == 'dev' - - libcst>=1.4.0 ; extra == 'dev' - - ty ; extra == 'dev' - - typing-extensions>=4.8.0 ; extra == 'dev' - - types-pyyaml ; extra == 'dev' - - types-simplejson ; extra == 'dev' - - types-toml ; extra == 'dev' - - types-tqdm ; extra == 'dev' - - types-urllib3 ; extra == 'dev' - requires_python: '>=3.10.0' - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl name: sphinxcontrib-serializinghtml version: 2.0.0 @@ -4293,13 +4034,6 @@ packages: - uri-template ; extra == 'format-nongpl' - webcolors>=24.6.0 ; extra == 'format-nongpl' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl - name: hf-xet - version: 1.5.1 - sha256: f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5 - requires_dist: - - pytest ; extra == 'tests' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/6e/34/4b3208b35dea488263a5c9f4a464ef20316f663e9e90d5de61349c31b327/simpleitk-2.5.5-cp311-abi3-macosx_11_0_arm64.whl name: simpleitk version: 2.5.5 @@ -4433,18 +4167,6 @@ packages: version: 1.0.0 sha256: fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - name: httpcore - version: 1.0.9 - sha256: 2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 - requires_dist: - - certifi - - h11>=0.16 - - anyio>=4.0,<5.0 ; extra == 'asyncio' - - h2>=3,<5 ; extra == 'http2' - - socksio==1.* ; extra == 'socks' - - trio>=0.22.0,<1.0 ; extra == 'trio' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl name: urllib3 version: 2.7.0 @@ -5211,31 +4933,11 @@ packages: version: 10.4.0.35 sha256: 1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc requires_python: '>=3' -- pypi: https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl - name: lxml - version: 6.1.1 - sha256: 68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736 - requires_dist: - - cssselect>=0.7 ; extra == 'cssselect' - - html5lib ; extra == 'html5' - - beautifulsoup4 ; extra == 'htmlsoup' - - lxml-html-clean ; extra == 'html-clean' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: librt version: 0.11.0 sha256: 7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - name: lxml - version: 6.1.1 - sha256: 1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085 - requires_dist: - - cssselect>=0.7 ; extra == 'cssselect' - - html5lib ; extra == 'html5' - - beautifulsoup4 ; extra == 'htmlsoup' - - lxml-html-clean ; extra == 'html-clean' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl name: coloredlogs version: 15.0.1 @@ -5299,16 +5001,6 @@ packages: requires_dist: - numpy>=1.21.2 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl - name: anyio - version: 4.14.1 - sha256: 4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72 - requires_dist: - - exceptiongroup>=1.0.2 ; python_full_version < '3.11' - - idna>=2.8 - - typing-extensions>=4.5 ; python_full_version < '3.13' - - trio>=0.32.0 ; extra == 'trio' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl name: pyyaml version: 6.0.3 @@ -5911,23 +5603,11 @@ packages: version: 0.2.3 sha256: 8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8 requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - name: hf-xet - version: 1.5.1 - sha256: 892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350 - requires_dist: - - pytest ; extra == 'tests' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl name: packaging version: '26.2' sha256: 5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - name: shellingham - version: 1.5.4 - sha256: 7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 - requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/e1/75/20bb8fe9c1ad6538cce8cd0391b51927ae5af0b17ed1eab44b8824465dc1/torch-2.12.1-cp313-cp313-manylinux_2_28_x86_64.whl name: torch version: 2.12.1 @@ -6138,16 +5818,6 @@ packages: - zstandard ; python_full_version < '3.14' and extra == 'test-full' - tqdm ; extra == 'tqdm' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl - name: lxml - version: 6.1.1 - sha256: a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660 - requires_dist: - - cssselect>=0.7 ; extra == 'cssselect' - - html5lib ; extra == 'html5' - - beautifulsoup4 ; extra == 'htmlsoup' - - lxml-html-clean ; extra == 'html-clean' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl name: jupyter-core version: 5.9.1 diff --git a/pyproject.toml b/pyproject.toml index bb1f667e..cb47ee9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,10 +36,7 @@ dependencies = [ "tqdm", "numpy", "ruamel.yaml", - "psutil", - "lxml", - "requests", - "huggingface_hub" + "psutil" ] [project.urls] @@ -62,7 +59,6 @@ vtk = ["vtk"] lpips = ["lpips"] smp = ["segmentation-models-pytorch"] ssim = ["scikit-image"] -fid = ["scipy", "torchvision"] cluster = ["submitit"] dicom = ["pydicom"] omezarr = ["zarr>=3", "ngff-zarr>=0.45", "dask"] @@ -78,13 +74,15 @@ all = [ "konfai[vtk]", "konfai[lpips]", "konfai[ssim]", - "konfai[fid]", "konfai[cluster]", "konfai[dicom]", "konfai[omezarr]", "konfai[s3]", "konfai[export]", - "konfai[smp]" + "konfai[smp]", + # The IMPACT criteria download their feature extractors from the Hub; imported lazily, + # and their refusal names this extra. + "huggingface_hub" ] dev = [ "SimpleITK>=2.0", diff --git a/run_tests_here.py b/run_tests_here.py deleted file mode 100644 index d3f59336..00000000 --- a/run_tests_here.py +++ /dev/null @@ -1,6 +0,0 @@ -import sys, os -sys.meta_path = [f for f in sys.meta_path if "editable" not in type(f).__module__.lower()] -sys.path.insert(0, os.getcwd()) -import konfai; assert konfai.__file__.startswith(os.getcwd()), konfai.__file__ -import pytest -sys.exit(pytest.main(sys.argv[1:])) diff --git a/studio/konfai_studio/agent.py b/studio/konfai_studio/agent.py index 134a3f7d..cfe125a8 100644 --- a/studio/konfai_studio/agent.py +++ b/studio/konfai_studio/agent.py @@ -81,15 +81,15 @@ def _require_claude_code() -> None: "NEVER start training, fine-tuning, prediction, evaluation or a dataset transform without asking first. Say in one line " "what it will cost: how many cases, on which device, roughly how long, and wait for a yes. The " "only exception is relaunching a run you just corrected after a failure.\n\n" - "A run is a result only when its job says so. After any run_* or fine_tune_app, wait for the job to " + "A run is a result only when its job says so. After any run_* launch, wait for the job to " "reach a terminal state, then open what it produced and report THAT. A launch that returned cleanly is " "not a success and must never be presented as one.\n\n" "When a run fails: the cause in one line, never a traceback, then the correction. A correction may be " "relaunched straight away; anything else that costs GPU time is asked first. Two failed attempts is the " "limit: past it, lay out the options and ask. Never settle a scientific choice yourself (loss, " "architecture, split, label mapping, which data is the right one): those are the user's.\n\n" - "Tuning an app run: training knobs (epochs, it_validation, lr, batch_size) are fine_tune_app's OWN " - "parameters, never set_parameters entries. set_parameters takes the app's model tunables by their bare " + "Tuning an app run: fine-tune an imported app with import_app then run_resume (weights_only), whose " + "config carries the training knobs. set_parameters takes the app's model tunables by their bare " "name (list_app_parameters shows them); any OTHER config key needs its full dotted path from the config " "root ({'Trainer.Dataset.num_workers': 2}): a bare config key is refused as an unknown model " "parameter.\n\n" diff --git a/studio/konfai_studio/workflow.py b/studio/konfai_studio/workflow.py index 6757c93b..1327c07f 100644 --- a/studio/konfai_studio/workflow.py +++ b/studio/konfai_studio/workflow.py @@ -30,7 +30,7 @@ "read the log, give the cause in one line and the fix." ) _FOLLOW_READ = "Report it in two lines, then the next step." -_LAUNCHES = ("run_", "fine_tune_") +_LAUNCHES = ("run_",) # Stages reached before `initialize_session` has made a workspace: nothing there can be summarised yet. _BEFORE_WORKSPACE = {"dataset_inspection", "action_selection", "app_selection"} # One ceiling for the whole chain: the assistant's own block, the derived fill-in, and the bar. Past diff --git a/tests/integration/test_konfai_auto_patch_prediction.py b/tests/integration/test_konfai_auto_patch_prediction.py index a33f4411..03fd8c73 100644 --- a/tests/integration/test_konfai_auto_patch_prediction.py +++ b/tests/integration/test_konfai_auto_patch_prediction.py @@ -39,6 +39,7 @@ import torch import konfai.predictor as predictor_module +import konfai.predictor.loop as predictor_loop import konfai.utils.vram as vram_module from konfai.predictor import build_predict, predict from konfai.trainer import train @@ -48,7 +49,7 @@ def install_auto_patch_probes() -> None: """Force the first attempt to OOM and stub the CUDA readings (this is a CPU-only run).""" - original_run = predictor_module._Predictor.run + original_run = predictor_loop._Predictor.run def run_with_forced_oom(self): ATTEMPTS.append(list(self.dataset.get_patch_config()[0])) @@ -56,7 +57,7 @@ def run_with_forced_oom(self): raise torch.cuda.OutOfMemoryError("forced OOM: pretend the full-slice forward does not fit") return original_run(self) - predictor_module._Predictor.run = run_with_forced_oom + predictor_loop._Predictor.run = run_with_forced_oom vram_module.transient_at_oom = lambda device: None vram_module.usable_after_oom = lambda device: 1.0 diff --git a/tests/unit/test_case_expansion.py b/tests/unit/test_case_expansion.py index a807f48f..864a0226 100644 --- a/tests/unit/test_case_expansion.py +++ b/tests/unit/test_case_expansion.py @@ -457,7 +457,7 @@ def spy(self, a, *args, **kwargs): ("draw", "regime", "atol"), [ (lambda: _draw(Noise(1.0)), (Verdict.STREAM, Regime.SHARED), 0.0), # a field hashed on position: per-voxel - (lambda: _draw(CutOUT(1.0, 0.5, 0.0)), (Verdict.STREAM, Regime.SHARED), 0.0), # a box on the volume's grid + (lambda: _draw(CutOUT(0.5, 0.0)), (Verdict.STREAM, Regime.SHARED), 0.0), # a box on the volume's grid ( lambda: _draw(Rotate(a_min=10.0, a_max=10.0)), (Verdict.STREAM, Regime.SOLO), diff --git a/tests/unit/test_data_manager.py b/tests/unit/test_data_manager.py index c6e34102..0eab94a5 100644 --- a/tests/unit/test_data_manager.py +++ b/tests/unit/test_data_manager.py @@ -43,9 +43,9 @@ PredictionSubset, Subset, WindowedCaseSampler, - _cache_worker_count, collate_konfai, ) +from konfai.data.data_manager.samples import _cache_worker_count from konfai.data.patching import DatasetManager, DatasetPatch from konfai.data.transform import Gradient, TensorCast, Transform, TransformLoader from konfai.utils.clock import restart_startup_clock diff --git a/tests/unit/test_data_stream.py b/tests/unit/test_data_stream.py index bbf7a4f6..36322505 100644 --- a/tests/unit/test_data_stream.py +++ b/tests/unit/test_data_stream.py @@ -459,7 +459,7 @@ def test_publishing_an_entry_retires_dead_writers_debris_and_keeps_live_ones(tmp import subprocess import sys - from konfai.utils.dataset import _retire_dead_debris + from konfai.utils.dataset.staging import _retire_dead_debris final = tmp_path / "CT.ome.zarr" final.mkdir() @@ -547,7 +547,7 @@ def spy(self, source, dest): def _kill_between_the_two_moves(dataset: Dataset, name: str, volume: np.ndarray, attributes: Attribute) -> None: """Leave on disk exactly what a writer killed between the move-aside and the publish leaves: the previous entry under its backup name, nothing under its own.""" - from konfai.utils.dataset import _replaced_name + from konfai.utils.dataset.staging import _replaced_name dataset.write("CT", name, volume, attributes) if dataset.file_format == "h5": diff --git a/tests/unit/test_dataset.py b/tests/unit/test_dataset.py index 7a1449d5..56f7155b 100644 --- a/tests/unit/test_dataset.py +++ b/tests/unit/test_dataset.py @@ -26,7 +26,9 @@ import numpy as np import pytest import torch -from konfai.utils.dataset import Attribute, Dataset, _get_h5_file_lock, get_infos, image_to_data +from konfai.utils.dataset import Attribute, Dataset, get_infos, image_to_data +from konfai.utils.dataset import raw_block as raw_block_module +from konfai.utils.dataset.h5 import _get_h5_file_lock from konfai.utils.errors import DatasetManagerError sitk = pytest.importorskip("SimpleITK") @@ -153,7 +155,7 @@ def test_h5_read_chunk_cache_takes_its_slice_of_the_declared_budget() -> None: capacity now stays inside the same cache share every other decoded-block cache draws from.""" pytest.importorskip("h5py") from konfai.utils.budget import BUDGET_SHARES, set_per_rank_budget - from konfai.utils.dataset import _H5ReadPool + from konfai.utils.dataset.h5 import _H5ReadPool try: set_per_rank_budget(256 << 20) @@ -470,7 +472,7 @@ def test_init_keeps_token_for_plain_file_dataset(tmp_path: Path) -> None: def test_a_statistics_chunk_is_budgeted_with_its_channels() -> None: # A chunk spans every other axis whole, the channels included, and is accumulated in float64. Cut # on a plane alone, a 122-channel volume holds 122 times the budget: 7 GiB where 0.06 was meant. - from konfai.utils.dataset import _STATISTICS_CHUNK_ELEMENTS, _statistics_chunk_length + from konfai.utils.dataset.statistics import _STATISTICS_CHUNK_ELEMENTS, _statistics_chunk_length for channels in (1, 4, 122): shape = [channels, 400, 512, 512] @@ -481,7 +483,7 @@ def test_a_statistics_chunk_is_budgeted_with_its_channels() -> None: def test_a_statistics_chunk_reaches_further_on_a_thin_volume() -> None: - from konfai.utils.dataset import _statistics_chunk_length + from konfai.utils.dataset.statistics import _statistics_chunk_length thin, wide = [1, 400, 64, 64], [1, 400, 512, 512] assert _statistics_chunk_length(thin, 1, budget=1 << 20) > _statistics_chunk_length(wide, 1, budget=1 << 20) @@ -664,7 +666,7 @@ def test_an_evicted_h5_handle_goes_back_with_the_view_it_had(tmp_path: Path) -> it on the way back would hand it the store as it is now and launder a stale view into a fresh-looking one, so the write that arrived meanwhile would stay invisible for the rest of the process.""" pytest.importorskip("h5py") - from konfai.utils.dataset import _h5_read_pool + from konfai.utils.dataset.h5 import _h5_read_pool root = str(tmp_path / "ds") + "/" Path(root).mkdir() @@ -850,7 +852,7 @@ def _block_image(data: np.ndarray, direction: np.ndarray) -> "sitk.Image": def _write_block_fixture(root: Path, kind: str) -> tuple[Path, np.ndarray]: """One file of ``kind`` under ``root``, and the channel-first array it holds.""" - from konfai.utils.dataset import _MhaDataStream, _NiftiDataStream + from konfai.utils.dataset.stream import _MhaDataStream, _NiftiDataStream rng = np.random.default_rng(len(kind)) scalar = (rng.normal(size=(1, 12, 14, 16)) * 100).astype(np.float32) @@ -937,10 +939,9 @@ def test_a_region_off_the_raw_block_is_the_one_itk_decodes( tmp_path: Path, monkeypatch, kind: str, corner: bool ) -> None: """Same bytes, same dtype, same attribute record (keys, order, text) as ITK's streaming reader.""" - from konfai.utils import dataset as dataset_module path, data = _write_block_fixture(tmp_path, kind) - assert dataset_module._pixel_block(str(path)) is not None + assert raw_block_module._pixel_block(str(path)) is not None assert Dataset.SitkFile._supports_region_read(str(path)) region = _block_region(data, corner) backend = _block_backend(path) @@ -993,10 +994,9 @@ def test_every_patch_of_a_grid_records_what_itk_records(tmp_path: Path, monkeypa @pytest.mark.parametrize("kind", _BLOCK_LEFT_TO_ITK) def test_a_file_the_block_route_declines_is_still_read_by_itk(tmp_path: Path, kind: str) -> None: """Compressed, detached, rescaled, or another format: the block route steps aside, ITK answers.""" - from konfai.utils import dataset as dataset_module path, data = _write_block_fixture(tmp_path, kind) - assert dataset_module._pixel_block(str(path)) is None + assert raw_block_module._pixel_block(str(path)) is None region = _block_region(data) got, attributes = _block_backend(path).file_to_data_slice("", path.name.split(".", 1)[0], region) @@ -1066,7 +1066,6 @@ def test_a_stepped_region_carries_the_same_geometry_record_whatever_the_backend( def test_the_raw_block_header_is_read_once_and_follows_a_rewrite(tmp_path: Path, monkeypatch) -> None: """ITK reads the header once per file, not once per region; a file rewritten under the same name gets a record of its own.""" - from konfai.utils import dataset as dataset_module path, data = _write_block_fixture(tmp_path, "scalar.mha") reads = {"header": 0} @@ -1077,7 +1076,7 @@ def counting(self): return real(self) monkeypatch.setattr(sitk.ImageFileReader, "ReadImageInformation", counting) - dataset_module._pixel_block_at.cache_clear() + raw_block_module._pixel_block_at.cache_clear() backend = _block_backend(path) for plane in range(10): region = (slice(None), slice(plane, plane + 1), slice(None), slice(None)) diff --git a/tests/unit/test_dataset_backends.py b/tests/unit/test_dataset_backends.py index 50a3f53b..23af494a 100644 --- a/tests/unit/test_dataset_backends.py +++ b/tests/unit/test_dataset_backends.py @@ -7,7 +7,7 @@ def test_a_scan_raises_its_grain_onto_the_store_block_it_would_decode_anyway() - """A chunked store decodes whole blocks, so a scan stepping finer decodes the same block again at every step inside it -- measured at 85x, and 170x where the step straddled two. Where the budget holds a whole block the grain is raised to it, which reads each block once.""" - from konfai.utils.dataset import _scan_block_on_the_store_grid + from konfai.utils.dataset.statistics import _scan_block_on_the_store_grid rows, held = _scan_block_on_the_store_grid(rows=3, extent=512, plane=1000, granularity=[64], budget=1 << 30) assert rows == 64, "raised onto the grid" @@ -17,7 +17,7 @@ def test_a_scan_raises_its_grain_onto_the_store_block_it_would_decode_anyway() - def test_a_scan_that_cannot_afford_a_whole_block_is_charged_for_the_one_it_decodes() -> None: """Where the budget cannot hold a stored block the grain stays fine -- and what the store decodes is charged, so the plan refuses instead of the kernel.""" - from konfai.utils.dataset import _scan_block_on_the_store_grid + from konfai.utils.dataset.statistics import _scan_block_on_the_store_grid tight = 4 * 1000 * 4 * 8 # far under one 64-row block rows, held = _scan_block_on_the_store_grid(rows=3, extent=512, plane=1000, granularity=[64], budget=tight) @@ -26,7 +26,7 @@ def test_a_scan_that_cannot_afford_a_whole_block_is_charged_for_the_one_it_decod def test_an_unchunked_store_is_charged_for_what_it_is_asked_for() -> None: - from konfai.utils.dataset import _scan_block_on_the_store_grid + from konfai.utils.dataset.statistics import _scan_block_on_the_store_grid rows, held = _scan_block_on_the_store_grid(rows=7, extent=512, plane=1000, granularity=None, budget=1 << 30) assert (rows, held) == (7, 7 * 3 * 1000 * 4) diff --git a/tests/unit/test_dataset_statistics.py b/tests/unit/test_dataset_statistics.py index a771c7be..b06d7529 100644 --- a/tests/unit/test_dataset_statistics.py +++ b/tests/unit/test_dataset_statistics.py @@ -27,10 +27,11 @@ import numpy as np import pytest -from konfai.utils import dataset as dataset_module from konfai.utils.budget import set_per_rank_budget -from konfai.utils.dataset import ( - Dataset, +from konfai.utils.dataset import Dataset +from konfai.utils.dataset import h5 as h5_module +from konfai.utils.dataset import statistics as statistics_module +from konfai.utils.dataset.statistics import ( _finalize_running_statistics, _statistics_chunk_length, _update_pieces, @@ -93,10 +94,10 @@ def small_blocks(monkeypatch: pytest.MonkeyPatch) -> None: def _former_h5_walk(path: Path, group: str, name: str) -> Iterator[np.ndarray]: """Chunks along axis 1 (axis 0 for a vector), the dataset held open across them.""" - with dataset_module._open_h5(str(path), "r") as file: # the pool's handle is unlocked: agree with it + with h5_module._open_h5(str(path), "r") as file: # the pool's handle is unlocked: agree with it dataset = file[group][name] axis = 1 if dataset.ndim > 1 else 0 - length = _statistics_chunk_length(dataset.shape, axis, dataset_module._STATISTICS_CHUNK_ELEMENTS) + length = _statistics_chunk_length(dataset.shape, axis, statistics_module._STATISTICS_CHUNK_ELEMENTS) for start in range(0, dataset.shape[axis], length): slices = [slice(None)] * dataset.ndim slices[axis] = slice(start, min(dataset.shape[axis], start + length)) @@ -140,7 +141,7 @@ def _former_image_slab_walk(directory: Path, name: str, group: str, extension: s reader.SetFileName(path) reader.ReadImageInformation() shape = [reader.GetNumberOfComponents(), *reversed(reader.GetSize())] - length = _statistics_chunk_length(shape, 1, dataset_module._STATISTICS_CHUNK_ELEMENTS) + length = _statistics_chunk_length(shape, 1, statistics_module._STATISTICS_CHUNK_ELEMENTS) file = Dataset.SitkFile(f"{directory / name}/", True, extension) for start in range(0, shape[1], length): slices = [slice(None)] * len(shape) @@ -217,7 +218,7 @@ def test_a_npy_folds_within_ulps_of_its_former_whole_pass(tmp_path: Path, small_ def _former_zarr_walk(directory: Path, name: str, group: str) -> Iterator[np.ndarray]: file = Dataset.OmeZarrFile(f"{directory / name}/", True) shape, _ = file.get_infos("", group) - length = _statistics_chunk_length(shape, 1, dataset_module._STATISTICS_CHUNK_ELEMENTS) + length = _statistics_chunk_length(shape, 1, statistics_module._STATISTICS_CHUNK_ELEMENTS) for start in range(0, shape[1], length): slices = [slice(None)] * len(shape) slices[1] = slice(start, min(shape[1], start + length)) @@ -418,11 +419,11 @@ def test_a_scan_of_a_float64_source_reads_blocks_the_budget_holds( dataset.write("CT", "P0", volume, image_attributes([0.0, 0.0, 0.0], [1.0, 1.0, 1.0])) assert dataset.read_data_slice("CT", "P0", (slice(0, 1),) * 4)[0].dtype == np.float64 - budget = dataset_module._STATISTICS_BLOCKS_IN_FLIGHT * 8 * 8 * 40 * 40 # three eight-row float64 blocks + budget = statistics_module._STATISTICS_BLOCKS_IN_FLIGHT * 8 * 8 * 40 * 40 # three eight-row float64 blocks set_per_rank_budget(budget) blocks = _scan_blocks(dataset, monkeypatch) got = dataset.read_data_statistics("CT", "P0") - held = max(int(np.prod(shape)) for shape in blocks) * 8 * dataset_module._STATISTICS_BLOCKS_IN_FLIGHT + held = max(int(np.prod(shape)) for shape in blocks) * 8 * statistics_module._STATISTICS_BLOCKS_IN_FLIGHT assert held <= budget, f"{held} B held against a {budget} B budget" _assert_close_to_numpy(got, volume) diff --git a/tests/unit/test_dataset_streaming.py b/tests/unit/test_dataset_streaming.py index c9a7641c..f36cfbf5 100644 --- a/tests/unit/test_dataset_streaming.py +++ b/tests/unit/test_dataset_streaming.py @@ -29,10 +29,8 @@ import pytest import torch from konfai.data.augmentation import DataAugmentationsList -from konfai.data.data_manager import ( - DatasetIter, - Group, - GroupTransform, +from konfai.data.data_manager import DatasetIter, Group, GroupTransform +from konfai.data.data_manager.groups import ( _check_patch_transform_invertible, _check_patch_transform_locality, _check_patch_transform_shape, @@ -43,7 +41,6 @@ Dilate, Flip, Gradient, - KonfAIInference, LocalityKind, Mask, Normalize, @@ -785,7 +782,6 @@ def test_streaming_still_seeds_a_global_stat_behind_a_reorientation(patch_manage ("transform", "kind"), [ (Standardize(mask="MASK"), LocalityKind.WHOLE_VOLUME), - (KonfAIInference(), LocalityKind.WHOLE_VOLUME), (Gradient(), LocalityKind.HALO), (Dilate(dilate=2), LocalityKind.HALO), (Flip(), LocalityKind.ORIENTATION), diff --git a/tests/unit/test_imaging_formats.py b/tests/unit/test_imaging_formats.py index 25b24ed6..cd6fa715 100644 --- a/tests/unit/test_imaging_formats.py +++ b/tests/unit/test_imaging_formats.py @@ -42,7 +42,7 @@ def test_flatten_transforms_recurses_into_nested_composites() -> None: nested (``GetNthTransform`` returns it as-is), so a single-level walk would hand that composite to the per-leaf type switch, which rejects it: the recursion is what keeps a nested chain storable.""" sitk = pytest.importorskip("SimpleITK") - from konfai.utils.dataset import _flatten_transforms + from konfai.utils.dataset.attribute import _flatten_transforms inner = sitk.CompositeTransform([sitk.Euler3DTransform(), sitk.AffineTransform(3)]) outer = sitk.CompositeTransform(3) diff --git a/tests/unit/test_itk_transform_backend.py b/tests/unit/test_itk_transform_backend.py index 642fd90b..6bdcc75d 100644 --- a/tests/unit/test_itk_transform_backend.py +++ b/tests/unit/test_itk_transform_backend.py @@ -30,6 +30,7 @@ pytest.importorskip("h5py") from konfai.utils.dataset import Attribute, Dataset # noqa: E402 +from konfai.utils.dataset import h5 as h5_module # noqa: E402 from konfai.utils.errors import DatasetManagerError # noqa: E402 from oracle_support import geometry # noqa: E402 @@ -215,12 +216,11 @@ def test_without_h5py_the_backend_names_the_extra_to_install(tmp_path: Path, mon def test_a_region_read_opens_the_file_once_for_the_process(tmp_path: Path, monkeypatch) -> None: """The header and the region come off one pooled handle: past the first region, no open at all.""" - from konfai.utils import dataset as dataset_module dataset = Dataset(tmp_path / "out", "itktransform") dataset.write("Transform", "P000", _field(6), _attributes()) opens = {"count": 0} - real = dataset_module._open_h5 + real = h5_module._open_h5 def counting(*args, **kwargs): opens["count"] += 1 diff --git a/tests/unit/test_measure.py b/tests/unit/test_measure.py index 06f4a1d4..e24fe3d7 100644 --- a/tests/unit/test_measure.py +++ b/tests/unit/test_measure.py @@ -30,8 +30,8 @@ LabelledValues, PerceptualLoss, Variance, - _require_optional, ) +from konfai.metric.measure.base import _require_optional from konfai.network.network import CriterionsAttr from konfai.utils.errors import MeasureError @@ -829,7 +829,7 @@ class TestMaskedFeatureLoss: @staticmethod def _run(weights, mask=None, patch_shape=None, project=None, x=None, y=None): - from konfai.metric.measure import _masked_feature_loss + from konfai.metric.measure.impact import _masked_feature_loss torch.manual_seed(0) x = torch.rand(1, 1, 32, 32) if x is None else x @@ -881,7 +881,7 @@ class NaNModel(torch.nn.Module): def forward(self, x, nb_layer, stats=None): return [x, torch.full_like(x, torch.nan)] - from konfai.metric.measure import _masked_feature_loss + from konfai.metric.measure.impact import _masked_feature_loss x, y = torch.rand(1, 1, 8, 8), torch.rand(1, 1, 8, 8) triple = lambda t: [t, torch.tensor([2]), torch.tensor([[0.0, 0.5, 1.0, 0.2]])] # noqa: E731 diff --git a/tests/unit/test_mind_descriptor.py b/tests/unit/test_mind_descriptor.py index 6f7f5656..e77b4596 100644 --- a/tests/unit/test_mind_descriptor.py +++ b/tests/unit/test_mind_descriptor.py @@ -100,7 +100,7 @@ def test_konfai_mind_is_a_frozen_network_and_survives_load_init() -> None: model.set_name("MIND") assert isinstance(model, Network) # No learnable parameters (all shift kernels are frozen). - assert not any(p.requires_grad for p in model.parameters(pretrained=False)) + assert not any(p.requires_grad for p in model.parameters()) kernel_before = model["Descriptor"].conv1.weight.detach().clone() model.load({}, init=True) # the trainer's fresh-start call diff --git a/tests/unit/test_ome_zarr_data_surface.py b/tests/unit/test_ome_zarr_data_surface.py index 05ed2336..81f09720 100644 --- a/tests/unit/test_ome_zarr_data_surface.py +++ b/tests/unit/test_ome_zarr_data_surface.py @@ -37,7 +37,8 @@ write_ome_zarr, ) from konfai.data.transform import Transform -from konfai.utils.dataset import Attribute, Dataset, _store_chunks +from konfai.utils.dataset import Attribute, Dataset +from konfai.utils.dataset.ome_zarr_file import _store_chunks from konfai.utils.errors import DatasetManagerError from oracle_support import geometry @@ -324,9 +325,9 @@ def test_forgetting_one_store_leaves_the_others_alone() -> None: def test_a_sweep_declares_the_regions_it_will_read(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """The sweep folds every block's pull map before it reads the first, so it can say what is coming; a store that caches decoded blocks is the one thing that can use it.""" - from konfai.data import patching as patching_module from konfai.data.materialize import CaseMaterializer from konfai.data.patching import DatasetManager, DatasetPatch + from konfai.data.patching import sweep as sweep_module from konfai.data.transform import Clip, Save monkeypatch.setattr("konfai.data.patching.budget.SWEEP_SLAB_ROWS", 3) @@ -351,7 +352,7 @@ def test_a_sweep_declares_the_regions_it_will_read(tmp_path: Path, monkeypatch: assert declared, "the sweep declared nothing" windows = declared[0] - assert len(windows) == len(list(patching_module._sweep_targets([12, 8, 6], [3, 8, 6]))) + assert len(windows) == len(list(sweep_module._sweep_targets([12, 8, 6], [3, 8, 6]))) assert all(len(window) == 4 for window in windows), "a window covers the channel axis too" diff --git a/tests/unit/test_package_exports.py b/tests/unit/test_package_exports.py new file mode 100644 index 00000000..695cd8d3 --- /dev/null +++ b/tests/unit/test_package_exports.py @@ -0,0 +1,44 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""The package ``__init__`` files re-export the public surface only. + +A private name reachable through a package path invites external code onto internals, and a +re-export of a module global its home module rebinds (``_rank_pool``) is a frozen snapshot that +goes stale after the first rebind. Private names import from their defining submodule.""" + +import importlib + +import pytest + +PACKAGES = [ + "konfai.data.augmentation", + "konfai.data.data_manager", + "konfai.data.patching", + "konfai.data.transform", + "konfai.metric.measure", + "konfai.network.network", + "konfai.predictor", + "konfai.utils.dataset", + "konfai.utils.runtime", +] + + +@pytest.mark.parametrize("package", PACKAGES) +def test_package_init_re_exports_no_private_names(package: str) -> None: + module = importlib.import_module(package) + private = sorted(n for n in vars(module) if n.startswith("_") and not n.startswith("__")) + assert not private, f"{package} re-exports private names: {private}; import them from their defining submodule" diff --git a/tests/unit/test_packaging.py b/tests/unit/test_packaging.py index ac79d5ef..dfade86f 100644 --- a/tests/unit/test_packaging.py +++ b/tests/unit/test_packaging.py @@ -88,10 +88,11 @@ def import_without_simpleitk(name, *args, **kwargs): builtins.__import__ = import_without_simpleitk import konfai.data.transform +import konfai.utils.ITK assert konfai.data.transform.sitk is None try: - konfai.data.transform._require_simpleitk() + konfai.utils.ITK._require_simpleitk() except Exception as exc: assert "pip install konfai[itk]" in str(exc), str(exc) else: diff --git a/tests/unit/test_predictor.py b/tests/unit/test_predictor.py index f6780a83..2e058a64 100644 --- a/tests/unit/test_predictor.py +++ b/tests/unit/test_predictor.py @@ -32,7 +32,9 @@ import torch from konfai.data.augmentation import Flip from konfai.data.patching import Accumulator, blend_axes -from konfai.predictor import PREDICTION_CLOCK, OutputDataset, _AsyncWriter, _Predictor +from konfai.predictor import PREDICTION_CLOCK, OutputDataset +from konfai.predictor.loop import _Predictor +from konfai.predictor.output import _AsyncWriter from konfai.utils.dataset import Dataset from konfai.utils.errors import PredictorError from konfai.utils.utils import get_patch_slices_from_shape diff --git a/tests/unit/test_predictor_memory.py b/tests/unit/test_predictor_memory.py index 23bbfee5..0e31737f 100644 --- a/tests/unit/test_predictor_memory.py +++ b/tests/unit/test_predictor_memory.py @@ -23,15 +23,9 @@ from konfai.data.data_manager import BatchDataItem, DatasetIter from konfai.data.transform import TransformInverse from konfai.network.network import Network -from konfai.predictor import ( - PREDICTION_CLOCK, - Mean, - ModelComposite, - OutputDataset, - _colocate_loaded_modules, - _prediction_report, - _Predictor, -) +from konfai.predictor import PREDICTION_CLOCK, Mean, ModelComposite, OutputDataset +from konfai.predictor.ensemble import _colocate_loaded_modules +from konfai.predictor.loop import _prediction_report, _Predictor from konfai.utils.clock import SweepClock from konfai.utils.dataset import Attribute diff --git a/tests/unit/test_resample_to_reference.py b/tests/unit/test_resample_to_reference.py index c1502edd..955c692d 100644 --- a/tests/unit/test_resample_to_reference.py +++ b/tests/unit/test_resample_to_reference.py @@ -31,7 +31,7 @@ import pytest import torch from konfai.data.case_reduction import CaseReduction -from konfai.data.data_manager import _check_patch_transform_locality +from konfai.data.data_manager.groups import _check_patch_transform_locality from konfai.data.geometry import AffineMap, Grid, TransformBound from konfai.data.materialize import CaseMaterializer, Verdict from konfai.data.patching import DatasetManager, DatasetPatch diff --git a/tests/unit/test_resample_transform.py b/tests/unit/test_resample_transform.py index a4108c4d..624b1f02 100644 --- a/tests/unit/test_resample_transform.py +++ b/tests/unit/test_resample_transform.py @@ -29,13 +29,8 @@ import pytest import torch from konfai.data.geometry import DisplacementStage, Grid -from konfai.data.transform import ( - LocalityKind, - RegionContext, - Resample, - _optional_image_filler, - _SitkInput, -) +from konfai.data.transform import LocalityKind, RegionContext, Resample +from konfai.data.transform.resample import _optional_image_filler, _SitkInput from konfai.utils.dataset import DISPLACEMENT_FIELD_ATTRIBUTE, Attribute from konfai.utils.errors import TransformError @@ -757,7 +752,7 @@ def test_a_diagonal_stored_map_resamples_separably_within_the_routes_it_replaces blend, and nothing at all on a nearest pick, where every route copies the same voxel. """ from konfai.data.sampling import gather, source_index - from konfai.data.transform import _resample_with_sitk + from konfai.data.transform.resample import _resample_with_sitk image = _image(oblique=False) counts = (_phantom() * 4.0).astype(np.int16) diff --git a/tests/unit/test_runtime.py b/tests/unit/test_runtime.py index f9ca1de1..13218c00 100644 --- a/tests/unit/test_runtime.py +++ b/tests/unit/test_runtime.py @@ -992,3 +992,23 @@ def run_process(self, world_size, global_rank, local_rank, dataloaders) -> None: assert budget_module.per_rank_budget_bytes() == 96 << 20 assert ome_zarr._chunk_cache().capacity == ome_zarr.chunk_cache_capacity() + + +def test_run_distributed_app_refuses_a_kwarg_the_entrypoint_does_not_declare() -> None: + """A kwarg outside the signature and the cluster set must refuse, not vanish: the silent drop + is what forced main.py's --plan short-circuit.""" + + class Sentinel(Exception): + pass + + @rt_dist.run_distributed_app + def build(gpu: list[int] | None = None, cpu: int | None = None) -> None: + raise Sentinel + + with pytest.raises(ConfigError, match="plan"): + build(plan=True) + + # The tolerated names pass the gate and reach the build: the cluster set is read from the raw + # kwargs and 'command' is the CLI dispatch discriminator only TRAIN/RESUME declares. + with pytest.raises(Sentinel): + build(command="PREDICTION") diff --git a/tests/unit/test_sampling.py b/tests/unit/test_sampling.py index 3cad95f3..77df1f8d 100644 --- a/tests/unit/test_sampling.py +++ b/tests/unit/test_sampling.py @@ -142,7 +142,7 @@ def test_the_host_resampler_and_the_walk_agree_on_an_integer_payload(): handful of voxels) so a real divergence, which moves voxels by far more, cannot hide in it. ``nearest`` copies voxels and must be exact, integers included. """ - from konfai.data.transform import _resample_with_sitk + from konfai.data.transform.resample import _resample_with_sitk image = _image(oblique=True) counts = (sitk.GetArrayFromImage(image) * 4.0).astype(np.int16) diff --git a/tests/unit/test_save_streaming.py b/tests/unit/test_save_streaming.py index c718dee3..01185250 100644 --- a/tests/unit/test_save_streaming.py +++ b/tests/unit/test_save_streaming.py @@ -27,6 +27,7 @@ from konfai.data.patching import DatasetManager, DatasetPatch from konfai.data.transform import Clip, Permute, Save, Standardize, Transform from konfai.utils.dataset import Attribute, Dataset +from konfai.utils.dataset import stream as stream_module from oracle_support import geometry, manager pytest.importorskip("SimpleITK") @@ -128,7 +129,6 @@ def test_multi_slab_sweep_writes_the_same_cache_as_the_whole_volume_load( def test_failed_multi_slab_sweep_leaves_no_partial_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """A failure after the first slab is written must remove the partial entry, not publish it.""" - from konfai.utils import dataset as dataset_module monkeypatch.setattr("konfai.data.patching.budget.SWEEP_SLAB_ROWS", 4) source = _source(tmp_path) @@ -136,7 +136,7 @@ def test_failed_multi_slab_sweep_leaves_no_partial_cache(tmp_path: Path, monkeyp reference = _whole_volume_patches(manager, [Clip(0.0, 50.0)]) calls = 0 - real_write = dataset_module._MhaDataStream.write_slice + real_write = stream_module._MhaDataStream.write_slice def failing_after_first(self, slices, data): nonlocal calls @@ -145,10 +145,10 @@ def failing_after_first(self, slices, data): raise OSError("disk full") real_write(self, slices, data) - monkeypatch.setattr(dataset_module._MhaDataStream, "write_slice", failing_after_first) + monkeypatch.setattr(stream_module._MhaDataStream, "write_slice", failing_after_first) with pytest.warns(UserWarning, match="Falling back to the whole-volume path"): patch = manager.get_data(0, 0, [], True) - monkeypatch.setattr(dataset_module._MhaDataStream, "write_slice", real_write) + monkeypatch.setattr(stream_module._MhaDataStream, "write_slice", real_write) assert calls > 1 assert torch.equal(patch, reference[0]) @@ -215,7 +215,6 @@ def test_unstreamable_destination_keeps_the_whole_volume_path(tmp_path: Path) -> def test_failed_sweep_falls_back_to_the_whole_volume_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - from konfai.utils import dataset as dataset_module source = _source(tmp_path) transforms = [Clip(0.0, 50.0), Save(str(tmp_path / "cache"))] @@ -225,7 +224,7 @@ def test_failed_sweep_falls_back_to_the_whole_volume_path(tmp_path: Path, monkey def broken_write(self, slices, data): raise OSError("disk full") - monkeypatch.setattr(dataset_module._MhaDataStream, "write_slice", broken_write) + monkeypatch.setattr(stream_module._MhaDataStream, "write_slice", broken_write) with pytest.warns(UserWarning, match="Falling back to the whole-volume path"): patch = manager.get_data(0, 0, [], True) monkeypatch.undo() @@ -248,7 +247,6 @@ def test_an_interrupted_sweep_aborts_its_stream_and_does_not_fall_back( """Ctrl-C in the middle of a slab is not a sweep failure: the partial entry is removed and the interrupt propagates. Treated as one, it would be warned away, and the whole-volume fallback would then run the whole case the user just asked to stop.""" - from konfai.utils import dataset as dataset_module source = _source(tmp_path) manager = _manager(source, [Clip(0.0, 50.0), Save(str(tmp_path / "cache"))]) @@ -256,7 +254,7 @@ def test_an_interrupted_sweep_aborts_its_stream_and_does_not_fall_back( def interrupted_write(self, slices, data): raise KeyboardInterrupt - monkeypatch.setattr(dataset_module._MhaDataStream, "write_slice", interrupted_write) + monkeypatch.setattr(stream_module._MhaDataStream, "write_slice", interrupted_write) with pytest.raises(KeyboardInterrupt): manager.get_data(0, 0, [], True) monkeypatch.undo() diff --git a/tests/unit/test_streamed_oracle_expansion.py b/tests/unit/test_streamed_oracle_expansion.py index 3459778f..ecb7bc2b 100644 --- a/tests/unit/test_streamed_oracle_expansion.py +++ b/tests/unit/test_streamed_oracle_expansion.py @@ -28,8 +28,10 @@ import numpy as np import pytest +import torch from konfai.data.augmentation import CutOUT, DataAugmentation, Elastix, Noise, Rotate, Scale from konfai.data.augmentation import Flip as FlipDraw +from konfai.data.augmentation import Foreign as ForeignDraw from konfai.data.materialize import CaseMaterializer, Regime, Verdict from konfai.data.patching import DatasetManager from konfai.data.transform import Clip, Expand, Mask, Transform, Write @@ -75,11 +77,12 @@ def _draws() -> dict[str, Draw]: Built per call, because a draw caches the parameters it drew for a case.""" return { "Noise": Draw(lambda: Noise(1.0), Regime.SHARED), - "CutOUT": Draw(lambda: CutOUT(1.0, 0.5, 0.0), Regime.SHARED), + "CutOUT": Draw(lambda: CutOUT(0.5, 0.0), Regime.SHARED), "Flip": Draw(lambda: FlipDraw(f_prob=[1.0, 1.0, 1.0]), Regime.SOLO), "QuarterRotate": Draw(lambda: Rotate(is_quarter=True), Regime.SOLO), "Rotate": Draw(lambda: Rotate(a_min=10.0, a_max=10.0), Regime.SOLO, AUGMENTATION_ATOL), "Scale": Draw(lambda: Scale(), Regime.SOLO, AUGMENTATION_ATOL), + "Elastix": Draw(lambda: Elastix(grid_spacing=8, max_displacement=2), Regime.SOLO, AUGMENTATION_ATOL), } @@ -171,12 +174,13 @@ def chain(destination: Path) -> list[Transform]: def test_a_copy_that_cannot_stream_is_refused_under_a_budget_its_whole_volume_exceeds(tmp_path: Path) -> None: - """``Elastix`` solves its field over the whole volume, so its copies take the whole-volume path. + """A ``Foreign`` draw says nothing about where its output reads from, so its copies take the + whole-volume path. That path is priced, not free: under a budget the assembled case does not fit, the copies must be refused with the working set named, and nothing written. Given room, the same copies land.""" dataset = build_case(tmp_path / "case", GEOMETRIES[MAIN]) - draw = Elastix() + draw = ForeignDraw(torch.nn.Sigmoid(), "torch.nn:Sigmoid") draw.load(1.0) refused = _expanded(dataset, draw, 2, tmp_path / "refused") with pytest.raises(PatchError, match="exceeds the per-rank budget"): diff --git a/tests/unit/test_streamed_tta.py b/tests/unit/test_streamed_tta.py index 5b54d54a..0ac4681e 100644 --- a/tests/unit/test_streamed_tta.py +++ b/tests/unit/test_streamed_tta.py @@ -29,7 +29,8 @@ import pytest import torch from konfai.data.augmentation import Brightness, DataAugmentationsList, Flip, Permute -from konfai.data.data_manager import DatasetIter, _interleaved_case_entries +from konfai.data.data_manager import DatasetIter +from konfai.data.data_manager.order import _interleaved_case_entries from konfai.data.patching import DatasetPatch, Gaussian, SlabAligner from konfai.data.transform import Flip as FlipTransform from konfai.data.transform import InferenceStack, LocalityKind, Sum diff --git a/tests/unit/test_streamed_write_dispatcher.py b/tests/unit/test_streamed_write_dispatcher.py index d6742689..459e7e20 100644 --- a/tests/unit/test_streamed_write_dispatcher.py +++ b/tests/unit/test_streamed_write_dispatcher.py @@ -32,7 +32,8 @@ import pytest import torch from konfai.data.data_manager import DatasetIter -from konfai.data.patching import SlabRegionStream, _halo_radii +from konfai.data.patching import SlabRegionStream +from konfai.data.patching.stage import _halo_radii from konfai.data.transform import ( Canonical, Dilate, @@ -49,7 +50,8 @@ Transform, TransformInverse, ) -from konfai.predictor import Mean, OutputDataset, Reduction, _FinalizeStage +from konfai.predictor import Mean, OutputDataset, Reduction +from konfai.predictor.output import _FinalizeStage from konfai.utils.dataset import Attribute from konfai.utils.errors import PatchError diff --git a/tests/unit/test_sweep_pipeline.py b/tests/unit/test_sweep_pipeline.py index 913cefda..9a1aa4a0 100644 --- a/tests/unit/test_sweep_pipeline.py +++ b/tests/unit/test_sweep_pipeline.py @@ -29,10 +29,8 @@ import numpy as np import pytest from konfai.data.materialize import CaseMaterializer, Verdict -from konfai.data.patching import ( - SWEEP_CLOCK, - DatasetManager, - DatasetPatch, +from konfai.data.patching import SWEEP_CLOCK, DatasetManager, DatasetPatch +from konfai.data.patching.sweep import ( _ReadAhead, _stage_failure, _sweep_pipeline_depth, @@ -412,7 +410,8 @@ def test_a_pipelined_sweep_holds_no_more_blocks_than_the_height_rule_prices( ahead, the one the reader holds while the queue is full, and the one being written behind. Counted here as blocks between their read and their write, with a writer slower than the reader so the queue fills and the reader blocks on it.""" - from konfai.data.patching import RegionWriter, _sweep_resident_regions + from konfai.data.patching import RegionWriter + from konfai.data.patching.sweep import _sweep_resident_regions depth = 1 monkeypatch.setattr("konfai.data.patching.budget.SWEEP_SLAB_ROWS", 3) diff --git a/tests/unit/test_sweep_tiling.py b/tests/unit/test_sweep_tiling.py index 9fdc771d..6d26640b 100644 --- a/tests/unit/test_sweep_tiling.py +++ b/tests/unit/test_sweep_tiling.py @@ -27,12 +27,11 @@ import numpy as np import pytest import torch -from konfai.data import patching as patching_module from konfai.data.materialize import CaseMaterializer, Verdict -from konfai.data.patching import ( - _SWEEP_ELEMENT_BYTES, - DatasetManager, - DatasetPatch, +from konfai.data.patching import DatasetManager, DatasetPatch +from konfai.data.patching import budget as budget_module +from konfai.data.patching.budget import _SWEEP_ELEMENT_BYTES +from konfai.data.patching.sweep import ( _cubic_tile, _pull_block_voxels, _sweep_pipeline_depth, @@ -40,8 +39,9 @@ _sweep_targets, ) from konfai.data.transform import OneHot, Resample, Save -from konfai.utils.dataset import Attribute, Dataset, _store_chunks +from konfai.utils.dataset import Attribute, Dataset from konfai.utils.dataset import chunk_hull_voxels as _chunk_hull_voxels +from konfai.utils.dataset.ome_zarr_file import _store_chunks from konfai.utils.errors import DatasetManagerError from oracle_support import geometry, manager @@ -403,7 +403,7 @@ def test_a_region_under_one_stored_block_is_priced_at_the_block_it_decodes( ungranular = _priced(manager, (), 4) _chunked(monkeypatch, (16, 128, 128)) - manager._read_granularity = patching_module._UNRESOLVED + manager._read_granularity = budget_module._UNRESOLVED assert _priced(manager, (), 4) > ungranular, "the block it decodes is charged" # Halving the rows does not halve the price: both cut inside one stored block, and that block is # decoded whole either way. @@ -416,7 +416,7 @@ def test_the_sizing_lands_on_the_store_grid_when_a_block_fits(tmp_path: Path, mo source, _volume = _sheared_fixture(tmp_path) manager = _manager(source, [Save(f"{tmp_path / 'out'}:h5")]) _chunked(monkeypatch, (16, 128, 128)) - manager._read_granularity = patching_module._UNRESOLVED + manager._read_granularity = budget_module._UNRESOLVED manager.set_memory_budget(float(_priced(manager, (), 20))) assert manager._sweep_tile(list(LANDING), 1)[0] % 16 == 0 @@ -433,7 +433,7 @@ def test_a_region_read_is_charged_only_for_what_the_block_grid_adds( assert manager.region_reads(6).widest_excess == 0, "a store with no block grid adds nothing" _chunked(monkeypatch, (16, 128, 128)) - manager._read_granularity = patching_module._UNRESOLVED + manager._read_granularity = budget_module._UNRESOLVED assert manager.region_reads(16).widest_excess < manager.region_reads(4).widest_excess, ( "a region under one stored block wastes more of the block it decodes, not less" ) diff --git a/tests/unit/test_transform_working_multiple.py b/tests/unit/test_transform_working_multiple.py index 89b157e0..c32c5ed4 100644 --- a/tests/unit/test_transform_working_multiple.py +++ b/tests/unit/test_transform_working_multiple.py @@ -65,7 +65,6 @@ #: own payload as a covered one, and five stages were being skipped without anyone choosing it. _NEEDS_MORE_THAN_A_PAYLOAD = { "HistogramMatching": "a reference dataset to match the histogram against", - "KonfAIInference": "a model to run", "Resample": "a reference dataset to resample onto", } From 4de4ddd97b79277460ddc8903d845163beaaedf5 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 08:33:55 +0200 Subject: [PATCH 20/28] test(budget): reach the cache element size through its defining submodule --- tests/unit/test_memory_budget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_memory_budget.py b/tests/unit/test_memory_budget.py index 4ed9dde8..56992633 100644 --- a/tests/unit/test_memory_budget.py +++ b/tests/unit/test_memory_budget.py @@ -215,7 +215,7 @@ def test_a_slurm_per_cpu_grant_is_multiplied_by_the_task_cpus(monkeypatch: pytes # 8 volumes x 512 elements x 4 bytes = 16384 bytes. _GROUP_SHAPE = [1, 8, 8, 8] _CASES = ["case_a", "case_b", "case_c", "case_d"] -_DATASET_BYTES = 2 * len(_CASES) * 512 * data_manager._CACHE_ELEMENT_BYTES +_DATASET_BYTES = 2 * len(_CASES) * 512 * data_manager.samples._CACHE_ELEMENT_BYTES def _make_train(memory_budget: str | float | None) -> DataTrain: From 00c456b3c9c5bbca609b7bcaaf91477065ad57fb Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 08:51:36 +0200 Subject: [PATCH 21/28] feat: konfai list discovery and the pretrained_from config key The config vocabulary IS the product surface, and only the MCP server could enumerate it: list_components moves into core (transforms, augmentations, criteria, reductions, both model catalogs, builder blocks -- the builder registries gained public accessors) with a konfai list subcommand, lazy so --help stays light. Model.pretrained_from turns the execution-order weight bridge, until now proven only by parity tests, into a YAML key: build the reference module by classpath, load its checkpoint, transfer into the KonfAI graph (every tensor filled or a named refusal), and TRAIN starts from the transferred weights exactly as from a .pt; RESUME and PREDICTION checkpoints always win. Includes the checkpoint(use_reentrant=False) migration and one belt leftover in the dispatcher test. --- konfai/api.py | 14 ++ konfai/main.py | 26 +++ konfai/network/network/model.py | 12 +- konfai/network/network/network.py | 15 +- konfai/utils/catalog.py | 214 ++++++++++++++++++++ konfai/utils/model_builder.py | 10 + konfai/utils/pretrained.py | 105 ++++++++++ tests/unit/test_api.py | 25 ++- tests/unit/test_main_cli.py | 32 +++ tests/unit/test_pretrained_from.py | 162 +++++++++++++++ tests/unit/test_streamed_read_dispatcher.py | 16 +- 11 files changed, 623 insertions(+), 8 deletions(-) create mode 100644 konfai/utils/catalog.py create mode 100644 tests/unit/test_pretrained_from.py diff --git a/konfai/api.py b/konfai/api.py index 9b34ffec..38ab2db4 100644 --- a/konfai/api.py +++ b/konfai/api.py @@ -56,6 +56,7 @@ if TYPE_CHECKING: from konfai.transformer import TransformPlan + from konfai.utils.catalog import Component from konfai.utils.runtime import DistributedObject _T = TypeVar("_T") @@ -229,6 +230,19 @@ def _stage_sequence(stages: object, where: str) -> Sequence[object]: ) +def list_components(kind: str) -> "list[Component]": + """Enumerate the shipped components of one kind, spelled as a YAML config references them. + + ``kind`` is ``transform``, ``augmentation``, ``criterion``, ``reduction``, ``model`` or + ``block`` (plural spellings accepted): the vocabulary the config trees above are written in. + Records carry ``name``, ``config_reference``, ``module`` and the one-line ``doc``. The catalog + imports the component families (torch included), hence the lazy import. + """ + from konfai.utils.catalog import list_components as _list_components + + return _list_components(kind) + + def _dataset_filenames(datasets: str | Path | Sequence[str | Path]) -> list[str]: entries = [datasets] if isinstance(datasets, (str, Path)) else list(datasets) if not entries: diff --git a/konfai/main.py b/konfai/main.py index db204ef6..a56756b7 100644 --- a/konfai/main.py +++ b/konfai/main.py @@ -198,6 +198,27 @@ def _add_transform(subparsers: argparse._SubParsersAction) -> None: ) +#: The component families `konfai list` prints, spelled as the CLI takes them. +_LIST_KINDS = ("transforms", "augmentations", "criteria", "reductions", "models", "blocks") + + +def _add_list(subparsers: argparse._SubParsersAction) -> None: + parser = subparsers.add_parser( + "list", help="List the components a YAML config can reference (name and one-line doc)." + ) + parser.add_argument("kind", choices=_LIST_KINDS, help="Component family to list.") + + +def _run_list(kind: str) -> None: + # Lazy: the catalog imports the component families (torch included), which --help must not pay for. + from konfai.utils.catalog import list_components + + components = list_components(kind) + width = max((len(component.config_reference) for component in components), default=0) + for component in components: + print(f"{component.config_reference:<{width}} {component.doc or ''}".rstrip()) + + # Command -> (implementation module, entrypoint, the kwarg the config path travels under). # Imports stay lazy and by name: the heavy modules load only for the command that runs. _COMMANDS: dict[str, tuple[str, str, str]] = { @@ -262,6 +283,10 @@ def _check_gpu_ids(parser: argparse.ArgumentParser, gpu: list[int]) -> None: def _dispatch(parser: argparse.ArgumentParser, args: dict[str, Any]) -> None: + if args["command"] == "list": + # Before the workflow machinery: `list` declares only its kind, none of the run flags. + _run_list(args["kind"]) + return if args["command"] not in _COMMANDS: # Exhaustive on purpose: a fallback would silently launch the trainer for any command it # does not know: a new workflow would train a UNet instead of failing. @@ -297,6 +322,7 @@ def _run(parser: argparse.ArgumentParser) -> None: _add_predict(subparsers) _add_evaluate(subparsers) _add_transform(subparsers) + _add_list(subparsers) parser.add_argument("--version", action=_VersionAction, help="Print KonfAI version and exit.") _dispatch(parser, vars(parser.parse_args())) diff --git a/konfai/network/network/model.py b/konfai/network/network/model.py index 9782b98a..58d51389 100644 --- a/konfai/network/network/model.py +++ b/konfai/network/network/model.py @@ -31,6 +31,7 @@ from konfai.utils.clock import SweepClock from konfai.utils.config import apply_config, config from konfai.utils.errors import ConfigError +from konfai.utils.pretrained import PretrainedFrom from konfai.utils.utils import get_module @@ -38,9 +39,15 @@ class ModelLoader: """Instantiate the root model graph declared in the active configuration.""" - def __init__(self, classpath: str = "default|segmentation.UNet.UNet", allow_head_resize: bool = False) -> None: + def __init__( + self, + classpath: str = "default|segmentation.UNet.UNet", + allow_head_resize: bool = False, + pretrained_from: PretrainedFrom | None = None, + ) -> None: self.classpath = classpath self.allow_head_resize = allow_head_resize + self.pretrained_from = pretrained_from def _apply_options(self, model: Network) -> Network: # The loader can only ENABLE the head resize: a model class that opted in through its own @@ -49,6 +56,9 @@ def _apply_options(self, model: Network) -> Network: for module in model.modules(): if isinstance(module, Network): module.allow_head_resize = True + # Consumed by Network.load: a fresh (checkpoint-less) TRAIN load seeds the initialised graph + # from the reference; a checkpoint's own weights always win over it. + model.pretrained_source = self.pretrained_from return model def _yaml_path(self) -> Path | None: diff --git a/konfai/network/network/network.py b/konfai/network/network/network.py index 24a6671b..3af36002 100644 --- a/konfai/network/network/network.py +++ b/konfai/network/network/network.py @@ -25,7 +25,7 @@ from collections.abc import Callable, Iterable, Iterator, Sequence from contextlib import nullcontext from functools import partial -from typing import Any, Self +from typing import TYPE_CHECKING, Any, Self import torch from torch.utils.checkpoint import checkpoint @@ -48,6 +48,9 @@ from konfai.utils.errors import ConfigError from konfai.utils.runtime import State, get_device, get_gpu_memory +if TYPE_CHECKING: + from konfai.utils.pretrained import PretrainedFrom + _log = logging.getLogger(__name__) @@ -358,7 +361,7 @@ def named_forward( out = checkpoint( module, *[branchs[i] for i in self._modulesArgs[name].in_branch], - use_reentrant=True, + use_reentrant=False, ) for ob in self._modulesArgs[name].out_branch: branchs[ob] = out @@ -593,6 +596,9 @@ def __init__( #: Opt-in: a checkpoint head whose out-channels mismatch may be re-initialised and #: overlap-copied instead of failing the load (transfer to a different label set). self.allow_head_resize = allow_head_resize + #: Set on the root by ModelLoader when ``Model.pretrained_from`` is declared: a fresh + #: (checkpoint-less) load seeds the initialised graph from the external reference. + self.pretrained_source: PretrainedFrom | None = None self._it = 0 self._nb_lr_update = 0 self.outputsGroup: list[OutputsGroup] = [] @@ -760,6 +766,11 @@ def load( else: model_state_dict[alias] = model_state_dict_tmp[alias] self.load_state_dict(model_state_dict) + elif self.pretrained_source is not None and not ema: + # A fresh TRAIN carries no checkpoint entry: the declared reference seeds the graph the + # init above just randomised. The EMA copy is deepcopied from the seeded model and must + # not pay the transfer again; a checkpoint's own weights take the branch above instead. + self.pretrained_source.seed(self) if f"{state_key}_optimizer_state_dict" in state_dict and self.optimizer: self.optimizer.load_state_dict(state_dict[f"{state_key}_optimizer_state_dict"]) if f"{state_key}_it" in state_dict: diff --git a/konfai/utils/catalog.py b/konfai/utils/catalog.py new file mode 100644 index 00000000..0dece6e9 --- /dev/null +++ b/konfai/utils/catalog.py @@ -0,0 +1,214 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Enumerate the component vocabulary a KonfAI config can reference. + +The config is the product surface, so its vocabulary must be discoverable without reading the +source: :func:`list_components` returns, per kind, every shipped component with the exact spelling +a YAML config references it by and the first line of its own documentation. The CLI surfaces it as +``konfai list ``; :func:`konfai.api.list_components` re-exports it for Python callers. + +The component families import torch and the imaging stack, so everything heavy is imported inside +the functions: importing this module stays cheap. +""" + +from __future__ import annotations + +import importlib +import inspect +import os +from dataclasses import dataclass +from typing import Any + +from konfai.utils.errors import ConfigError + +#: Kinds backed by "concrete subclasses of one base class, re-exported by one package". +_SUBCLASS_KINDS: dict[str, tuple[str, str]] = { + "transform": ("konfai.data.transform", "Transform"), + "augmentation": ("konfai.data.augmentation", "DataAugmentation"), + "criterion": ("konfai.metric.measure", "Criterion"), + "reduction": ("konfai.data.reduction", "Reduction"), +} + +COMPONENT_KINDS: tuple[str, ...] = (*_SUBCLASS_KINDS, "model", "block") + +_KIND_ALIASES: dict[str, str] = { + "transforms": "transform", + "augmentations": "augmentation", + "criteria": "criterion", + "loss": "criterion", + "losses": "criterion", + "metric": "criterion", + "metrics": "criterion", + "reductions": "reduction", + "models": "model", + "blocks": "block", +} + + +@dataclass(frozen=True) +class Component: + """One referenceable component of the shipped catalog.""" + + #: The class (or block/file) name. + name: str + #: The exact spelling a YAML config references it by: a bare class name for transforms, + #: augmentations, criteria and reductions; a ``Model.classpath`` value for models + #: (``segmentation.UNet.UNet`` or ``default|UNet.yml``); a module ``type`` for builder blocks. + config_reference: str + #: The importable module defining it (``None`` for a declarative catalog file or a block). + module: str | None + #: The first line of its own docstring (``None`` when it carries none). + doc: str | None + + +def normalize_kind(kind: str) -> str: + """The canonical kind for ``kind``, accepting the plural/synonym spellings.""" + canonical = _KIND_ALIASES.get(kind.strip().lower(), kind.strip().lower()) + if canonical not in COMPONENT_KINDS: + raise ConfigError( + f"Unknown component kind '{kind}'.", + f"Expected one of: {', '.join(COMPONENT_KINDS)} (plural spellings are accepted).", + ) + return canonical + + +def _doc_summary(obj: Any) -> str | None: + # The object's OWN docstring (``__doc__`` on the dict, not inherited): a component without one + # reports None rather than its base class's documentation. + own = obj.__dict__.get("__doc__") if isinstance(obj, type) else getattr(obj, "__doc__", None) + if not own or not own.strip(): + return None + return inspect.cleandoc(own).splitlines()[0].strip() or None + + +def _requires_callable_argument(cls: type) -> bool: + # A base helper taking an injected ``loss: Callable`` (MaskedLoss) cannot be built from YAML: + # listing it would advertise a spelling the reflection engine refuses. + try: + parameters = list(inspect.signature(cls).parameters.values()) + except (TypeError, ValueError): + return False + return any( + parameter.default is inspect.Parameter.empty and "Callable" in str(parameter.annotation) + for parameter in parameters + ) + + +def _list_subclasses(module_path: str, base_name: str) -> list[Component]: + module = importlib.import_module(module_path) + base = getattr(module, base_name) + components = [] + for name, obj in inspect.getmembers(module, inspect.isclass): + if obj is base or not issubclass(obj, base): + continue + if inspect.isabstract(obj) or name.startswith("_") or not obj.__module__.startswith("konfai"): + continue + if _requires_callable_argument(obj): + continue + components.append(Component(name=name, config_reference=name, module=obj.__module__, doc=_doc_summary(obj))) + return sorted(components, key=lambda component: component.name) + + +def _list_blocks() -> list[Component]: + from konfai.utils.model_builder import registered_module_types, registered_object_types + + registries = {**registered_object_types(), **registered_module_types()} + return sorted( + ( + Component(name=name, config_reference=name, module=None, doc=_doc_summary(factory)) + for name, factory in registries.items() + ), + key=lambda component: component.name, + ) + + +def _list_python_models() -> list[Component]: + # The builtin Python models live under konfai/models/python (PEP 420, no __init__.py): walk the + # files the way ModelLoader resolves a classpath, listing each Network subclass under the short + # '..' spelling. A module whose optional dependency is missing cannot be + # referenced either, so it is skipped rather than reported. + models_pkg = importlib.import_module("konfai.models.python") + network_base = importlib.import_module("konfai.network.network").Network + components: dict[str, Component] = {} + for root in list(getattr(models_pkg, "__path__", [])): + for dirpath, _dirs, files in os.walk(root): + for filename in sorted(files): + if not filename.endswith(".py") or filename.startswith("_"): + continue + rel = os.path.relpath(os.path.join(dirpath, filename), root)[: -len(".py")].replace(os.sep, ".") + module_name = f"konfai.models.python.{rel}" + try: + module = importlib.import_module(module_name) + except Exception: # nosec B112 - a catalog model needing an absent optional dep is not an error + continue + for name, obj in inspect.getmembers(module, inspect.isclass): + if obj.__module__ != module_name or name.startswith("_"): + continue + if obj is network_base or not issubclass(obj, network_base) or inspect.isabstract(obj): + continue + classpath = f"{rel}.{name}" + components.setdefault( + classpath, + Component(name=name, config_reference=classpath, module=module_name, doc=_doc_summary(obj)), + ) + return list(components.values()) + + +def _list_yaml_catalog_models() -> list[Component]: + # The declarative catalog (konfai/models/yaml): each file is referenced as 'default|.yml', + # and its leading comment lines are its documentation. + import konfai.models.yaml as yaml_catalog + + catalog_dir = os.path.dirname(str(yaml_catalog.__file__)) + components = [] + for filename in sorted(os.listdir(catalog_dir)): + if not filename.endswith((".yml", ".yaml")): + continue + doc_lines: list[str] = [] + with open(os.path.join(catalog_dir, filename), encoding="utf-8") as handle: + for line in handle: + stripped = line.strip() + if stripped.startswith("#"): + doc_lines.append(stripped.lstrip("# ").rstrip()) + elif stripped: + break + components.append( + Component( + name=filename.rsplit(".", 1)[0], + config_reference=f"default|{filename}", + module=None, + doc=" ".join(doc_lines).strip() or None, + ) + ) + return components + + +def list_components(kind: str) -> list[Component]: + """Every shipped component of one ``kind``, with the spelling a YAML config references it by. + + ``kind`` is one of :data:`COMPONENT_KINDS` (plural spellings accepted): ``transform``, + ``augmentation``, ``criterion`` (losses and metrics), ``reduction``, ``model`` (the Python + catalog classpaths and the ``default|.yml`` declarative catalog), or ``block`` (the YAML + model builder's registered types). + """ + canonical = normalize_kind(kind) + if canonical == "block": + return _list_blocks() + if canonical == "model": + models = _list_python_models() + _list_yaml_catalog_models() + return sorted(models, key=lambda component: component.config_reference) + return _list_subclasses(*_SUBCLASS_KINDS[canonical]) diff --git a/konfai/utils/model_builder.py b/konfai/utils/model_builder.py index 709161cb..fb502a50 100644 --- a/konfai/utils/model_builder.py +++ b/konfai/utils/model_builder.py @@ -185,6 +185,16 @@ def list_registered_modules() -> list[str]: return sorted(_MODULE_REGISTRY) +def registered_module_types() -> dict[str, ModuleFactory]: + """The builder's module registry (a YAML ``type:`` entry per name), as a copy.""" + return dict(_MODULE_REGISTRY) + + +def registered_object_types() -> dict[str, ObjectFactory]: + """The builder's object registry (a YAML ``$object`` entry per name), as a copy.""" + return dict(_OBJECT_REGISTRY) + + def _lookup_reference(path: str, parameters: dict[str, Any]) -> Any: value: Any = parameters for part in path.split("."): diff --git a/konfai/utils/pretrained.py b/konfai/utils/pretrained.py index effd88f0..33a5a91a 100644 --- a/konfai/utils/pretrained.py +++ b/konfai/utils/pretrained.py @@ -31,12 +31,18 @@ from __future__ import annotations +import math from collections.abc import Callable +from typing import TYPE_CHECKING, Any import torch +from konfai.utils.config import config from konfai.utils.errors import ConfigError +if TYPE_CHECKING: + from konfai.network.network.network import Network + def _parametric_leaves_in_execution_order(model: torch.nn.Module, run: Callable[[], object]) -> list[torch.nn.Module]: """Return the model's weighted leaf modules in the order their forward runs, via forward hooks. @@ -167,3 +173,102 @@ def transfer_weights_by_execution_order( ) target_leaf.load_state_dict(source_state) return len(target_leaves) + + +@config("pretrained_from") +class PretrainedFrom: + """The ``Model.pretrained_from`` config block: seed a model from an external reference checkpoint. + + ``builder`` names the reference class (``monai.networks.nets:UNet``), ``args`` its constructor + arguments, and ``checkpoint`` its trained weights: a raw ``state_dict`` file, or a checkpoint + dict holding one under ``state_dict``. When a fresh TRAIN initialises the model, + :func:`transfer_weights_by_execution_order` fills every tensor from the reference or raises; a + checkpoint load (RESUME, PREDICTION) carries its own weights and is never overridden. The + transfer runs both forwards on a synthetic input shaped from the model's own channels and + spatial rank; ``input_shape`` overrides its spatial extent when the derived one (the model's + patch size, else its downsampling multiple) does not fit the graph. + """ + + def __init__( + self, + checkpoint: str = "", + builder: str = "", + args: dict[str, Any] | None = None, + input_shape: list[int] | None = None, + ) -> None: + self.checkpoint = checkpoint + self.builder = builder + self.args = args + self.input_shape = input_shape + + def _reference(self) -> torch.nn.Module: + if not self.builder or not self.checkpoint: + raise ConfigError( + "Model.pretrained_from requires both 'builder' and 'checkpoint'.", + "builder names the reference class ('monai.networks.nets:UNet'), checkpoint its weights" + " (a state_dict .pt, or a checkpoint dict with a 'state_dict' entry).", + ) + from konfai.utils.utils import get_module + + module, name = get_module(self.builder, "torch.nn") + reference_class = getattr(module, name, None) + if reference_class is None: + raise ConfigError(f"Model.pretrained_from.builder: '{name}' does not exist in '{module.__name__}'.") + try: + reference = reference_class(**(self.args or {})) + except TypeError as error: + raise ConfigError(f"Model.pretrained_from.args do not construct '{self.builder}'.", str(error)) from error + + from konfai.utils.runtime.environment import safe_torch_load + + try: + state = safe_torch_load(self.checkpoint, torch.device("cpu")) + except (OSError, RuntimeError, ValueError) as error: + raise ConfigError( + f"Model.pretrained_from.checkpoint: cannot load '{self.checkpoint}'.", str(error) + ) from error + if isinstance(state, dict) and "state_dict" in state: + state = state["state_dict"] + try: + reference.load_state_dict(state) + except (RuntimeError, TypeError) as error: + raise ConfigError( + f"Model.pretrained_from.checkpoint: '{self.checkpoint}' does not fit the reference '{self.builder}'.", + str(error), + ) from error + return reference.eval() + + def _example_input(self, model: Network) -> torch.Tensor: + # The transfer only needs shapes and execution order, so any input the graph accepts will do. + if self.input_shape is not None: + spatial = [int(size) for size in self.input_shape] + elif ( + model.patch is not None + and model.patch.patch_size is not None + and all(int(size) > 0 for size in model.patch.patch_size) + ): + spatial = [int(size) for size in model.patch.patch_size] + else: + factors = model.downsampling_factor() or [1] * model.dim + if len(factors) != model.dim: + factors = [max(factors)] * model.dim + # The smallest extent of at least 16 the graph accepts: a multiple of the per-axis factor. + spatial = [factor * max(1, math.ceil(16 / factor)) for factor in factors] + return torch.randn(1, model.in_channels, *spatial) + + def seed(self, model: Network) -> int: + """Fill every tensor of ``model`` from the reference, or raise naming the config key.""" + reference = self._reference() + inputs = self._example_input(model) + try: + return transfer_weights_by_execution_order( + target=model, + source=reference, + target_forward=lambda: list(model.named_forward(inputs)), + source_forward=lambda: reference(inputs), + ) + except ConfigError as error: + raise ConfigError( + f"Model.pretrained_from: the reference '{self.builder}' cannot seed this model.", + *(str(message) for message in error.args), + ) from error diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index 88cec634..92ef52cb 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -294,9 +294,32 @@ def test_magnitude_is_the_channel_norm_and_pointwise() -> None: def test_a_config_tree_must_hold_the_workflow_root() -> None: - from konfai.utils.runtime import _materialized_config + from konfai.utils.runtime.environment import _materialized_config with pytest.raises(ConfigError, match="Transformer"): _materialized_config({"Trainer": {}}, "Transformer") path = _materialized_config({"Transformer": {"name": "X"}}, "Transformer") assert path.is_file() + + +# ---------------------------------------------------------------------------- component discovery + + +def test_list_components_names_the_config_vocabulary() -> None: + """The catalog answers with the exact spelling a YAML config references each component by.""" + transforms = {component.name: component for component in api.list_components("transforms")} + assert transforms["Resample"].config_reference == "Resample" and transforms["Resample"].doc + + assert {"Dice", "MAE"} <= {component.name for component in api.list_components("criteria")} + assert "Median" in {component.name for component in api.list_components("reductions")} + assert "Flip" in {component.name for component in api.list_components("augmentations")} + assert "Conv" in {component.name for component in api.list_components("blocks")} + + models = {component.config_reference for component in api.list_components("models")} + assert "default|UNet.yml" in models # the declarative catalog + assert "segmentation.UNet.UNet" in models # the Python catalog, in Model.classpath spelling + + +def test_list_components_refuses_an_unknown_kind() -> None: + with pytest.raises(ConfigError, match="component kind"): + api.list_components("optimizers") diff --git a/tests/unit/test_main_cli.py b/tests/unit/test_main_cli.py index 288a3226..8704c5a8 100644 --- a/tests/unit/test_main_cli.py +++ b/tests/unit/test_main_cli.py @@ -286,3 +286,35 @@ def test_predict_evaluate_expose_tensorboard_param(): params = inspect.signature(fn).parameters assert "tensorboard" in params, f"{fn.__name__} must accept 'tensorboard'" assert "tb" not in params, f"{fn.__name__} must not use the old 'tb' name" + + +@pytest.mark.parametrize( + ("kind", "member"), + [ + ("transforms", "Resample"), + ("augmentations", "Flip"), + ("criteria", "Dice"), + ("reductions", "Median"), + ("models", "default|UNet.yml"), + ("blocks", "Conv"), + ], +) +def test_konfai_list_prints_each_component_family( + monkeypatch: pytest.MonkeyPatch, capsys, kind: str, member: str +) -> None: + """`konfai list ` prints one aligned `name doc` line per component, no run machinery.""" + monkeypatch.setattr(sys, "argv", ["konfai", "list", kind]) + + main_module.main() + + lines = [line for line in capsys.readouterr().out.splitlines() if line.strip()] + assert any(line.split()[0] == member for line in lines) + + +def test_konfai_list_refuses_an_unknown_kind(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(sys, "argv", ["konfai", "list", "optimizers"]) + + with pytest.raises(SystemExit) as exc_info: + main_module.main() + + assert exc_info.value.code == 2 # an argparse choices error, before anything heavy loads diff --git a/tests/unit/test_pretrained_from.py b/tests/unit/test_pretrained_from.py new file mode 100644 index 00000000..87e0530e --- /dev/null +++ b/tests/unit/test_pretrained_from.py @@ -0,0 +1,162 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""The ``Model.pretrained_from`` config key: seed a model from an external reference checkpoint. + +The execution-order bridge (``transfer_weights_by_execution_order``) is reachable from YAML: the +block names a reference class, its constructor arguments and its checkpoint, and a fresh TRAIN +load starts from the transferred weights. A checkpoint's own weights (RESUME, PREDICTION) always +win over the reference, and a non-equivalent reference fails loudly, naming the key. +""" + +from pathlib import Path +from types import SimpleNamespace + +import pytest +import torch +from konfai.network.network import ModelLoader, Network +from konfai.utils.config import apply_config +from konfai.utils.errors import ConfigError + +TINY_MODEL = """ +name: Tiny +network: + in_channels: 1 + dim: 2 +modules: + - name: Conv + type: Conv + args: + dim: 2 + in_channels: 1 + out_channels: 2 + kernel_size: 3 + padding: 1 +""" + +REFERENCE_ARGS = """ + args: + in_channels: 1 + out_channels: 2 + kernel_size: 3 + padding: 1 +""" + + +def _bound_loader(write_config, tmp_path: Path, pretrained_block: str) -> ModelLoader: + """Bind a ModelLoader through the real binder, against the Tiny declarative model.""" + (tmp_path / "Tiny.yml").write_text(TINY_MODEL, encoding="utf-8") + write_config(f"Root:\n Model:\n classpath: Tiny.yml\n{pretrained_block}", name="Config.yml") + + class Root: + def __init__(self, model: ModelLoader = ModelLoader()) -> None: + self.model = model + + return apply_config("Root")(Root)().model + + +def test_pretrained_from_defaults_to_none_when_the_config_is_silent(write_config, tmp_path: Path) -> None: + loader = _bound_loader(write_config, tmp_path, "") + assert loader.pretrained_from is None + assert loader.get_model(train=True, konfai_args="Root.Model").pretrained_source is None + + +@pytest.mark.parametrize("wrap", [False, True], ids=["raw-state-dict", "checkpoint-dict"]) +def test_a_fresh_train_load_starts_from_the_reference_weights(write_config, tmp_path: Path, wrap: bool) -> None: + """The config route end to end: TRAIN's ``load({}, init=True)`` seeds the graph exactly.""" + reference = torch.nn.Conv2d(1, 2, 3, padding=1) + state = {"state_dict": reference.state_dict()} if wrap else reference.state_dict() + torch.save(state, tmp_path / "ref.pt") + loader = _bound_loader( + write_config, + tmp_path, + f" pretrained_from:\n checkpoint: {tmp_path / 'ref.pt'}\n" + f" builder: torch.nn:Conv2d\n{REFERENCE_ARGS}" + " input_shape: [8, 8]\n", + ) + net = loader.get_model(train=True, konfai_args="Root.Model") + assert isinstance(net, Network) and net.pretrained_source is loader.pretrained_from + + net.load({}, init=True) + + assert torch.equal(net["Conv"].weight, reference.weight) + assert torch.equal(net["Conv"].bias, reference.bias) + + +def test_the_example_input_is_derived_from_the_models_own_shape(write_config, tmp_path: Path) -> None: + """Without ``input_shape`` the synthetic input comes from the model's dim/in_channels.""" + reference = torch.nn.Conv2d(1, 2, 3, padding=1) + torch.save(reference.state_dict(), tmp_path / "ref.pt") + loader = _bound_loader( + write_config, + tmp_path, + f" pretrained_from:\n checkpoint: {tmp_path / 'ref.pt'}\n" + f" builder: torch.nn:Conv2d\n{REFERENCE_ARGS}", + ) + net = loader.get_model(train=True, konfai_args="Root.Model") + + example = loader.pretrained_from._example_input(net) + assert example.shape == (1, 1, 16, 16) # batch 1, the model's in_channels, dim-2 spatial + + net.load({}, init=True) + assert torch.equal(net["Conv"].weight, reference.weight) + + +def test_a_mismatched_reference_raises_naming_the_key(write_config, tmp_path: Path) -> None: + """The bridge's strict refusal surfaces as a ConfigError naming ``Model.pretrained_from``.""" + wrong = torch.nn.Conv2d(1, 4, 3, padding=1) # not weight-exact: 4 output channels against 2 + torch.save(wrong.state_dict(), tmp_path / "ref.pt") + loader = _bound_loader( + write_config, + tmp_path, + f" pretrained_from:\n checkpoint: {tmp_path / 'ref.pt'}\n" + " builder: torch.nn:Conv2d\n" + " args:\n" + " in_channels: 1\n" + " out_channels: 4\n" + " kernel_size: 3\n" + " padding: 1\n", + ) + net = loader.get_model(train=True, konfai_args="Root.Model") + + with pytest.raises(ConfigError, match=r"pretrained_from"): + net.load({}, init=True) + + +def test_a_missing_checkpoint_or_builder_is_refused_by_key(write_config, tmp_path: Path) -> None: + loader = _bound_loader(write_config, tmp_path, " pretrained_from:\n builder: torch.nn:Conv2d\n") + net = loader.get_model(train=True, konfai_args="Root.Model") + + with pytest.raises(ConfigError, match=r"pretrained_from requires both"): + net.load({}, init=True) + + +def test_a_checkpoints_own_weights_always_win_over_the_reference(write_config, tmp_path: Path) -> None: + """The seed fires only on a fresh load: a ``Model`` entry (RESUME/PREDICTION) or an EMA copy + (deepcopied from the already-seeded model) never pays or re-runs the transfer.""" + loader = _bound_loader(write_config, tmp_path, "") + net = loader.get_model(train=True, konfai_args="Root.Model") + seeded: list[Network] = [] + net.pretrained_source = SimpleNamespace(seed=seeded.append) + + net.load({"Model": {net.get_name(): net.state_dict()}}, init=True) # a checkpoint load + assert seeded == [] + + net.load({}, init=False, ema=True) # the EMA copy's load + assert seeded == [] + + net.load({}, init=True) # the fresh TRAIN load + assert seeded == [net] diff --git a/tests/unit/test_streamed_read_dispatcher.py b/tests/unit/test_streamed_read_dispatcher.py index 638c6aad..c34dacdc 100644 --- a/tests/unit/test_streamed_read_dispatcher.py +++ b/tests/unit/test_streamed_read_dispatcher.py @@ -38,6 +38,7 @@ from konfai.data.augmentation import Flip as FlipAugmentation from konfai.data.materialize import CaseMaterializer from konfai.data.patching import DatasetManager, DatasetPatch +from konfai.data.patching import sweep as sweep_module from konfai.data.transform import ( Canonical, Clip, @@ -385,7 +386,7 @@ def _one_row_budget(manager: DatasetManager) -> float: production rule rather than restated here.""" segment = (manager.sweep_segments(0) or [])[-1] tile = manager._sweep_shape(segment.landing, segment.plans, 1) - depth = patching._sweep_pipeline_depth() + depth = sweep_module._sweep_pipeline_depth() return float(manager.sweep_block_bytes(segment.landing, segment.channels, segment.plans, tile, depth)) @@ -443,13 +444,20 @@ def test_global_stat_after_float_cast_still_streams_and_matches(build_streaming_ def test_clip_percentile_and_mask_bounds_fall_back_to_whole_volume(build_streaming_manager) -> None: - # A percentile bound needs the whole histogram and a mask reads a second full volume: both - # genuinely require the whole volume, so the contract declares WHOLE_VOLUME and streaming is off. + # A percentile bound needs the whole histogram, mask or not: WHOLE_VOLUME, streaming off. A + # mask with fixed bounds is never read (POINTWISE); masked 'min'/'max' bounds seed themselves + # from the masked disk scan (GLOBAL_STAT, see test_masked_statistics.py). assert ( Clip(min_value="percentile:1", max_value="percentile:99").patch_locality(Attribute()).kind is LocalityKind.WHOLE_VOLUME ) - assert Clip(mask="SEG").patch_locality(Attribute()).kind is LocalityKind.WHOLE_VOLUME + assert Clip(min_value="percentile:1", max_value=99.0, mask="SEG").patch_locality(Attribute()).kind is ( + LocalityKind.WHOLE_VOLUME + ) + assert Clip(mask="SEG").patch_locality(Attribute()).kind is LocalityKind.POINTWISE + assert Clip(min_value="min", max_value="max", mask="SEG").patch_locality(Attribute()).kind is ( + LocalityKind.GLOBAL_STAT + ) volume = np.arange(1 * 8 * 8, dtype=np.float32).reshape(1, 8, 8) manager = build_streaming_manager(volume, [Clip(min_value="percentile:1", max_value="percentile:99")], [4, 4]) assert not manager.can_stream_patch(0) From 508e25db1cfcb8c33510a8e36e7af145e881499e Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 09:17:21 +0200 Subject: [PATCH 22/28] feat(mcp): fine_tune_app returns, proven end to end The one-call fine-tune stays: for an LLM agent, import_app -> config surgery -> run_resume is three error opportunities where one launch was none, and the GUI's suggested flows name it. Restored surgically from the pre-deletion tree (tool, job kind, runner, builder, anti-drift tests, guide/skill/reference texts) with both tiers now documented everywhere: fine_tune_app = one call, import_app + run_resume (weights_only) = full control. Studio maps finetune jobs into the kind vocabulary its panels gate on (it never did before). What never existed before: a real end-to-end test (fastmcp.Client, tiny app, 1 epoch CPU, ~12 s) that waits for the job, opens the produced bundle and asserts at least one weight tensor changed -- the tool is done because its output is verified, not because it returned. --- .claude/skills/konfai-experiments/SKILL.md | 2 +- .../references/tool-reference.md | 14 +- konfai-mcp/README.md | 6 +- konfai-mcp/konfai_mcp/capabilities.py | 5 +- konfai-mcp/konfai_mcp/dataset_inspection.py | 2 +- konfai-mcp/konfai_mcp/experiment_state.py | 5 +- konfai-mcp/konfai_mcp/guide.py | 34 +++- konfai-mcp/konfai_mcp/runner.py | 48 ++++++ konfai-mcp/konfai_mcp/server.py | 74 ++++++++- konfai-mcp/konfai_mcp/server_apps.py | 83 ++++++++- konfai-mcp/konfai_mcp/server_jobs.py | 5 + konfai-mcp/konfai_mcp/workflows.py | 5 +- konfai-mcp/tests/test_experiment_state.py | 2 +- konfai-mcp/tests/test_mcp_server_apps.py | 88 +++++++++- .../tests/test_mcp_server_finetune_e2e.py | 157 ++++++++++++++++++ .../tests/test_mcp_server_refine_loop.py | 9 + .../tests/test_mcp_server_tool_index.py | 1 + konfai-mcp/tests/test_workflow_registry.py | 2 +- studio/konfai_studio/agent.py | 6 +- studio/konfai_studio/jobs.py | 7 +- studio/konfai_studio/workflow.py | 2 +- 21 files changed, 513 insertions(+), 44 deletions(-) create mode 100644 konfai-mcp/tests/test_mcp_server_finetune_e2e.py diff --git a/.claude/skills/konfai-experiments/SKILL.md b/.claude/skills/konfai-experiments/SKILL.md index 356cfff0..c95e987d 100644 --- a/.claude/skills/konfai-experiments/SKILL.md +++ b/.claude/skills/konfai-experiments/SKILL.md @@ -34,7 +34,7 @@ This is the tool order verified by the segmentation and synthesis end-to-end tes the discovery steps only when the dataset and task are already understood. **Route first (cheapest fit wins)** -0. `list_apps` → `describe_app` → `run_app_infer`: when the user wants a RESULT, check whether a published app already solves it BEFORE authoring and training from scratch. `run_app_infer` / `run_app_evaluate` / `run_app_uncertainty` / `run_app_pipeline` run the app AS PUBLISHED (no config editing). `import_app` is the modify-or-fine-tune path: it copies the app into the session so it runs as a normal experiment (`run_prediction`, or `run_resume` with `weights_only=True` to fine-tune from its weights on the user's dataset). `run_resume` (without `weights_only`) continues an interrupted session training. +0. `list_apps` → `describe_app` → `run_app_infer`: when the user wants a RESULT, check whether a published app already solves it BEFORE authoring and training from scratch. `run_app_infer` / `run_app_evaluate` / `run_app_uncertainty` / `run_app_pipeline` run the app AS PUBLISHED (no config editing), and `fine_tune_app` is the one-call path to adapt it to the user's dataset. `import_app` is the full-control tier: it copies the app into the session so it runs as a normal experiment (`run_prediction`, or `run_resume` with `weights_only=True` to fine-tune with custom losses or config surgery). `run_resume` (without `weights_only`) continues an interrupted session training. **Discover (dataset-driven)** 1. `browse_dataset` → `inspect_dataset`: choose the real dataset root, see groups + sampled stats (`include_stats=False` for a fast structural peek; `groups=[...]` when you need intensity ranges for normalization). diff --git a/.claude/skills/konfai-experiments/references/tool-reference.md b/.claude/skills/konfai-experiments/references/tool-reference.md index 2d934d4d..5431de85 100644 --- a/.claude/skills/konfai-experiments/references/tool-reference.md +++ b/.claude/skills/konfai-experiments/references/tool-reference.md @@ -2,7 +2,7 @@ > GENERATED from the registry by `konfai-mcp/scripts/generate_tool_reference.py`: do not edit by hand. -61 tools, 4 prompts, 23 resources. The live equivalent is the `guide://tool-index` resource. +62 tools, 4 prompts, 23 resources. The live equivalent is the `guide://tool-index` resource. ## Tools @@ -36,7 +36,7 @@ Use when you want to remove the current session workspace. This deletes the work ### `describe_app` -Use to read one app's manifest so you can decide whether it matches the user's task: the app's free-text description is the primary signal, with the input/output modality confirming the fit. This resolves a single app and returns its app.json: display name, description, input and output modality (with volume types), inference/evaluation/uncertainty capabilities, checkpoints, and segmentation terminology. It is metadata-only and SAFE: it does not import the app's model code and does not pip-install its requirements (those happen only later, behind an explicit trust gate). Outputs: display_name, description, inputs, outputs, capabilities, checkpoints, terminology, next_actions. Next: run_app_infer / list_app_parameters / import_app when it fits (next_actions reflect the app's capabilities), or design_config_strategy if no app fits the task. +Use to read one app's manifest so you can decide whether it matches the user's task: the app's free-text description is the primary signal, with the input/output modality confirming the fit. This resolves a single app and returns its app.json: display name, description, input and output modality (with volume types), inference/evaluation/uncertainty capabilities, checkpoints, and segmentation terminology. It is metadata-only and SAFE: it does not import the app's model code and does not pip-install its requirements (those happen only later, behind an explicit trust gate). Outputs: display_name, description, inputs, outputs, capabilities, checkpoints, terminology, next_actions. Next: run_app_infer / list_app_parameters / import_app / fine_tune_app when it fits (next_actions reflect the app's capabilities), or design_config_strategy if no app fits the task. ### `describe_config_schema` @@ -70,6 +70,10 @@ Use to SAVE a HuggingFace / remote-cached app (optionally with tuned parameters) Use to EXPORT the full reproducibility record of one run: the job manifest (command, devices, environment snapshot with package versions and GPUs), the launch-time config snapshots' CONTENT, the post-run resolved config, every split's metrics, and a log tail: a Methods-section-grade record in one payload. It does not rerun anything. Caveat: resolved_config is read from the LIVE session config, which may have been rewritten since the run: the launch-time truth is config_snapshots. Outputs: job, manifest, config_snapshots (text), resolved_config, metrics per split, log_tail. Next: compare_runs or read_training_curves. +### `fine_tune_app` + +Use to TRAIN by starting from a published app instead of a blank slate: fine-tune an existing app's checkpoint(s) on the user's dataset, WITHOUT authoring or editing a config. This is the middle option between run_app_infer (use as-is, no training) and design_config_strategy (author a config and train from scratch). The produced training runs like any app fine-tune; for full control (custom losses, config surgery) use import_app + run_resume(weights_only=True) instead. It launches a tracked training job and writes a resolvable app bundle (config + code + fine-tuned checkpoint) to the output directory, which you can then run with run_app_infer. TRUST GATE: resolving the app imports its Python code and pip-installs its requirements, so pass allow_untrusted_code=True to confirm you trust the source. Local and HuggingFace apps only. It does not author a config or adapt the dataset layout for you. Training knobs are first-class parameters (epochs, it_validation, lr, batch_size); set_parameters is for the app's MODEL tunables (bare names) or any config key by its full dotted path. Outputs: a job payload (status, resources, next_actions) plus the bundle output path. Next: wait_for_job, then run_app_infer on the produced bundle (then run_app_evaluate to score and rank this fine-tune against other training trials via leaderboard / compare_runs). + ### `generate_folds` Use to SPLIT a dataset into K cross-validation folds: writes one case-list file per fold into the session workspace and returns the exact subset stanzas to paste into the configs. KonfAI's Dataset.subset accepts a case-list file ('folds/fold_0.txt' keeps those cases) and its '~file' negation (trains on every OTHER fold). Outputs: folds {fold_i: {cases, file, train_subset, eval_subset}}, how_to_use, next_actions. Next: write per-fold configs (distinct train_name each), then run_batch. @@ -84,7 +88,7 @@ Use to read the FULL evaluation metrics (per-case values + aggregates) of ONE na ### `import_app` -Use to RUN a published KonfAI app as a NORMAL experiment in this session. Prefer run_app_* when the app is used exactly as published; import_app is the tier for everything else: editing the config, fine-tuning (run_resume with weights_only=True), or wiring the app into a larger experiment. It copies the app's config(s), custom code, and .pt checkpoints into the session root and pip-installs its requirements, so predict / fine-tune / evaluate then go through the ordinary run_prediction / run_resume / run_evaluation tools (no app-specific wrapper, no extra sub-folder). The copied checkpoints are returned so run_prediction can pass them as models, and run_resume(weights_only=True) warm-starts a fine-tune from them. TRUST GATE: copying+running the app's Python code and installing its requirements is the trust boundary, so you MUST pass allow_untrusted_code=True to confirm you trust the source. Local/HuggingFace apps only: a remote server keeps its code remote and cannot be imported (drive a remote app with konfai-apps directly). Outputs: imported_to, files, checkpoints, configs, next_actions. Next: run_prediction (pass checkpoints as models) / run_resume (fine-tune) / run_evaluation. +Use to RUN a published KonfAI app as a NORMAL experiment in this session. Prefer run_app_* when the app is used exactly as published; import_app is the full-control tier: editing the config, fine-tuning with custom losses or config surgery (run_resume with weights_only=True; fine_tune_app is the one-call path when the app trains as published), or wiring the app into a larger experiment. It copies the app's config(s), custom code, and .pt checkpoints into the session root and pip-installs its requirements, so predict / fine-tune / evaluate then go through the ordinary run_prediction / run_resume / run_evaluation tools (no app-specific wrapper, no extra sub-folder). The copied checkpoints are returned so run_prediction can pass them as models, and run_resume(weights_only=True) warm-starts a fine-tune from them. TRUST GATE: copying+running the app's Python code and installing its requirements is the trust boundary, so you MUST pass allow_untrusted_code=True to confirm you trust the source. Local/HuggingFace apps only: a remote server keeps its code remote and cannot be imported (drive a remote app with konfai-apps directly). Outputs: imported_to, files, checkpoints, configs, next_actions. Next: run_prediction (pass checkpoints as models) / run_resume (fine-tune) / run_evaluation. ### `import_experiment` @@ -124,7 +128,7 @@ Use when you need the current job registry state. This lists jobs for the curren ### `package_app_from_session` -Use to PACKAGE a model trained in the current session (the train-from-scratch branch) into a resolvable KonfAI app bundle, so a from-scratch run can also finish as a reusable app. It gathers the session's checkpoints and a config, writes an app.json from the metadata you give, and assembles a bundle (app.json + config + checkpoint + optional Model.py/requirements) that describe_app / run_app_infer / import_app can consume. It does not train, and it does not upload the bundle anywhere. Outputs: bundle_path, the packaged checkpoints/configs, next_actions (and onnx path if requested). Next: describe_app or run_app_infer on the produced bundle. +Use to PACKAGE a model trained in the current session (the train-from-scratch branch) into a resolvable KonfAI app bundle: the same endpoint fine_tune_app produces, so a from-scratch run can also finish as a reusable app. It gathers the session's checkpoints and a config, writes an app.json from the metadata you give, and assembles a bundle (app.json + config + checkpoint + optional Model.py/requirements) that describe_app / run_app_infer / import_app can consume. It does not train, and it does not upload the bundle anywhere. Outputs: bundle_path, the packaged checkpoints/configs, next_actions (and onnx path if requested). Next: describe_app or run_app_infer on the produced bundle. ### `plan_transform` @@ -208,7 +212,7 @@ Use after prediction config review/validation and when a checkpoint exists. This ### `run_resume` -Use to RESUME an interrupted or crashed training run from a checkpoint: model, optimizer, scheduler, and epoch/iteration counters are restored (KonfAI's RESUME command). Set weights_only=True instead to WARM-START a fine-tune from an imported app: load only the checkpoint's model weights and restart epoch/optimizer from scratch (import_app + run_resume(weights_only=True) is THE fine-tune path for published apps). This launches a resumed training job from the current session Config.yml. It does not pick between runs: by default it resumes from the newest checkpoint of the configured run (falling back to the newest in the session), avoiding cross-run contamination. It trains up to the LIVE config's epochs: if the run already completed them, raise epochs in Config.yml first or the resume finishes immediately without adding checkpoints. Outputs: job payload with resources and next_actions; or, when a prerequisite is missing (dataset path, checkpoint), a blocker payload {ok, blocked, error, missing_paths, next_actions} with no job_id/status. Next: wait_for_job or read_live_metrics. +Use to RESUME an interrupted or crashed training run from a checkpoint: model, optimizer, scheduler, and epoch/iteration counters are restored (KonfAI's RESUME command). Set weights_only=True instead to WARM-START a fine-tune from an imported app: load only the checkpoint's model weights and restart epoch/optimizer from scratch; prefer fine_tune_app when the app is used as published, since it needs no config editing. This launches a resumed training job from the current session Config.yml. It does not pick between runs: by default it resumes from the newest checkpoint of the configured run (falling back to the newest in the session), avoiding cross-run contamination. It trains up to the LIVE config's epochs: if the run already completed them, raise epochs in Config.yml first or the resume finishes immediately without adding checkpoints. Outputs: job payload with resources and next_actions; or, when a prerequisite is missing (dataset path, checkpoint), a blocker payload {ok, blocked, error, missing_paths, next_actions} with no job_id/status. Next: wait_for_job or read_live_metrics. ### `run_train` diff --git a/konfai-mcp/README.md b/konfai-mcp/README.md index f752098e..a404bb5b 100644 --- a/konfai-mcp/README.md +++ b/konfai-mcp/README.md @@ -241,9 +241,9 @@ The `solve_task` prompt frames the entry decision as a three-way fork: app (config + code + checkpoints) into the session so it runs as a normal experiment through `run_prediction` / `run_resume` / `run_evaluation` 2. **Fine-tune an app**: start training from a published model rather than a - blank slate: `import_app` + `run_resume(weights_only=True)` warm-starts a - training on the user's dataset, and `package_app_from_session` turns the - result into a resolvable app bundle. + blank slate: `fine_tune_app` adapts it to the user's dataset in one call and + writes a resolvable app bundle; `import_app` + `run_resume(weights_only=True)` + is the full-control tier (custom losses, config surgery). 3. **Train from scratch**: author a config (the loop above) and, when done, `package_app_from_session` turns the trained model into a bundle too. diff --git a/konfai-mcp/konfai_mcp/capabilities.py b/konfai-mcp/konfai_mcp/capabilities.py index 1f5222a3..f147d6d4 100644 --- a/konfai-mcp/konfai_mcp/capabilities.py +++ b/konfai-mcp/konfai_mcp/capabilities.py @@ -76,8 +76,9 @@ def describe_konfai_capabilities() -> dict[str, Any]: "use an app as-is, else fine-tune one, else train from scratch.", "use_dont_train": "list_apps -> describe_app -> list_app_parameters -> run_app_infer / run_app_pipeline " "(runs the app as published); import_app copies it into the session when it must be MODIFIED first", - "fine_tune": "import_app -> run_resume(weights_only=True): a weights-only warm start on the user's " - "dataset; package_app_from_session then turns the result into a runnable bundle", + "fine_tune": "fine_tune_app (one call: weights-only warm start on the user's dataset -> runnable " + "bundle), or import_app -> run_resume(weights_only=True) for full control (custom losses, config " + "surgery)", "resume": "run_resume (true RESUME of an interrupted session training: optimizer/epoch restored)", "package": "package_app_from_session / export_app (turn a trained session or tuned app into a bundle)", }, diff --git a/konfai-mcp/konfai_mcp/dataset_inspection.py b/konfai-mcp/konfai_mcp/dataset_inspection.py index bd1a250c..76ef8030 100644 --- a/konfai-mcp/konfai_mcp/dataset_inspection.py +++ b/konfai-mcp/konfai_mcp/dataset_inspection.py @@ -472,7 +472,7 @@ def _infer_dataset_structure_payload(self, dataset_dir: Path, *, discover_candid # as is, fine-tune a close one, or train from scratch. Only when nothing was found does the next # step remain "locate the dataset". payload["next_actions"] = ( - ["list_apps", "run_app_infer", "import_app", "run_train", "design_config_strategy"] + ["list_apps", "run_app_infer", "fine_tune_app", "run_train", "design_config_strategy"] if payload["groups"] else ["browse_dataset", "inspect_dataset", "design_config_strategy", "initialize_session"] ) diff --git a/konfai-mcp/konfai_mcp/experiment_state.py b/konfai-mcp/konfai_mcp/experiment_state.py index d6f23af4..039f5504 100644 --- a/konfai-mcp/konfai_mcp/experiment_state.py +++ b/konfai-mcp/konfai_mcp/experiment_state.py @@ -101,8 +101,8 @@ # are candidates, not a script. STAGE_ACTIONS: dict[str, list[str]] = { "dataset_inspection": ["inspect_dataset", "browse_dataset", "design_config_strategy", "initialize_session"], - "action_selection": ["list_apps", "import_app", "design_config_strategy", "run_train"], - "app_selection": ["describe_app", "run_app_infer", "import_app", "list_app_parameters"], + "action_selection": ["list_apps", "fine_tune_app", "design_config_strategy", "run_train"], + "app_selection": ["describe_app", "run_app_infer", "fine_tune_app", "list_app_parameters"], "configuration": ["validate_config_semantics", "review_config_semantics", "run_train", "write_workflow_config"], "running": ["wait_for_job", "read_live_metrics", "get_job_status", "cancel_job"], "failed": ["read_job_log", "validate_config_semantics", "get_job_status"], @@ -252,6 +252,7 @@ def diagnose(text: str, *, status: str = "error") -> Diagnosis: # A finished job of this kind leaves the experiment at this stage: the successor step, not "done". _JOB_KIND_STAGE: dict[str, str] = { "train": "checkpoint_selection", + "finetune": "checkpoint_selection", "prediction": "prediction", "infer": "prediction", "evaluation": "evaluation", diff --git a/konfai-mcp/konfai_mcp/guide.py b/konfai-mcp/konfai_mcp/guide.py index 2a2706dd..6a651025 100644 --- a/konfai-mcp/konfai_mcp/guide.py +++ b/konfai-mcp/konfai_mcp/guide.py @@ -147,7 +147,7 @@ "It is metadata-only and SAFE: it does not import the app's model code and does not pip-install its " "requirements (those happen only later, behind an explicit trust gate). " "Outputs: display_name, description, inputs, outputs, capabilities, checkpoints, terminology, next_actions. " - "Next: run_app_infer / list_app_parameters / import_app when it fits (next_actions reflect " + "Next: run_app_infer / list_app_parameters / import_app / fine_tune_app when it fits (next_actions reflect " "the app's capabilities), or design_config_strategy if no app fits the task." ), "list_app_parameters": ( @@ -170,8 +170,9 @@ ), "import_app": ( "Use to RUN a published KonfAI app as a NORMAL experiment in this session. Prefer run_app_* when the app " - "is used exactly as published; import_app is the tier for everything else: editing the config, fine-tuning " - "(run_resume with weights_only=True), or wiring the app into a larger experiment. " + "is used exactly as published; import_app is the full-control tier: editing the config, fine-tuning with " + "custom losses or config surgery (run_resume with weights_only=True; fine_tune_app is the one-call path " + "when the app trains as published), or wiring the app into a larger experiment. " "It copies the app's config(s), custom code, and .pt checkpoints into the session root " "and pip-installs its requirements, so predict / fine-tune / evaluate then go through the ordinary " "run_prediction / run_resume / run_evaluation tools (no app-specific wrapper, no extra sub-folder). The " @@ -235,9 +236,27 @@ "allow_untrusted_code=True). Local and HuggingFace apps only. " "Outputs: a job payload plus the output directory. Next: wait_for_job, then inspect the output subdirectories." ), + "fine_tune_app": ( + "Use to TRAIN by starting from a published app instead of a blank slate: fine-tune an existing app's " + "checkpoint(s) on the user's dataset, WITHOUT authoring or editing a config. This is the middle option " + "between run_app_infer (use as-is, no training) and design_config_strategy (author a config and train from " + "scratch). The produced training runs like any app fine-tune; for full control (custom losses, config " + "surgery) use import_app + run_resume(weights_only=True) instead. It launches a tracked training job and " + "writes a resolvable app " + "bundle (config + code + fine-tuned checkpoint) to the output directory, which you can then run with " + "run_app_infer. " + "TRUST GATE: resolving the app imports its Python code and pip-installs its requirements, so pass " + "allow_untrusted_code=True to confirm you trust the source. Local and HuggingFace apps only. " + "It does not author a config or adapt the dataset layout for you. " + "Training knobs are first-class parameters (epochs, it_validation, lr, batch_size); set_parameters is for " + "the app's MODEL tunables (bare names) or any config key by its full dotted path. " + "Outputs: a job payload (status, resources, next_actions) plus the bundle output path. " + "Next: wait_for_job, then run_app_infer on the produced bundle (then run_app_evaluate to score and rank " + "this fine-tune against other training trials via leaderboard / compare_runs)." + ), "package_app_from_session": ( "Use to PACKAGE a model trained in the current session (the train-from-scratch branch) into a resolvable " - "KonfAI app bundle, so a from-scratch run can also finish as a " + "KonfAI app bundle: the same endpoint fine_tune_app produces, so a from-scratch run can also finish as a " "reusable app. It gathers the session's checkpoints " "and a config, writes an app.json from the metadata you give, and assembles a bundle (app.json + config + " "checkpoint + optional Model.py/requirements) that describe_app / run_app_infer / import_app can consume. " @@ -455,7 +474,7 @@ "Use to RESUME an interrupted or crashed training run from a checkpoint: model, optimizer, scheduler, and " "epoch/iteration counters are restored (KonfAI's RESUME command). Set weights_only=True instead to WARM-START " "a fine-tune from an imported app: load only the checkpoint's model weights and restart epoch/optimizer " - "from scratch (import_app + run_resume(weights_only=True) is THE fine-tune path for published apps). " + "from scratch; prefer fine_tune_app when the app is used as published, since it needs no config editing. " "This launches a resumed training job from the current session Config.yml. " "It does not pick between runs: by default it resumes from the newest checkpoint of the configured run " "(falling back to the newest in the session), avoiding cross-run contamination. " @@ -578,9 +597,8 @@ "candidate. Judge fit from the app's own description first, confirmed by its declared " "inputs/outputs. If one clearly does the job, run it with run_app_infer (or run_app_pipeline to " "also score it): done. Use import_app instead only when the app must be MODIFIED before running.\n" - "2. FINE-TUNE FROM AN APP. If no app is usable as-is but one is a close starting point, " - "import_app it into the session and train from its weights with run_resume(weights_only=True) " - "on the user's dataset; package_app_from_session can then turn the result into a bundle.\n" + "2. FINE-TUNE FROM AN APP. If no app is usable as-is but one is a close starting point, train " + "from it with fine_tune_app on the user's dataset, producing a bundle you can then run.\n" "3. TRAIN FROM A BLANK SLATE. If no app is a useful starting point, author a config from scratch " "via design_config_strategy and the train loop.\n\n" "Prefer the earliest option that truly fits: do not train when an app already solves it, and do " diff --git a/konfai-mcp/konfai_mcp/runner.py b/konfai-mcp/konfai_mcp/runner.py index b90bd319..59edd2ca 100644 --- a/konfai-mcp/konfai_mcp/runner.py +++ b/konfai-mcp/konfai_mcp/runner.py @@ -411,6 +411,54 @@ def _groups(value: list[list[str]]) -> list[list[Path]]: getattr(KonfAIApp(ref, download=True, force_update=force_update), action)(**call) +def run_finetune_api( + *, + ref: str, + dataset: str, + output: str, + name: str = "Finetune", + epochs: int = 10, + it_validation: int = 1000, + models: list[str] | None = None, + lr: float | None = None, + batch_size: int | None = None, + config_overrides: list[str] | None = None, + gpu: list[int] | None = None, + cpu: int | None = None, + config_file: str = "Config.yml", + force_update: bool = False, + quiet: bool = False, + cwd: str | None = None, +) -> None: + """Child entrypoint that fine-tunes a KonfAI app on the user's dataset, producing a bundle. + + Resolving the app imports its Python code and pip-installs its requirements (gated in the parent + tool). Local and HuggingFace apps only. + """ + with _runtime_context(cwd=Path(cwd).resolve() if cwd is not None else None): + _ensure_local_imports() + from konfai_apps.app import KonfAIApp + + app = KonfAIApp(ref, download=True, force_update=force_update) + common: dict[str, Any] = { + "dataset": Path(dataset).resolve(), + "output": Path(output).resolve(), + "name": name, + "epochs": epochs, + "it_validation": it_validation, + "models": models or [], + "lr": lr, + "batch_size": batch_size, + "config_file": config_file, + "quiet": quiet, + } + if gpu is not None: + common["gpu"] = gpu + if cpu is not None: + common["cpu"] = cpu + app.fine_tune(**common, config_overrides=config_overrides) + + def app_parameters_api(*, ref: str, force_update: bool = False) -> dict[str, Any]: """Child entrypoint that reads an app's tunable parameters (``{values, constraints}``). diff --git a/konfai-mcp/konfai_mcp/server.py b/konfai-mcp/konfai_mcp/server.py index 18ec83c4..f0801a10 100644 --- a/konfai-mcp/konfai_mcp/server.py +++ b/konfai-mcp/konfai_mcp/server.py @@ -477,17 +477,17 @@ def _config_overrides(set_parameters: dict[str, Any] | None) -> list[str] | None def _launch_app_job(spec: dict[str, Any]) -> dict[str, Any]: - """Launch an app job (inference, evaluation, uncertainty, pipeline) from an AppService + """Launch an app job (inference, evaluation, uncertainty, pipeline, fine-tune) from an AppService spec via the shared job registry. Unlike workflow jobs, an app job has no session YAML: it auto-creates the session workspace, tracks the run under the spec's kind, and carries its own runner target and kwargs. """ - kind = cast(JobKind, spec.get("kind", "infer")) # an app kind: infer / evaluate / uncertainty / pipeline + kind = cast(JobKind, spec.get("kind", "infer")) # an app kind: infer / evaluate / uncertainty / pipeline / finetune workspace = WORKSPACE_LAYOUT.ensure_session_workspace() WORKSPACE_LAYOUT.jobs_dir().mkdir(parents=True, exist_ok=True) kwargs = dict(spec["kwargs"]) - # config_overrides live directly in kwargs (infer) or nested under extra (pipeline). Recording + # config_overrides live directly in kwargs (infer / finetune) or nested under extra (pipeline). Recording # them links this trial's tuned parameters to the score it produces and gates the refine next_actions. set_parameters = kwargs.get("config_overrides") or (kwargs.get("extra") or {}).get("config_overrides") job = JOB_REGISTRY.launch( @@ -1573,6 +1573,74 @@ def run_app_pipeline( ) +@mcp.tool(description=(TOOL_DESCRIPTIONS["fine_tune_app"])) +def fine_tune_app( + ref: Annotated[str, Field(description=_APP_REF_DESC)], + dataset: Annotated[str, Field(description="KonfAI-style dataset directory to fine-tune on (must exist).")], + output: Annotated[ + str | None, + Field( + description="Destination for the produced app bundle (default: a unique dir under the session workspace AppBundles/)." + ), + ] = None, + name: Annotated[str, Field(description="Run name of the fine-tune training (default 'Finetune').")] = "Finetune", + epochs: Annotated[int, Field(description="Number of training epochs (must be > 0; default 10).")] = 10, + it_validation: Annotated[ + int, Field(description="Iterations between validation/checkpoint steps (KonfAI it_validation; default 1000).") + ] = 1000, + models: Annotated[ + list[str] | None, + Field(description="Which app checkpoints to fine-tune (default: the app's first advertised checkpoint)."), + ] = None, + lr: Annotated[ + float | None, Field(description="Learning-rate override; omit to keep the app config's value.") + ] = None, + batch_size: Annotated[ + int | None, + Field( + description="Training batch-size override (written to Trainer.Dataset.batch_size); " + "omit to keep the app config's value." + ), + ] = None, + set_parameters: Annotated[ + dict[str, Any] | None, + Field( + description="NAME->VALUE overrides baked into the training config before fine-tuning. A bare NAME " + "is a model parameter (see list_app_parameters, e.g. {'iterations': 300}); any other config key " + "needs its full dotted path from the config root (e.g. {'Trainer.Dataset.num_workers': 2}). " + "For batch size, prefer the batch_size parameter." + ), + ] = None, + gpu: Annotated[list[int] | None, Field(description=_APP_GPU_DESC)] = None, + cpu: Annotated[int | None, Field(description=_APP_CPU_DESC)] = None, + config_file: Annotated[ + str, Field(description="Which train config of the app to use (default 'Config.yml').") + ] = "Config.yml", + allow_untrusted_code: Annotated[bool, Field(description=_APP_TRUST_DESC)] = False, + force_update: Annotated[bool, Field(description=_APP_FORCE_UPDATE_DESC)] = False, +) -> dict[str, Any]: + """Fine-tune a published KonfAI app on the user's dataset and produce a resolvable app bundle.""" + return _launch_app_job( + APP_SERVICE.prepare_finetune( + ref=ref, + dataset=dataset, + output=output, + name=name, + epochs=epochs, + it_validation=it_validation, + models=models, + lr=lr, + batch_size=batch_size, + config_overrides=_config_overrides(set_parameters), + gpu=gpu, + cpu=cpu, + config_file=config_file, + allow_untrusted_code=allow_untrusted_code, + force_update=force_update, + ) + ) + + @mcp.tool(description=(TOOL_DESCRIPTIONS["register_app_source"])) def register_app_source( ref: Annotated[ diff --git a/konfai-mcp/konfai_mcp/server_apps.py b/konfai-mcp/konfai_mcp/server_apps.py index 2900882b..1ec0d5d7 100644 --- a/konfai-mcp/konfai_mcp/server_apps.py +++ b/konfai-mcp/konfai_mcp/server_apps.py @@ -288,7 +288,8 @@ def describe_app(self, ref: str, force_update: bool = False) -> dict[str, Any]: # Route by what the app can actually do instead of dead-ending on describe/design. The run_app_* # tools run the app AS PUBLISHED; import_app is offered beside them for the case where the app has - # to be modified first, and fine-tuning goes through import_app + run_resume(weights_only). + # to be modified first. fine_tune_app is only offered when the app ships a train config to + # warm-start from, so an inference-only bundle never routes the agent to a tool it cannot use. source = _source_of(info) next_actions: list[str] = [] if not inference: @@ -303,6 +304,8 @@ def describe_app(self, ref: str, force_update: bool = False) -> dict[str, Any]: next_actions.append("run_app_evaluate") if uncertainty: next_actions.append("run_app_uncertainty") + if finetunable: + next_actions.append("fine_tune_app") payload: dict[str, Any] = { "ref": ref, @@ -801,6 +804,77 @@ def prepare_pipeline( extra=extra, ) + def prepare_finetune( + self, + ref: str, + dataset: str, + output: str | None = None, + name: str = "Finetune", + epochs: int = 10, + it_validation: int = 1000, + models: list[str] | None = None, + lr: float | None = None, + batch_size: int | None = None, + config_overrides: list[str] | None = None, + gpu: list[int] | None = None, + cpu: int | None = None, + config_file: str = "Config.yml", + allow_untrusted_code: bool = False, + force_update: bool = False, + ) -> dict[str, Any]: + """Validate a fine-tune request and build the job spec for ``runner.run_finetune_api``. + + Fine-tuning starts training from an existing app's checkpoint(s) on the user's dataset and + produces a resolvable app bundle in ``output``. Same trust gate as inference: resolving the app + imports its code and pip-installs its requirements. + """ + dataset_path = Path(dataset).expanduser().resolve() + if not dataset_path.is_dir(): + raise ValueError(f"dataset must be an existing directory: {dataset}") + if epochs <= 0: + raise ValueError("epochs must be a positive integer.") + if batch_size is not None and batch_size <= 0: + raise ValueError("batch_size must be a positive integer.") + self._require_local_app(ref, "Fine-tuning", allow_untrusted_code) + + if cpu is not None and gpu is None: + gpu = [] + + # First-class knobs join the label the same way --set overrides do, so two fine-tunes differing + # only by batch size still read apart on the leaderboard. + labelled_params = ([f"batch_size={batch_size}"] if batch_size is not None else []) + (config_overrides or []) + label = self.workspace_layout.sanitize_name( + f"finetune_{self._app_label(ref)}{self._param_label_suffix(labelled_params)}" + ) + resolved_output = ( + str(Path(output).expanduser().resolve()) if output else self._default_output("AppBundles", label) + ) + + kwargs: dict[str, Any] = { + "ref": ref, + "dataset": str(dataset_path), + "output": resolved_output, + "name": name, + "epochs": epochs, + "it_validation": it_validation, + "models": models or [], + "lr": lr, + "batch_size": batch_size, + "config_overrides": config_overrides, + "gpu": gpu, + "cpu": cpu, + "config_file": config_file, + "force_update": force_update, + } + return { + "kind": "finetune", + "run_name": label, + "target": "konfai_mcp.runner:run_finetune_api", + "command": ["konfai_mcp.runner:run_finetune_api", ref, "->", resolved_output], + "kwargs": kwargs, + "output": resolved_output, + } + #: packaging ------------------------------------------------------------------------------ def package_from_session( @@ -890,7 +964,7 @@ def package_from_session( if any(Path(path).name == "Config.yml" for path in resolved_configs): result["warnings"] = [ "Config.yml is copied from the session as-is: make sure it describes the SAME architecture " - "as the packaged checkpoints (a fine-tune via import_app + run_resume will train with it)." + "as the packaged checkpoints (fine_tune_app will train with it)." ] if onnx: # The ONNX export instantiates and traces the packaged model: it imports the bundle's @@ -941,9 +1015,8 @@ def _resolve_package_checkpoints(self, checkpoints: list[str] | None) -> list[st def _resolve_package_configs(self, configs: list[str] | None) -> list[str]: if configs is None: - # Bundle BOTH the prediction config (to run) and the train config (so a fine-tune via - # import_app + run_resume can warm-start from the bundle) when present: a - # Prediction.yml-only bundle cannot be fine-tuned. + # Bundle BOTH the prediction config (to run) and the train config (so fine_tune_app can warm-start + # from the bundle) when present: a Prediction.yml-only bundle cannot be fine-tuned. prediction = self.workspace_layout.config_path("prediction") train = self.workspace_layout.config_path("train") configs = [str(path) for path in (prediction, train) if path.exists()] diff --git a/konfai-mcp/konfai_mcp/server_jobs.py b/konfai-mcp/konfai_mcp/server_jobs.py index 4641ad69..a6a1a23b 100644 --- a/konfai-mcp/konfai_mcp/server_jobs.py +++ b/konfai-mcp/konfai_mcp/server_jobs.py @@ -525,6 +525,11 @@ def payload(self, job: Job, isoformat: Callable[[float | None], str | None]) -> elif job.kind == "infer" and job.set_parameters: # Already tuning inference parameters: help close the loop toward a score. next_actions.extend(["run_app_evaluate", "run_app_pipeline", "compare_runs"]) + elif job.kind == "finetune": + # A fine-tune produces a bundle but keeps its training metrics out of it, so there is + # nothing to rank yet: use the bundle, then evaluate it; that evaluation lands where + # leaderboard/compare_runs can rank this fine-tune against other training trials. + next_actions.extend(["run_app_infer", "run_app_evaluate"]) else: # A finished workflow job is a step, not the end: point at the step that actually follows it # (a trained model is worth nothing until it has predicted, a prediction until it is scored), diff --git a/konfai-mcp/konfai_mcp/workflows.py b/konfai-mcp/konfai_mcp/workflows.py index 2e0a965d..bcad8b2f 100644 --- a/konfai-mcp/konfai_mcp/workflows.py +++ b/konfai-mcp/konfai_mcp/workflows.py @@ -89,6 +89,7 @@ class WorkflowSpec: # konfai-apps job kinds (no session YAML of their own) -> the tool that relaunches them. APP_JOB_RETRY_TOOLS: dict[str, str] = { "infer": "run_app_infer", + "finetune": "fine_tune_app", "evaluate": "run_app_evaluate", "uncertainty": "run_app_uncertainty", "pipeline": "run_app_pipeline", @@ -126,4 +127,6 @@ def workflow_choice_description(action: str) -> str: # Static mirrors of the table for tool signatures; pinned to it by the drift test. WorkflowKind = Literal["train", "prediction", "evaluation", "transform"] -JobKind = Literal["train", "prediction", "evaluation", "transform", "infer", "evaluate", "uncertainty", "pipeline"] +JobKind = Literal[ + "train", "prediction", "evaluation", "transform", "infer", "finetune", "evaluate", "uncertainty", "pipeline" +] diff --git a/konfai-mcp/tests/test_experiment_state.py b/konfai-mcp/tests/test_experiment_state.py index 7e5126c3..adedf7d8 100644 --- a/konfai-mcp/tests/test_experiment_state.py +++ b/konfai-mcp/tests/test_experiment_state.py @@ -308,7 +308,7 @@ def test_stage_derivation_is_total() -> None: """Every derived stage is one the focus and action tables know about.""" from konfai_mcp.experiment_state import Facts - for kind in ("train", "prediction", "evaluation", "infer", "pipeline", "uncertainty", ""): + for kind in ("train", "prediction", "evaluation", "infer", "finetune", "pipeline", "uncertainty", ""): for status in ("queued", "running", "done", "error", "killed", ""): facts = Facts(job_kind=kind, job_status=status, checkpoints=["a"], predictions=["b"], metrics=["c"]) assert derive_stage(facts) in STAGES diff --git a/konfai-mcp/tests/test_mcp_server_apps.py b/konfai-mcp/tests/test_mcp_server_apps.py index 345107a2..841f43c8 100644 --- a/konfai-mcp/tests/test_mcp_server_apps.py +++ b/konfai-mcp/tests/test_mcp_server_apps.py @@ -82,11 +82,12 @@ def test_describe_app_reads_local_manifest(tmp_path: Path) -> None: assert payload["checkpoints_available"] == ["tiny.pt"] assert payload["patch_size"] == [1, 64, 64] assert payload["task"] == "synthesis" - # The bundle ships a Config.yml, so it is finetunable (via import_app + run_resume). + # The bundle ships a Config.yml, so it is finetunable and offers fine_tune_app. assert payload["finetunable"] is True # An inference-capable app routes forward to the run/tune tools instead of dead-ending. assert payload["next_actions"][0] == "run_app_infer" - assert "import_app" in payload["next_actions"] # the modify/fine-tune path stays offered + assert "fine_tune_app" in payload["next_actions"] + assert "import_app" in payload["next_actions"] # the modify-then-run path stays offered assert "run_app_evaluate" not in payload["next_actions"] @@ -489,6 +490,7 @@ def test_server_registers_app_tools(tmp_path: Path, monkeypatch: pytest.MonkeyPa "run_app_evaluate", "run_app_uncertainty", "run_app_pipeline", + "fine_tune_app", "package_app_from_session", ) import asyncio @@ -502,11 +504,12 @@ def test_server_registers_app_tools(tmp_path: Path, monkeypatch: pytest.MonkeyPa assert callable(runner.run_app_api) assert callable(runner.run_app_action_api) + assert callable(runner.run_finetune_api) assert "solve_task" in index["prompts"] solve = server.prompt_solve_task("segment the liver", "one CT group") content = solve[0]["content"] - for tool in ("run_app_infer", "run_resume", "import_app", "design_config_strategy"): + for tool in ("run_app_infer", "fine_tune_app", "import_app", "design_config_strategy"): assert tool in content app_dir = _write_local_app(tmp_path) @@ -566,13 +569,14 @@ def _dummy_inputs(tmp_path: Path) -> list[list[str]]: def test_describe_app_inference_only_is_not_finetunable(tmp_path: Path) -> None: - """An app with no train Config.yml must report finetunable=False (a fine-tune would dead-end).""" + """An app with no train Config.yml must not advertise fine_tune_app (it would dead-end).""" app_dir = _write_local_app(tmp_path) (app_dir / "Config.yml").unlink() payload = _service(tmp_path).describe_app(str(app_dir)) assert payload["finetunable"] is False + assert "fine_tune_app" not in payload["next_actions"] # Inference routing is unaffected. assert payload["next_actions"][0] == "run_app_infer" @@ -696,6 +700,73 @@ def test_prepare_pipeline_spec_and_overrides(tmp_path: Path) -> None: assert "pipeline_TinyLocalApp__iterations_300" in tuned["output"] +def test_prepare_finetune_gates_local_app(tmp_path: Path) -> None: + app_dir = _write_local_app(tmp_path) + dataset = tmp_path / "Dataset" + dataset.mkdir() + service = _service(tmp_path) + + with pytest.raises(ValueError, match="allow_untrusted_code=True"): + service.prepare_finetune(ref=str(app_dir), dataset=str(dataset)) + + spec = service.prepare_finetune(ref=str(app_dir), dataset=str(dataset), allow_untrusted_code=True, epochs=3) + assert spec["kind"] == "finetune" + assert spec["target"] == "konfai_mcp.runner:run_finetune_api" + assert spec["kwargs"]["dataset"] == str(dataset.resolve()) + assert spec["kwargs"]["epochs"] == 3 + assert "finetune_TinyLocalApp" in spec["output"] + + +def test_prepare_finetune_rejects_remote_and_bad_dataset(tmp_path: Path) -> None: + app_dir = _write_local_app(tmp_path) + service = _service(tmp_path) + dataset = tmp_path / "Dataset" + dataset.mkdir() + + with pytest.raises(ValueError, match="remote app server"): + service.prepare_finetune(ref="localhost:8000:MyApp", dataset=str(dataset), allow_untrusted_code=True) + + with pytest.raises(ValueError, match="dataset must be an existing directory"): + service.prepare_finetune(ref=str(app_dir), dataset=str(tmp_path / "missing"), allow_untrusted_code=True) + + with pytest.raises(ValueError, match="epochs must be a positive integer"): + service.prepare_finetune(ref=str(app_dir), dataset=str(dataset), epochs=0, allow_untrusted_code=True) + + +def test_prepare_finetune_bakes_set_parameters(tmp_path: Path) -> None: + app_dir = _write_local_app(tmp_path) + dataset = tmp_path / "Dataset" + dataset.mkdir() + + spec = _service(tmp_path).prepare_finetune( + ref=str(app_dir), + dataset=str(dataset), + allow_untrusted_code=True, + config_overrides=["iterations=300"], + ) + # The overrides reach the runner (so they bake into the training config) ... + assert spec["kwargs"]["config_overrides"] == ["iterations=300"] + # ... and the default output dir is param-legible, so the leaderboard metrics_path names the trial. + assert "finetune_TinyLocalApp__iterations_300" in spec["output"] + + +def test_prepare_finetune_carries_the_batch_size_as_a_training_knob(tmp_path: Path) -> None: + """batch_size is a first-class training knob like epochs, NOT an app tunable: routed through + set_parameters it was refused ('batch_size' is no model parameter) and the job died at launch.""" + app_dir = _write_local_app(tmp_path) + dataset = tmp_path / "Dataset" + dataset.mkdir() + service = _service(tmp_path) + + spec = service.prepare_finetune(ref=str(app_dir), dataset=str(dataset), allow_untrusted_code=True, batch_size=4) + assert spec["kwargs"]["batch_size"] == 4 + # The knob still labels the trial's output dir, so the leaderboard names what produced the score. + assert "finetune_TinyLocalApp__batch_size_4" in spec["output"] + + with pytest.raises(ValueError, match="batch_size must be a positive integer"): + service.prepare_finetune(ref=str(app_dir), dataset=str(dataset), allow_untrusted_code=True, batch_size=0) + + def test_app_tools_launch_tracked_app_jobs(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """The tool -> prepare_* -> job-registry wiring: kind, runner target, devices, manifest and the tuned parameters that gate the refine loop (the launch itself is stubbed: no subprocess).""" @@ -747,6 +818,14 @@ def fake_launch(**kwargs: object) -> Job: assert payload["kind"] == "infer" assert payload["output"] == captured["kwargs"]["output"] # type: ignore[index] assert payload["set_parameters"] == ["iterations=300"] + + dataset = tmp_path / "Dataset" + dataset.mkdir(exist_ok=True) + tuned = server.fine_tune_app(ref=str(app_dir), dataset=str(dataset), allow_untrusted_code=True, epochs=2, cpu=1) + assert captured["kind"] == "finetune" + assert captured["target"] == "konfai_mcp.runner:run_finetune_api" + assert captured["kwargs"]["epochs"] == 2 # type: ignore[index] + assert tuned["kind"] == "finetune" finally: sys.modules.pop("konfai_mcp.server", None) @@ -773,6 +852,7 @@ async def scenario() -> dict[str, dict]: "run_app_evaluate": {"ref", "inputs", "gt"}, "run_app_uncertainty": {"ref", "inputs"}, "run_app_pipeline": {"ref", "inputs", "gt"}, + "fine_tune_app": {"ref", "dataset", "output"}, } for name, required_params in expected.items(): assert name in schemas, f"{name} is not exposed to the client" diff --git a/konfai-mcp/tests/test_mcp_server_finetune_e2e.py b/konfai-mcp/tests/test_mcp_server_finetune_e2e.py new file mode 100644 index 00000000..9a393421 --- /dev/null +++ b/konfai-mcp/tests/test_mcp_server_finetune_e2e.py @@ -0,0 +1,157 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""fine_tune_app end to end, black-box through the MCP client: real app, real training, changed weights.""" + +import asyncio +import importlib.util +import json +from collections.abc import Callable +from pathlib import Path +from types import ModuleType + +import pytest +from mcp_test_helpers import create_synthesis_dataset, resource_to_text + +fastmcp = pytest.importorskip("fastmcp") +pytest.importorskip("SimpleITK") + +pytestmark = pytest.mark.slow + +# The tiny synthesis training assets shared with the konfai-apps fine-tune integration suite. +WORKFLOW_ASSETS_DIR = Path(__file__).resolve().parents[2] / "tests" / "assets" / "Workflows" + +_PRETRAINED_EPOCH = 10 +_PRETRAINED_IT = 125134 + + +def _write_finetunable_app(app_dir: Path) -> None: + """A minimal REAL app bundle: manifest + train config + model code + a genuine checkpoint.""" + import torch + + app_dir.mkdir(parents=True, exist_ok=True) + (app_dir / "app.json").write_text( + json.dumps( + { + "display_name": "Tiny Synth", + "description": "Tiny local synthesis app for the fine-tune e2e", + "short_description": "Tiny synth", + "task": "synthesis", + "tta": 0, + "mc_dropout": 0, + "models": ["tiny_0.pt"], + "inputs": {"MR": {"display_name": "MR", "volume_type": "VOLUME", "required": True}}, + "outputs": {"sCT": {"display_name": "sCT", "volume_type": "VOLUME", "required": True}}, + } + ), + encoding="utf-8", + ) + config = (WORKFLOW_ASSETS_DIR / "Config.yml").read_text(encoding="utf-8") + config = config.replace("__DATASET_DIR__", "./Dataset").replace("__TRAIN_NAME__", "FT") + (app_dir / "Config.yml").write_text(config, encoding="utf-8") + (app_dir / "TinySynth.py").write_text( + (WORKFLOW_ASSETS_DIR / "TinySynth.py").read_text(encoding="utf-8"), encoding="utf-8" + ) + + spec = importlib.util.spec_from_file_location("TinySynth", app_dir / "TinySynth.py") + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + torch.save( + { + "epoch": _PRETRAINED_EPOCH, + "it": _PRETRAINED_IT, + "loss": 0.0, + "Model": module.TinySynthNet().network_states(), + }, + app_dir / "tiny_0.pt", + ) + + +def _model_tensors(checkpoint_path: Path) -> dict[str, "object"]: + import torch + + state = torch.load(checkpoint_path, map_location="cpu", weights_only=False) + flat: dict[str, object] = {} + for network_name, network_state in state["Model"].items(): + for key, tensor in network_state.items(): + flat[f"{network_name}.{key}"] = tensor + return flat + + +@pytest.mark.usefixtures("workspace_root") +def test_fine_tune_app_end_to_end_produces_a_bundle_with_changed_weights( + tmp_path: Path, load_mcp_server: Callable[[], ModuleType] +) -> None: + import torch + + app_dir = tmp_path / "TinySynthApp" + dataset_dir = tmp_path / "dataset" + output_dir = tmp_path / "FTBundle" + _write_finetunable_app(app_dir) + create_synthesis_dataset(dataset_dir) + + mcp_server = load_mcp_server() + + async def scenario() -> None: + async with fastmcp.Client(mcp_server.mcp) as client: + described = await client.call_tool("describe_app", {"ref": str(app_dir)}) + assert described.structured_content["finetunable"] is True + assert "fine_tune_app" in described.structured_content["next_actions"] + + job = await client.call_tool( + "fine_tune_app", + { + "ref": str(app_dir), + "dataset": str(dataset_dir), + "output": str(output_dir), + "name": "FT", + "epochs": 1, + "it_validation": 1, + "cpu": 1, + "allow_untrusted_code": True, + }, + ) + payload = job.structured_content + assert payload["kind"] == "finetune" + assert payload["output"] == str(output_dir) + + done = await client.call_tool( + "wait_for_job", {"job_id": payload["job_id"], "timeout_s": 300.0, "poll_interval_s": 0.5} + ) + if done.structured_content["status"] != "done": + log = await client.read_resource(f"job://{payload['job_id']}/log") + raise AssertionError(f"fine_tune_app failed: {done.structured_content}\n{resource_to_text(log)}") + assert "run_app_infer" in done.structured_content["next_actions"] + + asyncio.run(scenario()) + + # The produced bundle is a resolvable app: manifest + train config + code + fine-tuned checkpoint. + metadata = json.loads((output_dir / "app.json").read_text(encoding="utf-8")) + assert metadata["models"] == ["tiny_0.pt"] + assert (output_dir / "Config.yml").is_file() + assert (output_dir / "TinySynth.py").is_file() + + # The training really ran: counters restarted from the sanitized weights-only checkpoint... + produced = torch.load(output_dir / "tiny_0.pt", map_location="cpu", weights_only=False) + assert produced["epoch"] < _PRETRAINED_EPOCH + assert 0 < produced["it"] < _PRETRAINED_IT + + # ...and at least one weight tensor moved away from the input app's checkpoint. + before = _model_tensors(app_dir / "tiny_0.pt") + after = _model_tensors(output_dir / "tiny_0.pt") + assert set(after) == set(before) + assert any(not torch.equal(before[key], after[key]) for key in before), "fine-tune left every weight unchanged" diff --git a/konfai-mcp/tests/test_mcp_server_refine_loop.py b/konfai-mcp/tests/test_mcp_server_refine_loop.py index 18c0fbb4..0f7aa687 100644 --- a/konfai-mcp/tests/test_mcp_server_refine_loop.py +++ b/konfai-mcp/tests/test_mcp_server_refine_loop.py @@ -63,3 +63,12 @@ def test_evaluate_and_pipeline_incite_ranking_and_reexport(tmp_path: Path) -> No assert "leaderboard" in actions assert "compare_runs" in actions assert "export_app" in actions + + +def test_finetune_points_at_use_then_evaluate_not_empty_leaderboard(tmp_path: Path) -> None: + """A fine-tune keeps its training metrics out of the bundle, so it points at use+score, not a leaderboard + that would have nothing to rank yet.""" + actions = _next_actions(_done_job(tmp_path, "finetune")) + assert "run_app_infer" in actions + assert "run_app_evaluate" in actions + assert "leaderboard" not in actions diff --git a/konfai-mcp/tests/test_mcp_server_tool_index.py b/konfai-mcp/tests/test_mcp_server_tool_index.py index 1de4452e..6889dccf 100644 --- a/konfai-mcp/tests/test_mcp_server_tool_index.py +++ b/konfai-mcp/tests/test_mcp_server_tool_index.py @@ -59,6 +59,7 @@ def test_job_payload_next_actions_are_registered_tools( "evaluation", "transform", "infer", + "finetune", "evaluate", "uncertainty", "pipeline", diff --git a/konfai-mcp/tests/test_workflow_registry.py b/konfai-mcp/tests/test_workflow_registry.py index 048c7a03..bb351c3c 100644 --- a/konfai-mcp/tests/test_workflow_registry.py +++ b/konfai-mcp/tests/test_workflow_registry.py @@ -31,7 +31,7 @@ def test_literal_aliases_match_the_table() -> None: - # A job kind is either a workflow kind or a konfai-apps kind (run_app_*), never else. + # A job kind is either a workflow kind or a konfai-apps kind (run_app_* / fine_tune_app), never else. assert set(get_args(WorkflowKind)) == set(WORKFLOW_SPECS) assert set(get_args(JobKind)) == set(JOB_KINDS) assert set(JOB_KINDS) == set(WORKFLOW_SPECS) | set(APP_JOB_KINDS) diff --git a/studio/konfai_studio/agent.py b/studio/konfai_studio/agent.py index cfe125a8..134a3f7d 100644 --- a/studio/konfai_studio/agent.py +++ b/studio/konfai_studio/agent.py @@ -81,15 +81,15 @@ def _require_claude_code() -> None: "NEVER start training, fine-tuning, prediction, evaluation or a dataset transform without asking first. Say in one line " "what it will cost: how many cases, on which device, roughly how long, and wait for a yes. The " "only exception is relaunching a run you just corrected after a failure.\n\n" - "A run is a result only when its job says so. After any run_* launch, wait for the job to " + "A run is a result only when its job says so. After any run_* or fine_tune_app, wait for the job to " "reach a terminal state, then open what it produced and report THAT. A launch that returned cleanly is " "not a success and must never be presented as one.\n\n" "When a run fails: the cause in one line, never a traceback, then the correction. A correction may be " "relaunched straight away; anything else that costs GPU time is asked first. Two failed attempts is the " "limit: past it, lay out the options and ask. Never settle a scientific choice yourself (loss, " "architecture, split, label mapping, which data is the right one): those are the user's.\n\n" - "Tuning an app run: fine-tune an imported app with import_app then run_resume (weights_only), whose " - "config carries the training knobs. set_parameters takes the app's model tunables by their bare " + "Tuning an app run: training knobs (epochs, it_validation, lr, batch_size) are fine_tune_app's OWN " + "parameters, never set_parameters entries. set_parameters takes the app's model tunables by their bare " "name (list_app_parameters shows them); any OTHER config key needs its full dotted path from the config " "root ({'Trainer.Dataset.num_workers': 2}): a bare config key is refused as an unknown model " "parameter.\n\n" diff --git a/studio/konfai_studio/jobs.py b/studio/konfai_studio/jobs.py index 147de576..2ca98c6d 100644 --- a/studio/konfai_studio/jobs.py +++ b/studio/konfai_studio/jobs.py @@ -42,9 +42,10 @@ "Transforms": "transform", } # App job kinds → the workflow vocabulary the client speaks: an `infer` job runs a prediction, an -# `evaluate` job an evaluation. Announced under the app vocabulary, a run sits outside every panel the -# client gates on kind (the evaluation table, the prediction browse target, the sub-tab order). -_APP_RUN_KIND = {"infer": "prediction", "evaluate": "evaluation"} +# `evaluate` job an evaluation, a `finetune` job a training. Announced under the app vocabulary, a run +# sits outside every panel the client gates on kind (the evaluation table, the prediction browse +# target, the sub-tab order). +_APP_RUN_KIND = {"infer": "prediction", "evaluate": "evaluation", "finetune": "train"} def _finite(value: Any) -> Any: diff --git a/studio/konfai_studio/workflow.py b/studio/konfai_studio/workflow.py index 1327c07f..6757c93b 100644 --- a/studio/konfai_studio/workflow.py +++ b/studio/konfai_studio/workflow.py @@ -30,7 +30,7 @@ "read the log, give the cause in one line and the fix." ) _FOLLOW_READ = "Report it in two lines, then the next step." -_LAUNCHES = ("run_",) +_LAUNCHES = ("run_", "fine_tune_") # Stages reached before `initialize_session` has made a workspace: nothing there can be summarised yet. _BEFORE_WORKSPACE = {"dataset_inspection", "action_selection", "app_selection"} # One ceiling for the whole chain: the assistant's own block, the derived fill-in, and the bar. Past From 71827cde23263db04d5fb360b2875aad2a2f9b05 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 09:26:10 +0200 Subject: [PATCH 23/28] feat(transform): a tiered contract, streamable Elastix and masked statistics The Transform contract is tiered in form as it always was in practice: tier 0 is __call__ alone, tier 1 one locality class attribute (the ~25 unconditional patch_locality methods collapse to it), tier 2 the streaming-aware methods -- documented in the base docstring. Elastix stores only its drawn B-spline control lattice and evaluates the displacement through the in-tree ITK-bit-compatible kernel instead of materializing a full sampling grid per copy per case (~19 GB resident becomes ~100 KB, the draw streams as a bounded halo, SimpleITK leaves the class). Standardize/Clip with a mask stream through a masked disk-statistics scan instead of forcing two whole volumes (the published Synthesis pattern). The signed-permutation and mirror/permute region math that lived twice with two tolerances is one geometry.py family; an ambiguous bare Flip/Permute past an Expand records which class won as a plan note. CutOUT binds a float fraction and refuses outside (0, 1] (every YAML value was silently a no-op or an eraser). --- .../reference/components/augmentations.md | 6 +- konfai/data/augmentation/base.py | 11 + konfai/data/augmentation/color.py | 11 +- konfai/data/augmentation/placed.py | 25 +- konfai/data/augmentation/spatial.py | 311 ++++++++++-------- konfai/data/geometry.py | 90 +++++ konfai/data/transform/base.py | 70 +++- konfai/data/transform/ensemble.py | 37 +-- konfai/data/transform/intensity.py | 193 ++++++++--- konfai/data/transform/labels.py | 27 +- konfai/data/transform/resample.py | 16 +- konfai/data/transform/shape.py | 147 +++------ konfai/utils/dataset/statistics.py | 60 ++++ tests/unit/oracle_support.py | 13 +- tests/unit/test_augmentation.py | 149 +++++++-- tests/unit/test_case_reduction.py | 4 +- tests/unit/test_dataset_streaming.py | 2 +- tests/unit/test_geometry.py | 81 +++++ tests/unit/test_masked_statistics.py | 128 +++++++ tests/unit/test_transform.py | 228 ++++--------- tests/unit/test_transformer_workflow.py | 2 +- 21 files changed, 1067 insertions(+), 544 deletions(-) create mode 100644 tests/unit/test_masked_statistics.py diff --git a/docs/source/reference/components/augmentations.md b/docs/source/reference/components/augmentations.md index 8934cc6e..1ab9a40b 100644 --- a/docs/source/reference/components/augmentations.md +++ b/docs/source/reference/components/augmentations.md @@ -51,7 +51,7 @@ Reversible affine warps via `grid_sample` (nearest-neighbour for label tensors). | `Rotate` | Random rotation (degrees). | `a_min=0, a_max=360, is_quarter=False` | **yes** with `is_quarter: true` | **yes** | **yes**: an index remap with `is_quarter: true`, and a free angle streams through the affine's own pull box | | `Scale` | Random log2-normal isotropic scale. | `s_std=0.2` | no | **yes** | **yes**: the region pulls its own window through the affine, so no fixed halo is needed | | `Flip` | Per-axis random flip; optional vector-field channel negation. | `f_prob=[0.33,0.33,0.33], vector_field=False` | no | **yes** (self-inverse) | **yes**: index remap; no with `vector_field: true` (negating a channel changes values) | -| `Elastix` | Random BSpline elastic warp (SimpleITK). | `grid_spacing=16, max_displacement=16` | no | no | no: the displacement field is built at the full shape and indexed by absolute position | +| `Elastix` | Random cubic-BSpline elastic warp, drawn as a control-point lattice. | `grid_spacing=16, max_displacement=16` (world units) | no | no | **yes**: the displacement is evaluated lazily from the lattice, and no voxel moves further than `max_displacement`, which bounds the source box a region pulls | | `Permute` | Random spatial-axis permutation (**3-D only**). | `prob_permute=[0.5,0.5]` | **yes** | **yes** | **yes**: index remap | | `Mask` | Randomly place a mask volume; outside → `value` (SimpleITK). | `mask` (required), `value` (required) | **yes** | no | no: the output grid is the mask's, and the mask is already resident | @@ -75,9 +75,9 @@ them streams: a voxel comes out the same whatever region it was read in. | Name | Purpose | Key args (defaults) | Notes | Stream | | --- | --- | --- | --- | --- | | `Noise` | Diffusion-style forward noising (zero-terminal-SNR β schedule). | `n_std` (required), `noise_step=1000` | Its `prob` is the max noise timestep, not an apply probability; it always applies. | **yes**: the field is a function of the voxel's position and the copy's seed, so a region draws the values it would have had in the whole volume | -| `CutOUT` | Random cutout box filled with `value`. | `c_prob`, `cutout_size`, `value` (all required) | Gating uses the base probability. | **yes**: the box is placed in the whole volume's coordinates, so a region sees the part of it that falls inside it | +| `CutOUT` | Random cutout box filled with `value`. | `cutout_size` (a fraction of the extent per axis, in `(0, 1]`), `value` (both required) | Gating uses the base probability; a `cutout_size` outside `(0, 1]` is refused. | **yes**: the box is placed in the whole volume's coordinates, so a region sees the part of it that falls inside it | -`Elastix` and `Mask` require SimpleITK. The `vector_field` flag on `Flip` should +`Mask` requires SimpleITK. The `vector_field` flag on `Flip` should only be enabled for single-channel or genuine vector-field groups. ## Next steps diff --git a/konfai/data/augmentation/base.py b/konfai/data/augmentation/base.py index 01f4184a..e7fb6e9d 100644 --- a/konfai/data/augmentation/base.py +++ b/konfai/data/augmentation/base.py @@ -184,6 +184,15 @@ def set_datasets(self, datasets: list[Dataset]) -> None: class DataAugmentation(NeedDevice, ABC): + #: Tier-1 declaration, exactly as :attr:`konfai.data.transform.Transform.locality`: the one + #: :class:`LocalityKind` every draw of this class makes, when it is unconditional. The base + #: ``_patch_locality`` answers from it; ``None`` (the default) keeps the fail-safe + #: ``WHOLE_VOLUME``. A declaration that depends on the draw overrides the method instead. + locality: LocalityKind | None = None + + #: Tier-1 companion to a ``HALO`` :attr:`locality`: per-spatial-axis radius in array order. + halo: tuple[int, ...] = () + def __init_subclass__(cls, **kwargs: object) -> None: # A draw is a chain stage too: record its constructor arguments as given, so konfai.api can # write the config tree back from live objects (see Transform.__init_subclass__). @@ -274,6 +283,8 @@ def patch_locality(self, index: int, a: int, cache_attribute: Attribute) -> Patc return self._patch_locality(index, self._slot(index, a), cache_attribute) def _patch_locality(self, index: int, a: int, cache_attribute: Attribute) -> PatchLocality: + if self.locality is not None: + return PatchLocality(self.locality, halo=self.halo) return PatchLocality(LocalityKind.WHOLE_VOLUME) def stream_region_source( diff --git a/konfai/data/augmentation/color.py b/konfai/data/augmentation/color.py index 8167381b..953ec9f6 100644 --- a/konfai/data/augmentation/color.py +++ b/konfai/data/augmentation/color.py @@ -21,21 +21,20 @@ import torch from konfai.data.augmentation.base import DataAugmentation, _axis_rotation_matrix, _scale_matrix, _translate_matrix -from konfai.data.transform import LocalityKind, PatchLocality +from konfai.data.transform import LocalityKind from konfai.utils.dataset import Attribute from konfai.utils.errors import AugmentationError class ColorTransform(DataAugmentation): + # The draw is a colour matrix applied to each voxel on its own: no neighbour, no coordinate, + # no extent. Whatever region a voxel is read in, it comes out the same. + locality = LocalityKind.POINTWISE + def __init__(self, groups: list[str] | None = None) -> None: super().__init__(groups) self.matrix: dict[int, list[torch.Tensor]] = {} - def _patch_locality(self, index: int, a: int, cache_attribute: Attribute) -> PatchLocality: - # The draw is a colour matrix applied to each voxel on its own: no neighbour, no coordinate, - # no extent. Whatever region a voxel is read in, it comes out the same. - return PatchLocality(LocalityKind.POINTWISE) - def _compute(self, name: str, index: int, a: int, tensor: torch.Tensor) -> torch.Tensor: matrix = self.matrix[index][a] result = tensor.reshape([*tensor.shape[:1], int(np.prod(tensor.shape[1:]))]) diff --git a/konfai/data/augmentation/placed.py b/konfai/data/augmentation/placed.py index dc78e084..7ef1ac12 100644 --- a/konfai/data/augmentation/placed.py +++ b/konfai/data/augmentation/placed.py @@ -27,7 +27,7 @@ except ImportError: sitk = None # type: ignore[assignment] from konfai.data.augmentation.base import DataAugmentation, _hashed_normal_field, _require_simpleitk -from konfai.data.transform import LocalityKind, PatchLocality, RegionContext +from konfai.data.transform import LocalityKind, RegionContext from konfai.utils.dataset import Attribute from konfai.utils.errors import AugmentationError @@ -39,8 +39,7 @@ class PlacedDraw(DataAugmentation): spatial extent (``full``), which the whole volume passes as zeros and its own shape. """ - def _patch_locality(self, index: int, a: int, cache_attribute: Attribute) -> PatchLocality: - return PatchLocality(LocalityKind.POINTWISE) + locality = LocalityKind.POINTWISE @abstractmethod def _apply( @@ -121,16 +120,28 @@ def _apply( class CutOUT(PlacedDraw): + """Cut a box out of the copy and fill it with ``value``. + + ``cutout_size`` is the box's edge as a FRACTION of the volume's extent per axis, in (0, 1]: + the box is placed in normalised coordinates, so a 0.34 box cuts about ``0.34**rank`` of the + volume. An integer count of voxels is not a size this draw takes: through the YAML binder it + once bound silently and erased the whole copy. + """ + def __init__( self, - c_prob: float, - cutout_size: int, + cutout_size: float, value: float, groups: list[str] | None = None, ) -> None: super().__init__(groups) - self.c_prob = c_prob - self.cutout_size = cutout_size + if not 0.0 < float(cutout_size) <= 1.0: + raise AugmentationError( + f"'CutOUT' was given cutout_size={cutout_size!r}, which is not in (0, 1].", + "cutout_size is the box's edge as a fraction of the volume's extent per axis:" + " cutout_size: 0.34 cuts about a third of each axis.", + ) + self.cutout_size = float(cutout_size) self.centers: dict[int, list[torch.Tensor]] = {} self.value = value diff --git a/konfai/data/augmentation/spatial.py b/konfai/data/augmentation/spatial.py index 6959c48b..949a6a09 100644 --- a/konfai/data/augmentation/spatial.py +++ b/konfai/data/augmentation/spatial.py @@ -21,22 +21,29 @@ import torch import torch.nn.functional as F -try: - import SimpleITK as sitk -except ImportError: - sitk = None # type: ignore[assignment] from konfai.data.augmentation.base import ( DataAugmentation, _reflect_interval, - _require_simpleitk, _rotation_2d_matrix, _rotation_3d_matrix, _scale_matrix, _translate_matrix, ) -from konfai.data.geometry import AffineMap, WorldBox +from konfai.data.geometry import ( + SIGNED_PERMUTATION_ATOL_FLOAT32, + AffineMap, + AxisRemap, + DisplacementStage, + Grid, + WorldBox, + apply_remap, + remap_region, + remap_shape, + signed_permutation, +) +from konfai.data.sampling import _apply, _displacement_at, _to_index from konfai.data.transform import LocalityKind, PatchLocality, RegionContext -from konfai.utils.dataset import Attribute, data_to_image +from konfai.utils.dataset import Attribute class EulerTransform(DataAugmentation): @@ -52,15 +59,14 @@ class EulerTransform(DataAugmentation): quarter turn) is handed the block its declaration asked for and applies the draw to it whole. """ + # A map about the centre displaces a voxel by an amount that grows with its distance to it, + # so no constant halo bounds the read: each target region pulls the source box it maps to. + locality = LocalityKind.REGRID + def __init__(self) -> None: super().__init__() self.matrix: dict[int, list[torch.Tensor]] = {} - def _patch_locality(self, index: int, a: int, cache_attribute: Attribute) -> PatchLocality: - # A map about the centre displaces a voxel by an amount that grows with its distance to it, - # so no constant halo bounds the read: each target region pulls the source box it maps to. - return PatchLocality(LocalityKind.REGRID) - def _grid_matrix(self, index: int, a: int, shape: list[int]) -> torch.Tensor: """Copy *a*'s affine, in the normalised coordinates ``affine_grid`` spans over ``shape``.""" return self.matrix[index][a] @@ -277,10 +283,6 @@ class Rotate(EulerTransform): to (a slab of a rotated volume pulls a wide band, which the plan prices). """ - # A quarter angle's cosines are computed in float32, so an entry of the composed matrix lands within - # ~1e-7 of the 0 or +/-1 it stands for rather than on it. - _QUARTER_ATOL = 1e-6 - def __init__(self, a_min: float = 0, a_max: float = 360, is_quarter: bool = False): super().__init__() self.a_min = a_min @@ -307,54 +309,33 @@ def _state_init(self, index: int, shapes: list[list[int]], caches_attribute: lis return [Rotate._draw_shape(self.matrix[index][a], shape) for a, shape in enumerate(shapes)] @classmethod - def _index_remap(cls, matrix: torch.Tensor) -> tuple[list[int], list[int]] | None: - """The permute dims and flip axes reproducing a rotation exactly, or ``None`` if it must be sampled. - - ``matrix`` maps an output coordinate onto the input it comes from, so it is a signed permutation - exactly when every row and column has a single +/-1: output axis ``pi(k)`` then reads input axis - ``k``, mirrored where the sign is negative. An orthonormal row of L1 norm 1 has one such entry, - which is what separates a quarter turn from any other angle. + def _index_remap(cls, matrix: torch.Tensor) -> AxisRemap | None: + """The exact index remap this draw is, or ``None`` if it must be sampled. - Dims and axes are channel-first, where physical axis ``k`` is dim ``n - k``. + ``matrix`` maps an output coordinate onto the input it comes from, so it is a signed + permutation exactly for a quarter turn: the shared predicate decides, at the tolerance of + the float32 cosines the matrix is composed from. """ - linear = matrix[0, :-1, :-1] - n = linear.shape[0] - unit = torch.ones(n) - if not torch.allclose(linear.abs().sum(0), unit, atol=cls._QUARTER_ATOL): - return None - if not torch.allclose(linear.abs().sum(1), unit, atol=cls._QUARTER_ATOL): - return None - - dims = [0] * (n + 1) - flips: list[int] = [] - for k in range(n): - source = int(linear[k].abs().argmax()) - dims[n - source] = n - k - if linear[k, source] < 0: - flips.append(n - source) - return dims, flips + return signed_permutation(matrix[0, :-1, :-1], SIGNED_PERMUTATION_ATOL_FLOAT32) @classmethod def _draw_shape(cls, matrix: torch.Tensor, shape: list[int]) -> list[int]: """The spatial extents a draw lands on, given the ones it is applied to. - Output dim ``i`` reads input dim ``dims[i]``, so it carries that axis's extent with it: what a - turn preserves is the volume, not which axis holds an extent. A sampled draw spans the extent it - is given. ``dims`` is channel-first, so spatial axis k is dim k + 1. + A quarter turn carries each extent with the axis it reads; a sampled draw spans the extent + it is given. """ remap = cls._index_remap(matrix) if remap is None: return list(shape) - dims, _ = remap - return [shape[dim - 1] for dim in dims[1:]] + return remap_shape(shape, remap) def _reorient(self, index: int, a: int, matrix: torch.Tensor, tensor: torch.Tensor) -> torch.Tensor: remap = Rotate._index_remap(matrix) if remap is None: return self._sample(matrix, tensor) - dims, flips = remap - # flip materialises the permuted view, so the copy never aliases the tensor it was drawn from. - return tensor.permute(dims).flip(flips) + # apply_remap materialises, so the copy never aliases the tensor it was drawn from. + return apply_remap(tensor, remap) def _compute(self, name: str, index: int, a: int, tensor: torch.Tensor) -> torch.Tensor: return self._reorient(index, a, self._grid_matrix(index, a, list(tensor.shape[1:])), tensor) @@ -382,20 +363,10 @@ def _stream_region_source( target_slices: tuple[slice, ...], source_spatial_shape: list[int], ) -> list[slice]: - # Output axis o reads input axis dims[o], so placing o's target slice at that input axis yields - # the region whose remap reproduces the patch; a MIRRORED output axis reads the mirror region - # ``[n - stop, n - start)`` of it. Dims and flips are channel-first, so spatial axis k is dim k + 1. remap = Rotate._index_remap(self.matrix[index][a]) if remap is None: return super()._stream_region_source(index, a, target_slices, source_spatial_shape) - dims, flips = remap - source_slices = [slice(0, n) for n in source_spatial_shape] - for out_dim in range(1, len(dims)): - in_axis = dims[out_dim] - 1 - sl = target_slices[out_dim - 1] - n = source_spatial_shape[in_axis] - source_slices[in_axis] = slice(n - sl.stop, n - sl.start) if out_dim in flips else slice(sl.start, sl.stop) - return source_slices + return remap_region(target_slices, source_spatial_shape, remap) class Scale(EulerTransform): @@ -459,18 +430,11 @@ def _stream_region_source( target_slices: tuple[slice, ...], source_spatial_shape: list[int], ) -> list[slice]: - # A flipped spatial axis reads the mirror region ``[n - stop, n - start)``; flipping that - # sub-region reproduces the target patch. Non-flipped axes read the identity region. ``flip`` - # holds channel-first tensor dims, so spatial axis k is dim k + 1. + # A mirror moves no axis: the remap is the identity permutation, mirrored on the flipped + # axes. ``flip`` holds channel-first tensor dims, so spatial axis k is dim k + 1. dims = self.flip[index][a] - return [ - ( - slice(source_spatial_shape[k] - sl.stop, source_spatial_shape[k] - sl.start) - if (k + 1) in dims - else slice(sl.start, sl.stop) - ) - for k, sl in enumerate(target_slices) - ] + remap: AxisRemap = [(k, (k + 1) in dims) for k in range(len(target_slices))] + return remap_region(target_slices, source_spatial_shape, remap) def _compute(self, name: str, index: int, a: int, tensor: torch.Tensor) -> torch.Tensor: return self._flip(tensor, self.flip[index][a]) @@ -502,7 +466,7 @@ def _state_init(self, index: int, shapes: list[list[int]], caches_attribute: lis raise ValueError("The number of augmentation images must be equal to 2") self.permute[index] = torch.eye(2, dtype=torch.bool) for i in range(len(shapes)): - shapes[i] = [shapes[i][axis] for axis in self._source_axes(index, i)] + shapes[i] = remap_shape(shapes[i], self._remap(index, i)) return shapes def _source_axes(self, index: int, a: int) -> list[int]: @@ -512,14 +476,17 @@ def _source_axes(self, index: int, a: int) -> list[int]: axes = [axes[dim - 1] for dim in permute[1:]] return axes - def _patch_locality(self, index: int, a: int, cache_attribute: Attribute) -> PatchLocality: - # Reordering axes moves every voxel and touches none, so the multiset of values is the input's: - # a bijection, which is what ORIENTATION promises. - return PatchLocality(LocalityKind.ORIENTATION) + # Reordering axes moves every voxel and touches none, so the multiset of values is the input's: + # a bijection, which is what ORIENTATION promises. + locality = LocalityKind.ORIENTATION + + def _remap(self, index: int, a: int) -> AxisRemap: + # Output axis k is source axis ``_source_axes()[k]``, never mirrored. + return [(axis, False) for axis in self._source_axes(index, a)] def _stream_shape(self, index: int, a: int, shape: list[int]) -> list[int]: # The same reorder state_init applied to the copy's grid. - return [shape[axis] for axis in self._source_axes(index, a)] + return remap_shape(shape, self._remap(index, a)) def _stream_region_source( self, @@ -528,12 +495,7 @@ def _stream_region_source( target_slices: tuple[slice, ...], source_spatial_shape: list[int], ) -> list[slice]: - # Output axis k is source axis ``_source_axes()[k]``, so placing each target slice back on its - # source axis gives the region whose permutation is the target patch. - source_slices = [slice(0, n) for n in source_spatial_shape] - for k, sl in enumerate(target_slices): - source_slices[self._source_axes(index, a)[k]] = slice(sl.start, sl.stop) - return source_slices + return remap_region(target_slices, source_spatial_shape, self._remap(index, a)) def _compute(self, name: str, index: int, a: int, tensor: torch.Tensor) -> torch.Tensor: for permute in self._permute_dims[self.permute[index][a]]: @@ -546,88 +508,142 @@ def _inverse(self, index: int, a: int, tensor: torch.Tensor) -> torch.Tensor: return tensor +#: Voxels one chunk of an Elastix warp evaluates at once: the float64 corner walk holds tens of +#: bytes per voxel it evaluates, so the sampling grid is filled in chunks and only the grid (12 +#: bytes per voxel of the region) stands at the peak. +_ELASTIX_CHUNK_VOXELS = 1 << 21 + + class Elastix(DataAugmentation): + """A random cubic-B-spline elastic warp, drawn as a control-point lattice per copy. + + The only state of a draw is its lattice (a :class:`DisplacementStage`, O(control points)); the + displacement at a voxel is evaluated lazily with the order-3 kernel ITK uses + (``konfai.data.sampling``), so a region computes exactly its part and the copies stream. + + ``grid_spacing`` and ``max_displacement`` are in the case's world units (its header spacing; a + headerless case counts voxels). Control values are uniform in ``[-max_displacement, + max_displacement]`` and the kernel is a convex combination of them, so no voxel's displacement + exceeds ``max_displacement``: what bounds the source box a target region pulls. + """ + + # A warp through a bounded field: each target region pulls its own box, widened by the field's + # reach, and samples it. The bound is constant, but REGRID (not HALO) keeps the pull exactly + # the mapped box and prices the sampling grid the warp builds beside its block. + locality = LocalityKind.REGRID + def __init__(self, grid_spacing: int = 16, max_displacement: int = 16) -> None: - _require_simpleitk() super().__init__() self.grid_spacing = grid_spacing self.max_displacement = max_displacement - #: Per case index, per selected copy: the sampling grid grid_sample reads, 12 bytes per - #: voxel of the copy. Kept for the run of the draw (every group of the case samples through - #: it) and dropped with the draw in :meth:`reset_state`. - self.displacement_fields: dict[int, list[torch.Tensor]] = {} + #: Per case index, per selected copy: the drawn lattice and the case grid it warps. + self.draws: dict[int, list[tuple[DisplacementStage, Grid]]] = {} def reset_state(self, index: int | None = None) -> None: super().reset_state(index) if index is None: - self.displacement_fields.clear() + self.draws.clear() else: - self.displacement_fields.pop(index, None) - - @staticmethod - def _format_loc(new_locs, shape): - for i in range(len(shape)): - new_locs[..., i] = 2 * (new_locs[..., i] / (shape[i] - 1) - 0.5) - new_locs = new_locs[..., list(reversed(range(len(shape))))] - return new_locs + self.draws.pop(index, None) def _state_init(self, index: int, shapes: list[list[int]], caches_attribute: list[Attribute]) -> list[list[int]]: - print(f"[KonfAI] Compute Displacement Field for index {index}") - self.displacement_fields[index] = [] - for i, (shape, cache_attribute) in enumerate(zip(shapes, caches_attribute, strict=False)): + self.draws[index] = [] + for shape, cache_attribute in zip(shapes, caches_attribute, strict=False): dim = len(shape) - if "Spacing" not in cache_attribute: - spacing = np.array([1.0 for _ in range(dim)]) - else: - spacing = cache_attribute.get_np_array("Spacing") + grid, _missing = Grid.from_header(list(shape), cache_attribute, "the case this draw warps") + # The transform domain covers the volume's physical footprint (voxel edges). The + # coefficient grid is what sitk.BSplineTransform(order=3) stores for that domain: one + # node's spacing before its origin, mesh + 3 nodes per axis (verified against + # GetCoefficientImages; the parity test holds it to TransformToDisplacementFieldFilter). + physical_xyz = np.array(list(reversed(shape)), dtype=np.float64) * grid.spacing_xyz + mesh_xyz = np.maximum(1, (physical_xyz / float(self.grid_spacing) + 0.5).astype(np.int64)) + node_spacing_xyz = physical_xyz / mesh_xyz + domain_origin_xyz = grid.origin_xyz - grid.direction_xyz @ (0.5 * grid.spacing_xyz) + coefficient_grid = Grid( + tuple(int(nodes) + 3 for nodes in reversed(mesh_xyz)), + domain_origin_xyz - grid.direction_xyz @ node_spacing_xyz, + node_spacing_xyz, + grid.direction_xyz, + ) + control = torch.rand((dim, *(int(nodes) + 3 for nodes in reversed(mesh_xyz))), dtype=torch.float64) + control = (control - 0.5) * (2.0 * self.max_displacement) + self.draws[index].append((DisplacementStage(coefficient_grid, control.numpy(), order=3), grid)) + return shapes - grid_physical_spacing = [self.grid_spacing] * dim - image_physical_size = [size * spacing for size, spacing in zip(shape, spacing, strict=False)] - mesh_size = [ - int(image_size / grid_spacing + 0.5) - for image_size, grid_spacing in zip(image_physical_size, grid_physical_spacing, strict=False) - ] - if "Spacing" not in cache_attribute: - cache_attribute["Spacing"] = np.array([1.0 for _ in range(dim)]) - if "Origin" not in cache_attribute: - cache_attribute["Origin"] = np.array([1.0 for _ in range(dim)]) - if "Direction" not in cache_attribute: - cache_attribute["Direction"] = np.eye(dim).flatten() + def _stream_region_source( + self, + index: int, + a: int, + target_slices: tuple[slice, ...], + source_spatial_shape: list[int], + ) -> list[slice]: + # |displacement| <= max_displacement in world units by convexity: in voxels that is the + # bound over the axis's spacing, plus one voxel for the far interpolation tap. + _stage, grid = self.draws[index][a] + rank = len(source_spatial_shape) + pull: list[slice] = [] + for k, (part, extent) in enumerate(zip(target_slices, source_spatial_shape, strict=True)): + reach = int(np.ceil(self.max_displacement / float(grid.spacing_xyz[rank - 1 - k]))) + 1 + start = max(0, part.start - reach) + stop = min(extent, part.stop + reach) + pull.append(slice(start, max(stop, start + 1))) + return pull - ref_image = data_to_image(np.expand_dims(np.zeros(shape), 0), cache_attribute) + def _sampling_grid( + self, + stage: DisplacementStage, + grid: Grid, + target: tuple[slice, ...], + source: tuple[slice, ...], + device: torch.device, + ) -> torch.Tensor: + """Where each target voxel samples from, normalised on the source block for ``grid_sample``. - bspline_transform = sitk.BSplineTransformInitializer( - image1=ref_image, transformDomainMeshSize=mesh_size, order=3 + Per target voxel: its world point, plus the lattice's displacement there, back to a + continuous index, re-expressed on the block. Filled slab by slab along the first target + axis: the corner walk's float64 temporaries then stay chunk-sized while the float32 grid is + the one region-sized tensor held. + """ + target_shape = tuple(int(part.stop - part.start) for part in target) + rank = len(target_shape) + starts = torch.tensor([float(part.start) for part in reversed(source)], dtype=torch.float64, device=device) + spans = torch.tensor( + [float(part.stop - part.start - 1) for part in reversed(source)], dtype=torch.float64, device=device + ) + out = torch.empty((*target_shape, rank), dtype=torch.float32, device=device) + plane = int(np.prod(target_shape[1:], dtype=np.int64)) if rank > 1 else 1 + rows = max(1, _ELASTIX_CHUNK_VOXELS // max(1, plane)) + for begin in range(0, target_shape[0], rows): + slab = (slice(target[0].start + begin, min(target[0].stop, target[0].start + begin + rows)), *target[1:]) + axes = [torch.arange(part.start, part.stop, dtype=torch.float64, device=device) for part in slab] + mesh = torch.meshgrid(*axes, indexing="ij") + index_xyz = torch.stack(list(reversed(mesh)), dim=-1) + world = _apply(index_xyz, grid.index_to_world, device) + world = world + _displacement_at(stage, world, device) + coordinates = _to_index(world, grid, device) + local = torch.where( + spans > 0, (coordinates - starts) * 2.0 / spans.clamp(min=1.0) - 1.0, torch.zeros_like(coordinates) ) - displacement_filter = sitk.TransformToDisplacementFieldFilter() - displacement_filter.SetReferenceImage(ref_image) - - vectors = [torch.arange(0, s) for s in shape] - grids = torch.meshgrid(vectors, indexing="ij") - grid = torch.stack(grids) - grid = torch.unsqueeze(grid, 0) - grid = grid.type(torch.float).permute([0] + [i + 2 for i in range(len(shape))] + [1]) - - control_points = torch.rand(*[size + 3 for size in mesh_size], dim) - control_points -= 0.5 - control_points *= 2 * self.max_displacement - bspline_transform.SetParameters(control_points.flatten().tolist()) - displacement = sitk.GetArrayFromImage(displacement_filter.Execute(bspline_transform)) - new_locs = grid + torch.unsqueeze(torch.from_numpy(displacement), 0).type(torch.float32) - self.displacement_fields[index].append(Elastix._format_loc(new_locs, shape)) - print(f"[KonfAI] Compute in progress : {(i + 1) / len(shapes) * 100:.2f} %") - return shapes + out[begin : begin + (slab[0].stop - slab[0].start)] = local.to(torch.float32) + return out - # WHOLE_VOLUME on purpose: _state_init materialises the displacement field at the full shape and - # indexes it by absolute position, so streaming the image saves nothing while the field is - # resident. - def _compute(self, name: str, index: int, a: int, tensor: torch.Tensor) -> torch.Tensor: - # Integer tensors are label maps: nearest-neighbour keeps class ids intact. + def _warp( + self, + stage: DisplacementStage, + grid: Grid, + tensor: torch.Tensor, + source: tuple[slice, ...], + target: tuple[slice, ...], + ) -> torch.Tensor: + # Integer tensors are label maps: nearest-neighbour keeps class ids intact. The whole + # volume is the region that covers everything, so a streamed region and the whole-volume + # copy run the same arithmetic and agree to grid_sample's own float rounding. mode = "nearest" if not tensor.dtype.is_floating_point else "bilinear" + sampling = self._sampling_grid(stage, grid, target, source, tensor.device).unsqueeze(0) return ( F.grid_sample( tensor.type(torch.float32).unsqueeze(0), - self.displacement_fields[index][a].to(tensor.device), + sampling, align_corners=True, mode=mode, padding_mode="border", @@ -636,5 +652,16 @@ def _compute(self, name: str, index: int, a: int, tensor: torch.Tensor) -> torch .squeeze(0) ) + def _compute(self, name: str, index: int, a: int, tensor: torch.Tensor) -> torch.Tensor: + stage, grid = self.draws[index][a] + whole = tuple(slice(0, int(extent)) for extent in tensor.shape[1:]) + return self._warp(stage, grid, tensor, whole, whole) + + def _stream_region( + self, name: str, index: int, a: int, tensor: torch.Tensor, context: RegionContext + ) -> torch.Tensor: + stage, grid = self.draws[index][a] + return self._warp(stage, grid, tensor, tuple(context.source), tuple(context.target)) + def _inverse(self, index: int, a: int, tensor: torch.Tensor) -> torch.Tensor: raise NotImplementedError("Elastix augmentation has no inverse; do not use it for invertible TTA.") diff --git a/konfai/data/geometry.py b/konfai/data/geometry.py index cb48747b..b0ef7069 100644 --- a/konfai/data/geometry.py +++ b/konfai/data/geometry.py @@ -371,6 +371,96 @@ def sub_grid(self, region_zyx: tuple[slice, ...]) -> Grid: ) +# ---------------------------------------------------------------------------------- index remaps +# A signed permutation of the axes is the one map that is an exact index remap: values only change +# place, so an ORIENTATION stage's bijection promise (and everything preserves_statistics lets a +# later stage trust) rests on the predicate below. It is written once, here, because two copies of +# it (a draw's quarter turn, a header's reorientation) is exactly where a silent divergence starts. + +#: Whether a matrix entry stands for exactly 0 or +/-1, by the matrix's provenance. Two tolerances, +#: because the matrices come at two precisions: a draw's quarter-turn affine is composed from +#: float32 cosines, so an entry lands within ~1e-7 of the value it stands for; a header +#: reorientation is a float64 product of orthonormal matrices and lands within a few double ulps. +#: One shared 1e-6 would remap a float64 direction whose obliqueness is real, not rounding. +SIGNED_PERMUTATION_ATOL_FLOAT32 = 1e-6 +SIGNED_PERMUTATION_ATOL_FLOAT64 = 1e-9 + +#: Per output SPATIAL axis in array order: ``(source_axis, mirrored)`` — which source axis it reads, +#: and whether it reads it backwards. +AxisRemap = list[tuple[int, bool]] + + +def signed_permutation(matrix: object, atol: float) -> AxisRemap | None: + """The exact index remap ``matrix`` is, or ``None`` where it must be sampled. + + ``matrix`` maps an output coordinate onto the input it comes from, in physical ``(x, y, z)`` + order: it is a signed permutation exactly when every column holds a single +/-1 and every row + carries unit weight. The three tests together admit exactly those: unit column sums alone also + pass an axis-averaging matrix, unit peaks alone a superposing one, and columns alone a + rank-deficient one that reads the same axis twice. The answer is in array order, where physical + axis ``k`` is array axis ``n - 1 - k``. + """ + linear = np.asarray(matrix, dtype=np.float64) + n = int(linear.shape[0]) + magnitude = np.abs(linear) + unit = np.ones(n) + if not np.allclose(magnitude.sum(axis=0), unit, atol=atol): + return None + if not np.allclose(magnitude.max(axis=0), unit, atol=atol): + return None + if not np.allclose(magnitude.sum(axis=1), unit, atol=atol): + return None + remap: AxisRemap = [] + for column in reversed(range(n)): + row = int(magnitude[:, column].argmax()) + remap.append((n - 1 - row, bool(linear[row, column] < 0))) + return remap + + +def remap_shape(shape: list[int], remap: AxisRemap) -> list[int]: + """The spatial extents a remap lands on: output axis ``k`` carries the extent of the axis it + reads. What a permutation preserves is the volume, not which axis holds an extent.""" + return [int(shape[source]) for source, _ in remap] + + +def remap_region(target_slices: tuple[slice, ...], source_shape: list[int], remap: AxisRemap) -> list[slice]: + """The source region a target region reads under a remap. + + Output axis ``k``'s slice lands on the source axis it reads; a MIRRORED axis reads the mirror + region ``[n - stop, n - start)`` of it, because a flip restricted to a contiguous region is that + region reversed. The remap covers every axis exactly once, so every source axis is assigned. + """ + source_slices: list[slice] = [slice(0, int(extent)) for extent in source_shape] + for target, (source, mirrored) in zip(target_slices, remap, strict=True): + extent = int(source_shape[source]) + source_slices[source] = ( + slice(extent - target.stop, extent - target.start) if mirrored else slice(target.start, target.stop) + ) + return source_slices + + +def invert_remap(remap: AxisRemap) -> AxisRemap: + """The remap undoing ``remap``: source axis ``s`` reads back the output axis that carried it, + mirrored exactly where the forward read was.""" + inverted: AxisRemap = [(0, False)] * len(remap) + for axis, (source, mirrored) in enumerate(remap): + inverted[source] = (axis, mirrored) + return inverted + + +def apply_remap(tensor: torch.Tensor, remap: AxisRemap) -> torch.Tensor: + """The remap, materialised on a tensor whose trailing axes are the spatial ones. + + Leading axes (channel, and anything before it) are left in place. ``flip`` materialises the + permuted view even for an empty mirror list, so the result never aliases the tensor it was + read from: a remapped copy may be handed on while the source tensor lives its own life. + """ + offset = tensor.dim() - len(remap) + dims = list(range(offset)) + [offset + source for source, _ in remap] + flips = [offset + axis for axis, (_, mirrored) in enumerate(remap) if mirrored] + return tensor.permute(dims).flip(flips) + + @dataclass(frozen=True) class TransformBound: """What a stored transform is guaranteed to do: an exact affine part and a bounded interval. diff --git a/konfai/data/transform/base.py b/konfai/data/transform/base.py index 0862b979..4d27c350 100644 --- a/konfai/data/transform/base.py +++ b/konfai/data/transform/base.py @@ -18,6 +18,7 @@ """The transform contract: locality, regions, the base classes and the loader.""" import importlib +import warnings from abc import ABC, abstractmethod from collections.abc import Iterable, Sequence from dataclasses import dataclass, field @@ -159,7 +160,37 @@ def stat_seed_valid(upstream: Iterable[PatchLocality]) -> bool: class Transform(NeedDevice, ABC): - """Base class for transforms operating on tensors and cached attributes.""" + """Base class for transforms operating on tensors and cached attributes. + + The contract is tiered, and every default is fail-safe, so a stage owes only what its behaviour + actually needs: + + - **Tier 0 — correct**: implement ``__call__`` alone. The stage runs on the whole volume + (the default declaration is ``WHOLE_VOLUME``), keeps its shape and channels, and nothing + silently breaks. + - **Tier 1 — streaming**: set the :attr:`locality` class attribute (plus :attr:`halo` for a + bounded neighbourhood), and override :meth:`transform_shape` / :meth:`output_channels` only + if the stage changes the spatial shape or the channel count. A per-voxel value map is one + attribute away from streaming. + - **Tier 2 — streaming-aware**: the method overrides, needed only where the answer depends on + the case (:meth:`patch_locality` read off the header) or where the stage owns a region's + geometry or reads beside it (:meth:`stream_region_source`, :meth:`stream_region`, + :meth:`plan_region_reads`, :meth:`stream_slab`, :meth:`write_stream_cache_attribute`). + """ + + #: Tier-1 declaration: the one :class:`LocalityKind` this stage's contract is, when it is + #: unconditional. The base :meth:`patch_locality` answers from it; ``None`` (the default) keeps + #: the fail-safe ``WHOLE_VOLUME``. A declaration that depends on the configuration or the case + #: overrides the method instead, as does one carrying ``stat_keys`` or a ``reason``. + locality: LocalityKind | None = None + + #: Tier-1 companion to a ``HALO`` :attr:`locality`: the per-spatial-axis radius in array order + #: (a length-1 tuple broadcasts to every axis), exactly as :class:`PatchLocality` carries it. + halo: tuple[int, ...] = () + + #: The loader's resolution sentence for a bare name both stage namespaces define, surfaced as + #: the default :meth:`plan_note`; ``None`` for the unambiguous rest. + _ambiguous_name_note: str | None = None #: What ``__call__`` allocates ON TOP of its input and its output, in volumes-worth of the case. #: Every sizing route reads it: the sweep prices a region with it, a reduction charges the member @@ -247,8 +278,9 @@ def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: decides (a reorientation that is only a flip when the direction cosines are axis-aligned, a resample whose halo is the case's own scale) can still declare it up front. - The default ``WHOLE_VOLUME`` is the safety net: any transform (including third-party custom - ones) that does not override this falls to the whole-volume path, so nothing silently breaks. + The base answers from the :attr:`locality` attribute where one is set; otherwise the + default ``WHOLE_VOLUME`` is the safety net: any transform (including third-party custom + ones) that declares nothing falls to the whole-volume path, so nothing silently breaks. An override is bound by three rules: @@ -263,6 +295,8 @@ def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: with an empty ``Attribute``, and a group carries only what its writer stored, so a missing key must return ``WHOLE_VOLUME``, never raise. """ + if self.locality is not None: + return PatchLocality(self.locality, halo=self.halo) return PatchLocality(LocalityKind.WHOLE_VOLUME) def stream_region_source( @@ -327,10 +361,11 @@ def plan_note(self, group_dest: str, name: str, shape: list[int], cache_attribut once, so a note about the STAGE may repeat per case without repeating on the page, while a note about the CASE stays one line each. - The base holds nothing: most stages have nothing to add to their regime and their bytes. + The base carries only what the loader recorded: the resolution sentence for a bare name + both stage namespaces define, so the plan says which class actually ran. """ del group_dest, name, shape, cache_attribute - return None + return self._ambiguous_name_note def stream_abort(self, name: str) -> None: """Drop whatever ``stream_slab`` holds open for ``name`` after a mid-case failure. @@ -499,6 +534,11 @@ def get_transform(self, classpath: str, konfai_args: str, prefer_augmentation: b if not prefer_augmentation: first, second = second, first module, name = get_module(classpath, first) + ambiguity: str | None = None + if ":" not in classpath and hasattr(module, name): + ambiguity = self._ambiguity_sentence(name, first, second, prefer_augmentation) + if ambiguity is not None: + warnings.warn(ambiguity, stacklevel=2) if not hasattr(module, name) and ":" not in classpath: module, name = get_module(classpath, second) if not hasattr(module, name): @@ -524,6 +564,9 @@ def get_transform(self, classpath: str, konfai_args: str, prefer_augmentation: b subtree = f"{konfai_args}.{_escape_key_component(classpath)}" transform = apply_config(subtree)(factory)() if isinstance(transform, Transform): + if ambiguity is not None: + # Surfaced again as the stage's plan_note, so the TRANSFORM plan records which class ran. + transform._ambiguous_name_note = ambiguity transform.prepare(subtree) return transform if _is_augmentation(transform): @@ -533,6 +576,23 @@ def get_transform(self, classpath: str, konfai_args: str, prefer_augmentation: b return transform return Foreign(transform, classpath) + @staticmethod + def _ambiguity_sentence(name: str, winner: str, loser: str, prefer_augmentation: bool) -> str | None: + """One sentence naming what a bare name resolved to and the qualified spelling of the loser, + when both stage namespaces define it (Flip, Mask, Permute, Foreign): adding or removing an + Expand above such a name silently swaps a deterministic transform for a per-copy draw, and + with default arguments neither the binder nor strict_config would say so.""" + if not hasattr(importlib.import_module(loser), name): + return None + if prefer_augmentation: + marker = "past an Expand marker, a bare name is the copies' draw" + else: + marker = "before any Expand marker, a bare name is the transform" + return ( + f"'{name}' resolved to {winner}.{name} ({marker});" + f" spell '{loser}:{name}' for the {loser.rsplit('.', 1)[-1]}." + ) + @staticmethod def _closest_stage_name(name: str) -> str: """A 'did you mean' over BOTH stage namespaces, so the suggestion is never Python's own diff --git a/konfai/data/transform/ensemble.py b/konfai/data/transform/ensemble.py index cbba6de5..aaf02a01 100644 --- a/konfai/data/transform/ensemble.py +++ b/konfai/data/transform/ensemble.py @@ -22,7 +22,7 @@ import numpy as np import torch -from konfai.data.transform.base import LocalityKind, PatchLocality, Transform +from konfai.data.transform.base import LocalityKind, Transform from konfai.utils.dataset import Attribute, Dataset, DataStream from konfai.utils.utils import split_path_spec @@ -34,12 +34,11 @@ class _MemberSpread(Transform): working_multiple = 2.0 _spread: Callable[[torch.Tensor, int], torch.Tensor] + locality = LocalityKind.POINTWISE + def __init__(self) -> None: super().__init__() - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - return PatchLocality(LocalityKind.POINTWISE) - def __call__(self, name: str, tensors: torch.Tensor, cache_attribute: Attribute) -> torch.Tensor: # The member axis stays in both branches: var/std drop it and unsqueeze re-adds it. if tensors.shape[0] > 1: @@ -59,16 +58,15 @@ class SegmentationDisagreement(Transform): # What it holds beyond its input and its output: the pairwise comparison over the model axis: measured 9.33 on the CUDA allocator. working_multiple = 24.5 + # Per-voxel majority disagreement across the members. The global torch.unique only widens the + # label set with labels absent at a given voxel, which contribute zero counts there and never + # change that voxel's majority, so the result is decided voxel by voxel. + locality = LocalityKind.POINTWISE + def __init__(self, ignore_background: bool = False) -> None: super().__init__() self.ignore_background = ignore_background - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - # Per-voxel majority disagreement across the members. The global torch.unique only widens the - # label set with labels absent at a given voxel, which contribute zero counts there and never - # change that voxel's majority, so the result is decided voxel by voxel. - return PatchLocality(LocalityKind.POINTWISE) - def __call__(self, name: str, tensors: torch.Tensor, cache_attribute: Attribute) -> torch.Tensor: # tensors shape: [N, ...] with N segmentations and integer labels per voxel if tensors.shape[0] <= 1: @@ -103,13 +101,12 @@ class Percentage(Transform): # What it holds beyond its input and its output: the quantile's own copy: measured 1.00 on the CUDA allocator. working_multiple = 1.0 + locality = LocalityKind.POINTWISE + def __init__(self, baseline: float) -> None: super().__init__() self.baseline = baseline - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - return PatchLocality(LocalityKind.POINTWISE) - def __call__(self, name: str, tensors: torch.Tensor, cache_attribute: Attribute) -> torch.Tensor: return tensors / self.baseline * 100.0 @@ -126,12 +123,11 @@ class Magnitude(Transform): # Measured at 1.00 on the CUDA allocator, in volumes-worth of what it is handed. working_multiple = 1.0 + locality = LocalityKind.POINTWISE + def __init__(self) -> None: super().__init__() - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - return PatchLocality(LocalityKind.POINTWISE) - def __call__(self, name: str, tensors: torch.Tensor, cache_attribute: Attribute) -> torch.Tensor: return torch.linalg.norm(tensors.float(), dim=0, keepdim=True) @@ -184,11 +180,10 @@ def __init__(self, dataset: str, name: str, mode: str = "mean"): self._stack_sinks: dict[str, DataStream] = {} self._stack_buffers: dict[str, list[np.ndarray]] = {} - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - # The member reduction is per-voxel; the per-member stack write is the side effect that needs - # the slab's place in the volume, which is exactly what SLAB declares (whole-volume on the - # read side, streamed region by region on the write side via ``stream_slab``). - return PatchLocality(LocalityKind.SLAB) + # The member reduction is per-voxel; the per-member stack write is the side effect that needs + # the slab's place in the volume, which is exactly what SLAB declares (whole-volume on the + # read side, streamed region by region on the write side via ``stream_slab``). + locality = LocalityKind.SLAB def _stack(self, tensors: torch.Tensor) -> np.ndarray: if self.mode == "Seg": diff --git a/konfai/data/transform/intensity.py b/konfai/data/transform/intensity.py index c1af4648..8852a40d 100644 --- a/konfai/data/transform/intensity.py +++ b/konfai/data/transform/intensity.py @@ -21,8 +21,9 @@ import torch from konfai.data.transform.base import LocalityKind, PatchLocality, Transform, TransformInverse, sitk -from konfai.utils.dataset import Attribute, data_to_image, image_to_data -from konfai.utils.errors import DatasetManagerError +from konfai.utils.dataset import Attribute, Dataset, data_to_image, image_to_data +from konfai.utils.dataset.statistics import read_masked_data_statistics +from konfai.utils.errors import DatasetManagerError, TransformError from konfai.utils.ITK import _require_simpleitk @@ -37,6 +38,57 @@ def _seeded_scalar(cache_attribute: Attribute, key: str) -> float: return float(cache_attribute.get_tensor(key).reshape(-1)[0]) +def _dataset_holding(datasets: list[Dataset], group: str, name: str) -> Dataset: + """The dataset holding the case's ``group``, or a refusal naming it.""" + for dataset in datasets: + if dataset.is_dataset_exist(group, name): + return dataset + raise DatasetManagerError( + f"No dataset holds '{group}' for case '{name}'.", + "Check the group name against the datasets the run reads.", + ) + + +class _MaskedStatisticsSeed: + """The masked whole-volume statistics of a stage's own group, per case, from the stores. + + A masked ``Clip``/``Standardize`` needs the CASE's statistic under the mask before its first + region, and a region cannot derive it: the two volumes are scanned once per case, streamed + (:func:`read_masked_data_statistics`), and memoised here. The group the chain reads is the one + thing ``__call__`` is never told, so ``transform_shape`` records it: every plan folds it before + a region flows. The mask is assumed to sit on the volume's own grid, as :class:`~konfai.data. + transform.Mask` assumes; the scan refuses a mask whose extent is not the volume's. + """ + + def __init__(self, mask: str) -> None: + self.mask = mask + self.group: str | None = None + self._by_case: dict[str, dict[str, float]] = {} + + def record_group(self, group_src: str) -> None: + if group_src: + self.group = group_src + + def statistics(self, datasets: list[Dataset], name: str) -> dict[str, float]: + cached = self._by_case.get(name) + if cached is not None: + return cached + if self.group is None: + raise TransformError( + "The masked statistic has no group to scan: the chain was never planned.", + "Report this: transform_shape() records the group before any region flows.", + ) + stats = read_masked_data_statistics( + _dataset_holding(datasets, self.group, name), + self.group, + _dataset_holding(datasets, self.mask, name), + self.mask, + name, + ) + self._by_case[name] = stats + return stats + + class Clip(Transform): """Clip tensor intensities to a fixed or data-dependent value range.""" @@ -61,16 +113,21 @@ def __init__( self.save_clip_min = save_clip_min self.save_clip_max = save_clip_max self.mask = mask + self._masked_seed = _MaskedStatisticsSeed(mask) if mask is not None else None + + def transform_shape(self, group_src: str, name: str, shape: list[int], cache_attribute: Attribute) -> list[int]: + # Identity on the shape; a masked bound records the group the chain reads, which the masked + # disk scan needs and __call__ is never told. + if self._masked_seed is not None: + self._masked_seed.record_group(group_src) + return shape def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - # A mask reads a separate full volume, and a percentile bound needs the whole histogram: - # both force a whole-volume load. A 'min'/'max' bound needs a global disk statistic - # (GLOBAL_STAT); fixed float bounds clip each voxel independently (POINTWISE). - if self.mask is not None: - return PatchLocality( - LocalityKind.WHOLE_VOLUME, - reason=f"the bounds are read under mask '{self.mask}', a second whole volume; drop the mask to stream", - ) + # A percentile bound needs the whole histogram (whole-volume). A 'min'/'max' bound needs a + # global statistic: a seeded disk one (GLOBAL_STAT with its key), or under a mask a masked + # disk scan the stage seeds itself (GLOBAL_STAT with no key: the dispatcher still guards + # the seed's validity and seeds nothing). Fixed float bounds never read the mask and clip + # each voxel independently (POINTWISE). stat_keys: set[str] = set() for bound, key in ((self.min_value, "Min"), (self.max_value, "Max")): if isinstance(bound, str): @@ -84,26 +141,49 @@ def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: ) if not stat_keys: return PatchLocality(LocalityKind.POINTWISE) + if self.mask is not None: + return PatchLocality(LocalityKind.GLOBAL_STAT) return PatchLocality(LocalityKind.GLOBAL_STAT, stat_keys=frozenset(stat_keys)) + def _masked_values(self, name: str, tensor: torch.Tensor) -> torch.Tensor: + """The tensor's values under the mask, on the whole-volume path (the reference).""" + mask = self.read_companion(self.mask, name) # type: ignore[arg-type] + if tuple(mask.shape) != tuple(tensor.shape): + raise TransformError( + f"The mask '{self.mask}' has shape {list(mask.shape)} where the tensor in hand has" + f" {list(tensor.shape)}: it cannot be indexed against a region.", + "A masked bound needs the whole volume here; report this if the chain was planned.", + ) + return tensor[mask == 1] + def __call__(self, name: str, tensor: torch.Tensor, cache_attribute: Attribute) -> torch.Tensor: - tensor_masked = tensor if self.mask is None else tensor[self.read_companion(self.mask, name) == 1] + seeded_masked = self.mask is not None and "StatisticsSeeded" in cache_attribute + selected: torch.Tensor | None = None + + def values() -> torch.Tensor: + nonlocal selected + if selected is None: + selected = tensor if self.mask is None else self._masked_values(name, tensor) + return selected if isinstance(self.min_value, str): if self.min_value == "min": - # Seeded-first, as Normalize reads it: on a streamed path the dispatcher has read - # the CASE's statistic from disk and the tensor in hand is one region of it -- - # computed here, the bound (and what save_clip_min records) would be the region's. - if self.mask is None and "StatisticsSeeded" in cache_attribute and "Min" in cache_attribute: + # Seeded-first, as Normalize reads it: on a streamed path the tensor in hand is one + # region of the case -- computed here, the bound (and what save_clip_min records) + # would be the region's. A masked bound seeds from the masked disk scan instead: a + # bare seed may be an unmasked stage's. + if seeded_masked: + min_value = self._masked_seed.statistics(self.datasets, name)["min"] # type: ignore[union-attr] + elif self.mask is None and "StatisticsSeeded" in cache_attribute and "Min" in cache_attribute: min_value = _seeded_scalar(cache_attribute, "Min") else: - min_value = torch.min(tensor_masked) + min_value = torch.min(values()) elif self.min_value.startswith("percentile:"): try: percentile = float(self.min_value.split(":")[1]) # ``np.percentile`` cannot coerce a CUDA tensor (finalize slots may hand Clip a # GPU-resident volume); ``.cpu()`` is a no-op view on a host tensor. - min_value = np.percentile(tensor_masked.detach().cpu(), percentile) + min_value = np.percentile(values().detach().cpu(), percentile) except (IndexError, ValueError) as exc: raise ValueError( f"Invalid format for min_value: '{self.min_value}'. Expected 'percentile:'" @@ -118,14 +198,16 @@ def __call__(self, name: str, tensor: torch.Tensor, cache_attribute: Attribute) if isinstance(self.max_value, str): if self.max_value == "max": - if self.mask is None and "StatisticsSeeded" in cache_attribute and "Max" in cache_attribute: + if seeded_masked: + max_value = self._masked_seed.statistics(self.datasets, name)["max"] # type: ignore[union-attr] + elif self.mask is None and "StatisticsSeeded" in cache_attribute and "Max" in cache_attribute: max_value = _seeded_scalar(cache_attribute, "Max") else: - max_value = torch.max(tensor_masked) + max_value = torch.max(values()) elif self.max_value.startswith("percentile:"): try: percentile = float(self.max_value.split(":")[1]) - max_value = np.percentile(tensor_masked.detach().cpu(), percentile) + max_value = np.percentile(values().detach().cpu(), percentile) except (IndexError, ValueError) as exc: raise ValueError( f"Invalid format for max_value: '{self.max_value}'. Expected 'percentile:'" @@ -252,14 +334,13 @@ class UnNormalize(Transform): # CUDA allocator, the same as Standardize, whose arithmetic this is. working_multiple = 1.0 + locality = LocalityKind.POINTWISE + def __init__(self, min_value: int = -1024, max_value: int = 3071) -> None: super().__init__() self.min_value = min_value self.max_value = max_value - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - return PatchLocality(LocalityKind.POINTWISE) - def __call__(self, name: str, tensor: torch.Tensor, cache_attribute: Attribute) -> torch.Tensor: return (tensor + 1) / 2 * (self.max_value - self.min_value) + self.min_value @@ -282,17 +363,21 @@ def __init__( self.mean = mean self.std = std self.mask = mask + self._masked_seed = _MaskedStatisticsSeed(mask) if mask is not None else None + + def transform_shape(self, group_src: str, name: str, shape: list[int], cache_attribute: Attribute) -> list[int]: + # Identity on the shape; a masked statistic records the group the chain reads, which the + # masked disk scan needs and __call__ is never told. + if self._masked_seed is not None: + self._masked_seed.record_group(group_src) + return shape def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - # A mask reads a separate full volume (whole-volume). Any of mean/std left unset is taken from - # a volume-global disk statistic (GLOBAL_STAT); when both are given, the standardization is a - # per-voxel affine map with constant coefficients (POINTWISE). - if self.mask is not None: - return PatchLocality( - LocalityKind.WHOLE_VOLUME, - reason=f"the statistics are taken under mask '{self.mask}', a second whole volume;" - " drop the mask to stream", - ) + # Any of mean/std left unset is a global statistic: a seeded disk one (GLOBAL_STAT with its + # key), or under a mask a masked disk scan the stage seeds itself, once per case + # (GLOBAL_STAT with no key: the dispatcher still guards the seed's validity and seeds + # nothing). Once seeded, no region reads the mask: the map is a per-voxel affine. With both + # coefficients given, the mask selects nothing that is read (POINTWISE). stat_keys: set[str] = set() if self.mean is None: stat_keys.add("Mean") @@ -300,23 +385,57 @@ def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: stat_keys.add("Std") if not stat_keys: return PatchLocality(LocalityKind.POINTWISE) + if self.mask is not None: + return PatchLocality(LocalityKind.GLOBAL_STAT) return PatchLocality(LocalityKind.GLOBAL_STAT, stat_keys=frozenset(stat_keys)) + def _masked_values(self, name: str, tensor: torch.Tensor) -> torch.Tensor: + """The tensor's values under the mask, on the whole-volume path (the reference).""" + mask = self.read_companion(self.mask, name) # type: ignore[arg-type] + if tuple(mask.shape) != tuple(tensor.shape): + raise TransformError( + f"The mask '{self.mask}' has shape {list(mask.shape)} where the tensor in hand has" + f" {list(tensor.shape)}: it cannot be indexed against a region.", + "A masked statistic needs the whole volume here; report this if the chain was planned.", + ) + return tensor[mask == 1] + def __call__(self, name: str, tensor: torch.Tensor, cache_attribute: Attribute) -> torch.Tensor: - tensor_masked = tensor if self.mask is None else tensor[self.read_companion(self.mask, name) == 1] + if self.mask is not None and "StatisticsSeeded" in cache_attribute: + # A streamed region: the mask cannot be indexed against it, and a bare 'Mean' seed may + # be an unmasked stage's. The case's masked statistic is scanned from the stores once + # (memoised) and every region applies the same per-voxel affine map. + stats = self._masked_seed.statistics(self.datasets, name) # type: ignore[union-attr] + mean_value = torch.tensor(self.mean) if self.mean is not None else torch.tensor([float(stats["mean"])]) + std_value = torch.tensor(self.std) if self.std is not None else torch.tensor([float(stats["std"])]) + if "Mean" not in cache_attribute: + cache_attribute["Mean"] = mean_value + if "Std" not in cache_attribute: + cache_attribute["Std"] = std_value + if self.lazy: + return tensor + mean = self._broadcast(mean_value.to(tensor.device), tensor) + std = self._broadcast(std_value.to(tensor.device), tensor) + return (tensor - mean) / std + + selected: torch.Tensor | None = None + + def values() -> torch.Tensor: + nonlocal selected + if selected is None: + selected = tensor if self.mask is None else self._masked_values(name, tensor) + return selected if "Mean" not in cache_attribute: cache_attribute["Mean"] = ( - torch.tensor([torch.mean(tensor_masked.type(torch.float32))]) + torch.tensor([torch.mean(values().type(torch.float32))]) if self.mean is None else torch.tensor(self.mean) ) if "Std" not in cache_attribute: cache_attribute["Std"] = ( - torch.tensor([torch.std(tensor_masked.type(torch.float32))]) - if self.std is None - else torch.tensor(self.std) + torch.tensor([torch.std(values().type(torch.float32))]) if self.std is None else torch.tensor(self.std) ) if self.lazy: return tensor diff --git a/konfai/data/transform/labels.py b/konfai/data/transform/labels.py index 3ee0ceac..bf7c50cd 100644 --- a/konfai/data/transform/labels.py +++ b/konfai/data/transform/labels.py @@ -47,10 +47,9 @@ def __init__(self, path: str = "./default.mha", value_outside: int = 0) -> None: #: Cases whose stored mask was checked against the chain input's extent (once per case). self._aligned: set[str] = set() - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - # POINTWISE on the promise that the mask sits on the stage's input grid; a declaration may - # not do I/O, so the extent is checked at the point of use (stream_region), per case. - return PatchLocality(LocalityKind.POINTWISE) + # POINTWISE on the promise that the mask sits on the stage's input grid; a declaration may + # not do I/O, so the extent is checked at the point of use (stream_region), per case. + locality = LocalityKind.POINTWISE def _apply(self, tensor: torch.Tensor, mask: torch.Tensor | np.ndarray) -> torch.Tensor: # Index on the tensor's own device so the mask works whether the volume is on CPU or GPU @@ -264,9 +263,8 @@ class MergeLabels(Transform): # on the CUDA allocator, under a budget large enough not to clamp it. working_multiple = 3.75 - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - # Merges the leading model axis per voxel; spatial support is a single voxel. - return PatchLocality(LocalityKind.POINTWISE) + # Merges the leading model axis per voxel; spatial support is a single voxel. + locality = LocalityKind.POINTWISE def write_stream_cache_attribute( self, cache_attribute: Attribute, source_spatial_shape: list[int], name: str = "" @@ -340,13 +338,12 @@ class FlatLabel(Transform): # on the CUDA allocator, under a budget large enough not to clamp it. working_multiple = 1.0 + locality = LocalityKind.POINTWISE + def __init__(self, labels: list[int] | None = None) -> None: super().__init__() self.labels = labels - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - return PatchLocality(LocalityKind.POINTWISE) - def __call__(self, name: str, tensor: torch.Tensor, cache_attribute: Attribute) -> torch.Tensor: # Filled through the mask, not through the indices of what it selects: see Clip. data = torch.zeros_like(tensor) @@ -365,6 +362,8 @@ class SelectLabel(Transform): # on the CUDA allocator, under a budget large enough not to clamp it. working_multiple = 1.0 + locality = LocalityKind.POINTWISE + def __init__(self, labels: list[str]) -> None: super().__init__() try: @@ -375,9 +374,6 @@ def __init__(self, labels: list[str]) -> None: 'labels is a list of "(old,new)" strings: labels: ["(1,2)", "(3,1)"].', ) from None - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - return PatchLocality(LocalityKind.POINTWISE) - def __call__(self, name: str, tensor: torch.Tensor, cache_attribute: Attribute) -> torch.Tensor: data = torch.zeros_like(tensor) for old_label, new_label in self.labels: @@ -394,9 +390,8 @@ def __init__(self, num_classes: int, inverse: bool = True) -> None: super().__init__(inverse) self.num_classes = num_classes - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - # Expands each voxel's scalar label into a one-hot channel vector (spatially pointwise). - return PatchLocality(LocalityKind.POINTWISE) + # Expands each voxel's scalar label into a one-hot channel vector (spatially pointwise). + locality = LocalityKind.POINTWISE def output_channels(self, channels: int) -> int: return self.num_classes * channels diff --git a/konfai/data/transform/resample.py b/konfai/data/transform/resample.py index 8dfe00fe..cf045dd0 100644 --- a/konfai/data/transform/resample.py +++ b/konfai/data/transform/resample.py @@ -479,7 +479,7 @@ def _stages_bytes(stages: SpatialStages) -> int: #: What one element of a decoded field weighs under the bit-exact walk: read as float64 whatever #: the store holds (:meth:`_DisplacementSource.read`), while the plan counts its volumes at -#: :data:`~konfai.data.patching._SWEEP_ELEMENT_BYTES`: the ratio is what a field window costs in +#: :data:`~konfai.data.patching.budget._SWEEP_ELEMENT_BYTES`: the ratio is what a field window costs in #: the currency the plan is written in. Under ``precision: fast`` the field is held in float32 and #: weighs half (:meth:`Resample._field_element_bytes`). _FIELD_ELEMENT_BYTES = 8 @@ -1069,18 +1069,6 @@ def _stages(self, name: str, region: Grid) -> SpatialStages: stages.extend(self._stored_stages(name, box)) return tuple(stages) - def _bound(self, name: str) -> TransformBound: - """What the map is guaranteed to do, from stored coefficients alone, no voxel read.""" - rank = self._source_grid(name).rank - folded = TransformBound.exact(AffineMap.identity(rank)) - if self.displacement is not None or (self.transforms is not None and self._stored_map(name).field): - raise TransformError( - "a field's reach is unknown before its values are read; nothing bounds it from headers." - ) - if self.transforms is not None: - folded = self._stored_map(name).bound.after(folded) - return folded - def _pricing_bound(self, name: str) -> TransformBound: """The map's bound as the PLAN prices it: headers and declarations, never a voxel. @@ -1255,7 +1243,7 @@ def case_working_multiple(self, name: str) -> float: # purpose so an externally written double field is not quantized before the exact # arithmetic), so each of its components weighs two of the plan's volumes, not one. # Charging it at one was counting eight bytes as four. - from konfai.data.patching import _SWEEP_ELEMENT_BYTES + from konfai.data.patching.budget import _SWEEP_ELEMENT_BYTES widening = self._field_element_bytes / _SWEEP_ELEMENT_BYTES window = max(1, int(shape[0])) * (target_voxel / field_voxel) * widening diff --git a/konfai/data/transform/shape.py b/konfai/data/transform/shape.py index 7700f2d8..716d4621 100644 --- a/konfai/data/transform/shape.py +++ b/konfai/data/transform/shape.py @@ -24,7 +24,14 @@ import torch.nn.functional as F from konfai.data.geometry import ( + SIGNED_PERMUTATION_ATOL_FLOAT64, + AxisRemap, Grid, + apply_remap, + invert_remap, + remap_region, + remap_shape, + signed_permutation, ) from konfai.data.transform.base import LocalityKind, PatchLocality, RegionContext, Transform, TransformInverse from konfai.utils.dataset import Attribute, Dataset @@ -43,6 +50,8 @@ class Padding(TransformInverse): # Measured at 0.00 on the CUDA allocator: it holds nothing beyond what it is handed. working_multiple = 0.0 + locality = LocalityKind.REGRID + def __init__(self, padding: list[int] = [0, 0, 0, 0, 0, 0], mode: str = "constant", inverse: bool = True) -> None: super().__init__(inverse) self.padding = padding @@ -76,9 +85,6 @@ def __call__(self, name: str, tensor: torch.Tensor, cache_attribute: Attribute) def transform_shape(self, group_src: str, name: str, shape: list[int], cache_attribute: Attribute) -> list[int]: return [extent + before + after for extent, (before, after) in zip(shape, self._pairs(len(shape)), strict=True)] - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - return PatchLocality(LocalityKind.REGRID) - def write_stream_cache_attribute( self, cache_attribute: Attribute, source_spatial_shape: list[int], name: str = "" ) -> None: @@ -328,6 +334,8 @@ class Permute(TransformInverse): # Measured at 0.00 on the CUDA allocator: it holds nothing beyond what it is handed. working_multiple = 0.0 + locality = LocalityKind.ORIENTATION + def __init__(self, dims: str = "1|0|2", inverse: bool = True) -> None: super().__init__(inverse) try: @@ -338,11 +346,13 @@ def __init__(self, dims: str = "1|0|2", inverse: bool = True) -> None: "dims is the new spatial axis order as a '|'-separated string: dims: \"1|0|2\" (not a list).", ) from None - def transform_shape(self, group_src: str, name: str, shape: list[int], cache_attribute: Attribute) -> list[int]: - return [shape[it - 1] for it in self.dims[1:]] + def _remap(self) -> AxisRemap: + # Output spatial axis k reads input axis ``self.dims[k + 1] - 1`` (self.dims is + # channel-inclusive), never mirrored. + return [(d - 1, False) for d in self.dims[1:]] - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - return PatchLocality(LocalityKind.ORIENTATION) + def transform_shape(self, group_src: str, name: str, shape: list[int], cache_attribute: Attribute) -> list[int]: + return remap_shape(shape, self._remap()) def stream_region_source( self, @@ -351,19 +361,10 @@ def stream_region_source( source_spatial_shape: list[int], cache_attribute: Attribute, ) -> list[slice]: - # Output spatial axis k comes from input axis ``self.dims[k + 1] - 1`` (self.dims is - # channel-inclusive). Placing each target slice at its source axis yields the source region - # whose permutation reproduces the target patch exactly. - source_slices = [slice(0, n) for n in source_spatial_shape] - for k, sl in enumerate(target_slices): - source_slices[self.dims[k + 1] - 1] = slice(sl.start, sl.stop) - return source_slices + return remap_region(target_slices, source_spatial_shape, self._remap()) def inverse_transform_shape(self, shape: list[int], cache_attribute: Attribute) -> list[int]: - result = list(shape) - for k, d in enumerate(self.dims[1:]): - result[d - 1] = shape[k] - return result + return remap_shape(shape, invert_remap(self._remap())) def stream_region_target( self, @@ -372,9 +373,9 @@ def stream_region_target( source_spatial_shape: list[int], cache_attribute: Attribute, ) -> list[slice]: - # Input axis k carries output axis ``dims[k + 1] - 1``: a written region pulls, per input axis, - # the slice of the output axis it came from. - return [slice(target_slices[d - 1].start, target_slices[d - 1].stop) for d in self.dims[1:]] + # The write mirror pulls through the inverse remap: input axis k carries the output axis + # that read it. + return remap_region(target_slices, source_spatial_shape, invert_remap(self._remap())) def __call__(self, name: str, tensor: torch.Tensor, cache_attribute: Attribute) -> torch.Tensor: return tensor.permute(tuple(self.dims)) @@ -387,14 +388,13 @@ class Flip(TransformInverse): # Measured at 0.00 on the CUDA allocator: it holds nothing beyond what it is handed. working_multiple = 0.0 + locality = LocalityKind.ORIENTATION + def __init__(self, dims: str = "1|0|2", inverse: bool = True) -> None: super().__init__(inverse) self.dims = [int(d) + 1 for d in str(dims).split("|")] - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - return PatchLocality(LocalityKind.ORIENTATION) - def stream_region_source( self, name: str, @@ -402,16 +402,9 @@ def stream_region_source( source_spatial_shape: list[int], cache_attribute: Attribute, ) -> list[slice]: - # A flipped spatial axis reads the mirror region ``[n - stop, n - start)``; applying the flip - # to that sub-region reproduces the target patch. Non-flipped axes read the identity region. - source_slices: list[slice] = [] - for k, sl in enumerate(target_slices): - n = source_spatial_shape[k] - if (k + 1) in self.dims: - source_slices.append(slice(n - sl.stop, n - sl.start)) - else: - source_slices.append(slice(sl.start, sl.stop)) - return source_slices + # A mirror moves no axis: the remap is the identity permutation, mirrored on the flipped axes. + remap: AxisRemap = [(k, (k + 1) in self.dims) for k in range(len(target_slices))] + return remap_region(target_slices, source_spatial_shape, remap) def stream_region_target( self, @@ -444,10 +437,6 @@ class Canonical(TransformInverse): onto the reoriented shape. """ - # An orthonormal direction's entries are exactly 0 or +/-1 when it is axis-aligned, but the - # reorientation is a product with an inverse, so it lands within a few double ulps of them. - _AXIS_ALIGNED_ATOL = 1e-9 - working_multiple = 3.0 # an oblique case is resampled: the resample's own figure def __init__(self, inverse: bool = True) -> None: @@ -464,31 +453,7 @@ def _reorientation(self, cache_attribute: Attribute) -> torch.Tensor: initial_matrix = cache_attribute.get_tensor("Direction").reshape(3, 3).to(torch.double) return initial_matrix.inverse() @ self.canonical_direction - @classmethod - def _index_remap(cls, reorientation: torch.Tensor) -> list[tuple[int, bool]] | None: - """Per output SPATIAL axis, the source axis it reads and whether it reads it mirrored. - - ``reorientation`` maps an output coordinate onto the input it comes from, so it is an exact - remap exactly when it is a signed permutation: output physical axis ``c`` then reads input - physical axis ``r``, backwards where the sign is negative. Anything else mixes axes. Axes are - returned in array order, where physical axis k is array axis ``n - 1 - k``. The test (every - column of L1 norm 1 with peak 1) admits exactly the signed permutations: unit column sums - alone would also pass an axis-averaging matrix. - """ - n = reorientation.shape[0] - unit = torch.ones(n, dtype=reorientation.dtype) - columns = reorientation.abs() - if not torch.allclose(columns.sum(0), unit, atol=cls._AXIS_ALIGNED_ATOL): - return None - if not torch.allclose(columns.amax(0), unit, atol=cls._AXIS_ALIGNED_ATOL): - return None - remap = [] - for c in reversed(range(n)): - r = int(columns[:, c].argmax()) - remap.append((n - 1 - r, bool(reorientation[r, c] < 0))) - return remap - - def _orthogonal_remap(self, cache_attribute: Attribute) -> list[tuple[int, bool]] | None: + def _orthogonal_remap(self, cache_attribute: Attribute) -> AxisRemap | None: """The exact index remap this case's reorientation is, or ``None`` where it is not one. Total: a case whose header carries no usable direction cosines has no remap to make, and an @@ -497,7 +462,7 @@ def _orthogonal_remap(self, cache_attribute: Attribute) -> list[tuple[int, bool] """ if "Direction" not in cache_attribute or cache_attribute.get_np_array("Direction").size != 9: return None - return Canonical._index_remap(self._reorientation(cache_attribute)) + return signed_permutation(self._reorientation(cache_attribute), SIGNED_PERMUTATION_ATOL_FLOAT64) @staticmethod def _carried(per_physical_axis: torch.Tensor, remap: list[tuple[int, bool]] | None) -> torch.Tensor: @@ -566,7 +531,7 @@ def transform_shape(self, group_src: str, name: str, shape: list[int], cache_att remap = self._orthogonal_remap(cache_attribute) if remap is None: return shape - return [shape[source] for source, _ in remap] + return remap_shape(shape, remap) def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: # Only the case can say which reorientation this is, so only the header can answer. An orthogonal @@ -587,23 +552,13 @@ def stream_region_source( source_spatial_shape: list[int], cache_attribute: Attribute, ) -> list[slice]: - # Target axis k reads source axis ``source``, so the target slice IS the source's: taken at the - # far end ``[n - stop, n - start)`` where the remap reads that axis backwards. Flipping the region - # read reproduces the patch: a flip restricted to a contiguous region is that region reversed. - # Both the slices and the remap are in array order, and the remap covers every axis exactly once. remap = self._orthogonal_remap(cache_attribute) if remap is None: raise TransformError( "Canonical declared a region patch-locality for a direction it cannot remap exactly.", "Report this: patch_locality() and stream_region_source() disagree about the case.", ) - source_slices = [slice(None)] * len(remap) - for target, (source, mirrored) in zip(target_slices, remap, strict=False): - extent = source_spatial_shape[source] - source_slices[source] = ( - slice(extent - target.stop, extent - target.start) if mirrored else slice(target.start, target.stop) - ) - return source_slices + return remap_region(target_slices, source_spatial_shape, remap) def write_stream_cache_attribute( self, cache_attribute: Attribute, source_spatial_shape: list[int], name: str = "" @@ -627,7 +582,7 @@ def write_stream_cache_attribute( center = initial_matrix @ half_extent + initial_origin cache_attribute["Origin"] = center - self.canonical_direction @ Canonical._carried(half_extent, remap) - def _inverse_remap(self, cache_attribute: Attribute) -> list[tuple[int, bool]] | None: + def _inverse_remap(self, cache_attribute: Attribute) -> AxisRemap | None: """The forward remap judged on the state ``inverse`` runs from: the popped-to source direction. The inverse pops the canonical geometry and reorients back through the SOURCE direction under @@ -656,10 +611,7 @@ def inverse_transform_shape(self, shape: list[int], cache_attribute: Attribute) remap = self._inverse_remap(cache_attribute) if remap is None: return shape - result = list(shape) - for k, (source, _) in enumerate(remap): - result[source] = shape[k] - return result + return remap_shape(shape, invert_remap(remap)) def stream_region_target( self, @@ -668,23 +620,15 @@ def stream_region_target( source_spatial_shape: list[int], cache_attribute: Attribute, ) -> list[slice]: - # Canonical axis k holds source axis ``source``'s content: a written region pulls, per input - # axis, the slice of the output axis it carries: taken mirrored within the input extent where - # the remap reads that axis backwards (a flip restricted to a region is that region reversed). + # Canonical axis k holds source axis ``source``'s content: a written region pulls through + # the inverse remap, per input axis, the slice of the output axis it carries. remap = self._inverse_remap(cache_attribute) if remap is None: raise TransformError( "Canonical declared a region inverse patch-locality for a direction it cannot remap exactly.", "Report this: inverse_patch_locality() and stream_region_target() disagree about the case.", ) - source_slices: list[slice] = [] - for k, (source, mirrored) in enumerate(remap): - target = target_slices[source] - extent = source_spatial_shape[k] - source_slices.append( - slice(extent - target.stop, extent - target.start) if mirrored else slice(target.start, target.stop) - ) - return source_slices + return remap_region(target_slices, source_spatial_shape, invert_remap(remap)) def _reorient(self, tensor: torch.Tensor, reorientation: torch.Tensor) -> torch.Tensor: """Apply a reorientation: an exact index remap where it is one, a resample where it is not. @@ -692,16 +636,11 @@ def _reorient(self, tensor: torch.Tensor, reorientation: torch.Tensor) -> torch. An orthogonal reorientation is a bijection on the voxels, so it must reproduce the input's multiset bit for bit, which only a permute and a flip do. """ - remap = Canonical._index_remap(reorientation) + remap = signed_permutation(reorientation, SIGNED_PERMUTATION_ATOL_FLOAT64) if remap is None: matrix = Canonical._affine_matrix(reorientation, torch.tensor([0, 0, 0])) return Canonical._resample_affine(tensor, matrix.unsqueeze(0)) - # The remap is spatial and the tensor is channel-first, so the channel axes lead it unpermuted. - offset = tensor.dim() - len(remap) - dims = list(range(offset)) + [offset + source for source, _ in remap] - flips = [offset + axis for axis, (_, mirrored) in enumerate(remap) if mirrored] - # flip materialises the permuted view, so the result never aliases the tensor it was read from. - return tensor.permute(dims).flip(flips) + return apply_remap(tensor, remap) def __call__(self, name: str, tensor: torch.Tensor, cache_attribute: Attribute) -> torch.Tensor: # Read the source geometry before recording the canonical one over it: the attribute stacks. @@ -724,15 +663,15 @@ class Gradient(Transform): #: destination IS the output). working_multiple = 5.0 + # First-difference gradient: each output voxel reads its immediate neighbour, a HALO of radius + # 1. The far-edge ConstantPad reproduces the whole-volume border once the halo clamps there. + locality = LocalityKind.HALO + halo = (1,) + def __init__(self, per_dim: bool = False): super().__init__() self.per_dim = per_dim - def patch_locality(self, cache_attribute: Attribute) -> PatchLocality: - # First-difference gradient: each output voxel reads its immediate neighbour, a HALO of radius - # 1. The far-edge ConstantPad reproduces the whole-volume border once the halo clamps there. - return PatchLocality(LocalityKind.HALO, halo=(1,)) - @staticmethod def _differences(image: torch.Tensor) -> torch.Tensor: """The first difference along each spatial axis, written where it belongs. diff --git a/konfai/utils/dataset/statistics.py b/konfai/utils/dataset/statistics.py index 5fe449a4..5417ce5e 100644 --- a/konfai/utils/dataset/statistics.py +++ b/konfai/utils/dataset/statistics.py @@ -27,6 +27,7 @@ import numpy as np from konfai.utils.budget import per_rank_budget_bytes +from konfai.utils.errors import DatasetManagerError #: Elements a block of ``Dataset.iter_data_blocks`` holds when no budget was declared: the read grain #: of a scan (the statistics fold, the quantile scan), whatever the backend. @@ -336,6 +337,65 @@ def _empty_statistics_state(channels: int) -> dict[str, Any]: } +def read_masked_data_statistics( + source: Any, + group: str, + mask_source: Any, + mask_group: str, + name: str, +) -> dict[str, Any]: + """Min/max/mean/std of one entry over the voxels where the mask entry equals 1, streamed. + + The masked twin of ``Dataset.read_data_statistics``: both volumes are walked slab by slab along + the first spatial axis (the slab aligned to the volume's own read granularity where it states + one), and only the selected values enter the running fold, so neither volume is ever held. A + store that cannot serve bounded region reads is read whole ONCE and sliced in memory, exactly + as ``Dataset.iter_data_blocks`` serves such stores: a region read of them decodes the whole + volume anyway, so reading per slab would hold the same peak once per slab. + + The mask must sit on the volume's own grid (same spatial extent): a mask elsewhere would select + from the wrong place and the statistics would look right. The channel counts must agree too, + since selection is ``volume[mask == 1]``. ``source`` and ``mask_source`` are Datasets (duck + typed: this module is below the Dataset class). + """ + shape, _ = source.get_infos(group, name) + mask_shape, _ = mask_source.get_infos(mask_group, name) + if list(shape) != list(mask_shape): + raise DatasetManagerError( + f"'{name}': the mask '{mask_group}' has shape {list(mask_shape)} where '{group}' has {list(shape)}.", + "A masked statistic reads the two volumes voxel by voxel: store the mask on the" + " volume's own grid (same extent, same channels).", + ) + + def slab_reader(dataset: Any, entry_group: str) -> Callable[[tuple[slice, ...]], np.ndarray]: + if len(shape) >= 2 and dataset.bounded_region_reads(entry_group, name): + return lambda slices: dataset.read_data_slice(entry_group, name, slices)[0] + resident = dataset.read_data(entry_group, name)[0] + return lambda slices: resident[slices] + + read_volume = slab_reader(source, group) + read_mask = slab_reader(mask_source, mask_group) + rows = _statistics_chunk_length(shape, 1, _statistics_block_elements()) if len(shape) >= 2 else 1 + granularity = source.read_granularity(group, name) if len(shape) >= 2 else None + if granularity is not None and len(granularity) > 1: + block = max(1, int(granularity[1])) + rows = max(block, rows // block * block) + extent = int(shape[1]) if len(shape) >= 2 else 1 + state: dict[str, Any] | None = None + for start in range(0, extent, rows): + slices: tuple[slice, ...] = ( + slice(None), + slice(start, min(extent, start + rows)), + *(slice(None) for _ in shape[2:]), + ) + if len(shape) < 2: + slices = (slice(None),) + selected = read_volume(slices)[read_mask(slices) == 1] + if selected.size: + state = _update_running_statistics(state, selected.reshape(1, -1)) + return _finalize_running_statistics(state) + + def _finalize_running_statistics(state: dict[str, Any] | None) -> dict[str, Any]: """Convert a running-statistics state into the public stats dictionary. diff --git a/tests/unit/oracle_support.py b/tests/unit/oracle_support.py index 2ae329f2..419c138e 100644 --- a/tests/unit/oracle_support.py +++ b/tests/unit/oracle_support.py @@ -672,8 +672,17 @@ def augmentation_cases() -> dict[str, list[AugmentationCase]]: "Contrast": [AugmentationCase(augmentation_module.Contrast(0.5), LocalityKind.POINTWISE, True)], # The box is normalised to the volume; a region keeps its part of it. A wide box so it lands # in more than one patch of the fixture. - "CutOUT": [AugmentationCase(augmentation_module.CutOUT(1.0, 0.5, 0.0), LocalityKind.POINTWISE, True)], - "Elastix": [AugmentationCase(augmentation_module.Elastix(), LocalityKind.WHOLE_VOLUME, False)], + "CutOUT": [AugmentationCase(augmentation_module.CutOUT(0.5, 0.0), LocalityKind.POINTWISE, True)], + # A lattice draw with a bounded reach (|d| <= max_displacement by convexity): each region + # pulls its own widened box and the warp resamples it, so the copies stream as a REGRID. + "Elastix": [ + AugmentationCase( + augmentation_module.Elastix(grid_spacing=8, max_displacement=2), + LocalityKind.REGRID, + True, + atol=AUGMENTATION_ATOL, + ) + ], "Flip": [ AugmentationCase(FlipAugmentation(f_prob=[1.0, 1.0, 1.0]), LocalityKind.ORIENTATION, True), # A displacement field's flipped components are negated, which is not a bijection on values. diff --git a/tests/unit/test_augmentation.py b/tests/unit/test_augmentation.py index 48973bbb..979c40fc 100644 --- a/tests/unit/test_augmentation.py +++ b/tests/unit/test_augmentation.py @@ -47,7 +47,7 @@ def test_hue_axis_rotation_preserves_luma() -> None: # Hue rotation is a rotation of RGB about the luma axis: it must be identity at theta=0 and leave a # grey pixel unchanged for any angle (an Euler XYZ rotation about the coordinate axes would recolour it). - from konfai.data.augmentation import _axis_rotation_matrix + from konfai.data.augmentation.base import _axis_rotation_matrix v = torch.tensor([1.0, 1.0, 1.0]) / torch.sqrt(torch.tensor(3.0)) assert torch.allclose(_axis_rotation_matrix(torch.tensor(0.0), v), torch.eye(4), atol=1e-6) @@ -153,7 +153,7 @@ def test_intensity_augmentation_inverses_are_identity(): noise.who_index[0] = [0] assert torch.equal(noise.inverse(0, 0, x.clone()), x) - cutout = CutOUT(c_prob=1.0, cutout_size=2, value=0.0) + cutout = CutOUT(cutout_size=0.5, value=0.0) cutout.who_index[0] = [0] assert torch.equal(cutout.inverse(0, 0, x.clone()), x) @@ -345,7 +345,8 @@ def test_a_regrid_draw_pulls_the_hull_of_its_mapped_corners() -> None: """The pull box is the affine image of the target box (``WorldBox.image_under``): the hull of the ``2^n`` mapped corners, which is enumerated here the long way and must give the very same voxel window, for a rotation composed with a scale at free angles.""" - from konfai.data.augmentation import EulerTransform, _reflect_interval, _rotation_3d_matrix, _scale_matrix + from konfai.data.augmentation import EulerTransform + from konfai.data.augmentation.base import _reflect_interval, _rotation_3d_matrix, _scale_matrix class _Draw(EulerTransform): def _state_init(self, index, shapes, caches_attribute): @@ -429,10 +430,10 @@ def test_simpleitk_augmentations_fail_clearly_when_dependency_is_missing( ) -> None: monkeypatch.setattr("konfai.data.augmentation.base.sitk", None) - with pytest.raises(AugmentationError, match="SimpleITK"): - Elastix() with pytest.raises(AugmentationError, match="SimpleITK"): Mask("mask.mha", 0) + # Elastix evaluates its lattice with KonfAI's own kernel: no SimpleITK needed. + Elastix() def test_mask_reads_pixels_only_on_first_compute(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: @@ -493,7 +494,8 @@ def test_euler_source_coordinates_are_bitwise_the_chain_of_temporaries(seed: int """The fused, in-place coordinate build equals the step-by-step one bit for bit, on random rotations in 2D and 3D, over regions that stay interior (where the reflection is skipped) and regions that cross the border (where it runs), and on a singleton axis.""" - from konfai.data.augmentation import EulerTransform, _rotation_2d_matrix, _rotation_3d_matrix + from konfai.data.augmentation import EulerTransform + from konfai.data.augmentation.base import _rotation_2d_matrix, _rotation_3d_matrix generator = torch.Generator().manual_seed(seed) for full in ((9, 11, 13), (17, 15), (1, 9, 11)): @@ -518,7 +520,8 @@ def test_euler_source_coordinates_build_on_the_block_s_device() -> None: """Handed a device, the coordinates are built there rather than built on the host and moved. The float32 matmul then runs on that device: how far its last bit lands from the host's is measured below, and the interpolation it feeds is grid_sample's own tolerance.""" - from konfai.data.augmentation import EulerTransform, _rotation_3d_matrix + from konfai.data.augmentation import EulerTransform + from konfai.data.augmentation.base import _rotation_3d_matrix if not torch.cuda.is_available(): pytest.skip("no CUDA device") @@ -534,7 +537,7 @@ def test_euler_source_coordinates_build_on_the_block_s_device() -> None: def test_cutout_broadcasts_its_mask_over_the_channels() -> None: """One bool mask over the volume, broadcast over the channels: the same voxels are cut, in every channel, as when the mask was repeated per channel and re-tested against 1.""" - draw = CutOUT(1.0, 0.5, -7.0) + draw = CutOUT(0.5, -7.0) draw._state_init(0, [[6, 7, 8]], [Attribute()]) tensor = torch.rand((3, 6, 7, 8)) got = draw._apply(0, 0, tensor, (0, 0, 0), (6, 7, 8)) @@ -550,23 +553,129 @@ def test_cutout_broadcasts_its_mask_over_the_channels() -> None: assert bool((got == -7.0).any()) and bool((got == tensor).any()) -def test_elastix_keeps_one_sampling_grid_per_copy_and_drops_it_with_the_draw() -> None: - """The draw keeps grid_sample's sampling grid and nothing beside it: the float64 displacement - it was built from (24 bytes per voxel per copy, read by nothing) is gone, and reset_state - drops the copies' grids with the draw they belonged to.""" - pytest.importorskip("SimpleITK") +def test_cutout_fraction_binds_through_the_config_and_cuts_its_share( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """``cutout_size`` is a FRACTION and must survive the YAML binder as one: 0.34 through + apply_config cuts about ``0.34**rank`` of the volume (the ``int`` annotation once bound it to + 0, a silent no-op, while any integer erased the whole copy).""" + config = tmp_path / "Config.yml" + config.write_text( + "Trainer:\n Dataset:\n augmentations:\n A:\n nb: 1\n" + " data_augmentations:\n CutOUT:\n" + " cutout_size: 0.34\n value: 0.0\n" + ) + monkeypatch.setenv("KONFAI_config_file", str(config)) + monkeypatch.setenv("KONFAI_CONFIG_MODE", "Done") + monkeypatch.setenv("KONFAI_ROOT", "Trainer") + from konfai.data.augmentation import DataAugmentationsList + from konfai.utils.config import apply_config + + augmentations = apply_config("Trainer.Dataset.augmentations.A")(DataAugmentationsList)() + augmentations.prepare("A") + draw = augmentations.data_augmentations[0] + assert isinstance(draw, CutOUT) + assert draw.cutout_size == pytest.approx(0.34) + + shape = [24, 24, 24] + draw._state_init(0, [list(shape)], [Attribute()]) + draw.centers[0][0] = torch.tensor([0.5, 0.5, 0.5]) # an interior box: nothing clips at a border + cut = draw._apply(0, 0, torch.ones(1, *shape), (0, 0, 0), tuple(shape)) + fraction = float((cut == 0.0).float().mean()) + assert fraction == pytest.approx(0.34**3, rel=0.15) + + +def test_cutout_refuses_a_size_outside_the_unit_interval() -> None: + # An integer count of voxels is the value the old ``int`` annotation invited, and every one of + # them erased the whole copy: refused with the fractional semantics spelled out. + for size in (0.0, -0.2, 2, 16): + with pytest.raises(AugmentationError, match="cutout_size"): + CutOUT(cutout_size=size, value=0.0) + + +def test_elastix_resident_state_is_the_control_lattice_not_the_volume() -> None: + """A draw's only state is its control-point lattice: O(control points), never O(volume), and + reset_state drops the copies' lattices with the draw they belonged to.""" draw = Elastix(grid_spacing=8, max_displacement=4) - shapes = [[12, 12, 12] for _ in range(3)] + shapes = [[48, 48, 48] for _ in range(3)] draw._state_init(0, shapes, [Attribute() for _ in shapes]) - assert not hasattr(draw, "displacement_fields_true") - assert [tuple(grid.shape) for grid in draw.displacement_fields[0]] == [(1, 12, 12, 12, 3)] * 3 - kept = sum(grid.numel() * grid.element_size() for grid in draw.displacement_fields[0]) - assert kept == 3 * 12**3 * 3 * 4 + kept = sum(stage.values.nbytes for stage, _grid in draw.draws[0]) + # 48 voxels at unit spacing over a node every 8: 6 mesh cells -> 9 nodes per axis, 3 float64 + # components. The volume itself is 48**3 voxels: the lattice must not scale with it. + assert kept == 3 * (6 + 3) ** 3 * 3 * 8 + assert kept < 48**3 draw.reset_state(0) - assert 0 not in draw.displacement_fields + assert 0 not in draw.draws draw._state_init(1, shapes[:1], [Attribute()]) draw.reset_state() - assert not draw.displacement_fields + assert not draw.draws + + +def test_elastix_displacement_matches_simpleitk_to_double_rounding() -> None: + """The lazily-evaluated lattice is the SAME transform SimpleITK materialises: the equivalent + sitk.BSplineTransform (same domain, same lattice as its parameters) run through + TransformToDisplacementFieldFilter agrees at every voxel to double rounding.""" + sitk = pytest.importorskip("SimpleITK") + from konfai.data.sampling import _apply, _displacement_at + + torch.manual_seed(3) + shape = [10, 12, 14] + attributes = Attribute() + attributes["Origin"] = np.asarray([-3.0, 5.0, 11.0]) + attributes["Spacing"] = np.asarray([1.5, 1.75, 2.0]) + attributes["Direction"] = np.eye(3).reshape(-1) + draw = Elastix(grid_spacing=8, max_displacement=4) + draw._state_init(0, [list(shape)], [attributes]) + stage, grid = draw.draws[0][0] + + mesh_xyz = [int(nodes) - 3 for nodes in reversed(stage.grid.size_zyx)] + transform = sitk.BSplineTransform(3, 3) + domain_origin = grid.origin_xyz - grid.direction_xyz @ (0.5 * grid.spacing_xyz) + transform.SetTransformDomainOrigin([float(v) for v in domain_origin]) + transform.SetTransformDomainPhysicalDimensions( + [float(mesh * spacing) for mesh, spacing in zip(mesh_xyz, stage.grid.spacing_xyz, strict=True)] + ) + transform.SetTransformDomainMeshSize(mesh_xyz) + transform.SetTransformDomainDirection([float(v) for v in grid.direction_xyz.flatten()]) + transform.SetParameters([float(v) for v in stage.values.flatten()]) + + reference = sitk.Image([int(v) for v in reversed(shape)], sitk.sitkUInt8) + reference.SetOrigin([-3.0, 5.0, 11.0]) + reference.SetSpacing([1.5, 1.75, 2.0]) + materialise = sitk.TransformToDisplacementFieldFilter() + materialise.SetReferenceImage(reference) + want = sitk.GetArrayFromImage(materialise.Execute(transform)) + + axes = [torch.arange(0, extent, dtype=torch.float64) for extent in shape] + index_xyz = torch.stack(list(reversed(torch.meshgrid(*axes, indexing="ij"))), dim=-1) + world = _apply(index_xyz, grid.index_to_world, torch.device("cpu")) + got = _displacement_at(stage, world, torch.device("cpu")).numpy() + np.testing.assert_allclose(got, want, rtol=0, atol=1e-12) + + +def test_elastix_streams_each_region_of_a_copy() -> None: + """A region of the warped copy equals the whole-volume warp on that region: the draw declares + REGRID, its pull box covers the bounded reach (|d| <= max_displacement plus the far tap), and + the same absolute-coordinate arithmetic runs on both routes.""" + torch.manual_seed(1) + draw = Elastix(grid_spacing=8, max_displacement=4) + shape = [12, 12, 12] + draw._state_init(0, [list(shape)], [Attribute()]) + assert draw._patch_locality(0, 0, Attribute()).kind is LocalityKind.REGRID + volume = torch.rand(1, *shape) + whole = draw._compute("case", 0, 0, volume) + for target in [ + (slice(2, 8), slice(3, 9), slice(0, 6)), + (slice(0, 12), slice(0, 4), slice(8, 12)), + ]: + source = tuple(draw._stream_region_source(0, 0, target, list(shape))) + for part in source: + assert part.start >= 0 and part.stop <= 12 + block = volume[(slice(None), *source)] + from konfai.data.transform import RegionContext + + region = draw._stream_region("case", 0, 0, block, RegionContext(source, target, tuple(shape))) + torch.testing.assert_close(region, whole[(slice(None), *target)], rtol=0, atol=1e-5) def test_an_augmentation_group_is_handed_the_case_not_a_clone_of_it(tmp_path: Path) -> None: diff --git a/tests/unit/test_case_reduction.py b/tests/unit/test_case_reduction.py index cc1168e6..8110ffe1 100644 --- a/tests/unit/test_case_reduction.py +++ b/tests/unit/test_case_reduction.py @@ -27,7 +27,7 @@ from pathlib import Path from types import SimpleNamespace -import konfai.data.transform as transform_module +import konfai.data.transform.resample as resample_module import numpy as np import pytest import torch @@ -927,7 +927,7 @@ def test_a_field_resample_prices_the_field_window_its_case_actually_holds(tmp_pa # is read as float64 and the plan counts a volume at four bytes), materialised THREE times over # while ITK is handed it. The general walk this case also takes is NOT added: it slabs itself # against the declared budget. - expected = warp.working_multiple + 3.0 * 2.0 * transform_module._FIELD_WINDOW_COPIES + expected = warp.working_multiple + 3.0 * 2.0 * resample_module._FIELD_WINDOW_COPIES assert warp.case_working_multiple("CASE_000") == pytest.approx(expected) # A case this stage has never met answers the class's figure rather than guessing at a grid. assert warp.case_working_multiple("NEVER_SEEN") == warp.working_multiple diff --git a/tests/unit/test_dataset_streaming.py b/tests/unit/test_dataset_streaming.py index f36cfbf5..0c20db18 100644 --- a/tests/unit/test_dataset_streaming.py +++ b/tests/unit/test_dataset_streaming.py @@ -781,7 +781,7 @@ def test_streaming_still_seeds_a_global_stat_behind_a_reorientation(patch_manage @pytest.mark.parametrize( ("transform", "kind"), [ - (Standardize(mask="MASK"), LocalityKind.WHOLE_VOLUME), + (Clip(min_value="percentile:1", max_value="percentile:99"), LocalityKind.WHOLE_VOLUME), (Gradient(), LocalityKind.HALO), (Dilate(dilate=2), LocalityKind.HALO), (Flip(), LocalityKind.ORIENTATION), diff --git a/tests/unit/test_geometry.py b/tests/unit/test_geometry.py index c73b350d..e42d32f4 100644 --- a/tests/unit/test_geometry.py +++ b/tests/unit/test_geometry.py @@ -263,3 +263,84 @@ def test_the_stage_bounds_a_one_sided_field_by_where_it_reaches(self): np.testing.assert_array_equal(bound.high_xyz, np.zeros(3)) # The envelope is still there for whoever wants one number, and it is the old sup |v|. np.testing.assert_array_equal(stage.bound_xyz, np.abs(values).reshape(3, -1).max(axis=1)) + + +class TestSignedPermutation: + """The one predicate and the one region rule every orientation stage shares.""" + + def test_the_predicate_admits_exactly_the_signed_permutations(self): + from konfai.data.geometry import SIGNED_PERMUTATION_ATOL_FLOAT64, signed_permutation + + # x<->z swap with x mirrored: output phys x reads input z, output z reads -input x. + matrix = np.asarray([[0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]]) + # Array order (z, y, x): output z reads x mirrored... derived below against apply_remap. + remap = signed_permutation(matrix, SIGNED_PERMUTATION_ATOL_FLOAT64) + assert remap is not None and sorted(source for source, _ in remap) == [0, 1, 2] + # Unit column sums alone pass an averaging matrix; unit peaks alone a superposing one; a + # rank-deficient matrix reading one axis twice passes both column tests. All three refuse. + averaging = np.linalg.inv(np.asarray([[0.5, 0.25, 0.25], [0.25, 0.5, 0.25], [0.25, 0.25, 0.5]])) + superposing = np.linalg.inv(np.asarray([[1.0, 0.0, 0.0], [0.5, 1.0, 0.0], [0.0, 0.0, 1.0]])) + degenerate = np.asarray([[1.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, 0.0]]) + for refused in (averaging, superposing, degenerate): + assert signed_permutation(refused, SIGNED_PERMUTATION_ATOL_FLOAT64) is None + + def test_a_float32_quarter_turn_is_admitted_at_its_own_tolerance(self): + import torch + from konfai.data.geometry import SIGNED_PERMUTATION_ATOL_FLOAT32, signed_permutation + + # The float32 provenance the looser constant exists for: a quarter-turn matrix composed + # from float32 cosines lands within ~1e-7 of the 0/+-1 entries it stands for, and must + # still be read as the exact remap it is. + angles = torch.deg2rad(torch.tensor([90.0, 270.0, 180.0])) + cos, sin = torch.cos(angles), torch.sin(angles) + matrix = torch.tensor([[cos[0], -sin[0], 0.0], [sin[0], cos[0], 0.0], [0.0, 0.0, 1.0]], dtype=torch.float32) + assert not torch.equal(matrix, torch.round(matrix)) # the entries really are off the lattice + assert signed_permutation(matrix, SIGNED_PERMUTATION_ATOL_FLOAT32) is not None + + def test_remap_region_is_the_index_image_of_apply_remap(self): + import itertools + + import torch + from konfai.data.geometry import ( + SIGNED_PERMUTATION_ATOL_FLOAT64, + apply_remap, + invert_remap, + remap_region, + remap_shape, + signed_permutation, + ) + + rng = np.random.default_rng(3) + volume = torch.from_numpy(rng.standard_normal((2, 5, 6, 7)).astype(np.float32)) + axes = np.eye(3) + for order in itertools.permutations(range(3)): + for signs in itertools.product((1.0, -1.0), repeat=3): + matrix = np.stack([axes[axis] * sign for axis, sign in zip(order, signs, strict=True)], axis=1) + remap = signed_permutation(matrix, SIGNED_PERMUTATION_ATOL_FLOAT64) + assert remap is not None + out = apply_remap(volume, remap) + assert list(out.shape[1:]) == remap_shape([5, 6, 7], remap) + # The region contract: reading the remapped source region and remapping IT + # reproduces the target patch exactly, mirrors included ([n - stop, n - start)). + target = tuple(slice(1, extent - 1) for extent in out.shape[1:]) + source = remap_region(target, [5, 6, 7], remap) + torch.testing.assert_close( + apply_remap(volume[(slice(None), *source)], remap), + out[(slice(None), *target)], + rtol=0, + atol=0, + ) + # The inverse remap undoes the forward, extents and values alike. + back = apply_remap(out, invert_remap(remap)) + torch.testing.assert_close(back, volume, rtol=0, atol=0) + + def test_apply_remap_materialises_the_copy(self): + import torch + from konfai.data.geometry import apply_remap + + volume = torch.arange(8.0).reshape(1, 2, 4) + # Even the identity remap with no mirror is a copy: a remapped copy may be handed on while + # the source tensor lives its own life. + out = apply_remap(volume, [(0, False), (1, False)]) + assert out.data_ptr() != volume.data_ptr() + torch.testing.assert_close(out, volume, rtol=0, atol=0) diff --git a/tests/unit/test_masked_statistics.py b/tests/unit/test_masked_statistics.py new file mode 100644 index 00000000..670d86d5 --- /dev/null +++ b/tests/unit/test_masked_statistics.py @@ -0,0 +1,128 @@ +# Copyright (c) 2025 Valentin Boussot +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Masked whole-volume statistics: the disk scan, and the streamed ``Clip``/``Standardize`` that +seed themselves from it instead of loading two whole volumes per case.""" + +from pathlib import Path + +import numpy as np +import pytest +from konfai.data.transform import Clip, LocalityKind, Standardize +from konfai.utils.dataset import Attribute, Dataset +from konfai.utils.dataset.statistics import read_masked_data_statistics +from konfai.utils.errors import DatasetManagerError +from oracle_support import ( + GEOMETRIES, + MAIN, + ROUTES, + Route, + build_case, + sweep, + whole_volume, +) + +pytest.importorskip("SimpleITK") # the fixture case is written as mha + + +# -------------------------------------------------------------------------------------- +# The scan +# -------------------------------------------------------------------------------------- + + +def _masked_pair(root: Path) -> Dataset: + rng = np.random.default_rng(7) + dataset = Dataset(root, "h5") + volume = (rng.standard_normal((1, 13, 9, 11)) * 300.0).astype(np.float32) + mask = (rng.random((1, 13, 9, 11)) < 0.4).astype(np.uint8) + dataset.write("CT", "CASE", volume, Attribute()) + dataset.write("MASK", "CASE", mask, Attribute()) + return dataset + + +def test_the_masked_scan_matches_numpy_over_the_selected_values(tmp_path: Path) -> None: + dataset = _masked_pair(tmp_path / "data") + stats = read_masked_data_statistics(dataset, "CT", dataset, "MASK", "CASE") + volume = dataset.read_data("CT", "CASE")[0] + selected = volume[dataset.read_data("MASK", "CASE")[0] == 1] + assert stats["min"] == pytest.approx(float(selected.min()), abs=0.0) + assert stats["max"] == pytest.approx(float(selected.max()), abs=0.0) + assert stats["mean"] == pytest.approx(float(selected.mean(dtype=np.float64)), rel=1e-12) + assert stats["std"] == pytest.approx(float(selected.std(ddof=1, dtype=np.float64)), rel=1e-9) + + +def test_the_masked_scan_refuses_a_mask_off_the_volume_grid(tmp_path: Path) -> None: + dataset = _masked_pair(tmp_path / "data") + small = Dataset(tmp_path / "small", "h5") + small.write("MASK", "CASE", np.ones((1, 4, 4, 4), dtype=np.uint8), Attribute()) + with pytest.raises(DatasetManagerError, match="grid"): + read_masked_data_statistics(dataset, "CT", small, "MASK", "CASE") + + +# -------------------------------------------------------------------------------------- +# The declarations +# -------------------------------------------------------------------------------------- + + +def test_masked_stages_declare_the_kind_their_configuration_makes_them() -> None: + # A masked statistic is GLOBAL_STAT with no stat key: the stage seeds itself from the masked + # scan, and the dispatcher still guards the seed's validity (stat_seed_valid) and seeds nothing. + assert Standardize(mask="MASK").patch_locality(Attribute()).kind is LocalityKind.GLOBAL_STAT + assert not Standardize(mask="MASK").patch_locality(Attribute()).stat_keys + # Both coefficients given: the mask selects nothing that is read. + assert Standardize(mean=[0.0], std=[1.0], mask="MASK").patch_locality(Attribute()).kind is LocalityKind.POINTWISE + clip = Clip(min_value="min", max_value="max", mask="MASK") + assert clip.patch_locality(Attribute()).kind is LocalityKind.GLOBAL_STAT + assert not clip.patch_locality(Attribute()).stat_keys + # Fixed bounds never read the mask; a percentile needs the whole histogram, mask or not. + assert Clip(-100.0, 100.0, mask="MASK").patch_locality(Attribute()).kind is LocalityKind.POINTWISE + assert Clip("percentile:5", 100.0, mask="MASK").patch_locality(Attribute()).kind is LocalityKind.WHOLE_VOLUME + + +# -------------------------------------------------------------------------------------- +# Streamed equals whole-volume +# -------------------------------------------------------------------------------------- + + +@pytest.mark.parametrize("route", ROUTES, ids=lambda route: route.name) +def test_a_masked_standardize_streams_and_matches_the_whole_volume( + route: Route, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The published Synthesis pattern: ``Standardize(mask: ...)`` must stream (the statistic is + seeded once from the masked disk scan) and produce the whole-volume path's values. The two + routes accumulate the moments at different widths (float32 over the assembled tensor, float64 + in the scan), so they agree to that rounding rather than bit for bit.""" + dataset = build_case(tmp_path / "case", GEOMETRIES[MAIN]) + got = sweep(dataset, "Intensity", Standardize(mask="Labels"), tmp_path / "streamed", route, monkeypatch) + want = whole_volume(dataset, "Intensity", Standardize(mask="Labels"), tmp_path / "whole") + assert got.verdict.name == "STREAM" + assert got.array.shape == want.array.shape + np.testing.assert_allclose(got.array, want.array, rtol=0, atol=1e-4) + + +@pytest.mark.parametrize("route", ROUTES, ids=lambda route: route.name) +def test_a_masked_clip_streams_and_matches_the_whole_volume_bit_for_bit( + route: Route, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Masked ``min``/``max`` bounds are order statistics, exact on both routes: the streamed case + must equal the whole-volume one bit for bit.""" + dataset = build_case(tmp_path / "case", GEOMETRIES[MAIN]) + got = sweep( + dataset, "Intensity", Clip(min_value="min", max_value="max", mask="Labels"), tmp_path / "s", route, monkeypatch + ) + want = whole_volume(dataset, "Intensity", Clip(min_value="min", max_value="max", mask="Labels"), tmp_path / "w") + assert got.verdict.name == "STREAM" + np.testing.assert_array_equal(got.array, want.array) diff --git a/tests/unit/test_transform.py b/tests/unit/test_transform.py index f7eb6b47..3086c0a0 100644 --- a/tests/unit/test_transform.py +++ b/tests/unit/test_transform.py @@ -15,11 +15,9 @@ # SPDX-License-Identifier: Apache-2.0 """Tests for ``konfai.data.transform``: Clip, Dilate, Norm, Crop, Standardize, Padding, -Resample, InferenceStack, and KonfAIInference.""" +Resample, and InferenceStack.""" -import os -import sys -import types +import builtins from pathlib import Path import numpy as np @@ -27,14 +25,11 @@ import torch import torch.nn.functional as F from konfai.data.transform import ( - DEFAULT_INFERENCE_MODEL_NAME, - DEFAULT_INFERENCE_REPO_ID, Canonical, Clip, Crop, Dilate, InferenceStack, - KonfAIInference, LocalityKind, Mask, Norm, @@ -422,7 +417,7 @@ def test_resample_to_shape_inverse_pops_pushed_spacing(): # -------------------------------------------------------------------------------------- -# InferenceStack / KonfAIInference +# InferenceStack / the KonfAIInference loader resolution (the stage itself lives in konfai-apps) # -------------------------------------------------------------------------------------- @@ -454,131 +449,33 @@ def write(self, group, name, data, cache_attribute): assert torch.allclose(out, torch.full((1, 2, 2), 4.0)) -@pytest.fixture(autouse=True) -def _ambient_ports_survive(monkeypatch: pytest.MonkeyPatch): - """infer_entry pops both port vars from the real environment; registering them with monkeypatch - makes teardown put an ambient value back instead of leaking the deletion into the session.""" - monkeypatch.delenv("KONFAI_MASTER_PORT", raising=False) - monkeypatch.delenv("KONFAI_TENSORBOARD_PORT", raising=False) +def test_konfai_inference_bare_name_resolves_from_konfai_apps() -> None: + """Published bundles (ImpactSynth) spell the bare name ``KonfAIInference:``; the package must + keep handing the class over, now imported from konfai-apps.""" + pytest.importorskip("konfai_apps") + import konfai.data.transform as transform_package + cls = transform_package.KonfAIInference + assert cls.__name__ == "KonfAIInference" + assert cls.__module__ == "konfai_apps.transforms" -def test_konfai_inference_reassembles_channels_in_sorted_order(tmp_path, monkeypatch): - """Per-channel outputs must be stacked in deterministic (sorted) case order.""" - sitk = pytest.importorskip("SimpleITK") - - output_dir = tmp_path / "Output" - files = [] - for i in range(3): - case_dir = output_dir / f"P{i:03d}" - case_dir.mkdir(parents=True) - array = np.full((2, 2, 2), float(i * 10), dtype=np.float32) - path = case_dir / "Volume.mha" - sitk.WriteImage(sitk.GetImageFromArray(array), str(path)) - files.append(path) - - # Simulate an arbitrary (here reversed) filesystem enumeration order. - scrambled = list(reversed(files)) - monkeypatch.setattr(Path, "rglob", lambda self, pattern: iter(scrambled)) - - result = KonfAIInference._reassemble_output(output_dir) - - assert list(result.shape) == [3, 2, 2, 2] - assert float(result[0].mean()) == 0.0 - assert float(result[1].mean()) == 10.0 - assert float(result[2].mean()) == 20.0 - - -def test_konfai_inference_default_repo_and_model_preserved(): - """Constructing without arguments keeps the current published repo/model default.""" - transform = KonfAIInference() - - assert transform.repo_id == DEFAULT_INFERENCE_REPO_ID - assert transform.model_name == DEFAULT_INFERENCE_MODEL_NAME - assert transform.repo_id == "VBoussot/MRSegmentator-KonfAI" - assert transform.model_name == "MRSegmentator" - - -def test_konfai_inference_forwards_configured_repo_and_model(monkeypatch): - """A custom repo/model is forwarded verbatim to the KonfAIApp spec, not the default.""" - captured = {} - - class _FakeKonfAIApp: - def __init__(self, spec, *args): - captured["spec"] = spec - - def infer(self, *args, **kwargs): - captured["infer"] = (args, kwargs) - - fake_module = types.ModuleType("konfai_apps") - fake_module.KonfAIApp = _FakeKonfAIApp - monkeypatch.setitem(sys.modules, "konfai_apps", fake_module) - - transform = KonfAIInference( - repo_id="acme/Custom-KonfAI", - model_name="CustomModel", - checkpoints_name=["fold_1"], - ) - transform.infer_entry(Path("dataset"), Path("output"), []) - - assert captured["spec"] == "acme/Custom-KonfAI:CustomModel" - - -def test_konfai_inference_raises_clear_error_inside_daemon_workers(monkeypatch: pytest.MonkeyPatch) -> None: - transform = KonfAIInference() - - class DaemonProcess: - daemon = True - - monkeypatch.setattr("konfai.data.transform.inference.current_process", lambda: DaemonProcess()) - - with pytest.raises(RuntimeError, match=r"Dataset\.num_workers: 0"): - transform("CASE_000", torch.zeros(1, 4, 4), Attribute()) - - -def test_konfai_inference_forwards_config_overrides_to_the_nested_run(monkeypatch: pytest.MonkeyPatch) -> None: - # The nested run is tunable from the calling code via the generic --set mechanism (not for shrinking a - # trained patch_size (that hurts the result), but for any legitimate config knob). - konfai_apps = pytest.importorskip("konfai_apps") - recorded: dict[str, object] = {} - class FakeKonfAIApp: - def __init__(self, ref: str, download: bool, force_update: bool) -> None: - recorded["ref"] = ref +def test_konfai_inference_without_konfai_apps_names_the_install(monkeypatch: pytest.MonkeyPatch) -> None: + """Without konfai-apps the resolution refuses with the install command, not an AttributeError + the loader would turn into 'no transform is named KonfAIInference'.""" + import konfai.data.transform as transform_package - def infer(self, *args: object, **kwargs: object) -> None: - recorded["config_overrides"] = kwargs.get("config_overrides") + real_import = builtins.__import__ - monkeypatch.setattr(konfai_apps, "KonfAIApp", FakeKonfAIApp) - overrides = ["iterations=300"] - transform = KonfAIInference(repo_id="Org/Repo", model_name="tiny", config_overrides=overrides) - transform.infer_entry(Path("/tmp/in"), Path("/tmp/out"), [0]) + def import_without_konfai_apps(name, *args, **kwargs): + if name.split(".")[0] == "konfai_apps": + raise ImportError("konfai_apps unavailable") + return real_import(name, *args, **kwargs) - assert recorded["ref"] == "Org/Repo:tiny" - assert recorded["config_overrides"] == overrides + monkeypatch.setattr(builtins, "__import__", import_without_konfai_apps) - -def test_konfai_inference_defragments_the_nested_allocator(monkeypatch: pytest.MonkeyPatch) -> None: - # A heavy nested model (e.g. a 3D segmentation a metric relies on) can OOM on a large volume purely from - # allocator fragmentation; the nested run enables expandable segments so it fits without config changes. - konfai_apps = pytest.importorskip("konfai_apps") - - class FakeKonfAIApp: - def __init__(self, ref: str, download: bool, force_update: bool) -> None: - pass - - def infer(self, *args: object, **kwargs: object) -> None: - pass - - monkeypatch.setattr(konfai_apps, "KonfAIApp", FakeKonfAIApp) - monkeypatch.delenv("PYTORCH_CUDA_ALLOC_CONF", raising=False) - - KonfAIInference(repo_id="Org/Repo", model_name="tiny").infer_entry(Path("/tmp/in"), Path("/tmp/out"), [0]) - assert "expandable_segments:True" in os.environ["PYTORCH_CUDA_ALLOC_CONF"] - - # An explicit caller setting must win (setdefault, not overwrite). - monkeypatch.setenv("PYTORCH_CUDA_ALLOC_CONF", "max_split_size_mb:128") - KonfAIInference(repo_id="Org/Repo", model_name="tiny").infer_entry(Path("/tmp/in"), Path("/tmp/out"), [0]) - assert os.environ["PYTORCH_CUDA_ALLOC_CONF"] == "max_split_size_mb:128" + with pytest.raises(TransformError, match="pip install konfai-apps"): + _ = transform_package.KonfAIInference # -------------------------------------------------------------------------------------- @@ -920,43 +817,6 @@ def test_string_encoded_parameters_are_refused_with_their_spelling() -> None: assert Permute(dims="2|0|1").dims == [0, 3, 1, 2] -def test_konfai_inference_hands_the_nested_run_this_ranks_device_only(tmp_path: Path, monkeypatch) -> None: - """Under --gpu 0 1 each rank runs on its own device; the nested prediction it spawns per case - must run there too, not on every device the launch was given (a two-GPU prediction per rank).""" - import konfai.data.transform.inference as transform_module - - pytest.importorskip("SimpleITK") - launched: dict[str, list[int]] = {} - - class _Process: - exitcode = 0 - - def __init__(self, target, args): - launched["gpu"] = list(args[2]) - - def start(self) -> None: - pass - - def join(self) -> None: - pass - - class _Context: - Process = _Process - - monkeypatch.setattr(transform_module, "get_context", lambda _method: _Context()) - monkeypatch.setattr(transform_module, "cuda_visible_devices", lambda: [4, 7]) - monkeypatch.setattr(torch.cuda, "is_available", lambda: True) - monkeypatch.setattr(torch.cuda, "empty_cache", lambda: None) - monkeypatch.setattr(torch.cuda, "current_device", lambda: 1) # local rank 1 - monkeypatch.setattr(KonfAIInference, "_reassemble_output", staticmethod(lambda _dir: torch.zeros(1, 2, 2, 2))) - attributes = Attribute() - attributes["Origin"] = np.zeros(3) - attributes["Spacing"] = np.ones(3) - attributes["Direction"] = np.eye(3).reshape(-1) - KonfAIInference()("case", torch.zeros(1, 2, 2, 2), attributes) - assert launched["gpu"] == [7], "the rank's own device, in the launch's numbering" - - def test_a_streamed_mask_refuses_a_mask_off_the_stage_input_grid(tmp_path: Path) -> None: """A region of the mask is read where the region of the volume sits, which only means anything when the two share a grid; a mask of another extent would be sliced from the wrong place and the @@ -1077,3 +937,45 @@ def test_gradient_keeps_the_axis_dimension_apart_from_the_channels() -> None: f" declares {stage.output_channels(channels)}, so a region is sized for the wrong width" ) assert list(out.shape[1:]) == [8, 16, 16] + + +def test_an_ambiguous_bare_name_warns_with_the_winner_and_the_qualified_loser( + tmp_path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Flip/Mask/Permute/Foreign exist in BOTH stage namespaces, and an Expand marker flips which + one a bare name means: the loader says which class won and how to spell the loser, because with + default arguments neither the binder nor strict_config would ever say so. The sentence is also + the stage's plan_note, so a TRANSFORM plan records which class ran.""" + import warnings as warnings_module + + from konfai.data.augmentation import Flip as FlipDraw + from konfai.data.transform import Flip as FlipTransform + from konfai.data.transform import TransformLoader + + config = tmp_path / "Config.yml" + config.write_text("T:\n transforms:\n Flip: {}\n Clip: {}\n") + monkeypatch.setenv("KONFAI_config_file", str(config)) + monkeypatch.setenv("KONFAI_CONFIG_MODE", "Done") + + with warnings_module.catch_warnings(record=True) as caught: + warnings_module.simplefilter("always") + transform = TransformLoader().get_transform("Flip", "T.transforms") + assert isinstance(transform, FlipTransform) + messages = [str(warning.message) for warning in caught] + assert any( + "konfai.data.transform.Flip" in message and "konfai.data.augmentation:Flip" in message for message in messages + ) + note = transform.plan_note("G", "case", [4, 4, 4], Attribute()) + assert note is not None and "konfai.data.transform.Flip" in note + + with warnings_module.catch_warnings(record=True) as caught: + warnings_module.simplefilter("always") + draw = TransformLoader().get_transform("Flip", "T.transforms", prefer_augmentation=True) + assert isinstance(draw, FlipDraw) + assert any("konfai.data.augmentation.Flip" in str(warning.message) for warning in caught) + + with warnings_module.catch_warnings(record=True) as caught: + warnings_module.simplefilter("always") + clip = TransformLoader().get_transform("Clip", "T.transforms") + assert not caught # an unambiguous name stays silent + assert clip.plan_note("G", "case", [4, 4, 4], Attribute()) is None diff --git a/tests/unit/test_transformer_workflow.py b/tests/unit/test_transformer_workflow.py index a7c65d41..e6c7a5f3 100644 --- a/tests/unit/test_transformer_workflow.py +++ b/tests/unit/test_transformer_workflow.py @@ -1748,7 +1748,7 @@ def test_the_plan_text_is_the_snapshot(tmp_path: Path, monkeypatch: pytest.Monke """ monkeypatch.delenv("KONFAI_LOCAL_RANKS", raising=False) monkeypatch.setattr( - "konfai.data.patching._sweep_pipeline_depth", lambda: 1 + "konfai.data.patching.sweep._sweep_pipeline_depth", lambda: 1 ) # the LOAD line's ~4.0x is priced at depth 1 _write_snapshot_cohort(tmp_path) plan = _build(tmp_path).compute_plan(2, overwrite=False) From 020b5da6e90ed8f0bd594ae295521603c93780d2 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 09:26:39 +0200 Subject: [PATCH 24/28] fix(workflows): reproducible by default, resumable everywhere, honest exits A default-config TRAIN now draws a concrete seed, records it in the workspace, and RESUME reads it back: the silent validation-leak on the most ordinary path (train, interrupt, resume) is gone. _Trainer.__exit__ no longer writes a full untrained checkpoint per auto-patch restart (they could be ensembled by a glob with no error) nor a multi-GB write+delete at every clean BEST end; a genuine crash save is named crash_*.pt and never a contender for best. The twin OOM auto-patch machinery is one VramAutoPatchMixin (the trainer keeps its rendezvous, the predictor its accumulation reserve). Evaluation appends each finished case to a JSONL and a rerun pays only the missing ones; PREDICTION skips cases whose outputs already exist (the transformer's resume semantics, at last symmetric). TensorBoard degrades to a no-op writer with one warning instead of refusing TRAIN on a default install. Entrypoint defaults stop probing CUDA at import (None = CPU, as the CLI and api document); one startup line names the resolved devices; the predictor's per-network extra forward in image logging (the bug the trainer had already fixed) is gone, and the Config__ rename ritual with it. --- konfai/evaluator.py | 85 +++++++++++++-- konfai/predictor/loop.py | 69 +++++++----- konfai/predictor/workflow.py | 109 ++++++++++--------- konfai/trainer.py | 156 +++++++++++++++------------- konfai/utils/runtime/__init__.py | 2 + konfai/utils/runtime/distributed.py | 9 ++ konfai/utils/runtime/logging.py | 12 +++ konfai/utils/vram.py | 65 ++++++++++++ tests/unit/test_evaluator_update.py | 1 + tests/unit/test_trainer.py | 1 + 10 files changed, 352 insertions(+), 157 deletions(-) diff --git a/konfai/evaluator.py b/konfai/evaluator.py index 42e3a0e1..d440e38c 100644 --- a/konfai/evaluator.py +++ b/konfai/evaluator.py @@ -135,6 +135,12 @@ def __init__(self, filename: Path) -> None: # Per-metric optimisation direction ("max"/"min"), declared by each criterion's `maximize` # property, so downstream ranking (the MCP leaderboard) reads it instead of guessing from names. self.directions: dict[str, str] = {} + self._incremental_path: Path | None = None + + def open_incremental(self, path: Path) -> None: + """Append every case recorded from now on to ``path``, one JSON object per line, as it + completes: what a crash keeps, and what a rerun reads back to skip the already-scored.""" + self._incremental_path = path def add(self, values: dict[str, float], name_dataset: str) -> None: """ @@ -148,6 +154,35 @@ def add(self, values: dict[str, float], name_dataset: str) -> None: if name_dataset not in self.measures: self.measures[name_dataset] = {} self.measures[name_dataset][name] = value + if self._incremental_path is not None and values: + recorded = {name: float(value) if np.isfinite(value) else None for name, value in values.items()} + with open(self._incremental_path, "a") as file: + file.write(json.dumps({"name": name_dataset, "values": recorded}, allow_nan=False) + "\n") + + @staticmethod + def load_incremental(paths: list[Path]) -> dict[str, dict[str, float]]: + """The cases the given JSONL files hold, last row per name winning; a truncated tail line + (a kill mid-append) is dropped, never an error. ``null`` reads back as the NaN it stood for. + """ + rows: dict[str, dict[str, float]] = {} + for path in paths: + try: + lines = path.read_text().splitlines() + except OSError: + continue + for line in lines: + if not line.strip(): + continue + try: + row = json.loads(line) + except json.JSONDecodeError: + continue + if not isinstance(row, dict) or not isinstance(row.get("values"), dict): + continue + rows[str(row["name"])] = { + key: (float("nan") if value is None else value) for key, value in row["values"].items() + } + return rows @staticmethod def get_statistic(values: list[float]) -> dict[str, float]: @@ -314,6 +349,9 @@ def __init__( self._pending: dict[tuple[str, str, int], tuple] = {} self._pending_name: str | None = None self._last_result: dict[str, float] = {} + #: Cases a previous, interrupted run already scored (read back from the per-rank case + #: files): their batches are skipped, and their rows still reach the final aggregate. + self._scored_names: set[str] = set() # Per-voxel error maps under the patched path: one region-write sink per (metric, case), # opened at the case's first patch, closed when the case flushes. Disjoint unpadded patches # mean every voxel is written exactly once: the streamed map equals the whole-volume one. @@ -361,7 +399,11 @@ def setup(self, world_size: int): world_size (int): Number of processes in the distributed evaluation setup. """ - if self.metric_path.exists() and len(list(self.metric_path.rglob("*.yml"))): + # An interrupted run (case rows on disk, no aggregate yet) resumes: the scored cases are + # read back and skipped, so no prompt and no clearing. A COMPLETED run keeps the usual + # overwrite confirmation; --overwrite clears everything, case rows included. + resumable = os.environ.get("KONFAI_OVERWRITE") != "True" and self._is_resumable() + if not resumable and self.metric_path.exists() and len(list(self.metric_path.rglob("*.yml"))): confirm_overwrite_or_raise(self.metric_path, "metric", EvaluatorError) if self.metric_path.exists(): # This directory holds the rank-0 evaluation log this process already has open: @@ -376,6 +418,17 @@ def setup(self, world_size: int): self.dataloader, _, _ = self.dataset.get_data(world_size) + def _incremental_case_files(self, statistics: Statistics) -> list[Path]: + """Every rank's case file for this split, this run's and an interrupted predecessor's alike.""" + return sorted(self.metric_path.glob(f"{statistics.filename.stem}.cases.*.jsonl")) + + def _is_resumable(self) -> bool: + """Whether an interrupted run left case rows without their aggregate for some split.""" + return any( + self._incremental_case_files(statistics) and not statistics.filename.exists() + for statistics in (self.statistics_train, self.statistics_validation) + ) + def update(self, batch_sample: BatchSample, statistics: Statistics) -> dict[str, float]: """ Compute metrics for a batch and update running statistics. @@ -390,6 +443,10 @@ def update(self, batch_sample: BatchSample, statistics: Statistics) -> dict[str, """ if self._streamed: return self._update_streamed(batch_sample, statistics) + if self._scored_names and len(self.metrics): + name = batch_sample[next(iter(self.metrics))].name[0] + if name in self._scored_names: + return statistics.measures.get(name, {}) result: dict[str, float] = {} moved = self._groups_on(batch_sample) for output_group in self.metrics: @@ -490,6 +547,8 @@ def _update_streamed(self, batch_sample: BatchSample, statistics: Statistics) -> end of the split closes the last one. """ name = batch_sample[next(iter(self.metrics))].name[0] + if name in self._scored_names: + return self._last_result if self._pending_name is not None and name != self._pending_name: with self._clock.phase("flush"): self._flush_pending(statistics) @@ -643,6 +702,20 @@ def description(measure): self._iter_dataset = dataloader.dataset self._clock = SweepClock() + # Per-case persistence: what an interrupted run already scored is read back and skipped, + # and every case scored from here on is appended to this rank's own case file as it + # completes, so a crash at case N-1 of N keeps N-1 cases. The aggregate below is built from + # the union. + scored = Statistics.load_incremental(self._incremental_case_files(statistics)) + self._scored_names = set(scored) + if scored: + statistics.measures.update(scored) + if global_rank == 0: + print( + f"[KonfAI] evaluation {label}: {len(scored)} case(s) already scored ->" + " skipped (--overwrite recomputes)." + ) + statistics.open_incremental(self.metric_path / f"{statistics.filename.stem}.cases.rank{global_rank}.jsonl") try: with ( self._clock.phase("split"), @@ -698,8 +771,8 @@ def _clock_report(self, label: str, min_seconds: float = 1.0) -> str | None: def build_evaluate( - evaluations_file: Path | str | dict = Path("./Evaluation.yml").resolve(), - evaluations_dir: Path | str = Path("./Evaluations").resolve(), + evaluations_file: Path | str | dict = Path("./Evaluation.yml"), + evaluations_dir: Path | str = Path("./Evaluations"), ) -> DistributedObject: """ Build and return the configured evaluation workflow without executing it. @@ -730,12 +803,12 @@ def build_evaluate( @run_distributed_app def evaluate( overwrite: bool = False, - gpu: list[int] | None = cuda_visible_devices(), + gpu: list[int] | None = None, cpu: int = 1, quiet: bool = False, tensorboard: bool = False, - evaluations_file: Path | str | dict = Path("./Evaluation.yml").resolve(), - evaluations_dir: Path | str = Path("./Evaluations").resolve(), + evaluations_file: Path | str | dict = Path("./Evaluation.yml"), + evaluations_dir: Path | str = Path("./Evaluations"), ) -> DistributedObject: """ Build and execute the configured evaluation workflow. diff --git a/konfai/predictor/loop.py b/konfai/predictor/loop.py index f8be6ad8..f780958b 100644 --- a/konfai/predictor/loop.py +++ b/konfai/predictor/loop.py @@ -38,6 +38,7 @@ from konfai.utils.runtime import ( DataLog, DistributedObject, + NullSummaryWriter, description, ) @@ -140,10 +141,16 @@ def __init__( ) if self._has_runtime_measures or len(self.data_log): if SummaryWriter is None: - raise ImportError( - "TensorBoard is required for prediction logging. Install it with: pip install konfai[tensorboard]" - ) - self.tb = SummaryWriter(log_dir=predict_path / "Metric") + # A missing logger must never refuse the run: the predictions are still written, + # only the curves and images are lost. One line says so; the extra keeps them. + if self.global_rank == 0: + print( + "[KonfAI] TensorBoard is not installed: no curves or images will be logged" + " (pip install konfai[tensorboard] to keep them)." + ) + self.tb = NullSummaryWriter() + else: + self.tb = SummaryWriter(log_dir=predict_path / "Metric") else: self.tb = None @@ -285,19 +292,6 @@ def _predict_log( sync=False, ) - images_log = [] - if len(self.data_log): - for name, data_type in self.data_log.items(): - if name in batch_sample: - data_type[0]( - self.tb, - f"Prediction/{name}", - batch_sample[name].tensor[: self.data_log[name][1]].detach().cpu().numpy(), - self.it, - ) - else: - images_log.append(name.replace(":", ".")) - for name, network in self.model_composite.module.get_networks().items(): if network.measure is not None: self.tb.add_scalars( @@ -310,14 +304,33 @@ def _predict_log( {k.replace(":", "."): v[1] for k, v in measures[name][1].items()}, self.it, ) - if len(images_log): - for name, layer, _ in self.model_composite.module.get_layers( - [v.tensor for v in batch_sample.values() if v.is_input], - images_log, - ): - self.data_log[name][0]( - self.tb, - f"Prediction/{name}", - layer[: self.data_log[name][1]].detach().cpu().numpy(), - self.it, - ) + + # Images are a progress peek, not a per-batch record, and a module-layer target re-runs a + # full forward (get_layers): both throttle to the status cadence. + if not len(self.data_log) or self.it % _DESCRIPTION_EVERY != 0: + return + images_log = [] + for name, data_type in self.data_log.items(): + if name in batch_sample: + data_type[0]( + self.tb, + f"Prediction/{name}", + batch_sample[name].tensor[: self.data_log[name][1]].detach().cpu().numpy(), + self.it, + ) + else: + images_log.append(name.replace(":", ".")) + if len(images_log): + # get_layers is model-scoped, not per-network: run it once per model, or a multi-network + # model (a GAN's generator + discriminator) repeats the forward extraction and writes + # each image event once per network. + for layer_name, layer, _ in self.model_composite.module.get_layers( + [v.tensor for v in batch_sample.values() if v.is_input], + images_log, + ): + self.data_log[layer_name][0]( + self.tb, + f"Prediction/{layer_name}", + layer[: self.data_log[layer_name][1]].detach().cpu().numpy(), + self.it, + ) diff --git a/konfai/predictor/workflow.py b/konfai/predictor/workflow.py index c888adf7..1d0d6da9 100644 --- a/konfai/predictor/workflow.py +++ b/konfai/predictor/workflow.py @@ -48,14 +48,13 @@ DistributedObject, State, configure_workflow_environment, - confirm_overwrite_or_raise, run_distributed_app, ) -from konfai.utils.utils import concretize_patch_size, get_module, size_free_axes +from konfai.utils.utils import concretize_patch_size, get_module @config() -class Predictor(DistributedObject): +class Predictor(vram.VramAutoPatchMixin, DistributedObject): """ KonfAI's main prediction controller. @@ -94,17 +93,10 @@ def __init__( super().__init__(train_name) self.manual_seed = manual_seed self.dataset = dataset - # Auto-patching (VRAM): a per-axis 0 in the user's patch_size marks a FREE axis and opts into - # the OOM restart loop: captured before any re-plan materialises concrete sizes over it. - patch = dataset.patch - self._vram_patch_template: list[int] | None = ( - [int(size) for size in patch.patch_size] - if patch is not None and patch.patch_size is not None and any(size == 0 for size in patch.patch_size) - else None - ) - self._vram_patch_candidate: list[int] | None = None - # Per-axis input multiple the model needs (its downsampling factor); a free axis is sized to it. - self._downsampling_factor: list[int] | None = None + self._capture_vram_patch_template(dataset.patch) + #: Cases whose every configured output already existed when the run started: frozen at + #: ``setup`` on the launcher, so every rank (restarts included) shards the same work list. + self._done_case_indices: set[int] = set() module, name = get_module(combine, "konfai.predictor") if module.__name__ == "konfai.predictor": self.combine = getattr(module, name)() @@ -203,14 +195,27 @@ def setup(self, world_size: int): """ for dataset_filename in self.datasets_filename: path = self.predict_path / dataset_filename - if os.path.exists(path) and len(list(Path(path).rglob("*.yml"))): - confirm_overwrite_or_raise(path, "prediction", PredictorError) - if not os.path.exists(path): os.makedirs(path) shutil.copyfile(config_file(), self.predict_path / "Prediction.yml") + # Per-case resume, the semantics TRANSFORM documents: a case whose every configured output + # is already on disk is skipped, so a rerun after a mid-cohort failure pays only the missing + # cases; --overwrite recomputes everything. The set is frozen here, on the launcher, so + # every rank (and every OOM-restart re-plan) shards the same reduced work list. + if os.environ.get("KONFAI_OVERWRITE") != "True" and self.outputs_dataset: + self._done_case_indices = { + index + for index, name in enumerate(self.dataset.case_names) + if all(output.is_dataset_exist(output.group, name) for output in self.outputs_dataset.values()) + } + if self._done_case_indices: + print( + f"[KonfAI] prediction: {len(self._done_case_indices)}/{len(self.dataset.case_names)}" + " case(s) already written -> skipped (--overwrite recomputes)." + ) + self.model_composite = ModelComposite(self.model, self.combine) if not self.path_to_models and any(parameter.numel() for parameter in self.model.parameters()): # A model WITH weights but no checkpoint would run with random weights and silently produce @@ -233,8 +238,22 @@ def setup(self, world_size: int): self.size = len(self.gpu_checkpoints) + 1 if self.gpu_checkpoints else 1 + self._drop_done_cases() self.dataloader, _, _ = self.dataset.get_data(world_size // self.size) + def _drop_done_cases(self) -> None: + """Drop the already-written cases' entries from the prepared patch mapping. + + Applied to the mapping rather than the case list so the surviving cases keep their indices + (the managers and the loader's remapping stay untouched), and re-applied after every + ``replan_patch``, which rebuilds the mapping from scratch. + """ + if not self._done_case_indices: + return + self.dataset._prepared_mapping = [ + entry for entry in self.dataset._prepared_mapping if entry[0] not in self._done_case_indices + ] + def _report_chain_drift(self) -> None: """Warn when the chain applied to a model input is not the one its checkpoint trained on. @@ -349,18 +368,10 @@ def run_process( model_composite = Model(model_composite) device = local_rank * self.size if len(cuda_visible_devices()) else None dataloader = dataloaders[0] - # Round a free patch axis up to the model's valid input multiple before the first attempt, so - # the network's encoder/decoder skips align instead of crashing on a non-divisible extent (the - # border padding fills the round-up, cropped back after the forward). A whole-axis extent still - # too large for VRAM OOMs into the shrink loop below, which keeps the size valid too. - if self._vram_patch_candidate is None: - sized = size_free_axes( - self._vram_patch_template, self.dataset.worst_case_shape(), self._downsampling_factor - ) - if sized is not None: - self._vram_patch_candidate = sized - self.dataset.replan_patch(sized) - dataloader = self.dataset.get_data(world_size)[0][global_rank][0] + # A whole-axis extent still too large for VRAM OOMs into the shrink loop below, which keeps + # the size valid too (the border padding fills the round-up, cropped back after the forward). + if self._vram_patch_candidate is None and self._presize_free_axes(): + dataloader = self._rank_dataloader(world_size, global_rank) while True: try: with _Predictor( @@ -396,19 +407,20 @@ def run_process( f"[KonfAI] VRAM: rank {global_rank} ran out of memory -> " f"re-planning the free patch axes to {candidate} and restarting this rank's cases." ) - self._vram_patch_candidate = candidate - self.dataset.replan_patch(candidate) - dataloader = self.dataset.get_data(world_size)[0][global_rank][0] + self._adopt_patch_candidate(candidate) + dataloader = self._rank_dataloader(world_size, global_rank) + + def _rank_dataloader(self, world_size: int, global_rank: int) -> DataLoader: + """This rank's loader over the re-planned grids, the already-written cases dropped again + (a re-plan rebuilds the mapping from scratch).""" + self._drop_done_cases() + return self.dataset.get_data(world_size)[0][global_rank][0] def _shrunken_patch(self, measured: int | None, usable: float) -> list[int] | None: - """One shrink step for the free patch axes after a CUDA OOM (``None`` = not auto, or floor). - - The first OOM starts from the worst prepared case at full extent (the size the failed grid - effectively ran); later ones shrink the current candidate further. When the framework picks - the size, it must also leave the blend on the GPU: the accumulation footprint is RESERVED - beside the forward, so the sized patch passes the accumulation gate. Only when that reserve - fits at no size (or cannot be priced) is the forward sized alone: the gate's memory-safe - CPU blend absorbs that case. + """The shared shrink step, with the blend kept on the GPU when it fits: the accumulation + footprint is RESERVED beside the forward, so the sized patch passes the accumulation gate. + Only when that reserve fits at no size (or cannot be priced) is the forward sized alone: + the gate's memory-safe CPU blend absorbs that case. """ if self._vram_patch_template is None: return None @@ -419,14 +431,11 @@ def _shrunken_patch(self, measured: int | None, usable: float) -> list[int] | No self._vram_patch_template, worst, self._downsampling_factor ) reserve = self._accumulation_reserve(candidate, worst) - snap = self._downsampling_factor if reserve is not None: - shrunk = vram.next_patch_candidate( - candidate, self._vram_patch_template, worst, measured, usable - reserve, snap - ) + shrunk = super()._shrunken_patch(measured, usable - reserve) if shrunk is not None: return shrunk - return vram.next_patch_candidate(candidate, self._vram_patch_template, worst, measured, usable, snap) + return super()._shrunken_patch(measured, usable) def _accumulation_reserve(self, candidate: list[int], worst: list[int]) -> float | None: """Bytes each case keeps resident while its patches accumulate, per output writer: the @@ -469,8 +478,8 @@ def __repr__(self) -> str: def build_predict( models: list[Path], - prediction_file: Path | str | dict = Path("./Prediction.yml").resolve(), - predictions_dir: Path | str = Path("./Predictions").resolve(), + prediction_file: Path | str | dict = Path("./Prediction.yml"), + predictions_dir: Path | str = Path("./Predictions"), ) -> DistributedObject: """ Build and return the configured prediction workflow without executing it. @@ -506,12 +515,12 @@ def build_predict( def predict( models: list[Path], overwrite: bool = False, - gpu: list[int] | None = cuda_visible_devices(), + gpu: list[int] | None = None, cpu: int = 1, quiet: bool = False, tensorboard: bool = False, - prediction_file: Path | str | dict = Path("./Prediction.yml").resolve(), - predictions_dir: Path | str = Path("./Predictions").resolve(), + prediction_file: Path | str | dict = Path("./Prediction.yml"), + predictions_dir: Path | str = Path("./Predictions"), ) -> DistributedObject: """ Build and execute the configured prediction workflow. diff --git a/konfai/trainer.py b/konfai/trainer.py index 8da5a753..02cef128 100644 --- a/konfai/trainer.py +++ b/konfai/trainer.py @@ -57,6 +57,7 @@ from konfai.utils.runtime import ( DataLog, DistributedObject, + NullSummaryWriter, State, clear_directory_except_logs, configure_workflow_environment, @@ -67,7 +68,6 @@ seed_all, synchronize_data, ) -from konfai.utils.utils import concretize_patch_size, size_free_axes class EarlyStoppingBase: @@ -259,6 +259,7 @@ def __init__( config_snapshot: Path, dataloader_training: DataLoader, dataloader_validation: DataLoader | None = None, + auto_patched: bool = False, ) -> None: self.world_size = world_size self.global_rank = global_rank @@ -286,13 +287,23 @@ def __init__( self._live_control = LiveControl(statistics_directory() / self.train_name / "control.json") self._interventions: list[dict[str, Any]] = [] if SummaryWriter is None: - raise ImportError( - "TensorBoard is required for training logging. Install it with: pip install konfai[tensorboard]" - ) - self.tb = SummaryWriter(log_dir=statistics_directory() / self.train_name / "tb") + # A missing logger must never refuse the run: training still produces a model, and only + # the curves are lost. One line says so; the extra keeps them. + if self.global_rank == 0: + print( + "[KonfAI] TensorBoard is not installed: no curves or images will be logged" + " (pip install konfai[tensorboard] to keep them)." + ) + self.tb: Any = NullSummaryWriter() + else: + self.tb = SummaryWriter(log_dir=statistics_directory() / self.train_name / "tb") self._best_checkpoint_path: Path | None = None self._best_checkpoint_loss: float | None = None self._checkpoint_writer = _CheckpointWriter() + #: Whether an OOM here restarts the run instead of ending it (a free patch axis declared). + self._auto_patched = auto_patched + #: The iteration of the last save: an exit at the same iteration has nothing new to record. + self._saved_at_it = it self._loss_keys: set[str] = set() if self.global_rank == 0 and self.save_checkpoint_mode == "BEST": self._initialize_best_checkpoint_state() @@ -302,10 +313,18 @@ def __enter__(self): return self def __exit__(self, exc_type, value, traceback): - """Closes the SummaryWriter if used.""" + """Close the writer and save an exit checkpoint only when it records anything. + + An exit at the last save's iteration adds nothing, and the auto-patch OOM restart is about + to rebuild and continue: both used to leave a multi-GB worst-score snapshot behind (the + restart's, of untrained weights, unprunable). A genuine failure that DID advance keeps its + crash-save, under a name that says what it is. + """ if self.tb is not None: self.tb.close() - self.checkpoint_save(None) + oom_restart = self._auto_patched and exc_type is not None and issubclass(exc_type, torch.cuda.OutOfMemoryError) + if not oom_restart and self.it != self._saved_at_it: + self.checkpoint_save(None, crash=exc_type is not None) self._checkpoint_writer.join() def _declare_measure_window(self) -> None: @@ -319,12 +338,18 @@ def _declare_measure_window(self) -> None: network.measure.set_window(max(self.it_validation, validation)) def _initialize_best_checkpoint_state(self) -> None: - """Bootstrap BEST-checkpoint tracking once, including resume scenarios.""" + """Bootstrap BEST-checkpoint tracking once, including resume scenarios. + + Crash saves (``crash_*.pt``) are the user's to manage: never a contender for best, never + pruned. When only unscored checkpoints remain (their stored loss is the worst-score + sentinel), they all hold the same claim, so the newest: the most-trained snapshot: is kept + and the rest are pruned; ``is_better`` is strict, so no scan can elect one best. + """ path = checkpoints_directory() / self.train_name if not path.exists(): return - all_checkpoints = sorted(path.glob("*.pt")) + all_checkpoints = sorted(p for p in path.glob("*.pt") if not p.name.startswith("crash_")) best_loss = self.early_stopping.worst_score best_ckpt: Path | None = None for checkpoint_path in all_checkpoints: @@ -336,9 +361,12 @@ def _initialize_best_checkpoint_state(self) -> None: best_loss = checkpoint_loss best_ckpt = checkpoint_path + if best_ckpt is None and len(all_checkpoints) > 1: + best_ckpt = max(all_checkpoints, key=lambda p: p.stat().st_mtime) + best_loss = self.early_stopping.worst_score if best_ckpt is not None: self._best_checkpoint_path = best_ckpt - self._best_checkpoint_loss = best_loss + self._best_checkpoint_loss = best_loss if math.isfinite(best_loss) else None for checkpoint_path in all_checkpoints: if checkpoint_path != best_ckpt: checkpoint_path.unlink() @@ -629,7 +657,7 @@ def _record_interventions(self) -> None: yaml.dump(data, file) os.replace(tmp, target) - def checkpoint_save(self, loss: float | None) -> None: + def checkpoint_save(self, loss: float | None, crash: bool = False) -> None: """ Saves model and optimizer states. Keeps either all checkpoints or only the best one. @@ -639,6 +667,9 @@ def checkpoint_save(self, loss: float | None) -> None: Args: loss (float): Current loss used for best checkpoint selection. + crash (bool): A save on an exceptional exit: named ``crash_.pt`` and left outside + BEST retention, so the last state survives beside the best one instead of being + retired by it. """ if self.global_rank != 0: return @@ -647,12 +678,13 @@ def checkpoint_save(self, loss: float | None) -> None: path = checkpoints_directory() / self.train_name path.mkdir(parents=True, exist_ok=True) - date = current_date() + date = f"crash_{current_date()}" if crash else current_date() save_path = path / f"{date}.pt" collision = 1 while save_path.exists(): save_path = path / f"{date}_{collision}.pt" collision += 1 + self._saved_at_it = self.it # An unscored checkpoint (the final save at close) carries the worst possible score so # `_update_best_checkpoint` retires it in BEST mode instead of leaving it beside the real best. @@ -699,7 +731,7 @@ def publish() -> None: staging = save_path.with_name(f"{save_path.name}.{os.getpid()}.tmp") torch.save(snapshot, staging) os.replace(staging, save_path) - if self.save_checkpoint_mode == "BEST": + if self.save_checkpoint_mode == "BEST" and not crash: self._update_best_checkpoint(save_path, checkpoint_loss) self._checkpoint_writer.submit(publish) @@ -847,7 +879,7 @@ def _agreed_patch(gathered: list, template: list[int]) -> list[int] | None: @config() -class Trainer(DistributedObject): +class Trainer(vram.VramAutoPatchMixin, DistributedObject): """ Public API for training a model using the KonfAI framework. Wraps setup, checkpointing, resuming, logging, and launching distributed _Trainer. @@ -898,16 +930,7 @@ def __init__( super().__init__(train_name) self.manual_seed = manual_seed self.dataset = dataset - # Auto-patching (VRAM): a per-axis 0 in the user's patch_size marks a FREE axis and opts into - # the OOM restart loop: captured before any re-plan materialises concrete sizes over it. - patch = dataset.patch - self._vram_patch_template: list[int] | None = ( - [int(size) for size in patch.patch_size] - if patch is not None and patch.patch_size is not None and any(size == 0 for size in patch.patch_size) - else None - ) - self._vram_patch_candidate: list[int] | None = None - self._downsampling_factor: list[int] | None = None + self._capture_vram_patch_template(dataset.patch) self.autocast = autocast self.channels_last = channels_last self.epochs = epochs @@ -927,20 +950,20 @@ def __init__( self.gpu_checkpoints = gpu_checkpoints self.save_checkpoint_mode = save_checkpoint_mode self.config_path_src = config_file() - config_namefile = self.config_path_src.name.replace(".yml", "") - self.config_namefile = statistics_directory() / self.name / f"{config_namefile}_{self.it}.yml" + self.config_namefile = statistics_directory() / self.name / self.config_path_src.name self.size = len(self.gpu_checkpoints) + 1 if self.gpu_checkpoints else 1 state = State[konfai_state()] # Cut the grids with the model's downsampling multiple already known, so each case's free axis # rounds up to a valid input size (the graph (hence the factor) is final before init()). self.dataset.set_free_axis_multiple(self.model.downsampling_factor()) - if self.manual_seed is not None: - # The train/validation split is drawn inside prepare() here on the launcher, before spawn. - # Without seeding, the global RNG is unseeded, so every run: a fresh TRAIN or a RESUME -- - # redraws a different split and leaks validation cases into training. Per-rank seeding for the - # actual training happens later in the distributed runtime. - seed_all(self.manual_seed) + # The train/validation split is drawn inside prepare() here on the launcher, before spawn. + # It always comes from a CONCRETE seed: the configured one, or the seed the previous run + # recorded, or a fresh draw: an unseeded split would be redrawn on RESUME and leak + # validation cases into training. Per-rank seeding for the actual training happens later in + # the distributed runtime, and stays opt-in (`manual_seed`), as do the cudnn flags. + self._split_seed = self._resolve_split_seed(state) + seed_all(self._split_seed) self.dataset.prepare() self.model.bind( self.autocast, state, self.dataset.get_groups_dest(), self.gradient_checkpoints, self.gpu_checkpoints @@ -948,6 +971,27 @@ def __init__( # The per-axis multiple a free patch axis rounds up to, read off the model's downsampling graph. self._downsampling_factor = self.model.downsampling_factor() + def _resolve_split_seed(self, state: State) -> int: + """The seed every draw in ``prepare()`` (the split first) comes from, always concrete. + + The configured ``manual_seed`` when there is one; on RESUME of an unseeded run, the seed the + TRAIN run recorded in its workspace, so the rebuilt split is the one the checkpoint trained + on; otherwise a fresh draw, recorded by ``setup`` for the next RESUME. + """ + if self.manual_seed is not None: + return self.manual_seed + if state == State.RESUME: + recorded = self._recorded_split_seed() + if recorded is not None: + return recorded + return int.from_bytes(os.urandom(4), "little") + + def _recorded_split_seed(self) -> int | None: + try: + return int((statistics_directory() / self.name / "Seed.txt").read_text().strip()) + except (OSError, ValueError): + return None + def setup(self, world_size: int): """ Initializes the training environment: @@ -999,6 +1043,9 @@ def setup(self, world_size: int): with open(statistics_directory() / self.name / f"Validation_{self.it}.txt", "w") as f: for name in validation_names: f.write(name + "\n") + # The seed the split was drawn from, kept where RESUME looks for it (_resolve_split_seed): + # written here, after the fresh-TRAIN clearing above, so it survives its own run. + (statistics_directory() / self.name / "Seed.txt").write_text(f"{self._split_seed}\n") def set_model(self, path_to_model: str | Path) -> None: self.path_to_model = str(path_to_model) @@ -1006,11 +1053,6 @@ def set_model(self, path_to_model: str | Path) -> None: def set_lr(self, lr: float | None) -> None: self.override_lr = lr - def __exit__(self, exc_type, value, traceback): - """Exit training context and trigger save of model/checkpoints.""" - super().__exit__(exc_type, value, traceback) - self._save() - def _load(self) -> dict[str, dict[str, torch.Tensor]]: """ Loads a previously saved checkpoint from local disk or URL. @@ -1029,14 +1071,6 @@ def _load(self) -> dict[str, dict[str, torch.Tensor]]: self.it = state_dict["it"] return state_dict - def _save(self) -> None: - if self.config_namefile.exists(): - new_name = f"{self.config_namefile.stem}_{self.it}.yml" - os.rename( - self.config_namefile, - self.config_namefile.parent / new_name, - ) - def _ema_update(self) -> dict[str, Callable]: """The EMA rule for AveragedModel: torch's fused ``multi_avg_fn``, one ``_foreach_lerp_`` per device and dtype.""" @@ -1075,13 +1109,7 @@ def run_process( if self.channels_last: Network.set_channels_last(self.model_ema.module) device = local_rank * self.size if len(cuda_visible_devices()) else None - # Round a free patch axis up to the model's valid input multiple before the first step, so the - # network's skips align instead of crashing on a non-divisible extent; every rank rounds the - # same worst case to the same size, so no rendezvous is needed here (unlike the OOM shrink). - sized = size_free_axes(self._vram_patch_template, self.dataset.worst_case_shape(), self._downsampling_factor) - if sized is not None: - self._vram_patch_candidate = sized - self.dataset.replan_patch(sized) + if self._presize_free_axes(): dataloaders = self.dataset.get_data(world_size)[0][global_rank] while True: try: @@ -1104,6 +1132,7 @@ def run_process( self.model_ema, self.config_namefile, *dataloaders, + auto_patched=self._vram_patch_template is not None, ) as t: t.run() return @@ -1139,29 +1168,10 @@ def run_process( f"[KonfAI] VRAM: rank {global_rank} ran out of memory -> " f"re-planning the free patch axes to {agreed} and restarting the training run." ) - self._vram_patch_candidate = agreed - self.dataset.replan_patch(agreed) + self._adopt_patch_candidate(agreed) vram.reset_peak(device) dataloaders = self.dataset.get_data(world_size)[0][global_rank] - def _shrunken_patch(self, measured: int | None, usable: float) -> list[int] | None: - """One shrink step for the free patch axes after a CUDA OOM (``None`` = not auto, or floor). - - The first OOM starts from the worst prepared case at full extent (the size the failed grid - effectively ran); later ones shrink the current candidate further. - """ - if self._vram_patch_template is None: - return None - worst = self.dataset.worst_case_shape() - if worst is None: - return None - candidate = self._vram_patch_candidate or concretize_patch_size( - self._vram_patch_template, worst, self._downsampling_factor - ) - return vram.next_patch_candidate( - candidate, self._vram_patch_template, worst, measured, usable, self._downsampling_factor - ) - def build_train( command: State = State.TRAIN, @@ -1222,7 +1232,7 @@ def train( command: State = State.TRAIN, overwrite: bool = False, model: Path | str | None = None, - gpu: list[int] | None = cuda_visible_devices(), + gpu: list[int] | None = None, cpu: int | None = None, quiet: bool = False, tensorboard: bool = False, diff --git a/konfai/utils/runtime/__init__.py b/konfai/utils/runtime/__init__.py index 6012f904..35643a38 100644 --- a/konfai/utils/runtime/__init__.py +++ b/konfai/utils/runtime/__init__.py @@ -51,6 +51,7 @@ from konfai.utils.runtime.logging import DataLog as DataLog from konfai.utils.runtime.logging import Log as Log from konfai.utils.runtime.logging import MinimalLog as MinimalLog +from konfai.utils.runtime.logging import NullSummaryWriter as NullSummaryWriter from konfai.utils.runtime.logging import TensorBoard as TensorBoard from konfai.utils.runtime.logging import record as record @@ -62,6 +63,7 @@ "Log", "MinimalLog", "NeedDevice", + "NullSummaryWriter", "State", "TensorBoard", "apply_cpu_thread_budget", diff --git a/konfai/utils/runtime/distributed.py b/konfai/utils/runtime/distributed.py index 940a6f04..48e61e1f 100644 --- a/konfai/utils/runtime/distributed.py +++ b/konfai/utils/runtime/distributed.py @@ -399,6 +399,15 @@ def execute_distributed_object( world_size = len(gpu_ids) if world_size == 0: world_size = cpu_workers + if not quiet: + # One line naming the resolved devices: omitting --gpu runs on CPU, and a + # silent CPU fallback on a GPU machine is a 10-100x slowdown nobody sees. + device_line = ( + "cuda:" + ",".join(str(i) for i in gpu_ids) + if gpu_ids + else f"CPU ({cpu_workers} worker{'s' if cpu_workers > 1 else ''})" + ) + print(f"[KonfAI] Running on {device_line}") with clock.phase("setup"): configured_object.setup(world_size) # Share tensors through /dev/shm files instead of one file descriptor per tensor: diff --git a/konfai/utils/runtime/logging.py b/konfai/utils/runtime/logging.py index ebbb520f..605558e8 100644 --- a/konfai/utils/runtime/logging.py +++ b/konfai/utils/runtime/logging.py @@ -45,6 +45,18 @@ from konfai.utils.errors import ConfigError +class NullSummaryWriter: + """Stands in for TensorBoard's ``SummaryWriter`` when the extra is absent: every ``add_*`` and + ``close`` call is absorbed, so the workflow still produces its outputs; only the curves are lost. + """ + + def __getattr__(self, name: str): + def _absorb(*args, **kwargs) -> None: + return None + + return _absorb + + def _log_signal_format(array: np.ndarray) -> dict[str, np.ndarray]: return {str(i): channel for i, channel in enumerate(array)} diff --git a/konfai/utils/vram.py b/konfai/utils/vram.py index 1f6b9ef8..4f9ece20 100644 --- a/konfai/utils/vram.py +++ b/konfai/utils/vram.py @@ -27,8 +27,12 @@ restarts. When everything fits (the common case) nothing here runs at all. """ +from typing import Any + import torch +from konfai.utils.utils import concretize_patch_size, size_free_axes + #: Fraction of the free VRAM a step may claim; the reserve absorbs allocator fragmentation and #: transients the measured run did not exercise (mirrors the accumulation gate's margin). VRAM_BUDGET_SAFETY_FRACTION = 0.8 @@ -116,3 +120,64 @@ def snapped(axis: int, value: int) -> int: for axis in free: shrunk[axis] = min(snapped(axis, int(candidate[axis] * ratio)), candidate[axis]) return shrunk if shrunk != list(candidate) else None + + +class VramAutoPatchMixin: + """The auto-patch state and shrink policy the training and prediction workflows share. + + The state lives on the workflow object itself: the free-axis template captured from the user's + patch (a per-axis ``0`` marks a FREE axis and opts into the OOM restart loop), the current + candidate, and the model's per-axis input multiple. Each workflow keeps only its own injection + points around this: the trainer its multi-rank shrink rendezvous, the predictor its + accumulation reserve and output reset. + """ + + #: The workflow's dataset (set by the subclass __init__): the grids re-cut on a re-plan. + dataset: Any + + def _capture_vram_patch_template(self, patch: Any) -> None: + """Capture the user's free-axis convention before any re-plan materialises sizes over it.""" + self._vram_patch_template: list[int] | None = ( + [int(size) for size in patch.patch_size] + if patch is not None and patch.patch_size is not None and any(size == 0 for size in patch.patch_size) + else None + ) + self._vram_patch_candidate: list[int] | None = None + #: Per-axis input multiple the model needs (its downsampling factor); a free axis snaps to + #: it. The subclass sets it once the model graph is final. + self._downsampling_factor: list[int] | None = None + + def _presize_free_axes(self) -> bool: + """Round the free patch axes up to the model's valid input multiple before the first step, + so the network's encoder/decoder skips align instead of crashing on a non-divisible extent. + Every rank rounds the same worst case to the same size, so no rendezvous is needed here + (unlike the OOM shrink). True when the grids were re-cut: the caller re-fetches its loaders. + """ + sized = size_free_axes(self._vram_patch_template, self.dataset.worst_case_shape(), self._downsampling_factor) + if sized is None: + return False + self._adopt_patch_candidate(sized) + return True + + def _adopt_patch_candidate(self, candidate: list[int]) -> None: + """Record ``candidate`` and re-cut every prepared grid to it.""" + self._vram_patch_candidate = candidate + self.dataset.replan_patch(candidate) + + def _shrunken_patch(self, measured: int | None, usable: float) -> list[int] | None: + """One shrink step for the free patch axes after a CUDA OOM (``None`` = not auto, or floor). + + The first OOM starts from the worst prepared case at full extent (the size the failed grid + effectively ran); later ones shrink the current candidate further. + """ + if self._vram_patch_template is None: + return None + worst = self.dataset.worst_case_shape() + if worst is None: + return None + candidate = self._vram_patch_candidate or concretize_patch_size( + self._vram_patch_template, worst, self._downsampling_factor + ) + return next_patch_candidate( + candidate, self._vram_patch_template, worst, measured, usable, self._downsampling_factor + ) diff --git a/tests/unit/test_evaluator_update.py b/tests/unit/test_evaluator_update.py index 6ad5fd9a..2c272f3f 100644 --- a/tests/unit/test_evaluator_update.py +++ b/tests/unit/test_evaluator_update.py @@ -42,6 +42,7 @@ def _evaluator(metrics: dict[str, dict[str, dict[torch.nn.Module, None]]], strea evaluator._pending_name = None evaluator._last_result = {} evaluator._map_sinks = {} + evaluator._scored_names = set() return evaluator diff --git a/tests/unit/test_trainer.py b/tests/unit/test_trainer.py index c606d9d1..9508cf17 100644 --- a/tests/unit/test_trainer.py +++ b/tests/unit/test_trainer.py @@ -518,6 +518,7 @@ def load(self, state_dict: dict, **kwargs) -> None: trainer.name = "RUN" trainer.size = 1 trainer.it = 0 + trainer._split_seed = 0 trainer.ema_decay = 0.999 trainer.model_ema = None trainer.override_lr = None From 514cfff93eb10daa3bbf60bd3e7a86694274632d Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 09:49:08 +0200 Subject: [PATCH 25/28] docs: the campaign's surface, protected and truthful A docs CI job builds the site at -W (every baseline warning fixed at source), so the hand-maintained stub layer can no longer rot silently. The headline performance claims link the tracked benchmarks/ harness and its one-command reproductions; llms.txt/llms-full.txt are generated into the site for agent consumption. The PyPI page renders again (absolute asset URLs). The storage-backend and Reduction extension contracts leave AGENTS.md for the public extension guide, written against the new declarative registry. Everything the campaign changed is documented where users read: --init replaces the dead generation modes, konfai list, Model.pretrained_from and allow_head_resize, the three Transform tiers, recorded-seed RESUME, crash_*.pt, optional TensorBoard, prediction/evaluation resume, the null spelling, and a state_dict -> network_states migration note. One docs-deps list instead of three; installation.md stops contradicting troubleshooting. The changelog draft for the next release sits in .audit-local. --- .github/workflows/konfai_ci.yml | 36 +- README.md | 28 +- docs/requirements.txt | 3 - docs/source/concepts/configuration.md | 32 +- docs/source/concepts/datasets.md | 4 +- docs/source/concepts/streaming.md | 7 +- docs/source/conf.py | 70 +- docs/source/config_guide/evaluation.md | 4 + docs/source/config_guide/prediction.md | 4 + docs/source/config_guide/training.md | 14 +- docs/source/config_guide/transform.md | 7 +- docs/source/development.md | 5 +- docs/source/examples/visual-gallery.md | 2 +- docs/source/getting-started/installation.md | 11 +- docs/source/index.rst | 1 + docs/source/konfai.data.rst | 5 - docs/source/konfai.utils.rst | 26 + docs/source/modules.rst | 10 +- docs/source/reference/api/extension-points.md | 155 ++- docs/source/reference/cli.md | 37 +- docs/source/reference/components/index.md | 5 +- docs/source/reference/components/models.md | 46 + .../reference/components/storage-backends.md | 2 + .../source/reference/components/transforms.md | 2 +- docs/source/troubleshooting.md | 7 +- docs/source/usage/adopting-konfai.md | 9 +- docs/source/usage/benchmarks.md | 61 + docs/source/usage/custom-models.md | 16 +- docs/source/usage/index.rst | 1 + docs/source/usage/python-workflows.md | 14 + konfai/transformer.py | 4 +- konfai/utils/config.py | 6 +- pixi.lock | 1034 +---------------- pyproject.toml | 7 +- 34 files changed, 574 insertions(+), 1101 deletions(-) create mode 100644 docs/source/usage/benchmarks.md diff --git a/.github/workflows/konfai_ci.yml b/.github/workflows/konfai_ci.yml index eea69512..3935d35b 100644 --- a/.github/workflows/konfai_ci.yml +++ b/.github/workflows/konfai_ci.yml @@ -10,7 +10,7 @@ on: - "konfai/**" - "konfai-apps/**" - "tests/**" - - "docs/source/config_guide/**" + - "docs/**" - "examples/Transform/**" - "pyproject.toml" - "README.md" @@ -20,7 +20,7 @@ on: - "konfai/**" - "konfai-apps/**" - "tests/**" - - "docs/source/config_guide/**" + - "docs/**" - "examples/Transform/**" - "pyproject.toml" - "README.md" @@ -104,6 +104,38 @@ jobs: - name: Check formatting run: ruff format --check konfai konfai-apps/konfai_apps tests + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + # This job runs code the pull request controls; the token has no business staying + # in .git/config while it does. + persist-credentials: false + # setuptools-scm derives both packages' versions from the tag history. + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.12" + cache: "pip" + cache-dependency-path: | + pyproject.toml + docs/requirements.txt + + - name: Install dependencies + # autodoc imports konfai and konfai_apps, so both install from this checkout; the imaging + # extra keeps the guarded backend modules importable. docs/requirements.txt is the single + # source for the Sphinx toolchain (what ReadTheDocs installs). + run: | + python -m pip install -U pip + pip install -e ".[imaging]" -e ./konfai-apps -r docs/requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu + + - name: Build the docs strictly + # -W turns any warning into a failure; --keep-going still reports them all. + run: python -m sphinx -b html -W --keep-going docs/source /tmp/docs-html + build: runs-on: ubuntu-latest steps: diff --git a/README.md b/README.md index 8bb3a6e4..b7831c7a 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,8 @@ konfai TRAIN -c Config.yml --gpu 0 # then PREDICTION, then EVALUATION

- - KonfAI reads medical data regionally, executes transforms and PyTorch graphs patch by patch, reconstructs outputs, and delivers medical datasets, Apps, HTTP services, Slicer workflows, and agent-operated experiments. + + KonfAI reads medical data regionally, executes transforms and PyTorch graphs patch by patch, reconstructs outputs, and delivers medical datasets, Apps, HTTP services, Slicer workflows, and agent-operated experiments.

@@ -76,7 +76,7 @@ registration, and synthesis: - **Running a published model?** → the one-command App install ([Real workloads](#real-workloads-one-app-contract)). - **Adapting an experiment?** → the [Quickstart](#quickstart-first-smoke-run) (train → predict → evaluate). - **Building an App?** → [`konfai-apps`](https://konfai.readthedocs.io/en/latest/usage/apps.html). -- **Contributing?** → [`AGENTS.md`](AGENTS.md). +- **Contributing?** → [`AGENTS.md`](https://github.com/fideus-labs/KonfAI/blob/main/AGENTS.md). ## Why KonfAI? @@ -126,7 +126,11 @@ registration systems, not reduced demonstration networks: These figures retain each bundle's stated case, ensemble and hardware conditions; they are evidence of executable scale, not a cross-task leaderboard. The per-app time and RAM ratios come from each bundle's own -small/medium/large benchmark table (see the bundle READMEs under `apps/`). The bundles share the same App contract across local directories, +small/medium/large benchmark table (see the bundle READMEs under +[`apps/`](https://github.com/fideus-labs/KonfAI/tree/main/apps)); the shared +measurement protocol and the runnable harness are in +[`benchmarks/`](https://github.com/fideus-labs/KonfAI/tree/main/benchmarks). +The bundles share the same App contract across local directories, Hugging Face and HTTP, with SlicerKonfAI for general Apps and SlicerImpactReg for dedicated registration. @@ -215,7 +219,7 @@ The shipped `epochs: 5` is demo-sized: it walks the complete path in a few minutes and is not meant to produce a useful checkpoint; raise it to 100+ for a real run. To do all of the above in one go, including predict, evaluate and a plot of the result, run every cell of -[`examples/Segmentation/Segmentation_demo.ipynb`](examples/Segmentation/Segmentation_demo.ipynb). +[`examples/Segmentation/Segmentation_demo.ipynb`](https://github.com/fideus-labs/KonfAI/blob/main/examples/Segmentation/Segmentation_demo.ipynb). The full walkthrough (predict, evaluate, what to inspect, common first issues, notebook entry points) lives in the @@ -242,7 +246,10 @@ A chain streams when every step declares the region it needs: the exact patch (`OneHot`), a halo (`Dilate`), a remap (`Flip`), a resample (`Resample`), or a whole-volume statistic read once from disk (`Normalize`). On the stream path, a 16 GiB uncompressed `.mha` trains at patch 64³ under an 8 GiB memory cap -with a peak resident set of 0.46 GiB. +with a peak resident set of 0.46 GiB. Reproduce the bounded-memory claim with +`python benchmarks/bench_streaming.py --gib 16 --budget 1` (the tracked +[`benchmarks/`](https://github.com/fideus-labs/KonfAI/tree/main/benchmarks) +harness pins the protocol). `konfai TRANSFORM` decides that per case *before* it writes a byte: STREAM or LOAD, WHOLE-VOLUME naming the stage that refused to stream, REDUCE or REFUSED @@ -358,8 +365,11 @@ Contributions are welcome: improve examples, clarify docs, add tests, or extend models / transforms / apps. See the [developer guide](https://konfai.readthedocs.io/en/latest/development.html). -**AI coding agents:** start with [`AGENTS.md`](AGENTS.md), the canonical -reference for conventions, commands, and repository rules. +**AI coding agents:** start with [`AGENTS.md`](https://github.com/fideus-labs/KonfAI/blob/main/AGENTS.md), the canonical +reference for conventions, commands, and repository rules. The docs site also +publishes [llms.txt](https://konfai.readthedocs.io/en/latest/llms.txt) and +[llms-full.txt](https://konfai.readthedocs.io/en/latest/llms-full.txt): the +quickstart, config guides and component catalog in one agent-ingestible file. --- @@ -374,4 +384,4 @@ reference for conventions, commands, and repository rules. } ``` -Licensed under [Apache-2.0](LICENSE). +Licensed under [Apache-2.0](https://github.com/fideus-labs/KonfAI/blob/main/LICENSE). diff --git a/docs/requirements.txt b/docs/requirements.txt index d8603f44..892fa67e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,9 +1,6 @@ sphinx>=7.0 shibuya myst-parser -nbsphinx -ipython -ipykernel sphinx-autodoc-typehints sphinx-copybutton sphinx-togglebutton diff --git a/docs/source/concepts/configuration.md b/docs/source/concepts/configuration.md index a17beb80..bace2ac9 100644 --- a/docs/source/concepts/configuration.md +++ b/docs/source/concepts/configuration.md @@ -10,7 +10,9 @@ Reading a config **mutates it**: loading a run resolves every default and rewrites the YAML file in place, so the file on disk becomes the fully-resolved record of the experiment. One consequence: a `None` value round-trips as the literal string `"None"`: it is written back as `"None"` and reparsed to -`None` on the next read. +`None` on the next read. An explicit `name: null` (or an empty `name:`) also +binds `None`: null is the disabled spelling and is never replaced by the +default. ``` KonfAI is fundamentally a **configuration-driven object builder**. @@ -141,21 +143,34 @@ construction. ## Config modes -`KONFAI_CONFIG_MODE` selects how KonfAI reacts to a missing file or missing keys: +`KONFAI_CONFIG_MODE` selects how KonfAI reacts to a missing file: | Mode | Behavior | | --- | --- | -| `Done` | Normal run mode. The config file must already exist; values are read and the visited subtree is written back. A missing file raises `ConfigError`. | -| `default` | Materialize defaults non-interactively. Missing files or keys are created from each field's `default\|...` value (or its Python default), and the file is written. | -| `interactive` | Like `default`, but prompt on stdin for each `default\|...` field so a config can be generated interactively. Falls back to `default` when stdin is unavailable. | +| `Done` | Normal run mode. The config file must already exist; values are read and the visited subtree is written back. A missing file raises `ConfigError` naming `konfai --init` as the way to generate one. | | `Import` | Skip config binding entirely. The decorated object is called with the arguments it was given, without reading the YAML: used when importing or constructing classes outside the config-driven flow. | -| `remove` | Delete the config file on context exit instead of writing it back: used for throwaway configs, for example in tests. | An *unknown* value behaves like `Done`. An **unset** `KONFAI_CONFIG_MODE` is different: `apply_config` binds nothing at all (as under `Import`), and using `Config` directly raises `KeyError` on exit. Tests that build configurable objects directly must therefore set **both** variables explicitly. +**Generating a config is a CLI verb, not a mode**: `konfai --init` +creates the file when missing (seeded with its root key), binds the workflow +once so every default resolves into it, and exits without running. The former +generation modes (`default`, `interactive`, `remove`) are gone. + +Two binding rules worth knowing: + +- **An explicit null stays null.** `name:` (empty) or `name: null` binds + `None`, the disabled spelling, exactly like the string `"None"`. The default + is not substituted: that would silently reactivate the very thing the line + was written to suppress. +- **A wrong-shaped value is refused, by dotted path.** A key given a nested + block or a list where its parameter takes a scalar raises `ConfigError` + naming the path (`Parameter 'Trainer.Dataset.batch_size' was given a nested + block, but it takes a int.`) instead of binding something silently. + ## `classpath` Many configurable components are selected dynamically through a `classpath` @@ -187,8 +202,9 @@ directory. It is usually the least ambiguous option. The `default|...` prefix is an important KonfAI convention. Its behavior is inferred directly from `konfai.utils.config.Config._get_input_default()`. -It is used to express a fallback value that can still be overridden by config or -interactive generation. Examples from the codebase include: +It is used to express a fallback value that can still be overridden by the +config, and it is what `--init` materialises into a generated file. Examples +from the codebase include: - `train_name: str = "default|TRAIN_01"` - `classpath: str = "default|segmentation.UNet.UNet"` diff --git a/docs/source/concepts/datasets.md b/docs/source/concepts/datasets.md index e9a92441..57147e7b 100644 --- a/docs/source/concepts/datasets.md +++ b/docs/source/concepts/datasets.md @@ -169,8 +169,10 @@ From the dataset code, `validation` may be: - `None` - a float such as `0.2` -- a slice string such as `0:10` +- a slice string such as `0:10` (a negative end counts from the end, + Python-style: `0:-2`) - a path to a text file listing case names +- a `~path.txt` exclusion file - a list of indices - a list of case names - a list mixing case names and case-list files diff --git a/docs/source/concepts/streaming.md b/docs/source/concepts/streaming.md index 4c69941c..1e9243b4 100644 --- a/docs/source/concepts/streaming.md +++ b/docs/source/concepts/streaming.md @@ -4,7 +4,9 @@ region is read straight from the file, and the result is written slab by slab as it completes. Neither the input nor the output is ever held whole. A 16 GiB uncompressed volume trains at a peak of **0.46 GiB of host RAM**, stable across -epochs, with VRAM equal to one batch. +epochs, with VRAM equal to one batch. The bounded-memory claim is reproducible +with one command, `python benchmarks/bench_streaming.py --gib 16 --budget 1`: +see {doc}`../usage/benchmarks`. That is not only a memory story. Running published models through KonfAI, on the same weights and the same card, against their reference implementations: @@ -22,7 +24,8 @@ tables, including small and medium cases, are in the [MRSegmentator](https://github.com/fideus-labs/KonfAI/tree/main/apps/mrsegmentator) and [TotalSegmentator](https://github.com/fideus-labs/KonfAI/tree/main/apps/totalsegmentator) -app pages. +app pages; the measurement protocol behind every number is +{doc}`../usage/benchmarks`. Nothing in YAML asks for any of it. KonfAI reads your preprocessing chain, works out whether a patch's answer can be computed from a bounded region of the file, diff --git a/docs/source/conf.py b/docs/source/conf.py index 95502dfb..02534d6f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -83,6 +83,9 @@ myst_heading_anchors = 3 suppress_warnings = [ "sphinx_autodoc_typehints.local_function", + # Whether a third-party guarded import resolves depends on the environment (torch's + # tensorboard writer guards dateutil), so it must not fail a -W build. + "sphinx_autodoc_typehints.guarded_import", "intersphinx.external", ] @@ -93,9 +96,74 @@ } autodoc_member_order = "bysource" autosummary_generate = True -autodoc_mock_imports = ["SimpleITK"] +# SimpleITK is heavy; requests and huggingface_hub are konfai_apps' own dependencies, which the +# ReadTheDocs build (core + docs/requirements.txt only) does not install, while apps.rst autodocs +# konfai_apps from the source tree on sys.path. +autodoc_mock_imports = ["SimpleITK", "requests", "huggingface_hub"] intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "torch": ("https://pytorch.org/docs/stable/", None), } + +# --------------------------------------------------------------------------- +# llms.txt / llms-full.txt: agent-ingestible copies of the pages an agent needs +# to author a config (quickstart, the config guides, the component catalog). +# Emitted into the HTML output root, so they publish at /llms.txt beside the +# site. llms.txt is the index; llms-full.txt concatenates the page sources. +# --------------------------------------------------------------------------- + +_LLMS_BASE_URL = "https://konfai.readthedocs.io/en/latest" + +#: (section, source file, published page) in reading order. +_LLMS_PAGES = [ + ("Getting started", "quickstart.rst", "quickstart.html"), + ("Config guide", "config_guide/training.md", "config_guide/training.html"), + ("Config guide", "config_guide/prediction.md", "config_guide/prediction.html"), + ("Config guide", "config_guide/evaluation.md", "config_guide/evaluation.html"), + ("Config guide", "config_guide/transform.md", "config_guide/transform.html"), + ("Components", "reference/components/index.md", "reference/components/index.html"), + ("Components", "reference/components/models.md", "reference/components/models.html"), + ("Components", "reference/components/losses-metrics.md", "reference/components/losses-metrics.html"), + ("Components", "reference/components/transforms.md", "reference/components/transforms.html"), + ("Components", "reference/components/augmentations.md", "reference/components/augmentations.html"), + ("Components", "reference/components/schedulers.md", "reference/components/schedulers.html"), + ("Components", "reference/components/storage-backends.md", "reference/components/storage-backends.html"), + ("Reference", "reference/cli.md", "reference/cli.html"), +] + +_LLMS_HEADER = ( + "# KonfAI\n\n" + "> KonfAI is a declarative deep-learning framework for medical imaging: a model, its data\n" + "> pipeline, losses/metrics, and the whole train/predict/evaluate/transform workflow are\n" + "> described in YAML and run by the `konfai` CLI. Configs are complete, reproducible records\n" + "> of an experiment; volumes are read as patches and never loaded whole on a streamable route.\n" +) + + +def _write_llms_txt(app, exception): + if exception is not None or app.builder.name != "html": + return + from pathlib import Path as _Path + + srcdir, outdir = _Path(app.srcdir), _Path(app.outdir) + index_lines = [_LLMS_HEADER] + full_parts = [_LLMS_HEADER] + section = None + for page_section, source, page in _LLMS_PAGES: + source_path = srcdir / source + if not source_path.exists(): + continue + if page_section != section: + section = page_section + index_lines.append(f"\n## {section}\n") + url = f"{_LLMS_BASE_URL}/{page}" + index_lines.append(f"- [{source}]({url})") + full_parts.append(f"\n\n---\nSource: {url}\n---\n\n{source_path.read_text(encoding='utf-8')}") + index_lines.append(f"\n\nFull content: {_LLMS_BASE_URL}/llms-full.txt\n") + (outdir / "llms.txt").write_text("\n".join(index_lines), encoding="utf-8") + (outdir / "llms-full.txt").write_text("".join(full_parts), encoding="utf-8") + + +def setup(app): + app.connect("build-finished", _write_llms_txt) diff --git a/docs/source/config_guide/evaluation.md b/docs/source/config_guide/evaluation.md index a94d411f..8fe9f2ad 100644 --- a/docs/source/config_guide/evaluation.md +++ b/docs/source/config_guide/evaluation.md @@ -27,6 +27,10 @@ konfai EVALUATION -y --config Evaluation.yml The output directory is controlled by `Evaluator.train_name` in the YAML and `--evaluations-dir` on the CLI. +Evaluation persists per case as it goes: each rank appends finished cases to a +`*.cases.rank.jsonl` file beside the metric JSON, so a rerun after an +interruption pays only the cases that are not yet recorded. + ## Top-level fields | Field | Type | Default in code | Required | Effect | diff --git a/docs/source/config_guide/prediction.md b/docs/source/config_guide/prediction.md index 97b001f2..a1fb528a 100644 --- a/docs/source/config_guide/prediction.md +++ b/docs/source/config_guide/prediction.md @@ -38,6 +38,10 @@ konfai PREDICTION -y --gpu 0 --config Prediction.yml \ When multiple checkpoints are provided, the predictor combines them using the `combine` strategy from the YAML, usually `Mean` or `Median`. +A rerun resumes: a case whose every configured output is already on disk is +skipped (the run prints how many), so a mid-cohort failure pays only the +missing cases. `-y`/`--overwrite` recomputes everything. + ## Top-level fields | Field | Type | Default in code | Required | Effect | diff --git a/docs/source/config_guide/training.md b/docs/source/config_guide/training.md index 7b9688d6..48be4341 100644 --- a/docs/source/config_guide/training.md +++ b/docs/source/config_guide/training.md @@ -31,6 +31,10 @@ automatically: konfai TRAIN -y --gpu 0 --config Config.yml -tb ``` +TensorBoard is optional: without the `tensorboard` extra the run trains +normally with a no-op writer and one warning naming +`pip install konfai[tensorboard]`; only the scalar logs are lost. + Resume from an existing checkpoint with `RESUME`. Checkpoints are named after the moment they were written, so substitute the one training produced: `--model` takes exactly one: @@ -40,6 +44,14 @@ konfai RESUME -y --config Config.yml \ --model Checkpoints/SEG_BASELINE/2026_08_03_02_36_00.pt ``` +A run is reproducible by default: the seed every preparation draw comes from +(the train/validation split first) is recorded in +`Statistics//Seed.txt`, and RESUME of an unseeded run reads it +back, so resuming never re-splits the cohort. Set `manual_seed` only to pick +the seed yourself. A save on an exceptional exit is named `crash_.pt` and +sits outside the `save_checkpoint_mode` pruning: never a contender for best, +and yours to delete. + You can also change the output directories: ```bash @@ -55,7 +67,7 @@ konfai TRAIN -y --config Config.yml \ | `Model` | mapping | `ModelLoader()` | Yes | Selects and configures the model graph. | | `Dataset` | mapping | `DataTrain()` | Yes | Defines training data loading, transforms, augmentation, and patching. | | `train_name` | string | `TRAIN_01` | No | Names the run and its output folders. | -| `manual_seed` | int or null | `None` | No | Sets the random seed when provided. | +| `manual_seed` | int or null | `None` | No | Picks the seed. `None` still runs seeded: a fresh TRAIN draws one, records it in `Statistics//Seed.txt`, and RESUME reads it back. | | `epochs` | int | `100` | No | Number of training epochs. | | `it_validation` | int or null | `None` | No | Validation and checkpoint interval in iterations. | | `it_lr_update` | int or null | `None` | No | Scheduler-step interval in iterations. `None` steps once per epoch (it resolves to the training dataloader's length). Every resolved config on disk carries this key. | diff --git a/docs/source/config_guide/transform.md b/docs/source/config_guide/transform.md index e5c8b890..9c2acb50 100644 --- a/docs/source/config_guide/transform.md +++ b/docs/source/config_guide/transform.md @@ -306,7 +306,7 @@ Under `Dataset:`: | Field | Type | Default | Effect | | --- | --- | --- | --- | | `dataset_filenames` | list of `path[:format]` | `["./Dataset:mha"]` | Where cases are read. | -| `memory_budget` | size string or number | `auto` | Per-rank ceiling on the buffers the sweep holds, the OME-Zarr decoded-chunk cache included (a third of it, printed in the plan's header), not on the process: peak RSS is this plus a floor (interpreter, torch, the chain's own working set). A bare number is GiB; `"8G"` is decimal (8 x 10^9 = 7.45 GiB), `"8GiB"` binary; `"512MB"` also works. `auto` is 80% of the node's memory, split across ranks. | +| `memory_budget` | size string or number | `auto` | Per-rank ceiling on the buffers the sweep holds, the OME-Zarr decoded-chunk cache included (a third of it, printed in the plan's header), not on the process: peak RSS is this plus a floor (interpreter, torch, the chain's own working set). A bare number is GiB; `"8G"` is decimal (8 x 10^9 = 7.45 GiB), `"8GiB"` binary; `"512MB"` also works. `auto` is 80% of the node's memory, split across ranks. A declared budget below 256 MiB warns: the process floor alone exceeds it (declare at least 512 MiB, or `auto`). | | `subset` | string / list / null | `null` | Restricts which cases run: a flat selector: a case name, a case-list file, `~file` to exclude, a `start:end` slice, or a list of those. **Not** a nested mapping; a block written under it is refused. | | `groups_src` | mapping |: | The chains, keyed by source group then destination group. | @@ -370,7 +370,10 @@ plan says how many regions that is, and `memory_budget` sizes and refuses agains it. `Concat` puts the cases side by side: the output carries `N × C` channels. A custom operator must declare `voxel_local = True`: one that reads across space cannot stream and is refused outright. It should also declare `working_multiple` if it allocates over the buffer it is handed, -or the plan promises a working set the run exceeds. +or the plan promises a working set the run exceeds. The full operator contract +(`voxel_local` and its corruption trap, `incremental`, `working_multiple_for`, +`output_channels`), with a code skeleton, is in +{doc}`../reference/api/extension-points`. ```{warning} `Mean` and `Median` are for intensities. Both answer with values that were in no diff --git a/docs/source/development.md b/docs/source/development.md index 2ac453ab..aa0c5df0 100644 --- a/docs/source/development.md +++ b/docs/source/development.md @@ -208,7 +208,10 @@ pip install -r docs/requirements.txt make -C docs html ``` -The output lands in `docs/_build/html/`. +`docs/requirements.txt` is the single source for the docs toolchain: it is what +ReadTheDocs and the CI docs job install, and the `[dev]` extra carries the same +list. The `make` route writes to `docs/build/html/`; the live-reload task +(`dev-docs`) serves from `docs/_build/html/`. ### Documentation style diff --git a/docs/source/examples/visual-gallery.md b/docs/source/examples/visual-gallery.md index 50eb46b6..4c7b0ab2 100644 --- a/docs/source/examples/visual-gallery.md +++ b/docs/source/examples/visual-gallery.md @@ -122,7 +122,7 @@ reproducible; normal training draws new states after the epoch reset.
  • The source after a sampled Brightness augmentation.
    BrightnessSampled intensity offsetb_std 0.35
  • The source after a sampled Contrast augmentation.
    ContrastSampled intensity scalec_std 0.75
  • The source after a Noise augmentation sampled below a 55 percent maximum diffusion timestep.
    NoiseSampled below a 55% timestep ceilingn_std 0.65 · prob 0.55
  • -
  • The source after a rectangular CutOUT augmentation.
    CutOUTRectangular region replaced by −1cutout_size 0.34 · c_prob 1
  • +
  • The source after a rectangular CutOUT augmentation.
    CutOUTRectangular region replaced by −1cutout_size 0.34
  • One source image, six sampled augmentation states.Open any card independently · fixed documentation seeds · shared spatial state across a case

    diff --git a/docs/source/getting-started/installation.md b/docs/source/getting-started/installation.md index 4b5f0788..1e365c5d 100644 --- a/docs/source/getting-started/installation.md +++ b/docs/source/getting-started/installation.md @@ -6,7 +6,7 @@ KonfAI needs **Python 3.11 or newer**. This is the line most people want: python -m pip install "konfai[imaging]" ``` -`konfai` on its own brings PyTorch, NumPy, `ruamel.yaml`, `huggingface_hub` and +`konfai` on its own brings PyTorch, NumPy, `ruamel.yaml`, `psutil` and the engine, but **no image reader**, so it cannot open a `.mha`. The `[imaging]` extra adds SimpleITK, h5py, pydicom, zarr and ngff-zarr, which covers all four storage backends at once. You do not need `[dicom]` or `[omezarr]` on top of it. @@ -60,8 +60,9 @@ python -m pip install konfai-apps It gives you the `konfai-apps` and `konfai-apps-server` commands, plus the Python API under `konfai_apps`. Check them with `konfai-apps --help` and -`konfai-apps-server --help`; `konfai-cluster --help` comes with the `cluster` -extra. +`konfai-apps-server --help`. `konfai-cluster` ships with the core `konfai` +package itself; the `cluster` extra only adds `submitit`, which actual SLURM +submission needs. Installing one of the bundled apps (`apps/impact_seg`, `apps/impact_synth`, `apps/impact_reg`, `apps/mrsegmentator`, `apps/totalsegmentator`) **from a @@ -129,7 +130,9 @@ first, then KonfAI. For containers, see {doc}`../usage/docker`. `python -c "import torch; print(torch.cuda.is_available(), torch.cuda.device_count())"` and `echo "$CUDA_VISIBLE_DEVICES"`. - **`konfai-apps-server` not found**: `pip install konfai-apps`. -- **`konfai-cluster` not found**: `pip install "konfai[cluster]"`. +- **`konfai-cluster` not found**: the command ships with `konfai` itself, so + "not found" means the environment mismatch of the first bullet. Install + `konfai[cluster]` only when submission fails on a missing `submitit`. Next: {doc}`../quickstart` runs a real train, predict and evaluate loop in about seven minutes. {doc}`../reference/cli` lists every command and flag. diff --git a/docs/source/index.rst b/docs/source/index.rst index a9122acf..2ed0b7d9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -278,6 +278,7 @@ KonfAI usage/python-workflows usage/large-images usage/custom-models + usage/benchmarks usage/docker .. toctree:: diff --git a/docs/source/konfai.data.rst b/docs/source/konfai.data.rst index 9bad618c..f5e0ecb2 100644 --- a/docs/source/konfai.data.rst +++ b/docs/source/konfai.data.rst @@ -122,11 +122,6 @@ konfai.data.transform package :show-inheritance: :undoc-members: -.. automodule:: konfai.data.transform.inference - :members: - :show-inheritance: - :undoc-members: - .. automodule:: konfai.data.transform.io :members: :show-inheritance: diff --git a/docs/source/konfai.utils.rst b/docs/source/konfai.utils.rst index c022257e..8728282d 100644 --- a/docs/source/konfai.utils.rst +++ b/docs/source/konfai.utils.rst @@ -93,6 +93,32 @@ konfai.utils.dataset package :show-inheritance: :undoc-members: +konfai.utils.errors module +-------------------------- + +The ``KonfAIError`` taxonomy: every designed refusal a workflow raises. The +Python workflows (:doc:`usage/python-workflows`) let these propagate; only the +CLI catches them. + +.. automodule:: konfai.utils.errors + :members: + :show-inheritance: + :undoc-members: + +konfai.utils.pretrained module +------------------------------ + +The pretrained-weights bridge: pairs weighted leaves in forward-execution order, +so a checkpoint from another framework (MONAI, torchvision, nnU-Net) seeds a +KonfAI graph without a key map. ``PretrainedFrom`` is the ``Model.pretrained_from`` +config entry; ``transfer_weights_by_execution_order`` is the underlying transfer. +It fills every target tensor or raises: a partial load is never reported as success. + +.. automodule:: konfai.utils.pretrained + :members: + :show-inheritance: + :undoc-members: + konfai.utils.utils module ------------------------- diff --git a/docs/source/modules.rst b/docs/source/modules.rst index b8bc2ed0..3c08c6ed 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -4,10 +4,12 @@ Full module reference This section documents the modules that make up KonfAI. It is intentionally broader than the curated API pages and therefore includes lower-level helpers used by extension authors. The stubs are **hand-maintained**, not generated, so the coverage -is partial: `konfai.data`, `konfai.metric` and `konfai.network` are complete, while -`konfai.models.**` and several `konfai.utils` helpers (`errors`, `runtime`, -`model_builder`, `ome_zarr`, `dicom`, `pretrained`, `vram`, `live_control`) and -`konfai.export` have no page yet. Read them from the source until they do. +is partial: `konfai.data`, `konfai.metric` and `konfai.network` are complete, and +`konfai.utils` now covers `errors` (the `KonfAIError` taxonomy) and `pretrained` +(the weights bridge behind `Model.pretrained_from`). `konfai.models.**`, a few +`konfai.utils` helpers (`runtime`, `model_builder`, `ome_zarr`, `dicom`, `vram`, +`live_control`, `catalog`) and `konfai.export` have no page yet. Read them from +the source until they do; `konfai list models` prints the model catalog. .. toctree:: :maxdepth: 4 diff --git a/docs/source/reference/api/extension-points.md b/docs/source/reference/api/extension-points.md index 3d40cb3e..f5eaa9cd 100644 --- a/docs/source/reference/api/extension-points.md +++ b/docs/source/reference/api/extension-points.md @@ -112,11 +112,29 @@ The safe default is to declare nothing: A transform that overrides only `__call__` therefore takes the whole-volume path. The case is loaded, your `__call__` sees the tensor it always would, and patches are cut from the result. Custom transforms never have to know streaming exists. +That is the whole tier-0 contract: `__call__`, plus `transform_shape()` when the +spatial shape changes. -To opt in, override `patch_locality(cache_attribute)` and return a -`PatchLocality`. Augmentations override `_patch_locality(index, a, -cache_attribute)`: an augmentation declares per case *and* per copy, because the -halo of a geometric draw is that draw's own. +To opt in to streaming (tier 1), set the `locality` class attribute to a +`LocalityKind` (plus the `halo` attribute for a bounded neighbourhood): + +```python +class Threshold(Transform): + locality = LocalityKind.POINTWISE + + def __call__(self, name, tensor, cache_attribute): + return (tensor > 0.5).to(tensor.dtype) +``` + +The base `patch_locality` answers from the attribute. Override the method +`patch_locality(cache_attribute)` itself (tier 2) only when the answer depends +on the case (read off the header) or carries `stat_keys` or a `reason`; the +other tier-2 methods (`stream_region_source`, `stream_region`, +`plan_region_reads`, `stream_slab`, `write_stream_cache_attribute`) are owed +only where the table below says so. Augmentations declare the same way +(`_patch_locality(index, a, cache_attribute)` as the method form): an +augmentation declares per case *and* per copy, because the halo of a geometric +draw is that draw's own. | Declared kind | Meaning | What you must also implement | | --- | --- | --- | @@ -324,6 +342,133 @@ Runtime contracts, each base class names exactly what you must implement: | `konfai.data.transform.TransformInverse` | the above, plus `inverse(name, tensor, cache_attribute)` |: | | `konfai.data.augmentation.DataAugmentation` | three: `_state_init(index, shapes, caches_attribute)`, `_compute(name, index, a, tensor)`, `_inverse(index, a, tensor)` |: | +## A storage backend + +An imaging format is one class plus one registry entry. Subclass +`konfai.utils.dataset.AbstractFile`, declare the backend's facts as class +attributes, and register the format token in +`konfai.utils.dataset.BACKENDS`; `backend_for(file_format)` then dispatches to +it and nothing else needs a format branch. A token that is also a file suffix +(like `h5`) additionally belongs in `SUPPORTED_EXTENSIONS` +(`konfai.utils.utils`); a token no file on disk ever carries (`:itktransform` +writes `.h5`) goes in `SUPPORTED_BACKEND_FORMATS` instead, because only +extensions are probed on disk. + +```python +# chunked_backend.py +from konfai.utils.dataset import AbstractFile, Attribute, BACKENDS +from konfai.utils.errors import DatasetManagerError + + +class ChunkedFile(AbstractFile): + """One case per store, decoded in blocks.""" + + single_store = False # True: one store holds every case (like one .h5 file) + concurrent_write_safe = False # entries share handles/metadata, so writes stay serial + case_file_suffix = None # what a case carries implicitly on disk (H5File: ".h5") + reads_remote = False # True: the backend opens URI roots (OME-Zarr does) + writes_pyramid = False # True: a written store can hold multiscale levels + lists_case_entries = False # True: a case is a directory the backend enumerates + + def __init__(self, filename: str, read: bool) -> None: + try: + import mychunklib # noqa: F401 + except ImportError as e: + raise DatasetManagerError( + "mychunklib is required to read '.blk' stores.", + "Install it with: pip install mychunklib", + ) from e + ... + + def __enter__(self): ... + def __exit__(self, exc_type, value, traceback): ... + def file_to_data(self, group, name): ... # whole entry + Attribute + def file_to_data_slice(self, group, name, slices): ... # one region + def data_to_file(self, name, data, attributes=None): ... # whole write + + def read_granularity(self, name): + # The stored block a region read is served in, as a C[Z]YX shape. + return (1, 64, 64, 64) + + +BACKENDS["blk"] = ChunkedFile +``` + +Two of the contract's methods matter more than they look: + +- **`read_granularity(name)`**: the block a region read is actually served in. + A chunked store decodes whole blocks, so a window costs the block-aligned + hull covering it; a memory-mapped one is served band by band (`SitkFile` + answers `(1, 1, Y, X)` for a `.mha`: one step along the outermost axis a + window spans, every axis below it whole, because those are the pages the + read touches). The streaming sweep is priced and cut on this grid, so the + grain need not be isotropic and need not come from a compressor. A backend + that stays silent (`None`) is priced at what its reads ask for, which is + right only when a read costs exactly that. +- **`bounded_region_reads(name)`**: whether a region read decodes only the + region. The base answers `False`, which is the safe direction: a wrong + `False` costs speed (the plan prefers one ordered whole read), never + correctness. + +Import-guard the heavy library and raise a `DatasetManagerError` naming the +install, at the point of use, never a bare `ImportError` at import time: a bare +install must still import `konfai.utils.dataset`. Declare +`can_stream(file_format, attributes)` `True` (and implement +`open_data_stream`) only when the backend serves incremental region writes; +the default routes writes whole through `data_to_file`. + +## A reduction operator + +A reduction folds N tensors into one, and one vocabulary serves two engines: +the predictor folds one case's copies (ensemble, TTA), and the TRANSFORM +`Reduce` stage folds N cases into one. Subclass `konfai.data.reduction.Reduction` +and reference it by classpath wherever a `reduction` is configured. + +```python +# my_reduction.py +import torch + +from konfai.data.reduction import Reduction + + +class TrimmedMean(Reduction): + """Mean of the members left after dropping each voxel's min and max.""" + + voxel_local = True # every output voxel reads only the SAME voxel of each member + incremental = False # __call__ needs all members at once + working_multiple = 4.0 # the stacked float copy plus the reduction buffers + + def __call__(self, tensors: list[torch.Tensor]) -> torch.Tensor: + stack = torch.stack([tensor.float() for tensor in tensors]) + trimmed = stack.sum(dim=0) - stack.amax(dim=0) - stack.amin(dim=0) + return trimmed / (len(tensors) - 2) +``` + +The list is the fold axis: one tensor per member (a model of an ensemble, a +case of a cohort), each in the `[1, K, C, *spatial]` layout both engines hand +over. The declarations: + +- **`voxel_local`**: declare `True` only if every output voxel depends on the + same voxel of each input. **The streamed gates trust this flag and check + nothing else: a wrong `True` corrupts a streamed output** (each region is + reduced with its own members only), while a wrong `False` merely costs the + whole-volume path. The TRANSFORM `Reduce` stage refuses a non-`voxel_local` + operator outright. +- **`incremental`**: `True` when the operator can fold members one at a time; + then override the `start` / `accumulate` / `finalize` protocol and the + working set stays two regions whatever N is. `Mean` and `Std` do; `Median` + cannot. +- **`working_multiple`** and **`working_multiple_for(cases)`**: the + buffers-worth the operator allocates on top of what it is handed; the plan + multiplies it into the peak it sizes regions against. The attribute is the + worst case; an operator whose route depends on the member count overrides + the method (`Median` selects the middle through element-wise min/max + networks up to five members and sorts the stack past that, so it answers + per count). +- **`output_channels(channels, cases)`**: override when the fold changes the + channel count (`Concat` returns `channels * cases`; the default returns + `channels`). + ## Quick contract table | Extension point | Recommended base class | Typical YAML entry point | @@ -332,6 +477,8 @@ Runtime contracts, each base class names exactly what you must implement: | Custom transform | `konfai.data.transform.Transform` or `TransformInverse` | `groups_dest..transforms` | | Custom augmentation | `konfai.data.augmentation.DataAugmentation` | `Dataset.augmentations.*.data_augmentations` | | Custom loss / metric | `konfai.metric.measure.Criterion` family | `outputs_criterions.*.targets_criterions.*.criterions_loader` | +| Storage backend | `konfai.utils.dataset.AbstractFile` (plus a `BACKENDS` entry) | the `:format` token in a group's `path` | +| Reduction operator | `konfai.data.reduction.Reduction` | `reduction` (predictor ensemble/TTA, TRANSFORM `Reduce`) | For a practical, contract-oriented guide with code snippets, see {doc}`../../usage/custom-models`. diff --git a/docs/source/reference/cli.md b/docs/source/reference/cli.md index d19e00ce..ac3753a7 100644 --- a/docs/source/reference/cli.md +++ b/docs/source/reference/cli.md @@ -8,7 +8,7 @@ KonfAI ships six command-line entrypoints, across four packages: | Command | Package | Purpose | | --- | --- | --- | | `konfai` | `konfai` | run a YAML workflow: train, predict, evaluate, transform | -| `konfai-cluster` | `konfai` (`cluster` extra) | submit those workflows to SLURM | +| `konfai-cluster` | `konfai` (submission needs the `cluster` extra) | submit those workflows to SLURM | | `konfai-apps` | `konfai-apps` | run a packaged App | | `konfai-apps-server` | `konfai-apps` | serve Apps over HTTP | | `konfai-mcp` | `konfai-mcp` | expose KonfAI to an LLM agent | @@ -29,6 +29,7 @@ Use `konfai` when you are still designing a workflow directly from YAML. | `PREDICTION` | Run inference using one or more checkpoints. | | `EVALUATION` | Compute metrics on saved outputs. | | `TRANSFORM` | Prepare a dataset: apply a transform chain and write the result. | +| `list` | Print the components a YAML config can reference (see below). | ### Common options @@ -41,9 +42,10 @@ meanings noted below, and has **no** `-tb`. | `-c`, `--config` | YAML file to use. | | `-y`, `--overwrite` | Overwrite existing outputs without prompting. Under `TRANSFORM`: recompute cases whose output exists, without it such a case is skipped, and nothing prompts. | | `--gpu` | One or more GPU ids. | -| `--cpu` | Number of CPU workers when not using GPUs. Under `TRANSFORM`: shard the cases over N worker processes (default 1). | +| `--cpu` | Number of CPU worker processes when no `--gpu` is given; the run stays on CPU unless `--gpu` is passed. Under `TRANSFORM`: shard the cases over N worker processes (default 1). | | `-q`, `--quiet` | Reduce console output. | | `-tb`, `--tensorboard` | Launch TensorBoard. Not accepted by `TRANSFORM`. | +| `--init` | Create the config file if missing, resolve every default into it, and exit without running. | ### Default config file per command @@ -60,6 +62,27 @@ current directory**: Reading a config rewrites it on disk: after a run your YAML holds the resolved defaults. See {doc}`../concepts/configuration`. +### Generating a config: `--init` + +`konfai --init` is how a config file is generated: it creates the +command's config file when missing (seeded with its root key), binds the +workflow once so every default resolves into the file, and exits without +running anything. `-c` picks the filename. A binding error after partial +resolution still leaves what resolved on disk, plus the error naming the key. + +```bash +konfai TRAIN --init -c Config.yml +``` + +### `konfai list` + +`konfai list {transforms,augmentations,criteria,reductions,models,blocks}` +prints one component family: the exact spelling a YAML config references, and +each component's one-line doc. `konfai list models` covers both the Python +catalog (`segmentation.UNet.UNet`) and the declarative catalog +(`default|UNet.yml`). `list` takes none of the run flags and loads no torch for +`--help`. + ### Command-specific options `TRAIN` @@ -108,7 +131,10 @@ defaults. See {doc}`../concepts/configuration`. The default is **CPU**: `--gpu` defaults to an empty list, so pass `--gpu 0` to use a card. An id that is not among the visible CUDA devices is a usage error (exit code 2), checked once the command is dispatched so that `--help` never -loads torch. `--cpu` must be greater than 0. +loads torch. `--cpu` must be greater than 0. Unless `-q` is passed, every run +prints one startup line naming the resolved devices (`[KonfAI] Running on +cuda:0`, or `[KonfAI] Running on CPU (4 workers)`), so a silent CPU fallback on +a GPU machine is visible. `--version` works on the root parser, `konfai --version`, not on a subcommand. ## `konfai-apps` @@ -235,8 +261,9 @@ Important options: ## `konfai-cluster` Cluster-oriented wrapper around the low-level `konfai` commands: it takes the -same workflow arguments and submits them to SLURM through `submitit`. Depends on -the optional `cluster` extra. +same workflow arguments and submits them to SLURM through `submitit`. The +command ships with the core package; submitting needs `submitit`, which the +`cluster` extra installs. | Option | Default | Meaning | | --- | --- | --- | diff --git a/docs/source/reference/components/index.md b/docs/source/reference/components/index.md index 3bb8cf66..fd287f57 100644 --- a/docs/source/reference/components/index.md +++ b/docs/source/reference/components/index.md @@ -50,11 +50,12 @@ reflection engine binds YAML keys directly to constructor parameter names. Two ways to get the exhaustive list for any component: 1. **Let KonfAI materialise the defaults.** Reference the component in a config - and run the workflow (or run with `KONFAI_CONFIG_MODE=default`). KonfAI writes + and run `konfai --init` (or the workflow itself). KonfAI writes every resolved default back into the YAML file, giving you a complete, fully-expanded subtree to edit. (This is the same [config-mutation behaviour](../../concepts/configuration.md) that surprises - new users: here it is a feature.) + new users: here it is a feature.) `konfai list ` prints every + component's exact YAML spelling. 2. **Read the signature.** Where a bare name is looked up depends on the kind: | Kind | Bare name resolves in | diff --git a/docs/source/reference/components/models.md b/docs/source/reference/components/models.md index 8fd06c0a..e8e6e0c6 100644 --- a/docs/source/reference/components/models.md +++ b/docs/source/reference/components/models.md @@ -110,6 +110,52 @@ reusable pieces are the vocabulary: - **Tensor ops** (leaf modules): `Add`, `Multiply`, `Concat`, `Detach`, `ArgMax`, `Select`, `View`, `Permute`, `NormalNoise`, and more. +## Start from MONAI, torchvision or nnU-Net weights (`pretrained_from`) + +A fresh TRAIN can seed its model from a checkpoint trained in another +framework, from config alone. `Model.pretrained_from` builds the reference +network, loads the checkpoint into it, and transfers the weights into the +KonfAI graph by forward-execution order (no key map): the bridge fills **every** +target tensor or raises, so a partial transfer is never reported as success. + +```yaml +Trainer: + Model: + classpath: default|PlainConvUNet.yml + pretrained_from: + checkpoint: ./nnunet_fold0.pt # raw state_dict, or a dict with a 'state_dict' entry; + # an https:// URL is accepted (weights-only load) + builder: monai.networks.nets:UNet # classpath of the reference class + args: {spatial_dims: 3, in_channels: 1, out_channels: 2, channels: [32, 64], strides: [2]} + input_shape: [96, 96, 96] # optional; else derived from the model's own + # patch size or downsampling factors +``` + +The seed runs only on a fresh TRAIN: a RESUME or PREDICTION checkpoint always +wins, and PREDICTION never builds (or needs) the reference. A multi-input graph +or a free-axis patch size cannot derive a synthetic input on its own: +`input_shape` is the escape hatch, and the failure is a `ConfigError` naming +`Model.pretrained_from`. The transfer itself is +`konfai.utils.pretrained.transfer_weights_by_execution_order` (see the API +reference). + +## Fine-tuning across a different head (`allow_head_resize`) + +By default a checkpoint load **refuses shape mismatches**: the strict load +raises, naming the tensor and both shapes. To fine-tune across a head whose +shape changed (a different label count, say), opt in with +`Model.allow_head_resize: true`: the load then warm-starts the overlapping +slice of each mismatched tensor and logs a warning per resized tensor. The +loader propagates the opt-in to every nested network and can only enable it, +never disable a model class's own constructor opt-in. + +```yaml +Trainer: + Model: + classpath: segmentation.UNet.UNet + allow_head_resize: true +``` + ## Next steps - {doc}`../../concepts/model-graph`: named outputs, `outputs_criterions`, patching diff --git a/docs/source/reference/components/storage-backends.md b/docs/source/reference/components/storage-backends.md index 90c246f5..b405ae14 100644 --- a/docs/source/reference/components/storage-backends.md +++ b/docs/source/reference/components/storage-backends.md @@ -352,3 +352,5 @@ multi-dimensional does not survive a read. Geometry is safe because `Origin` and - {doc}`../../concepts/datasets`: grouped dataset layout, selectors, patching - {doc}`../../concepts/streaming`: locality declarations, planner rules, and fallbacks - {doc}`transforms`: transform capabilities and streamability +- {doc}`../api/extension-points`: adding your own backend (the `AbstractFile` + declarations and the `BACKENDS` registry) diff --git a/docs/source/reference/components/transforms.md b/docs/source/reference/components/transforms.md index 0b12e849..360a5ebe 100644 --- a/docs/source/reference/components/transforms.md +++ b/docs/source/reference/components/transforms.md @@ -180,7 +180,7 @@ case (`mha`, `nii`, …) has no such window. `dataset` takes a format token (`mh | `Write` | A `Save` that is a **deliverable**: same boundary and same arguments, but `dataset` has no default, so a bare `Write:` fails at config time instead of writing into the source tree. | same as `Save` | | `Reduce` | Folds every case of a group into one volume, at fixed voxel: the stage that makes a chain N-to-1. `operator` is a `Reduction` classpath resolved against `konfai.data.reduction` (`Mean`, `Median`, `Std`, `Vote`, `Concat`), `output` names the single result, and `grid` demands `strict` (extents and geometry), `shape_only` or `reference:`. | `operator="Median"`, `output` (required), `grid="strict"`, `grid_tolerance=1e-6` | | `Expand` | Turns one case into `nb` copies at a declared point of the chain: `Reduce`'s mirror, 1-to-N. Stages before it run once per case, stages after it once per copy. `pattern` is a `str.format` template over `{name}` and `{a}`, both required so copies never collide. | `nb=2`, `pattern="{name}_{a:02d}"`, `seed=None` | -| `KonfAIInference` | Run a nested KonfAI app inference on the case: the volume is written to a temporary `.mha`, a spawned process resolves the app and loads the model, and the output is read back. That happens **once per case**, so a cohort loads the model as many times as it has cases; inference over a cohort is `PREDICTION`'s job, this stage is for an inference that is one step of a chain. Needs `konfai-apps` and `num_workers: 0`; defaults to a specific HF repo. | no, whole-volume by construction | +| `KonfAIInference` | Run a nested KonfAI app inference on the case: the volume is written to a temporary `.mha`, a spawned process resolves the app and loads the model, and the output is read back. That happens **once per case**, so a cohort loads the model as many times as it has cases; inference over a cohort is `PREDICTION`'s job, this stage is for an inference that is one step of a chain. Lives in `konfai_apps.transforms`: the bare name resolves through the loader when `konfai-apps` is installed and refuses with the install hint otherwise. Needs `num_workers: 0`; defaults to a specific HF repo. | no, whole-volume by construction | `†` changes the **channel** dimension, not spatial: no `transform_shape` override needed. diff --git a/docs/source/troubleshooting.md b/docs/source/troubleshooting.md index 8a44b332..196a2477 100644 --- a/docs/source/troubleshooting.md +++ b/docs/source/troubleshooting.md @@ -85,13 +85,14 @@ When in doubt: - `None` - a float ratio -- a `start:stop` slice string -- a path to a text file +- a `start:stop` slice string (a negative stop counts from the end) +- a path to a text file (`~path.txt` excludes instead) - an explicit list of indices - an explicit list of case names - a list mixing case names and text-file paths -If the split looks wrong, check which form your config is actually using. +`subset` accepts the same spellings: one grammar. If the split looks wrong, +check which form your config is actually using. ## Runtime problems diff --git a/docs/source/usage/adopting-konfai.md b/docs/source/usage/adopting-konfai.md index c407dcc3..22abd83b 100644 --- a/docs/source/usage/adopting-konfai.md +++ b/docs/source/usage/adopting-konfai.md @@ -66,6 +66,10 @@ For a weight-exact pair, `konfai.utils.pretrained.transfer_weights_by_execution_order` pairs weighted leaf modules in forward-execution order and checks every local state shape. It is useful when graph names differ but execution structure and parameters match. +The config entry point is `Model.pretrained_from` (checkpoint + reference +builder classpath + its args), so a fresh TRAIN seeds from another framework's +checkpoint without any Python: see the +["Start from MONAI, torchvision or nnU-Net weights" section](../reference/components/models.md). This bridge is strict, not universal. It raises `ConfigError` on a different leaf count, a per-leaf key or shape mismatch, a target tensor no traced leaf owns, and a @@ -109,7 +113,10 @@ the next layer. - MONAI's component breadth; - Lightning's ecosystem and maturity for arbitrary training-loop patterns; - a general spatial dependency compiler for every custom transform; -- published controlled benchmarks proving universal speedups over these tools. +- proof of universal speedups over these tools: the tracked harness + ({doc}`benchmarks`) reproduces the bounded-memory claim and pins the app + tables' protocol, but the comparisons are per app and per case, not a + general claim. ## Trust boundary diff --git a/docs/source/usage/benchmarks.md b/docs/source/usage/benchmarks.md new file mode 100644 index 00000000..907f7a2d --- /dev/null +++ b/docs/source/usage/benchmarks.md @@ -0,0 +1,61 @@ +# Benchmarks and protocol + +Every performance number the documentation carries is meant to be re-runnable. +The tracked +[`benchmarks/`](https://github.com/fideus-labs/KonfAI/tree/main/benchmarks) +directory holds the harness; this page says what each script evidences and +under which protocol, so a published figure and your own re-run are compared on +the same footing. + +## Protocol + +- Wall time is the median of 3 runs after 1 warmup, on an otherwise idle + machine. +- Host memory is the peak resident set of the whole process tree (`psutil`), + sampled at 50 ms, so DataLoader workers and spawned ranks count. +- Device memory is `torch.cuda.max_memory_allocated()` plus the NVML process + figure when available. +- Every report line carries the konfai/torch/SimpleITK versions, CPU model, + GPU model, and the input's shape, dtype and checksum. + +## The streaming claim + +{doc}`../concepts/streaming` states that a case never has to fit in RAM: a +16 GiB uncompressed volume is transformed with peak host memory bounded by the +declared `memory_budget`, not by the volume. Reproduce it with one command from +a checkout (needs the `imaging` extra and free disk for the synthetic volume): + +```bash +python benchmarks/bench_streaming.py --gib 16 --budget 1 +``` + +The script synthesizes a volume of `--gib` GiB, runs a real TRANSFORM chain +over it under a declared `--budget` GiB, and reports the whole-process-tree +peak RSS beside both figures. Smaller sizes (`--gib 4`, the default) tell the +same story faster. + +`benchmarks/bench_hotpaths.py` covers the framework-side hot paths (the +residual `Add` fold, the one-pass collate view, the deferred criterion +readout): micro-costs the docs assert are held rather than headline claims. + +## The app comparison tables + +The published tables (KonfAI-MRSegmentator and KonfAI-TotalSegmentator against +the original tools, same weights, same card) live with the apps: +[MRSegmentator](https://github.com/fideus-labs/KonfAI/tree/main/apps/mrsegmentator) +and +[TotalSegmentator](https://github.com/fideus-labs/KonfAI/tree/main/apps/totalsegmentator). +Each bundle README states its case sizes, ensemble and hardware conditions and +is produced under the protocol above; re-running them needs the published +weights and a licensed case, which is why they are app-level entries rather +than a synthetic one-command script. + +Those tables are per-app measurements, not a claim of universal speedups: +compare on your own cases before drawing conclusions for your workload. + +## See also + +- {doc}`../concepts/streaming`: the mechanism behind the memory claim +- {doc}`large-images`: putting bounded-memory runs to work +- [`benchmarks/README.md`](https://github.com/fideus-labs/KonfAI/tree/main/benchmarks): + the harness's own documentation diff --git a/docs/source/usage/custom-models.md b/docs/source/usage/custom-models.md index 474a1396..198a3185 100644 --- a/docs/source/usage/custom-models.md +++ b/docs/source/usage/custom-models.md @@ -201,11 +201,17 @@ reliable custom behavior, inheriting from `Network` is the supported path. Use `konfai.data.transform.Transform` for one-way transforms and `TransformInverse` when KonfAI must be able to invert the operation later. -The key methods are: - -- `__call__(name, tensor, cache_attribute)` to transform the tensor -- `transform_shape(...)` if the transform changes the tensor shape -- `inverse(...)` if you inherit from `TransformInverse` +The contract is tiered, and tier 0 is all a correct transform owes: + +- **Tier 0, correct**: `__call__(name, tensor, cache_attribute)`, plus + `transform_shape(...)` if the transform changes the spatial shape (and + `inverse(...)` if you inherit from `TransformInverse`). The stage then runs + on the whole volume and nothing silently breaks. +- **Tier 1, streaming**: set the `locality` class attribute (a `LocalityKind`; + plus `halo` for a bounded neighbourhood) and the stage streams. +- **Tier 2, streaming-aware**: method overrides, only where the answer depends + on the case or the stage owns a region's geometry or reads beside it. See + {doc}`../reference/api/extension-points`. `cache_attribute` is where you should save anything needed later by the inverse transform. diff --git a/docs/source/usage/index.rst b/docs/source/usage/index.rst index ab6c8e81..07f5d9a7 100644 --- a/docs/source/usage/index.rst +++ b/docs/source/usage/index.rst @@ -24,4 +24,5 @@ The guides: - :doc:`python-workflows` - :doc:`large-images` - :doc:`custom-models` +- :doc:`benchmarks` - :doc:`docker` diff --git a/docs/source/usage/python-workflows.md b/docs/source/usage/python-workflows.md index 6f35552a..f31f48bc 100644 --- a/docs/source/usage/python-workflows.md +++ b/docs/source/usage/python-workflows.md @@ -43,6 +43,20 @@ in place. Two stages of the same class in one chain spell the second one module- `konfai.plan_transform(...)` takes the same arguments and returns the `TransformPlan` without running anything: plan first is the same reflex in Python as on the CLI. +```{note} +**Migration note for Python callers of `Network`.** `Network.state_dict()` now +honors the torch signature and returns the torch-native flat dict (still +skipping nested `Network`s); the KonfAI aggregate that checkpoints are built +from is `network_states()`. Checkpoint **files on disk are unchanged**: nothing +saved by an earlier version needs converting, and RESUME/PREDICTION read them +as before. Only code that builds or unpacks checkpoint dicts in Python must +switch from `state_dict()` to `network_states()`. The KonfAI traversals moved +with it: `graph_parameters(pretrained=...)` replaces the old `parameters(pretrained)` +override, and `graph_apply()` the custom `apply()`; torch's native +`parameters()` / `named_parameters()` / `apply()` are back to their own +semantics. +``` + ## Which spelling fits which workflow | Workflow | Its config is… | The Python spelling | diff --git a/konfai/transformer.py b/konfai/transformer.py index cb8a8245..795be532 100644 --- a/konfai/transformer.py +++ b/konfai/transformer.py @@ -118,8 +118,8 @@ class TransformPlan: #: columns above have no room for. Part of the plan, not of the run, so ``--plan`` carries it: #: a note only worth reading after the bytes are written is not worth printing. notes: tuple[str, ...] = () - #: Per (group_src, group_dest): the chain spelled out with its destination: the one fact a - #: reader wants from a plan line ("what runs, and where does it land"). + #: The chain spelled out with its destination, one label per ``(group_src, group_dest)`` + #: pair: the one fact a reader wants from a plan line ("what runs, and where does it land"). chain_labels: dict[tuple[str, str], str] = field(default_factory=dict) #: What the store's decoded-chunk cache may hold out of the budget: part of what the process #: holds, so the header says it, and says when a budget puts it under the floor it is worth. diff --git a/konfai/utils/config.py b/konfai/utils/config.py index 6e308856..5573ced8 100755 --- a/konfai/utils/config.py +++ b/konfai/utils/config.py @@ -271,14 +271,14 @@ class Config: Context manager for reading and updating a subtree of the active YAML config. + Inside a :func:`strict_config` block the context reads the block's in-memory tree and folds + what it set back into it on exit; outside one it loads and writes the file itself. + Parameters ---------- key : str Dot-separated path pointing to the configuration subtree to inspect or materialize. - - Inside a :func:`strict_config` block the context reads the block's in-memory tree and folds - what it set back into it on exit; outside one it loads and writes the file itself. """ def __init__(self, key: str) -> None: diff --git a/pixi.lock b/pixi.lock index 8e8dfd08..1b30dca5 100644 --- a/pixi.lock +++ b/pixi.lock @@ -51,11 +51,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/a6/bd32357e6cc1ecb473afd78193d7231724f284435d2db25696ecfaaa1503/onnx-1.22.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl @@ -70,12 +67,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/4e/8d7656ccaab3e81e97258b8a9bc5f0c8502513a92fb4ceb0a2cbfebc17bf/numpy-2.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -83,41 +78,25 @@ environments: - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl @@ -132,28 +111,20 @@ environments: - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/50/dfce42eff2cecabcd5a9bbad5489449c87db3415f408d23ffee417ce01f6/coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9f/68/ed67a355a62848ee04bb4f01e89d3be871052c2c3ae6d5fc0fb2f6010979/simpleitk-2.5.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl @@ -164,9 +135,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl @@ -174,29 +143,22 @@ environments: - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c4/e6/d359fdd37498e74d26a167f7a51e54542e642ea47181eb4e643a69a066c3/numcodecs-0.16.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/19/14929c3c2fe0b79b41cce24463062bf3afa4cdd3c19dccf00319caa92bff/onnxruntime-1.27.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d7/8c/e9019a28e908214031310aefd78e4755221d02303190b54b2c85cb69573e/wasmtime-45.0.0-py3-none-manylinux1_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl @@ -206,20 +168,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f0/48/3d07340e3256b2cdf441b883da4fd2f7ab89d4894da94a4c627ba8b2f9eb/zarrista-0.1.0-cp311-abi3-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f3/bd/d3348d62296e73a91420f0edf671450c9003ecd8704da40b1e3d9b868459/sphinxcontrib_mermaid-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl @@ -247,11 +204,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl - - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0b/35/1cceccc5fcb50fa2ed53e2aa278cd032f3902682a73e763fb1ac3be8e6fa/rich_argparse-1.8.0-py3-none-any.whl @@ -264,28 +217,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/d8/f37480ebc669af91ebab0b01ab649413446bd5cce365f3f50b71d05be2af/zarrista-0.1.0-cp311-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl @@ -293,19 +237,12 @@ environments: - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5b/0b/a81b5daf5adea482ecb68d9615f6a348486ab4d8e980a915d4420e57ee4d/wasmtime-45.0.0-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/34/4b3208b35dea488263a5c9f4a464ef20316f663e9e90d5de61349c31b327/simpleitk-2.5.5-cp311-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl @@ -318,69 +255,49 @@ environments: - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/4b/953118a730ee3b35e28645e0eb4cf9beec5bdbb954e1ac2f5fcefba6bbc3/numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/c0/5f84ba7525577c1b9909fc2d06ef11314825fc4ad4378f61d0e4c9883b4a/numcodecs-0.16.5-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl @@ -389,16 +306,12 @@ environments: - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/6a/481561f1093834376ed493e4ca42a73e5be0d50031f2969c86593bdc7c96/onnx-1.22.0-cp312-abi3-macosx_12_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f3/bd/d3348d62296e73a91420f0edf671450c9003ecd8704da40b1e3d9b868459/sphinxcontrib_mermaid-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl @@ -426,11 +339,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl @@ -451,41 +361,25 @@ environments: - pypi: https://files.pythonhosted.org/packages/27/72/6663cc0382ddbb866136c255c837bcb96cc7ce5e83562efec55e1b995941/numcodecs-0.16.5-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/53/491e1c47c55b62ccc6a63c1c5b8635c73fc2258dddeb9bda27cae4a0ae96/numpy-2.5.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/ab/3a03de43173749171340ad974636105cb02c780f76055551c874c3df7fff/simpleitk-2.5.5-cp311-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/e8/196ebc25d8f34c06d43a6e9c8513c9266ef8dbf3b5672beb1a00cf5e29fa/coverage-7.14.3-cp313-cp313-win_amd64.whl @@ -499,25 +393,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl @@ -525,10 +413,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/2e/59f3ab896f5a8c76a3d009c40015a32660b1b05b0ff055efcee3aa977caa/zarrista-0.1.0-cp311-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl @@ -536,27 +422,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/fa/824ddb662af55b2eabc0dbb7b57c7c0b1bcd93693754a2b8509ec4d16490/torch-2.12.1-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl @@ -565,20 +443,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f3/bd/d3348d62296e73a91420f0edf671450c9003ecd8704da40b1e3d9b868459/sphinxcontrib_mermaid-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl dev: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -614,11 +486,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/a6/bd32357e6cc1ecb473afd78193d7231724f284435d2db25696ecfaaa1503/onnx-1.22.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl @@ -635,12 +504,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/4e/8d7656ccaab3e81e97258b8a9bc5f0c8502513a92fb4ceb0a2cbfebc17bf/numpy-2.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -648,45 +515,29 @@ environments: - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl @@ -703,31 +554,23 @@ environments: - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/50/dfce42eff2cecabcd5a9bbad5489449c87db3415f408d23ffee417ce01f6/coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9f/68/ed67a355a62848ee04bb4f01e89d3be871052c2c3ae6d5fc0fb2f6010979/simpleitk-2.5.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl @@ -738,9 +581,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl @@ -750,29 +591,22 @@ environments: - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c4/e6/d359fdd37498e74d26a167f7a51e54542e642ea47181eb4e643a69a066c3/numcodecs-0.16.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/19/14929c3c2fe0b79b41cce24463062bf3afa4cdd3c19dccf00319caa92bff/onnxruntime-1.27.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d7/8c/e9019a28e908214031310aefd78e4755221d02303190b54b2c85cb69573e/wasmtime-45.0.0-py3-none-manylinux1_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl @@ -782,23 +616,18 @@ environments: - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ea/bb/711e1c2ebd18a21202c972dd5d5c8e09a921f2d3560e3a53d6350c808ab7/submitit-1.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f0/48/3d07340e3256b2cdf441b883da4fd2f7ab89d4894da94a4c627ba8b2f9eb/zarrista-0.1.0-cp311-abi3-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f3/bd/d3348d62296e73a91420f0edf671450c9003ecd8704da40b1e3d9b868459/sphinxcontrib_mermaid-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl @@ -827,11 +656,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl - - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0b/35/1cceccc5fcb50fa2ed53e2aa278cd032f3902682a73e763fb1ac3be8e6fa/rich_argparse-1.8.0-py3-none-any.whl @@ -844,31 +669,22 @@ environments: - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/d8/f37480ebc669af91ebab0b01ab649413446bd5cce365f3f50b71d05be2af/zarrista-0.1.0-cp311-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl @@ -876,19 +692,12 @@ environments: - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5b/0b/a81b5daf5adea482ecb68d9615f6a348486ab4d8e980a915d4420e57ee4d/wasmtime-45.0.0-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/34/4b3208b35dea488263a5c9f4a464ef20316f663e9e90d5de61349c31b327/simpleitk-2.5.5-cp311-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl @@ -902,72 +711,52 @@ environments: - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/4b/953118a730ee3b35e28645e0eb4cf9beec5bdbb954e1ac2f5fcefba6bbc3/numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/c0/5f84ba7525577c1b9909fc2d06ef11314825fc4ad4378f61d0e4c9883b4a/numcodecs-0.16.5-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl @@ -976,20 +765,16 @@ environments: - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ea/bb/711e1c2ebd18a21202c972dd5d5c8e09a921f2d3560e3a53d6350c808ab7/submitit-1.5.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/6a/481561f1093834376ed493e4ca42a73e5be0d50031f2969c86593bdc7c96/onnx-1.22.0-cp312-abi3-macosx_12_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f3/ab/a7653bce9a3b204be6a6931767a9e23595807bb84790ce6685e4d7e5bd08/nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f3/bd/d3348d62296e73a91420f0edf671450c9003ecd8704da40b1e3d9b868459/sphinxcontrib_mermaid-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl @@ -1018,11 +803,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl @@ -1043,44 +825,28 @@ environments: - pypi: https://files.pythonhosted.org/packages/27/72/6663cc0382ddbb866136c255c837bcb96cc7ce5e83562efec55e1b995941/numcodecs-0.16.5-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/53/491e1c47c55b62ccc6a63c1c5b8635c73fc2258dddeb9bda27cae4a0ae96/numpy-2.5.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/ab/3a03de43173749171340ad974636105cb02c780f76055551c874c3df7fff/simpleitk-2.5.5-cp311-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/e8/196ebc25d8f34c06d43a6e9c8513c9266ef8dbf3b5672beb1a00cf5e29fa/coverage-7.14.3-cp313-cp313-win_amd64.whl @@ -1097,27 +863,21 @@ environments: - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/fa/2b5d684e3edf1e81bfd02d298c78c3e3da77ca1d8a2be3183a79544a7548/nh3-0.3.6-cp38-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl @@ -1125,10 +885,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/2e/59f3ab896f5a8c76a3d009c40015a32660b1b05b0ff055efcee3aa977caa/zarrista-0.1.0-cp311-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl @@ -1136,27 +894,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/fa/824ddb662af55b2eabc0dbb7b57c7c0b1bcd93693754a2b8509ec4d16490/torch-2.12.1-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl @@ -1166,23 +916,17 @@ environments: - pypi: https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ea/bb/711e1c2ebd18a21202c972dd5d5c8e09a921f2d3560e3a53d6350c808ab7/submitit-1.5.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f3/bd/d3348d62296e73a91420f0edf671450c9003ecd8704da40b1e3d9b868459/sphinxcontrib_mermaid-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl docs: channels: @@ -1219,11 +963,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/a6/bd32357e6cc1ecb473afd78193d7231724f284435d2db25696ecfaaa1503/onnx-1.22.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl @@ -1238,12 +979,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/4e/8d7656ccaab3e81e97258b8a9bc5f0c8502513a92fb4ceb0a2cbfebc17bf/numpy-2.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -1251,41 +990,25 @@ environments: - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl @@ -1300,28 +1023,20 @@ environments: - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/50/dfce42eff2cecabcd5a9bbad5489449c87db3415f408d23ffee417ce01f6/coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9f/68/ed67a355a62848ee04bb4f01e89d3be871052c2c3ae6d5fc0fb2f6010979/simpleitk-2.5.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl @@ -1332,9 +1047,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl @@ -1342,29 +1055,22 @@ environments: - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c4/e6/d359fdd37498e74d26a167f7a51e54542e642ea47181eb4e643a69a066c3/numcodecs-0.16.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/19/14929c3c2fe0b79b41cce24463062bf3afa4cdd3c19dccf00319caa92bff/onnxruntime-1.27.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d7/8c/e9019a28e908214031310aefd78e4755221d02303190b54b2c85cb69573e/wasmtime-45.0.0-py3-none-manylinux1_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl @@ -1374,21 +1080,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f0/48/3d07340e3256b2cdf441b883da4fd2f7ab89d4894da94a4c627ba8b2f9eb/zarrista-0.1.0-cp311-abi3-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f3/bd/d3348d62296e73a91420f0edf671450c9003ecd8704da40b1e3d9b868459/sphinxcontrib_mermaid-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl @@ -1416,11 +1116,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl - - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0b/35/1cceccc5fcb50fa2ed53e2aa278cd032f3902682a73e763fb1ac3be8e6fa/rich_argparse-1.8.0-py3-none-any.whl @@ -1433,28 +1129,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/df/5ba47e12638ac537a8166a454c497032af83f2d5eb826bca81024ac8f78c/itkwasm-1.0b195-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/d8/f37480ebc669af91ebab0b01ab649413446bd5cce365f3f50b71d05be2af/zarrista-0.1.0-cp311-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl @@ -1462,19 +1149,12 @@ environments: - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5b/0b/a81b5daf5adea482ecb68d9615f6a348486ab4d8e980a915d4420e57ee4d/wasmtime-45.0.0-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/34/4b3208b35dea488263a5c9f4a464ef20316f663e9e90d5de61349c31b327/simpleitk-2.5.5-cp311-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl @@ -1487,69 +1167,49 @@ environments: - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/4b/953118a730ee3b35e28645e0eb4cf9beec5bdbb954e1ac2f5fcefba6bbc3/numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/c0/5f84ba7525577c1b9909fc2d06ef11314825fc4ad4378f61d0e4c9883b4a/numcodecs-0.16.5-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl @@ -1558,17 +1218,12 @@ environments: - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/6a/481561f1093834376ed493e4ca42a73e5be0d50031f2969c86593bdc7c96/onnx-1.22.0-cp312-abi3-macosx_12_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f3/bd/d3348d62296e73a91420f0edf671450c9003ecd8704da40b1e3d9b868459/sphinxcontrib_mermaid-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl @@ -1596,11 +1251,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl @@ -1621,41 +1273,25 @@ environments: - pypi: https://files.pythonhosted.org/packages/27/72/6663cc0382ddbb866136c255c837bcb96cc7ce5e83562efec55e1b995941/numcodecs-0.16.5-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/53/491e1c47c55b62ccc6a63c1c5b8635c73fc2258dddeb9bda27cae4a0ae96/numpy-2.5.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/ab/3a03de43173749171340ad974636105cb02c780f76055551c874c3df7fff/simpleitk-2.5.5-cp311-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/e8/196ebc25d8f34c06d43a6e9c8513c9266ef8dbf3b5672beb1a00cf5e29fa/coverage-7.14.3-cp313-cp313-win_amd64.whl @@ -1669,25 +1305,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl @@ -1695,10 +1325,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/2e/59f3ab896f5a8c76a3d009c40015a32660b1b05b0ff055efcee3aa977caa/zarrista-0.1.0-cp311-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl @@ -1706,27 +1334,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/c4/82/482a2e77a79a8ebc2f40d8e38cc186f2a72f2bf003c8e7395c3eefb9bc3f/shibuya-2026.5.19-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/fa/824ddb662af55b2eabc0dbb7b57c7c0b1bcd93693754a2b8509ec4d16490/torch-2.12.1-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl @@ -1735,21 +1355,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f3/bd/d3348d62296e73a91420f0edf671450c9003ecd8704da40b1e3d9b868459/sphinxcontrib_mermaid-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl lint: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -2684,13 +2297,11 @@ packages: - sphinx>=7.0 ; extra == 'dev' - shibuya ; extra == 'dev' - myst-parser ; extra == 'dev' - - nbsphinx ; extra == 'dev' - - ipython ; extra == 'dev' - - ipykernel ; extra == 'dev' - sphinx-autodoc-typehints ; extra == 'dev' - sphinx-copybutton ; extra == 'dev' - sphinx-togglebutton ; extra == 'dev' - sphinx-tabs ; extra == 'dev' + - sphinxcontrib-mermaid ; extra == 'dev' requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl name: distlib @@ -2778,23 +2389,6 @@ packages: - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' requires_python: '>=3.12' -- pypi: https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl - name: tornado - version: 6.5.7 - sha256: 148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl - name: nbsphinx - version: 0.9.8 - sha256: 92d95ee91784e56bc633b60b767a6b6f23a0445f891e24641ce3c3f004759ccf - requires_dist: - - docutils>=0.18.1 - - jinja2 - - nbconvert>=5.3,!=5.4 - - nbformat - - sphinx>=1.8,!=8.2.0,!=8.2.1 - - traitlets>=5 - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl name: roman-numerals version: 4.1.0 @@ -2805,11 +2399,6 @@ packages: version: 3.0.3 sha256: 9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - name: decorator - version: 5.3.1 - sha256: f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl name: triton version: 3.7.1 @@ -2830,11 +2419,6 @@ packages: - pandas ; extra == 'tutorials' - tabulate ; extra == 'tutorials' requires_python: '>=3.10,<3.15' -- pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - name: defusedxml - version: 0.7.1 - sha256: a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*' - pypi: https://files.pythonhosted.org/packages/09/a6/bd32357e6cc1ecb473afd78193d7231724f284435d2db25696ecfaaa1503/onnx-1.22.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl name: onnx version: 1.22.0 @@ -3052,10 +2636,6 @@ packages: - pytest-mypy ; extra == 'testing' - pytest ; extra == 'testing' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - name: ptyprocess - version: 0.7.0 - sha256: 4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 - pypi: https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl name: nvidia-ml-py version: 13.610.43 @@ -3132,15 +2712,6 @@ packages: - python-discovery>=1.4.2 - typing-extensions>=4.13.2 ; python_full_version < '3.11' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - name: referencing - version: 0.37.0 - sha256: 381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 - requires_dist: - - attrs>=22.2.0 - - rpds-py>=0.7.0 - - typing-extensions>=4.4.0 ; python_full_version < '3.13' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl name: ruff version: 0.15.2 @@ -3216,49 +2787,6 @@ packages: - sphinx>=5 ; extra == 'standalone' - pytest ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - name: nbclient - version: 0.11.0 - sha256: ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895 - requires_dist: - - jupyter-client>=7.0.0 - - jupyter-core>=5.4.0 - - nbformat>=5.2.0 - - traitlets>=5.13 - - pre-commit ; extra == 'dev' - - autodoc-traits ; extra == 'docs' - - flaky ; extra == 'docs' - - ipykernel>=6.19.3 ; extra == 'docs' - - ipython ; extra == 'docs' - - ipywidgets ; extra == 'docs' - - mock ; extra == 'docs' - - moto ; extra == 'docs' - - myst-parser ; extra == 'docs' - - nbconvert>=7.1.0 ; extra == 'docs' - - pytest-asyncio>=1.3.0 ; extra == 'docs' - - pytest-cov>=4.0 ; extra == 'docs' - - pytest>=9.0.1,<10 ; extra == 'docs' - - sphinx-book-theme ; extra == 'docs' - - sphinx>=1.7 ; extra == 'docs' - - sphinxcontrib-spelling ; extra == 'docs' - - testpath ; extra == 'docs' - - xmltodict ; extra == 'docs' - - flaky ; extra == 'test' - - ipykernel>=6.19.3 ; extra == 'test' - - ipython ; extra == 'test' - - ipywidgets ; extra == 'test' - - nbconvert>=7.1.0 ; extra == 'test' - - pytest-asyncio>=1.3.0 ; extra == 'test' - - pytest-cov>=4.0 ; extra == 'test' - - pytest>=9.0.1,<10 ; extra == 'test' - - testpath ; extra == 'test' - - xmltodict ; extra == 'test' - requires_python: '>=3.10.0' -- pypi: https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - name: tornado - version: 6.5.7 - sha256: 8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972 - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl name: twine version: 6.2.0 @@ -3288,46 +2816,6 @@ packages: version: 3.4.5 sha256: 290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80 requires_python: '>=3' -- pypi: https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl - name: ipykernel - version: 7.3.0 - sha256: 897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057 - requires_dist: - - appnope>=0.1.2 ; sys_platform == 'darwin' - - comm>=0.1.1 - - debugpy>=1.6.5 - - ipython>=7.23.1 - - jupyter-client>=8.9.0 - - jupyter-core>=5.1,!=6.0.* - - matplotlib-inline>=0.1 - - nest-asyncio2>=1.7.0 - - packaging>=22 - - psutil>=5.7 - - pyzmq>=25 - - tornado>=6.4.1 - - traitlets>=5.4.0 - - coverage[toml] ; extra == 'cov' - - matplotlib ; extra == 'cov' - - pytest-cov ; extra == 'cov' - - trio ; extra == 'cov' - - intersphinx-registry ; extra == 'docs' - - myst-parser ; extra == 'docs' - - pydata-sphinx-theme ; extra == 'docs' - - sphinx ; extra == 'docs' - - sphinx-autodoc-typehints ; extra == 'docs' - - sphinxcontrib-github-alt ; extra == 'docs' - - sphinxcontrib-spelling ; extra == 'docs' - - trio ; extra == 'docs' - - pyqt5 ; extra == 'pyqt5' - - pyside6 ; extra == 'pyside6' - - flaky ; extra == 'test' - - ipyparallel ; extra == 'test' - - pre-commit ; extra == 'test' - - pytest-asyncio>=0.23.5 ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-timeout ; extra == 'test' - - pytest>=7.0,<10 ; extra == 'test' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl name: onnxruntime version: 1.23.2 @@ -3354,37 +2842,6 @@ packages: requires_dist: - requests>=2.0.1,<3.0.0 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' -- pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - name: jupyter-client - version: 8.9.1 - sha256: 0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81 - requires_dist: - - jupyter-core>=5.1 - - python-dateutil>=2.8.2 - - pyzmq>=25.0 - - tornado>=6.4.1 - - traitlets>=5.3 - - typing-extensions>=4.13.0 - - ipykernel ; extra == 'docs' - - myst-parser ; extra == 'docs' - - pydata-sphinx-theme ; extra == 'docs' - - sphinx-autodoc-typehints ; extra == 'docs' - - sphinx>=4 ; extra == 'docs' - - sphinxcontrib-github-alt ; extra == 'docs' - - sphinxcontrib-spelling ; extra == 'docs' - - orjson ; extra == 'orjson' - - anyio ; extra == 'test' - - coverage ; extra == 'test' - - ipykernel>=6.14 ; extra == 'test' - - msgpack ; extra == 'test' - - mypy ; platform_python_implementation != 'PyPy' and extra == 'test' - - paramiko ; sys_platform == 'win32' and extra == 'test' - - pre-commit ; extra == 'test' - - pytest ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-jupyter[client]>=0.6.2 ; extra == 'test' - - pytest-timeout ; extra == 'test' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: nvidia-cufile version: 1.15.1.6 @@ -3422,79 +2879,11 @@ packages: - trove-classifiers>=2024.10.12 ; extra == 'tests' - defusedxml ; extra == 'xmp' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - name: ipython - version: 9.15.0 - sha256: 515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e - requires_dist: - - colorama>=0.4.4 ; sys_platform == 'win32' - - decorator>=5.1.0 - - ipython-pygments-lexers>=1.0.0 - - jedi>=0.18.2 - - matplotlib-inline>=0.1.6 - - pexpect>4.6 ; sys_platform != 'emscripten' and sys_platform != 'win32' - - prompt-toolkit>=3.0.41,<3.1.0 - - psutil>=7 ; sys_platform != 'cygwin' and sys_platform != 'emscripten' - - pygments>=2.14.0 - - stack-data>=0.6.0 - - traitlets>=5.13.0 - - typing-extensions>=4.6 ; python_full_version < '3.12' - - black ; extra == 'black' - - docrepr ; extra == 'doc' - - exceptiongroup ; extra == 'doc' - - intersphinx-registry ; extra == 'doc' - - ipykernel ; extra == 'doc' - - ipython[matplotlib,test] ; extra == 'doc' - - setuptools>=80.0 ; extra == 'doc' - - sphinx-toml==0.0.4 ; extra == 'doc' - - sphinx-rtd-theme>=0.1.8 ; extra == 'doc' - - sphinx>=8.0 ; extra == 'doc' - - typing-extensions ; extra == 'doc' - - pytest>=7.0.0 ; extra == 'test' - - pytest-asyncio>=1.0.0 ; extra == 'test' - - testpath>=0.2 ; extra == 'test' - - packaging>=23.0.0 ; extra == 'test' - - setuptools>=80.0 ; extra == 'test' - - ipython[test] ; extra == 'test-extra' - - curio ; extra == 'test-extra' - - jupyter-ai ; extra == 'test-extra' - - ipython[matplotlib] ; extra == 'test-extra' - - nbformat ; extra == 'test-extra' - - nbclient ; extra == 'test-extra' - - ipykernel>6.30 ; extra == 'test-extra' - - numpy>=2.0 ; extra == 'test-extra' - - pandas>2.1 ; extra == 'test-extra' - - trio>=0.22.0 ; extra == 'test-extra' - - matplotlib>3.9 ; extra == 'matplotlib' - - ipython[doc,matplotlib,terminal,test,test-extra] ; extra == 'all' - - argcomplete>=3.0 ; extra == 'all' - - types-decorator ; extra == 'all' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - name: matplotlib-inline - version: 0.2.2 - sha256: 3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6 - requires_dist: - - traitlets - - flake8 ; extra == 'test' - - nbdime ; extra == 'test' - - nbval ; extra == 'test' - - notebook ; extra == 'test' - - pytest ; extra == 'test' - - matplotlib ; extra == 'test' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl name: ast-serialize version: 0.5.0 sha256: cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - name: jsonschema-specifications - version: 2025.9.1 - sha256: 98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe - requires_dist: - - referencing>=0.31.0 - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/41/d8/f37480ebc669af91ebab0b01ab649413446bd5cce365f3f50b71d05be2af/zarrista-0.1.0-cp311-abi3-macosx_11_0_arm64.whl name: zarrista version: 0.1.0 @@ -3537,13 +2926,6 @@ packages: - sphinx ; extra == 'docs' - gmpy2>=2.1.0a4 ; platform_python_implementation != 'PyPy' and extra == 'gmpy' - pytest>=4.6 ; extra == 'tests' -- pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - name: mistune - version: 3.3.2 - sha256: a678a56387d487db7368ede4647cb2ba1deff22ce61f92343e4ebe0ddfce4f2d - requires_dist: - - typing-extensions ; python_full_version < '3.11' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl name: pydicom version: 3.0.2 @@ -3645,11 +3027,6 @@ packages: - sphinx<6 ; extra == 'full' - tifffile ; extra == 'full' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - name: rpds-py - version: 2026.5.1 - sha256: b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e - requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl name: snowballstemmer version: 3.1.1 @@ -3755,14 +3132,6 @@ packages: - nvidia-cuda-opencl==13.0.85.* ; (sys_platform == 'linux' and extra == 'opencl') or (sys_platform == 'win32' and extra == 'opencl') - nvidia-cuda-profiler-api==13.0.85.* ; (sys_platform == 'linux' and extra == 'profiler') or (sys_platform == 'win32' and extra == 'profiler') - nvidia-cuda-sanitizer-api==13.0.85.* ; (sys_platform == 'linux' and extra == 'sanitizer') or (sys_platform == 'win32' and extra == 'sanitizer') -- pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - name: bleach - version: 6.4.0 - sha256: 4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081 - requires_dist: - - webencodings - - tinycss2>=1.1.0 ; extra == 'css' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/5b/0b/a81b5daf5adea482ecb68d9615f6a348486ab4d8e980a915d4420e57ee4d/wasmtime-45.0.0-py3-none-macosx_11_0_arm64.whl name: wasmtime version: 45.0.0 @@ -3799,11 +3168,6 @@ packages: - sphinx>=5 ; extra == 'standalone' - pytest ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - name: soupsieve - version: 2.8.4 - sha256: e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65 - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl name: imagesize version: 2.0.0 @@ -3818,24 +3182,6 @@ packages: - nvidia-nvjitlink - nvidia-cusparse requires_python: '>=3' -- pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl - name: tinycss2 - version: 1.5.1 - sha256: 3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 - requires_dist: - - webencodings>=0.4 - - sphinx ; extra == 'doc' - - furo ; extra == 'doc' - - pytest ; extra == 'test' - - ruff ; extra == 'test' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - name: comm - version: 0.2.3 - sha256: c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417 - requires_dist: - - pytest ; extra == 'test' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl name: librt version: 0.11.0 @@ -3849,11 +3195,6 @@ packages: - markupsafe>=2.0 - babel>=2.7 ; extra == 'i18n' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - name: attrs - version: 26.1.0 - sha256: c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl name: scikit-image version: 0.26.0 @@ -3956,84 +3297,6 @@ packages: version: 2.29.7 sha256: edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d requires_python: '>=3' -- pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - name: nbconvert - version: 7.17.1 - sha256: aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8 - requires_dist: - - beautifulsoup4 - - bleach[css]!=5.0.0 - - defusedxml - - importlib-metadata>=3.6 ; python_full_version < '3.10' - - jinja2>=3.0 - - jupyter-core>=4.7 - - jupyterlab-pygments - - markupsafe>=2.0 - - mistune>=2.0.3,<4 - - nbclient>=0.5.0 - - nbformat>=5.7 - - packaging - - pandocfilters>=1.4.1 - - pygments>=2.4.1 - - traitlets>=5.1 - - flaky ; extra == 'all' - - intersphinx-registry ; extra == 'all' - - ipykernel ; extra == 'all' - - ipython ; extra == 'all' - - ipywidgets>=7.5 ; extra == 'all' - - myst-parser ; extra == 'all' - - nbsphinx>=0.2.12 ; extra == 'all' - - playwright ; extra == 'all' - - pydata-sphinx-theme ; extra == 'all' - - pyqtwebengine>=5.15 ; extra == 'all' - - pytest>=7 ; extra == 'all' - - sphinx>=5.0.2 ; extra == 'all' - - sphinxcontrib-spelling ; extra == 'all' - - tornado>=6.1 ; extra == 'all' - - intersphinx-registry ; extra == 'docs' - - ipykernel ; extra == 'docs' - - ipython ; extra == 'docs' - - myst-parser ; extra == 'docs' - - nbsphinx>=0.2.12 ; extra == 'docs' - - pydata-sphinx-theme ; extra == 'docs' - - sphinx>=5.0.2 ; extra == 'docs' - - sphinxcontrib-spelling ; extra == 'docs' - - pyqtwebengine>=5.15 ; extra == 'qtpdf' - - pyqtwebengine>=5.15 ; extra == 'qtpng' - - tornado>=6.1 ; extra == 'serve' - - flaky ; extra == 'test' - - ipykernel ; extra == 'test' - - ipywidgets>=7.5 ; extra == 'test' - - pytest>=7 ; extra == 'test' - - playwright ; extra == 'webpdf' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - name: jsonschema - version: 4.26.0 - sha256: d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce - requires_dist: - - attrs>=22.2.0 - - jsonschema-specifications>=2023.3.6 - - referencing>=0.28.4 - - rpds-py>=0.25.0 - - fqdn ; extra == 'format' - - idna ; extra == 'format' - - isoduration ; extra == 'format' - - jsonpointer>1.13 ; extra == 'format' - - rfc3339-validator ; extra == 'format' - - rfc3987 ; extra == 'format' - - uri-template ; extra == 'format' - - webcolors>=1.11 ; extra == 'format' - - fqdn ; extra == 'format-nongpl' - - idna ; extra == 'format-nongpl' - - isoduration ; extra == 'format-nongpl' - - jsonpointer>1.13 ; extra == 'format-nongpl' - - rfc3339-validator ; extra == 'format-nongpl' - - rfc3986-validator>0.1.0 ; extra == 'format-nongpl' - - rfc3987-syntax>=1.1.0 ; extra == 'format-nongpl' - - uri-template ; extra == 'format-nongpl' - - webcolors>=24.6.0 ; extra == 'format-nongpl' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/6e/34/4b3208b35dea488263a5c9f4a464ef20316f663e9e90d5de61349c31b327/simpleitk-2.5.5-cp311-abi3-macosx_11_0_arm64.whl name: simpleitk version: 2.5.5 @@ -4045,11 +3308,6 @@ packages: requires_dist: - nvidia-cublas requires_python: '>=3' -- pypi: https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl - name: rpds-py - version: 2026.5.1 - sha256: 88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf - requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl name: pillow version: 12.2.0 @@ -4264,11 +3522,6 @@ packages: - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - name: appnope - version: 0.1.4 - sha256: 502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c - requires_python: '>=3.6' - pypi: https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl name: tifffile version: 2026.6.1 @@ -4361,13 +3614,6 @@ packages: version: 0.3.6 sha256: f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - name: prompt-toolkit - version: 3.0.52 - sha256: 9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955 - requires_dist: - - wcwidth - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/85/4b/953118a730ee3b35e28645e0eb4cf9beec5bdbb954e1ac2f5fcefba6bbc3/numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl name: numpy version: 2.5.0 @@ -4431,19 +3677,6 @@ packages: version: 1.10.0 sha256: 5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' -- pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl - name: beautifulsoup4 - version: 4.15.0 - sha256: d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9 - requires_dist: - - soupsieve>=1.6.1 - - typing-extensions>=4.0.0 - - cchardet ; extra == 'cchardet' - - chardet ; extra == 'chardet' - - charset-normalizer ; extra == 'charset-normalizer' - - html5lib ; extra == 'html5lib' - - lxml ; extra == 'lxml' - requires_python: '>=3.7.0' - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl name: lazy-loader version: '0.5' @@ -4489,12 +3722,6 @@ packages: - ml-dtypes>=0.5.0 - sympy>=1.13 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - name: pure-eval - version: 0.2.3 - sha256: 1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0 - requires_dist: - - pytest ; extra == 'tests' - pypi: https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl name: scikit-image version: 0.26.0 @@ -4572,13 +3799,6 @@ packages: version: 1.8.0 sha256: 57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl - name: pyzmq - version: 27.1.0 - sha256: 452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc - requires_dist: - - cffi ; implementation_name == 'pypy' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl name: werkzeug version: 3.1.8 @@ -4612,26 +3832,6 @@ packages: requires_dist: - ukkonen ; extra == 'license' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl - name: debugpy - version: 1.8.21 - sha256: b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - name: traitlets - version: 5.15.1 - sha256: 770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92 - requires_dist: - - myst-parser ; extra == 'docs' - - pydata-sphinx-theme ; extra == 'docs' - - sphinx ; extra == 'docs' - - argcomplete>=3.0.3 ; extra == 'test' - - mypy>=1.17.0,<1.19 ; extra == 'test' - - pre-commit ; extra == 'test' - - pytest-mock ; extra == 'test' - - pytest-mypy-testing ; extra == 'test' - - pytest>=7.0,<8.2 ; extra == 'test' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl name: jaraco-functools version: 4.5.0 @@ -4674,60 +3874,6 @@ packages: requires_dist: - pycparser ; implementation_name != 'PyPy' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - name: parso - version: 0.8.7 - sha256: a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c - requires_dist: - - flake8==5.0.4 ; extra == 'qa' - - types-setuptools==67.2.0.1 ; extra == 'qa' - - zuban==0.5.1 ; extra == 'qa' - - docopt ; extra == 'testing' - - pytest ; extra == 'testing' - requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - name: jedi - version: 0.20.0 - sha256: 7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67 - requires_dist: - - parso>=0.8.6,<0.9.0 - - django ; extra == 'dev' - - attrs ; extra == 'dev' - - colorama ; extra == 'dev' - - docopt ; extra == 'dev' - - flake8==7.1.2 ; extra == 'dev' - - pytest<9.0.0 ; extra == 'dev' - - types-setuptools==80.9.0.20250529 ; extra == 'dev' - - typing-extensions ; extra == 'dev' - - zuban==0.7.0 ; extra == 'dev' - - jinja2==3.1.6 ; extra == 'docs' - - markupsafe==3.0.3 ; extra == 'docs' - - pygments==2.20.0 ; extra == 'docs' - - sphinx==9.1.0 ; extra == 'docs' - - alabaster==1.0.0 ; extra == 'docs' - - babel==2.18.0 ; extra == 'docs' - - certifi==2026.4.22 ; extra == 'docs' - - charset-normalizer==3.4.7 ; extra == 'docs' - - docutils==0.22.4 ; extra == 'docs' - - idna==3.13 ; extra == 'docs' - - imagesize==2.0.0 ; extra == 'docs' - - iniconfig==2.3.0 ; extra == 'docs' - - packaging==26.2 ; extra == 'docs' - - pluggy==1.6.0 ; extra == 'docs' - - pytest==9.0.3 ; extra == 'docs' - - requests==2.33.1 ; extra == 'docs' - - roman-numerals==4.1.0 ; extra == 'docs' - - snowballstemmer==3.0.1 ; extra == 'docs' - - sphinx-rtd-theme==3.1.0 ; extra == 'docs' - - sphinxcontrib-applehelp==2.0.0 ; extra == 'docs' - - sphinxcontrib-devhelp==2.0.0 ; extra == 'docs' - - sphinxcontrib-htmlhelp==2.1.0 ; extra == 'docs' - - sphinxcontrib-jquery==4.1 ; extra == 'docs' - - sphinxcontrib-jsmath==1.0.1 ; extra == 'docs' - - sphinxcontrib-qthelp==2.0.0 ; extra == 'docs' - - sphinxcontrib-serializinghtml==2.0.0 ; extra == 'docs' - - urllib3==2.6.3 ; extra == 'docs' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl name: markupsafe version: 3.0.3 @@ -4774,12 +3920,6 @@ packages: - sphinx-book-theme ; extra == 'rtd' - sphinx-examples ; extra == 'rtd' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - name: pexpect - version: 4.9.0 - sha256: 7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 - requires_dist: - - ptyprocess>=0.5 - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl name: networkx version: 3.6.1 @@ -4958,25 +4098,6 @@ packages: version: 3.0.3 sha256: ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - name: nbformat - version: 5.10.4 - sha256: 3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b - requires_dist: - - fastjsonschema>=2.15 - - jsonschema>=2.6 - - jupyter-core>=4.12,!=5.0.* - - traitlets>=5.1 - - myst-parser ; extra == 'docs' - - pydata-sphinx-theme ; extra == 'docs' - - sphinx ; extra == 'docs' - - sphinxcontrib-github-alt ; extra == 'docs' - - sphinxcontrib-spelling ; extra == 'docs' - - pep440 ; extra == 'test' - - pre-commit ; extra == 'test' - - pytest ; extra == 'test' - - testpath ; extra == 'test' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl name: execnet version: 2.1.2 @@ -5006,11 +4127,6 @@ packages: version: 6.0.3 sha256: 2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - name: jupyterlab-pygments - version: 0.3.0 - sha256: 841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780 - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl name: zarr-metadata version: 0.5.0 @@ -5193,11 +4309,6 @@ packages: - cryptography>=2.0 - jeepney>=0.6 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - name: six - version: 1.17.0 - sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' - pypi: https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl name: scipy version: 1.18.0 @@ -5260,11 +4371,6 @@ packages: version: 1.2.0 sha256: 9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl - name: wcwidth - version: 0.8.1 - sha256: f453740b1e4a4f3291faa37944c555d71056c4da08d59809b307ef4feba695c8 - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl name: h5py version: 3.16.0 @@ -5277,19 +4383,6 @@ packages: version: 3.4.7 sha256: f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - name: executing - version: 2.2.1 - sha256: 760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017 - requires_dist: - - asttokens>=2.1.0 ; extra == 'tests' - - ipython ; extra == 'tests' - - pytest ; extra == 'tests' - - coverage ; extra == 'tests' - - coverage-enable-subprocess ; extra == 'tests' - - littleutils ; extra == 'tests' - - rich ; python_full_version >= '3.11' and extra == 'tests' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl name: sphinxcontrib-jsmath version: 1.0.1 @@ -5347,16 +4440,6 @@ packages: - importlib-metadata ; extra == 'test-extras' - crc32c ; extra == 'test-extras' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl - name: nest-asyncio2 - version: 1.7.2 - sha256: f5dfa702f3f81f6a03857e9a19e2ba578c0946a4ad417b4c50a24d7ba641fe01 - requires_python: '>=3.5' -- pypi: https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl - name: tornado - version: 6.5.7 - sha256: de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4 - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl name: pytest-xdist version: 3.8.0 @@ -5368,11 +4451,6 @@ packages: - psutil>=3.0 ; extra == 'psutil' - setproctitle ; extra == 'setproctitle' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl - name: rpds-py - version: 2026.5.1 - sha256: c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3 - requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/cb/19/14929c3c2fe0b79b41cce24463062bf3afa4cdd3c19dccf00319caa92bff/onnxruntime-1.27.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl name: onnxruntime version: 1.27.0 @@ -5385,19 +4463,6 @@ packages: - sympy ; extra == 'symbolic' - ml-dtypes ; extra == 'quantization' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - name: fastjsonschema - version: 2.21.2 - sha256: 1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463 - requires_dist: - - colorama ; extra == 'devel' - - jsonschema ; extra == 'devel' - - json-spec ; extra == 'devel' - - pylint ; extra == 'devel' - - pytest ; extra == 'devel' - - pytest-benchmark ; extra == 'devel' - - pytest-cache ; extra == 'devel' - - validictory ; extra == 'devel' - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl name: iniconfig version: 2.3.0 @@ -5465,17 +4530,6 @@ packages: - optree>=0.13.0 ; extra == 'optree' - pyyaml ; extra == 'pyyaml' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - name: asttokens - version: 3.0.1 - sha256: 15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a - requires_dist: - - astroid>=2,<5 ; extra == 'astroid' - - astroid>=2,<5 ; extra == 'test' - - pytest<9.0 ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-xdist ; extra == 'test' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl name: mypy version: 2.1.0 @@ -5538,13 +4592,6 @@ packages: - envwrap ; extra == 'telegram' - ipywidgets>=6 ; extra == 'notebook' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - name: ipython-pygments-lexers - version: 1.1.1 - sha256: a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c - requires_dist: - - pygments - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl name: ml-dtypes version: 0.5.4 @@ -5818,25 +4865,6 @@ packages: - zstandard ; python_full_version < '3.14' and extra == 'test-full' - tqdm ; extra == 'tqdm' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - name: jupyter-core - version: 5.9.1 - sha256: ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407 - requires_dist: - - platformdirs>=2.5 - - traitlets>=5.3 - - intersphinx-registry ; extra == 'docs' - - myst-parser ; extra == 'docs' - - pydata-sphinx-theme ; extra == 'docs' - - sphinx-autodoc-typehints ; extra == 'docs' - - sphinxcontrib-spelling ; extra == 'docs' - - traitlets ; extra == 'docs' - - ipykernel ; extra == 'test' - - pre-commit ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-timeout ; extra == 'test' - - pytest<9 ; extra == 'test' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl name: flatbuffers version: 25.12.19 @@ -5880,13 +4908,6 @@ packages: - pylint>=2.6.0 ; extra == 'dev' - pyink ; extra == 'dev' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - name: python-dateutil - version: 2.9.0.post0 - sha256: a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 - requires_dist: - - six>=1.5 - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' - pypi: https://files.pythonhosted.org/packages/ee/6a/481561f1093834376ed493e4ca42a73e5be0d50031f2969c86593bdc7c96/onnx-1.22.0-cp312-abi3-macosx_12_0_universal2.whl name: onnx version: 1.22.0 @@ -5903,11 +4924,6 @@ packages: version: 2026.6.17 sha256: 2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - name: pandocfilters - version: 1.5.1 - sha256: 93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' - pypi: https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl name: humanfriendly version: '10.0' @@ -5924,19 +4940,6 @@ packages: requires_dist: - zarr-metadata>=0.4 requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - name: stack-data - version: 0.6.3 - sha256: d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695 - requires_dist: - - executing>=1.2.0 - - asttokens>=2.1.0 - - pure-eval - - pytest ; extra == 'tests' - - typeguard ; extra == 'tests' - - pygments ; extra == 'tests' - - littleutils ; extra == 'tests' - - cython ; extra == 'tests' - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl name: pathspec version: 1.1.1 @@ -5986,10 +4989,6 @@ packages: - ruff ; extra == 'test' - sphinx ; extra == 'test' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - name: webencodings - version: 0.5.1 - sha256: a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl name: pygments version: 2.20.0 @@ -6113,18 +5112,6 @@ packages: - zarr ; extra == 'test' - jsonschema ; extra == 'validate' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl - name: debugpy - version: 1.8.21 - sha256: aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - name: pyzmq - version: 27.1.0 - sha256: 43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31 - requires_dist: - - cffi ; implementation_name == 'pypy' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: charset-normalizer version: 3.4.7 @@ -6149,13 +5136,6 @@ packages: requires_dist: - colorama ; sys_platform == 'win32' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl - name: pyzmq - version: 27.1.0 - sha256: 9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf - requires_dist: - - cffi ; implementation_name == 'pypy' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl name: rfc3986 version: 2.0.0 diff --git a/pyproject.toml b/pyproject.toml index cb47ee9f..f012a0a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,16 +105,15 @@ dev = [ "mypy", "build", "types-requests", + # The docs toolchain: keep in sync with docs/requirements.txt (the RTD list). "sphinx>=7.0", "shibuya", "myst-parser", - "nbsphinx", - "ipython", - "ipykernel", "sphinx-autodoc-typehints", "sphinx-copybutton", "sphinx-togglebutton", - "sphinx-tabs" + "sphinx-tabs", + "sphinxcontrib-mermaid" ] [tool.setuptools_scm] From bf874a599ee0a3c098374c35b200a7de5da8717f Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 10:15:19 +0200 Subject: [PATCH 26/28] ci: changelog for v1.8.3 --- CHANGELOG.md | 132 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f50eae1..9266b79a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,138 @@ draft, then say what a user of the package gets that they did not have -- and re against the commits that landed *after* you drafted it. Running the command over a section already written replaces it. +## v1.8.3 (2026-09-02) + +### Features + +- `konfai --init` generates a resolved default config: the file is created when + missing (seeded with its root key), every default is bound into it, and the command exits + without running. The former `KONFAI_CONFIG_MODE` generation modes + (`default`, `interactive`, `remove`) are gone; `Done` and `Import` remain. +- `konfai list {transforms,augmentations,criteria,reductions,models,blocks}` prints each + component family as the exact YAML spelling plus a one-line doc; the same catalog is + `konfai.list_components(kind)` in Python. +- `Model.pretrained_from` config entry: a fresh TRAIN seeds its model from another framework's + checkpoint (MONAI, torchvision, nnU-Net) by building the reference from `builder` + `args`, + loading `checkpoint` into it, and transferring weights in forward-execution order. The + transfer fills every target tensor or raises. +- `Model.allow_head_resize` (default false): checkpoint loads now refuse shape mismatches with + an error naming the tensor and both shapes; the opt-in warm-starts the overlapping slice for + fine-tuning across a different head. +- Tracked `benchmarks/` harness behind the documented performance claims: `bench_streaming.py` + reproduces the bounded-memory claim in one command, `bench_hotpaths.py` pins the framework + hot paths; docs gained a "Benchmarks and protocol" page and the site publishes `llms.txt` / + `llms-full.txt`. +- Transform authoring is a tiered contract: tier 0 is `__call__` alone (whole volume, nothing + breaks), tier 1 is the `locality` class attribute (plus `halo`), tier 2 the streaming-aware + method overrides; 25 method overrides collapsed into declarations. +- Storage backends are declarative: `AbstractFile` carries the per-backend facts as class + attributes (`single_store`, `concurrent_write_safe`, `case_file_suffix`, `reads_remote`, + `writes_pyramid`, `lists_case_entries`, `can_stream`) and `BACKENDS` is the one + token-to-class registry; a new format is one module plus one entry. +- DICOM series stream first-class: `read_granularity` answers the plane, a budget-capped + decoded-plane cache serves region reads, and a region no longer pays one full `dcmread` per + touched slice. +- Elastix-based transforms stream: fields are held as the control lattice only and evaluated + per region; masked `Standardize`/`Clip` are `GLOBAL_STAT` via masked dataset statistics. +- konfai-mcp: `fine_tune_app` restored as the one-call tier next to + `import_app` + `run_resume(weights_only=True)`, proven by an end-to-end fine-tune test. + +### Fixed + +- Config binder: wrong-shaped YAML (a nested block or list where a scalar is taken) is refused + with the dotted path instead of silently bound; an explicit `name: null` binds `None`, the + disabled spelling, instead of reactivating the default. +- RESUME never re-splits: the seed every preparation draw comes from is recorded in + `Statistics//Seed.txt` and read back on RESUME, so unseeded runs are reproducible + by default. +- Evaluation persists per case (JSONL beside the metric JSON) and prediction skips cases whose + outputs already exist (printed count, `--overwrite` recomputes), so both workflows resume + after a mid-cohort failure. +- Honest exits: no checkpoint junk on restart/no-progress exits, crash saves are named + `crash_*.pt` and never contend for best; TensorBoard is optional (no-op writer plus one + warning naming the extra); one startup line names the resolved devices; no CUDA probe at + import. +- Network/torch protocol: `state_dict()` honors the torch signature and returns the flat + torch-native dict (checkpoint files on disk unchanged); `network_states()` is the KonfAI + aggregate; `graph_parameters()` / `graph_apply()` carry the KonfAI traversals and torch's + native `parameters`/`named_parameters`/`apply` are back; EMA pairs both sides with the same + native traversal. +- Two distinct nested networks sharing a name are refused (the name is the checkpoint key); + a shared object under several module names is visited once. +- Alias-based weight remapping is segment-aligned (`layer1` no longer claims `layer10.*`) and + an alias shortfall raises a `ConfigError` naming the module. +- Criterion results are one typed contract (`CriterionResult`): losses, `(value, labels)` + pairs and maps normalize through it, a non-tensor return is refused naming the criterion; + mid-forward host syncs removed from the masked-loss family, Dice and TRE (deferred one-batch + readout); FID, TripletLoss, MutualInformationLoss, L1LossRepresentation and WGP deleted in + favor of classpath routes (torchmetrics/MONAI/torch); LPIPS scores every batch item instead + of the first; FocalLoss `alpha=None` means uniform; PSNR and SSIM share the CT dynamic-range + default 4095. +- Models: residual `Add` is a sequential fold (no stacked copy, ONNX `Add` nodes), attention + uses `scaled_dot_product_attention`, the YAML registry gained the modern atoms (SiLU, Mish, + ConstantPad, PixelShuffle, ...), GeneratorV3's head wiring fixed (it could never forward), + `Gan()` no longer shares import-time default subnetworks. +- Data manager: destination groups with disagreeing patch counts are refused at `prepare()` + naming case, groups and chains; one-pass DDP shards are balanced by patch load; the + batch-size-1 evaluation path collates a view instead of a copy; `subset` and `validation` + share one selector grammar (`~` exclusion, negative slice ends, mixed lists). +- Patching/reduction: sweep pricing keyed to the segment it prices; the REDUCE route probes + the run's first fold and replays the stat pass's regions. +- API: `predict` model lists normalized, unspellable sweep trees refused. +- `konfai.data.transform.inference` moved to `konfai_apps.transforms`; the bare stage name + `KonfAIInference` still resolves through the loader when konfai-apps is installed and + refuses with the install hint otherwise. +- CutOUT `cutout_size` is a fraction of the extent per axis in `(0, 1]` (was an int voxel + size); `c_prob` deleted. +- A declared `memory_budget` below 256 MiB warns: the stack cannot honor less. + +### Performance + +- Residual junctions no longer materialize an N-tensor stack; attention runs through SDPA + (measured max abs diff 7.15e-7 against the MONAI ViT parity pin). +- One-pass workflows batch a singleton as a view; deferred criterion values transfer once per + device per step. +- Remote datasets pay no per-patch path-resolution round trips; H5 read-chunk caches and the + DICOM plane cache take their slice of the declared budget. + +### Build, CI, dependencies + +- `lxml`, `requests` and `huggingface_hub` are no longer hard dependencies (stdlib XML and + urllib; `huggingface_hub` moved to the `all` extra for the IMPACT criteria); the orphaned + `fid` extra is gone; konfai-apps declares its own `requests` + `huggingface_hub`. +- One dev-dependency list, one mypy config, `py.typed` everywhere. +- CI: suites run under pytest-xdist with pip caching and CPU torch wheels on Linux; the five + `apps/*` bundle suites run in CI; publish uploads recover from partial runs + (`skip-existing`); the studio wheel's built front is verified. +- konfai-mcp pins `konfai==` / `konfai-apps==` at its scm version like konfai-apps does. +- New docs job builds the Sphinx site with `-W` on every docs/core change; docs deps unified + on `docs/requirements.txt`. + +### Breaking / migration notes + +- **Python callers of `Network` checkpoints**: `Network.state_dict()` now returns the + torch-native flat dict; build/unpack KonfAI checkpoints through `network_states()`. + Checkpoint files on disk are unchanged; RESUME/PREDICTION read them as before. + `parameters(pretrained)` / custom `apply()` are now `graph_parameters(pretrained=...)` / + `graph_apply()`. +- **Checkpoint loads refuse shape mismatches by default**: set `Model.allow_head_resize: true` + to restore the warm-start overlap copy. +- **`CutOUT.cutout_size`** is now a fraction in `(0, 1]` per axis; `c_prob` is gone. +- **Deleted criteria**: `FID`, `TripletLoss`, `MutualInformationLoss`, `L1LossRepresentation`, + `WGP`. Classpath replacements: `torchmetrics.image.fid:FrechetInceptionDistance`, + `torch:nn:TripletMarginLoss`, `monai.losses:GlobalMutualInformationLoss`. +- **SSIM's default `dynamic_range` moved 4024 to 4095** (the shared CT constant): default-config + SSIM numbers shift on the order of 1e-3. +- **`Dice` (soft route) and `TRE` return `(loss, LabelledValues)`** instead of `(loss, dict)`: + an out-of-tree caller reading `[1]` as a dict must materialize it. +- **Config generation modes removed**: `KONFAI_CONFIG_MODE=default|interactive|remove` no + longer exist; use `konfai --init`. +- **`validation: `** now selects that position (matching `subset:`) instead of refusing. +- **`TrainSubset` deleted**: it was an identity subclass; spell `Subset`. +- **`konfai` package private re-exports removed**: the nine package `__init__`s no longer + re-export single-underscore names; import from the defining submodule. + ## v1.8.2 (2026-08-31) The streaming engine now prices its reads on the block the store actually decodes, a dataset root From 9d266857945d3f0c23a04d7ff2708818a7d6ecbb Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 11:03:18 +0200 Subject: [PATCH 27/28] fix: the review findings on the audit branch Four real defects the review caught: Elastix's pull reach used spacing alone where the world-to-index row norms are the honest bound on oblique volumes (worst-case 45-degree regression test discriminates), signed_permutation's atol was swamped by the default rtol, a missing Save cache's grain was memoized past cache publication, and a Tensor-annotated parameter (Optional included) fell into object binding and silently dropped its configured value. Beside them: the bench sampler no longer shadows Thread._stop, a failed bench cleans its scratch, the docs claims are scoped to streamable cases, Standardize honors its POINTWISE declaration when seeded, CriterionResult refuses multi-element plain tensors, state_dict metadata survives load with aliases remapped by longest prefix, and the resume integration test stops expecting the deleted duplicate exit checkpoint. --- benchmarks/bench_streaming.py | 97 +++++++++++++------------ docs/source/usage/benchmarks.md | 13 ++-- konfai/data/augmentation/spatial.py | 9 ++- konfai/data/geometry.py | 8 +- konfai/data/patching/manager.py | 14 ++-- konfai/data/transform/intensity.py | 2 +- konfai/network/network/measure.py | 10 ++- konfai/network/network/network.py | 35 ++++++--- konfai/utils/config.py | 7 ++ tests/integration/test_konfai_resume.py | 6 +- tests/unit/test_augmentation.py | 33 +++++++++ tests/unit/test_config.py | 19 +++++ tests/unit/test_geometry.py | 11 +++ 13 files changed, 183 insertions(+), 81 deletions(-) diff --git a/benchmarks/bench_streaming.py b/benchmarks/bench_streaming.py index c0a32a59..63ca6ff1 100644 --- a/benchmarks/bench_streaming.py +++ b/benchmarks/bench_streaming.py @@ -53,16 +53,17 @@ class PeakSampler(threading.Thread): def __init__(self) -> None: super().__init__(daemon=True) self._process = psutil.Process() - self._stop = threading.Event() + # Not `_stop`: threading.Thread.join() calls its own internal `_stop()` method. + self._stop_event = threading.Event() self.peak = 0 def run(self) -> None: - while not self._stop.is_set(): + while not self._stop_event.is_set(): self.peak = max(self.peak, _tree_rss(self._process)) time.sleep(0.05) def stop(self) -> int: - self._stop.set() + self._stop_event.set() self.join() return self.peak @@ -78,9 +79,7 @@ def synthesize(root: Path, gib: float) -> tuple[Path, list[int]]: store = root / "Dataset.h5" rng = np.random.default_rng(0) with h5py.File(store, "w") as file: - dataset = file.create_dataset( - "CT/CASE_000", shape=(1, *shape), dtype=np.float32, chunks=(1, 64, side, side) - ) + dataset = file.create_dataset("CT/CASE_000", shape=(1, *shape), dtype=np.float32, chunks=(1, 64, side, side)) dataset.attrs["Origin"] = "[0. 0. 0.]" dataset.attrs["Spacing"] = "[1. 1. 1.]" dataset.attrs["Direction"] = "[1. 0. 0. 0. 1. 0. 0. 0. 1.]" @@ -101,48 +100,50 @@ def main() -> None: scratch = Path(tempfile.mkdtemp(prefix="konfai_bench_")) print(f"[bench] scratch: {scratch}") - print(f"[bench] synthesizing ~{args.gib:g} GiB volume ...", flush=True) - store, shape = synthesize(scratch, args.gib) - - from konfai.data.transform import Normalize, Write - - sampler = PeakSampler() - sampler.start() - start = time.perf_counter() - result = konfai.transform( - "BENCH", - f"{store}:h5", - {"CT": {"CT": [Normalize(min_value=-1, max_value=1), Write(dataset=f"{scratch / 'Out'}:h5")]}}, - memory_budget=f"{args.budget}gib", - transforms_dir=scratch / "Transforms", - quiet=True, - ) - elapsed = time.perf_counter() - start - peak = sampler.stop() - - del result - report = { - "volume_gib": round(args.gib, 2), - "declared_budget_gib": round(args.budget, 2), - "peak_tree_rss_gib": round(peak / 2**30, 2), - "wall_s": round(elapsed, 1), - "shape_zyx": shape, - "versions": { - "konfai": getattr(konfai, "__version__", "dev"), - "numpy": np.__version__, - "python": platform.python_version(), - }, - "host": platform.node(), - } - print(json.dumps(report, indent=2)) - print( - f"| {args.gib:g} GiB volume | budget {args.budget:g} GiB " - f"| peak {peak / 2**30:.2f} GiB | {elapsed:.1f} s |" - ) - if not args.keep: - import shutil - - shutil.rmtree(scratch, ignore_errors=True) + try: + print(f"[bench] synthesizing ~{args.gib:g} GiB volume ...", flush=True) + store, shape = synthesize(scratch, args.gib) + + from konfai.data.transform import Normalize, Write + + sampler = PeakSampler() + sampler.start() + start = time.perf_counter() + result = konfai.transform( + "BENCH", + f"{store}:h5", + {"CT": {"CT": [Normalize(min_value=-1, max_value=1), Write(dataset=f"{scratch / 'Out'}:h5")]}}, + memory_budget=f"{args.budget}gib", + transforms_dir=scratch / "Transforms", + quiet=True, + ) + elapsed = time.perf_counter() - start + peak = sampler.stop() + + del result + report = { + "volume_gib": round(args.gib, 2), + "declared_budget_gib": round(args.budget, 2), + "peak_tree_rss_gib": round(peak / 2**30, 2), + "wall_s": round(elapsed, 1), + "shape_zyx": shape, + "versions": { + "konfai": getattr(konfai, "__version__", "dev"), + "numpy": np.__version__, + "python": platform.python_version(), + }, + "host": platform.node(), + } + print(json.dumps(report, indent=2)) + print( + f"| {args.gib:g} GiB volume | budget {args.budget:g} GiB | peak {peak / 2**30:.2f} GiB | {elapsed:.1f} s |" + ) + finally: + # A failed large-volume run must not leave its synthetic input and output on disk. + if not args.keep: + import shutil + + shutil.rmtree(scratch, ignore_errors=True) if __name__ == "__main__": diff --git a/docs/source/usage/benchmarks.md b/docs/source/usage/benchmarks.md index 907f7a2d..b3f32c4b 100644 --- a/docs/source/usage/benchmarks.md +++ b/docs/source/usage/benchmarks.md @@ -13,16 +13,19 @@ the same footing. machine. - Host memory is the peak resident set of the whole process tree (`psutil`), sampled at 50 ms, so DataLoader workers and spawned ranks count. -- Device memory is `torch.cuda.max_memory_allocated()` plus the NVML process - figure when available. +- Device memory is `torch.cuda.max_memory_allocated()`, with the NVML + per-process figure reported beside it when available; the two overlap and are + never summed. - Every report line carries the konfai/torch/SimpleITK versions, CPU model, GPU model, and the input's shape, dtype and checksum. ## The streaming claim -{doc}`../concepts/streaming` states that a case never has to fit in RAM: a -16 GiB uncompressed volume is transformed with peak host memory bounded by the -declared `memory_budget`, not by the volume. Reproduce it with one command from +{doc}`../concepts/streaming` states that a streamable case never has to fit in +RAM: a 16 GiB uncompressed volume is transformed with peak host memory bounded +by the declared `memory_budget`, not by the volume. A chain that cannot stream +falls back to the whole volume, which TRANSFORM sizes against the same budget +and refuses when it does not fit. Reproduce the claim with one command from a checkout (needs the `imaging` extra and free disk for the synthetic volume): ```bash diff --git a/konfai/data/augmentation/spatial.py b/konfai/data/augmentation/spatial.py index 949a6a09..12aaad3b 100644 --- a/konfai/data/augmentation/spatial.py +++ b/konfai/data/augmentation/spatial.py @@ -577,13 +577,16 @@ def _stream_region_source( target_slices: tuple[slice, ...], source_spatial_shape: list[int], ) -> list[slice]: - # |displacement| <= max_displacement in world units by convexity: in voxels that is the - # bound over the axis's spacing, plus one voxel for the far interpolation tap. + # |displacement| <= max_displacement per world component by convexity. An index axis + # combines the components through its row of the inverse affine (an oblique direction + # mixes them), so its reach is that row's L1 norm times the bound (which reduces to + # 1/spacing on an axis-aligned grid), plus one voxel for the far interpolation tap. _stage, grid = self.draws[index][a] rank = len(source_spatial_shape) + row_reach_xyz = np.abs(grid.world_to_index.matrix).sum(axis=1) pull: list[slice] = [] for k, (part, extent) in enumerate(zip(target_slices, source_spatial_shape, strict=True)): - reach = int(np.ceil(self.max_displacement / float(grid.spacing_xyz[rank - 1 - k]))) + 1 + reach = int(np.ceil(self.max_displacement * float(row_reach_xyz[rank - 1 - k]))) + 1 start = max(0, part.start - reach) stop = min(extent, part.stop + reach) pull.append(slice(start, max(stop, start + 1))) diff --git a/konfai/data/geometry.py b/konfai/data/geometry.py index b0ef7069..44b32284 100644 --- a/konfai/data/geometry.py +++ b/konfai/data/geometry.py @@ -404,11 +404,13 @@ def signed_permutation(matrix: object, atol: float) -> AxisRemap | None: n = int(linear.shape[0]) magnitude = np.abs(linear) unit = np.ones(n) - if not np.allclose(magnitude.sum(axis=0), unit, atol=atol): + # rtol=0: the default 1e-5 relative slack against unit targets would swamp atol and let a + # near-axis rotation with off-axis terms around 5e-6 pass as a permutation. + if not np.allclose(magnitude.sum(axis=0), unit, rtol=0.0, atol=atol): return None - if not np.allclose(magnitude.max(axis=0), unit, atol=atol): + if not np.allclose(magnitude.max(axis=0), unit, rtol=0.0, atol=atol): return None - if not np.allclose(magnitude.sum(axis=1), unit, atol=atol): + if not np.allclose(magnitude.sum(axis=1), unit, rtol=0.0, atol=atol): return None remap: AxisRemap = [] for column in reversed(range(n)): diff --git a/konfai/data/patching/manager.py b/konfai/data/patching/manager.py index 61ed7661..8f2aa289 100644 --- a/konfai/data/patching/manager.py +++ b/konfai/data/patching/manager.py @@ -1383,13 +1383,13 @@ def _entry_granularity(self, dataset: Dataset, group: str, entry: str) -> tuple[ return self.read_granularity() key = (str(dataset.filename), group, entry) if key not in self._granularities: - granularity = None - # A cache this run has still to write has no metadata to ask; its chunks will be the - # very tile being sized, so its reads align by construction and None is its honest grain. - if dataset.is_dataset_exist(group, entry): - stored = dataset.read_granularity(group, entry) - granularity = None if stored is None else tuple(stored[1:]) - self._granularities[key] = granularity + # A cache this run has still to write has no metadata to ask. That answer is not + # memoized: once the upstream sweep publishes the cache, a re-planned downstream + # segment must price its reads on the stored chunks, not on a stale exact-read grain. + if not dataset.is_dataset_exist(group, entry): + return None + stored = dataset.read_granularity(group, entry) + self._granularities[key] = None if stored is None else tuple(stored[1:]) return self._granularities[key] def _get_streamed_data( diff --git a/konfai/data/transform/intensity.py b/konfai/data/transform/intensity.py index 8852a40d..8816d04c 100644 --- a/konfai/data/transform/intensity.py +++ b/konfai/data/transform/intensity.py @@ -401,7 +401,7 @@ def _masked_values(self, name: str, tensor: torch.Tensor) -> torch.Tensor: return tensor[mask == 1] def __call__(self, name: str, tensor: torch.Tensor, cache_attribute: Attribute) -> torch.Tensor: - if self.mask is not None and "StatisticsSeeded" in cache_attribute: + if self.mask is not None and (self.mean is None or self.std is None) and "StatisticsSeeded" in cache_attribute: # A streamed region: the mask cannot be indexed against it, and a bare 'Mean' seed may # be an unmasked stage's. The case's masked statistic is scanned from the stores once # (memoised) and every region applies the same per-voxel affine map. diff --git a/konfai/network/network/measure.py b/konfai/network/network/measure.py index 26f63bcd..07a48c0f 100644 --- a/konfai/network/network/measure.py +++ b/konfai/network/network/measure.py @@ -63,7 +63,8 @@ class CriterionResult(NamedTuple): @classmethod def of(cls, raw: CriterionOutput, criterion: str = "criterion") -> "CriterionResult": if isinstance(raw, torch.Tensor): - return cls(raw, raw.detach(), None) + # Funnel through the tuple path so the bare-loss value passes the same shape check. + raw = (raw, raw.detach()) if not isinstance(raw, tuple) or not 2 <= len(raw) <= 3 or not isinstance(raw[0], torch.Tensor): raise MeasureError( f"'{criterion}' returned {type(raw).__name__} instead of a criterion result.", @@ -80,6 +81,13 @@ def of(cls, raw: CriterionOutput, criterion: str = "criterion") -> "CriterionRes elif isinstance(value, tuple) and not isinstance(value, LabelledValues): if len(value) == 2 and isinstance(value[0], torch.Tensor) and isinstance(value[1], list): value = LabelledValues(value[0], value[1]) + if isinstance(value, torch.Tensor) and value.numel() != 1: + # A multi-element plain value would silently shift the deferred per-device readout. + raise MeasureError( + f"'{criterion}' reported a tensor of {value.numel()} elements.", + "The reported value is a single number: reduce it, or report a (values, labels) " + "pair for a per-label metric.", + ) if not isinstance(value, float | torch.Tensor | dict | LabelledValues) or ( map_ is not None and not isinstance(map_, torch.Tensor) ): diff --git a/konfai/network/network/network.py b/konfai/network/network/network.py index 3af36002..8a6e381b 100644 --- a/konfai/network/network/network.py +++ b/konfai/network/network/network.py @@ -616,7 +616,11 @@ def state_dict( # type: ignore[override] children: each owns its optimizer/state and is saved under its own ``network_states`` key.""" if destination is None: destination = OrderedDict() + destination._metadata = OrderedDict() # type: ignore[attr-defined] local_metadata = {"version": self._version} + if hasattr(destination, "_metadata"): + # As torch.nn.Module.state_dict records it: version-aware modules read it back at load. + destination._metadata[prefix[:-1]] = local_metadata # type: ignore[attr-defined] self._save_to_state_dict(destination, prefix, keep_vars) for name, module in self._modules.items(): if module is not None: @@ -634,9 +638,9 @@ def load_state_dict(self, state_dict: dict[str, torch.Tensor]): error_msgs: list[str] = [] metadata = getattr(state_dict, "_metadata", None) + # A plain copy: as a KEY the metadata would be collected as an unexpected key by the + # strict per-module load; the closure below reads the local variable. state_dict = state_dict.copy() - if metadata is not None: - state_dict["_metadata"] = metadata def load(module: torch.nn.Module, prefix=""): local_metadata = {} if metadata is None else metadata.get(prefix[:-1], {}) @@ -754,17 +758,26 @@ def load( modules_name = self.get_mapping() model_state_dict: OrderedDict[str, torch.Tensor] = OrderedDict() - for alias in model_state_dict_tmp.keys(): - prefix = ".".join(alias.split(".")[:-1]) + def remap(path: str) -> str: # Segment-aligned: alias 'layer1' must not claim a module 'layer10', and only the - # leading prefix is rewritten, never a later occurrence of the same substring. - alias_list = [(a, b) for a, b in modules_name.items() if prefix == a or prefix.startswith(a + ".")] + # leading prefix is rewritten, never a later occurrence of the same substring. Of + # the matches the longest wins: a nested alias 'p.a1' beats its parent 'p'. + candidates = [(a, b) for a, b in modules_name.items() if path == a or path.startswith(a + ".")] + if not candidates: + return path + a, b = max(candidates, key=lambda item: len(item[0])) + return b + path[len(a) :] - if len(alias_list): - a, b = alias_list[0] - model_state_dict[b + alias[len(a) :]] = model_state_dict_tmp[alias] - else: - model_state_dict[alias] = model_state_dict_tmp[alias] + for alias in model_state_dict_tmp.keys(): + prefix = ".".join(alias.split(".")[:-1]) + model_state_dict[remap(prefix) + alias[len(prefix) :]] = model_state_dict_tmp[alias] + source_metadata = getattr(model_state_dict_tmp, "_metadata", None) + if source_metadata is not None: + # Keys are module paths: remapped like the tensors so version-aware modules + # (_load_from_state_dict) find their entry. + model_state_dict._metadata = OrderedDict( # type: ignore[attr-defined] + (remap(path), meta) for path, meta in source_metadata.items() + ) self.load_state_dict(model_state_dict) elif self.pretrained_source is not None and not ema: # A fresh TRAIN carries no checkpoint entry: the declared reference seeds the graph the diff --git a/konfai/utils/config.py b/konfai/utils/config.py index 5573ced8..0d339eae 100755 --- a/konfai/utils/config.py +++ b/konfai/utils/config.py @@ -735,6 +735,13 @@ def _bind_parameter(function, config: Config, param: inspect.Parameter, section_ value = config.get_value(param.name, param.default) return None if value is None else _convert_union_sequence_value(value, get_args(annotation), param.name) + tensor = _tensor_type() + if tensor is not None and annotation is tensor: + # A bare (or Optional) Tensor parameter is a value, not a nested config object: bind the + # YAML scalar/list through torch.tensor, as the union path does. + value = config.get_value(param.name, param.default) + return None if value is None else _convert_union_sequence_value(value, (tensor,), param.name) + if annotation in _CONFIG_PRIMITIVE_TYPES or annotation is Any: return _bind_primitive(config, param, annotation, section_key) diff --git a/tests/integration/test_konfai_resume.py b/tests/integration/test_konfai_resume.py index 015be7c9..f3f4d56b 100644 --- a/tests/integration/test_konfai_resume.py +++ b/tests/integration/test_konfai_resume.py @@ -127,8 +127,10 @@ def test_konfai_cli_resume_continues_training(tmp_path: Path) -> None: epochs_rerun = EPOCHS_TOTAL - epoch_end assert max(new_epochs) == EPOCHS_TOTAL - 1 assert max(new_its) == it_end + epochs_rerun * its_per_epoch - # One checkpoint per training iteration (it_validation: 1) plus the final exit save. - assert len(new_checkpoints) == epochs_rerun * its_per_epoch + 1 + # One checkpoint per training iteration (it_validation: 1). The exit no longer writes a + # duplicate of the last scored save: an exit save happens only when iterations advanced + # past it (a crash), and it is then named crash_*.pt. + assert len(new_checkpoints) == epochs_rerun * its_per_epoch # The optimizer state itself round-tripped: AdamW step counters equal the total # number of iterations across both runs (not just the resumed run's own count). diff --git a/tests/unit/test_augmentation.py b/tests/unit/test_augmentation.py index 979c40fc..aae42080 100644 --- a/tests/unit/test_augmentation.py +++ b/tests/unit/test_augmentation.py @@ -678,6 +678,39 @@ def test_elastix_streams_each_region_of_a_copy() -> None: torch.testing.assert_close(region, whole[(slice(None), *target)], rtol=0, atol=1e-5) +def test_elastix_streams_each_region_of_an_oblique_copy() -> None: + """An oblique Direction mixes the world displacement components on an index axis, so the pull + reach comes from the inverse affine's rows: a 45-degree grid combines two components into + sqrt(2) times the per-axis bound, and a reach from the spacing alone under-pulls the block + (border padding then made a streamed region differ from the whole-volume warp by 0.63 here).""" + from konfai.data.geometry import DisplacementStage + from konfai.data.transform import RegionContext + + torch.manual_seed(2) + draw = Elastix(grid_spacing=8, max_displacement=4) + shape = [24, 24, 24] + cos, sin = float(np.cos(np.pi / 4)), float(np.sin(np.pi / 4)) + attribute = Attribute() + attribute["Origin"] = np.zeros(3) + attribute["Spacing"] = np.ones(3) + attribute["Direction"] = np.asarray([[cos, -sin, 0.0], [sin, cos, 0.0], [0.0, 0.0, 1.0]]).reshape(-1) + draw._state_init(0, [list(shape)], [attribute]) + stage, grid = draw.draws[0][0] + # The worst-case draw: every control value at the +bound, so d = (+4, +4, +4) at every voxel + # and the combined index reach along the rotated axes is realized, not merely possible. + draw.draws[0][0] = (DisplacementStage(stage.grid, np.full_like(stage.values, 4.0), stage.order), grid) + volume = torch.rand(1, *shape) + whole = draw._compute("case", 0, 0, volume) + for target in [ + (slice(8, 14), slice(8, 14), slice(8, 14)), + (slice(0, 24), slice(6, 10), slice(14, 24)), + ]: + source = tuple(draw._stream_region_source(0, 0, target, list(shape))) + block = volume[(slice(None), *source)] + region = draw._stream_region("case", 0, 0, block, RegionContext(source, target, tuple(shape))) + torch.testing.assert_close(region, whole[(slice(None), *target)], rtol=0, atol=1e-5) + + def test_an_augmentation_group_is_handed_the_case_not_a_clone_of_it(tmp_path: Path) -> None: """The copies of a case start as the case tensor itself: a draw that selects a copy hands back a tensor of its own, one that does not leaves the case's, and nothing is cloned for diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index cea55045..5b09b52f 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -377,6 +377,25 @@ def __init__( assert list(root.per_axis) == [10, 20, 0] and isinstance(root.per_axis, list) # not the string "[10, 20, 0]" +def test_apply_config_binds_a_bare_tensor_parameter_as_a_value(write_config) -> None: + # A bare (or Optional) ``torch.Tensor`` annotation is a value, not a nested config object: + # without the tensor dispatch it fell through to _bind_config_object, which dropped the + # configured value (Optional) or tried to instantiate Tensor from the config subtree. + import torch + + write_config("Root:\n weight:\n - 1.0\n - 2.0\n - 3.0\n") + + class Root: + def __init__(self, weight: torch.Tensor = None, bias: torch.Tensor | None = None) -> None: + self.weight = weight + self.bias = bias + + root = apply_config("Root")(Root)() + + assert isinstance(root.weight, torch.Tensor) and root.weight.tolist() == [1.0, 2.0, 3.0] + assert root.bias is None # unconfigured Optional stays None + + def test_apply_config_refuses_a_nested_block_where_a_value_is_expected(write_config) -> None: """A mapping where the annotation says ``str`` is a parse error, not a stringified mapping. diff --git a/tests/unit/test_geometry.py b/tests/unit/test_geometry.py index e42d32f4..ffa7a68c 100644 --- a/tests/unit/test_geometry.py +++ b/tests/unit/test_geometry.py @@ -284,6 +284,17 @@ def test_the_predicate_admits_exactly_the_signed_permutations(self): for refused in (averaging, superposing, degenerate): assert signed_permutation(refused, SIGNED_PERMUTATION_ATOL_FLOAT64) is None + def test_a_near_axis_rotation_is_refused(self): + from konfai.data.geometry import SIGNED_PERMUTATION_ATOL_FLOAT64, signed_permutation + + # Off-axis terms around 5e-6 sit above atol but under np.allclose's default rtol of 1e-5 + # against the unit targets: with rtol left on this oblique matrix passed as a permutation. + theta = 5e-6 + matrix = np.asarray( + [[np.cos(theta), -np.sin(theta), 0.0], [np.sin(theta), np.cos(theta), 0.0], [0.0, 0.0, 1.0]] + ) + assert signed_permutation(matrix, SIGNED_PERMUTATION_ATOL_FLOAT64) is None + def test_a_float32_quarter_turn_is_admitted_at_its_own_tolerance(self): import torch from konfai.data.geometry import SIGNED_PERMUTATION_ATOL_FLOAT32, signed_permutation From 43d1625a7836bde6e559dbeb417659529f2f8152 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Wed, 2 Sep 2026 13:42:01 +0200 Subject: [PATCH 28/28] fix: the second review pass (level-keyed grain, labelled-pair shape, bench finalizer) An OME-Zarr handle's pyramid level is part of its read grain: the granularity memo now keys on it (and the backend token), so two levels of one store stop sharing a grain and the sizing prices the block it will actually decode. CriterionResult refuses a (values, labels) pair whose tensor is not one value per label at the boundary instead of far away in the materialized zip. The bench stops its sampler from the finalizer when the run raises. --- benchmarks/bench_streaming.py | 3 +++ konfai/data/patching/manager.py | 2 +- konfai/network/network/measure.py | 6 ++++++ tests/unit/test_measure.py | 14 ++++++++++++++ tests/unit/test_sweep_tiling.py | 25 +++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 1 deletion(-) diff --git a/benchmarks/bench_streaming.py b/benchmarks/bench_streaming.py index 63ca6ff1..bfe1a76d 100644 --- a/benchmarks/bench_streaming.py +++ b/benchmarks/bench_streaming.py @@ -100,6 +100,7 @@ def main() -> None: scratch = Path(tempfile.mkdtemp(prefix="konfai_bench_")) print(f"[bench] scratch: {scratch}") + sampler = None try: print(f"[bench] synthesizing ~{args.gib:g} GiB volume ...", flush=True) store, shape = synthesize(scratch, args.gib) @@ -139,6 +140,8 @@ def main() -> None: f"| {args.gib:g} GiB volume | budget {args.budget:g} GiB | peak {peak / 2**30:.2f} GiB | {elapsed:.1f} s |" ) finally: + if sampler is not None and sampler.is_alive(): + sampler.stop() # A failed large-volume run must not leave its synthetic input and output on disk. if not args.keep: import shutil diff --git a/konfai/data/patching/manager.py b/konfai/data/patching/manager.py index 8f2aa289..83a573df 100644 --- a/konfai/data/patching/manager.py +++ b/konfai/data/patching/manager.py @@ -1381,7 +1381,7 @@ def _entry_granularity(self, dataset: Dataset, group: str, entry: str) -> tuple[ # knob tests and callers already reset; the memo below is for the other segment sources. if dataset is self.dataset and group == self.group_src: return self.read_granularity() - key = (str(dataset.filename), group, entry) + key = (str(dataset.filename), dataset.file_format, getattr(dataset, "level", None), group, entry) if key not in self._granularities: # A cache this run has still to write has no metadata to ask. That answer is not # memoized: once the upstream sweep publishes the cache, a re-planned downstream diff --git a/konfai/network/network/measure.py b/konfai/network/network/measure.py index 07a48c0f..971fed3c 100644 --- a/konfai/network/network/measure.py +++ b/konfai/network/network/measure.py @@ -88,6 +88,12 @@ def of(cls, raw: CriterionOutput, criterion: str = "criterion") -> "CriterionRes "The reported value is a single number: reduce it, or report a (values, labels) " "pair for a per-label metric.", ) + if isinstance(value, LabelledValues) and (value.values.ndim != 1 or value.values.numel() != len(value.labels)): + # A misshaped pair passes here but explodes far away, in the materialized() zip. + raise MeasureError( + f"'{criterion}' reported {tuple(value.values.shape)} values for {len(value.labels)} labels.", + "A per-label metric reports a 1-D tensor holding exactly one value per label.", + ) if not isinstance(value, float | torch.Tensor | dict | LabelledValues) or ( map_ is not None and not isinstance(map_, torch.Tensor) ): diff --git a/tests/unit/test_measure.py b/tests/unit/test_measure.py index e24fe3d7..4c98d4ff 100644 --- a/tests/unit/test_measure.py +++ b/tests/unit/test_measure.py @@ -1180,3 +1180,17 @@ def test_psnr_and_ssim_share_the_ct_dynamic_range_default() -> None: from konfai.metric.measure import PSNR assert PSNR()._dynamic_range == SSIM()._dynamic_range == 4095.0 + + +def test_criterion_result_refuses_a_misshaped_labelled_pair() -> None: + """A (values, labels) pair whose tensor is not one value per label would only explode far + away, in the materialized() zip: refused at the boundary instead, naming the criterion.""" + from konfai.network.network.measure import CriterionResult, LabelledValues + + loss = torch.tensor(0.5) + with pytest.raises(MeasureError, match="values for 2 labels"): + CriterionResult.of((loss, LabelledValues(torch.tensor([[0.1, 0.2]]), ["a", "b"])), "Dice") + with pytest.raises(MeasureError, match="values for 2 labels"): + CriterionResult.of((loss, LabelledValues(torch.tensor(0.1), ["a", "b"])), "Dice") + ok = CriterionResult.of((loss, LabelledValues(torch.tensor([0.1, 0.2]), ["a", "b"])), "Dice") + assert ok.value.labels == ["a", "b"] diff --git a/tests/unit/test_sweep_tiling.py b/tests/unit/test_sweep_tiling.py index 6d26640b..5b0aeb3b 100644 --- a/tests/unit/test_sweep_tiling.py +++ b/tests/unit/test_sweep_tiling.py @@ -437,3 +437,28 @@ def test_a_region_read_is_charged_only_for_what_the_block_grid_adds( assert manager.region_reads(16).widest_excess < manager.region_reads(4).widest_excess, ( "a region under one stored block wastes more of the block it decodes, not less" ) + + +def test_entry_granularity_is_keyed_by_pyramid_level(tmp_path: Path) -> None: + """Two handles on one store at different levels must not share a memoized grain. + + OmeZarrFile.read_granularity answers level-specific chunk metadata; a key of + (filename, group, entry) alone handed level 1 the grain memoized for level 0, + and the sizing then priced the wrong block. + """ + from types import SimpleNamespace + + source, _volume = _sheared_fixture(tmp_path) + holder = _manager(source, []) + + def stub(level: int) -> SimpleNamespace: + return SimpleNamespace( + filename=tmp_path / "pyramid", + file_format="omezarr", + level=level, + is_dataset_exist=lambda group, entry: True, + read_granularity=lambda group, entry, _level=level: (1, 8 * (_level + 1), 8, 8), + ) + + assert holder._entry_granularity(stub(0), "CT", "CASE_000") == (8, 8, 8) + assert holder._entry_granularity(stub(1), "CT", "CASE_000") == (16, 8, 8)