-
Notifications
You must be signed in to change notification settings - Fork 6
feat(transforms): add xeltofab mesh transform bridge #240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
xarthurx
wants to merge
3
commits into
IDEALLab:main
Choose a base branch
from
xarthurx:feat/xeltofab-transforms
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Transforms for converting EngiBench designs to other representations.""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| """Bridge between EngiBench density-field problems and xeltofab mesh generation. | ||
|
|
||
| Example:: | ||
|
|
||
| from engibench.problems.beams2d import Beams2D | ||
| from engibench.transforms.xeltofab import to_mesh, save | ||
|
|
||
| problem = Beams2D() | ||
| design, _ = problem.random_design() | ||
| state = to_mesh(problem, design) | ||
| save(state, "beam.stl") # 3-D only | ||
| """ | ||
|
|
||
| from engibench.transforms.xeltofab._core import save | ||
| from engibench.transforms.xeltofab._core import to_mesh | ||
| from engibench.transforms.xeltofab._presets import PROBLEM_PRESETS | ||
|
|
||
| __all__ = ["PROBLEM_PRESETS", "save", "to_mesh"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| """Core bridge between EngiBench density-field problems and xeltofab mesh generation.""" | ||
|
|
||
| from typing import Any, TYPE_CHECKING | ||
|
|
||
| import numpy as np | ||
| import numpy.typing as npt | ||
|
|
||
| from engibench.transforms.xeltofab._presets import PROBLEM_PRESETS | ||
| from engibench.transforms.xeltofab._validate import validate_input | ||
| from engibench.transforms.xeltofab._validate import validate_output | ||
|
|
||
| if TYPE_CHECKING: | ||
| from pathlib import Path | ||
|
|
||
| from engibench.core import Problem | ||
|
|
||
| try: | ||
| from xeltofab import PipelineParams | ||
| from xeltofab import PipelineState | ||
| from xeltofab import process as _process | ||
| from xeltofab import save_mesh as _save_mesh | ||
|
|
||
| _HAS_XELTOFAB = True | ||
| except ImportError: | ||
| _HAS_XELTOFAB = False | ||
|
|
||
|
|
||
| def _check_xeltofab() -> None: | ||
| if not _HAS_XELTOFAB: | ||
| msg = ( | ||
| "xeltofab >= 0.3.0 is required for mesh transforms. " | ||
| "Install with: pip install 'engibench[transforms]' (requires Python >= 3.13)" | ||
| ) | ||
| raise ImportError(msg) | ||
|
|
||
|
|
||
| def to_mesh( | ||
| problem: "Problem", | ||
| design: npt.NDArray, | ||
| *, | ||
| params: "PipelineParams | None" = None, | ||
| validate: bool = True, | ||
| volume_tolerance: float = 0.05, | ||
| **kwargs: Any, | ||
| ) -> "PipelineState": | ||
| """Convert an EngiBench density-field design to a mesh via xeltofab. | ||
|
|
||
| Args: | ||
| problem: An EngiBench ``Problem`` instance. Used to look up | ||
| per-problem pipeline presets by class name. | ||
| design: A 2-D or 3-D numpy array with values in ``[0, 1]`` | ||
| representing a density field. | ||
| params: Explicit ``PipelineParams``. When provided, presets and | ||
| *kwargs* are ignored for pipeline parameters. | ||
| validate: Whether to run post-conversion validation checks. | ||
| volume_tolerance: Maximum allowed absolute change in volume | ||
| fraction between input field and output mesh / contours. | ||
| **kwargs: Forwarded to ``PipelineParams(...)`` and merged on top | ||
| of the per-problem preset defaults. | ||
|
|
||
| Returns: | ||
| A ``PipelineState`` containing the processed mesh (3-D) or | ||
| contours (2-D). | ||
|
|
||
| Raises: | ||
| TypeError: If *design* is not a numpy array. | ||
| ValueError: If *design* is not 2-D or 3-D. | ||
| ImportError: If xeltofab is not installed. | ||
| """ | ||
| _check_xeltofab() | ||
|
|
||
| design = validate_input(design) | ||
|
|
||
| if params is None: | ||
| problem_name = type(problem).__name__ | ||
| preset = PROBLEM_PRESETS.get(problem_name, {}) | ||
| merged = {**preset, **kwargs} | ||
| params = PipelineParams(**merged) | ||
|
|
||
| input_vf = float(np.mean(design)) if validate else 0.0 | ||
|
|
||
| # Preserve float32 to avoid doubling memory for large 3D fields. | ||
| if not np.issubdtype(design.dtype, np.floating): | ||
| design = design.astype(np.float64, copy=False) | ||
|
|
||
| state = PipelineState(field=design, params=params) | ||
| state = _process(state) | ||
|
|
||
| if validate: | ||
| validate_output(state, input_vf, volume_tolerance) | ||
|
|
||
| return state | ||
|
|
||
|
|
||
| def save(state: "PipelineState", path: "str | Path") -> None: | ||
| """Save a processed mesh to a file. | ||
|
|
||
| Convenience wrapper around ``xeltofab.save_mesh``. | ||
|
|
||
| Args: | ||
| state: A ``PipelineState`` returned by :func:`to_mesh`. | ||
| path: Output file path. Format is inferred from the extension | ||
| (``.stl``, ``.obj``, ``.ply``). | ||
|
|
||
| Raises: | ||
| ImportError: If xeltofab is not installed. | ||
| """ | ||
| _check_xeltofab() | ||
| _save_mesh(state, str(path)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| """Per-problem default pipeline parameters for xeltofab mesh generation. | ||
|
|
||
| This module contains no xeltofab imports — it stores plain dicts of kwargs | ||
| that are passed to ``PipelineParams(...)`` at runtime. | ||
| """ | ||
|
|
||
| from typing import Any | ||
|
|
||
| # 2D problems: contour extraction only; repair/remesh/decimate are 3D-only and irrelevant. | ||
| # 3D problems: full pipeline with marching cubes, repair, remesh, and decimation. | ||
|
|
||
| _BASE_2D: dict[str, Any] = { | ||
| "field_type": "density", | ||
| "threshold": 0.5, | ||
| "smooth_sigma": 1.0, | ||
| "morph_radius": 1, | ||
| } | ||
|
|
||
| _BASE_3D: dict[str, Any] = { | ||
| **_BASE_2D, | ||
| "extraction_method": "mc", | ||
| "repair": True, | ||
| "remesh": True, | ||
| "decimate": True, | ||
| "decimate_ratio": 0.5, | ||
| } | ||
|
|
||
| PROBLEM_PRESETS: dict[str, dict[str, Any]] = { | ||
| # --- 2D density-field problems --- | ||
| "Beams2D": {**_BASE_2D}, | ||
| "ThermoElastic2D": {**_BASE_2D, "smooth_sigma": 0.8}, | ||
| "Photonics2D": { | ||
| **_BASE_2D, | ||
| "smooth_sigma": 0.5, # lower: photonics designs have sharp features | ||
| "morph_radius": 0, # no morphological cleanup for sharp features | ||
| }, | ||
| "HeatConduction2D": {**_BASE_2D}, | ||
| # --- 3D density-field problems --- | ||
| "ThermoElastic3D": {**_BASE_3D}, | ||
| "HeatConduction3D": {**_BASE_3D}, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| """Input and output validation for the xeltofab mesh transform.""" | ||
|
|
||
| from typing import TYPE_CHECKING | ||
| import warnings | ||
|
|
||
| import numpy as np | ||
| import numpy.typing as npt | ||
|
|
||
| if TYPE_CHECKING: | ||
| from xeltofab import PipelineState | ||
|
|
||
|
|
||
| def validate_input(design: npt.NDArray) -> npt.NDArray: | ||
| """Validate and sanitize a density-field design array. | ||
|
|
||
| Args: | ||
| design: A numpy array representing a density field. | ||
|
|
||
| Returns: | ||
| The (possibly clipped) design array. | ||
|
|
||
| Raises: | ||
| TypeError: If *design* is not a numpy array. | ||
| ValueError: If *design* is not 2-D or 3-D. | ||
| """ | ||
| if not isinstance(design, np.ndarray): | ||
| msg = f"design must be a numpy ndarray, got {type(design).__name__}" | ||
| raise TypeError(msg) | ||
|
|
||
| if not np.issubdtype(design.dtype, np.floating) and not np.issubdtype(design.dtype, np.integer): | ||
| msg = f"design must have a numeric dtype, got {design.dtype}" | ||
| raise TypeError(msg) | ||
|
|
||
| if design.size == 0: | ||
| msg = f"design must be non-empty, got shape {design.shape}" | ||
| raise ValueError(msg) | ||
|
|
||
| if design.ndim not in (2, 3): | ||
| msg = f"design must be 2-D or 3-D, got {design.ndim}-D with shape {design.shape}" | ||
| raise ValueError(msg) | ||
|
|
||
| # Single pass: min/max propagate NaN, so NaN detection comes for free. | ||
| vmin, vmax = float(design.min()), float(design.max()) | ||
| if np.isnan(vmin) or np.isnan(vmax) or np.isinf(vmin) or np.isinf(vmax): | ||
| msg = "design contains non-finite values (NaN or Inf)" | ||
| raise ValueError(msg) | ||
|
|
||
| if vmin < 0.0 or vmax > 1.0: | ||
| warnings.warn( | ||
| f"Design values outside [0, 1] (min={vmin:.4f}, max={vmax:.4f}). Clipping.", | ||
| stacklevel=3, | ||
| ) | ||
| design = np.clip(design, 0.0, 1.0) | ||
|
|
||
| return design | ||
|
|
||
|
|
||
| def validate_output( | ||
| state: "PipelineState", | ||
| input_volume_fraction: float, | ||
| tolerance: float, | ||
| ) -> list[str]: | ||
| """Run post-pipeline validation checks. | ||
|
|
||
| Args: | ||
| state: A ``xeltofab.PipelineState`` instance. | ||
| input_volume_fraction: Volume fraction of the input design (``np.mean(design)``). | ||
| tolerance: Maximum allowed absolute deviation in volume fraction. | ||
|
|
||
| Returns: | ||
| A list of warning messages (empty if all checks pass). | ||
|
|
||
| Raises: | ||
| RuntimeError: If the pipeline produced no mesh (3-D) or no contours (2-D). | ||
| """ | ||
| warnings_list: list[str] = [] | ||
|
|
||
| ndim: int = getattr(state, "ndim", 0) | ||
|
|
||
| if ndim == 3: # noqa: PLR2004 | ||
xarthurx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| vertices = getattr(state, "vertices", None) | ||
| faces = getattr(state, "faces", None) | ||
| if vertices is None or faces is None: | ||
| msg = "3-D pipeline produced no mesh (vertices or faces are None)." | ||
| raise RuntimeError(msg) | ||
| elif ndim == 2: # noqa: PLR2004 | ||
| contours = getattr(state, "contours", None) | ||
| if contours is None or len(contours) == 0: | ||
| msg = "2-D pipeline produced no contours." | ||
| raise RuntimeError(msg) | ||
xarthurx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| else: | ||
| msg = f"Unsupported or missing 'ndim' in pipeline state: {ndim!r}. Expected 2 or 3." | ||
| raise RuntimeError(msg) | ||
|
|
||
| output_vf = getattr(state, "volume_fraction", None) | ||
| if output_vf is not None: | ||
| delta = abs(input_volume_fraction - output_vf) | ||
| if delta > tolerance: | ||
| warnings_list.append( | ||
| f"Volume fraction changed by {delta:.4f} " | ||
| f"(input={input_volume_fraction:.4f}, output={output_vf:.4f}, " | ||
| f"tolerance={tolerance:.4f})." | ||
| ) | ||
|
|
||
| for msg in warnings_list: | ||
| warnings.warn(msg, stacklevel=3) | ||
|
|
||
| return warnings_list | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.