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] = []

    # 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)

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()

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,
    )

results_viewer

results_viewer(results: str | None = None, *, point_data: dict | None = None, cell_data: dict | None = None, blocking: bool = False) -> None

Open the results viewer (apeGmshViewer).

Parameters

results : str, optional Path to a .vtu, .vtk, or .pvd file. point_data : dict, optional Nodal fields as numpy arrays: {name: ndarray}. cell_data : dict, optional Element fields as numpy arrays: {name: ndarray}. blocking : bool If False (default), the viewer runs non-blocking.

Source code in src/apeGmsh/mesh/Mesh.py
def results_viewer(
    self,
    results: str | None = None,
    *,
    point_data: dict | None = None,
    cell_data: dict | None = None,
    blocking: bool = False,
) -> None:
    """Open the results viewer (apeGmshViewer).

    Parameters
    ----------
    results : str, optional
        Path to a ``.vtu``, ``.vtk``, or ``.pvd`` file.
    point_data : dict, optional
        Nodal fields as numpy arrays: ``{name: ndarray}``.
    cell_data : dict, optional
        Element fields as numpy arrays: ``{name: ndarray}``.
    blocking : bool
        If False (default), the viewer runs non-blocking.
    """
    if results is not None:
        from apeGmshViewer import show
        show(results, blocking=blocking)
    elif point_data is not None or cell_data is not None:
        raise NotImplementedError(
            "g.mesh.viewer(point_data=..., cell_data=...) was a thin "
            "wrapper around the legacy Results class which has been "
            "rebuilt. The new flow is being designed as part of the "
            "viewer rebuild project — see internal_docs/"
            "Results_architecture.md (Phase 9). Until then, call "
            "g.mesh.viewer() to show the bare mesh, or pass results=path "
            "to load a pre-written .vtu/.pvd file."
        )
    else:
        import tempfile

        from apeGmshViewer import show

        from ..viz.VTKExport import VTKExport
        vtk_export = VTKExport(self._parent)
        tmp = tempfile.NamedTemporaryFile(suffix=".vtu", delete=False)
        vtk_export.write(tmp.name)
        tmp.close()
        show(tmp.name, blocking=blocking)

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)
    """
    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.
    """
    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."""
    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)
    """
    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._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]
    """
    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))
    """
    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)
    """
    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)
    """
    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))
    """
    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``.
    """
    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.
    """
    # 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)
    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

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("box", 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("box", 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],
        )
    """
    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("box", 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("box", 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

    # 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.

Source code in src/apeGmsh/mesh/_mesh_structured.py
def recombine(self) -> "_Structured":
    """Globally recombine all triangular elements into quads."""
    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.
    """
    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)
    """
    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)
    """
    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.
    """
    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.
    """
    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``.
    """
    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
    """
    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)])
    """
    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))
    """
    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(verbose: bool = True) -> '_Editing'

Merge nodes that share the same position within tolerance.

Parameters

verbose : if True (default), print how many nodes were merged.

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

    Parameters
    ----------
    verbose : if True (default), print how many nodes were merged.
    """
    before = len(gmsh.model.mesh.getNodes()[0])
    gmsh.model.mesh.removeDuplicateNodes()
    after  = len(gmsh.model.mesh.getNodes()[0])
    removed = before - after
    if verbose:
        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."""
    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",
        )
    """
    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

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")
    """
    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
    """
    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
    """
    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'``.
    """
    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.

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.

    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
    result = FEMData.from_gmsh(
        dim=dim, session=parent, remove_orphans=remove_orphans)

    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}"
    )

    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"``
    """
    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())
    """
    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

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
    """
    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) -> PartitionInfo

Partition the mesh into n_parts sub-domains (METIS).

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

Parameters

n_parts : int Number of partitions (>= 1).

Returns

PartitionInfo

Source code in src/apeGmsh/mesh/_mesh_partitioning.py
def partition(self, n_parts: int) -> PartitionInfo:
    """Partition the mesh into *n_parts* sub-domains (METIS).

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

    Parameters
    ----------
    n_parts : int
        Number of partitions (>= 1).

    Returns
    -------
    PartitionInfo
    """
    if n_parts < 1:
        raise ValueError(f"n_parts must be >= 1, got {n_parts}")
    gmsh.model.mesh.partition(n_parts)
    info = self._gather_partition_info()
    self._mesh._log(f"partition(n_parts={n_parts})")
    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
    """
    if len(elem_tags) != len(parts):
        raise ValueError(
            f"len(elem_tags)={len(elem_tags)} != "
            f"len(parts)={len(parts)}")
    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."""
    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)."""
    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."""
    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``.
    """
    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
    """
    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.
    """
    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
    """
    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.
    """
    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
    """
    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."""
    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)
    """
    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
    """
    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.
    """
    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
    """
    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
    """
    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
    """
    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
    """
    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
    """
    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)

Start a daisy-chainable selection over the live mesh.

Returns a :class:~apeGmsh.mesh._mesh_selection_chain.MeshSelectionChain (point family) — the fluent equivalent of the eager :meth:add_nodes / :meth:add_elements::

g.mesh_selection.select().in_box(lo, hi).on_plane(p, n, tol=1e-6)
g.mesh_selection.select(level="element", dim=3).in_box(lo, hi)
g.mesh_selection.select(ids=a) | g.mesh_selection.select(ids=b)
g.mesh_selection.select(name="base").in_sphere(c, r)

The chain seeds its atoms and then the standard point-family verbs (in_box / on_plane / in_sphere / nearest_to / where + | & - ^) narrow it, operating on the same live-mesh coordinates the eager API uses (node coords for level="node", element centroids for level="element") — so select().in_box(b).on_plane(p, n, tol=t) selects the same nodes/elements as add_nodes(in_box=b, on_plane=(...)).

Parameters

level : "node" (default) — atoms are mesh node ids; or "element" — atoms are element ids for dim. dim : element dimension (1/2/3) used when level == "element" (ignored for the node level); mirrors :meth:add_elements's dim. ids : optional explicit id list to seed from. Omitted (and no name) → the full live-mesh node universe (level="node") or the full live-mesh element universe of dim (level="element"), exactly the universe :meth:add_nodes / :meth:add_elements start from before their filters. name : optional name of an existing g.mesh_selection set to seed from (its node ids for level="node", its element ids for level="element"), so select(name=N).<spatial> is the id-for-id fluent equivalent of :meth:filter_set over that set. Mutually exclusive with ids=. Resolution delegates verbatim to the existing :meth:get_tag / :meth:get_nodes / :meth:get_elements surface (no new resolver); an unknown name fails loud.

Notes

.select() is additive: it does not register a set into :attr:_sets, does not allocate a tag, and does not perturb the eager API (name= only reads :attr:_sets). MeshSelectionChain is imported deferred (mirrors mesh/_mesh_structured.py); the chain module imports only the package-root leaf apeGmsh._chain + numpy, so this adds no eager cross-package edge (tests/test_import_dag_polarity.py baseline unchanged).

name= seeds from an already-registered mesh-selection set only. Seeding directly from a raw gmsh physical-group name or an apeGmsh label is not a select() parameter: MeshSelectionSet has no non-registering, non-reimplementing resolver for those — its only PG bridge, :meth:from_physical, registers a set + allocates a tag (which select() must not do) and is node-only; label / geometry resolution lives off MeshSelectionSet entirely, and a mesh-selection name is deliberately not a geometry-resolver tier (see docs/plans/selection-unification.md §9 and tests/test_resolution_contract.py). The supported, existing-surface route is the documented two step from_physical(dim, "PG", ms_name="foo") / from_geometric(sel, name="foo") then select(name="foo"). Persistence is now available on the returned MeshSelection via .save_as(name) (selection-unification-v2 P2-I): it registers the (already narrowed) id set through the existing :meth:MeshSelectionSet.add surface, so the named set snapshots into fem.mesh_selection and round-trips via FEMData HDF5 as selection= — the live-mesh engine is the only context where the mutable mesh-selection store is reachable (see ADR 0015).

Source code in src/apeGmsh/mesh/MeshSelectionSet.py
def select(
    self,
    *,
    level: str = "node",
    dim: int = 2,
    ids=None,
    name=None,
):
    """Start a daisy-chainable selection over the **live** mesh.

    Returns a
    :class:`~apeGmsh.mesh._mesh_selection_chain.MeshSelectionChain`
    (point family) — the fluent equivalent of the eager
    :meth:`add_nodes` / :meth:`add_elements`::

        g.mesh_selection.select().in_box(lo, hi).on_plane(p, n, tol=1e-6)
        g.mesh_selection.select(level="element", dim=3).in_box(lo, hi)
        g.mesh_selection.select(ids=a) | g.mesh_selection.select(ids=b)
        g.mesh_selection.select(name="base").in_sphere(c, r)

    The chain seeds its atoms and then the standard point-family
    verbs (``in_box`` / ``on_plane`` / ``in_sphere`` /
    ``nearest_to`` / ``where`` + ``| & - ^``) narrow it, operating
    on the same live-mesh coordinates the eager API uses (node
    coords for ``level="node"``, element centroids for
    ``level="element"``) — so
    ``select().in_box(b).on_plane(p, n, tol=t)`` selects the same
    nodes/elements as ``add_nodes(in_box=b, on_plane=(...))``.

    Parameters
    ----------
    level : ``"node"`` (default) — atoms are mesh node ids; or
        ``"element"`` — atoms are element ids for ``dim``.
    dim : element dimension (1/2/3) used when
        ``level == "element"`` (ignored for the node level);
        mirrors :meth:`add_elements`'s ``dim``.
    ids : optional explicit id list to seed from.  Omitted (and
        no ``name``) → the full live-mesh node universe
        (``level="node"``) or the full live-mesh element universe
        of ``dim`` (``level="element"``), exactly the universe
        :meth:`add_nodes` / :meth:`add_elements` start from before
        their filters.
    name : optional name of an **existing** ``g.mesh_selection``
        set to seed from (its node ids for ``level="node"``, its
        element ids for ``level="element"``), so
        ``select(name=N).<spatial>`` is the id-for-id fluent
        equivalent of :meth:`filter_set` over that set.  Mutually
        exclusive with ``ids=``.  Resolution **delegates verbatim**
        to the existing :meth:`get_tag` / :meth:`get_nodes` /
        :meth:`get_elements` surface (no new resolver); an unknown
        name fails loud.

    Notes
    -----
    ``.select()`` is **additive**: it does not register a set into
    :attr:`_sets`, does not allocate a tag, and does not perturb
    the eager API (``name=`` only *reads* :attr:`_sets`).
    ``MeshSelectionChain`` is imported **deferred** (mirrors
    ``mesh/_mesh_structured.py``); the chain module imports only
    the package-root leaf ``apeGmsh._chain`` + numpy, so this adds
    no eager cross-package edge
    (``tests/test_import_dag_polarity.py`` baseline unchanged).

    ``name=`` seeds from an **already-registered** mesh-selection
    set only.  Seeding *directly* from a raw gmsh physical-group
    name or an apeGmsh label is **not** a ``select()`` parameter:
    ``MeshSelectionSet`` has no non-registering, non-reimplementing
    resolver for those — its only PG bridge,
    :meth:`from_physical`, *registers* a set + allocates a tag
    (which ``select()`` must not do) and is node-only; label /
    geometry resolution lives off ``MeshSelectionSet`` entirely,
    and a mesh-selection name is deliberately *not* a
    geometry-resolver tier (see
    ``docs/plans/selection-unification.md`` §9 and
    ``tests/test_resolution_contract.py``).  The supported,
    existing-surface route is the documented two step
    ``from_physical(dim, "PG", ms_name="foo")`` /
    ``from_geometric(sel, name="foo")`` **then**
    ``select(name="foo")``.  Persistence is now available on the
    returned ``MeshSelection`` via ``.save_as(name)``
    (selection-unification-v2 P2-I): it registers the (already
    narrowed) id set through the existing
    :meth:`MeshSelectionSet.add` surface, so the named set
    snapshots into ``fem.mesh_selection`` and round-trips via
    FEMData HDF5 as ``selection=`` — the live-mesh engine is the
    only context where the mutable mesh-selection store is
    reachable (see ADR 0015).
    """
    # 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
    """
    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
    """
    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*
    """
    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)
    """
    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])

Result of a mesh partitioning operation.

Attributes

n_parts : int Number of partitions created. elements_per_partition : dict[int, int] {partition_id: element_count}.

Source code in src/apeGmsh/mesh/_mesh_partitioning.py
def __init__(
    self,
    n_parts: int,
    elements_per_partition: dict[int, int],
) -> None:
    self.n_parts = n_parts
    self.elements_per_partition = elements_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] = {}