Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 9 additions & 14 deletions docs/api/physicsnemo/manifest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,33 +45,28 @@ decides which domain the model lives on.
Reference
=========

These live in :mod:`monai_physio.physicsnemo_tools`, which is not re-exported
from the top-level package - import it by module:
:class:`PhysicsNemoTools` is re-exported from the top-level package;
``SubjectManifest`` and ``PhaseEntry`` are not - import those by module:

.. code-block:: python

from monai_physio.physicsnemo_tools import SubjectManifest, parse_manifest
from monai_physio import PhysicsNemoTools
from monai_physio.physicsnemo_tools import SubjectManifest

manifest = PhysicsNemoTools.parse_manifest(manifest_path)

.. autoclass:: SubjectManifest
:exclude-members: subject_id, fitted_reference_mesh, pca_coefficients, target_array, phases

.. autoclass:: PhaseEntry
:exclude-members: mesh, stage

.. autofunction:: parse_manifest

.. autofunction:: load_target_array

.. autofunction:: load_pca_coefficients

Supporting helpers
==================

.. autofunction:: build_node_features

.. autofunction:: mesh_to_edge_index

.. autofunction:: compute_edge_features
.. autoclass:: PhysicsNemoTools
:members: parse_manifest, load_target_array, load_pca_coefficients,
build_node_features, mesh_to_edge_index, compute_edge_features

.. autoclass:: PhaseSampleDataset
:members:
Expand Down
8 changes: 4 additions & 4 deletions docs/api/usd/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ Create Anatomical Scene
.. code-block:: python

from monai_physio import usd_anatomy_tools
stage = usd_anatomy_tools.create_anatomical_stage()
usd_anatomy_tools.add_heart_model(stage, "heart.vtk")
usd_anatomy_tools.add_lungs_model(stage, "lungs.vtk")

stage = anatomy_tools.create_anatomical_stage()
anatomy_tools.add_heart_model(stage, "heart.vtk")
anatomy_tools.add_lungs_model(stage, "lungs.vtk")
Comment thread
aylward marked this conversation as resolved.
stage.Save()

See Also
Expand Down
39 changes: 39 additions & 0 deletions docs/developer/migration_next.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,45 @@ uv pip install --torch-backend=auto monai-physio # auto-detected PyTorch, no C
not Python symbols; no code referenced `[physicsnemo]` or the other removed
extras, so only install commands change.

## `physicsnemo_tools.py` free functions - wrapped into `PhysicsNemoTools`

**Change:** `physicsnemo_tools.py` had no class - it was free functions
(`parse_manifest`, `load_target_array`, `build_node_features`,
`mesh_to_edge_index`, `compute_edge_features`, `import_meshgraphnet`,
`unwrap_model`, `uncompiled_state_dict`, `strip_compile_prefix`). Those are
now `@staticmethod`s on a new `PhysicsNemoTools` class, so `parse_manifest(path)`
becomes `PhysicsNemoTools.parse_manifest(path)`, etc. `distributed_context` is
unaffected: it remains a top-level free function (`from monai_physio import
distributed_context`), now implemented as a thin wrapper around
`PhysicsNemoTools.distributed_context()`. `PhaseEntry`, `SubjectManifest`,
`DistributedContext`, `PhaseSampleDataset` are unaffected -
`from monai_physio import DistributedContext` still works.

**Why:** grouping the PhysicsNeMo helpers under one class namespace matches
the `*Tools` pattern used by every other utility module in the project
(`ImageTools`, `ContourTools`, `TransformTools`, etc.), instead of being the
only module exposing a flat set of free functions at import time.

**Before**

```python
from monai_physio.physicsnemo_tools import parse_manifest

manifest = parse_manifest(manifest_path)
```

**After**

```python
from monai_physio import PhysicsNemoTools

manifest = PhysicsNemoTools.parse_manifest(manifest_path)
```

**Automated conversion:** `None needed` - the class is a thin wrapper, so a
manual search-and-replace of `physicsnemo_tools.<function>(` ->
`PhysicsNemoTools.<function>(` covers every call site.
Comment thread
aylward marked this conversation as resolved.

## Entry template

Append one section per breaking change, newest last, using this shape:
Expand Down
2 changes: 1 addition & 1 deletion docs/developer/usd_generation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ produced by ``TransformTools.convert_transform_to_usd_visualization`` and
that already has a Camera does not produce a duplicate transform op.

Anatomy Materials with USDAnatomyTools
=======================================
==========================================

:class:`monai_physio.USDAnatomyTools` applies OmniSurface materials to
labeled meshes after conversion. It reads :class:`AnatomyTaxonomy` from the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
#

# %%
import itk

from pathlib import Path

import itk
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@
# - Mask-based approach focuses registration on the anatomical structures

# %%
import itk
from pathlib import Path

import itk
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pyvista as pv

from pathlib import Path

from monai_physio.contour_tools import ContourTools
from monai_physio.register_models_distance_maps import RegisterModelsDistanceMaps

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def landmark_rms_errors(
landmarks, in sorted-name order.
"""
errors: list[tuple[str, float]] = []
for name in fixed_landmarks.keys():
for name in fixed_landmarks:
if name not in warped_landmarks:
errors.append((name, float("nan")))
continue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
arr = itk.array_from_image(image)
print(arr.shape)
arr = np.where(arr == 0, -1000, arr)
for i in range(0, 21):
for i in range(21):
print(f"Processing slice {i:03d}...")
tmp_arr = itk.array_from_image(
itk.imread(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ def _ensure_mgn_inference_assets(
edge_index_file = model_dir / "shared_edge_index.pt"
edge_feats_file = model_dir / "shared_edge_features.pt"
if not edge_index_file.exists() or not edge_feats_file.exists():
edge_index = pnt.mesh_to_edge_index(mean_surface)
edge_index = pnt.PhysicsNemoTools.mesh_to_edge_index(mean_surface)
coords = np.asarray(mean_surface.points, dtype=np.float32)
edge_feats = pnt.compute_edge_features(coords, edge_index)
edge_feats = pnt.PhysicsNemoTools.compute_edge_features(coords, edge_index)
torch.save(edge_index, str(edge_index_file))
torch.save(edge_feats, str(edge_feats_file))

Expand Down
2 changes: 1 addition & 1 deletion experiments/Lung-GatedCT_To_USD/1-make_dirlab_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import pyvista as pv
from data_dirlab_4d_ct import DataDirLab4DCT

from monai_physio.contour_tools import ContourTools
from monai_physio import ConvertVTKToUSD
from monai_physio.contour_tools import ContourTools
from monai_physio.segment_chest_total_segmentator import SegmentChestTotalSegmentator

# Defensive: today this script only reads `seg.all_mask_ids`, but if anyone
Expand Down
6 changes: 3 additions & 3 deletions src/monai_physio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,9 @@
stacklevel=2,
)

# Data processing utilities
from .anatomy_taxonomy import AnatomyGroup, AnatomyTaxonomy
from .contour_tools import ContourTools

# Data processing utilities
from .convert_image_4d_to_3d import ConvertImage4DTo3D
from .convert_vtk_to_usd import ConvertVTKToUSD
from .data_download_tools import DataDownloadTools
Expand All @@ -73,7 +72,7 @@

# Base classes
from .monai_physio_base import MONAIPhysioBase
from .physicsnemo_tools import DistributedContext, distributed_context
from .physicsnemo_tools import DistributedContext, PhysicsNemoTools, distributed_context
from .register_images_ants import RegisterImagesANTS

# Registration classes
Expand Down Expand Up @@ -152,6 +151,7 @@
# Base classes
"MONAIPhysioBase",
"MovementGroundTruth",
"PhysicsNemoTools",
"RegisterImagesANTS",
# Registration classes
"RegisterImagesBase",
Expand Down
18 changes: 13 additions & 5 deletions src/monai_physio/contour_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

import logging
import os
from typing import Optional, Sequence, cast
from collections.abc import Sequence
from typing import Optional, cast

import itk
import numpy as np
Expand Down Expand Up @@ -758,7 +759,7 @@ def trim_tetrahedra_to_surface(
def repair_inverted_tetrahedra(
self,
tetrahedra: pv.UnstructuredGrid,
max_iterations: int = 20,
max_iterations: int = 100,
) -> pv.UnstructuredGrid:
"""Relax the nodes of any inverted or degenerate tetrahedron.

Expand Down Expand Up @@ -828,13 +829,20 @@ def volumes(points: np.ndarray) -> np.ndarray:
if len(neighbor_points):
points[node] = neighbor_points.mean(axis=0)

still_bad = int(np.sum(volumes(points) <= 0.0))
final_volumes = volumes(points)
still_bad_mask = final_volumes <= 0.0
still_bad = int(np.sum(still_bad_mask))
if still_bad:
details = "; ".join(
f"cell {cell_id} (nodes {connectivity[cell_id].tolist()}): "
f"volume={final_volumes[cell_id]:.3e}"
for cell_id in np.nonzero(still_bad_mask)[0]
)
Comment thread
aylward marked this conversation as resolved.
raise ValueError(
f"{still_bad} of {len(connectivity)} tetrahedra are still "
f"inverted or degenerate after {max_iterations} repair "
"passes; the fitted mesh needs a real re-fit, not just "
"smoothing."
f"passes ({details}); the fitted mesh needs a real re-fit, "
"not just smoothing."
)

self.log_warning(
Expand Down
12 changes: 6 additions & 6 deletions src/monai_physio/convert_vtk_to_usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def from_files(
mask_ids: Optional[dict[int, str]] = None,
segmenter: Optional[SegmentAnatomyBase] = None,
log_level: int | str = logging.INFO,
) -> "ConvertVTKToUSD":
) -> ConvertVTKToUSD:
"""Create a converter by loading VTK files from disk.

Accepts .vtk (legacy), .vtp (PolyData), and .vtu (UnstructuredGrid) files.
Expand Down Expand Up @@ -462,7 +462,7 @@ def set_colormap(
color_by_array: Optional[str] = None,
colormap: str = "plasma",
intensity_range: Optional[tuple[float, float]] = None,
) -> "ConvertVTKToUSD":
) -> ConvertVTKToUSD:
"""
Configure colormap for visualization.

Expand Down Expand Up @@ -495,7 +495,7 @@ def compute_von_mises_stress(
self,
stress_array_name: str = "stress",
output_name: str = "von_mises_stress",
) -> "ConvertVTKToUSD":
) -> ConvertVTKToUSD:
"""Add a scalar von Mises stress array derived from a 9-component
stress tensor on every input mesh.

Expand Down Expand Up @@ -875,9 +875,9 @@ def _vtk_to_mesh_data(

# Extract surface if needed
if self.convert_to_surface and not isinstance(vtk_mesh, pv.PolyData):
if isinstance(vtk_mesh, pv.UnstructuredGrid):
vtk_mesh = vtk_mesh.extract_surface(algorithm="dataset_surface")
elif hasattr(vtk_mesh, "extract_surface"):
if isinstance(vtk_mesh, pv.UnstructuredGrid) or hasattr(
vtk_mesh, "extract_surface"
):
vtk_mesh = vtk_mesh.extract_surface(algorithm="dataset_surface")
elif hasattr(vtk_mesh, "extract_geometry"):
vtk_mesh = vtk_mesh.extract_geometry()
Expand Down
Loading
Loading