Source code for qpdk.simulation.comsol.metal

"""Build unsolved extruded metal geometry from an extracted layout.

:class:`~qpdk.simulation.comsol.model.COMSOL` extends MPh's model with QPDK
layout, study, and mesh methods and builds a model through this builder; the
sheet builder for RF and electrostatic studies lives in
:mod:`qpdk.simulation.comsol.sheet`.
"""

from __future__ import annotations

import contextlib
import math
from typing import TYPE_CHECKING

from qpdk.simulation.comsol._util import _add_polygon, _format_number

if TYPE_CHECKING:
    import mph

    from qpdk.simulation.comsol.layout import ComsolLayout


[docs] def build_comsol_metal_model( client: mph.Client, layout: ComsolLayout, *, metal_thickness_um: float = 0.2, name: str = "QPDK metal", ) -> mph.Model: """Create a COMSOL 3D model holding the layout's metal as extruded polygons. Works for any extracted layout, fed or not: the feed ports are not used here. Each polygon outline becomes a solid polygon on work plane ``wp1``; each hole becomes a second polygon subtracted by its own ``Difference`` feature. The finished work plane is extruded to ``metal_thickness_um`` by ``ext1``. Args: client: A connected :class:`mph.Client`. layout: Extracted metal polygons and feed ports in µm. metal_thickness_um: Extrusion height in µm, strictly positive. name: Name of the COMSOL model. Returns: The MPh model. Geometry only: no physics, materials, ports, or studies have been added, and the model has not been saved. Raises: ValueError: If ``metal_thickness_um`` is not positive and finite, or the layout has no polygons to extrude. """ if not math.isfinite(metal_thickness_um) or metal_thickness_um <= 0.0: raise ValueError( "metal_thickness_um must be positive and finite, " f"got {metal_thickness_um!r}" ) if not layout.polygons: raise ValueError("layout has no polygons to extrude") model = client.create(name) try: # ruff: ignore[too-many-statements-in-try-clause] model.java.component().create("comp1") geometry = model.java.component("comp1").geom().create("geom1", 3) geometry.lengthUnit("um") geometry.create("wp1", "WorkPlane") work_plane = geometry.feature("wp1").geom() for index, polygon in enumerate(layout.polygons): # One Difference per hole: selection().set() takes a single feature name. target = f"pol{index}" _add_polygon(work_plane, target, polygon.outline) for hole, points in enumerate(polygon.holes): hole_tag = f"hole{index}_{hole}" _add_polygon(work_plane, hole_tag, points) difference_tag = f"dif{index}_{hole}" work_plane.create(difference_tag, "Difference") difference = work_plane.feature(difference_tag) difference.selection("input").set(target) difference.selection("input2").set(hole_tag) target = difference_tag geometry.create("ext1", "Extrude") extrude = geometry.feature("ext1") extrude.set("workplane", "wp1") extrude.selection("input").set("wp1") extrude.set("distance", _format_number(metal_thickness_um)) geometry.run() except BaseException: # A half-built native model would otherwise pile up in the COMSOL # process on every retry; the build failure is what matters, so a # failing cleanup must not mask it. with contextlib.suppress(Exception): client.remove(model) raise return model