Skip to content

Mesh — g.mesh

Meshing composite. Seven focused sub-composites.

g.mesh

apeGmsh.mesh.Mesh.Mesh

Mesh(parent: '_ApeGmshSession')

Bases: _HasLogging

Thin composition container for meshing. Every action lives in a focused sub-composite — see the module docstring for the full map.

Parameters

parent : _SessionBase Owning session — used to read _verbose and access the physical composite during _by_physical helpers.

Source code in src/apeGmsh/mesh/Mesh.py
def __init__(self, parent: "_ApeGmshSession") -> None:
    self._parent = parent

    # Directive log — records every write-only mesh setting that
    # cannot be read back from gmsh (transfinite, setSize, recombine,
    # fields, per-entity algorithm, smoothing).  Used by
    # ``Inspect.print_summary()`` to show what's been applied.
    self._directives: list[dict] = []

    # gmsh has no getter for the background field — tracked here by
    # FieldHelper.set_background so g.mesh.recipe can fold a
    # user-set background into its Min combiner (ADR 0059 §3).
    self._background_field_tag: int | None = None

    # Sub-composites — each keeps a reference to self.
    self.field        = FieldHelper(self)
    self.generation   = _Generation(self)
    self.sizing       = _Sizing(self)
    self.structured   = _Structured(self)
    self.editing      = _Editing(self)
    self.queries      = _Queries(self)
    self.partitioning = _Partitioning(self)
    self.options      = _Options(self)
    self.recipe       = _Recipe(self)

viewer

viewer(**kwargs)

Open the interactive mesh viewer.

The viewer supports picking (BRep entities, elements, nodes), color modes, and load/constraint/mass overlays.

Parameters are forwarded to :class:MeshViewer.

Source code in src/apeGmsh/mesh/Mesh.py
def viewer(self, **kwargs):
    """Open the interactive mesh viewer.

    The viewer supports picking (BRep entities, elements, nodes),
    color modes, and load/constraint/mass overlays.

    Parameters are forwarded to :class:`MeshViewer`.
    """
    from ..viewers.mesh_viewer import MeshViewer
    mv = MeshViewer(self._parent, **kwargs)
    return mv.show()

render

render(path: 'str | Path', *, camera: str = 'iso', window_size: tuple[int, int] = (1280, 720)) -> 'Path | None'

Write one offscreen undeformed mesh still (ADR 0094 S4).

Live session only. Uses build_mesh_scene (the same live Gmsh scene as :meth:viewer), not get_fem_data + build_fem_scene. VTK offscreen — no Qt window, no event loop.

Returns the written :class:~pathlib.Path, or None (and prints the [skip viewer] notice) under APEGMSH_SKIP_VIEWER=1 or with no GL. Raises RuntimeError on a from_h5 / closed session, or when no mesh has been generated. Snapshot stills stay on fem.render.

Source code in src/apeGmsh/mesh/Mesh.py
def render(
    self,
    path: "str | Path",
    *,
    camera: str = "iso",
    window_size: tuple[int, int] = (1280, 720),
) -> "Path | None":
    """Write one offscreen undeformed mesh still (ADR 0094 S4).

    Live session only. Uses ``build_mesh_scene`` (the same live
    Gmsh scene as :meth:`viewer`), not ``get_fem_data`` +
    ``build_fem_scene``. VTK offscreen — no Qt window, no event
    loop.

    Returns the written :class:`~pathlib.Path`, or ``None`` (and
    prints the ``[skip viewer]`` notice) under
    ``APEGMSH_SKIP_VIEWER=1`` or with no GL. Raises
    ``RuntimeError`` on a ``from_h5`` / closed session, or when
    no mesh has been generated. Snapshot stills stay on
    ``fem.render``.
    """
    from apeGmsh.viewers.render import render_mesh

    return render_mesh(
        self._parent, path, camera=camera, window_size=window_size,
    )

preview

preview(*, dims: list[int] | None = None, show_nodes: bool = True, browser: bool = False, return_fig: bool = False)

Interactive WebGL preview of the mesh.

Zero Qt dependency — works inline in Jupyter / VS Code / Colab, or in a dedicated browser tab when browser=True. Hover over an element to see its BRep dim and tag; hover over a node to see its node=N id. Single-click a legend entry to hide a trace, double-click to isolate it.

Parameters

dims : list of int, optional Mesh dimensions to render. Defaults to [1, 2, 3] — surface / volume / 1D curve elements. show_nodes : bool Render the full mesh-node cloud as a separate trace (default True). Matches the Qt mesh viewer, which always shows the node cloud. Disable for very large meshes where the nodes overwhelm the element rendering. browser : bool If True, open in a new browser tab (temp HTML file) instead of rendering inline. return_fig : bool If True, skip display and return the raw :class:plotly.graph_objects.Figure.

Source code in src/apeGmsh/mesh/Mesh.py
def preview(
    self,
    *,
    dims: list[int] | None = None,
    show_nodes: bool = True,
    browser: bool = False,
    return_fig: bool = False,
):
    """Interactive WebGL preview of the mesh.

    Zero Qt dependency — works inline in Jupyter / VS Code / Colab,
    or in a dedicated browser tab when ``browser=True``. Hover over
    an element to see its BRep ``dim`` and ``tag``; hover over a
    node to see its ``node=N`` id. Single-click a legend entry to
    hide a trace, double-click to isolate it.

    Parameters
    ----------
    dims : list of int, optional
        Mesh dimensions to render. Defaults to ``[1, 2, 3]`` —
        surface / volume / 1D curve elements.
    show_nodes : bool
        Render the full mesh-node cloud as a separate trace
        (default ``True``). Matches the Qt mesh viewer, which
        always shows the node cloud. Disable for very large meshes
        where the nodes overwhelm the element rendering.
    browser : bool
        If ``True``, open in a new browser tab (temp HTML file)
        instead of rendering inline.
    return_fig : bool
        If ``True``, skip display and return the raw
        :class:`plotly.graph_objects.Figure`.
    """
    from ..viz.NotebookPreview import preview_mesh
    return preview_mesh(
        self._parent,
        dims=dims,
        show_nodes=show_nodes,
        browser=browser,
        return_fig=return_fig,
    )

Sub-composites

g.mesh.generation

apeGmsh.mesh._mesh_generation._Generation

_Generation(parent_mesh: 'Mesh')

Mesh generation, high-order elevation, refinement, optimisation, and algorithm selection.

Source code in src/apeGmsh/mesh/_mesh_generation.py
def __init__(self, parent_mesh: "Mesh") -> None:
    self._mesh = parent_mesh

generate

generate(dim: int = 3) -> '_Generation'

Generate a mesh up to the given dimension.

Parameters

dim : 1 = edges only, 2 = surface mesh, 3 = volume mesh (default)

Source code in src/apeGmsh/mesh/_mesh_generation.py
def generate(self, dim: int = 3) -> "_Generation":
    """
    Generate a mesh up to the given dimension.

    Parameters
    ----------
    dim : 1 = edges only, 2 = surface mesh, 3 = volume mesh (default)
    """
    # Phase 3B.2d / ADR 0038 — meshing in chain phase would diverge
    # the FEMData snapshot from the live gmsh model.
    from apeGmsh.core._compose_errors import chain_phase_guard
    chain_phase_guard(self._mesh._parent, "g.mesh.generation.generate")
    self._validate_pre_mesh()
    gmsh.model.mesh.generate(dim)
    self._mesh._log(f"generate(dim={dim})")
    return self

set_order

set_order(order: int, *, bubble: bool = True) -> '_Generation'

Elevate elements to high order.

Parameters

order : 1 = linear, 2 = quadratic, 3 = cubic, … bubble : include interior (bubble) nodes for order ≥ 2. True → complete Lagrange (e.g. Q9, T6+bubble). False → serendipity / incomplete (e.g. Q8, T6). Global Gmsh flag — applies to the entire mesh.

Source code in src/apeGmsh/mesh/_mesh_generation.py
def set_order(self, order: int, *, bubble: bool = True) -> "_Generation":
    """
    Elevate elements to high order.

    Parameters
    ----------
    order  : 1 = linear, 2 = quadratic, 3 = cubic, …
    bubble : include interior (bubble) nodes for order ≥ 2.
             True  → complete Lagrange (e.g. Q9, T6+bubble).
             False → serendipity / incomplete (e.g. Q8, T6).
             Global Gmsh flag — applies to the entire mesh.
    """
    from apeGmsh.core._compose_errors import chain_phase_guard
    chain_phase_guard(
        self._mesh._parent, "g.mesh.generation.set_order"
    )
    if order >= 2:
        gmsh.option.setNumber("Mesh.SecondOrderIncomplete", 0 if bubble else 1)
    gmsh.model.mesh.setOrder(order)
    self._mesh._log(f"set_order({order}, bubble={bubble})")
    return self

refine

refine() -> '_Generation'

Uniformly refine by splitting every element once.

Source code in src/apeGmsh/mesh/_mesh_generation.py
def refine(self) -> "_Generation":
    """Uniformly refine by splitting every element once."""
    from apeGmsh.core._compose_errors import chain_phase_guard
    chain_phase_guard(self._mesh._parent, "g.mesh.generation.refine")
    gmsh.model.mesh.refine()
    self._mesh._log("refine()")
    return self

optimize

optimize(method: str = '', *, force: bool = False, niter: int = 1, dim_tags: list[tuple[int, int]] | None = None) -> '_Generation'

Optimise mesh quality.

Parameters

method : one of OptimizeMethod.* constants force : apply optimisation even to already-valid elements niter : number of passes dim_tags : limit to specific entities (None = all)

Example

::

g.mesh.generation.generate(3).optimize(OptimizeMethod.NETGEN, niter=5)
Source code in src/apeGmsh/mesh/_mesh_generation.py
def optimize(
    self,
    method  : str  = "",
    *,
    force   : bool = False,
    niter   : int  = 1,
    dim_tags: list[tuple[int, int]] | None = None,
) -> "_Generation":
    """
    Optimise mesh quality.

    Parameters
    ----------
    method   : one of ``OptimizeMethod.*`` constants
    force    : apply optimisation even to already-valid elements
    niter    : number of passes
    dim_tags : limit to specific entities (``None`` = all)

    Example
    -------
    ::

        g.mesh.generation.generate(3).optimize(OptimizeMethod.NETGEN, niter=5)
    """
    from apeGmsh.core._compose_errors import chain_phase_guard
    chain_phase_guard(self._mesh._parent, "g.mesh.generation.optimize")
    gmsh.model.mesh.optimize(method, force=force, niter=niter,
                              dimTags=dim_tags or [])
    self._mesh._log(f"optimize(method={method!r}, niter={niter})")
    return self

set_algorithm

set_algorithm(tag, algorithm, *, dim: int = 2) -> '_Generation'

Choose the meshing algorithm for a surface (dim=2) or globally for all volumes (dim=3).

tag accepts int, label, or PG name. If it resolves to multiple surfaces, the algorithm is applied to each.

Example

::

g.mesh.generation.set_algorithm("col.web", "frontal_delaunay_quads")
g.mesh.generation.set_algorithm(0, "hxt", dim=3)
Source code in src/apeGmsh/mesh/_mesh_generation.py
def set_algorithm(
    self,
    tag,
    algorithm,
    *,
    dim      : int = 2,
) -> "_Generation":
    """
    Choose the meshing algorithm for a surface (dim=2) or globally
    for all volumes (dim=3).

    ``tag`` accepts int, label, or PG name.  If it resolves to
    multiple surfaces, the algorithm is applied to each.

    Example
    -------
    ::

        g.mesh.generation.set_algorithm("col.web", "frontal_delaunay_quads")
        g.mesh.generation.set_algorithm(0, "hxt", dim=3)
    """
    from apeGmsh.core._compose_errors import chain_phase_guard
    chain_phase_guard(
        self._mesh._parent, "g.mesh.generation.set_algorithm"
    )
    from apeGmsh.core._helpers import resolve_to_tags

    if dim not in (2, 3):
        raise ValueError(f"set_algorithm: dim must be 2 or 3, got {dim!r}")

    alg_int = _normalize_algorithm(algorithm, dim)

    if dim == 2:
        tags_resolved = resolve_to_tags(tag, dim=2, session=self._mesh._parent)
        for t in tags_resolved:
            gmsh.model.mesh.setAlgorithm(2, t, alg_int)
            self._mesh._directives.append({
                'kind': 'algorithm', 'dim': 2, 'tag': t,
                'algorithm': alg_int, 'requested': algorithm,
            })
            self._mesh._log(
                f"set_algorithm(dim=2, tag={t}, alg={algorithm!r} -> {alg_int})"
            )
    else:  # dim == 3
        gmsh.option.setNumber("Mesh.Algorithm3D", alg_int)
        self._mesh._directives.append({
            'kind': 'algorithm', 'dim': 3, 'tag': 0,
            'algorithm': alg_int, 'requested': algorithm,
        })
        self._mesh._log(
            f"set_algorithm(dim=3, alg={algorithm!r} -> {alg_int})  [global option]"
        )
    return self

set_algorithm_by_physical

set_algorithm_by_physical(name: str, algorithm, *, dim: int = 2) -> '_Generation'

Deprecated. set_algorithm accepts a PG name directly.

With dim=3 the original wrapper passed tag=0 (since Mesh.Algorithm3D is a global option); preserve that here.

Source code in src/apeGmsh/mesh/_mesh_generation.py
def set_algorithm_by_physical(
    self,
    name     : str,
    algorithm,
    *,
    dim      : int = 2,
) -> "_Generation":
    """Deprecated.  ``set_algorithm`` accepts a PG name directly.

    With ``dim=3`` the original wrapper passed tag=0 (since
    ``Mesh.Algorithm3D`` is a global option); preserve that here.
    """
    import warnings
    warnings.warn(
        "set_algorithm_by_physical is deprecated; pass the "
        "physical-group name to set_algorithm() as tag.",
        DeprecationWarning,
        stacklevel=2,
    )
    if dim == 3:
        return self.set_algorithm(0, algorithm, dim=3)
    return self.set_algorithm(name, algorithm, dim=dim)

g.mesh.sizing

apeGmsh.mesh._mesh_sizing._Sizing

_Sizing(parent_mesh: 'Mesh')

Global and per-entity element size control.

Source code in src/apeGmsh/mesh/_mesh_sizing.py
def __init__(self, parent_mesh: "Mesh") -> None:
    self._mesh = parent_mesh

set_global_size

set_global_size(max_size: float, min_size: float = 0.0) -> '_Sizing'

Set the global element-size band.

Parameters

max_size : upper bound assigned to Mesh.MeshSizeMax. Acts as a ceiling on every size source. min_size : lower bound assigned to Mesh.MeshSizeMin. Defaults to 0.0 — i.e. no floor, so per-point refinements (set_size(..., 100)) are free to produce elements smaller than max_size.

Example

::

m1.mesh.sizing.set_global_size(6000)            # ceiling only
m1.mesh.sizing.set_global_size(6000, 200)       # band [200, 6000]
Source code in src/apeGmsh/mesh/_mesh_sizing.py
def set_global_size(
    self,
    max_size: float,
    min_size: float = 0.0,
) -> "_Sizing":
    """
    Set the global element-size band.

    Parameters
    ----------
    max_size : upper bound assigned to ``Mesh.MeshSizeMax``.  Acts
               as a ceiling on every size source.
    min_size : lower bound assigned to ``Mesh.MeshSizeMin``.
               Defaults to ``0.0`` — i.e. no floor, so per-point
               refinements (``set_size(..., 100)``) are free to
               produce elements smaller than ``max_size``.

    Example
    -------
    ::

        m1.mesh.sizing.set_global_size(6000)            # ceiling only
        m1.mesh.sizing.set_global_size(6000, 200)       # band [200, 6000]
    """
    self._guard("set_global_size")
    gmsh.option.setNumber("Mesh.MeshSizeMax", max_size)
    gmsh.option.setNumber("Mesh.MeshSizeMin", min_size)
    self._mesh._log(f"set_global_size(max={max_size}, min={min_size})")
    return self

set_size_sources

set_size_sources(*, from_points: bool | None = None, from_curvature: bool | None = None, extend_from_boundary: bool | None = None) -> '_Sizing'

Control which size sources Gmsh consults when meshing.

Gmsh combines several size sources at each node and takes the minimum. When from_points is on (Gmsh default), every BRep point carries its own characteristic length — imported CAD files typically bake in small lc values that silently override :meth:set_global_size.

Example

::

(g.mesh.sizing
   .set_size_sources(from_points=False,
                     from_curvature=False,
                     extend_from_boundary=False)
   .set_global_size(6000))
Source code in src/apeGmsh/mesh/_mesh_sizing.py
def set_size_sources(
    self,
    *,
    from_points          : bool | None = None,
    from_curvature       : bool | None = None,
    extend_from_boundary : bool | None = None,
) -> "_Sizing":
    """
    Control *which* size sources Gmsh consults when meshing.

    Gmsh combines several size sources at each node and takes the
    minimum.  When ``from_points`` is on (Gmsh default), every
    BRep point carries its own characteristic length — imported
    CAD files typically bake in small ``lc`` values that silently
    override :meth:`set_global_size`.

    Example
    -------
    ::

        (g.mesh.sizing
           .set_size_sources(from_points=False,
                             from_curvature=False,
                             extend_from_boundary=False)
           .set_global_size(6000))
    """
    self._guard("set_size_sources")
    if from_points is not None:
        gmsh.option.setNumber("Mesh.MeshSizeFromPoints", int(bool(from_points)))
    if from_curvature is not None:
        gmsh.option.setNumber("Mesh.MeshSizeFromCurvature", int(bool(from_curvature)))
    if extend_from_boundary is not None:
        gmsh.option.setNumber("Mesh.MeshSizeExtendFromBoundary",
                              int(bool(extend_from_boundary)))
    self._mesh._log(
        f"set_size_sources(from_points={from_points}, "
        f"from_curvature={from_curvature}, "
        f"extend_from_boundary={extend_from_boundary})"
    )
    return self

set_size_global

set_size_global(*, min_size: float | None = None, max_size: float | None = None) -> '_Sizing'

Set the global mesh-size bounds independently.

Example

::

g.mesh.sizing.set_size_global(min_size=15, max_size=25)
g.mesh.sizing.set_size_global(max_size=50)
Source code in src/apeGmsh/mesh/_mesh_sizing.py
def set_size_global(
    self,
    *,
    min_size: float | None = None,
    max_size: float | None = None,
) -> "_Sizing":
    """
    Set the global mesh-size bounds independently.

    Example
    -------
    ::

        g.mesh.sizing.set_size_global(min_size=15, max_size=25)
        g.mesh.sizing.set_size_global(max_size=50)
    """
    self._guard("set_size_global")
    if min_size is not None:
        gmsh.option.setNumber("Mesh.MeshSizeMin", min_size)
    if max_size is not None:
        gmsh.option.setNumber("Mesh.MeshSizeMax", max_size)
    self._mesh._log(f"set_size_global(min={min_size}, max={max_size})")
    return self

set_size

set_size(tags, size: float, *, dim: int | None = None) -> '_Sizing'

Assign a target element size by walking to the entity's points.

Gmsh's setSize only honours characteristic lengths on points (dim=0). This method accepts any reference shape and recursively walks higher-dimensional entities down to their BRep points before applying the size — so passing a volume label, surface PG, or (3, vol_tag) dimtag does what you expect.

Parameters

tags : int, str, (dim, tag), or list of those Entity reference. Strings resolve via labels first, then physical groups, at the entity's native dimension. size : float Target characteristic length on the resolved points. dim : int, optional Disambiguation hint. Used only when the input is a raw int tag or when a label exists at multiple dimensions. Leave as None (default) for label/PG/dimtag inputs.

Example

::

# Per-volume sizing by label (auto-walks to corner points)
g.mesh.sizing.set_size("box",   1.0)
g.mesh.sizing.set_size("box_2", 0.3)

# Direct point list still works
g.mesh.sizing.set_size([p1, p2, p3], 0.05)

# Dimtag form for an explicit volume
g.mesh.sizing.set_size([(3, vol_tag)], 10.0)
Source code in src/apeGmsh/mesh/_mesh_sizing.py
def set_size(
    self,
    tags,
    size: float,
    *,
    dim : int | None = None,
) -> "_Sizing":
    """
    Assign a target element size by walking to the entity's points.

    Gmsh's ``setSize`` only honours characteristic lengths on
    points (dim=0).  This method accepts any reference shape and
    recursively walks higher-dimensional entities down to their
    BRep points before applying the size — so passing a volume
    label, surface PG, or ``(3, vol_tag)`` dimtag does what you
    expect.

    Parameters
    ----------
    tags : int, str, (dim, tag), or list of those
        Entity reference.  Strings resolve via labels first, then
        physical groups, at the entity's native dimension.
    size : float
        Target characteristic length on the resolved points.
    dim : int, optional
        Disambiguation hint.  Used only when the input is a raw
        int tag or when a label exists at multiple dimensions.
        Leave as ``None`` (default) for label/PG/dimtag inputs.

    Example
    -------
    ::

        # Per-volume sizing by label (auto-walks to corner points)
        g.mesh.sizing.set_size("box",   1.0)
        g.mesh.sizing.set_size("box_2", 0.3)

        # Direct point list still works
        g.mesh.sizing.set_size([p1, p2, p3], 0.05)

        # Dimtag form for an explicit volume
        g.mesh.sizing.set_size([(3, vol_tag)], 10.0)
    """
    self._guard("set_size")
    from apeGmsh.core._helpers import resolve_to_dimtags

    # Resolve to dimtags at native dim (label/PG aware).
    # `dim` falls back to 0 only as the raw-int default to
    # preserve the historical "bare ints are points" behaviour.
    default_dim = 0 if dim is None else dim
    dimtags = resolve_to_dimtags(
        tags, default_dim=default_dim, session=self._mesh._parent,
    )

    # Walk any non-point entity to its boundary points.
    point_dimtags: list[tuple[int, int]] = []
    seen: set[int] = set()
    for d, t in dimtags:
        if d == 0:
            if t not in seen:
                seen.add(t)
                point_dimtags.append((0, t))
            continue
        boundary = gmsh.model.getBoundary(
            [(d, t)], combined=False, oriented=False, recursive=True,
        )
        for _, pt in boundary:
            pt = abs(int(pt))
            if pt not in seen:
                seen.add(pt)
                point_dimtags.append((0, pt))

    if not point_dimtags:
        raise ValueError(
            f"set_size: no points resolved from {tags!r} "
            f"(dimtags={dimtags}). Cannot apply characteristic length."
        )

    gmsh.model.mesh.setSize(point_dimtags, size)
    self._mesh._directives.append({
        'kind': 'set_size', 'dim': 0,
        'tags': [t for _, t in point_dimtags], 'size': size,
        'source_ref': repr(tags),
    })
    self._mesh._log(
        f"set_size(ref={tags!r}, size={size}, "
        f"n_points={len(point_dimtags)})"
    )
    return self

set_size_all_points

set_size_all_points(size: float) -> '_Sizing'

Assign the same characteristic length to every BRep point in the model.

Typical use — normalising per-point lc values after an IGES/STEP/DXF import.

Example

::

(g.mesh.sizing
   .set_size_all_points(6000)
   .set_global_size(6000))
Source code in src/apeGmsh/mesh/_mesh_sizing.py
def set_size_all_points(self, size: float) -> "_Sizing":
    """
    Assign the same characteristic length to every BRep point in
    the model.

    Typical use — normalising per-point ``lc`` values after an
    IGES/STEP/DXF import.

    Example
    -------
    ::

        (g.mesh.sizing
           .set_size_all_points(6000)
           .set_global_size(6000))
    """
    self._guard("set_size_all_points")
    pts = gmsh.model.getEntities(dim=0)
    if pts:
        gmsh.model.mesh.setSize(pts, size)
    self._mesh._directives.append({
        'kind': 'set_size_all_points', 'size': size,
        'n_points': len(pts),
    })
    self._mesh._log(f"set_size_all_points(size={size}, n={len(pts)})")
    return self

set_size_callback

set_size_callback(func: Callable[[float, float, float, float, int, int], float]) -> '_Sizing'

Register a Python callback that returns the desired element size at any point in the model.

Callback signature func(dim, tag, x, y, z, lc) -> float.

Source code in src/apeGmsh/mesh/_mesh_sizing.py
def set_size_callback(
    self,
    func: Callable[[float, float, float, float, int, int], float],
) -> "_Sizing":
    """
    Register a Python callback that returns the desired element
    size at any point in the model.

    Callback signature ``func(dim, tag, x, y, z, lc) -> float``.
    """
    self._guard("set_size_callback")
    gmsh.model.mesh.setSizeCallback(func)
    self._mesh._directives.append({
        'kind': 'set_size_callback',
        'func_name': getattr(func, '__name__', '<callable>'),
    })
    self._mesh._log("set_size_callback(<callable>)")
    return self

set_size_by_physical

set_size_by_physical(name: str, size: float, *, dim: int | None = None) -> '_Sizing'

Apply :meth:set_size to every entity in a physical group.

Resolves the PG (optionally restricted to dim), then delegates to :meth:set_size, which walks higher-dimensional entities down to their boundary points automatically.

Parameters

name : physical-group name. size : target characteristic length. dim : optional dimension filter; None resolves at the PG's native dim.

Source code in src/apeGmsh/mesh/_mesh_sizing.py
def set_size_by_physical(
    self,
    name : str,
    size : float,
    *,
    dim  : int | None = None,
) -> "_Sizing":
    """
    Apply :meth:`set_size` to every entity in a physical group.

    Resolves the PG (optionally restricted to ``dim``), then
    delegates to :meth:`set_size`, which walks higher-dimensional
    entities down to their boundary points automatically.

    Parameters
    ----------
    name : physical-group name.
    size : target characteristic length.
    dim  : optional dimension filter; ``None`` resolves at the
           PG's native dim.
    """
    self._guard("set_size_by_physical")
    # If dim is given, restrict resolution; otherwise let set_size
    # auto-resolve via the string path (label-then-PG).
    if dim is not None:
        tags = self._mesh._resolve_physical(name, dim)
        dimtags = [(dim, t) for t in tags]
        self._mesh._log(
            f"set_size_by_physical(name={name!r}, dim={dim}, "
            f"tags={tags}, size={size})"
        )
        self.set_size(dimtags, size)
    else:
        self._mesh._log(
            f"set_size_by_physical(name={name!r}, size={size})"
        )
        self.set_size(name, size)
    return self

g.mesh.field

apeGmsh.mesh._mesh_field.FieldHelper

FieldHelper(parent_mesh: 'Mesh')

Fluent wrapper around gmsh.model.mesh.field.

Accessed via g.mesh.field. Two usage levels:

Raw control (full flexibility)::

f = g.mesh.field.add("Distance")
g.mesh.field.set_numbers(f, "CurvesList", [1, 2, 3])
g.mesh.field.set_background(f)

Convenience builders (common fields with named parameters)::

dist  = g.mesh.field.distance(curves=[1, 2])
thr   = g.mesh.field.threshold(dist, size_min=0.05, size_max=0.5,
                                 dist_min=0.1, dist_max=1.0)
g.mesh.field.set_background(thr)
Source code in src/apeGmsh/mesh/_mesh_field.py
def __init__(self, parent_mesh: "Mesh") -> None:
    self._mesh = parent_mesh

add

add(field_type: str) -> int

Create a new field of the given type and return its tag.

Source code in src/apeGmsh/mesh/_mesh_field.py
def add(self, field_type: str) -> int:
    """Create a new field of the given type and return its tag."""
    tag = gmsh.model.mesh.field.add(field_type)
    self._mesh._directives.append({
        'kind': 'field_add', 'field_type': field_type, 'field_tag': tag,
    })
    self._log(f"add({field_type!r}) -> field tag {tag}")
    return tag

set_number

set_number(tag: int, name: str, value: float) -> 'FieldHelper'

Set a scalar parameter on a field.

Source code in src/apeGmsh/mesh/_mesh_field.py
def set_number(self, tag: int, name: str, value: float) -> "FieldHelper":
    """Set a scalar parameter on a field."""
    gmsh.model.mesh.field.setNumber(tag, name, value)
    return self

set_numbers

set_numbers(tag: int, name: str, values: list[float]) -> 'FieldHelper'

Set a list parameter on a field.

Source code in src/apeGmsh/mesh/_mesh_field.py
def set_numbers(self, tag: int, name: str, values: list[float]) -> "FieldHelper":
    """Set a list parameter on a field."""
    gmsh.model.mesh.field.setNumbers(tag, name, values)
    return self

set_string

set_string(tag: int, name: str, value: str) -> 'FieldHelper'

Set a string parameter on a field.

Source code in src/apeGmsh/mesh/_mesh_field.py
def set_string(self, tag: int, name: str, value: str) -> "FieldHelper":
    """Set a string parameter on a field."""
    gmsh.model.mesh.field.setString(tag, name, value)
    return self

set_background

set_background(tag: int) -> 'FieldHelper'

Register a field as the global background mesh size.

Source code in src/apeGmsh/mesh/_mesh_field.py
def set_background(self, tag: int) -> "FieldHelper":
    """Register a field as the global background mesh size."""
    gmsh.model.mesh.field.setAsBackgroundMesh(tag)
    # gmsh has no getter for the background field; track it so
    # g.mesh.recipe can fold a user-set background into its Min
    # combiner instead of silently replacing it (ADR 0059 §3).
    self._mesh._background_field_tag = tag
    self._mesh._directives.append({
        'kind': 'field_background', 'field_tag': tag,
    })
    self._log(f"set_background(field={tag})")
    return self

set_boundary_layer_field

set_boundary_layer_field(tag: int) -> 'FieldHelper'

Register a BoundaryLayer field to be applied during meshing.

Source code in src/apeGmsh/mesh/_mesh_field.py
def set_boundary_layer_field(self, tag: int) -> "FieldHelper":
    """Register a BoundaryLayer field to be applied during meshing."""
    gmsh.model.mesh.field.setAsBoundaryLayer(tag)
    self._log(f"set_boundary_layer_field(field={tag})")
    return self

distance

distance(*, curves=None, surfaces=None, points=None, sampling: int = 100) -> int

Create a Distance field measuring shortest distance to entities.

Each of curves, surfaces, points accepts an int, a label or PG name, a (dim, tag) tuple, or a list of any mix. Refs are validated against the expected dimension.

Source code in src/apeGmsh/mesh/_mesh_field.py
def distance(
    self,
    *,
    curves  = None,
    surfaces = None,
    points   = None,
    sampling: int = 100,
) -> int:
    """Create a ``Distance`` field measuring shortest distance to entities.

    Each of ``curves``, ``surfaces``, ``points`` accepts an int, a
    label or PG name, a ``(dim, tag)`` tuple, or a list of any mix.
    Refs are validated against the expected dimension.
    """
    curve_tags = self._resolve_at_dim(curves, 1, "distance(curves=)")
    surf_tags  = self._resolve_at_dim(surfaces, 2, "distance(surfaces=)")
    point_tags = self._resolve_at_dim(points, 0, "distance(points=)")

    tag = gmsh.model.mesh.field.add("Distance")
    if curve_tags:
        gmsh.model.mesh.field.setNumbers(tag, "CurvesList",   curve_tags)
    if surf_tags:
        gmsh.model.mesh.field.setNumbers(tag, "SurfacesList", surf_tags)
    if point_tags:
        gmsh.model.mesh.field.setNumbers(tag, "PointsList",   point_tags)
    gmsh.model.mesh.field.setNumber(tag, "Sampling", sampling)
    self._log(
        f"distance(curves={curve_tags!r}, surfaces={surf_tags!r}, "
        f"points={point_tags!r}) -> field {tag}"
    )
    return tag

threshold

threshold(distance_field: int, *, size_min: float, size_max: float, dist_min: float, dist_max: float, sigmoid: bool = False, stop_at_dist_max: bool = False) -> int

Create a Threshold field ramping size from size_min to size_max.

Source code in src/apeGmsh/mesh/_mesh_field.py
def threshold(
    self,
    distance_field : int,
    *,
    size_min       : float,
    size_max       : float,
    dist_min       : float,
    dist_max       : float,
    sigmoid        : bool = False,
    stop_at_dist_max: bool = False,
) -> int:
    """Create a ``Threshold`` field ramping size from size_min to size_max."""
    tag = gmsh.model.mesh.field.add("Threshold")
    gmsh.model.mesh.field.setNumber(tag, "InField",        distance_field)
    gmsh.model.mesh.field.setNumber(tag, "SizeMin",        size_min)
    gmsh.model.mesh.field.setNumber(tag, "SizeMax",        size_max)
    gmsh.model.mesh.field.setNumber(tag, "DistMin",        dist_min)
    gmsh.model.mesh.field.setNumber(tag, "DistMax",        dist_max)
    gmsh.model.mesh.field.setNumber(tag, "Sigmoid",        int(sigmoid))
    gmsh.model.mesh.field.setNumber(tag, "StopAtDistMax",  int(stop_at_dist_max))
    self._log(
        f"threshold(in={distance_field}, "
        f"size=[{size_min},{size_max}], "
        f"dist=[{dist_min},{dist_max}]) -> field {tag}"
    )
    return tag

math_eval

math_eval(expression: str) -> int

Create a MathEval field using an expression in x, y, z.

Source code in src/apeGmsh/mesh/_mesh_field.py
def math_eval(self, expression: str) -> int:
    """Create a ``MathEval`` field using an expression in x, y, z."""
    tag = gmsh.model.mesh.field.add("MathEval")
    gmsh.model.mesh.field.setString(tag, "F", expression)
    self._log(f"math_eval({expression!r}) -> field {tag}")
    return tag

box

box(*, x_min: float, y_min: float, z_min: float, x_max: float, y_max: float, z_max: float, size_in: float, size_out: float, thickness: float = 0.0) -> int

Create a Box field: size_in inside, size_out outside.

Source code in src/apeGmsh/mesh/_mesh_field.py
def box(
    self,
    *,
    x_min    : float,
    y_min    : float,
    z_min    : float,
    x_max    : float,
    y_max    : float,
    z_max    : float,
    size_in  : float,
    size_out : float,
    thickness: float = 0.0,
) -> int:
    """Create a ``Box`` field: size_in inside, size_out outside."""
    tag = gmsh.model.mesh.field.add("Box")
    gmsh.model.mesh.field.setNumber(tag, "VIn",  size_in)
    gmsh.model.mesh.field.setNumber(tag, "VOut", size_out)
    gmsh.model.mesh.field.setNumber(tag, "XMin", x_min)
    gmsh.model.mesh.field.setNumber(tag, "YMin", y_min)
    gmsh.model.mesh.field.setNumber(tag, "ZMin", z_min)
    gmsh.model.mesh.field.setNumber(tag, "XMax", x_max)
    gmsh.model.mesh.field.setNumber(tag, "YMax", y_max)
    gmsh.model.mesh.field.setNumber(tag, "ZMax", z_max)
    if thickness > 0.0:
        gmsh.model.mesh.field.setNumber(tag, "Thickness", thickness)
    self._log(
        f"box(size_in={size_in}, size_out={size_out}, "
        f"x=[{x_min},{x_max}], y=[{y_min},{y_max}], "
        f"z=[{z_min},{z_max}]) -> field {tag}"
    )
    return tag

minimum

minimum(field_tags: list[int]) -> int

Create a Min field — element-wise minimum of several fields.

Source code in src/apeGmsh/mesh/_mesh_field.py
def minimum(self, field_tags: list[int]) -> int:
    """Create a ``Min`` field — element-wise minimum of several fields."""
    tag = gmsh.model.mesh.field.add("Min")
    gmsh.model.mesh.field.setNumbers(tag, "FieldsList", field_tags)
    self._log(f"minimum({field_tags}) -> field {tag}")
    return tag

boundary_layer

boundary_layer(*, curves=None, points=None, size_near: float, ratio: float = 1.2, n_layers: int = 5, thickness: float | None = None, fan_points=None) -> int

Create a BoundaryLayer field for wall-resolved meshes.

curves, points, and fan_points accept any flexible reference shape (int / label / PG / (dim, tag) / list).

Source code in src/apeGmsh/mesh/_mesh_field.py
def boundary_layer(
    self,
    *,
    curves     = None,
    points     = None,
    size_near  : float,
    ratio      : float        = 1.2,
    n_layers   : int          = 5,
    thickness  : float | None = None,
    fan_points = None,
) -> int:
    """Create a ``BoundaryLayer`` field for wall-resolved meshes.

    ``curves``, ``points``, and ``fan_points`` accept any flexible
    reference shape (int / label / PG / ``(dim, tag)`` / list).
    """
    curve_tags = self._resolve_at_dim(curves, 1, "boundary_layer(curves=)")
    point_tags = self._resolve_at_dim(points, 0, "boundary_layer(points=)")
    fan_tags   = self._resolve_at_dim(fan_points, 0,
                                      "boundary_layer(fan_points=)")

    tag = gmsh.model.mesh.field.add("BoundaryLayer")
    if curve_tags:
        gmsh.model.mesh.field.setNumbers(tag, "CurvesList",    curve_tags)
    if point_tags:
        gmsh.model.mesh.field.setNumbers(tag, "PointsList",    point_tags)
    if fan_tags:
        gmsh.model.mesh.field.setNumbers(tag, "FanPointsList", fan_tags)
    gmsh.model.mesh.field.setNumber(tag, "Size",     size_near)
    gmsh.model.mesh.field.setNumber(tag, "Ratio",    ratio)
    gmsh.model.mesh.field.setNumber(tag, "NbLayers", n_layers)
    if thickness is not None:
        gmsh.model.mesh.field.setNumber(tag, "Thickness", thickness)
    self._log(
        f"boundary_layer(size={size_near}, ratio={ratio}, "
        f"layers={n_layers}) -> field {tag}"
    )
    return tag

g.mesh.options

apeGmsh.mesh._mesh_options._Options

_Options(parent_mesh: 'Mesh')

Global Gmsh mesher options.

Each set_* method maps to one gmsh.option.setNumber("Mesh.X", v) call but adds string-enum input, validation, and logging. Methods return self for chaining.

Source code in src/apeGmsh/mesh/_mesh_options.py
def __init__(self, parent_mesh: "Mesh") -> None:
    self._mesh = parent_mesh

set_subdivision_algorithm

set_subdivision_algorithm(algorithm) -> '_Options'

Post-process tets/prisms into hexes after generation.

Maps to Mesh.SubdivisionAlgorithm.

Parameters

algorithm : str or int One of:

- ``"none"`` (0)     no subdivision (default)
- ``"all_quad"`` (1) split tris into 3 quads each
- ``"all_hex"`` (2)  split tets/prisms into hexes
Notes

Redundant for transfinite + face-recombined volumes — those produce hexes natively via the structured mesher. This option is for the unstructured-tet → hex conversion path.

Example

::

g.mesh.options.set_subdivision_algorithm("all_hex")
Source code in src/apeGmsh/mesh/_mesh_options.py
def set_subdivision_algorithm(self, algorithm) -> "_Options":
    """Post-process tets/prisms into hexes after generation.

    Maps to ``Mesh.SubdivisionAlgorithm``.

    Parameters
    ----------
    algorithm : str or int
        One of:

        - ``"none"`` (0)     no subdivision (default)
        - ``"all_quad"`` (1) split tris into 3 quads each
        - ``"all_hex"`` (2)  split tets/prisms into hexes

    Notes
    -----
    Redundant for transfinite + face-recombined volumes — those
    produce hexes natively via the structured mesher.  This option
    is for the unstructured-tet → hex conversion path.

    Example
    -------
    ::

        g.mesh.options.set_subdivision_algorithm("all_hex")
    """
    code = _resolve_enum(algorithm, _SUBDIVISION_ALGORITHM,
                         "set_subdivision_algorithm")
    gmsh.option.setNumber("Mesh.SubdivisionAlgorithm", code)
    self._mesh._log(f"options.set_subdivision_algorithm({algorithm!r})")
    return self

get_subdivision_algorithm

get_subdivision_algorithm()

Return the current Mesh.SubdivisionAlgorithm as a string or int.

Source code in src/apeGmsh/mesh/_mesh_options.py
def get_subdivision_algorithm(self):
    """Return the current ``Mesh.SubdivisionAlgorithm`` as a string or int."""
    return _enum_string_or_int(
        int(gmsh.option.getNumber("Mesh.SubdivisionAlgorithm")),
        _SUBDIVISION_ALGORITHM,
    )

set_smoothing

set_smoothing(iterations: int) -> '_Options'

Number of global Laplacian smoothing passes applied after generation.

Maps to Mesh.Smoothing.

Parameters

iterations : int Number of smoothing passes. 0 disables. Higher values improve element quality on unstructured meshes; on transfinite meshes the option is effectively a no-op since interior nodes are already on the structured grid.

See Also

g.mesh.structured.set_smoothing : Per-entity smoothing constraint (different — sets gmsh.model.mesh.setSmoothing on a specific tag).

Source code in src/apeGmsh/mesh/_mesh_options.py
def set_smoothing(self, iterations: int) -> "_Options":
    """Number of global Laplacian smoothing passes applied after generation.

    Maps to ``Mesh.Smoothing``.

    Parameters
    ----------
    iterations : int
        Number of smoothing passes.  ``0`` disables.  Higher values
        improve element quality on unstructured meshes; on
        transfinite meshes the option is effectively a no-op since
        interior nodes are already on the structured grid.

    See Also
    --------
    g.mesh.structured.set_smoothing :
        Per-entity smoothing constraint (different — sets
        ``gmsh.model.mesh.setSmoothing`` on a specific tag).
    """
    gmsh.option.setNumber("Mesh.Smoothing", int(iterations))
    self._mesh._log(f"options.set_smoothing(iterations={iterations})")
    return self

get_smoothing

get_smoothing() -> int

Return the current Mesh.Smoothing value.

Source code in src/apeGmsh/mesh/_mesh_options.py
def get_smoothing(self) -> int:
    """Return the current ``Mesh.Smoothing`` value."""
    return int(gmsh.option.getNumber("Mesh.Smoothing"))

set_element_order

set_element_order(order: int) -> '_Options'

Element interpolation order.

Maps to Mesh.ElementOrder.

Parameters

order : int 1 for linear elements (default), 2 for quadratic, higher orders supported but rarely used. Higher-order meshes have additional mid-edge / mid-face nodes; downstream FEM code must support the element type.

Example

::

g.mesh.options.set_element_order(2)  # quadratic hexes/tets
Source code in src/apeGmsh/mesh/_mesh_options.py
def set_element_order(self, order: int) -> "_Options":
    """Element interpolation order.

    Maps to ``Mesh.ElementOrder``.

    Parameters
    ----------
    order : int
        ``1`` for linear elements (default), ``2`` for quadratic,
        higher orders supported but rarely used.  Higher-order
        meshes have additional mid-edge / mid-face nodes; downstream
        FEM code must support the element type.

    Example
    -------
    ::

        g.mesh.options.set_element_order(2)  # quadratic hexes/tets
    """
    gmsh.option.setNumber("Mesh.ElementOrder", int(order))
    self._mesh._log(f"options.set_element_order({order})")
    return self

get_element_order

get_element_order() -> int

Return the current Mesh.ElementOrder value.

Source code in src/apeGmsh/mesh/_mesh_options.py
def get_element_order(self) -> int:
    """Return the current ``Mesh.ElementOrder`` value."""
    return int(gmsh.option.getNumber("Mesh.ElementOrder"))

set_algorithm_2d

set_algorithm_2d(algorithm) -> '_Options'

2-D meshing algorithm.

Maps to Mesh.Algorithm.

Parameters

algorithm : str or int One of:

- ``"meshadapt"`` (1)             adaptive Delaunay
- ``"automatic"`` (2)             default — let Gmsh choose
- ``"delaunay"`` (5)              pure Delaunay
- ``"frontal"`` (6)               frontal-Delaunay
- ``"bamg"`` (7)                  anisotropic remeshing
- ``"frontal_quads"`` (8)         frontal-Delaunay for quads
- ``"packing"`` (9)               packing of parallelograms
- ``"quasi_structured_quad"`` (11) quasi-structured quads
  (Gmsh ≥ 4.10)
Source code in src/apeGmsh/mesh/_mesh_options.py
def set_algorithm_2d(self, algorithm) -> "_Options":
    """2-D meshing algorithm.

    Maps to ``Mesh.Algorithm``.

    Parameters
    ----------
    algorithm : str or int
        One of:

        - ``"meshadapt"`` (1)             adaptive Delaunay
        - ``"automatic"`` (2)             default — let Gmsh choose
        - ``"delaunay"`` (5)              pure Delaunay
        - ``"frontal"`` (6)               frontal-Delaunay
        - ``"bamg"`` (7)                  anisotropic remeshing
        - ``"frontal_quads"`` (8)         frontal-Delaunay for quads
        - ``"packing"`` (9)               packing of parallelograms
        - ``"quasi_structured_quad"`` (11) quasi-structured quads
          (Gmsh ≥ 4.10)
    """
    code = _resolve_enum(algorithm, _ALGORITHM_2D, "set_algorithm_2d")
    gmsh.option.setNumber("Mesh.Algorithm", code)
    self._mesh._log(f"options.set_algorithm_2d({algorithm!r})")
    return self

get_algorithm_2d

get_algorithm_2d()

Return the current Mesh.Algorithm as a string or int.

Source code in src/apeGmsh/mesh/_mesh_options.py
def get_algorithm_2d(self):
    """Return the current ``Mesh.Algorithm`` as a string or int."""
    return _enum_string_or_int(
        int(gmsh.option.getNumber("Mesh.Algorithm")),
        _ALGORITHM_2D,
    )

set_algorithm_3d

set_algorithm_3d(algorithm) -> '_Options'

3-D meshing algorithm.

Maps to Mesh.Algorithm3D.

Parameters

algorithm : str or int One of:

- ``"delaunay"`` (1)         pure Delaunay (default)
- ``"frontal"`` (4)          frontal
- ``"frontal_delaunay"`` (5) frontal-Delaunay
- ``"mmg3d"`` (7)            anisotropic remeshing
- ``"rtree"`` (9)            R-tree based
- ``"hxt"`` (10)             HXT — much faster than Delaunay
  on large unstructured tet meshes
Source code in src/apeGmsh/mesh/_mesh_options.py
def set_algorithm_3d(self, algorithm) -> "_Options":
    """3-D meshing algorithm.

    Maps to ``Mesh.Algorithm3D``.

    Parameters
    ----------
    algorithm : str or int
        One of:

        - ``"delaunay"`` (1)         pure Delaunay (default)
        - ``"frontal"`` (4)          frontal
        - ``"frontal_delaunay"`` (5) frontal-Delaunay
        - ``"mmg3d"`` (7)            anisotropic remeshing
        - ``"rtree"`` (9)            R-tree based
        - ``"hxt"`` (10)             HXT — much faster than Delaunay
          on large unstructured tet meshes
    """
    code = _resolve_enum(algorithm, _ALGORITHM_3D, "set_algorithm_3d")
    gmsh.option.setNumber("Mesh.Algorithm3D", code)
    self._mesh._log(f"options.set_algorithm_3d({algorithm!r})")
    return self

get_algorithm_3d

get_algorithm_3d()

Return the current Mesh.Algorithm3D as a string or int.

Source code in src/apeGmsh/mesh/_mesh_options.py
def get_algorithm_3d(self):
    """Return the current ``Mesh.Algorithm3D`` as a string or int."""
    return _enum_string_or_int(
        int(gmsh.option.getNumber("Mesh.Algorithm3D")),
        _ALGORITHM_3D,
    )

set_recombination_algorithm

set_recombination_algorithm(algorithm) -> '_Options'

Strategy used when recombining triangles into quads.

Maps to Mesh.RecombinationAlgorithm.

Parameters

algorithm : str or int One of:

- ``"simple"`` (0)       simple
- ``"blossom"`` (1)      Blossom — better quality, default
- ``"simple_full"`` (2)  simple, full recombination
- ``"blossom_full"`` (3) Blossom, full recombination —
  highest quality for all-quad meshes
Source code in src/apeGmsh/mesh/_mesh_options.py
def set_recombination_algorithm(self, algorithm) -> "_Options":
    """Strategy used when recombining triangles into quads.

    Maps to ``Mesh.RecombinationAlgorithm``.

    Parameters
    ----------
    algorithm : str or int
        One of:

        - ``"simple"`` (0)       simple
        - ``"blossom"`` (1)      Blossom — better quality, default
        - ``"simple_full"`` (2)  simple, full recombination
        - ``"blossom_full"`` (3) Blossom, full recombination —
          highest quality for all-quad meshes
    """
    code = _resolve_enum(algorithm, _RECOMBINATION_ALGORITHM,
                         "set_recombination_algorithm")
    gmsh.option.setNumber("Mesh.RecombinationAlgorithm", code)
    self._mesh._log(f"options.set_recombination_algorithm({algorithm!r})")
    return self

get_recombination_algorithm

get_recombination_algorithm()

Return the current Mesh.RecombinationAlgorithm as a string or int.

Source code in src/apeGmsh/mesh/_mesh_options.py
def get_recombination_algorithm(self):
    """Return the current ``Mesh.RecombinationAlgorithm`` as a string or int."""
    return _enum_string_or_int(
        int(gmsh.option.getNumber("Mesh.RecombinationAlgorithm")),
        _RECOMBINATION_ALGORITHM,
    )

Common recipes

# All-hex output for an unstructured tet model
g.mesh.options.set_subdivision_algorithm("all_hex")

# Quadratic elements (8-node hex → 27-node hex, 4-node tet → 10-node tet)
g.mesh.options.set_element_order(2)

# HXT algorithm — much faster than Delaunay for large unstructured tet meshes
g.mesh.options.set_algorithm_3d("hxt")

# Highest-quality all-quad surface mesh
g.mesh.options.set_recombination_algorithm("blossom_full")
g.mesh.options.set_algorithm_2d("frontal_quads")

# Fluent chaining — every setter returns self
(g.mesh.options
    .set_algorithm_3d("hxt")
    .set_element_order(2)
    .set_smoothing(iterations=3))

For options not wrapped here, drop to gmsh.option.setNumber("Mesh.X", v) directly. The wrapper covers the ~6 options users actually tune; Gmsh has ~50+ Mesh.* options total.

g.mesh.structured

apeGmsh.mesh._mesh_structured._Structured

_Structured(parent_mesh: 'Mesh')

Transfinite constraints, recombination, smoothing, compounds.

Source code in src/apeGmsh/mesh/_mesh_structured.py
def __init__(self, parent_mesh: "Mesh") -> None:
    self._mesh = parent_mesh
    # Dimtags warn-skipped by the most recent set_transfinite() call
    # (non-decomposable entities).  Read by g.mesh.recipe.structured
    # to apply its sizing fallback (ADR 0059 §4); reset per call.
    self._last_skipped: list[DimTag] = []

set_transfinite_curve

set_transfinite_curve(tag, n_nodes: int, *, mesh_type: str = 'Progression', coef: float = 1.0) -> '_Structured'

Force a curve to be meshed with a deterministic node count and distribution.

A "transfinite" curve has its nodes placed by formula rather than by the unstructured mesher. This is the building block for structured meshes: when every bounding curve of a surface is transfinite, the surface itself can be made transfinite (a structured grid); same for volumes built from transfinite surfaces.

Parameters

tag : Curve identifier. Accepts an int tag, a label string, a physical-group name, a (1, tag) dimtag, or a list of any of these (constraint applied to each). Works with :meth:Selection.tags() <apeGmsh.core._selection.Selection.tags> output. n_nodes : int Number of nodes along the curve (≥ 2). n_nodes - 1 elements are produced. mesh_type : str, default "Progression" Node distribution rule. One of:

- ``"Progression"`` — geometric progression.  ``coef`` is
  the ratio between successive intervals.
  ``coef = 1`` ⇒ uniform; ``coef > 1`` clusters nodes
  toward the curve's end point; ``coef < 1`` clusters
  toward the start.
- ``"Bump"`` — symmetric biasing.  ``coef > 1`` clusters
  toward the middle; ``coef < 1`` clusters toward both
  ends; ``coef = 1`` ⇒ uniform.
- ``"Beta"`` — Beta-distribution biasing (Gmsh ≥ 4.10).

coef : float, default 1.0 Distribution parameter — meaning depends on mesh_type (see above). Use 1.0 for a uniform distribution.

Returns

_Structured self for chaining.

Notes

The constraint is only honored if the curve is part of a transfinite surface (and the surface part of a transfinite volume, for 3-D meshes). A transfinite curve in isolation will simply seed the unstructured mesher with that node count.

Curve direction matters for non-uniform distributions: which end is "start" vs "end" follows the curve's intrinsic orientation in Gmsh. Reverse the orientation (or pass coef = 1/r instead of r) to flip the clustering.

Examples

Uniform 11-node spacing on a single curve::

m.mesh.structured.set_transfinite_curve(curve_tag, n_nodes=11)

Geometric refinement toward the curve's end (boundary-layer style)::

m.mesh.structured.set_transfinite_curve(
    curve_tag, n_nodes=21,
    mesh_type="Progression", coef=1.1,
)

Symmetric refinement toward the middle (capture a feature at mid-span)::

m.mesh.structured.set_transfinite_curve(
    curve_tag, n_nodes=21,
    mesh_type="Bump", coef=0.25,
)

Pass a Selection's tags to constrain many curves at once::

edges = m.model.select(None, dim=1).result()
m.mesh.structured.set_transfinite_curve(
    edges.parallel_to("z").tags(),
    n_nodes=21,
)
Source code in src/apeGmsh/mesh/_mesh_structured.py
def set_transfinite_curve(
    self,
    tag,
    n_nodes  : int,
    *,
    mesh_type: str   = "Progression",
    coef     : float = 1.0,
) -> "_Structured":
    """Force a curve to be meshed with a deterministic node count and distribution.

    A "transfinite" curve has its nodes placed by formula rather than by
    the unstructured mesher.  This is the building block for structured
    meshes: when every bounding curve of a surface is transfinite, the
    surface itself can be made transfinite (a structured grid); same for
    volumes built from transfinite surfaces.

    Parameters
    ----------
    tag :
        Curve identifier.  Accepts an int tag, a label string, a
        physical-group name, a ``(1, tag)`` dimtag, or a list of any
        of these (constraint applied to each).  Works with
        :meth:`Selection.tags() <apeGmsh.core._selection.Selection.tags>`
        output.
    n_nodes : int
        Number of nodes along the curve (≥ 2).  ``n_nodes - 1``
        elements are produced.
    mesh_type : str, default ``"Progression"``
        Node distribution rule.  One of:

        - ``"Progression"`` — geometric progression.  ``coef`` is
          the ratio between successive intervals.
          ``coef = 1`` ⇒ uniform; ``coef > 1`` clusters nodes
          toward the curve's end point; ``coef < 1`` clusters
          toward the start.
        - ``"Bump"`` — symmetric biasing.  ``coef > 1`` clusters
          toward the middle; ``coef < 1`` clusters toward both
          ends; ``coef = 1`` ⇒ uniform.
        - ``"Beta"`` — Beta-distribution biasing (Gmsh ≥ 4.10).
    coef : float, default ``1.0``
        Distribution parameter — meaning depends on ``mesh_type``
        (see above).  Use ``1.0`` for a uniform distribution.

    Returns
    -------
    _Structured
        ``self`` for chaining.

    Notes
    -----
    The constraint is only honored if the curve is part of a
    transfinite surface (and the surface part of a transfinite volume,
    for 3-D meshes).  A transfinite curve in isolation will simply
    seed the unstructured mesher with that node count.

    Curve **direction** matters for non-uniform distributions: which
    end is "start" vs "end" follows the curve's intrinsic orientation
    in Gmsh.  Reverse the orientation (or pass ``coef = 1/r`` instead
    of ``r``) to flip the clustering.

    Examples
    --------
    Uniform 11-node spacing on a single curve::

        m.mesh.structured.set_transfinite_curve(curve_tag, n_nodes=11)

    Geometric refinement toward the curve's end (boundary-layer style)::

        m.mesh.structured.set_transfinite_curve(
            curve_tag, n_nodes=21,
            mesh_type="Progression", coef=1.1,
        )

    Symmetric refinement toward the middle (capture a feature at mid-span)::

        m.mesh.structured.set_transfinite_curve(
            curve_tag, n_nodes=21,
            mesh_type="Bump", coef=0.25,
        )

    Pass a Selection's tags to constrain many curves at once::

        edges = m.model.select(None, dim=1).result()
        m.mesh.structured.set_transfinite_curve(
            edges.parallel_to("z").tags(),
            n_nodes=21,
        )
    """
    for t in self._resolve(tag, dim=1):
        gmsh.model.mesh.setTransfiniteCurve(t, n_nodes,
                                             meshType=mesh_type, coef=coef)
        self._mesh._directives.append({
            'kind': 'transfinite_curve', 'tag': t,
            'n_nodes': n_nodes, 'mesh_type': mesh_type, 'coef': coef,
        })
        self._mesh._log(
            f"set_transfinite_curve(tag={t}, n={n_nodes}, "
            f"type={mesh_type!r}, coef={coef})"
        )
    return self

set_transfinite_surface

set_transfinite_surface(tag, *, arrangement: str = 'Left', corners: list[int] | None = None) -> '_Structured'

Force a surface to be meshed as a structured grid.

A transfinite surface has its interior nodes laid out by transfinite interpolation between its bounding curves. Combined with transfinite curves on every bounding edge, this produces a fully structured surface mesh (triangles by default, quads with :meth:set_recombine).

Parameters

tag : Surface identifier. Accepts an int tag, label string, physical-group name, (2, tag) dimtag, or list of any. arrangement : str, default "Left" Diagonal direction for the structured triangles. Ignored once the surface is recombined to quads. Values:

- ``"Left"``        all diagonals slant the same way
- ``"Right"``       all diagonals slant the other way
- ``"AlternateLeft"`` alternating pattern, starting Left
- ``"AlternateRight"`` alternating pattern, starting Right

corners : list[int] | None, default None Tags of the 3 or 4 corner points defining the structured topology. Required when the surface has more than 4 bounding curves (e.g. after a face split) or when Gmsh can't auto-detect the corners. Pass None for a clean 3- or 4-sided face.

Returns

_Structured self for chaining.

Prerequisites

Every bounding curve of the surface must already be transfinite (see :meth:set_transfinite_curve). Opposite edges must have matching n_nodes (or the mesher will fail at generation time with a "transfinite surface: inconsistent number of nodes" error).

Examples

Clean rectangle::

m.mesh.structured.set_transfinite_surface(face_tag)

Surface with 5+ bounding curves — pick the 4 logical corners::

m.mesh.structured.set_transfinite_surface(
    face_tag, corners=[p1, p2, p3, p4],
)

Apply to every horizontal face of a layer::

faces = m.model.select("layer_1", dim=2).result()
m.mesh.structured.set_transfinite_surface(
    faces.normal_along("z").tags(),
)
Notes

For a quad/hex mesh, follow with :meth:set_recombine on the same surface. The all-in-one helper :meth:set_transfinite_box does this automatically for clean hex volumes.

Source code in src/apeGmsh/mesh/_mesh_structured.py
def set_transfinite_surface(
    self,
    tag,
    *,
    arrangement: str            = "Left",
    corners    : list[int] | None = None,
) -> "_Structured":
    """Force a surface to be meshed as a structured grid.

    A transfinite surface has its interior nodes laid out by transfinite
    interpolation between its bounding curves.  Combined with transfinite
    curves on every bounding edge, this produces a fully structured
    surface mesh (triangles by default, quads with
    :meth:`set_recombine`).

    Parameters
    ----------
    tag :
        Surface identifier.  Accepts an int tag, label string,
        physical-group name, ``(2, tag)`` dimtag, or list of any.
    arrangement : str, default ``"Left"``
        Diagonal direction for the structured triangles.  Ignored
        once the surface is recombined to quads.  Values:

        - ``"Left"``        all diagonals slant the same way
        - ``"Right"``       all diagonals slant the other way
        - ``"AlternateLeft"`` alternating pattern, starting Left
        - ``"AlternateRight"`` alternating pattern, starting Right
    corners : list[int] | None, default ``None``
        Tags of the 3 or 4 corner points defining the structured
        topology.  Required when the surface has **more than 4
        bounding curves** (e.g. after a face split) or when Gmsh
        can't auto-detect the corners.  Pass ``None`` for a clean
        3- or 4-sided face.

    Returns
    -------
    _Structured
        ``self`` for chaining.

    Prerequisites
    -------------
    Every bounding curve of the surface must already be transfinite
    (see :meth:`set_transfinite_curve`).  Opposite edges must have
    matching ``n_nodes`` (or the mesher will fail at generation time
    with a "transfinite surface: inconsistent number of nodes" error).

    Examples
    --------
    Clean rectangle::

        m.mesh.structured.set_transfinite_surface(face_tag)

    Surface with 5+ bounding curves — pick the 4 logical corners::

        m.mesh.structured.set_transfinite_surface(
            face_tag, corners=[p1, p2, p3, p4],
        )

    Apply to every horizontal face of a layer::

        faces = m.model.select("layer_1", dim=2).result()
        m.mesh.structured.set_transfinite_surface(
            faces.normal_along("z").tags(),
        )

    Notes
    -----
    For a quad/hex mesh, follow with :meth:`set_recombine` on the
    same surface.  The all-in-one helper :meth:`set_transfinite_box`
    does this automatically for clean hex volumes.
    """
    for t in self._resolve(tag, dim=2):
        gmsh.model.mesh.setTransfiniteSurface(t, arrangement=arrangement,
                                               cornerTags=corners or [])
        self._mesh._directives.append({
            'kind': 'transfinite_surface', 'tag': t,
            'arrangement': arrangement,
            'corners': corners or [],
        })
        self._mesh._log(
            f"set_transfinite_surface(tag={t}, "
            f"arrangement={arrangement!r})"
        )
    return self

set_transfinite_volume

set_transfinite_volume(tag, *, corners: list[int] | None = None) -> '_Structured'

Force a volume to be meshed as a structured grid.

A transfinite volume has its interior nodes laid out by transfinite interpolation between its bounding surfaces. Combined with :meth:set_recombine on each face, this produces a pure hex mesh.

Parameters

tag : Volume identifier. Accepts an int tag, label string, physical-group name, (3, tag) dimtag, or list of any. corners : list[int] | None, default None Tags of the 6 (prism) or 8 (hex) corner points that define the structured topology. Required when the volume has irregular face counts or when Gmsh can't auto-detect the corners. Pass None for a clean 5- or 6-faced volume.

Returns

_Structured self for chaining.

Prerequisites
  • Every bounding surface must already be transfinite (:meth:set_transfinite_surface).
  • Opposite surfaces must have matching node counts on their shared edges.
Examples

Single hex (after edges and faces are already transfinite)::

m.mesh.structured.set_transfinite_volume(vol_tag)
Notes

For the common case of "transfinite + recombine + hex on every face of a clean box," use :meth:set_transfinite_box instead — it sets the constraints on edges, faces, and volume in one call.

Source code in src/apeGmsh/mesh/_mesh_structured.py
def set_transfinite_volume(
    self,
    tag,
    *,
    corners: list[int] | None = None,
) -> "_Structured":
    """Force a volume to be meshed as a structured grid.

    A transfinite volume has its interior nodes laid out by transfinite
    interpolation between its bounding surfaces.  Combined with
    :meth:`set_recombine` on each face, this produces a pure hex mesh.

    Parameters
    ----------
    tag :
        Volume identifier.  Accepts an int tag, label string,
        physical-group name, ``(3, tag)`` dimtag, or list of any.
    corners : list[int] | None, default ``None``
        Tags of the 6 (prism) or 8 (hex) corner points that define
        the structured topology.  Required when the volume has
        irregular face counts or when Gmsh can't auto-detect the
        corners.  Pass ``None`` for a clean 5- or 6-faced volume.

    Returns
    -------
    _Structured
        ``self`` for chaining.

    Prerequisites
    -------------
    - Every bounding surface must already be transfinite
      (:meth:`set_transfinite_surface`).
    - Opposite surfaces must have matching node counts on their
      shared edges.

    Examples
    --------
    Single hex (after edges and faces are already transfinite)::

        m.mesh.structured.set_transfinite_volume(vol_tag)

    Notes
    -----
    For the common case of "transfinite + recombine + hex on every
    face of a clean box," use :meth:`set_transfinite_box` instead —
    it sets the constraints on edges, faces, and volume in one call.
    """
    for t in self._resolve(tag, dim=3):
        gmsh.model.mesh.setTransfiniteVolume(t, cornerTags=corners or [])
        self._mesh._directives.append({
            'kind': 'transfinite_volume', 'tag': t,
            'corners': corners or [],
        })
        self._mesh._log(f"set_transfinite_volume(tag={t})")
    return self

set_transfinite_automatic

set_transfinite_automatic(dim_tags: list[DimTag] | None = None, *, corner_angle: float = 2.35, recombine: bool = True) -> '_Structured'

Auto-detect transfinite-compatible entities and constrain them.

Walks each surface and volume in dim_tags (or the entire model if None). A face counts as "transfinite-compatible" if it has 3 or 4 corners whose angles are within corner_angle of a flat angle (π radians). Compatible faces and the volumes built from them get transfinite + (optionally) recombine constraints applied automatically.

Useful as a fallback after boolean operations leave you with a mix of clean and split faces — :meth:set_transfinite_box would fail on the split faces, but automatic simply skips them.

Parameters

dim_tags : list[(dim, tag)] | None, default None Restrict the search to these entities. None ⇒ walk every entity in the model. corner_angle : float, default 2.35 Threshold angle in radians for the "is this a corner?" test. The default ≈ 135° is Gmsh's own — vertices whose interior angle deviates from π (180°) by less than this tolerance are not counted as corners. Reduce for stricter corner detection; raise to admit more rounded transitions. recombine : bool, default True Recombine detected faces into quads (and volumes into hexes). Set False for a transfinite tet mesh.

Returns

_Structured self for chaining.

Examples

Mesh-everything-it-can fallback after a boolean op::

m.model.boolean.fragment("box_a", "box_b")
m.mesh.structured.set_transfinite_automatic()
m.mesh.generation.generate(dim=3)

Restrict to a subset (only the volumes you care about)::

m.mesh.structured.set_transfinite_automatic(
    dim_tags=[(3, t) for t in vol_tags],
)
Source code in src/apeGmsh/mesh/_mesh_structured.py
def set_transfinite_automatic(
    self,
    dim_tags    : list[DimTag] | None = None,
    *,
    corner_angle: float = 2.35,
    recombine   : bool  = True,
) -> "_Structured":
    """Auto-detect transfinite-compatible entities and constrain them.

    Walks each surface and volume in ``dim_tags`` (or the entire model
    if ``None``).  A face counts as "transfinite-compatible" if it
    has 3 or 4 corners whose angles are within ``corner_angle`` of a
    flat angle (π radians).  Compatible faces and the volumes built
    from them get transfinite + (optionally) recombine constraints
    applied automatically.

    Useful as a fallback after boolean operations leave you with a
    mix of clean and split faces — :meth:`set_transfinite_box`
    would fail on the split faces, but ``automatic`` simply skips
    them.

    Parameters
    ----------
    dim_tags : list[(dim, tag)] | None, default ``None``
        Restrict the search to these entities.  ``None`` ⇒ walk
        every entity in the model.
    corner_angle : float, default ``2.35``
        Threshold angle in **radians** for the "is this a corner?"
        test.  The default ≈ 135° is Gmsh's own — vertices whose
        interior angle deviates from π (180°) by less than this
        tolerance are not counted as corners.  Reduce for stricter
        corner detection; raise to admit more rounded transitions.
    recombine : bool, default ``True``
        Recombine detected faces into quads (and volumes into hexes).
        Set ``False`` for a transfinite tet mesh.

    Returns
    -------
    _Structured
        ``self`` for chaining.

    Examples
    --------
    Mesh-everything-it-can fallback after a boolean op::

        m.model.boolean.fragment("box_a", "box_b")
        m.mesh.structured.set_transfinite_automatic()
        m.mesh.generation.generate(dim=3)

    Restrict to a subset (only the volumes you care about)::

        m.mesh.structured.set_transfinite_automatic(
            dim_tags=[(3, t) for t in vol_tags],
        )
    """
    # The only sibling on this composite that never called _guard;
    # it writes a mesh directive like the rest of them.
    self._guard("set_transfinite_automatic")
    gmsh.model.mesh.setTransfiniteAutomatic(
        dimTags=dim_tags or [],
        cornerAngle=corner_angle,
        recombine=recombine,
    )
    self._mesh._directives.append({
        'kind': 'transfinite_automatic',
        'dim_tags': dim_tags or [],
        'corner_angle': corner_angle,
        'recombine': recombine,
    })
    self._mesh._log(
        f"set_transfinite_automatic("
        f"corner_angle={math.degrees(corner_angle):.1f}°, "
        f"recombine={recombine})"
    )
    return self

set_transfinite_box

set_transfinite_box(vol, *, size: float | None = None, n: int | None = None, recombine: bool = True) -> '_Structured'

Apply transfinite + recombine constraints to a clean hex volume.

Captures the full "structured hex" setup in one call. Walks the volume's bounding curves, assigns a node count per edge, marks every bounding surface as transfinite (and recombined to quads when recombine=True), then marks the volume itself as transfinite.

Parameters

vol : Volume identifier. Accepts an int tag, a label string, a physical-group name, or a (3, tag) dimtag. If it resolves to multiple volumes, all are constrained. size : float, optional Target element edge length. Node count per edge is round(edge_length / size) + 1 (clamped to a minimum of 2 nodes). Provides isotropic sizing — each edge gets its own n_nodes based on its length. n : int, optional Uniform node count on every edge of the volume. Overrides per-edge length-based sizing. recombine : bool, default True Recombine each face to quads (gives a hex mesh). Set False for a transfinite tet mesh.

Returns

_Structured self for chaining.

Raises

ValueError If neither size nor n is given, or both are.

Requirements

The volume must be hex-decomposable: exactly 5 or 6 faces, each a 3- or 4-sided patch. After :meth:boolean.fragment operations, volumes may end up with split faces and stop being transfinite-compatible — in that case use :meth:set_transfinite_automatic instead, which silently skips incompatible faces.

Examples

Uniform mesh — same n per edge regardless of edge length::

m.mesh.structured.set_transfinite_box("box", n=11)

Length-based sizing — denser mesh on longer edges::

m.mesh.structured.set_transfinite_box("box", size=0.5)

Per-axis control — use the lower-level methods instead::

edges = m.model.select(None, dim=1).result()
m.mesh.structured.set_transfinite_curve(
    edges.parallel_to("x").tags(), n_nodes=11)
m.mesh.structured.set_transfinite_curve(
    edges.parallel_to("y").tags(), n_nodes=11)
m.mesh.structured.set_transfinite_curve(
    edges.parallel_to("z").tags(), n_nodes=21)
# then surfaces + volume via set_transfinite_box(recombine=...)
Source code in src/apeGmsh/mesh/_mesh_structured.py
def set_transfinite_box(
    self,
    vol,
    *,
    size: float | None = None,
    n   : int | None   = None,
    recombine: bool    = True,
) -> "_Structured":
    """Apply transfinite + recombine constraints to a clean hex volume.

    Captures the full "structured hex" setup in one call.  Walks the
    volume's bounding curves, assigns a node count per edge, marks
    every bounding surface as transfinite (and recombined to quads
    when ``recombine=True``), then marks the volume itself as
    transfinite.

    Parameters
    ----------
    vol :
        Volume identifier.  Accepts an int tag, a label string, a
        physical-group name, or a ``(3, tag)`` dimtag.  If it
        resolves to multiple volumes, all are constrained.
    size : float, optional
        Target element edge length.  Node count per edge is
        ``round(edge_length / size) + 1`` (clamped to a minimum
        of 2 nodes).  Provides isotropic sizing — each edge gets
        its own ``n_nodes`` based on its length.
    n : int, optional
        Uniform node count on **every** edge of the volume.
        Overrides per-edge length-based sizing.
    recombine : bool, default ``True``
        Recombine each face to quads (gives a hex mesh).  Set
        ``False`` for a transfinite tet mesh.

    Returns
    -------
    _Structured
        ``self`` for chaining.

    Raises
    ------
    ValueError
        If neither ``size`` nor ``n`` is given, or both are.

    Requirements
    ------------
    The volume must be **hex-decomposable**: exactly 5 or 6 faces,
    each a 3- or 4-sided patch.  After :meth:`boolean.fragment`
    operations, volumes may end up with split faces and stop being
    transfinite-compatible — in that case use
    :meth:`set_transfinite_automatic` instead, which silently
    skips incompatible faces.

    Examples
    --------
    Uniform mesh — same ``n`` per edge regardless of edge length::

        m.mesh.structured.set_transfinite_box("box", n=11)

    Length-based sizing — denser mesh on longer edges::

        m.mesh.structured.set_transfinite_box("box", size=0.5)

    Per-axis control — use the lower-level methods instead::

        edges = m.model.select(None, dim=1).result()
        m.mesh.structured.set_transfinite_curve(
            edges.parallel_to("x").tags(), n_nodes=11)
        m.mesh.structured.set_transfinite_curve(
            edges.parallel_to("y").tags(), n_nodes=11)
        m.mesh.structured.set_transfinite_curve(
            edges.parallel_to("z").tags(), n_nodes=21)
        # then surfaces + volume via set_transfinite_box(recombine=...)
    """
    if (size is None) == (n is None):
        raise ValueError("Pass exactly one of size= or n=.")

    # Resolve volume → list of dim=3 tags
    tags = self._resolve(vol, dim=3)

    for vtag in tags:
        edges = self._mesh._parent.model.queries.boundary_curves(vtag)
        faces = self._mesh._parent.model.queries.boundary(vtag, oriented=False)

        for _, ctag in edges:
            if n is not None:
                n_edge = n
            else:
                bb = self._mesh._parent.model.queries.bounding_box(ctag, dim=1)
                L  = max(bb[3] - bb[0], bb[4] - bb[1], bb[5] - bb[2])
                n_edge = max(2, round(L / size) + 1)
            self.set_transfinite_curve(ctag, n_edge)

        for _, stag in faces:
            self.set_transfinite_surface(stag)
            if recombine:
                self.set_recombine(stag, dim=2)

        self.set_transfinite_volume(vtag)

    self._mesh._log(
        f"set_transfinite_box(vol={vol!r}, size={size}, n={n}, "
        f"recombine={recombine}) — applied to {len(tags)} volume(s)"
    )
    return self

set_transfinite

set_transfinite(target=None, *, n=None, size=None, recombine: bool = True, dim: int | None = None, angle_tol_deg: float = 5.0) -> '_Structured'

Apply transfinite (+ optional recombine) to one entity, many, or the whole model.

High-level dispatcher that infers the dim of the target and applies the full cascade for that dim — curves of bounding edges, surfaces, recombine, and (for volumes) the volume itself.

Parameters

target : What to constrain. Accepts:

- ``None``  → every volume in the model
- ``"name"`` (label or PG name) → all entities under that
  name, any dim (each gets the cascade for its dim)
- a Selection — uses its dimtags
- a ``(dim, tag)`` dimtag, or list of them
- a bare int — must be paired with ``dim=``
int | tuple | dict | None

Node-count sizing. Pass exactly one of n or size.

  • int — uniform on every edge of each entity, any orientation
  • dict — per-axis, keyed by "x" / "y" / "z". Requires axis-aligned entities (raises otherwise — the error reports the cluster directions so you can switch to tuple form).
  • tuple — per principal axis, in the order (X-aligned, Y-aligned, Z-aligned). Works on any rotation; closest-global-axis assignment with lex tie-break. Length must match the entity's principal-axis count (3 for volumes, 2 for surfaces, 1 for curves).

size : float | tuple | dict | None Length-based sizing — node count per edge is round(edge_length / size) + 1. Same grammar as n. recombine : bool, default True Recombine to quads on each face and (for volumes) hex on the volume. Set False for a structured tet mesh. dim : int | None, default None Only used when target is a bare int. Ignored otherwise. angle_tol_deg : float, default 5.0 Tolerance for grouping edges into principal-axis clusters. Raise if your geometry has nearly-parallel non-orthogonal edges that you want kept separate.

Returns

_Structured self for chaining.

Behavior
  • Volumes are processed first so their bounding surfaces and edges aren't reset by a later surface-level call.
  • Entities whose edges don't cluster into the expected principal-axis count (e.g. a face split by a boolean op into a 5-sided patch) are warned and skipped rather than failing the whole call.
Examples

Whole-model, uniform sizing::

g.mesh.structured.set_transfinite(n=11)

Axis-aligned box, per-axis sizing::

g.mesh.structured.set_transfinite(
    "layer_top", n={"x": 101, "y": 101, "z": 6},
)

Rotated box, per principal axis (tuple)::

g.mesh.structured.set_transfinite("rotated_box", n=(11, 11, 21))

Length-based sizing on a named surface::

g.mesh.structured.set_transfinite("base", size={"x": 100, "y": 100})

For per-edge bias (Progression/Bump/Beta) or explicit corner lists, fall back to the granular methods — :meth:set_transfinite_curve, :meth:set_transfinite_surface, :meth:set_transfinite_volume.

Source code in src/apeGmsh/mesh/_mesh_structured.py
def set_transfinite(
    self,
    target=None,
    *,
    n=None,
    size=None,
    recombine: bool = True,
    dim: int | None = None,
    angle_tol_deg: float = 5.0,
) -> "_Structured":
    """Apply transfinite (+ optional recombine) to one entity, many, or the whole model.

    High-level dispatcher that infers the dim of the target and
    applies the full cascade for that dim — curves of bounding
    edges, surfaces, recombine, and (for volumes) the volume itself.

    Parameters
    ----------
    target :
        What to constrain.  Accepts:

        - ``None``  → every volume in the model
        - ``"name"`` (label or PG name) → all entities under that
          name, any dim (each gets the cascade for its dim)
        - a Selection — uses its dimtags
        - a ``(dim, tag)`` dimtag, or list of them
        - a bare int — must be paired with ``dim=``

    n : int | tuple | dict | None
        Node-count sizing.  Pass exactly one of ``n`` or ``size``.

        - ``int`` — uniform on every edge of each entity, any
          orientation
        - ``dict`` — per-axis, keyed by ``"x"`` / ``"y"`` / ``"z"``.
          Requires axis-aligned entities (raises otherwise — the
          error reports the cluster directions so you can switch to
          tuple form).
        - ``tuple`` — per principal axis, in the order
          ``(X-aligned, Y-aligned, Z-aligned)``.  Works on any
          rotation; closest-global-axis assignment with lex
          tie-break.  Length must match the entity's principal-axis
          count (3 for volumes, 2 for surfaces, 1 for curves).
    size : float | tuple | dict | None
        Length-based sizing — node count per edge is
        ``round(edge_length / size) + 1``.  Same grammar as ``n``.
    recombine : bool, default True
        Recombine to quads on each face and (for volumes) hex on
        the volume.  Set ``False`` for a structured tet mesh.
    dim : int | None, default None
        Only used when ``target`` is a bare int.  Ignored otherwise.
    angle_tol_deg : float, default 5.0
        Tolerance for grouping edges into principal-axis clusters.
        Raise if your geometry has nearly-parallel non-orthogonal
        edges that you want kept separate.

    Returns
    -------
    _Structured
        ``self`` for chaining.

    Behavior
    --------
    - Volumes are processed first so their bounding surfaces and
      edges aren't reset by a later surface-level call.
    - Entities whose edges don't cluster into the expected
      principal-axis count (e.g. a face split by a boolean op into
      a 5-sided patch) are **warned and skipped** rather than
      failing the whole call.

    Examples
    --------
    Whole-model, uniform sizing::

        g.mesh.structured.set_transfinite(n=11)

    Axis-aligned box, per-axis sizing::

        g.mesh.structured.set_transfinite(
            "layer_top", n={"x": 101, "y": 101, "z": 6},
        )

    Rotated box, per principal axis (tuple)::

        g.mesh.structured.set_transfinite("rotated_box", n=(11, 11, 21))

    Length-based sizing on a named surface::

        g.mesh.structured.set_transfinite("base", size={"x": 100, "y": 100})

    For per-edge bias (Progression/Bump/Beta) or explicit corner
    lists, fall back to the granular methods —
    :meth:`set_transfinite_curve`, :meth:`set_transfinite_surface`,
    :meth:`set_transfinite_volume`.
    """
    from apeGmsh.core._helpers import resolve_to_dimtags
    from apeGmsh.core._selection import (
        _cluster_edge_directions,
        _order_clusters_by_global_axis,
        _AXIS_VECTORS,
    )
    import warnings

    if (n is None) == (size is None):
        raise ValueError(
            "set_transfinite: pass exactly one of n= or size=."
        )
    spec_kind = "n" if n is not None else "size"
    spec = n if n is not None else size
    self._last_skipped = []

    # Resolve target → list of dimtags
    if target is None:
        dts = [(3, t) for _, t in gmsh.model.getEntities(3)]
    else:
        dts = resolve_to_dimtags(
            target, default_dim=dim or 3, session=self._mesh._parent,
        )

    # Group by dim — process volumes first
    by_dim = {1: [], 2: [], 3: []}
    for dt in dts:
        if dt[0] in by_dim:
            by_dim[dt[0]].append(dt)

    for vol_dt in by_dim[3]:
        self._cascade_volume(
            vol_dt, spec, spec_kind, recombine, angle_tol_deg,
        )
    # Standalone surfaces (not part of any volume we just processed)
    already_handled_faces = set()
    for vol_dt in by_dim[3]:
        for fdt in self._mesh._parent.model.queries.boundary(
            vol_dt, oriented=False,
        ):
            already_handled_faces.add(tuple(fdt))
    for face_dt in by_dim[2]:
        if tuple(face_dt) in already_handled_faces:
            continue
        self._cascade_surface(
            face_dt, spec, spec_kind, recombine, angle_tol_deg,
        )
    for curve_dt in by_dim[1]:
        # Curves take only a scalar; reject dict/tuple at the curve level
        if isinstance(spec, (dict, tuple, list)):
            # Pull the matching value
            count = self._extract_curve_count(curve_dt, spec, spec_kind,
                                               angle_tol_deg)
        else:
            count = self._spec_to_curve_count(curve_dt, spec, spec_kind)
        self.set_transfinite_curve(curve_dt[1], n_nodes=count)

    return self

set_transfinite_by_physical

set_transfinite_by_physical(name: str, *, dim: int, **kwargs) -> '_Structured'

Deprecated. set_transfinite_curve/surface/volume already accept a label or physical-group name directly — pass it as tag.

Example

::

# old
g.mesh.structured.set_transfinite_by_physical("flange", dim=2,
                                              arrangement="Left")
# new
g.mesh.structured.set_transfinite_surface("flange",
                                          arrangement="Left")
Source code in src/apeGmsh/mesh/_mesh_structured.py
def set_transfinite_by_physical(
    self,
    name : str,
    *,
    dim  : int,
    **kwargs,
) -> "_Structured":
    """
    Deprecated.  ``set_transfinite_curve/surface/volume`` already
    accept a label or physical-group name directly — pass it as
    ``tag``.

    Example
    -------
    ::

        # old
        g.mesh.structured.set_transfinite_by_physical("flange", dim=2,
                                                      arrangement="Left")
        # new
        g.mesh.structured.set_transfinite_surface("flange",
                                                  arrangement="Left")
    """
    import warnings
    warnings.warn(
        "set_transfinite_by_physical is deprecated; "
        "set_transfinite_curve/surface/volume already accept a "
        "physical-group name as tag.",
        DeprecationWarning,
        stacklevel=2,
    )
    if dim == 1:
        return self.set_transfinite_curve(name, **kwargs)
    if dim == 2:
        return self.set_transfinite_surface(name, **kwargs)
    if dim == 3:
        return self.set_transfinite_volume(name, **kwargs)
    raise ValueError(
        f"set_transfinite_by_physical: dim must be 1, 2, or 3, got {dim!r}"
    )

set_recombine

set_recombine(tag, *, dim: int = 2, angle: float = 45.0) -> '_Structured'

Request quad recombination. tag accepts int, label, or PG name.

Source code in src/apeGmsh/mesh/_mesh_structured.py
def set_recombine(
    self,
    tag,
    *,
    dim  : int   = 2,
    angle: float = 45.0,
) -> "_Structured":
    """Request quad recombination. ``tag`` accepts int, label, or PG name."""
    for t in self._resolve(tag, dim=dim):
        gmsh.model.mesh.setRecombine(dim, t, angle)
        self._mesh._directives.append({
            'kind': 'recombine', 'dim': dim, 'tag': t, 'angle': angle,
        })
        self._mesh._log(f"set_recombine(dim={dim}, tag={t}, angle={angle}°)")
    return self

recombine

recombine() -> '_Structured'

Globally recombine all triangular elements into quads.

This is a 2-D surface operation. Running it while 3-D tetrahedra exist corrupts the volume mesh: Gmsh deletes interior nodes but leaves the tets that referenced them pointing at node 0, which later aborts OpenSees with no Node 0 exists. We warn up front; the corruption itself is caught fail-loud at FEM extraction (_validate_connectivity). For a hexahedral volume mesh use a structured/transfinite setup instead.

Source code in src/apeGmsh/mesh/_mesh_structured.py
def recombine(self) -> "_Structured":
    """Globally recombine all triangular elements into quads.

    This is a **2-D surface** operation.  Running it while 3-D
    tetrahedra exist corrupts the volume mesh: Gmsh deletes interior
    nodes but leaves the tets that referenced them pointing at node
    ``0``, which later aborts OpenSees with ``no Node 0 exists``.
    We warn up front; the corruption itself is caught fail-loud at
    FEM extraction (``_validate_connectivity``).  For a hexahedral
    volume mesh use a structured/transfinite setup instead.
    """
    self._guard("recombine")
    etypes, etags, _ = gmsh.model.mesh.getElements(dim=3, tag=-1)
    if any(len(t) for t in etags):
        import warnings
        warnings.warn(
            "g.mesh.structured.recombine() is a 2-D surface "
            "operation, but the mesh contains 3-D elements. Running "
            "it corrupts the volume mesh (deletes interior nodes, "
            "leaving tets that point at node 0 -- OpenSees then "
            "aborts with 'no Node 0 exists'). For a hexahedral "
            "volume mesh use g.mesh.structured.set_transfinite(...) "
            "instead of the global recombine.",
            UserWarning,
            stacklevel=2,
        )
    gmsh.model.mesh.recombine()
    self._mesh._log("recombine()")
    return self

set_recombine_by_physical

set_recombine_by_physical(name: str, *, dim: int = 2, angle: float = 45.0) -> '_Structured'

Deprecated. set_recombine accepts a PG name directly.

Source code in src/apeGmsh/mesh/_mesh_structured.py
def set_recombine_by_physical(
    self,
    name : str,
    *,
    dim  : int = 2,
    angle: float = 45.0,
) -> "_Structured":
    """Deprecated.  ``set_recombine`` accepts a PG name directly."""
    import warnings
    warnings.warn(
        "set_recombine_by_physical is deprecated; pass the "
        "physical-group name to set_recombine() as tag.",
        DeprecationWarning,
        stacklevel=2,
    )
    return self.set_recombine(name, dim=dim, angle=angle)

set_smoothing

set_smoothing(tag, val: int, *, dim: int = 2) -> '_Structured'

Set smoothing passes. tag accepts int, label, or PG name.

Source code in src/apeGmsh/mesh/_mesh_structured.py
def set_smoothing(self, tag, val: int, *, dim: int = 2) -> "_Structured":
    """Set smoothing passes. ``tag`` accepts int, label, or PG name."""
    for t in self._resolve(tag, dim=dim):
        gmsh.model.mesh.setSmoothing(dim, t, val)
        self._mesh._directives.append({
            'kind': 'smoothing', 'dim': dim, 'tag': t, 'val': val,
        })
        self._mesh._log(f"set_smoothing(dim={dim}, tag={t}, val={val})")
    return self

set_smoothing_by_physical

set_smoothing_by_physical(name: str, val: int, *, dim: int = 2) -> '_Structured'

Deprecated. set_smoothing accepts a PG name directly.

Source code in src/apeGmsh/mesh/_mesh_structured.py
def set_smoothing_by_physical(
    self,
    name: str,
    val : int,
    *,
    dim : int = 2,
) -> "_Structured":
    """Deprecated.  ``set_smoothing`` accepts a PG name directly."""
    import warnings
    warnings.warn(
        "set_smoothing_by_physical is deprecated; pass the "
        "physical-group name to set_smoothing() as tag.",
        DeprecationWarning,
        stacklevel=2,
    )
    return self.set_smoothing(name, val, dim=dim)

set_compound

set_compound(dim: int, tags) -> '_Structured'

Merge entities so they are meshed together as a single compound.

tags accepts int, label/PG name, (dim, tag) tuple, or a list of any mix.

Source code in src/apeGmsh/mesh/_mesh_structured.py
def set_compound(self, dim: int, tags) -> "_Structured":
    """Merge entities so they are meshed together as a single compound.

    ``tags`` accepts int, label/PG name, ``(dim, tag)`` tuple, or a
    list of any mix.
    """
    resolved = self._resolve(tags, dim=dim)
    gmsh.model.mesh.setCompound(dim, resolved)
    self._mesh._log(f"set_compound(dim={dim}, tags={resolved})")
    return self

remove_constraints

remove_constraints(dim_tags=None) -> '_Structured'

Remove all meshing constraints from the given (or all) entities.

dim_tags accepts any flexible-ref form (int, label/PG name, (dim, tag), or list thereof). None clears every entity in the model.

Source code in src/apeGmsh/mesh/_mesh_structured.py
def remove_constraints(self, dim_tags=None) -> "_Structured":
    """Remove all meshing constraints from the given (or all) entities.

    ``dim_tags`` accepts any flexible-ref form (int, label/PG name,
    ``(dim, tag)``, or list thereof).  ``None`` clears every
    entity in the model.
    """
    self._guard("remove_constraints")
    if dim_tags is None:
        dts: list[DimTag] = []
    else:
        from apeGmsh.core._helpers import resolve_to_dimtags
        dts = resolve_to_dimtags(
            dim_tags, default_dim=3, session=self._mesh._parent,
        )
    gmsh.model.mesh.removeConstraints(dimTags=dts)
    self._mesh._log(f"remove_constraints(dim_tags={dim_tags})")
    return self

One-call recipe — set_transfinite()

For the common case of "apply transfinite + recombine to one entity, many, or the whole model," use the unified set_transfinite() method. It infers the dim from the target and runs the appropriate cascade (curves → faces → recombine → volume):

# Whole model, uniform — typical "just give me hexes"
g.mesh.structured.set_transfinite(n=11)

# Axis-aligned hex, per-axis sizing (most readable)
g.mesh.structured.set_transfinite("layer_top",
                                   n={"x": 101, "y": 101, "z": 6})

# Rotated hex, per principal axis (works for any orientation)
g.mesh.structured.set_transfinite("rotated_box", n=(11, 11, 21))

# Length-based sizing (per axis)
g.mesh.structured.set_transfinite("layer_top",
                                   size={"x": 100, "y": 100, "z": 10})

Sizing forms — all three coexist; pick by readability:

Form Use when Example
scalar n=11 uniform on every edge; any orientation n=11
dict n={"x":..., "y":..., "z":...} axis-aligned hex, want per-axis counts n={"x":11, "y":11, "z":21}
tuple n=(n1, n2, n3) any rotation; counts in (X-closest, Y-closest, Z-closest) order n=(11, 11, 21)

Behavior on incompatible geometry — entities whose edges don't cluster into the expected principal-axis count (e.g. a face split by a boolean op into a 5-sided patch) are warned and skipped rather than failing the whole call. Use set_transfinite_automatic() if you want silent skipping with no warning.

For per-edge bias (Progression / Bump / Beta with coef=), explicit corner lists, or custom triangle arrangement, drop to the granular methods: set_transfinite_curve(), set_transfinite_surface(), set_transfinite_volume().

g.mesh.editing

apeGmsh.mesh._mesh_editing._Editing

_Editing(parent_mesh: 'Mesh')

Mesh mutation, embedding, periodicity, STL import.

Source code in src/apeGmsh/mesh/_mesh_editing.py
def __init__(self, parent_mesh: "Mesh") -> None:
    self._mesh = parent_mesh

embed

embed(tags, in_tag, *, dim: int = 0, in_dim: int = 3) -> '_Editing'

Embed lower-dimensional entities inside a higher-dimensional entity so the mesh is conforming along them.

Parameters accept int tags, label/PG strings, or lists thereof.

Example

::

g.mesh.editing.embed("crack_surf", "body", dim=2, in_dim=3)
g.mesh.editing.embed([p1, p2, p3], surf_tag, dim=0, in_dim=2)
Source code in src/apeGmsh/mesh/_mesh_editing.py
def embed(
    self,
    tags,
    in_tag,
    *,
    dim   : int = 0,
    in_dim: int = 3,
) -> "_Editing":
    """
    Embed lower-dimensional entities inside a higher-dimensional
    entity so the mesh is conforming along them.

    Parameters accept int tags, label/PG strings, or lists thereof.

    Example
    -------
    ::

        g.mesh.editing.embed("crack_surf", "body", dim=2, in_dim=3)
        g.mesh.editing.embed([p1, p2, p3], surf_tag, dim=0, in_dim=2)
    """
    self._guard("embed")
    from apeGmsh.core._helpers import resolve_to_tags
    tag_list = resolve_to_tags(tags, dim=dim, session=self._mesh._parent)
    in_tags = resolve_to_tags(in_tag, dim=in_dim, session=self._mesh._parent)
    in_tag_resolved = in_tags[0]
    gmsh.model.mesh.embed(dim, tag_list, in_dim, in_tag_resolved)
    self._mesh._log(
        f"embed(dim={dim}, tags={tag_list}, "
        f"in_dim={in_dim}, in_tag={in_tag})"
    )
    return self

set_periodic

set_periodic(tags, master_tags, transform: list[float], *, dim: int = 2) -> '_Editing'

Declare periodic mesh correspondence between entities.

Parameters

tags : slave entity reference(s) — int, label, PG name, (dim, tag) tuple, or list of any mix. master_tags : master entity reference(s) — same flexible form. transform : 16-element row-major 4×4 affine matrix mapping master -> slave coordinates dim : entity dimension (1 = curves, 2 = surfaces)

Source code in src/apeGmsh/mesh/_mesh_editing.py
def set_periodic(
    self,
    tags,
    master_tags,
    transform  : list[float],
    *,
    dim        : int = 2,
) -> "_Editing":
    """
    Declare periodic mesh correspondence between entities.

    Parameters
    ----------
    tags        : slave entity reference(s) — int, label, PG name,
                  ``(dim, tag)`` tuple, or list of any mix.
    master_tags : master entity reference(s) — same flexible form.
    transform   : 16-element row-major 4×4 affine matrix mapping
                  master -> slave coordinates
    dim         : entity dimension (1 = curves, 2 = surfaces)
    """
    self._guard("set_periodic")
    from apeGmsh.core._helpers import resolve_to_tags
    slave_resolved = resolve_to_tags(
        tags, dim=dim, session=self._mesh._parent,
    )
    master_resolved = resolve_to_tags(
        master_tags, dim=dim, session=self._mesh._parent,
    )
    if len(slave_resolved) != len(master_resolved):
        raise ValueError(
            f"set_periodic: slave/master count mismatch — "
            f"slaves={slave_resolved} ({len(slave_resolved)}), "
            f"masters={master_resolved} ({len(master_resolved)}). "
            f"Each slave needs exactly one master under the same "
            f"transform."
        )
    gmsh.model.mesh.setPeriodic(
        dim, slave_resolved, master_resolved, transform,
    )
    self._mesh._log(
        f"set_periodic(dim={dim}, tags={slave_resolved}, "
        f"master={master_resolved})"
    )
    return self

import_stl

import_stl() -> '_Editing'

Classify an STL mesh previously loaded into the gmsh model via gmsh.merge as a discrete surface mesh.

Source code in src/apeGmsh/mesh/_mesh_editing.py
def import_stl(self) -> "_Editing":
    """
    Classify an STL mesh previously loaded into the gmsh model via
    ``gmsh.merge`` as a discrete surface mesh.
    """
    self._guard("import_stl")
    gmsh.model.mesh.importStl()
    self._mesh._log("import_stl()")
    return self

classify_surfaces

classify_surfaces(angle: float, *, boundary: bool = True, for_reparametrization: bool = False, curve_angle: float = math.pi, export_discrete: bool = True) -> '_Editing'

Partition a discrete STL mesh into surface patches based on dihedral angle.

Source code in src/apeGmsh/mesh/_mesh_editing.py
def classify_surfaces(
    self,
    angle              : float,
    *,
    boundary           : bool  = True,
    for_reparametrization: bool = False,
    curve_angle        : float = math.pi,
    export_discrete    : bool  = True,
) -> "_Editing":
    """
    Partition a discrete STL mesh into surface patches based on
    dihedral angle.
    """
    self._guard("classify_surfaces")
    gmsh.model.mesh.classifySurfaces(
        angle,
        boundary=boundary,
        forReparametrization=for_reparametrization,
        curveAngle=curve_angle,
        exportDiscrete=export_discrete,
    )
    self._mesh._log(
        f"classify_surfaces(angle={math.degrees(angle):.1f}°, "
        f"boundary={boundary})"
    )
    return self

create_geometry

create_geometry(dim_tags: list[DimTag] | None = None) -> '_Editing'

Create a proper CAD-like geometry from classified discrete surfaces. Must be called after classify_surfaces.

Source code in src/apeGmsh/mesh/_mesh_editing.py
def create_geometry(
    self,
    dim_tags: list[DimTag] | None = None,
) -> "_Editing":
    """
    Create a proper CAD-like geometry from classified discrete surfaces.
    Must be called after ``classify_surfaces``.
    """
    self._guard("create_geometry")
    gmsh.model.mesh.createGeometry(dimTags=dim_tags or [])
    self._mesh._log("create_geometry()")
    return self

clear

clear(dim_tags=None) -> '_Editing'

Clear mesh data (nodes + elements).

dim_tags accepts any flexible-ref form — int, label/PG name, (dim, tag), or a list mixing those — resolved via :func:resolve_to_dimtags (default_dim=3). None (the default) clears every entity in the model.

Example

::

g.mesh.editing.clear()                  # clear everything
g.mesh.editing.clear("col.body")        # clear a labelled volume
g.mesh.editing.clear([(2, 5), "fillet"]) # mixed refs
Source code in src/apeGmsh/mesh/_mesh_editing.py
def clear(self, dim_tags=None) -> "_Editing":
    """Clear mesh data (nodes + elements).

    ``dim_tags`` accepts any flexible-ref form — int, label/PG name,
    ``(dim, tag)``, or a list mixing those — resolved via
    :func:`resolve_to_dimtags` (default_dim=3).  ``None`` (the
    default) clears every entity in the model.

    Example
    -------
    ::

        g.mesh.editing.clear()                  # clear everything
        g.mesh.editing.clear("col.body")        # clear a labelled volume
        g.mesh.editing.clear([(2, 5), "fillet"]) # mixed refs
    """
    self._guard("clear")
    if dim_tags is None:
        dts: list[DimTag] = []
    else:
        from apeGmsh.core._helpers import resolve_to_dimtags
        dts = resolve_to_dimtags(
            dim_tags, default_dim=3, session=self._mesh._parent,
        )
    gmsh.model.mesh.clear(dimTags=dts)
    self._mesh._log(f"clear(dim_tags={dim_tags})")
    return self

reverse

reverse(dim_tags=None) -> '_Editing'

Reverse the orientation of mesh elements in the given entities.

dim_tags accepts any flexible-ref form (int, label/PG name, (dim, tag), or list thereof). None reverses every entity in the model.

Example

::

g.mesh.editing.reverse("inverted_face")
g.mesh.editing.reverse([(2, 5), (2, 6)])
Source code in src/apeGmsh/mesh/_mesh_editing.py
def reverse(self, dim_tags=None) -> "_Editing":
    """Reverse the orientation of mesh elements in the given entities.

    ``dim_tags`` accepts any flexible-ref form (int, label/PG name,
    ``(dim, tag)``, or list thereof).  ``None`` reverses every
    entity in the model.

    Example
    -------
    ::

        g.mesh.editing.reverse("inverted_face")
        g.mesh.editing.reverse([(2, 5), (2, 6)])
    """
    self._guard("reverse")
    if dim_tags is None:
        dts: list[DimTag] = []
    else:
        from apeGmsh.core._helpers import resolve_to_dimtags
        dts = resolve_to_dimtags(
            dim_tags, default_dim=3, session=self._mesh._parent,
        )
    gmsh.model.mesh.reverse(dimTags=dts)
    self._mesh._log(f"reverse(dim_tags={dim_tags})")
    return self

relocate_nodes

relocate_nodes(*, dim: int = -1, tag=-1) -> '_Editing'

Project mesh nodes back onto their underlying geometry.

tag accepts an int, label/PG name, (dim, tag), or a list mixing those. Because gmsh's relocateNodes operates on a single entity at a time, when a reference resolves to multiple entities the wrapper iterates and calls gmsh once per resolved (dim, tag).

tag=-1 (the default) relocates nodes for every entity in the model; dim is forwarded to gmsh in that case.

Example

::

g.mesh.editing.relocate_nodes()                 # all entities
g.mesh.editing.relocate_nodes(tag="col.faces")  # whole label
g.mesh.editing.relocate_nodes(tag=(2, 5))
Source code in src/apeGmsh/mesh/_mesh_editing.py
def relocate_nodes(self, *, dim: int = -1, tag=-1) -> "_Editing":
    """Project mesh nodes back onto their underlying geometry.

    ``tag`` accepts an int, label/PG name, ``(dim, tag)``, or a
    list mixing those.  Because gmsh's ``relocateNodes`` operates
    on a single entity at a time, when a reference resolves to
    multiple entities the wrapper iterates and calls gmsh once
    per resolved ``(dim, tag)``.

    ``tag=-1`` (the default) relocates nodes for every entity in
    the model; ``dim`` is forwarded to gmsh in that case.

    Example
    -------
    ::

        g.mesh.editing.relocate_nodes()                 # all entities
        g.mesh.editing.relocate_nodes(tag="col.faces")  # whole label
        g.mesh.editing.relocate_nodes(tag=(2, 5))
    """
    self._guard("relocate_nodes")
    if tag == -1:
        gmsh.model.mesh.relocateNodes(dim=dim, tag=-1)
        self._mesh._log(f"relocate_nodes(dim={dim}, tag=-1)")
        return self

    from apeGmsh.core._helpers import resolve_to_dimtags
    default_dim = dim if dim != -1 else 3
    dts = resolve_to_dimtags(
        tag, default_dim=default_dim, session=self._mesh._parent,
    )
    for d, t in dts:
        gmsh.model.mesh.relocateNodes(dim=d, tag=t)
    self._mesh._log(f"relocate_nodes(resolved={dts})")
    return self

remove_duplicate_nodes

remove_duplicate_nodes() -> '_Editing'

Merge nodes that share the same position within tolerance.

Node removal is always announced on stdout — there is no silent mode. Deleting nodes from a meshed model is a destructive operation; the visibility floor is intentional so that an unexpected dedup never hides in a long pipeline log.

Source code in src/apeGmsh/mesh/_mesh_editing.py
def remove_duplicate_nodes(self) -> "_Editing":
    """
    Merge nodes that share the same position within tolerance.

    Node removal is **always** announced on stdout — there is no
    silent mode. Deleting nodes from a meshed model is a
    destructive operation; the visibility floor is intentional so
    that an unexpected dedup never hides in a long pipeline log.
    """
    self._guard("remove_duplicate_nodes")
    before = len(gmsh.model.mesh.getNodes()[0])
    gmsh.model.mesh.removeDuplicateNodes()
    after  = len(gmsh.model.mesh.getNodes()[0])
    removed = before - after
    if removed > 0:
        print(f"remove_duplicate_nodes: merged {removed} "
              f"node(s) ({before} -> {after})")
    else:
        print(f"remove_duplicate_nodes: no duplicates found "
              f"({before} nodes unchanged)")
    self._mesh._log(f"remove_duplicate_nodes() removed={removed}")
    return self

remove_duplicate_elements

remove_duplicate_elements(verbose: bool = True) -> '_Editing'

Remove elements with identical node connectivity.

Source code in src/apeGmsh/mesh/_mesh_editing.py
def remove_duplicate_elements(self, verbose: bool = True) -> "_Editing":
    """Remove elements with identical node connectivity."""
    self._guard("remove_duplicate_elements")
    def _count() -> int:
        _, tags, _ = gmsh.model.mesh.getElements()
        return sum(len(t) for t in tags)

    before = _count()
    gmsh.model.mesh.removeDuplicateElements()
    after  = _count()
    removed = before - after
    if verbose:
        if removed > 0:
            print(f"remove_duplicate_elements: removed {removed} "
                  f"element(s) ({before} -> {after})")
        else:
            print(f"remove_duplicate_elements: no duplicates found "
                  f"({before} elements unchanged)")
    self._mesh._log(f"remove_duplicate_elements() removed={removed}")
    return self

crack

crack(physical_group: str, *, dim: int = 1, open_boundary: str | None = None, normal: tuple[float, float, float] | None = None, side_labels: tuple[str, str] | bool = True) -> '_Editing'

Duplicate mesh nodes along a physical group to create a crack.

Wraps Gmsh's built-in Crack plugin (gmsh.plugin.run("Crack")). After meshing, the plugin walks the elements on one side of physical_group and reconnects them to a freshly duplicated set of nodes — the crack is therefore a discontinuity in the mesh, not in the geometry.

By default the plugin keeps the boundary vertices of the crack curve shared (e.g. the crack tip in fracture mechanics). Naming a sub-region of those boundary vertices in open_boundary overrides the default and duplicates them too — that is how you model a crack mouth that opens onto a free surface.

Must be called after g.mesh.generation.generate(...) — the plugin operates on the mesh, not the geometry.

Parameters

physical_group : str Name of the physical group containing the crack curves (dim=1) or surfaces (dim=2). Create it ahead of time via g.physical.add_curve(..., name=...) or g.physical.add_surface(..., name=...). dim : int Dimension of the crack itself. 1 for a 1-D crack in a 2-D mesh; 2 for a 2-D crack in a 3-D mesh. open_boundary : str, optional Name of a physical group, one dimension lower than dim, naming the crack-curve boundary vertices that should also be duplicated. Use this for the crack mouth (where the crack reaches a free surface). Leave None for an interior crack so every boundary vertex (including the tip) stays shared. normal : (nx, ny, nz), optional Hint vector forwarded to the plugin to disambiguate the two sides of the crack when topology alone is not enough. Almost never needed for clean transfinite or unstructured meshes. side_labels : tuple[str, str] or bool, default True Post-plugin, the crack is owned by two distinct face entities (the original entity plus a new one created by the plugin to host the duplicated side). This argument controls whether they get named physical groups attached:

  * ``True`` (default) — auto-derive
    ``f"{physical_group}_normal"`` and
    ``f"{physical_group}_inverted"`` and add them as
    physical groups, one per face entity.
  * ``(normal_name, inverted_name)`` tuple — use these
    explicit names instead.
  * ``False`` — skip side labeling (legacy behaviour).

**Convention.**  ``<pg>_normal`` is the face entity whose
adjacent volume elements sit on the side the *original*
surface normal points toward; ``<pg>_inverted`` is the
face on the opposite side.  This is computed at runtime
from the signed distance between an adjacent tet's
centroid and the crack plane along that normal — it does
not assume the plugin's "original vs new" mapping is
stable.  Only supported for ``dim=2`` cracks in 3D meshes.
Returns

_Editing (self, for chaining)

Example

Edge crack reaching the bottom edge — duplicate the mouth, keep the tip shared::

g.physical.add_curve([crack_curve], name="Crack")
g.physical.add_point([base_point],   name="CrackBase")
g.mesh.generation.generate(dim=2)
g.mesh.editing.crack(
    "Crack", dim=1, open_boundary="CrackBase",
)
Source code in src/apeGmsh/mesh/_mesh_editing.py
def crack(
    self,
    physical_group: str,
    *,
    dim          : int                              = 1,
    open_boundary: str | None                       = None,
    normal       : tuple[float, float, float] | None = None,
    side_labels  : tuple[str, str] | bool           = True,
) -> "_Editing":
    """
    Duplicate mesh nodes along a physical group to create a crack.

    Wraps Gmsh's built-in ``Crack`` plugin
    (``gmsh.plugin.run("Crack")``).  After meshing, the plugin
    walks the elements on one side of ``physical_group`` and
    reconnects them to a freshly duplicated set of nodes — the
    crack is therefore a discontinuity in the mesh, not in the
    geometry.

    By default the plugin keeps the **boundary vertices of the
    crack curve shared** (e.g. the crack tip in fracture
    mechanics).  Naming a sub-region of those boundary vertices
    in ``open_boundary`` overrides the default and **duplicates
    them too** — that is how you model a crack mouth that opens
    onto a free surface.

    Must be called **after** ``g.mesh.generation.generate(...)``
    — the plugin operates on the mesh, not the geometry.

    Parameters
    ----------
    physical_group : str
        Name of the physical group containing the crack
        curves (``dim=1``) or surfaces (``dim=2``).  Create it
        ahead of time via ``g.physical.add_curve(..., name=...)``
        or ``g.physical.add_surface(..., name=...)``.
    dim : int
        Dimension of the crack itself.  ``1`` for a 1-D crack
        in a 2-D mesh; ``2`` for a 2-D crack in a 3-D mesh.
    open_boundary : str, optional
        Name of a physical group, **one dimension lower than**
        ``dim``, naming the crack-curve boundary vertices that
        should *also* be duplicated.  Use this for the crack
        mouth (where the crack reaches a free surface).  Leave
        ``None`` for an interior crack so every boundary vertex
        (including the tip) stays shared.
    normal : (nx, ny, nz), optional
        Hint vector forwarded to the plugin to disambiguate the
        two sides of the crack when topology alone is not enough.
        Almost never needed for clean transfinite or unstructured
        meshes.
    side_labels : tuple[str, str] or bool, default True
        Post-plugin, the crack is owned by **two** distinct face
        entities (the original entity plus a new one created by the
        plugin to host the duplicated side).  This argument
        controls whether they get named physical groups attached:

          * ``True`` (default) — auto-derive
            ``f"{physical_group}_normal"`` and
            ``f"{physical_group}_inverted"`` and add them as
            physical groups, one per face entity.
          * ``(normal_name, inverted_name)`` tuple — use these
            explicit names instead.
          * ``False`` — skip side labeling (legacy behaviour).

        **Convention.**  ``<pg>_normal`` is the face entity whose
        adjacent volume elements sit on the side the *original*
        surface normal points toward; ``<pg>_inverted`` is the
        face on the opposite side.  This is computed at runtime
        from the signed distance between an adjacent tet's
        centroid and the crack plane along that normal — it does
        not assume the plugin's "original vs new" mapping is
        stable.  Only supported for ``dim=2`` cracks in 3D meshes.

    Returns
    -------
    _Editing  (self, for chaining)

    Example
    -------
    Edge crack reaching the bottom edge — duplicate the mouth,
    keep the tip shared::

        g.physical.add_curve([crack_curve], name="Crack")
        g.physical.add_point([base_point],   name="CrackBase")
        g.mesh.generation.generate(dim=2)
        g.mesh.editing.crack(
            "Crack", dim=1, open_boundary="CrackBase",
        )
    """
    self._guard("crack")
    physical = getattr(self._mesh._parent, 'physical', None)
    if physical is None:
        raise RuntimeError(
            "crack: session has no 'physical' composite — "
            "physical groups are required to invoke the Crack plugin."
        )

    crack_pg_tag = physical.get_tag(dim, physical_group)
    if crack_pg_tag is None:
        raise KeyError(
            f"crack: no physical group named {physical_group!r} at "
            f"dim={dim}.  Create it first via "
            f"g.physical.add_curve(..., name={physical_group!r}) "
            f"(or add_surface for dim=2)."
        )

    open_pg_tag = 0  # plugin default — no open boundary
    if open_boundary is not None:
        open_dim = dim - 1
        open_pg_tag = physical.get_tag(open_dim, open_boundary)
        if open_pg_tag is None:
            raise KeyError(
                f"crack: no physical group named {open_boundary!r} "
                f"at dim={open_dim}.  The open boundary lives one "
                f"dimension lower than the crack itself."
            )

    # Snapshot pre-plugin state for side-labeling.  We capture
    # the source surface's analytic OCC normal *before* the plugin
    # runs because the plugin produces a discrete (mesh-only)
    # surface for the duplicated side that has no parameterisation
    # — only the original entity is OCC-backed and queryable via
    # gmsh.model.getNormal.
    do_side_labels = (
        side_labels is not False and dim == 2
    )
    pre_ents: set[int] = set()
    src_ents: list[int] = []
    ref_origin: np.ndarray | None = None
    ref_normal: np.ndarray | None = None
    if do_side_labels:
        pre_ents = {
            int(t) for d_, t in gmsh.model.getEntities(dim) if d_ == dim
        }
        src_ents = sorted(
            int(e) for e in
            gmsh.model.getEntitiesForPhysicalGroup(dim, crack_pg_tag)
        )
        if len(src_ents) < 1:
            raise RuntimeError(
                f"crack: source PG {physical_group!r} resolves to "
                f"no entities; side_labels= cannot be applied."
            )
        src_tag = src_ents[0]
        nrm = gmsh.model.getNormal(src_tag, [0.5, 0.5])
        n_arr = np.asarray(nrm, dtype=float)
        if float(np.linalg.norm(n_arr)) < 1e-12:
            raise RuntimeError(
                f"crack: source surface {src_tag} returned a "
                f"degenerate analytic normal — pass "
                f"side_labels=False or a normal= hint."
            )
        ref_origin = np.asarray(
            gmsh.model.occ.getCenterOfMass(2, src_tag), dtype=float,
        )
        ref_normal = n_arr / float(np.linalg.norm(n_arr))

    gmsh.plugin.setNumber("Crack", "Dimension", float(dim))
    gmsh.plugin.setNumber("Crack", "PhysicalGroup", float(crack_pg_tag))
    gmsh.plugin.setNumber(
        "Crack", "OpenBoundaryPhysicalGroup", float(open_pg_tag),
    )
    if normal is not None:
        nx, ny, nz = normal
        gmsh.plugin.setNumber("Crack", "NormalX", float(nx))
        gmsh.plugin.setNumber("Crack", "NormalY", float(ny))
        gmsh.plugin.setNumber("Crack", "NormalZ", float(nz))

    gmsh.plugin.run("Crack")

    if do_side_labels:
        post_ents = {
            int(t) for d_, t in gmsh.model.getEntities(dim) if d_ == dim
        }
        new_ents = sorted(post_ents - pre_ents)
        if len(new_ents) != 1:
            raise RuntimeError(
                f"crack: expected exactly 1 new face entity, got "
                f"{len(new_ents)} (src_ents={src_ents}).  "
                f"side_labels= cannot be applied unambiguously; pass "
                f"side_labels=False to skip auto-labeling."
            )

        normal_name, inverted_name = (
            (f"{physical_group}_normal",
             f"{physical_group}_inverted")
            if side_labels is True
            else side_labels
        )

        new_tag  = new_ents[0]
        orig_tag = src_ents[0]
        # The plugin reconnects exactly one side; the original and
        # new entities are guaranteed to lie on opposite sides, so
        # we only need to probe one of them.  ref_origin/ref_normal
        # are the source surface's analytic plane.
        assert ref_origin is not None and ref_normal is not None
        new_side = self._classify_face_side(
            new_tag, ref_origin, ref_normal,
        )
        normal_tag   = new_tag  if new_side > 0 else orig_tag
        inverted_tag = orig_tag if new_side > 0 else new_tag

        physical.add_surface([normal_tag],   name=normal_name)
        physical.add_surface([inverted_tag], name=inverted_name)

        self._mesh._log(
            f"crack: side labels {normal_name!r}->entity {normal_tag}, "
            f"{inverted_name!r}->entity {inverted_tag}"
        )

    self._mesh._log(
        f"crack(physical_group={physical_group!r}, dim={dim}, "
        f"open_boundary={open_boundary!r}) "
        f"-> crack_pg_tag={crack_pg_tag}, "
        f"open_pg_tag={open_pg_tag}"
    )
    return self

split_higher_order_lines

split_higher_order_lines(physical_group: str | Iterable[str], *, policy: Literal['forbid', 'split', 'constrain'], dim: int = 1) -> '_Editing'

Demote higher-order line elements (Line3) on the named PG(s) to the 1st-order Line2 elements that OpenSees beam-columns require.

2nd-order continuum meshes (quadratic shells, solid tet10s, etc.) propagate their order to every line entity in the same Gmsh model — even line entities the user intended as frame elements get meshed as Line3 (Gmsh type 8, three nodes (i, j, mid)). OpenSees beam-columns are strictly 2-node 1st-order, so the bridge raises at _check_two_nodes whenever it sees a Line3 in a frame PG. This method is the broker-side resolution: rewrite the mesh so the bridge sees only Line2 on the named PG(s), keeping the continuum side untouched.

Three policies pick what happens to the mid-side node:

  • "split" — each Line3 (i, j, mid) is replaced by two Line2 elements (i, mid) and (mid, j) on the SAME dim=1 entity (PG membership tracks at entity level — no rebinding). The mid-node becomes a real FE node with its own DOFs. Exact subdivision for prismatic elastic frames; for distributed- plasticity frames the integration-point count doubles (each sub-element gets its own N-IP rule), which can shift hinge locations under softening / cyclic degradation. Concentrated- plasticity integration rules (HingeRadau, HingeRadauTwo, HingeMidpoint, HingeEndpoint) are structurally incompatible with this policy — splitting puts the calibrated end-region hinges in the wrong places. Use "forbid" if you must lock in 1st-order lines, or fall back to "this PG stays 1st-order in the mesh"; "constrain" is reserved but not implemented this round.
  • "constrain" — RESERVED but NOT IMPLEMENTED this round. The kinematically clean answer (mid-node interpolated linearly from i and j via a 2-master/1-slave constraint) requires an OpenSees primitive that does not exist today: ASDEmbeddedNodeElement accepts exactly 3 or 4 retained nodes (ADR 0036), so a Line2 master pair (2 nodes) cannot be expressed. A future round paired with upstream OpenSees work (new element class accepting N-retained-node coupling, or a new MP_Constraint primitive) will land this policy. Raises NotImplementedError until then.
  • "forbid" — fail-loud if the named PG(s) contain any Line3 element. Use as a build-time invariant lock when you must guarantee a PG remained 1st-order through meshing.

Call timing: AFTER g.mesh.generation.generate(...) (the method operates on the live mesh), BEFORE g.mesh.queries.get_fem_data(...) (the snapshot must see the rewritten topology). Never call inside a stage block — the mesh edit is global and must complete before the bridge builds.

Parameters

physical_group PG name (str) or iterable of PG names (list/ tuple). Required — there is no "split everything" mode, callers must name what they're rewriting. Each name must already exist as a physical group of dim; unknown names raise KeyError naming the missing one. policy "forbid" / "split" / "constrain" — see above. Required kwarg; no default in the spirit of fail-loud: destructive mesh mutation should never happen by accident. dim Dimension of the higher-order lines. Currently only 1 is supported; dim != 1 raises NotImplementedError. Future work (line4, cubic edges from order-3 meshes) lands via the same kwarg.

Returns

_Editing (self, for chaining)

Raises

NotImplementedError policy="constrain" (deferred to a follow-up paired with upstream OpenSees work — ADR 0036 future track). dim != 1 (line4 / cubic lines are out of scope this round; structure permits adding them with the same kwarg). RuntimeError policy="forbid" and any named PG contains at least one Line3 element. Message names the offending PG and count. KeyError A named PG does not exist at dim. ValueError policy is not one of the three accepted values.

Example

::

# Quadratic shell + frame model.  The 2nd-order shell mesh
# forces every line entity to Line3; the frame PG needs
# to come back to Line2 before the bridge sees it.
g.mesh.generation.generate(dim=3)
g.mesh.editing.split_higher_order_lines(
    "Beams", policy="split",
)
fem = g.mesh.queries.get_fem_data(dim=3)
ops = apeSees(fem)
ops.element.elasticBeamColumn(pg="Beams", ...)  # works now
Source code in src/apeGmsh/mesh/_mesh_editing.py
def split_higher_order_lines(
    self,
    physical_group: str | Iterable[str],
    *,
    policy: Literal["forbid", "split", "constrain"],
    dim: int = 1,
) -> "_Editing":
    """
    Demote higher-order line elements (Line3) on the named PG(s) to
    the 1st-order Line2 elements that OpenSees beam-columns require.

    2nd-order continuum meshes (quadratic shells, solid tet10s, etc.)
    propagate their order to every line entity in the same Gmsh
    model — even line entities the user intended as frame elements
    get meshed as Line3 (Gmsh type 8, three nodes ``(i, j, mid)``).
    OpenSees beam-columns are strictly 2-node 1st-order, so the
    bridge raises at ``_check_two_nodes`` whenever it sees a Line3
    in a frame PG.  This method is the broker-side resolution:
    rewrite the mesh so the bridge sees only Line2 on the named
    PG(s), keeping the continuum side untouched.

    Three policies pick what happens to the mid-side node:

    * ``"split"`` — each Line3 ``(i, j, mid)`` is replaced by two
      Line2 elements ``(i, mid)`` and ``(mid, j)`` on the SAME dim=1
      entity (PG membership tracks at entity level — no rebinding).
      The mid-node becomes a real FE node with its own DOFs.
      Exact subdivision for prismatic elastic frames; for distributed-
      plasticity frames the integration-point count doubles (each
      sub-element gets its own N-IP rule), which can shift hinge
      locations under softening / cyclic degradation.  Concentrated-
      plasticity integration rules (``HingeRadau``, ``HingeRadauTwo``,
      ``HingeMidpoint``, ``HingeEndpoint``) are structurally
      incompatible with this policy — splitting puts the calibrated
      end-region hinges in the wrong places.  Use ``"forbid"`` if
      you must lock in 1st-order lines, or fall back to "this PG
      stays 1st-order in the mesh"; ``"constrain"`` is reserved
      but not implemented this round.
    * ``"constrain"`` — RESERVED but NOT IMPLEMENTED this round.
      The kinematically clean answer (mid-node interpolated linearly
      from i and j via a 2-master/1-slave constraint) requires an
      OpenSees primitive that does not exist today:
      ``ASDEmbeddedNodeElement`` accepts exactly 3 or 4 retained
      nodes (ADR 0036), so a Line2 master pair (2 nodes) cannot be
      expressed.  A future round paired with upstream OpenSees work
      (new element class accepting N-retained-node coupling, or a
      new MP_Constraint primitive) will land this policy.  Raises
      ``NotImplementedError`` until then.
    * ``"forbid"`` — fail-loud if the named PG(s) contain any Line3
      element.  Use as a build-time invariant lock when you must
      guarantee a PG remained 1st-order through meshing.

    Call timing: AFTER ``g.mesh.generation.generate(...)`` (the
    method operates on the live mesh), BEFORE
    ``g.mesh.queries.get_fem_data(...)`` (the snapshot must see the
    rewritten topology).  Never call inside a stage block — the
    mesh edit is global and must complete before the bridge builds.

    Parameters
    ----------
    physical_group
        PG name (``str``) or iterable of PG names (``list``/
        ``tuple``).  Required — there is no "split everything"
        mode, callers must name what they're rewriting.  Each name
        must already exist as a physical group of ``dim``; unknown
        names raise ``KeyError`` naming the missing one.
    policy
        ``"forbid"`` / ``"split"`` / ``"constrain"`` — see above.
        Required kwarg; no default in the spirit of fail-loud:
        destructive mesh mutation should never happen by accident.
    dim
        Dimension of the higher-order lines.  Currently only ``1``
        is supported; ``dim != 1`` raises ``NotImplementedError``.
        Future work (line4, cubic edges from order-3 meshes) lands
        via the same kwarg.

    Returns
    -------
    _Editing  (self, for chaining)

    Raises
    ------
    NotImplementedError
        ``policy="constrain"`` (deferred to a follow-up paired with
        upstream OpenSees work — ADR 0036 future track).
        ``dim != 1`` (line4 / cubic lines are out of scope this
        round; structure permits adding them with the same kwarg).
    RuntimeError
        ``policy="forbid"`` and any named PG contains at least one
        Line3 element.  Message names the offending PG and count.
    KeyError
        A named PG does not exist at ``dim``.
    ValueError
        ``policy`` is not one of the three accepted values.

    Example
    -------
    ::

        # Quadratic shell + frame model.  The 2nd-order shell mesh
        # forces every line entity to Line3; the frame PG needs
        # to come back to Line2 before the bridge sees it.
        g.mesh.generation.generate(dim=3)
        g.mesh.editing.split_higher_order_lines(
            "Beams", policy="split",
        )
        fem = g.mesh.queries.get_fem_data(dim=3)
        ops = apeSees(fem)
        ops.element.elasticBeamColumn(pg="Beams", ...)  # works now
    """
    self._guard("split_higher_order_lines")
    if policy not in ("forbid", "split", "constrain"):
        raise ValueError(
            f"split_higher_order_lines: policy must be 'forbid', "
            f"'split', or 'constrain'; got {policy!r}."
        )
    if dim != 1:
        raise NotImplementedError(
            f"split_higher_order_lines: dim={dim} is not supported "
            "this round (only dim=1 / Line3 is implemented).  Cubic "
            "lines (Line4, Gmsh type 26) from order-3 meshes are "
            "deferred to a follow-up that generalizes this verb to "
            "higher-order lines."
        )
    if policy == "constrain":
        raise NotImplementedError(
            "split_higher_order_lines: policy='constrain' is "
            "reserved but not implemented this round.  The "
            "kinematically clean linear-interp mid-node constraint "
            "requires an OpenSees primitive that does not exist "
            "today (ASDEmbeddedNodeElement needs 3-4 retained "
            "nodes per ADR 0036; a Line2 master pair has 2).  Use "
            "policy='split' or policy='forbid' this round, or "
            "track the follow-up that pairs this work with "
            "upstream OpenSees changes."
        )

    physical = getattr(self._mesh._parent, "physical", None)
    if physical is None:
        raise RuntimeError(
            "split_higher_order_lines: session has no 'physical' "
            "composite — physical groups are required to name what "
            "to rewrite."
        )

    if isinstance(physical_group, str):
        pg_names: tuple[str, ...] = (physical_group,)
    else:
        pg_names = tuple(physical_group)
    if not pg_names:
        raise ValueError(
            "split_higher_order_lines: physical_group must be a "
            "non-empty PG name or iterable of names."
        )

    # Resolve every PG to its entity list before doing any
    # mutation, so we fail loud on unknown names before touching
    # the mesh.
    pg_entities: list[tuple[str, list[int]]] = []
    for name in pg_names:
        pg_tag = physical.get_tag(dim, name)
        if pg_tag is None:
            raise KeyError(
                f"split_higher_order_lines: no physical group named "
                f"{name!r} at dim={dim}.  Create it first via "
                f"g.physical.add_curve(..., name={name!r})."
            )
        ents = [
            int(e)
            for e in gmsh.model.getEntitiesForPhysicalGroup(
                dim, pg_tag,
            )
        ]
        pg_entities.append((name, ents))

    # For policy="forbid", scan every entity for Line3 elements
    # and raise on first hit.  For "split", do the surgery
    # entity-by-entity.
    total_split = 0
    for name, ents in pg_entities:
        n_line3_in_pg = 0
        for ent in ents:
            line3_tags, line3_conn = self._gather_line3(ent)
            if not line3_tags:
                continue
            n_line3_in_pg += len(line3_tags)
            if policy == "forbid":
                continue   # accumulate then raise at end of PG
            # policy == "split"
            self._replace_line3_with_line2_pair(
                ent, line3_tags, line3_conn,
            )
            total_split += len(line3_tags)
        if policy == "forbid" and n_line3_in_pg > 0:
            raise RuntimeError(
                f"split_higher_order_lines: PG {name!r} (dim={dim}) "
                f"contains {n_line3_in_pg} Line3 element(s); "
                f"policy='forbid' refuses to rewrite the mesh.  "
                "Re-mesh the source curve(s) at order 1, or call "
                "this method with policy='split' to demote them."
            )

    if policy == "split":
        if total_split > 0:
            print(
                f"split_higher_order_lines: demoted {total_split} "
                f"Line3 -> 2x Line2 across "
                f"{len(pg_names)} PG(s)"
            )
        else:
            print(
                f"split_higher_order_lines: no Line3 elements found "
                f"on PG(s) {list(pg_names)} — no-op"
            )
    self._mesh._log(
        f"split_higher_order_lines(pg={list(pg_names)}, "
        f"policy={policy!r}, dim={dim}) -> split={total_split}"
    )
    return self

affine_transform

affine_transform(matrix: list[float], dim_tags=None) -> '_Editing'

Apply an affine transformation to mesh nodes (12 coefficients, row-major 4x3 matrix — translation in last column).

dim_tags accepts any flexible-ref form (int, label/PG name, (dim, tag), or list thereof). None transforms every entity in the model.

Example

::

identity = [1, 0, 0, 0,  0, 1, 0, 0,  0, 0, 1, 0]
g.mesh.editing.affine_transform(identity, "col.body")
Source code in src/apeGmsh/mesh/_mesh_editing.py
def affine_transform(
    self,
    matrix  : list[float],
    dim_tags=None,
) -> "_Editing":
    """
    Apply an affine transformation to mesh nodes (12 coefficients,
    row-major 4x3 matrix — translation in last column).

    ``dim_tags`` accepts any flexible-ref form (int, label/PG name,
    ``(dim, tag)``, or list thereof).  ``None`` transforms every
    entity in the model.

    Example
    -------
    ::

        identity = [1, 0, 0, 0,  0, 1, 0, 0,  0, 0, 1, 0]
        g.mesh.editing.affine_transform(identity, "col.body")
    """
    self._guard("affine_transform")
    if dim_tags is None:
        dts: list[DimTag] = []
    else:
        from apeGmsh.core._helpers import resolve_to_dimtags
        dts = resolve_to_dimtags(
            dim_tags, default_dim=3, session=self._mesh._parent,
        )
    gmsh.model.mesh.affineTransform(matrix, dimTags=dts)
    self._mesh._log(f"affine_transform(dim_tags={dim_tags})")
    return self

g.mesh.queries

apeGmsh.mesh._mesh_queries._Queries

_Queries(parent_mesh: 'Mesh')

Read-only mesh data extraction and quality reporting.

Source code in src/apeGmsh/mesh/_mesh_queries.py
def __init__(self, parent_mesh: "Mesh") -> None:
    self._mesh = parent_mesh

get_nodes

get_nodes(*, dim: int = -1, tag: int = -1, include_boundary: bool = False, return_parametric: bool = False) -> dict

Query mesh nodes.

Returns

dict 'tags' : ndarray(N,) — node tags 'coords' : ndarray(N, 3) — XYZ coordinates 'parametric_coords' : ndarray — only if requested

Source code in src/apeGmsh/mesh/_mesh_queries.py
def get_nodes(
    self,
    *,
    dim              : int  = -1,
    tag              : int  = -1,
    include_boundary : bool = False,
    return_parametric: bool = False,
) -> dict:
    """
    Query mesh nodes.

    Returns
    -------
    dict
        ``'tags'``              : ndarray(N,)   — node tags
        ``'coords'``            : ndarray(N, 3) — XYZ coordinates
        ``'parametric_coords'`` : ndarray       — only if requested
    """
    self._require_kernel("g.mesh.queries.get_nodes()")
    node_tags, coords, param = gmsh.model.mesh.getNodes(
        dim=dim, tag=tag,
        includeBoundary=include_boundary,
        returnParametricCoord=return_parametric,
    )
    result: dict = {
        'tags'  : np.array(node_tags, dtype=np.int64),
        'coords': np.array(coords).reshape(-1, 3),
    }
    if return_parametric and len(param):
        result['parametric_coords'] = np.array(param)
    self._mesh._log(f"get_nodes -> {len(node_tags)} nodes")
    return result

get_elements

get_elements(*, dim: int = -1, tag: int = -1) -> dict

Query mesh elements.

Returns

dict 'types' : list[int] — gmsh element type codes 'tags' : list[ndarray] — element tags per type 'node_tags' : list[ndarray] — connectivity per type

Source code in src/apeGmsh/mesh/_mesh_queries.py
def get_elements(
    self,
    *,
    dim: int = -1,
    tag: int = -1,
) -> dict:
    """
    Query mesh elements.

    Returns
    -------
    dict
        ``'types'``     : list[int]         — gmsh element type codes
        ``'tags'``      : list[ndarray]     — element tags per type
        ``'node_tags'`` : list[ndarray]     — connectivity per type
    """
    self._require_kernel("g.mesh.queries.get_elements()")
    elem_types, elem_tags, node_tags = gmsh.model.mesh.getElements(
        dim=dim, tag=tag
    )
    result = {
        'types'    : list(elem_types),
        'tags'     : [np.array(t, dtype=np.int64) for t in elem_tags],
        'node_tags': [np.array(n, dtype=np.int64) for n in node_tags],
    }
    total = sum(len(t) for t in result['tags'])
    self._mesh._log(
        f"get_elements -> {total} elements "
        f"({len(elem_types)} types)"
    )
    return result

get_element_properties

get_element_properties(element_type: int) -> dict

Return metadata for a given gmsh element type code.

Returns

dict 'name', 'dim', 'order', 'n_nodes', 'n_primary_nodes', 'local_coords'.

Source code in src/apeGmsh/mesh/_mesh_queries.py
def get_element_properties(self, element_type: int) -> dict:
    """
    Return metadata for a given gmsh element type code.

    Returns
    -------
    dict
        ``'name'``, ``'dim'``, ``'order'``, ``'n_nodes'``,
        ``'n_primary_nodes'``, ``'local_coords'``.
    """
    # Model-INdependent (a pure lookup on the type code), so unlike
    # the reads above it cannot answer from the wrong model — but it
    # still needs the runtime up, and a chain-phase caller wanting
    # element-type metadata is better served by fem.info.types.
    self._require_kernel("g.mesh.queries.get_element_properties()")
    name, dim, order, n_nodes, local_coords, n_primary = \
        gmsh.model.mesh.getElementProperties(element_type)
    d = max(dim, 1)
    return {
        'name'           : name,
        'dim'            : dim,
        'order'          : order,
        'n_nodes'        : n_nodes,
        'n_primary_nodes': n_primary,
        'local_coords'   : np.array(local_coords).reshape(-1, d),
    }

get_fem_data

get_fem_data(dim: int | None = None, *, remove_orphans: bool = False) -> FEMData

Extract solver-ready FEM data as a :class:FEMData object.

Must be called after generate().

Parameters

dim : int or None Element dimension to extract. None extracts all dimensions present in the mesh. remove_orphans : bool If True, remove mesh nodes not connected to any element. Nodes referenced by constraints, loads, or masses are always kept. Default False.

Caching (Phase 3B.2b-prep / ADR 0038)

Repeat calls return the same :class:FEMData object identity until a broker mutation invalidates the cache (every g.constraints.X / g.loads.X / g.masses.X call bumps an internal counter; the cache is fresh iff the counter has not advanced since the last extraction). This means the session has a single canonical "chain head" snapshot that FEMData.with_* transforms and the upcoming FEMData.compose(...) engine (Phase 3B.2c) will update.

Vanilla sessions (no _fem_counter attribute — direct FEMData.from_gmsh fixtures or test stubs) skip the cache entirely and behave like the pre-cache extraction call.

Example

::

fem = g.mesh.queries.get_fem_data()          # all dims
fem = g.mesh.queries.get_fem_data(dim=3)     # 3D only
Source code in src/apeGmsh/mesh/_mesh_queries.py
def get_fem_data(
    self,
    dim: int | None = None,
    *,
    remove_orphans: bool = False,
) -> FEMData:
    """Extract solver-ready FEM data as a :class:`FEMData` object.

    Must be called **after** ``generate()``.

    Parameters
    ----------
    dim : int or None
        Element dimension to extract.  ``None`` extracts all
        dimensions present in the mesh.
    remove_orphans : bool
        If True, remove mesh nodes not connected to any element.
        Nodes referenced by constraints, loads, or masses are
        always kept.  Default False.

    Caching (Phase 3B.2b-prep / ADR 0038)
    ------------------------------------
    Repeat calls return the same :class:`FEMData` object identity
    until a broker mutation invalidates the cache (every
    ``g.constraints.X`` / ``g.loads.X`` / ``g.masses.X`` call bumps
    an internal counter; the cache is fresh iff the counter has
    not advanced since the last extraction).  This means the
    session has a single canonical "chain head" snapshot that
    ``FEMData.with_*`` transforms and the upcoming
    ``FEMData.compose(...)`` engine (Phase 3B.2c) will update.

    Vanilla sessions (no ``_fem_counter`` attribute — direct
    ``FEMData.from_gmsh`` fixtures or test stubs) skip the cache
    entirely and behave like the pre-cache extraction call.

    Example
    -------
    ::

        fem = g.mesh.queries.get_fem_data()          # all dims
        fem = g.mesh.queries.get_fem_data(dim=3)     # 3D only
    """
    parent = self._mesh._parent

    # ── Cache path — Phase 3B.2b-prep ────────────────────────
    # Only honoured for the default extraction signature: caching
    # a ``dim=`` / ``remove_orphans=`` variant alongside the
    # canonical full snapshot would silently swap between
    # ``dim=2`` and ``dim=3`` slices.  Variant calls always
    # re-extract and never poison the cache.
    is_default = (dim is None and remove_orphans is False)
    has_cache = hasattr(parent, "_fem_is_fresh")
    if is_default and has_cache and parent._fem_is_fresh():
        cached = parent._fem
        self._mesh._log(
            f"get_fem_data(dim={dim}) -> cached "
            f"{cached.info.n_nodes} nodes, "
            f"{cached.info.n_elems} elements, "
            f"bw={cached.info.bandwidth}"
        )
        return cached

    # ── Chain-phase short-circuit (Phase 3B.2d / ADR 0038) ────
    # Once ``_fem`` exists (either built via ``apeGmsh.from_h5`` OR
    # stamped by the first build-phase extraction), it IS the
    # canonical chain head.  Geometry-mutation gates raise
    # ``ChainPhaseError`` so the gmsh state can not drift from the
    # broker; ``g.constraints/loads/masses.X`` shims route through
    # ``FEMData.with_*`` via the chain-phase router so the broker
    # snapshot stays current with every shim call.
    #
    # Re-extracting from gmsh in this state would either return an
    # empty FEM (from_h5 case — gmsh has no mesh) or duplicate the
    # records already routed via with_* (regular chain-phase case).
    # Both are wrong; the simple answer is "hand back ``_fem`` and
    # mark the cache fresh".
    if (
        is_default
        and has_cache
        and parent._fem is not None
        and getattr(parent, "_fem_from_h5", False)
    ):
        base_fem = parent._fem
        parent._mark_fem_fresh()
        self._mesh._log(
            f"get_fem_data(dim={dim}) -> chain-phase "
            f"{base_fem.info.n_nodes} nodes, "
            f"{base_fem.info.n_elems} elements, "
            f"bw={base_fem.info.bandwidth}"
        )
        return base_fem

    result = FEMData.from_gmsh(
        dim=dim, session=parent, remove_orphans=remove_orphans)

    # Re-apply any stored compose bundles in compose-call order so
    # composed modules survive broker mutations on the session.
    bundles = getattr(parent, "_compose_bundles", ())
    if bundles and is_default:
        from ._compose import _merge_bundle_into_fem
        for bundle in bundles:
            result = _merge_bundle_into_fem(result, bundle)

    self._mesh._log(
        f"get_fem_data(dim={dim}) -> "
        f"{result.info.n_nodes} nodes, "
        f"{result.info.n_elems} elements, "
        f"bw={result.info.bandwidth}"
    )

    # Only the canonical extraction populates the cache; variant
    # calls (dim=, remove_orphans=) are returned uncached.
    if is_default and has_cache:
        parent._fem = result
        parent._mark_fem_fresh()

    return result

get_element_qualities

get_element_qualities(element_tags: list[int] | ndarray, quality_name: str = 'minSICN') -> ndarray

Compute quality metrics for the given elements.

Parameters

element_tags : element tags to evaluate quality_name : "minSICN", "minSIGE", "gamma", or "minSJ"

Source code in src/apeGmsh/mesh/_mesh_queries.py
def get_element_qualities(
    self,
    element_tags: list[int] | ndarray,
    quality_name: str = "minSICN",
) -> ndarray:
    """
    Compute quality metrics for the given elements.

    Parameters
    ----------
    element_tags : element tags to evaluate
    quality_name : ``"minSICN"``, ``"minSIGE"``, ``"gamma"``, or
                   ``"minSJ"``
    """
    self._require_kernel("g.mesh.queries.get_element_qualities()")
    tags = list(element_tags) if not isinstance(element_tags, list) else element_tags
    q = gmsh.model.mesh.getElementQualities(tags, qualityName=quality_name)
    return np.asarray(q)

quality_report

quality_report(*, dim: int = -1, metrics: list[str] | None = None) -> 'pd.DataFrame'

Compute a summary quality report for all mesh elements.

For each element type and quality metric, reports count, min, max, mean, std, and the percentage of elements below common thresholds.

Must be called after generate().

Example

::

g.mesh.generation.generate(2)
print(g.mesh.queries.quality_report().to_string())
Source code in src/apeGmsh/mesh/_mesh_queries.py
def quality_report(
    self,
    *,
    dim: int = -1,
    metrics: list[str] | None = None,
) -> "pd.DataFrame":
    """
    Compute a summary quality report for all mesh elements.

    For each element type and quality metric, reports count, min,
    max, mean, std, and the percentage of elements below common
    thresholds.

    Must be called **after** ``generate()``.

    Example
    -------
    ::

        g.mesh.generation.generate(2)
        print(g.mesh.queries.quality_report().to_string())
    """
    self._require_kernel("g.mesh.queries.quality_report()")
    import pandas as pd

    if metrics is None:
        metrics = ["minSICN", "minSIGE", "gamma", "minSJ"]

    elems = self.get_elements(dim=dim)

    rows: list[dict] = []
    for etype, etags in zip(elems['types'], elems['tags']):
        if len(etags) == 0:
            continue
        props = self.get_element_properties(etype)
        etype_name = props.get('name', str(etype))

        for metric in metrics:
            try:
                q = gmsh.model.mesh.getElementQualities(
                    list(etags.astype(int)), qualityName=metric,
                )
                q = np.asarray(q)
            except Exception:
                continue  # metric not supported for this element type

            if len(q) == 0:
                continue

            row: dict = {
                'element_type' : etype_name,
                'gmsh_code'    : int(etype),
                'metric'       : metric,
                'count'        : len(q),
                'min'          : float(q.min()),
                'max'          : float(q.max()),
                'mean'         : float(q.mean()),
                'std'          : float(q.std()),
                'pct_below_0.1': float((q < 0.1).sum() / len(q) * 100),
                'pct_below_0.3': float((q < 0.3).sum() / len(q) * 100),
            }
            rows.append(row)

    df = pd.DataFrame(rows)
    if not df.empty:
        df = df.set_index(['element_type', 'metric']).sort_index()

    self._mesh._log(
        f"quality_report(dim={dim}) -> "
        f"{len(rows)} metric rows across "
        f"{df.index.get_level_values('element_type').nunique() if not df.empty else 0} "
        f"element types"
    )

    if self._mesh._parent._verbose and not df.empty:
        print("\n--- Mesh Quality Report ---")
        print(df.to_string())

    return df

g.mesh.partitioning

apeGmsh.mesh._mesh_partitioning._Partitioning

_Partitioning(parent_mesh: 'Mesh')

Mesh partitioning plus node / element renumbering.

Accessed via g.mesh.partitioning.

Source code in src/apeGmsh/mesh/_mesh_partitioning.py
def __init__(self, parent_mesh: "Mesh") -> None:
    self._mesh = parent_mesh
    # Per-element weights from the most-recent weighted ``partition()``
    # call, keyed by Gmsh element tag. Cleared on every ``partition*()``
    # entry so ``summary()``/``_gather_partition_info()`` know whether
    # to populate ``PartitionInfo.weights_per_partition``.
    self._last_weights: dict[int, float] | None = None

renumber

renumber(dim: int = 2, *, method: str = 'rcm', base: int = 1) -> RenumberResult

Renumber nodes and elements in the Gmsh model.

After this call every Gmsh query returns solver-ready contiguous IDs. Call once, before extracting FEM data with :meth:~_Queries.get_fem_data.

Parameters

dim : int Element dimension used to compute bandwidth and to collect element tags for renumbering. method : "simple" | "rcm" | "hilbert" | "metis" "simple" — contiguous IDs, no optimisation. "rcm" — Reverse Cuthill-McKee (bandwidth reduction). "hilbert" — Hilbert space-filling curve (cache locality). "metis" — METIS graph-partitioner ordering. base : int Starting ID (default 1 = OpenSees / Abaqus convention).

Returns

RenumberResult

Source code in src/apeGmsh/mesh/_mesh_partitioning.py
def renumber(
    self,
    dim: int = 2,
    *,
    method: str = "rcm",
    base: int = 1,
) -> RenumberResult:
    """Renumber nodes and elements in the Gmsh model.

    After this call every Gmsh query returns solver-ready contiguous
    IDs.  Call **once**, before extracting FEM data with
    :meth:`~_Queries.get_fem_data`.

    Parameters
    ----------
    dim : int
        Element dimension used to compute bandwidth and to collect
        element tags for renumbering.
    method : ``"simple"`` | ``"rcm"`` | ``"hilbert"`` | ``"metis"``
        ``"simple"``  — contiguous IDs, no optimisation.
        ``"rcm"``     — Reverse Cuthill-McKee (bandwidth reduction).
        ``"hilbert"`` — Hilbert space-filling curve (cache locality).
        ``"metis"``   — METIS graph-partitioner ordering.
    base : int
        Starting ID (default 1 = OpenSees / Abaqus convention).

    Returns
    -------
    RenumberResult
    """
    # Phase 3B.2d / ADR 0038 — chain-phase freeze.
    from apeGmsh.core._compose_errors import chain_phase_guard
    chain_phase_guard(
        self._mesh._parent, "g.mesh.partitioning.renumber"
    )
    from ._fem_extract import extract_raw
    from .FEMData import _compute_bandwidth
    from ._fem_factory import _build_element_groups

    # 1. Bandwidth BEFORE ────────────────────────────────────
    raw = extract_raw(dim=dim)
    groups = _build_element_groups(raw['groups'])
    bw_before = _compute_bandwidth(groups)
    n_nodes = len(raw['node_tags'])
    n_elems = len(raw['elem_tags'])

    # 2. Node renumbering ────────────────────────────────────
    if method == "simple":
        self._renumber_nodes_simple(base)
    elif method in _METHOD_MAP:
        old, new = gmsh.model.mesh.computeRenumbering(
            method=_METHOD_MAP[method])
        gmsh.model.mesh.renumberNodes(
            oldTags=list(old), newTags=list(new))
    else:
        raise ValueError(
            f"Unknown method {method!r}. "
            f"Use 'simple', 'rcm', 'hilbert', or 'metis'.")

    # 3. Element renumbering (always simple contiguous) ──────
    self._renumber_elements_simple(dim, base)

    # 4. Bandwidth AFTER ─────────────────────────────────────
    raw_after = extract_raw(dim=dim)
    groups_after = _build_element_groups(raw_after['groups'])
    bw_after = _compute_bandwidth(groups_after)

    result = RenumberResult(
        method=method,
        n_nodes=n_nodes,
        n_elements=n_elems,
        bandwidth_before=bw_before,
        bandwidth_after=bw_after,
    )
    self._mesh._log(
        f"renumber(method={method!r}, dim={dim}): "
        f"{n_nodes} nodes, {n_elems} elements, "
        f"bw {bw_before}\u2192{bw_after}")
    return result

partition

partition(n_parts: int, *, weights: Sequence[float] | ndarray | None = None, backend: PartitionBackend | None = None, uncuttable_elements: Sequence[int] | ndarray | None = None) -> PartitionInfo

Partition the mesh into n_parts sub-domains.

Must be called after g.mesh.generation.generate().

Two flavours, dispatched by weights / backend:

============= ================== ================================== weights backend Path ============= ================== ================================== None None Gmsh-native METIS (unweighted) None "gmsh" Gmsh-native METIS (explicit alias) None "pymetis" pymetis with vwgt = ones(n) None "networkx-... networkx-metis, unit weights sequence None pymetis (default for weighted) sequence "pymetis" pymetis with the given weights sequence "networkx-... networkx-metis with weights sequence "gmsh" ValueError — Gmsh has no vwgt ============= ================== ==================================

uncuttable_elements applies after either flavour above has run (ADR 0092 INV-4 — "the partitioner may cut the slave side of a contact interface, never the master"): every element tag it names is forced onto a single partition — whichever one currently holds the most of them, ties breaking to the lowest partition id — by overriding the just-computed assignment through :meth:partition_explicit. This is a deterministic correction, not a bias: METIS vertex weights (weights= above) balance partition size, they do not stop it from cutting through a specific group, so a group that must stay whole is enforced as a post-hoc override instead.

Parameters

n_parts : int Number of partitions (>= 1). weights : sequence of float, optional Per-element vertex weights for METIS. Length must match the total number of elements across all dimensions, ordered by flattened gmsh.model.mesh.getElements(dim=-1, tag=-1) traversal — the same order used to drive partition_explicit internally. backend : "gmsh" | "pymetis" | "networkx-metis", optional Backend selector. See dispatch table above. uncuttable_elements : sequence of int, optional Gmsh element tags (any dimension) that must all end up on the same partition — e.g. a contact interface's master-surface face elements plus their backing solid elements. None or empty is a no-op: the base partition from weights/ backend is returned unchanged.

Returns

PartitionInfo weights_per_partition is populated when weights was passed, else None.

Raises

ValueError On n_parts < 1, backend="gmsh" with weights, wrong-length weights, or an uncuttable_elements tag absent from the partitioned mesh. ImportError When the selected backend is not installed (with a pip install apeGmsh[partition-pymetis] hint).

Source code in src/apeGmsh/mesh/_mesh_partitioning.py
def partition(
    self,
    n_parts: int,
    *,
    weights: Sequence[float] | np.ndarray | None = None,
    backend: PartitionBackend | None = None,
    uncuttable_elements: Sequence[int] | np.ndarray | None = None,
) -> PartitionInfo:
    """Partition the mesh into *n_parts* sub-domains.

    Must be called after ``g.mesh.generation.generate()``.

    Two flavours, dispatched by ``weights`` / ``backend``:

    ============= ================== ==================================
    ``weights``   ``backend``        Path
    ============= ================== ==================================
    ``None``      ``None``           Gmsh-native METIS (unweighted)
    ``None``      ``"gmsh"``         Gmsh-native METIS (explicit alias)
    ``None``      ``"pymetis"``      pymetis with ``vwgt = ones(n)``
    ``None``      ``"networkx-...``  networkx-metis, unit weights
    sequence      ``None``           pymetis (default for weighted)
    sequence      ``"pymetis"``      pymetis with the given weights
    sequence      ``"networkx-...``  networkx-metis with weights
    sequence      ``"gmsh"``         ``ValueError`` — Gmsh has no vwgt
    ============= ================== ==================================

    ``uncuttable_elements`` applies *after* either flavour above has
    run (ADR 0092 INV-4 — "the partitioner may cut the slave side of a
    contact interface, never the master"): every element tag it names
    is forced onto a single partition — whichever one currently holds
    the most of them, ties breaking to the lowest partition id — by
    overriding the just-computed assignment through
    :meth:`partition_explicit`. This is a deterministic correction,
    not a bias: METIS vertex weights (``weights=`` above) balance
    partition *size*, they do not stop it from cutting through a
    specific group, so a group that must stay whole is enforced as a
    post-hoc override instead.

    Parameters
    ----------
    n_parts : int
        Number of partitions (>= 1).
    weights : sequence of float, optional
        Per-element vertex weights for METIS.  Length must match the
        total number of elements across **all** dimensions, ordered
        by flattened ``gmsh.model.mesh.getElements(dim=-1, tag=-1)``
        traversal — the same order used to drive
        ``partition_explicit`` internally.
    backend : ``"gmsh"`` | ``"pymetis"`` | ``"networkx-metis"``, optional
        Backend selector.  See dispatch table above.
    uncuttable_elements : sequence of int, optional
        Gmsh element tags (any dimension) that must all end up on the
        same partition — e.g. a contact interface's master-surface
        face elements plus their backing solid elements.  ``None`` or
        empty is a no-op: the base partition from ``weights``/
        ``backend`` is returned unchanged.

    Returns
    -------
    PartitionInfo
        ``weights_per_partition`` is populated when ``weights`` was
        passed, else ``None``.

    Raises
    ------
    ValueError
        On ``n_parts < 1``, ``backend="gmsh"`` with ``weights``,
        wrong-length ``weights``, or an ``uncuttable_elements`` tag
        absent from the partitioned mesh.
    ImportError
        When the selected backend is not installed (with a
        ``pip install apeGmsh[partition-pymetis]`` hint).
    """
    # Phase 3B.2d / ADR 0038 — chain-phase freeze.
    from apeGmsh.core._compose_errors import chain_phase_guard
    chain_phase_guard(
        self._mesh._parent, "g.mesh.partitioning.partition"
    )
    if n_parts < 1:
        raise ValueError(f"n_parts must be >= 1, got {n_parts}")

    # Default backend resolution.
    if backend is None:
        backend = "pymetis" if weights is not None else "gmsh"

    if backend == "gmsh":
        if weights is not None:
            raise ValueError(
                "Gmsh has no vwgt API; use backend='pymetis' or "
                "backend='networkx-metis' to pass weights.")
        self._last_weights = None
        gmsh.model.mesh.partition(n_parts)
        info = self._gather_partition_info()
        self._mesh._log(f"partition(n_parts={n_parts})")
    elif backend in ("pymetis", "networkx-metis"):
        info = self._partition_weighted(n_parts, weights, backend)
    else:
        raise ValueError(
            f"Unknown backend {backend!r}. "
            "Use 'gmsh', 'pymetis', or 'networkx-metis'.")

    if uncuttable_elements is not None and len(uncuttable_elements) > 0:
        info = self._enforce_uncuttable(n_parts, uncuttable_elements)

    return info

partition_explicit

partition_explicit(n_parts: int, elem_tags: list[int], parts: list[int]) -> PartitionInfo

Partition with an explicit per-element assignment.

Parameters

n_parts : int Total number of partitions declared. elem_tags : list[int] Element tags to assign. parts : list[int] Parallel list of 1-based partition IDs.

Returns

PartitionInfo

Source code in src/apeGmsh/mesh/_mesh_partitioning.py
def partition_explicit(
    self,
    n_parts: int,
    elem_tags: list[int],
    parts: list[int],
) -> PartitionInfo:
    """Partition with an explicit per-element assignment.

    Parameters
    ----------
    n_parts : int
        Total number of partitions declared.
    elem_tags : list[int]
        Element tags to assign.
    parts : list[int]
        Parallel list of 1-based partition IDs.

    Returns
    -------
    PartitionInfo
    """
    # Phase 3B.2d / ADR 0038 — chain-phase freeze.
    from apeGmsh.core._compose_errors import chain_phase_guard
    chain_phase_guard(
        self._mesh._parent, "g.mesh.partitioning.partition_explicit"
    )
    if len(elem_tags) != len(parts):
        raise ValueError(
            f"len(elem_tags)={len(elem_tags)} != "
            f"len(parts)={len(parts)}")
    # External callers of partition_explicit don't supply weights;
    # clear so ``_gather_partition_info`` reports unweighted state.
    # The internal weighted path sets ``_last_weights`` *before*
    # calling ``partition_explicit`` and re-asserts it after, so its
    # cache survives this clear.
    self._last_weights = None
    gmsh.model.mesh.partition(
        n_parts, elementTags=elem_tags, partitions=parts)
    info = self._gather_partition_info()
    self._mesh._log(
        f"partition_explicit(n_parts={n_parts}, "
        f"n_elements={len(elem_tags)})")
    return info

unpartition

unpartition() -> '_Partitioning'

Remove the partition structure and restore a monolithic mesh.

Source code in src/apeGmsh/mesh/_mesh_partitioning.py
def unpartition(self) -> "_Partitioning":
    """Remove the partition structure and restore a monolithic mesh."""
    # Phase 3B.2d / ADR 0038 — chain-phase freeze.
    from apeGmsh.core._compose_errors import chain_phase_guard
    chain_phase_guard(
        self._mesh._parent, "g.mesh.partitioning.unpartition"
    )
    self._last_weights = None
    gmsh.model.mesh.unpartition()
    self._mesh._log("unpartition()")
    return self

n_partitions

n_partitions() -> int

Return the current number of partitions (0 if not partitioned).

Source code in src/apeGmsh/mesh/_mesh_partitioning.py
def n_partitions(self) -> int:
    """Return the current number of partitions (0 if not partitioned)."""
    self._require_kernel("g.mesh.partitioning.n_partitions()")
    return gmsh.model.getNumberOfPartitions()

summary

summary() -> str

Concise text summary of the partition state.

Source code in src/apeGmsh/mesh/_mesh_partitioning.py
def summary(self) -> str:
    """Concise text summary of the partition state."""
    # Guarded at its own verb rather than inheriting the one from
    # n_partitions() below: unguarded against an empty kernel this
    # reports "not partitioned" for a model whose broker may hold
    # partitions — a wrong answer, not a failure.
    self._require_kernel("g.mesh.partitioning.summary()")
    n = self.n_partitions()
    model_name = getattr(
        getattr(self._mesh, '_parent', None), 'name', '?')
    if n == 0:
        return f"Partitioning(model={model_name!r}): not partitioned"
    lines = [
        f"Partitioning(model={model_name!r}): {n} partition(s)"]
    df = self.entity_table()
    if not df.empty:
        partitioned = df[df['partitions'] != '']
        counts = (
            partitioned
            .reset_index()
            .groupby('dim')
            .size()
            .rename(index={
                0: 'points', 1: 'curves',
                2: 'surfaces', 3: 'volumes'}))
        for dim_label, count in counts.items():
            lines.append(
                f"  {dim_label:10s}: {count} partitioned entities")
    return "\n".join(lines)

entity_table

entity_table(dim: int = -1) -> 'pd.DataFrame'

DataFrame of all model entities and their partition membership.

Parameters

dim : int Restrict to a single dimension (-1 = all).

Returns

pd.DataFrame Columns: dim, tag, partitions, parent_dim, parent_tag.

Source code in src/apeGmsh/mesh/_mesh_partitioning.py
def entity_table(self, dim: int = -1) -> "pd.DataFrame":
    """DataFrame of all model entities and their partition membership.

    Parameters
    ----------
    dim : int
        Restrict to a single dimension (``-1`` = all).

    Returns
    -------
    pd.DataFrame
        Columns: ``dim``, ``tag``, ``partitions``,
        ``parent_dim``, ``parent_tag``.
    """
    self._require_kernel("g.mesh.partitioning.entity_table()")
    import pandas as pd

    rows: list[dict] = []
    entities = (
        gmsh.model.getEntities(dim=dim)
        if dim != -1
        else gmsh.model.getEntities())
    for ent_dim, ent_tag in entities:
        try:
            parts = list(gmsh.model.getPartitions(ent_dim, ent_tag))
        except Exception:
            parts = []
        try:
            p_dim, p_tag = gmsh.model.getParent(ent_dim, ent_tag)
        except Exception:
            p_dim, p_tag = -1, -1
        rows.append({
            'dim':        ent_dim,
            'tag':        ent_tag,
            'partitions': ", ".join(str(p) for p in parts),
            'parent_dim': p_dim,
            'parent_tag': p_tag,
        })

    if not rows:
        return pd.DataFrame(
            columns=['dim', 'tag', 'partitions',
                     'parent_dim', 'parent_tag'])
    return pd.DataFrame(rows).set_index(['dim', 'tag'])

save

save(path: Path | str, *, one_file_per_partition: bool = False, create_topology: bool = False, create_physicals: bool = True) -> '_Partitioning'

Write the partitioned mesh to file(s).

Parameters

path : Path or str Output file path (format inferred from extension). one_file_per_partition : bool Write one file per partition alongside the combined file. create_topology : bool Pass to Mesh.PartitionCreateTopology. create_physicals : bool Pass to Mesh.PartitionCreatePhysicals.

Returns

self — for chaining

Source code in src/apeGmsh/mesh/_mesh_partitioning.py
def save(
    self,
    path: Path | str,
    *,
    one_file_per_partition: bool = False,
    create_topology: bool = False,
    create_physicals: bool = True,
) -> "_Partitioning":
    """Write the partitioned mesh to file(s).

    Parameters
    ----------
    path : Path or str
        Output file path (format inferred from extension).
    one_file_per_partition : bool
        Write one file per partition alongside the combined file.
    create_topology : bool
        Pass to ``Mesh.PartitionCreateTopology``.
    create_physicals : bool
        Pass to ``Mesh.PartitionCreatePhysicals``.

    Returns
    -------
    self — for chaining
    """
    # Without this, a kernel-less session does not merely fail — it
    # writes whatever empty model gmsh happens to hold, producing a
    # plausible-looking mesh file with no elements in it.
    self._require_kernel("g.mesh.partitioning.save()")
    path = Path(path)
    gmsh.option.setNumber(
        "Mesh.PartitionCreateTopology", int(create_topology))
    gmsh.option.setNumber(
        "Mesh.PartitionCreatePhysicals", int(create_physicals))
    gmsh.option.setNumber(
        "Mesh.PartitionSplitMeshFiles", int(one_file_per_partition))
    gmsh.write(str(path))
    self._mesh._log(
        f"save({path}, "
        f"one_file_per_partition={one_file_per_partition})")
    return self

Fluent selection — g.mesh_selection.select()

g.mesh_selection (a session attribute, MeshSelectionSet) is the live-mesh entry of the unified, daisy-chainable selection idiom. select() returns a MeshSelection (point family, bi-level) with .ids / .coords / .result() and the live-engine-only .save_as(name) terminal. The former add_nodes / add_elements spatial registrars (and from_geometric) have been removed; persist a selection with .select(...).save_as(name) (live engine), or register explicit ids with the retained g.mesh_selection.add(dim, ids, name=) / g.mesh_selection.from_physical(...). filter_set / sort_set / union / intersection / difference remain.

node_set = (g.mesh_selection.select()              # node level
    .in_box((0, 0, 0), (1, 1, 1))                  # half-open [lo, hi)
    .on_plane((0, 0, 0), (0, 0, 1), tol=1e-9)
    .save_as("base"))                              # live-engine persist

hexes = g.mesh_selection.select(level="element", dim=3).in_box(
    lo, hi, inclusive=True).ids

S2 — point-family box is half-open

The point-family in_box(lo, hi) is half-open [lo, hi) (to match results). Pass inclusive=True to get a closed [lo, hi] box. See the changelog for the migration note.

See Selection for the full idiom and the removed-vs-retained migration table.

Supporting types

apeGmsh.mesh.PhysicalGroups.PhysicalGroups

PhysicalGroups(parent: SessionProtocol)

Bases: _HasLogging

Physical-group composite attached to a apeGmsh instance as g.physical.

Wraps the physical-group subset of the Gmsh Python API with a clean, method-chaining interface organised into:

  • Creationadd, add_point, add_curve, add_surface, add_volume
  • Namingset_name, remove_name
  • Removalremove, remove_all
  • Queriesget_all, get_entities, get_groups_for_entity, get_name, get_tag, summary
  • Mesh nodesget_nodes

All mutating methods return self for chaining::

(g.physical
   .add_surface([s_inlet],  name="Inlet")
   .add_surface([s1, s2],   name="Wall")
   .add_volume([vol],       name="Fluid"))

Parameters

parent : _SessionBase The owning instance — used for _verbose.

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def __init__(self, parent: _SessionBase) -> None:
    self._parent = parent

add

add(dim: int, tags, *, name: str = '', tag: Tag = -1) -> Tag

Create or append to a physical group.

If name matches an existing PG at this dim, the new tags are merged into it (upsert). Otherwise a new PG is created.

Parameters

dim : entity dimension (0 = points, 1 = curves, 2 = surfaces, 3 = volumes) tags : entity tags — accepts int, str (label or PG name), (dim, tag) tuples, or a list mixing all of these. String references are resolved via g.labels first, then g.physical. name : human-readable name. If a PG with this name already exists at dim, the new entities are appended. A physical-group name maps to exactly one dimension — reusing name at a different dim raises ValueError. tag : requested physical-group tag (-1 = auto-assign). Ignored when appending to an existing named PG.

Returns

Tag the physical-group tag (existing or newly created)

Raises

ValueError If name is already used by a physical group at a different dimension. Multi-dimensional physical groups are not supported — pick a distinct name per dimension.

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def add(
    self,
    dim     : int,
    tags,
    *,
    name    : str = "",
    tag     : Tag = -1,
) -> Tag:
    """Create or append to a physical group.

    If *name* matches an existing PG at this *dim*, the new *tags*
    are merged into it (upsert).  Otherwise a new PG is created.

    Parameters
    ----------
    dim  : entity dimension (0 = points, 1 = curves, 2 = surfaces,
           3 = volumes)
    tags : entity tags — accepts ``int``, ``str`` (label or PG name),
           ``(dim, tag)`` tuples, or a list mixing all of these.
           String references are resolved via ``g.labels`` first,
           then ``g.physical``.
    name : human-readable name.  If a PG with this name already
           exists at *dim*, the new entities are appended.  A
           physical-group name maps to exactly one dimension —
           reusing *name* at a different dim raises ``ValueError``.
    tag  : requested physical-group tag (``-1`` = auto-assign).
           Ignored when appending to an existing named PG.

    Returns
    -------
    Tag  the physical-group tag (existing or newly created)

    Raises
    ------
    ValueError
        If *name* is already used by a physical group at a
        different dimension.  Multi-dimensional physical groups
        are not supported — pick a distinct name per dimension.
    """
    # Phase 3B.2d / ADR 0038 — PG mutation in chain phase would
    # diverge the FEMData snapshot from the live gmsh model.
    from apeGmsh.core._compose_errors import chain_phase_guard
    chain_phase_guard(
        self._parent, f"g.physical.add(dim={dim}, name={name!r})"
    )
    from typing import cast
    from apeGmsh.core._helpers import resolve_to_tags
    if isinstance(tags, (str, int)):
        tags = [tags]
    # self._parent honours the SessionProtocol structural contract
    # that resolve_to_tags requires — cast to the nominal type
    # mypy expects.
    from apeGmsh._session import _SessionBase
    resolved = resolve_to_tags(
        tags, dim=dim, session=cast(_SessionBase, self._parent),
    )

    # Upsert: if a PG with this name already exists, merge
    if name:
        conflict = [
            d for d in (0, 1, 2, 3)
            if d != dim and self.get_tag(d, name) is not None
        ]
        if conflict:
            raise ValueError(
                f"Physical group {name!r} already exists at "
                f"dim={conflict[0]}.  A physical-group name maps to "
                f"a single dimension — use a distinct name for the "
                f"dim={dim} entities (multi-dimensional physical "
                f"groups are not supported)."
            )
        existing_tag = self.get_tag(dim, name)
        if existing_tag is not None:
            old_ents = list(
                gmsh.model.getEntitiesForPhysicalGroup(
                    dim, existing_tag))
            combined = sorted(
                set(old_ents) | set(int(t) for t in resolved))
            gmsh.model.removePhysicalGroups(
                dimTags=[(dim, existing_tag)])
            pg_tag = gmsh.model.addPhysicalGroup(
                dim, combined, tag=existing_tag)
            gmsh.model.setPhysicalName(dim, pg_tag, name)
            n_new = len(combined) - len(old_ents)
            self._log(
                f"add(dim={dim}, name={name!r}): appended "
                f"{n_new} entity(ies) -> {len(combined)} total")
            return pg_tag

    pg_tag = gmsh.model.addPhysicalGroup(dim, resolved, tag=tag)
    if name:
        gmsh.model.setPhysicalName(dim, pg_tag, name)
    self._log(
        f"add(dim={dim}, entities={tags}) -> pg_tag={pg_tag}"
        + (f", name={name!r}" if name else "")
    )
    return pg_tag

add_point

add_point(tags: list[Tag], *, name: str = '', tag: Tag = -1) -> PhysicalGroups

Shorthand: add(dim=0, tags, ...) — returns self for chaining.

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def add_point(self, tags: list[Tag], *, name: str = "", tag: Tag = -1) -> PhysicalGroups:
    """Shorthand: ``add(dim=0, tags, ...)`` — returns ``self`` for chaining."""
    self.add(0, tags, name=name, tag=tag)
    return self

add_curve

add_curve(tags: list[Tag], *, name: str = '', tag: Tag = -1) -> PhysicalGroups

Shorthand: add(dim=1, tags, ...) — returns self for chaining.

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def add_curve(self, tags: list[Tag], *, name: str = "", tag: Tag = -1) -> PhysicalGroups:
    """Shorthand: ``add(dim=1, tags, ...)`` — returns ``self`` for chaining."""
    self.add(1, tags, name=name, tag=tag)
    return self

add_surface

add_surface(tags: list[Tag], *, name: str = '', tag: Tag = -1) -> PhysicalGroups

Shorthand: add(dim=2, tags, ...) — returns self for chaining.

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def add_surface(self, tags: list[Tag], *, name: str = "", tag: Tag = -1) -> PhysicalGroups:
    """Shorthand: ``add(dim=2, tags, ...)`` — returns ``self`` for chaining."""
    self.add(2, tags, name=name, tag=tag)
    return self

add_volume

add_volume(tags: list[Tag], *, name: str = '', tag: Tag = -1) -> PhysicalGroups

Shorthand: add(dim=3, tags, ...) — returns self for chaining.

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def add_volume(self, tags: list[Tag], *, name: str = "", tag: Tag = -1) -> PhysicalGroups:
    """Shorthand: ``add(dim=3, tags, ...)`` — returns ``self`` for chaining."""
    self.add(3, tags, name=name, tag=tag)
    return self

from_label

from_label(label_name: str, *, name: str | None = None, dim: int | None = None) -> Tag

Create (or append to) a PG from a label's entities.

Parameters

label_name : str Label name (without _label: prefix). name : str, optional PG name. Defaults to the label name. dim : int, optional Dimension. If None, inferred from the label.

Returns

Tag the physical-group tag

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def from_label(
    self,
    label_name: str,
    *,
    name: str | None = None,
    dim: int | None = None,
) -> Tag:
    """Create (or append to) a PG from a label's entities.

    Parameters
    ----------
    label_name : str
        Label name (without ``_label:`` prefix).
    name : str, optional
        PG name.  Defaults to the label name.
    dim : int, optional
        Dimension.  If ``None``, inferred from the label.

    Returns
    -------
    Tag  the physical-group tag
    """
    labels = getattr(self._parent, 'labels', None)
    if labels is None:
        raise RuntimeError("No labels composite on session.")
    ent_tags = labels.entities(label_name, dim=dim)
    # Infer dim from the label's PG
    if dim is None:
        for d in range(4):
            for pg_dim, pg_tag in gmsh.model.getPhysicalGroups(d):
                from apeGmsh._kernel._label_prefix import add_prefix
                if gmsh.model.getPhysicalName(pg_dim, pg_tag) == add_prefix(label_name):
                    dim = pg_dim
                    break
            if dim is not None:
                break
    if dim is None:
        raise KeyError(
            f"Cannot infer dimension for label {label_name!r}.")
    pg_name = name if name is not None else label_name
    return self.add(dim, ent_tags, name=pg_name)

from_labels

from_labels(label_names: list[str], *, name: str, dim: int | None = None) -> Tag

Create (or append to) a PG from multiple labels (union).

Parameters

label_names : list[str] Label names to combine. name : str PG name for the combined group. dim : int, optional Dimension. If None, inferred from the first label.

Returns

Tag the physical-group tag

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def from_labels(
    self,
    label_names: list[str],
    *,
    name: str,
    dim: int | None = None,
) -> Tag:
    """Create (or append to) a PG from multiple labels (union).

    Parameters
    ----------
    label_names : list[str]
        Label names to combine.
    name : str
        PG name for the combined group.
    dim : int, optional
        Dimension.  If ``None``, inferred from the first label.

    Returns
    -------
    Tag  the physical-group tag
    """
    labels = getattr(self._parent, 'labels', None)
    if labels is None:
        raise RuntimeError("No labels composite on session.")
    all_tags: list[int] = []
    inferred_dim = dim
    for lbl in label_names:
        ent_tags = labels.entities(lbl, dim=dim)
        all_tags.extend(ent_tags)
        if inferred_dim is None:
            # Infer from first label
            for d in range(4):
                for pg_dim, pg_tag in gmsh.model.getPhysicalGroups(d):
                    from apeGmsh._kernel._label_prefix import add_prefix
                    if gmsh.model.getPhysicalName(pg_dim, pg_tag) == add_prefix(lbl):
                        inferred_dim = pg_dim
                        break
                if inferred_dim is not None:
                    break
    if inferred_dim is None:
        raise KeyError(
            f"Cannot infer dimension for labels {label_names!r}.")
    return self.add(inferred_dim, sorted(set(all_tags)), name=name)

set_name

set_name(dim: int, tag: Tag, name: str) -> PhysicalGroups

Assign (or rename) the label of an existing physical group.

Parameters

dim : dimension of the physical group tag : physical-group tag name : new label

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def set_name(self, dim: int, tag: Tag, name: str) -> PhysicalGroups:
    """
    Assign (or rename) the label of an existing physical group.

    Parameters
    ----------
    dim  : dimension of the physical group
    tag  : physical-group tag
    name : new label
    """
    # Frozen on the same terms as add(): the broker's
    # PhysicalGroupSet is a snapshot keyed by name, so renaming a
    # PG here leaves the two disagreeing about what exists.
    self._require_unfrozen(f"g.physical.set_name({name!r})")
    gmsh.model.setPhysicalName(dim, tag, name)
    self._log(f"set_name(dim={dim}, tag={tag}, name={name!r})")
    return self

remove_name

remove_name(name: str) -> PhysicalGroups

Remove the name-to-tag mapping for name.

The physical group itself is not deleted — only the name entry.

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def remove_name(self, name: str) -> PhysicalGroups:
    """
    Remove the name-to-tag mapping for *name*.

    The physical group itself is not deleted — only the name entry.
    """
    self._require_unfrozen(f"g.physical.remove_name({name!r})")
    gmsh.model.removePhysicalName(name)
    self._log(f"remove_name({name!r})")
    return self

remove

remove(dim_tags: list[DimTag]) -> PhysicalGroups

Remove specific physical groups.

Parameters

dim_tags : [(dim, pg_tag), ...] pairs identifying groups to remove

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def remove(self, dim_tags: list[DimTag]) -> PhysicalGroups:
    """
    Remove specific physical groups.

    Parameters
    ----------
    dim_tags : ``[(dim, pg_tag), ...]`` pairs identifying groups to remove
    """
    self._require_unfrozen("g.physical.remove()")
    gmsh.model.removePhysicalGroups(dimTags=dim_tags)
    self._log(f"remove({dim_tags})")
    return self

remove_all

remove_all() -> PhysicalGroups

Remove every physical group in the current model.

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def remove_all(self) -> PhysicalGroups:
    """Remove every physical group in the current model."""
    self._require_unfrozen("g.physical.remove_all()")
    gmsh.model.removePhysicalGroups()
    self._log("remove_all()")
    return self

get_all

get_all(dim: int = -1) -> list[DimTag]

Return all user-facing physical groups as (dim, tag) pairs. Internal label PGs (Tier 1 naming, prefixed with _label:) are filtered out — use g.labels.get_all() to see those.

Parameters

dim : filter to a single dimension (-1 = all)

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def get_all(self, dim: int = -1) -> list[DimTag]:
    """
    Return all **user-facing** physical groups as ``(dim, tag)``
    pairs.  Internal label PGs (Tier 1 naming, prefixed with
    ``_label:``) are filtered out — use ``g.labels.get_all()``
    to see those.

    Parameters
    ----------
    dim : filter to a single dimension (``-1`` = all)
    """
    from apeGmsh.core._compose_errors import raise_if_no_live_kernel
    raise_if_no_live_kernel(self._parent, "g.physical.get_all()")
    return [
        (d, t) for d, t in gmsh.model.getPhysicalGroups(dim=dim)
        if not is_label_pg(gmsh.model.getPhysicalName(d, t))
    ]

get_entities

get_entities(dim: int, tag: Tag) -> list[Tag]

Return the model-entity tags contained in a physical group.

Parameters

dim : dimension of the physical group tag : physical-group tag

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def get_entities(self, dim: int, tag: Tag) -> list[Tag]:
    """
    Return the model-entity tags contained in a physical group.

    Parameters
    ----------
    dim : dimension of the physical group
    tag : physical-group tag
    """
    from apeGmsh.core._compose_errors import raise_if_no_live_kernel
    raise_if_no_live_kernel(self._parent, "g.physical.get_entities()")
    return list(gmsh.model.getEntitiesForPhysicalGroup(dim, tag))

entities

entities(name_or_tag, *, dim: int | None = None) -> list[Tag]

Resolve a physical group to its entity tags — by name or by tag.

Parameters

name_or_tag : str | int Physical group name, or the raw PG tag (dim required). dim : int | None, optional Dimension to search. If omitted and name_or_tag is a string, all dimensions are searched. A physical-group name maps to a single dimension; if a legacy model nonetheless carries the name at multiple dims this raises — pass dim= to read one slice.

Returns

list[Tag] Flat list of model-entity tags contained in the group.

Raises

KeyError If no physical group with that name exists. ValueError If name_or_tag is a string, dim is omitted, and the name matches physical groups at more than one dimension. TypeError If name_or_tag is an int but dim is not provided.

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def entities(
    self,
    name_or_tag,
    *,
    dim: int | None = None,
) -> list[Tag]:
    """
    Resolve a physical group to its entity tags — by **name** or by tag.

    Parameters
    ----------
    name_or_tag : str | int
        Physical group name, or the raw PG tag (``dim`` required).
    dim : int | None, optional
        Dimension to search.  If omitted and *name_or_tag* is a string,
        all dimensions are searched.  A physical-group name maps to a
        single dimension; if a legacy model nonetheless carries the
        name at multiple dims this **raises** — pass ``dim=`` to read
        one slice.

    Returns
    -------
    list[Tag]
        Flat list of model-entity tags contained in the group.

    Raises
    ------
    KeyError
        If no physical group with that name exists.
    ValueError
        If *name_or_tag* is a string, ``dim`` is omitted, and the
        name matches physical groups at more than one dimension.
    TypeError
        If ``name_or_tag`` is an int but ``dim`` is not provided.
    """
    from apeGmsh.core._compose_errors import raise_if_no_live_kernel
    raise_if_no_live_kernel(self._parent, "g.physical.entities()")
    if isinstance(name_or_tag, str):
        if dim is None:
            matches = [
                d for d in (0, 1, 2, 3)
                if self.get_tag(d, name_or_tag) is not None
            ]
            if not matches:
                raise KeyError(
                    f"No physical group named {name_or_tag!r} at any "
                    f"dimension.  If this is a label, use "
                    f"g.labels.entities({name_or_tag!r}) or promote it "
                    f"with g.labels.promote_to_physical({name_or_tag!r})."
                )
            if len(matches) > 1:
                raise ValueError(
                    f"Physical group {name_or_tag!r} exists at "
                    f"multiple dimensions {matches}. Multi-dimensional "
                    f"physical groups are not supported; pass `dim=` "
                    f"to read one slice."
                )
            d = matches[0]
            pg_tag = self.get_tag(d, name_or_tag)
            return list(gmsh.model.getEntitiesForPhysicalGroup(d, pg_tag))
        pg_tag = self.get_tag(dim, name_or_tag)
        if pg_tag is None:
            raise KeyError(
                f"No physical group named {name_or_tag!r} at dim={dim}.  "
                f"If this is a label, use g.labels.entities() or "
                f"g.labels.promote_to_physical()."
            )
        return list(gmsh.model.getEntitiesForPhysicalGroup(dim, pg_tag))

    # int / PG tag path
    if dim is None:
        raise TypeError(
            "entities(): when passing a raw PG tag, `dim` must be given"
        )
    return list(gmsh.model.getEntitiesForPhysicalGroup(dim, int(name_or_tag)))

get_groups_for_entity

get_groups_for_entity(dim: int, tag: Tag) -> list[Tag]

Return the physical-group tags that contain a given model entity.

Parameters

dim : entity dimension tag : model-entity tag

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def get_groups_for_entity(self, dim: int, tag: Tag) -> list[Tag]:
    """
    Return the physical-group tags that contain a given model entity.

    Parameters
    ----------
    dim : entity dimension
    tag : model-entity tag
    """
    from apeGmsh.core._compose_errors import raise_if_no_live_kernel
    raise_if_no_live_kernel(
        self._parent, "g.physical.get_groups_for_entity()")
    return list(gmsh.model.getPhysicalGroupsForEntity(dim, tag))

get_name

get_name(dim: int, tag: Tag) -> str

Return the name of a physical group, or "" if unnamed.

Parameters

dim : dimension of the physical group tag : physical-group tag

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def get_name(self, dim: int, tag: Tag) -> str:
    """
    Return the name of a physical group, or ``""`` if unnamed.

    Parameters
    ----------
    dim : dimension of the physical group
    tag : physical-group tag
    """
    from apeGmsh.core._compose_errors import raise_if_no_live_kernel
    raise_if_no_live_kernel(self._parent, "g.physical.get_name()")
    return gmsh.model.getPhysicalName(dim, tag)

get_tag

get_tag(dim: int, name: str) -> Tag | None

Look up the tag of a named physical group.

Returns None if no group with that name and dimension exists.

Parameters

dim : dimension to search name : human-readable label

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def get_tag(self, dim: int, name: str) -> Tag | None:
    """
    Look up the tag of a named physical group.

    Returns ``None`` if no group with that name and dimension exists.

    Parameters
    ----------
    dim  : dimension to search
    name : human-readable label
    """
    from apeGmsh.core._compose_errors import raise_if_no_live_kernel
    raise_if_no_live_kernel(self._parent, "g.physical.get_tag()")
    for _, pg_tag in gmsh.model.getPhysicalGroups(dim=dim):
        pg_name = gmsh.model.getPhysicalName(dim, pg_tag)
        if is_label_pg(pg_name):
            continue
        if pg_name == name:
            return pg_tag
    return None

summary

summary() -> pd.DataFrame

Build a DataFrame describing every user-facing physical group in the model. Internal label PGs are excluded.

Returns

pd.DataFrame indexed by (dim, pg_tag) with columns:

dim entity dimension pg_tag physical-group tag name label (empty string if unnamed) n_entities number of model entities in the group entity_tags comma-separated entity tags as a string

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def summary(self) -> pd.DataFrame:
    """
    Build a DataFrame describing every **user-facing** physical
    group in the model.  Internal label PGs are excluded.

    Returns
    -------
    pd.DataFrame  indexed by ``(dim, pg_tag)`` with columns:

    ``dim``           entity dimension
    ``pg_tag``        physical-group tag
    ``name``          label (empty string if unnamed)
    ``n_entities``    number of model entities in the group
    ``entity_tags``   comma-separated entity tags as a string
    """
    from apeGmsh.core._compose_errors import raise_if_no_live_kernel
    raise_if_no_live_kernel(self._parent, "g.physical.summary()")
    rows: list[dict] = []
    for dim, pg_tag in gmsh.model.getPhysicalGroups():
        name = gmsh.model.getPhysicalName(dim, pg_tag)
        if is_label_pg(name):
            continue
        entities = gmsh.model.getEntitiesForPhysicalGroup(dim, pg_tag)
        rows.append({
            'dim'        : dim,
            'pg_tag'     : pg_tag,
            'name'       : name,
            'n_entities' : len(entities),
            'entity_tags': ", ".join(str(t) for t in entities),
        })

    if not rows:
        return pd.DataFrame(
            columns=['dim', 'pg_tag', 'name', 'n_entities', 'entity_tags']
        )

    df = (
        pd.DataFrame(rows)
        .set_index(['dim', 'pg_tag'])
        .sort_index()
    )

    if self._parent._verbose:
        print("\n--- Physical Groups ---")
        print(df.to_string())

    return df

get_nodes

get_nodes(dim: int, tag: Tag) -> dict

Return the mesh nodes belonging to a physical group.

Must be called after mesh generation.

Parameters

dim : dimension of the physical group tag : physical-group tag

Returns

dict 'tags' : ndarray(N,) — node tags 'coords' : ndarray(N, 3) — XYZ coordinates

Source code in src/apeGmsh/mesh/PhysicalGroups.py
def get_nodes(
    self,
    dim: int,
    tag: Tag,
) -> dict:
    """
    Return the mesh nodes belonging to a physical group.

    Must be called after mesh generation.

    Parameters
    ----------
    dim : dimension of the physical group
    tag : physical-group tag

    Returns
    -------
    dict
        ``'tags'``   : ndarray(N,)   — node tags
        ``'coords'`` : ndarray(N, 3) — XYZ coordinates
    """
    from apeGmsh.core._compose_errors import raise_if_no_live_kernel
    raise_if_no_live_kernel(self._parent, "g.physical.get_nodes()")
    import numpy as np
    node_tags, coords = gmsh.model.mesh.getNodesForPhysicalGroup(dim, tag)
    result = {
        'tags'  : np.array(node_tags, dtype=np.int64),
        'coords': np.array(coords).reshape(-1, 3),
    }
    name = gmsh.model.getPhysicalName(dim, tag) or str(tag)
    self._log(
        f"get_nodes(dim={dim}, pg={name!r}) -> {len(node_tags)} nodes"
    )
    return result

apeGmsh.mesh.MeshSelectionSet.MeshSelectionSet

MeshSelectionSet(parent: '_SessionBase')

Bases: _HasLogging

Post-mesh selection composite — complementary to PhysicalGroups.

Attached to g.mesh_selection by the session framework. Stores sets of mesh node/element IDs, resolved by spatial queries or explicit tag lists. Mirrors the PhysicalGroups API shape so downstream consumers (solvers, FEMData) can treat both identically.

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def __init__(self, parent: "_SessionBase") -> None:
    self._parent = parent
    self._sets: dict[DimTag, dict] = {}
    self._next_tag: dict[int, int] = {0: 1, 1: 1, 2: 1, 3: 1}

add

add(dim: int, tags: list[int], *, name: str = '', tag: int = -1) -> int

Add a mesh selection set from explicit node/element IDs.

Parameters

dim : 0 for node set, 1/2/3 for element set tags : list of node IDs (dim=0) or element IDs (dim>=1) name : optional label tag : explicit tag (auto-allocated if -1)

Returns

int — the allocated set tag

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def add(
    self,
    dim: int,
    tags: list[int],
    *,
    name: str = "",
    tag: int = -1,
) -> int:
    """Add a mesh selection set from explicit node/element IDs.

    Parameters
    ----------
    dim : 0 for node set, 1/2/3 for element set
    tags : list of node IDs (dim=0) or element IDs (dim>=1)
    name : optional label
    tag : explicit tag (auto-allocated if -1)

    Returns
    -------
    int — the allocated set tag
    """
    t = self._alloc_tag(dim, tag)
    all_ids, all_coords = self._get_mesh_nodes()

    if dim == 0:
        # Node set
        arr = np.array(tags, dtype=np.int64)
        mask = np.isin(all_ids, arr)
        self._store_node_set(t, name, all_ids[mask], all_coords[mask])
        self._log(f"add(dim=0, tag={t}, name='{name}') -> {int(mask.sum())} nodes")
    else:
        # Element set
        elem_ids_all, conn_all = self._get_mesh_elements(dim)
        arr = np.array(tags, dtype=np.int64)
        mask = np.isin(elem_ids_all, arr)
        sel_ids = elem_ids_all[mask]
        sel_conn = conn_all[mask]
        # Gather nodes from connectivity
        used = set(int(n) for n in sel_conn.ravel() if n >= 0)
        nmask = np.isin(all_ids, list(used))
        self._store_element_set(
            dim, t, name, sel_ids, sel_conn,
            all_ids[nmask], all_coords[nmask],
        )
        self._log(f"add(dim={dim}, tag={t}, name='{name}') -> "
                   f"{len(sel_ids)} elements, {int(nmask.sum())} nodes")
    return t

get_entities

get_entities(dim: int, tag: int) -> list[int]

Return node IDs (dim=0) or element IDs (dim>=1).

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def get_entities(self, dim: int, tag: int) -> list[int]:
    """Return node IDs (dim=0) or element IDs (dim>=1)."""
    info = self._sets.get((dim, tag))
    if info is None:
        raise KeyError(f"No mesh selection (dim={dim}, tag={tag})")
    if dim == 0:
        return list(int(n) for n in info["node_ids"])
    return list(int(e) for e in info["element_ids"])

get_nodes

get_nodes(dim: int, tag: int) -> dict

Return {'tags': ndarray, 'coords': ndarray(N,3)}.

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def get_nodes(self, dim: int, tag: int) -> dict:
    """Return ``{'tags': ndarray, 'coords': ndarray(N,3)}``."""
    info = self._sets.get((dim, tag))
    if info is None:
        raise KeyError(f"No mesh selection (dim={dim}, tag={tag})")
    return {
        "tags": np.asarray(info["node_ids"]).astype(object),
        "coords": np.asarray(info["node_coords"], dtype=np.float64),
    }

get_elements

get_elements(dim: int, tag: int) -> dict

Return {'element_ids': ndarray, 'connectivity': ndarray(E,npe)}.

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def get_elements(self, dim: int, tag: int) -> dict:
    """Return ``{'element_ids': ndarray, 'connectivity': ndarray(E,npe)}``."""
    info = self._sets.get((dim, tag))
    if info is None:
        raise KeyError(f"No mesh selection (dim={dim}, tag={tag})")
    eids = info.get("element_ids")
    conn = info.get("connectivity")
    if eids is None or conn is None:
        name = info.get("name", f"(dim={dim}, tag={tag})")
        raise ValueError(
            f"Mesh selection '{name}' has no element data. "
            f"Element data is only available for dim >= 1 sets."
        )
    return {
        "element_ids": np.asarray(eids).astype(object),
        "connectivity": np.asarray(conn).astype(object),
    }

union

union(dim: int, tag_a: int, tag_b: int, *, name: str = '', tag: int = -1) -> int

Create a new set = A ∪ B.

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def union(
    self, dim: int, tag_a: int, tag_b: int,
    *, name: str = "", tag: int = -1,
) -> int:
    """Create a new set = A ∪ B."""
    a = self._sets.get((dim, tag_a))
    b = self._sets.get((dim, tag_b))
    if a is None or b is None:
        raise KeyError(f"Set (dim={dim}, tag={tag_a}) or tag={tag_b} not found")
    if dim == 0:
        ids = np.union1d(a["node_ids"], b["node_ids"])
        return self.add(0, list(ids), name=name, tag=tag)
    ids = np.union1d(a["element_ids"], b["element_ids"])
    return self.add(dim, list(ids), name=name, tag=tag)

intersection

intersection(dim: int, tag_a: int, tag_b: int, *, name: str = '', tag: int = -1) -> int

Create a new set = A ∩ B.

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def intersection(
    self, dim: int, tag_a: int, tag_b: int,
    *, name: str = "", tag: int = -1,
) -> int:
    """Create a new set = A ∩ B."""
    a = self._sets.get((dim, tag_a))
    b = self._sets.get((dim, tag_b))
    if a is None or b is None:
        raise KeyError(f"Set (dim={dim}, tag={tag_a}) or tag={tag_b} not found")
    if dim == 0:
        ids = np.intersect1d(a["node_ids"], b["node_ids"])
        return self.add(0, list(ids), name=name, tag=tag)
    ids = np.intersect1d(a["element_ids"], b["element_ids"])
    return self.add(dim, list(ids), name=name, tag=tag)

difference

difference(dim: int, tag_a: int, tag_b: int, *, name: str = '', tag: int = -1) -> int

Create a new set = A \ B.

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def difference(
    self, dim: int, tag_a: int, tag_b: int,
    *, name: str = "", tag: int = -1,
) -> int:
    """Create a new set = A \\ B."""
    a = self._sets.get((dim, tag_a))
    b = self._sets.get((dim, tag_b))
    if a is None or b is None:
        raise KeyError(f"Set (dim={dim}, tag={tag_a}) or tag={tag_b} not found")
    if dim == 0:
        ids = np.setdiff1d(a["node_ids"], b["node_ids"])
        return self.add(0, list(ids), name=name, tag=tag)
    ids = np.setdiff1d(a["element_ids"], b["element_ids"])
    return self.add(dim, list(ids), name=name, tag=tag)

from_physical

from_physical(dim: int, name_or_tag: str | int, *, ms_name: str = '', ms_tag: int = -1) -> int

Import a physical group as a mesh selection set.

Parameters

dim : dimension of the physical group name_or_tag : physical group name or tag ms_name : name for the new mesh selection ms_tag : tag for the new mesh selection (-1 = auto)

Returns

int — mesh selection tag

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def from_physical(
    self, dim: int, name_or_tag: str | int,
    *, ms_name: str = "", ms_tag: int = -1,
) -> int:
    """Import a physical group as a mesh selection set.

    Parameters
    ----------
    dim : dimension of the physical group
    name_or_tag : physical group name or tag
    ms_name : name for the new mesh selection
    ms_tag : tag for the new mesh selection (-1 = auto)

    Returns
    -------
    int — mesh selection tag
    """
    # Resolve name -> tag
    if isinstance(name_or_tag, str):
        for pg_dim, pg_tag in gmsh.model.getPhysicalGroups(dim):
            try:
                if gmsh.model.getPhysicalName(pg_dim, pg_tag) == name_or_tag:
                    name_or_tag = pg_tag
                    break
            except Exception:
                pass
        else:
            raise KeyError(f"Physical group '{name_or_tag}' not found at dim={dim}")

    pg_tag = int(name_or_tag)
    node_tags, coords = gmsh.model.mesh.getNodesForPhysicalGroup(dim, pg_tag)
    t = self._alloc_tag(0, ms_tag)
    nids = np.asarray(node_tags, dtype=np.int64)
    ncoords = np.asarray(coords, dtype=np.float64).reshape(-1, 3)
    label = ms_name or gmsh.model.getPhysicalName(dim, pg_tag)
    self._store_node_set(t, label, nids, ncoords)
    self._log(f"from_physical(dim={dim}, pg_tag={pg_tag}) -> "
               f"node set tag={t}, {len(nids)} nodes")
    return t

filter_set

filter_set(dim: int, tag: int, *, name: str = '', new_tag: int = -1, on_plane: tuple | None = None, in_box: tuple | list | None = None, in_sphere: tuple | None = None, closest_to: tuple | None = None, count: int = 1, predicate: Callable[[ndarray], ndarray] | None = None, inclusive: bool = False) -> int

Refine an existing set with spatial filters -> create a new set.

For node sets, filters apply to node coordinates. For element sets, filters apply to element centroids. All filters are AND-combined.

Parameters

dim, tag : source set identifier name, new_tag : identifier for the resulting set on_plane, in_box, in_sphere, closest_to, predicate : same semantics as :meth:add_nodes (in_box is half-open on the upper side by default) inclusive : if True, in_box uses a closed upper bound (restores the pre-S2 closed-closed behavior).

Returns

int — tag of the new (filtered) set

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def filter_set(
    self,
    dim: int,
    tag: int,
    *,
    name: str = "",
    new_tag: int = -1,
    on_plane: tuple | None = None,
    in_box: tuple | list | None = None,
    in_sphere: tuple | None = None,
    closest_to: tuple | None = None,
    count: int = 1,
    predicate: Callable[[np.ndarray], np.ndarray] | None = None,
    inclusive: bool = False,
) -> int:
    """Refine an existing set with spatial filters -> create a new set.

    For node sets, filters apply to node coordinates.  For element
    sets, filters apply to element centroids.  All filters are
    AND-combined.

    Parameters
    ----------
    dim, tag : source set identifier
    name, new_tag : identifier for the resulting set
    on_plane, in_box, in_sphere, closest_to, predicate :
        same semantics as :meth:`add_nodes` (``in_box`` is half-open
        on the upper side by default)
    inclusive : if True, ``in_box`` uses a closed upper bound
        (restores the pre-S2 closed-closed behavior).

    Returns
    -------
    int — tag of the new (filtered) set
    """
    info = self._sets.get((dim, tag))
    if info is None:
        raise KeyError(
            f"No mesh selection (dim={dim}, tag={tag}). "
            f"Available: {self.get_all()}"
        )

    if dim == 0:
        ids = np.asarray(info["node_ids"])
        coords = np.asarray(info["node_coords"], dtype=np.float64)
    else:
        # Element set: centroids drive the filtering
        elem_ids = np.asarray(info.get("element_ids", []))
        conn = np.asarray(info.get("connectivity"))
        node_ids, node_coords = self._get_mesh_nodes()
        id_to_idx = {int(n): i for i, n in enumerate(node_ids)}
        coords = _flt.element_centroids(conn, id_to_idx, node_coords)
        ids = elem_ids

    mask = np.ones(len(ids), dtype=bool)
    if on_plane is not None:
        axis, value = on_plane[0], on_plane[1]
        atol = on_plane[2] if len(on_plane) > 2 else 1e-6
        mask &= _flt.nodes_on_plane(coords, axis, value, atol)
    if in_box is not None:
        mask &= _flt.nodes_in_box(coords, in_box, inclusive=inclusive)
    if in_sphere is not None:
        cx, cy, cz, r = in_sphere
        mask &= _flt.nodes_in_sphere(coords, (cx, cy, cz), r)
    if closest_to is not None:
        mask &= _flt.nodes_nearest(coords, closest_to, count)
    if predicate is not None:
        mask &= predicate(coords)

    t = self._alloc_tag(dim, new_tag)
    if dim == 0:
        self._store_node_set(t, name, ids[mask], coords[mask])
    else:
        self._sets[(dim, t)] = {
            "name": name,
            "node_ids": np.unique(conn[mask].ravel()),
            "node_coords": np.array([], dtype=np.float64).reshape(0, 3),
            "element_ids": ids[mask],
            "connectivity": conn[mask],
        }
    self._log(
        f"filter_set(dim={dim}, src={tag}) -> tag={t}, "
        f"{int(mask.sum())}/{len(mask)} kept"
    )
    return t

select

select(*, level: str = 'node', dim: int = 2, ids=None, name=None)

Build a named mesh-selection set from the live mesh.

Returns a :class:~apeGmsh.mesh._mesh_selection.MeshSelection (point family) that chains spatial-refinement verbs. Finish with .save_as(name) to register the result — it then snapshots into FEMData and round-trips via HDF5::

# capture all base nodes and persist
(g.mesh_selection.select()
     .in_box((0, 0, 0), (10, 10, 0.01))
     .on_plane((0, 0, 0), (0, 0, 1), tol=1e-6)
     .save_as("base_nodes"))

# 3-D solid elements in a region (dim=3 required for tets)
(g.mesh_selection.select(level="element", dim=3)
     .in_box(lo, hi)
     .save_as("core_tets"))

# narrow an existing set further
g.mesh_selection.select(name="base_nodes").in_sphere(c, r)

# set algebra
g.mesh_selection.select(ids=a) | g.mesh_selection.select(ids=b)

No arguments seeds the full live-mesh universe for the chosen level and dim.

Parameters

level : "node" | "element" "node" (default) — atoms are node ids. "element" — atoms are element ids for dim. dim : Element dimension (1=line, 2=surface, 3=volume) used when level="element" (ignored for nodes). Defaults to 2 (surface elements) — pass dim=3 for solid/tet meshes. ids : Explicit id list to seed from. Mutually exclusive with name=. name : Name of an existing g.mesh_selection set to seed from. Node ids are used for level="node", element ids for level="element". Mutually exclusive with ids=. An unknown name raises.

Refining verbs

Each returns a new MeshSelection and composes freely:

  • .in_box(lo, hi, *, inclusive=False)
  • .in_sphere(center, radius)
  • .on_plane(point, normal, *, tol)tol= is required; raises TypeError if omitted.
  • .nearest_to(point, *, count=1)
  • .where(predicate)
  • | & - ^ (set algebra).
Terminals
  • .idslist[int].
  • .coordsndarray (N, 3).
  • .save_as(name) — registers the narrowed id set as a named mesh-selection, so it snapshots into FEMData and round-trips via HDF5 as selection=.
Notes

To seed from a physical group or label, register it first::

g.mesh_selection.from_physical(2, "BottomFace",
                               ms_name="bottom")
g.mesh_selection.select(name="bottom").in_sphere(c, r)
Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def select(
    self,
    *,
    level: str = "node",
    dim: int = 2,
    ids=None,
    name=None,
):
    """Build a named mesh-selection set from the live mesh.

    Returns a :class:`~apeGmsh.mesh._mesh_selection.MeshSelection`
    (point family) that chains spatial-refinement verbs.  Finish
    with ``.save_as(name)`` to register the result — it then
    snapshots into ``FEMData`` and round-trips via HDF5::

        # capture all base nodes and persist
        (g.mesh_selection.select()
             .in_box((0, 0, 0), (10, 10, 0.01))
             .on_plane((0, 0, 0), (0, 0, 1), tol=1e-6)
             .save_as("base_nodes"))

        # 3-D solid elements in a region (dim=3 required for tets)
        (g.mesh_selection.select(level="element", dim=3)
             .in_box(lo, hi)
             .save_as("core_tets"))

        # narrow an existing set further
        g.mesh_selection.select(name="base_nodes").in_sphere(c, r)

        # set algebra
        g.mesh_selection.select(ids=a) | g.mesh_selection.select(ids=b)

    No arguments seeds the full live-mesh universe for the chosen
    ``level`` and ``dim``.

    Parameters
    ----------
    level : ``"node"`` | ``"element"``
        ``"node"`` (default) — atoms are node ids.
        ``"element"`` — atoms are element ids for ``dim``.
    dim :
        Element dimension (1=line, 2=surface, **3=volume**) used
        when ``level="element"`` (ignored for nodes).
        Defaults to 2 (surface elements) — **pass** ``dim=3``
        **for solid/tet meshes.**
    ids :
        Explicit id list to seed from.  Mutually exclusive
        with ``name=``.
    name :
        Name of an **existing** ``g.mesh_selection`` set to
        seed from.  Node ids are used for ``level="node"``,
        element ids for ``level="element"``.  Mutually
        exclusive with ``ids=``.  An unknown name raises.

    Refining verbs
    --------------
    Each returns a new ``MeshSelection`` and composes freely:

    - ``.in_box(lo, hi, *, inclusive=False)``
    - ``.in_sphere(center, radius)``
    - ``.on_plane(point, normal, *, tol)`` — ``tol=`` is
      **required**; raises ``TypeError`` if omitted.
    - ``.nearest_to(point, *, count=1)``
    - ``.where(predicate)``
    - ``|`` ``&`` ``-`` ``^`` (set algebra).

    Terminals
    ---------
    - ``.ids`` — ``list[int]``.
    - ``.coords`` — ``ndarray (N, 3)``.
    - ``.save_as(name)`` — registers the narrowed id set as a
      named mesh-selection, so it snapshots into ``FEMData``
      and round-trips via HDF5 as ``selection=``.

    Notes
    -----
    To seed from a physical group or label, register it first::

        g.mesh_selection.from_physical(2, "BottomFace",
                                       ms_name="bottom")
        g.mesh_selection.select(name="bottom").in_sphere(c, r)
    """
    # selection-unification-v2 P2-I (§6.1 STOP-2): return the v2
    # terminal ``MeshSelection`` (legacy ``MeshSelectionChain`` left
    # defined-but-unwired; P3 deletes it).  ``engine_for`` is kept
    # verbatim — the per-``(level, dim)`` memoised ``_LiveMeshEngine``
    # singleton is what ``SelectionChain._compatible`` gates
    # set-algebra identity on, and ``MeshSelection`` delegates the
    # per-engine live-mesh read back to a ``MeshSelectionChain``
    # built from this exact same engine (byte-faithful).  Same
    # deferred-import idiom; ``_mesh_selection`` imports only the
    # package-root leaf ``_kernel.chain`` at load (one declared
    # downward BASELINE triple; no new eager cross-package edge).
    from ._live_engine import engine_for            # deferred — plan §3
    from ._mesh_selection import MeshSelection      # deferred — plan §3

    if level not in ("node", "element"):
        raise ValueError(
            f"select(level=) must be 'node' or 'element', "
            f"got {level!r}."
        )
    if ids is not None and name is not None:
        raise ValueError(
            "select(ids=, name=) are mutually exclusive — pass an "
            "explicit id list OR an existing selection-set name, "
            "not both."
        )

    if level == "node":
        eng = engine_for(self, "node", 0)
        if ids is not None:
            atoms = [int(n) for n in ids]
        elif name is not None:
            atoms = [int(n) for n in self._seed_ids_by_name(0, name)]
        else:
            all_ids, _ = self._get_mesh_nodes()
            atoms = [int(n) for n in all_ids]
    else:
        d = int(dim)
        eng = engine_for(self, "element", d)
        if ids is not None:
            atoms = [int(e) for e in ids]
        elif name is not None:
            atoms = [int(e) for e in self._seed_ids_by_name(d, name)]
        else:
            elem_ids, _ = self._get_mesh_elements(d)
            atoms = [int(e) for e in elem_ids]

    return MeshSelection(atoms, _engine=eng)

sort_set

sort_set(dim: int, tag: int, *, by: str = 'x', descending: bool = False) -> None

Sort the entries of a set in place by coordinate axis.

Parameters

dim, tag : set identifier by : "x", "y", or "z" — axis to sort along descending : reverse the order

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def sort_set(
    self,
    dim: int,
    tag: int,
    *,
    by: str = "x",
    descending: bool = False,
) -> None:
    """Sort the entries of a set in place by coordinate axis.

    Parameters
    ----------
    dim, tag : set identifier
    by : "x", "y", or "z" — axis to sort along
    descending : reverse the order
    """
    info = self._sets.get((dim, tag))
    if info is None:
        raise KeyError(f"No mesh selection (dim={dim}, tag={tag}).")

    axis_idx = {"x": 0, "y": 1, "z": 2}[by.lower()]

    if dim == 0:
        coords = np.asarray(info["node_coords"], dtype=np.float64)
        order = np.argsort(coords[:, axis_idx])
        if descending:
            order = order[::-1]
        info["node_ids"] = np.asarray(info["node_ids"])[order]
        info["node_coords"] = coords[order]
    else:
        conn = np.asarray(info.get("connectivity"))
        elem_ids = np.asarray(info.get("element_ids", []))
        node_ids, node_coords = self._get_mesh_nodes()
        id_to_idx = {int(n): i for i, n in enumerate(node_ids)}
        cents = _flt.element_centroids(conn, id_to_idx, node_coords)
        order = np.argsort(cents[:, axis_idx])
        if descending:
            order = order[::-1]
        info["element_ids"] = elem_ids[order]
        info["connectivity"] = conn[order]

summary

summary() -> pd.DataFrame

DataFrame describing all mesh selection sets.

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def summary(self) -> pd.DataFrame:
    """DataFrame describing all mesh selection sets."""
    rows: list[dict] = []
    for (dim, t), info in sorted(self._sets.items()):
        eids = info.get("element_ids")
        rows.append({
            "dim": dim,
            "tag": t,
            "name": info.get("name", ""),
            "n_nodes": len(info["node_ids"]),
            "n_elems": len(eids) if eids is not None else 0,
        })
    if not rows:
        return pd.DataFrame(columns=["dim", "tag", "name", "n_nodes", "n_elems"])
    return pd.DataFrame(rows).set_index(["dim", "tag"]).sort_index()

to_dataframe

to_dataframe(dim: int, tag: int) -> pd.DataFrame

Return a DataFrame of the entries of a single set.

For dim=0 (node set): columns [node_id, x, y, z]. For dim>0 (element set): columns [element_id, cx, cy, cz, n_nodes].

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def to_dataframe(self, dim: int, tag: int) -> pd.DataFrame:
    """Return a DataFrame of the entries of a single set.

    For dim=0 (node set): columns ``[node_id, x, y, z]``.
    For dim>0 (element set): columns ``[element_id, cx, cy, cz, n_nodes]``.
    """
    info = self._sets.get((dim, tag))
    if info is None:
        raise KeyError(
            f"No mesh selection (dim={dim}, tag={tag}). "
            f"Available: {self.get_all()}"
        )
    if dim == 0:
        return pd.DataFrame({
            "node_id": np.asarray(info["node_ids"]),
            "x": info["node_coords"][:, 0],
            "y": info["node_coords"][:, 1],
            "z": info["node_coords"][:, 2],
        })
    elem_ids = np.asarray(info.get("element_ids", []))
    conn = np.asarray(info.get("connectivity"))
    node_ids, node_coords = self._get_mesh_nodes()
    id_to_idx = {int(n): i for i, n in enumerate(node_ids)}
    cents = _flt.element_centroids(conn, id_to_idx, node_coords)
    return pd.DataFrame({
        "element_id": elem_ids,
        "cx": cents[:, 0],
        "cy": cents[:, 1],
        "cz": cents[:, 2],
        "n_nodes": [conn.shape[1]] * len(elem_ids),
    })

apeGmsh.mesh.MeshSelectionSet.MeshSelectionStore

MeshSelectionStore(sets: dict[DimTag, dict])

Immutable snapshot of mesh selections captured at get_fem_data() time.

Accessed via fem.mesh_selection. Mirrors the query API of :class:MeshSelectionSet and :class:PhysicalGroupSet.

Example

::

fem = g.mesh.queries.get_fem_data(dim=2)
fem.mesh_selection.get_all()
fem.mesh_selection.get_nodes(0, 1)
fem.mesh_selection.get_elements(2, 1)
fem.mesh_selection.summary()
Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def __init__(self, sets: dict[DimTag, dict]) -> None:
    self._sets = sets

get_nodes

get_nodes(dim: int, tag: int) -> dict

Return {'tags': ndarray, 'coords': ndarray(N,3)}.

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def get_nodes(self, dim: int, tag: int) -> dict:
    """Return ``{'tags': ndarray, 'coords': ndarray(N,3)}``."""
    info = self._sets.get((dim, tag))
    if info is None:
        raise KeyError(
            f"No mesh selection (dim={dim}, tag={tag}). "
            f"Available: {self.get_all()}"
        )
    return {
        "tags": np.asarray(info["node_ids"]).astype(object),
        "coords": np.asarray(info["node_coords"], dtype=np.float64),
    }

get_elements

get_elements(dim: int, tag: int) -> dict

Return {'element_ids': ndarray, 'connectivity': ndarray(E,npe)}.

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def get_elements(self, dim: int, tag: int) -> dict:
    """Return ``{'element_ids': ndarray, 'connectivity': ndarray(E,npe)}``."""
    info = self._sets.get((dim, tag))
    if info is None:
        raise KeyError(
            f"No mesh selection (dim={dim}, tag={tag}). "
            f"Available: {self.get_all()}"
        )
    eids = info.get("element_ids")
    conn = info.get("connectivity")
    if eids is None or conn is None:
        name = info.get("name", f"(dim={dim}, tag={tag})")
        raise ValueError(
            f"Mesh selection '{name}' has no element data. "
            f"Element data is only available for dim >= 1 sets."
        )
    return {
        "element_ids": np.asarray(eids).astype(object),
        "connectivity": np.asarray(conn).astype(object),
    }

names

names(dim: int = -1) -> list[str]

All non-empty selection names, optionally filtered by dim.

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def names(self, dim: int = -1) -> list[str]:
    """All non-empty selection names, optionally filtered by dim."""
    out = []
    for (d, _), info in sorted(self._sets.items()):
        n = info.get("name", "")
        if n and (dim == -1 or d == dim):
            out.append(n)
    return out

node_ids

node_ids(name: str) -> np.ndarray

Node IDs for any selection matching name (any dim).

For dim=0 selections returns the explicit node IDs. For dim>=1 selections returns the node IDs implied by the elements' connectivity.

Raises KeyError if no selection matches name.

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def node_ids(self, name: str) -> np.ndarray:
    """Node IDs for any selection matching ``name`` (any dim).

    For ``dim=0`` selections returns the explicit node IDs.
    For ``dim>=1`` selections returns the node IDs implied by the
    elements' connectivity.

    Raises ``KeyError`` if no selection matches ``name``.
    """
    for (dim, tag), info in self._sets.items():
        if info.get("name", "") == name:
            return np.asarray(info["node_ids"], dtype=np.int64)
    raise KeyError(
        f"No mesh selection named {name!r}. "
        f"Available: {self.names()}"
    )

element_ids

element_ids(name: str) -> np.ndarray

Element IDs for an element-bearing selection matching name.

Searches dim>=1 selections only (dim=0 selections have no element data).

Raises KeyError if no element-bearing selection matches.

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def element_ids(self, name: str) -> np.ndarray:
    """Element IDs for an element-bearing selection matching ``name``.

    Searches ``dim>=1`` selections only (``dim=0`` selections have
    no element data).

    Raises ``KeyError`` if no element-bearing selection matches.
    """
    for (dim, tag), info in self._sets.items():
        if dim < 1:
            continue
        if info.get("name", "") == name:
            eids = info.get("element_ids")
            if eids is None:
                continue
            return np.asarray(eids, dtype=np.int64)
    raise KeyError(
        f"No element-bearing mesh selection named {name!r}. "
        f"Available (dim>=1): "
        f"{self.names(1) + self.names(2) + self.names(3)}"
    )

apeGmsh.mesh.MshLoader.MshLoader

MshLoader(parent: '_SessionBase | None' = None)

Bases: _HasLogging

Load .msh files and produce solver-ready :class:FEMData.

Can be used standalone via the :meth:load classmethod, or as a composite on a apeGmsh / Assembly session via g.loader.

Parameters

parent : _SessionBase or None The owning session when used as a composite. None when used standalone.

Source code in src/apeGmsh/mesh/MshLoader.py
def __init__(self, parent: "_SessionBase | None" = None) -> None:
    self._parent = parent

load classmethod

load(path: str | Path, *, dim: int = 2, verbose: bool = False) -> 'FEMData'

Load a .msh file and return a :class:FEMData.

Manages its own Gmsh session internally — no apeGmsh instance, no begin()/end() needed. Supports MSH2 and MSH4 formats.

Parameters

path : str or Path Path to the .msh file. dim : int Element dimension to extract (1 = lines, 2 = tri/quad, 3 = tet/hex). Default is 2. verbose : bool Print a summary of what was loaded.

Returns

FEMData Self-contained solver-ready mesh data with physical groups, mesh statistics, and connectivity.

Example

::

from apeGmsh import MshLoader, Numberer

fem = MshLoader.load("bridge.msh", dim=2)

print(fem.info)
print(fem.physical.summary())

numb = Numberer(fem)
data = numb.renumber(method="rcm")
Source code in src/apeGmsh/mesh/MshLoader.py
@classmethod
def load(
    cls,
    path: str | Path,
    *,
    dim: int = 2,
    verbose: bool = False,
) -> "FEMData":
    """
    Load a ``.msh`` file and return a :class:`FEMData`.

    Manages its own Gmsh session internally — no ``apeGmsh``
    instance, no ``begin()``/``end()`` needed.  Supports MSH2
    and MSH4 formats.

    Parameters
    ----------
    path : str or Path
        Path to the ``.msh`` file.
    dim : int
        Element dimension to extract (1 = lines, 2 = tri/quad,
        3 = tet/hex).  Default is 2.
    verbose : bool
        Print a summary of what was loaded.

    Returns
    -------
    FEMData
        Self-contained solver-ready mesh data with physical
        groups, mesh statistics, and connectivity.

    Example
    -------
    ::

        from apeGmsh import MshLoader, Numberer

        fem = MshLoader.load("bridge.msh", dim=2)

        print(fem.info)
        print(fem.physical.summary())

        numb = Numberer(fem)
        data = numb.renumber(method="rcm")
    """
    from .FEMData import FEMData

    p = cls._validate_path(path)
    fem = FEMData.from_msh(str(p), dim=dim)

    cls._log_fem(fem, f"load({p.name!r})", verbose)
    return fem

from_msh

from_msh(path: str | Path, *, dim: int = 2) -> 'FEMData'

Load a .msh file into the active Gmsh session.

The mesh is merged via gmsh.merge(), so all composites (g.physical, g.plot, g.inspect, etc.) remain usable afterwards.

Parameters

path : str or Path Path to the .msh file. dim : int Element dimension to extract. Default is 2.

Returns

FEMData Self-contained solver-ready mesh data.

Raises

FileNotFoundError If path does not exist. RuntimeError If no Gmsh session is active (call g.begin() first).

Example

::

g = apeGmsh(model_name="imported")
g.begin()

fem = g.loader.from_msh("model.msh", dim=2)
print(fem.physical.summary())

g.end()
Source code in src/apeGmsh/mesh/MshLoader.py
def from_msh(
    self,
    path: str | Path,
    *,
    dim: int = 2,
) -> "FEMData":
    """
    Load a ``.msh`` file into the **active** Gmsh session.

    The mesh is merged via ``gmsh.merge()``, so all composites
    (``g.physical``, ``g.plot``, ``g.inspect``, etc.) remain
    usable afterwards.

    Parameters
    ----------
    path : str or Path
        Path to the ``.msh`` file.
    dim : int
        Element dimension to extract.  Default is 2.

    Returns
    -------
    FEMData
        Self-contained solver-ready mesh data.

    Raises
    ------
    FileNotFoundError
        If *path* does not exist.
    RuntimeError
        If no Gmsh session is active (call ``g.begin()`` first).

    Example
    -------
    ::

        g = apeGmsh(model_name="imported")
        g.begin()

        fem = g.loader.from_msh("model.msh", dim=2)
        print(fem.physical.summary())

        g.end()
    """
    from .FEMData import FEMData

    p = self._validate_path(path)

    if self._parent is None or not self._parent.is_active:
        raise RuntimeError(
            "No active Gmsh session. Call g.begin() first, "
            "or use MshLoader.load() for standalone loading."
        )

    self._log(f"merging {p.name} ...")
    gmsh.merge(str(p))

    fem = FEMData.from_gmsh(dim=dim)

    verbose = self._parent._verbose if self._parent else False
    self._log_fem(fem, f"from_msh({p.name!r})", verbose)

    return fem

Legacy

Partition below is the standalone, pre-composite class. New code should use the live g.mesh.partitioning composite documented above.

apeGmsh.mesh.Partition.Partition

Partition(parent: SessionProtocol)

Bases: _HasLogging

Mesh-partitioning composite attached to a apeGmsh instance as g.partition.

Wraps gmsh.model.mesh.partition / unpartition and the partition-aware model queries exposed after partitioning.

Workflow

::

with apeGmsh(model_name="Cube") as g:
    # build and mesh
    g.model.geometry.add_box(0, 0, 0, 1, 1, 1)
    g.model.sync()
    g.mesh.generation.generate(3)

    # auto-partition into 4 parts
    g.partition.auto(4)

    # inspect
    print(g.partition.summary())
    df = g.partition.entity_table()

    # save — one combined file or one per partition
    g.partition.save("cube_part.msh")
    g.partition.save("cube", one_file_per_partition=True)

Or with an explicit element -> partition assignment::

    elem_tags  = [1, 2, 3, 4, 5, 6]
    part_ids   = [1, 1, 2, 2, 3, 4]
    g.partition.explicit(4, element_tags=elem_tags,
                            partitions=part_ids)

Parameters

parent : _SessionBase The owning instance.

Source code in src/apeGmsh/mesh/Partition.py
def __init__(self, parent: _SessionBase) -> None:
    self._parent = parent

auto

auto(n_parts: int) -> Partition

Partition the current mesh into n_parts sub-domains using Gmsh's built-in partitioner (Metis when available).

Must be called after g.mesh.generation.generate().

Parameters

n_parts : number of partitions (≥ 1)

Returns

self — for chaining

Source code in src/apeGmsh/mesh/Partition.py
def auto(self, n_parts: int) -> Partition:
    """
    Partition the current mesh into *n_parts* sub-domains using
    Gmsh's built-in partitioner (Metis when available).

    Must be called after ``g.mesh.generation.generate()``.

    Parameters
    ----------
    n_parts : number of partitions (≥ 1)

    Returns
    -------
    self — for chaining
    """
    if n_parts < 1:
        raise ValueError(f"auto(): n_parts must be ≥ 1, got {n_parts}")
    gmsh.model.mesh.partition(n_parts)
    self._log(f"auto(n_parts={n_parts})")
    return self

explicit

explicit(n_parts: int, *, element_tags: list[int], partitions: list[int]) -> Partition

Partition the mesh with an explicit per-element assignment.

Parameters

n_parts : total number of partitions declared element_tags : list of element tags to assign partitions : parallel list of partition IDs (1-based) for each element in element_tags

Returns

self — for chaining

Example

::

g.partition.explicit(
    2,
    element_tags=[1, 2, 3, 4],
    partitions  =[1, 1, 2, 2],
)
Source code in src/apeGmsh/mesh/Partition.py
def explicit(
    self,
    n_parts     : int,
    *,
    element_tags: list[int],
    partitions  : list[int],
) -> Partition:
    """
    Partition the mesh with an explicit per-element assignment.

    Parameters
    ----------
    n_parts      : total number of partitions declared
    element_tags : list of element tags to assign
    partitions   : parallel list of partition IDs (1-based) for each
                   element in *element_tags*

    Returns
    -------
    self — for chaining

    Example
    -------
    ::

        g.partition.explicit(
            2,
            element_tags=[1, 2, 3, 4],
            partitions  =[1, 1, 2, 2],
        )
    """
    if len(element_tags) != len(partitions):
        raise ValueError(
            f"explicit(): len(element_tags)={len(element_tags)} != "
            f"len(partitions)={len(partitions)}"
        )
    gmsh.model.mesh.partition(
        n_parts,
        elementTags=element_tags,
        partitions=partitions,
    )
    self._log(
        f"explicit(n_parts={n_parts}, "
        f"n_elements={len(element_tags)})"
    )
    return self

unpartition

unpartition() -> Partition

Remove the partition structure and restore a monolithic mesh.

Returns

self — for chaining

Source code in src/apeGmsh/mesh/Partition.py
def unpartition(self) -> Partition:
    """
    Remove the partition structure and restore a monolithic mesh.

    Returns
    -------
    self — for chaining
    """
    gmsh.model.mesh.unpartition()
    self._log("unpartition()")
    return self

renumber

renumber(*, method: str = 'RCMK', element_tags: list[int] | None = None) -> Partition

Compute and apply a node renumbering (e.g. RCMK bandwidth reduction) — commonly run after partitioning to improve solver performance.

Parameters

method : renumbering algorithm passed to gmsh.model.mesh.computeRenumbering. Supported values: "RCMK" (bandwidth reduction), "Hilbert" (space-filling curve, good cache locality), "Metis" (graph-partitioner ordering). "RCMK" is the most commonly used default. element_tags : restrict to a subset of elements (None = all)

Returns

self — for chaining

Source code in src/apeGmsh/mesh/Partition.py
def renumber(
    self,
    *,
    method      : str            = "RCMK",
    element_tags: list[int] | None = None,
) -> Partition:
    """
    Compute and apply a node renumbering (e.g. RCMK bandwidth
    reduction) — commonly run after partitioning to improve solver
    performance.

    Parameters
    ----------
    method       : renumbering algorithm passed to
                   ``gmsh.model.mesh.computeRenumbering``.
                   Supported values: ``"RCMK"`` (bandwidth reduction),
                   ``"Hilbert"`` (space-filling curve, good cache
                   locality), ``"Metis"`` (graph-partitioner ordering).
                   ``"RCMK"`` is the most commonly used default.
    element_tags : restrict to a subset of elements (``None`` = all)

    Returns
    -------
    self — for chaining
    """
    old_tags, new_tags = gmsh.model.mesh.computeRenumbering(
        method=method,
        elementTags=element_tags or [],
    )
    gmsh.model.mesh.renumberNodes(
        oldTags=list(old_tags),
        newTags=list(new_tags),
    )
    self._log(
        f"renumber(method={method!r}, "
        f"n_nodes={len(old_tags)})"
    )
    return self

n_partitions

n_partitions() -> int

Return the current number of partitions (0 if not partitioned).

Source code in src/apeGmsh/mesh/Partition.py
def n_partitions(self) -> int:
    """Return the current number of partitions (0 if not partitioned)."""
    return gmsh.model.getNumberOfPartitions()

get_partitions

get_partitions(dim: int, tag: Tag) -> list[int]

Return the partition IDs that contain a given model entity.

Parameters

dim : entity dimension tag : entity tag

Returns

list[int] partition IDs (empty for non-partitioned entities)

Source code in src/apeGmsh/mesh/Partition.py
def get_partitions(self, dim: int, tag: Tag) -> list[int]:
    """
    Return the partition IDs that contain a given model entity.

    Parameters
    ----------
    dim : entity dimension
    tag : entity tag

    Returns
    -------
    list[int]  partition IDs (empty for non-partitioned entities)
    """
    return list(gmsh.model.getPartitions(dim, tag))

get_parent

get_parent(dim: int, tag: Tag) -> DimTag

Return the (dim, tag) of the parent entity of a partitioned sub-entity.

Parameters

dim : dimension of the partitioned entity tag : tag of the partitioned entity

Source code in src/apeGmsh/mesh/Partition.py
def get_parent(self, dim: int, tag: Tag) -> DimTag:
    """
    Return the ``(dim, tag)`` of the parent entity of a partitioned
    sub-entity.

    Parameters
    ----------
    dim : dimension of the partitioned entity
    tag : tag of the partitioned entity
    """
    p_dim, p_tag = gmsh.model.getParent(dim, tag)
    return (p_dim, p_tag)

entity_table

entity_table(dim: int = -1) -> pd.DataFrame

Build a DataFrame of all model entities and their partition membership.

Parameters

dim : restrict to a single dimension (-1 = all)

Returns

pd.DataFrame with columns:

dim entity dimension tag entity tag partitions comma-separated partition IDs (empty string = unpartitioned) parent_dim parent entity dimension (-1 if top-level) parent_tag parent entity tag (-1 if top-level)

Source code in src/apeGmsh/mesh/Partition.py
def entity_table(self, dim: int = -1) -> pd.DataFrame:
    """
    Build a DataFrame of all model entities and their partition
    membership.

    Parameters
    ----------
    dim : restrict to a single dimension (``-1`` = all)

    Returns
    -------
    pd.DataFrame  with columns:

    ``dim``         entity dimension
    ``tag``         entity tag
    ``partitions``  comma-separated partition IDs (empty string = unpartitioned)
    ``parent_dim``  parent entity dimension (``-1`` if top-level)
    ``parent_tag``  parent entity tag (``-1`` if top-level)
    """
    rows: list[dict] = []
    entities = (
        gmsh.model.getEntities(dim=dim)
        if dim != -1
        else gmsh.model.getEntities()
    )
    for ent_dim, ent_tag in entities:
        try:
            parts = list(gmsh.model.getPartitions(ent_dim, ent_tag))
        except Exception:
            parts = []
        try:
            p_dim, p_tag = gmsh.model.getParent(ent_dim, ent_tag)
        except Exception:
            p_dim, p_tag = -1, -1
        rows.append({
            'dim'        : ent_dim,
            'tag'        : ent_tag,
            'partitions' : ", ".join(str(p) for p in parts),
            'parent_dim' : p_dim,
            'parent_tag' : p_tag,
        })

    if not rows:
        return pd.DataFrame(
            columns=['dim', 'tag', 'partitions', 'parent_dim', 'parent_tag']
        )
    return pd.DataFrame(rows).set_index(['dim', 'tag'])

summary

summary() -> str

Return a concise text summary of the current partition state: number of partitions and a per-dimension entity count.

Source code in src/apeGmsh/mesh/Partition.py
def summary(self) -> str:
    """
    Return a concise text summary of the current partition state:
    number of partitions and a per-dimension entity count.
    """
    n = self.n_partitions()
    if n == 0:
        return (
            f"Partition(model={self._parent.name!r}): "
            f"not partitioned"
        )
    lines = [
        f"Partition(model={self._parent.name!r}): "
        f"{n} partition(s)",
    ]
    df = self.entity_table()
    if not df.empty:
        # count entities that belong to at least one partition
        partitioned = df[df['partitions'] != '']
        counts = (
            partitioned
            .reset_index()
            .groupby('dim')
            .size()
            .rename(index={0:'points',1:'curves',2:'surfaces',3:'volumes'})
        )
        for dim_label, count in counts.items():
            lines.append(f"  {dim_label:10s}: {count} partitioned entities")
    return "\n".join(lines)

save

save(path: Path | str, *, one_file_per_partition: bool = False, create_topology: bool = False, create_physicals: bool = True) -> Partition

Write the partitioned mesh to file(s).

Parameters

path : output file path or base name. The format is inferred from the extension (".msh" is the natural choice for partitioned meshes). one_file_per_partition : when True, Gmsh writes one file per partition alongside the combined file. The per-partition files are named <stem>_<k><suffix> (e.g. mesh_1.msh, mesh_2.msh …). create_topology : pass to Mesh.PartitionCreateTopology create_physicals : pass to Mesh.PartitionCreatePhysicals (keep True so solvers can find BC tags)

Returns

self — for chaining

Source code in src/apeGmsh/mesh/Partition.py
def save(
    self,
    path                  : Path | str,
    *,
    one_file_per_partition: bool = False,
    create_topology       : bool = False,
    create_physicals      : bool = True,
) -> Partition:
    """
    Write the partitioned mesh to file(s).

    Parameters
    ----------
    path                   : output file path or base name.
                             The format is inferred from the extension
                             (``".msh"`` is the natural choice for
                             partitioned meshes).
    one_file_per_partition : when ``True``, Gmsh writes one file per
                             partition alongside the combined file.
                             The per-partition files are named
                             ``<stem>_<k><suffix>`` (e.g.
                             ``mesh_1.msh``, ``mesh_2.msh`` …).
    create_topology        : pass to ``Mesh.PartitionCreateTopology``
    create_physicals       : pass to ``Mesh.PartitionCreatePhysicals``
                             (keep ``True`` so solvers can find BC tags)

    Returns
    -------
    self — for chaining
    """
    path = Path(path)

    gmsh.option.setNumber(
        "Mesh.PartitionCreateTopology", int(create_topology)
    )
    gmsh.option.setNumber(
        "Mesh.PartitionCreatePhysicals", int(create_physicals)
    )
    gmsh.option.setNumber(
        "Mesh.PartitionSplitMeshFiles",  int(one_file_per_partition)
    )

    gmsh.write(str(path))
    self._log(
        f"save({path}, "
        f"one_file_per_partition={one_file_per_partition})"
    )
    return self

apeGmsh.mesh.View.View

View(parent: SessionProtocol)

Bases: _HasLogging

Solver-agnostic post-processing view composite attached to a apeGmsh instance as g.view.

Wraps gmsh.view to inject scalar and vector fields onto the active mesh. No solver dependency — you compute the result arrays yourself and pass them in.

Usage::

# Element-wise scalar (constant per element)
g.view.add_element_scalar("VonMises", elem_tags, values)

# Nodal scalar (smooth contour via Gmsh interpolation)
g.view.add_node_scalar("sigma_xx avg", node_tags, values)

# Nodal vector (displacement arrows / deformed shape)
g.view.add_node_vector("Displacement", node_tags, vectors)

All add_* methods return the Gmsh view tag (int).

Parameters

parent : _SessionBase Owning instance — used for name and _verbose.

Source code in src/apeGmsh/mesh/View.py
def __init__(self, parent: _SessionBase) -> None:
    self._parent = parent
    self._views: dict[Tag, str] = {}          # view_tag -> name

add_element_scalar

add_element_scalar(name: str, elem_tags: list[int] | ndarray, values: list[float] | ndarray, *, step: int = 0, time: float = 0.0) -> Tag

Add a scalar field with one value per element.

Parameters

name : view name shown in the Gmsh GUI sidebar elem_tags : Gmsh element tags (from g.mesh.get_elements) values : one scalar per element, same order as elem_tags

Returns

int Gmsh view tag

Source code in src/apeGmsh/mesh/View.py
def add_element_scalar(
    self,
    name       : str,
    elem_tags  : list[int] | ndarray,
    values     : list[float] | ndarray,
    *,
    step       : int   = 0,
    time       : float = 0.0,
) -> Tag:
    """
    Add a scalar field with one value per element.

    Parameters
    ----------
    name      : view name shown in the Gmsh GUI sidebar
    elem_tags : Gmsh element tags (from ``g.mesh.get_elements``)
    values    : one scalar per element, same order as *elem_tags*

    Returns
    -------
    int  Gmsh view tag
    """
    from apeGmsh.core._compose_errors import raise_if_no_live_kernel
    raise_if_no_live_kernel(self._parent, "g.view.add_element_scalar()")
    tags = [int(t) for t in elem_tags]
    data = [[float(v)] for v in values]

    v = gmsh.view.add(name)
    gmsh.view.addModelData(
        v, step, self._parent.name, "ElementData",
        tags, data, time, 1,
    )
    gmsh.view.option.setNumber(v, "IntervalsType", 3)  # continuous map

    self._views[v] = name
    self._log(f"add_element_scalar({name!r}) -> view {v}  "
              f"({len(tags)} elements)")
    return v

add_element_vector

add_element_vector(name: str, elem_tags: list[int] | ndarray, vectors: ndarray, *, step: int = 0, time: float = 0.0) -> Tag

Add a vector field with one 3-component vector per element.

Parameters

vectors : shape (nElem, 3)[vx, vy, vz] per element

Source code in src/apeGmsh/mesh/View.py
def add_element_vector(
    self,
    name       : str,
    elem_tags  : list[int] | ndarray,
    vectors    : ndarray,
    *,
    step       : int   = 0,
    time       : float = 0.0,
) -> Tag:
    """
    Add a vector field with one 3-component vector per element.

    Parameters
    ----------
    vectors : shape ``(nElem, 3)`` — ``[vx, vy, vz]`` per element
    """
    from apeGmsh.core._compose_errors import raise_if_no_live_kernel
    raise_if_no_live_kernel(self._parent, "g.view.add_element_vector()")
    tags = [int(t) for t in elem_tags]
    data = [[float(vectors[i, 0]), float(vectors[i, 1]), float(vectors[i, 2])]
            for i in range(len(tags))]

    v = gmsh.view.add(name)
    gmsh.view.addModelData(
        v, step, self._parent.name, "ElementData",
        tags, data, time, 3,
    )
    self._views[v] = name
    self._log(f"add_element_vector({name!r}) -> view {v}")
    return v

add_node_scalar

add_node_scalar(name: str, node_tags: list[int] | ndarray, values: list[float] | ndarray, *, step: int = 0, time: float = 0.0) -> Tag

Add a scalar field with one value per node.

Parameters

node_tags : Gmsh node tags values : one scalar per node, same order as node_tags

Source code in src/apeGmsh/mesh/View.py
def add_node_scalar(
    self,
    name       : str,
    node_tags  : list[int] | ndarray,
    values     : list[float] | ndarray,
    *,
    step       : int   = 0,
    time       : float = 0.0,
) -> Tag:
    """
    Add a scalar field with one value per node.

    Parameters
    ----------
    node_tags : Gmsh node tags
    values    : one scalar per node, same order as *node_tags*
    """
    from apeGmsh.core._compose_errors import raise_if_no_live_kernel
    raise_if_no_live_kernel(self._parent, "g.view.add_node_scalar()")
    tags = [int(t) for t in node_tags]
    data = [[float(v)] for v in values]

    v = gmsh.view.add(name)
    gmsh.view.addModelData(
        v, step, self._parent.name, "NodeData",
        tags, data, time, 1,
    )
    gmsh.view.option.setNumber(v, "IntervalsType", 3)

    self._views[v] = name
    self._log(f"add_node_scalar({name!r}) -> view {v}  ({len(tags)} nodes)")
    return v

add_node_vector

add_node_vector(name: str, node_tags: list[int] | ndarray, vectors: ndarray, *, step: int = 0, time: float = 0.0, vector_type: int = 5) -> Tag

Add a vector field with one 3-component vector per node.

Parameters

vectors : shape (nNode, 2) or (nNode, 3) — missing components are zero-padded vector_type : Gmsh display style (1=arrows, 2=cones, 5=displacement)

Source code in src/apeGmsh/mesh/View.py
def add_node_vector(
    self,
    name       : str,
    node_tags  : list[int] | ndarray,
    vectors    : ndarray,
    *,
    step       : int   = 0,
    time       : float = 0.0,
    vector_type: int   = 5,
) -> Tag:
    """
    Add a vector field with one 3-component vector per node.

    Parameters
    ----------
    vectors     : shape ``(nNode, 2)`` or ``(nNode, 3)`` — missing components are zero-padded
    vector_type : Gmsh display style (1=arrows, 2=cones, 5=displacement)
    """
    from apeGmsh.core._compose_errors import raise_if_no_live_kernel
    raise_if_no_live_kernel(self._parent, "g.view.add_node_vector()")
    vecs = np.asarray(vectors)
    if vecs.ndim == 1:
        vecs = vecs.reshape(-1, 1)
    ncols = vecs.shape[1]
    if ncols < 3:
        vecs = np.pad(vecs, ((0, 0), (0, 3 - ncols)))
    tags = [int(t) for t in node_tags]
    data = [[float(vecs[i, 0]), float(vecs[i, 1]), float(vecs[i, 2])]
            for i in range(len(tags))]

    v = gmsh.view.add(name)
    gmsh.view.addModelData(
        v, step, self._parent.name, "NodeData",
        tags, data, time, 3,
    )
    gmsh.view.option.setNumber(v, "VectorType", vector_type)

    self._views[v] = name
    self._log(f"add_node_vector({name!r}) -> view {v}  ({len(tags)} nodes)")
    return v

list_views

list_views() -> dict[Tag, str]

Return {tag: name} for all views created through this class.

Source code in src/apeGmsh/mesh/View.py
def list_views(self) -> dict[Tag, str]:
    """Return ``{tag: name}`` for all views created through this class."""
    return dict(self._views)

count

count() -> int

Number of views created.

Source code in src/apeGmsh/mesh/View.py
def count(self) -> int:
    """Number of views created."""
    return len(self._views)

Algorithms & enums

apeGmsh.mesh._mesh_algorithms

Algorithm / optimise constants + _normalize_algorithm helper.

Split out of the old monolithic Mesh.py so the generation sub-composite can import the normaliser without pulling in the main :class:~apeGmsh.mesh.Mesh.Mesh module.

The public names here are re-exported from apeGmsh.mesh.Mesh and from the top-level apeGmsh package for backwards-compatible imports.

Algorithm2D

Bases: IntEnum

2-D meshing algorithm selector (legacy IntEnum form).

Prefer passing a string name to :meth:apeGmsh.mesh.Mesh._Generation.set_algorithm — see :data:ALGORITHM_2D and :class:MeshAlgorithm2D for the canonical names and the accepted aliases.

Algorithm3D

Bases: IntEnum

3-D meshing algorithm selector (legacy IntEnum form).

MeshAlgorithm2D

Canonical 2-D algorithm names as string constants (IDE autocomplete).

MeshAlgorithm3D

Canonical 3-D algorithm names as string constants (IDE autocomplete).

OptimizeMethod

Mesh optimisation method names — use with g.mesh.generation.optimize.

apeGmsh.mesh._mesh_partitioning.RenumberResult

RenumberResult(method: str, n_nodes: int, n_elements: int, bandwidth_before: int, bandwidth_after: int)

Result of a mesh renumbering operation.

Attributes

method : str Algorithm used ("simple", "rcm", "hilbert", "metis"). n_nodes : int Number of nodes renumbered. n_elements : int Number of elements renumbered. bandwidth_before : int Semi-bandwidth before renumbering. bandwidth_after : int Semi-bandwidth after renumbering.

Source code in src/apeGmsh/mesh/_mesh_partitioning.py
def __init__(
    self,
    method: str,
    n_nodes: int,
    n_elements: int,
    bandwidth_before: int,
    bandwidth_after: int,
) -> None:
    self.method = method
    self.n_nodes = n_nodes
    self.n_elements = n_elements
    self.bandwidth_before = bandwidth_before
    self.bandwidth_after = bandwidth_after

apeGmsh.mesh._mesh_partitioning.PartitionInfo

PartitionInfo(n_parts: int, elements_per_partition: dict[int, int], weights_per_partition: dict[int, float] | None = None)

Result of a mesh partitioning operation.

Attributes

n_parts : int Number of partitions created. elements_per_partition : dict[int, int] {partition_id: element_count}. weights_per_partition : dict[int, float] | None {partition_id: total_weight} when partition() was called with weights=, otherwise None. Populated by _gather_partition_info() from the per-element weight vector cached on _Partitioning during the weighted call.

Source code in src/apeGmsh/mesh/_mesh_partitioning.py
def __init__(
    self,
    n_parts: int,
    elements_per_partition: dict[int, int],
    weights_per_partition: dict[int, float] | None = None,
) -> None:
    self.n_parts = n_parts
    self.elements_per_partition = elements_per_partition
    self.weights_per_partition = weights_per_partition

Group sets

apeGmsh.mesh._group_set.PhysicalGroupSet

PhysicalGroupSet(groups: dict[tuple[int, int], dict])

Bases: NamedGroupSet

Snapshot of solver-facing physical groups.

Accessed via fem.nodes.physical / fem.elements.physical (shared reference) and indirectly via fem.nodes.select(pg="Base").

Source code in src/apeGmsh/mesh/_group_set.py
def __init__(self, groups: dict[tuple[int, int], dict]) -> None:
    # Apply dtype coercion once at construction time
    self._groups: dict[tuple[int, int], dict] = {}
    for key, info in groups.items():
        coerced = dict(info)
        coerced['node_ids'] = _to_object(info['node_ids'])
        coerced['node_coords'] = np.asarray(
            info['node_coords'], dtype=np.float64)
        if 'element_ids' in info:
            coerced['element_ids'] = _to_object(info['element_ids'])
        # Per-type groups (new extraction format)
        if 'groups' in info:
            coerced['groups'] = info['groups']
        # Legacy flat connectivity (keep if present)
        if 'connectivity' in info:
            coerced['connectivity'] = _to_object(info['connectivity'])
        self._groups[key] = coerced

    self._name_index: dict[str, list[tuple[int, int]]] | None = None
    # Cache for merged multi-dim info dicts
    self._merged_cache: dict[str, dict] = {}

apeGmsh.mesh._group_set.LabelSet

LabelSet(groups: dict[tuple[int, int], dict])

Bases: NamedGroupSet

Snapshot of geometry-time labels (Tier 1).

Accessed via fem.nodes.labels / fem.elements.labels (shared reference) and indirectly via fem.nodes.select(label="col.web").

Source code in src/apeGmsh/mesh/_group_set.py
def __init__(self, groups: dict[tuple[int, int], dict]) -> None:
    # Apply dtype coercion once at construction time
    self._groups: dict[tuple[int, int], dict] = {}
    for key, info in groups.items():
        coerced = dict(info)
        coerced['node_ids'] = _to_object(info['node_ids'])
        coerced['node_coords'] = np.asarray(
            info['node_coords'], dtype=np.float64)
        if 'element_ids' in info:
            coerced['element_ids'] = _to_object(info['element_ids'])
        # Per-type groups (new extraction format)
        if 'groups' in info:
            coerced['groups'] = info['groups']
        # Legacy flat connectivity (keep if present)
        if 'connectivity' in info:
            coerced['connectivity'] = _to_object(info['connectivity'])
        self._groups[key] = coerced

    self._name_index: dict[str, list[tuple[int, int]]] | None = None
    # Cache for merged multi-dim info dicts
    self._merged_cache: dict[str, dict] = {}