Skip to content

Viz

Matplotlib plotting, entity selection, inspection helpers, and VTK export.

g.plot

apeGmsh.viz.Plot.Plot

Plot(parent: SessionProtocol)

Bases: _HasLogging

Plotting composite attached to a apeGmsh instance as g.plot.

Provides matplotlib-based visualisation of both BRep geometry and mesh, with optional entity-tag labels for introspection. All methods return self so they can be chained::

(g.plot
   .geometry(label_tags=True, show=False)
   .mesh(alpha=0.6, show=False)
   .label_entities(dims=[2])
   .show())

The figure is created on the first drawing call and reused for subsequent layering calls. Call clear() to start a new figure.

Parameters

parent : _SessionBase The owning instance — used for name and _verbose.

Source code in src/apeGmsh/viz/Plot.py
def __init__(self, parent: _SessionBase) -> None:
    self._parent = parent
    self._fig: plt.Figure | None = None
    self._ax:  Axes3D | None     = None
    self._figsize: tuple[float, float] = (9, 7)

figsize

figsize(size: tuple[float, float]) -> Plot

Set the matplotlib figure size (width, height in inches).

Call this before any drawing method to control the size of the figure when it's first created. If a figure already exists, it is resized in place.

Parameters

size : (width, height) tuple in inches

Example

::

(g.plot
   .figsize((12, 9))
   .geometry(show=False)
   .mesh()
   .show())
Source code in src/apeGmsh/viz/Plot.py
def figsize(self, size: tuple[float, float]) -> Plot:
    """
    Set the matplotlib figure size (width, height in inches).

    Call this *before* any drawing method to control the size of the
    figure when it's first created.  If a figure already exists, it is
    resized in place.

    Parameters
    ----------
    size : (width, height) tuple in inches

    Example
    -------
    ::

        (g.plot
           .figsize((12, 9))
           .geometry(show=False)
           .mesh()
           .show())
    """
    self._figsize = (float(size[0]), float(size[1]))
    if self._fig is not None:
        self._fig.set_size_inches(*self._figsize, forward=True)
    self._log(f"figsize({self._figsize})")
    return self

use_axes

use_axes(ax: Axes3D) -> Plot

Install an externally-owned 3D axes as the current drawing target.

Useful when you want to embed apeGmsh plots into a larger matplotlib layout (e.g., side-by-side comparisons via fig.add_subplot(121, projection='3d')). Subsequent drawing methods draw onto ax instead of creating their own figure.

Parameters

ax : a 3D Axes3D instance

Example

::

fig = plt.figure(figsize=(12, 5))
ax1 = fig.add_subplot(121, projection='3d')
ax2 = fig.add_subplot(122, projection='3d')

g.plot.use_axes(ax1).geometry()
g.plot.use_axes(ax2).mesh()
plt.show()
Source code in src/apeGmsh/viz/Plot.py
def use_axes(self, ax: Axes3D) -> Plot:
    """Install an externally-owned 3D axes as the current drawing target.

    Useful when you want to embed apeGmsh plots into a larger
    matplotlib layout (e.g., side-by-side comparisons via
    ``fig.add_subplot(121, projection='3d')``).  Subsequent drawing
    methods draw onto ``ax`` instead of creating their own figure.

    Parameters
    ----------
    ax : a 3D ``Axes3D`` instance

    Example
    -------
    ::

        fig = plt.figure(figsize=(12, 5))
        ax1 = fig.add_subplot(121, projection='3d')
        ax2 = fig.add_subplot(122, projection='3d')

        g.plot.use_axes(ax1).geometry()
        g.plot.use_axes(ax2).mesh()
        plt.show()
    """
    _require_mpl()
    self._fig = ax.figure  # type: ignore[assignment]
    self._ax  = ax
    return self

geometry

geometry(*, n_curve_samples: int = 60, show_points: bool = True, show_curves: bool = True, show_surfaces: bool = True, label_tags: bool = False, color_points: str = COLOR_POINTS, color_curves: str = COLOR_CURVES, color_surfaces: str = COLOR_SURFACES, surface_alpha: float = 0.25, show: bool = False) -> Plot

Plot BRep geometry by parametric sampling. No mesh required.

Parameters

n_curve_samples : points sampled along each curve show_points : scatter-plot geometric vertices show_curves : draw sampled curve polylines show_surfaces : draw filled surface patches (best-fit-plane triangulation, hole-aware) label_tags : annotate each entity with its tag (e.g. C3) color_points/curves/surfaces : matplotlib colour specs surface_alpha : opacity of surface patches show : call plt.show() at the end

Returns

self — for method chaining

Source code in src/apeGmsh/viz/Plot.py
def geometry(
    self,
    *,
    n_curve_samples : int   = 60,
    show_points     : bool  = True,
    show_curves     : bool  = True,
    show_surfaces   : bool  = True,
    label_tags      : bool  = False,
    color_points    : str   = COLOR_POINTS,
    color_curves    : str   = COLOR_CURVES,
    color_surfaces  : str   = COLOR_SURFACES,
    surface_alpha   : float = 0.25,
    show            : bool  = False,
) -> Plot:
    """
    Plot BRep geometry by parametric sampling.  No mesh required.

    Parameters
    ----------
    n_curve_samples  : points sampled along each curve
    show_points      : scatter-plot geometric vertices
    show_curves      : draw sampled curve polylines
    show_surfaces    : draw filled surface patches (best-fit-plane
                       triangulation, hole-aware)
    label_tags       : annotate each entity with its tag (e.g. ``C3``)
    color_points/curves/surfaces : matplotlib colour specs
    surface_alpha    : opacity of surface patches
    show             : call ``plt.show()`` at the end

    Returns
    -------
    self — for method chaining
    """
    _, ax = self._ensure_axes()
    collected: list[ndarray] = []

    # --- Vertices (dim=0) ---
    if show_points:
        for _, tag in gmsh.model.getEntities(dim=0):
            xyz = np.array(gmsh.model.getValue(0, tag, []))
            ax.scatter(*xyz, color=color_points, s=20, zorder=5,
                       depthshade=False)
            if label_tags:
                ax.text(*xyz, f'  P{tag}', fontsize=6,
                        color=color_points, va='bottom')
            collected.append(xyz.reshape(1, 3))

    # --- Curves (dim=1): sample parametrically ---
    if show_curves:
        for _, tag in gmsh.model.getEntities(dim=1):
            try:
                lo, hi = gmsh.model.getParametrizationBounds(1, tag)
                u  = np.linspace(lo[0], hi[0], n_curve_samples)
                pts = np.array(
                    gmsh.model.getValue(1, tag, u.tolist())
                ).reshape(-1, 3)
                ax.plot(pts[:, 0], pts[:, 1], pts[:, 2],
                        color=color_curves, linewidth=0.9)
                if label_tags:
                    mid = pts[len(pts) // 2]
                    ax.text(*mid, f'  C{tag}', fontsize=6,
                            color=color_curves, va='bottom')
                collected.append(pts)
            except Exception:
                pass

    # --- Surfaces (dim=2): boundary-curve sampling -> Poly3DCollection ---
    # Surfaces: sample boundary curves into closed loops (outer + holes),
    # fit a plane via SVD, triangulate in that plane with a point-in-
    # polygon filter so holes and non-XY orientations come out correctly.
    if show_surfaces:
        tris: list[ndarray] = []
        for _, tag in gmsh.model.getEntities(dim=2):
            loops = _sample_surface_boundary_loops(tag, n_curve_samples)
            if not loops:
                continue
            surf_tris = _triangulate_surface_loops(loops)
            tris.extend(surf_tris)
            all_loop_pts = np.vstack(loops)
            collected.append(all_loop_pts)
            if label_tags:
                centroid = loops[0].mean(axis=0)
                ax.text(*centroid, f'  S{tag}', fontsize=6,
                        color=color_surfaces)

        if tris:
            ax.add_collection3d(
                Poly3DCollection(tris, alpha=surface_alpha,
                                 facecolor=color_surfaces,
                                 edgecolor='none')
            )

    if collected:
        self._autoscale(np.vstack(collected))

    n_pts  = len(gmsh.model.getEntities(dim=0))
    n_crv  = len(gmsh.model.getEntities(dim=1))
    n_srf  = len(gmsh.model.getEntities(dim=2))
    self._log(
        f"geometry(pts={n_pts}, curves={n_crv}, "
        f"surfaces={n_srf}, label_tags={label_tags})"
    )
    if show:
        self.show()
    return self

mesh

mesh(*, color: str = COLOR_MESH, edge_color: str = 'white', alpha: float = 0.7, linewidth: float = 0.3, show: bool = False) -> Plot

Plot the surface mesh as filled polygons.

For a 3-D volume mesh, only the dim=2 (surface) elements are drawn; interior tetrahedra are not individually shown. If no surface mesh exists, falls back to dim=1 edge elements (useful for 2-D models).

Parameters

color : face colour edge_color : element edge colour alpha : face opacity linewidth : element outline width show : call plt.show() at the end

Returns

self — for method chaining

Source code in src/apeGmsh/viz/Plot.py
def mesh(
    self,
    *,
    color      : str   = COLOR_MESH,
    edge_color : str   = 'white',
    alpha      : float = 0.70,
    linewidth  : float = 0.30,
    show       : bool  = False,
) -> Plot:
    """
    Plot the surface mesh as filled polygons.

    For a 3-D volume mesh, only the dim=2 (surface) elements are drawn;
    interior tetrahedra are not individually shown.  If no surface mesh
    exists, falls back to dim=1 edge elements (useful for 2-D models).

    Parameters
    ----------
    color      : face colour
    edge_color : element edge colour
    alpha      : face opacity
    linewidth  : element outline width
    show       : call ``plt.show()`` at the end

    Returns
    -------
    self — for method chaining
    """
    _, ax = self._ensure_axes()

    # Collect every entity whose mesh elements we can draw.  Both
    # dim=2 (surface polygons) and dim=1 (line segments) are included
    # so mixed models — e.g. slabs + column curves — render both.
    plot_entities: list[DimTag] = (
        list(gmsh.model.getEntities(dim=2))
        + list(gmsh.model.getEntities(dim=1))
    )

    if not plot_entities:
        self._log("mesh(): no surface or edge entities found")
        if show:
            self.show()
        return self

    coords = self._build_node_lookup()
    if coords is None:
        self._log("mesh(): no nodes — has the mesh been generated?")
        if show:
            self.show()
        return self

    polys: list[ndarray] = []
    segments: list[ndarray] = []
    for ent_dim, ent_tag in plot_entities:
        for _, verts in self._iter_elements(ent_dim, ent_tag, coords):
            if len(verts) == 2:
                segments.append(verts)
            else:
                polys.append(verts)

    all_pts: list[ndarray] = []

    if polys:
        ax.add_collection3d(
            Poly3DCollection(polys,
                             alpha=alpha,
                             facecolor=color,
                             edgecolor=edge_color,
                             linewidth=linewidth)
        )
        all_pts.append(np.vstack(polys))

    if segments:
        ax.add_collection3d(
            Line3DCollection(segments,
                             colors=color,
                             linewidths=max(linewidth, 1.0))
        )
        all_pts.append(np.vstack(segments))

    if all_pts:
        combined = np.vstack(all_pts)
        self._autoscale(combined)
        # Show nodes as scatter points for 1D meshes
        if not polys and segments:
            unique_pts = np.unique(combined, axis=0)
            ax.scatter(
                unique_pts[:, 0], unique_pts[:, 1], unique_pts[:, 2],
                c=edge_color, s=12, zorder=5, depthshade=False,
            )

    self._log(f"mesh(): {len(polys)} polygons, {len(segments)} segments")
    if show:
        self.show()
    return self

quality

quality(*, quality_name: str = 'minSICN', cmap: str = 'RdYlGn', vmin: float | None = None, vmax: float | None = None, alpha: float = 0.85, linewidth: float = 0.2, show_colorbar: bool = True, show: bool = False) -> Plot

Plot surface mesh elements coloured by a quality metric.

Parameters

quality_name : gmsh quality metric — "minSICN" (default), "minSIGE", "gamma", "minEdge", "maxEdge", "minAngle", "maxAngle" cmap : matplotlib colormap name vmin / vmax : colormap range clamp (None = data min / max) alpha : face opacity linewidth : element edge width show_colorbar : add a colour bar show : call plt.show() at the end

Returns

self — for method chaining

Source code in src/apeGmsh/viz/Plot.py
def quality(
    self,
    *,
    quality_name : str        = "minSICN",
    cmap         : str        = "RdYlGn",
    vmin         : float | None = None,
    vmax         : float | None = None,
    alpha        : float      = 0.85,
    linewidth    : float      = 0.20,
    show_colorbar: bool       = True,
    show         : bool       = False,
) -> Plot:
    """
    Plot surface mesh elements coloured by a quality metric.

    Parameters
    ----------
    quality_name  : gmsh quality metric — ``"minSICN"`` (default),
                    ``"minSIGE"``, ``"gamma"``, ``"minEdge"``,
                    ``"maxEdge"``, ``"minAngle"``, ``"maxAngle"``
    cmap          : matplotlib colormap name
    vmin / vmax   : colormap range clamp (``None`` = data min / max)
    alpha         : face opacity
    linewidth     : element edge width
    show_colorbar : add a colour bar
    show          : call ``plt.show()`` at the end

    Returns
    -------
    self — for method chaining
    """
    fig, ax = self._ensure_axes()

    coords = self._build_node_lookup()
    if coords is None:
        self._log("quality(): no nodes found")
        if show:
            self.show()
        return self

    polys:     list[ndarray] = []
    elem_tags: list[int]     = []
    for _, ent_tag in gmsh.model.getEntities(dim=2):
        for etag, verts in self._iter_elements(
            2, ent_tag, coords, min_corners=3,
        ):
            polys.append(verts)
            elem_tags.append(etag)

    if elem_tags:
        qualities = [
            float(q) for q in gmsh.model.mesh.getElementQualities(
                elem_tags, qualityName=quality_name,
            )
        ]
    else:
        qualities = []

    if not polys:
        self._log("quality(): no surface elements found")
        if show:
            self.show()
        return self

    q_arr  = np.array(qualities)
    q_min  = float(q_arr.min()) if vmin is None else vmin
    q_max  = float(q_arr.max()) if vmax is None else vmax
    norm   = mcolors.Normalize(vmin=q_min, vmax=q_max)
    cmap_f = _get_cmap(cmap)
    face_colors = [cmap_f(norm(q)) for q in qualities]

    ax.add_collection3d(
        Poly3DCollection(polys,
                         alpha=alpha,
                         facecolors=face_colors,
                         edgecolor='k',
                         linewidth=linewidth)
    )
    self._autoscale(np.vstack(polys))

    if show_colorbar:
        sm = _mpl_cm.ScalarMappable(cmap=cmap_f, norm=norm)
        sm.set_array([])
        fig.colorbar(sm, ax=ax, shrink=0.55, pad=0.10,
                     label=quality_name)

    self._log(
        f"quality(metric={quality_name!r}, "
        f"range=[{q_min:.4f}, {q_max:.4f}], "
        f"n={len(polys)} elements)"
    )
    if show:
        self.show()
    return self

label_entities

label_entities(*, dims: list[int] | None = None, show_dim: bool = True, fontsize: int = 7, show: bool = False) -> Plot

Annotate the current axes with entity tags positioned at each entity's geometric centroid. Works without a mesh.

Parameters

dims : dimensions to label (default: all present in model) show_dim : prefix label with dimension indicator (P / C / S / V) fontsize : annotation font size show : call plt.show() at the end

Returns

self — for method chaining

Source code in src/apeGmsh/viz/Plot.py
def label_entities(
    self,
    *,
    dims    : list[int] | None = None,
    show_dim: bool = True,
    fontsize: int  = 7,
    show    : bool = False,
) -> Plot:
    """
    Annotate the current axes with entity tags positioned at each
    entity's geometric centroid.  Works without a mesh.

    Parameters
    ----------
    dims     : dimensions to label (default: all present in model)
    show_dim : prefix label with dimension indicator (P / C / S / V)
    fontsize : annotation font size
    show     : call ``plt.show()`` at the end

    Returns
    -------
    self — for method chaining
    """
    _, ax = self._ensure_axes()
    dim_colors = {
        0: self.COLOR_POINTS,
        1: self.COLOR_CURVES,
        2: self.COLOR_SURFACES,
        3: '#888888',
    }
    target_dims = dims if dims is not None else [0, 1, 2, 3]
    n_labeled = 0

    for d in target_dims:
        for _, tag in gmsh.model.getEntities(dim=d):
            try:
                bounds = gmsh.model.getParametrizationBounds(d, tag)
                if d == 0:
                    xyz = np.array(gmsh.model.getValue(0, tag, []))
                elif d == 1:
                    u_mid = [0.5 * (bounds[0][0] + bounds[1][0])]
                    xyz = np.array(gmsh.model.getValue(1, tag, u_mid))
                elif d == 2:
                    uv_mid = [
                        0.5 * (bounds[0][i] + bounds[1][i])
                        for i in range(2)
                    ]
                    xyz = np.array(gmsh.model.getValue(2, tag, uv_mid))
                else:
                    xyz = np.array(gmsh.model.occ.getCenterOfMass(3, tag))

                label = (f"{_DIM_PREFIX[d]}{tag}"
                         if show_dim else str(tag))
                ax.text(*xyz, f'  {label}',
                        fontsize=fontsize,
                        color=dim_colors.get(d, 'k'),
                        va='bottom')
                n_labeled += 1
            except Exception:
                pass

    self._log(f"label_entities(dims={target_dims}, n={n_labeled})")
    if show:
        self.show()
    return self

label_nodes

label_nodes(*, dim: int = -1, tag: int = -1, stride: int = 1, fontsize: int = 5, color: str = 'black', prefix: str = 'n', offset: tuple[float, float, float] | None = None, show: bool = False) -> Plot

Annotate mesh nodes with their tag ids.

Parameters

dim, tag : restrict to nodes on the given entity (dim=-1 returns all nodes; see gmsh.model.mesh.getNodes) stride : label every Nth node (useful for dense meshes) fontsize : annotation font size color : text colour prefix : string prepended to each node tag (e.g. 'n' -> n17) offset : (dx, dy, dz) text offset applied to each label; defaults to a small positive x offset so labels do not overlap markers show : call plt.show() at the end

Returns

self — for method chaining

Source code in src/apeGmsh/viz/Plot.py
def label_nodes(
    self,
    *,
    dim     : int  = -1,
    tag     : int  = -1,
    stride  : int  = 1,
    fontsize: int  = 5,
    color   : str  = 'black',
    prefix  : str  = 'n',
    offset  : tuple[float, float, float] | None = None,
    show    : bool = False,
) -> Plot:
    """
    Annotate mesh nodes with their tag ids.

    Parameters
    ----------
    dim, tag : restrict to nodes on the given entity
               (``dim=-1`` returns all nodes; see ``gmsh.model.mesh.getNodes``)
    stride   : label every Nth node (useful for dense meshes)
    fontsize : annotation font size
    color    : text colour
    prefix   : string prepended to each node tag (e.g. ``'n'`` -> ``n17``)
    offset   : (dx, dy, dz) text offset applied to each label; defaults
               to a small positive x offset so labels do not overlap markers
    show     : call ``plt.show()`` at the end

    Returns
    -------
    self — for method chaining
    """
    _, ax = self._ensure_axes()

    node_tags, coords_flat, _ = gmsh.model.mesh.getNodes(
        dim=dim, tag=tag, includeBoundary=True,
    )
    if len(node_tags) == 0:
        self._log("label_nodes(): no nodes — has the mesh been generated?")
        return self

    xyz = np.asarray(coords_flat).reshape(-1, 3)
    dx, dy, dz = offset if offset is not None else (0.0, 0.0, 0.0)
    step = max(int(stride), 1)
    n_labeled = 0
    for t, p in zip(node_tags[::step], xyz[::step]):
        ax.text(p[0] + dx, p[1] + dy, p[2] + dz,
                f"  {prefix}{t}", fontsize=fontsize, color=color,
                va='bottom')
        n_labeled += 1

    self._autoscale(xyz)
    self._log(f"label_nodes(dim={dim}, tag={tag}, stride={step}, "
              f"n={n_labeled}/{len(node_tags)})")
    if show:
        self.show()
    return self

label_elements

label_elements(*, dim: int = -1, tag: int = -1, stride: int = 1, fontsize: int = 5, color: str = 'darkred', prefix: str = 'e', show: bool = False) -> Plot

Annotate mesh elements with their tag ids at element centroids.

Parameters

dim, tag : restrict to elements on the given entity (dim=-1 returns elements of all dimensions) stride : label every Nth element fontsize : annotation font size color : text colour prefix : string prepended to each element tag (e.g. 'e' -> e42) show : call plt.show() at the end

Returns

self — for method chaining

Source code in src/apeGmsh/viz/Plot.py
def label_elements(
    self,
    *,
    dim     : int  = -1,
    tag     : int  = -1,
    stride  : int  = 1,
    fontsize: int  = 5,
    color   : str  = 'darkred',
    prefix  : str  = 'e',
    show    : bool = False,
) -> Plot:
    """
    Annotate mesh elements with their tag ids at element centroids.

    Parameters
    ----------
    dim, tag : restrict to elements on the given entity
               (``dim=-1`` returns elements of all dimensions)
    stride   : label every Nth element
    fontsize : annotation font size
    color    : text colour
    prefix   : string prepended to each element tag (e.g. ``'e'`` -> ``e42``)
    show     : call ``plt.show()`` at the end

    Returns
    -------
    self — for method chaining
    """
    _, ax = self._ensure_axes()

    # Build node-tag -> XYZ lookup (needed for centroid computation)
    n_tags, n_coords_flat, _ = gmsh.model.mesh.getNodes(
        dim=-1, tag=-1, includeBoundary=True,
    )
    if len(n_tags) == 0:
        self._log("label_elements(): no nodes — mesh not generated?")
        return self
    n_xyz = np.asarray(n_coords_flat).reshape(-1, 3)
    node_lookup = {int(t): n_xyz[i] for i, t in enumerate(n_tags)}

    step = max(int(stride), 1)
    n_labeled = 0

    # Iterate over dims (or just the requested one) and element types
    dims_iter = (dim,) if dim >= 0 else (0, 1, 2, 3)
    for d in dims_iter:
        etypes, etags_list, enodes_list = gmsh.model.mesh.getElements(
            dim=d, tag=tag,
        )
        for etype, elem_tags_arr, enodes in zip(
                etypes, etags_list, enodes_list):
            n_total, n_corner = self._element_node_counts(etype)
            n_elem = len(elem_tags_arr)
            for k in range(0, n_elem, step):
                all_ns = enodes[k * n_total:(k + 1) * n_total]
                corner = all_ns[:n_corner]
                pts = np.array([node_lookup[int(t)] for t in corner])
                c = pts.mean(axis=0)
                etag = int(elem_tags_arr[k])
                ax.text(c[0], c[1], c[2],
                        f"{prefix}{etag}", fontsize=fontsize,
                        color=color, ha='center', va='center')
                n_labeled += 1

    self._log(
        f"label_elements(dim={dim}, tag={tag}, "
        f"stride={step}) -> {n_labeled} labels"
    )
    if show:
        self.show()
    return self

show

show() -> Plot

Flush the current figure to the screen.

Handles are not reset — a subsequent savefig() or chained drawing call still targets the same figure. Use clear() to explicitly discard the current figure. In Jupyter, the inline backend closes the figure after rendering, which _ensure_axes() detects so the next cell starts fresh anyway.

Source code in src/apeGmsh/viz/Plot.py
def show(self) -> Plot:
    """Flush the current figure to the screen.

    Handles are *not* reset — a subsequent ``savefig()`` or chained
    drawing call still targets the same figure.  Use ``clear()`` to
    explicitly discard the current figure.  In Jupyter, the inline
    backend closes the figure after rendering, which
    ``_ensure_axes()`` detects so the next cell starts fresh anyway.
    """
    _require_mpl()
    if self._fig is not None:
        self._fig.tight_layout()
    plt.show()
    return self

savefig

savefig(path, **kwargs) -> Plot

Save the current figure to path.

A drawing method must have been called first (otherwise there is no figure to save). All keyword arguments are forwarded to matplotlib.figure.Figure.savefig — common ones are dpi=110, bbox_inches='tight', transparent=True.

Example

::

g.plot.geometry().savefig('out.png', dpi=120)
Source code in src/apeGmsh/viz/Plot.py
def savefig(self, path, **kwargs) -> Plot:
    """Save the current figure to ``path``.

    A drawing method must have been called first (otherwise there
    is no figure to save).  All keyword arguments are forwarded to
    ``matplotlib.figure.Figure.savefig`` — common ones are
    ``dpi=110``, ``bbox_inches='tight'``, ``transparent=True``.

    Example
    -------
    ::

        g.plot.geometry().savefig('out.png', dpi=120)
    """
    _require_mpl()
    if self._fig is None:
        raise RuntimeError(
            "Plot.savefig(): no active figure — call a drawing "
            "method (geometry / mesh / quality / ...) first."
        )
    kwargs.setdefault('bbox_inches', 'tight')
    self._fig.savefig(path, **kwargs)
    self._log(f"savefig({str(path)!r})")
    return self

clear

clear() -> Plot

Discard the current figure without showing it.

Source code in src/apeGmsh/viz/Plot.py
def clear(self) -> Plot:
    """Discard the current figure without showing it."""
    if self._fig is not None:
        plt.close(self._fig)
    self._fig = None
    self._ax  = None
    return self

physical_groups

physical_groups(*, dims: list[int] | None = None, names: list[str] | None = None, cmap: str = 'tab20', n_curve_samples: int = 40, point_size: int = 60, linewidth: float = 2.5, surface_alpha: float = 0.35, label_groups: bool = True, show_legend: bool = True, show: bool = False) -> Plot

Colour BRep entities by the physical group they belong to.

Iterates over gmsh.model.getPhysicalGroups() and draws every entity in each group with a distinct colour. Works at the geometry level — no mesh required.

Parameters

dims : physical-group dimensions to draw (default: all) names : only draw groups whose name is in this list (None draws all groups) cmap : matplotlib colormap used to cycle group colours n_curve_samples : points sampled per curve point_size : marker size for dim=0 groups linewidth : width for dim=1 groups surface_alpha : opacity for dim=2 group patches label_groups : annotate each group at its centroid show_legend : draw a legend mapping colour -> group name show : call plt.show() at the end

Returns

self — for method chaining

Source code in src/apeGmsh/viz/Plot.py
def physical_groups(
    self,
    *,
    dims            : list[int] | None = None,
    names           : list[str] | None = None,
    cmap            : str   = "tab20",
    n_curve_samples : int   = 40,
    point_size      : int   = 60,
    linewidth       : float = 2.5,
    surface_alpha   : float = 0.35,
    label_groups    : bool  = True,
    show_legend     : bool  = True,
    show            : bool  = False,
) -> Plot:
    """
    Colour BRep entities by the physical group they belong to.

    Iterates over ``gmsh.model.getPhysicalGroups()`` and draws every
    entity in each group with a distinct colour.  Works at the
    geometry level — no mesh required.

    Parameters
    ----------
    dims             : physical-group dimensions to draw (default: all)
    names            : only draw groups whose name is in this list
                       (``None`` draws all groups)
    cmap             : matplotlib colormap used to cycle group colours
    n_curve_samples  : points sampled per curve
    point_size       : marker size for dim=0 groups
    linewidth        : width for dim=1 groups
    surface_alpha    : opacity for dim=2 group patches
    label_groups     : annotate each group at its centroid
    show_legend      : draw a legend mapping colour -> group name
    show             : call ``plt.show()`` at the end

    Returns
    -------
    self — for method chaining
    """
    gmsh.model.occ.synchronize()
    _, ax = self._ensure_axes()

    groups = list(gmsh.model.getPhysicalGroups())
    if dims is not None:
        groups = [(d, t) for (d, t) in groups if d in dims]
    if names is not None:
        name_set = set(names)
        groups = [
            (d, t) for (d, t) in groups
            if gmsh.model.getPhysicalName(d, t) in name_set
        ]

    if not groups:
        self._log("physical_groups(): no groups found")
        if show:
            self.show()
        return self

    cmap_f = _get_cmap(cmap)
    collected: list[ndarray] = []
    legend_handles: list = []

    for i, (pg_dim, pg_tag) in enumerate(groups):
        name = gmsh.model.getPhysicalName(pg_dim, pg_tag) or f"PG{pg_tag}"
        color = cmap_f(i % cmap_f.N)
        ents = gmsh.model.getEntitiesForPhysicalGroup(pg_dim, pg_tag)
        centroid_pts: list[ndarray] = []

        # --- dim=0 ---
        if pg_dim == 0:
            for t in ents:
                xyz = np.array(gmsh.model.getValue(0, int(t), []))
                ax.scatter(*xyz, color=color, s=point_size,
                           zorder=6, depthshade=False,
                           edgecolors='k', linewidths=0.4)
                collected.append(xyz.reshape(1, 3))
                centroid_pts.append(xyz)

        # --- dim=1 ---
        elif pg_dim == 1:
            segs: list[ndarray] = []
            for t in ents:
                try:
                    lo, hi = gmsh.model.getParametrizationBounds(1, int(t))
                    u = np.linspace(lo[0], hi[0], n_curve_samples)
                    pts = np.array(
                        gmsh.model.getValue(1, int(t), u.tolist())
                    ).reshape(-1, 3)
                    for k in range(len(pts) - 1):
                        segs.append(pts[k:k + 2])
                    collected.append(pts)
                    centroid_pts.append(pts.mean(axis=0))
                except Exception:
                    pass
            if segs:
                ax.add_collection3d(
                    Line3DCollection(segs, colors=[color],
                                     linewidths=linewidth)
                )

        # --- dim=2 ---
        elif pg_dim == 2:
            tris: list[ndarray] = []
            for t in ents:
                loops = _sample_surface_boundary_loops(
                    int(t), n_curve_samples,
                )
                if not loops:
                    continue
                tris.extend(_triangulate_surface_loops(loops))
                all_loop_pts = np.vstack(loops)
                collected.append(all_loop_pts)
                centroid_pts.append(loops[0].mean(axis=0))
            if tris:
                ax.add_collection3d(
                    Poly3DCollection(tris, alpha=surface_alpha,
                                     facecolor=color,
                                     edgecolor='none')
                )

        # --- dim=3 (centroids only, too expensive to render volumes) ---
        elif pg_dim == 3:
            for t in ents:
                try:
                    c = np.array(gmsh.model.occ.getCenterOfMass(3, int(t)))
                    ax.scatter(*c, color=color, s=point_size * 2,
                               marker='X', zorder=6, depthshade=False)
                    centroid_pts.append(c)
                    collected.append(c.reshape(1, 3))
                except Exception:
                    pass

        # --- label + legend entry ---
        if label_groups and centroid_pts:
            c = np.mean(np.vstack(centroid_pts), axis=0)
            ax.text(c[0], c[1], c[2], f"  {name}",
                    fontsize=7, color='black',
                    ha='left', va='bottom')
        legend_handles.append(
            plt.Line2D([0], [0], marker='s', color='w',
                       markerfacecolor=color, markersize=9,
                       label=f"[{_DIM_PREFIX[pg_dim]}] {name}")
        )

    if collected:
        self._autoscale(np.vstack(collected))

    if show_legend and legend_handles:
        ax.legend(handles=legend_handles, loc='upper right',
                  fontsize=7, framealpha=0.85)

    self._log(f"physical_groups(): drew {len(groups)} group(s)")
    if show:
        self.show()
    return self

physical_groups_mesh

physical_groups_mesh(*, dims: list[int] | None = None, names: list[str] | None = None, cmap: str = 'tab20', alpha: float = 0.8, linewidth: float = 0.3, edge_color: str = 'white', point_size: int = 50, seg_width: float = 2.5, show_legend: bool = True, show: bool = False) -> Plot

Colour mesh entities by the physical group they belong to.

For each physical group, collects the mesh nodes/elements on every member entity and renders them in the group's colour.

Parameters

dims : physical-group dimensions to draw (default: all) names : only draw groups whose name is in this list cmap : matplotlib colormap used to cycle group colours alpha : face opacity for dim=2 group polygons linewidth : element outline width for dim=2 polygons edge_color : edge colour for dim=2 polygons point_size : scatter size for dim=0 group nodes seg_width : line width for dim=1 group segments show_legend : draw legend mapping colour -> group name show : call plt.show() at the end

Returns

self — for method chaining

Source code in src/apeGmsh/viz/Plot.py
def physical_groups_mesh(
    self,
    *,
    dims         : list[int] | None = None,
    names        : list[str] | None = None,
    cmap         : str   = "tab20",
    alpha        : float = 0.80,
    linewidth    : float = 0.30,
    edge_color   : str   = 'white',
    point_size   : int   = 50,
    seg_width    : float = 2.5,
    show_legend  : bool  = True,
    show         : bool  = False,
) -> Plot:
    """
    Colour mesh entities by the physical group they belong to.

    For each physical group, collects the mesh nodes/elements on
    every member entity and renders them in the group's colour.

    Parameters
    ----------
    dims         : physical-group dimensions to draw (default: all)
    names        : only draw groups whose name is in this list
    cmap         : matplotlib colormap used to cycle group colours
    alpha        : face opacity for dim=2 group polygons
    linewidth    : element outline width for dim=2 polygons
    edge_color   : edge colour for dim=2 polygons
    point_size   : scatter size for dim=0 group nodes
    seg_width    : line width for dim=1 group segments
    show_legend  : draw legend mapping colour -> group name
    show         : call ``plt.show()`` at the end

    Returns
    -------
    self — for method chaining
    """
    _, ax = self._ensure_axes()

    groups = list(gmsh.model.getPhysicalGroups())
    if dims is not None:
        groups = [(d, t) for (d, t) in groups if d in dims]
    if names is not None:
        name_set = set(names)
        groups = [
            (d, t) for (d, t) in groups
            if gmsh.model.getPhysicalName(d, t) in name_set
        ]

    if not groups:
        self._log("physical_groups_mesh(): no groups found")
        if show:
            self.show()
        return self

    node_lookup = self._build_node_lookup()
    if node_lookup is None:
        self._log("physical_groups_mesh(): no nodes — mesh generated?")
        if show:
            self.show()
        return self

    cmap_f = _get_cmap(cmap)
    collected: list[ndarray] = []
    legend_handles: list = []

    for i, (pg_dim, pg_tag) in enumerate(groups):
        name = gmsh.model.getPhysicalName(pg_dim, pg_tag) or f"PG{pg_tag}"
        color = cmap_f(i % cmap_f.N)
        ents = gmsh.model.getEntitiesForPhysicalGroup(pg_dim, pg_tag)

        # --- dim=0: scatter the tagged nodes ---
        if pg_dim == 0:
            pts_list: list[ndarray] = []
            for t in ents:
                nt, ncoord, _ = gmsh.model.mesh.getNodes(
                    dim=0, tag=int(t), includeBoundary=True,
                )
                if len(nt) == 0:
                    # fall back to BRep coord if no mesh node
                    xyz = np.array(gmsh.model.getValue(0, int(t), []))
                    pts_list.append(xyz.reshape(1, 3))
                else:
                    pts_list.append(
                        np.asarray(ncoord).reshape(-1, 3)
                    )
            if pts_list:
                pts = np.vstack(pts_list)
                ax.scatter(pts[:, 0], pts[:, 1], pts[:, 2],
                           color=color, s=point_size, zorder=6,
                           depthshade=False, edgecolors='k',
                           linewidths=0.4)
                collected.append(pts)

        # --- dim=1: line segments per element ---
        elif pg_dim == 1:
            segs: list[ndarray] = []
            for t in ents:
                for _, verts in self._iter_elements(
                    1, int(t), node_lookup,
                ):
                    for s in range(len(verts) - 1):
                        segs.append(verts[s:s + 2])
            if segs:
                ax.add_collection3d(
                    Line3DCollection(segs, colors=[color],
                                     linewidths=seg_width)
                )
                collected.append(np.vstack(segs))

        # --- dim=2: polygon faces per element ---
        elif pg_dim == 2:
            polys: list[ndarray] = []
            for t in ents:
                for _, verts in self._iter_elements(
                    2, int(t), node_lookup, min_corners=3,
                ):
                    polys.append(verts)
            if polys:
                ax.add_collection3d(
                    Poly3DCollection(polys, alpha=alpha,
                                     facecolor=color,
                                     edgecolor=edge_color,
                                     linewidth=linewidth)
                )
                collected.append(np.vstack(polys))

        # --- dim=3: skip rendering interior tets; mark COM ---
        elif pg_dim == 3:
            for t in ents:
                try:
                    c = np.array(gmsh.model.occ.getCenterOfMass(3, int(t)))
                    ax.scatter(*c, color=color, s=point_size * 2,
                               marker='X', zorder=6, depthshade=False)
                    collected.append(c.reshape(1, 3))
                except Exception:
                    pass

        legend_handles.append(
            plt.Line2D([0], [0], marker='s', color='w',
                       markerfacecolor=color, markersize=9,
                       label=f"[{_DIM_PREFIX[pg_dim]}] {name}")
        )

    if collected:
        self._autoscale(np.vstack(collected))

    if show_legend and legend_handles:
        ax.legend(handles=legend_handles, loc='upper right',
                  fontsize=7, framealpha=0.85)

    self._log(
        f"physical_groups_mesh(): drew {len(groups)} group(s)"
    )
    if show:
        self.show()
    return self

Selection — viewer pick-result

The frozen DimTag snapshot exposed by an interactive viewer's .selection property (g.model.viewer() / g.mesh.viewer()). For programmatic entity selection use g.model.select(...) (see Selection).

apeGmsh.viz.Selection.Selection

Selection(dimtags: Iterable[DimTag], parent: apeGmsh)

Frozen snapshot of a set of Gmsh DimTags with set-algebra, refinement, and conversion helpers.

Selection objects are exposed by an interactive viewer's .selection property (g.model.viewer() / g.mesh.viewer()) — you do not normally instantiate them directly.

Attributes

dim : int Common dimension of all entries, or -1 if mixed (only possible after cross-dim set operations). dimtags : tuple[DimTag, ...] Frozen tuple of (dim, tag) pairs.

Source code in src/apeGmsh/viz/Selection.py
def __init__(
    self,
    dimtags : Iterable[DimTag],
    parent: _SessionBase,
) -> None:
    # Deduplicate while preserving order
    seen: set[DimTag] = set()
    uniq: list[DimTag] = []
    for dt in dimtags:
        t = (int(dt[0]), int(dt[1]))
        if t not in seen:
            seen.add(t)
            uniq.append(t)
    object.__setattr__(self, '_dimtags', tuple(uniq))
    dims = {d for d, _ in uniq}
    object.__setattr__(self, '_dim', dims.pop() if len(dims) == 1 else -1)
    object.__setattr__(self, '_parent', parent)

tags property

tags: tuple[Tag, ...]

Tuple of entity tags (requires homogeneous dim).

filter

filter(*, tags: Sequence[Tag] | None = None, exclude_tags: Sequence[Tag] | None = None, labels: str | Sequence[str] | None = None, kinds: str | Sequence[str] | None = None, physical: str | Tag | None = None, in_box: BBox | None = None, in_sphere: tuple[float, float, float, float] | None = None, on_plane: tuple[str, float, float] | None = None, on_axis: tuple[str, float] | None = None, at_point: tuple[float, float, float, float] | None = None, length_range: tuple[float, float] | None = None, area_range: tuple[float, float] | None = None, volume_range: tuple[float, float] | None = None, aligned: tuple[str, float] | None = None, horizontal: bool | None = None, vertical: bool | None = None, predicate: Callable[[int, int], bool] | None = None) -> Selection

Re-apply filters to this selection.

Source code in src/apeGmsh/viz/Selection.py
def filter(
    self,
    *,
    tags           : Sequence[Tag] | None = None,
    exclude_tags   : Sequence[Tag] | None = None,
    labels         : str | Sequence[str] | None = None,
    kinds          : str | Sequence[str] | None = None,
    physical       : str | Tag | None     = None,
    in_box         : BBox | None          = None,
    in_sphere      : tuple[float, float, float, float] | None = None,
    on_plane       : tuple[str, float, float] | None = None,
    on_axis        : tuple[str, float] | None = None,
    at_point       : tuple[float, float, float, float] | None = None,
    length_range   : tuple[float, float] | None = None,
    area_range     : tuple[float, float] | None = None,
    volume_range   : tuple[float, float] | None = None,
    aligned        : tuple[str, float] | None = None,
    horizontal     : bool | None          = None,
    vertical       : bool | None          = None,
    predicate      : Callable[[int, int], bool] | None = None,
) -> Selection:
    """Re-apply filters to this selection."""
    if self._dim == -1:
        raise ValueError("Cannot filter a mixed-dim selection.")
    filtered = _apply_filters(
        list(self._dimtags), dim=self._dim, parent=self._parent,
        tags=tags, exclude_tags=exclude_tags, labels=labels,
        kinds=kinds, physical=physical, in_box=in_box,
        in_sphere=in_sphere, on_plane=on_plane, on_axis=on_axis,
        at_point=at_point, length_range=length_range,
        area_range=area_range, volume_range=volume_range,
        aligned=aligned, horizontal=horizontal, vertical=vertical,
        predicate=predicate,
    )
    return Selection(filtered, self._parent)

limit

limit(n: int) -> Selection

Keep at most n entries.

Source code in src/apeGmsh/viz/Selection.py
def limit(self, n: int) -> Selection:
    """Keep at most *n* entries."""
    return Selection(self._dimtags[:n], self._parent)

sorted_by

sorted_by(key: str | Callable[[DimTag], float] = 'x') -> Selection

Sort by a coordinate ("x", "y", "z"), a metric ("length", "area", "volume", "mass"), or a callable (dim, tag) -> float.

Source code in src/apeGmsh/viz/Selection.py
def sorted_by(
    self,
    key: str | Callable[[DimTag], float] = "x",
) -> Selection:
    """
    Sort by a coordinate (``"x"``, ``"y"``, ``"z"``), a metric
    (``"length"``, ``"area"``, ``"volume"``, ``"mass"``), or a
    callable ``(dim, tag) -> float``.
    """
    if self._dim == -1:
        raise ValueError("Cannot sort a mixed-dim selection.")

    if callable(key):
        scored = [(key(dt), dt) for dt in self._dimtags]
    elif key in _AXIS_IDX:
        idx = _AXIS_IDX[key]
        centers = self.centers()
        scored = list(zip(centers[:, idx].tolist(), self._dimtags))
    elif key in ("length", "area", "volume", "mass"):
        masses = self.masses()
        scored = list(zip(masses.tolist(), self._dimtags))
    else:
        raise ValueError(f"Unknown sort key: {key!r}")
    scored.sort(key=lambda pair: pair[0])
    return Selection([dt for _, dt in scored], self._parent)

bbox

bbox() -> BBox

Return the axis-aligned bounding box of the union.

Source code in src/apeGmsh/viz/Selection.py
def bbox(self) -> BBox:
    """Return the axis-aligned bounding box of the union."""
    if not self._dimtags:
        raise ValueError("Empty selection — no bounding box.")
    xmins, ymins, zmins = [], [], []
    xmaxs, ymaxs, zmaxs = [], [], []
    for d, t in self._dimtags:
        x0, y0, z0, x1, y1, z1 = gmsh.model.getBoundingBox(d, t)
        xmins.append(x0)
        ymins.append(y0)
        zmins.append(z0)
        xmaxs.append(x1)
        ymaxs.append(y1)
        zmaxs.append(z1)
    return (min(xmins), min(ymins), min(zmins),
            max(xmaxs), max(ymaxs), max(zmaxs))

centers

centers() -> np.ndarray

(N, 3) array of entity centroids.

Source code in src/apeGmsh/viz/Selection.py
def centers(self) -> np.ndarray:
    """(N, 3) array of entity centroids."""
    pts = np.empty((len(self._dimtags), 3))
    for i, (d, t) in enumerate(self._dimtags):
        pts[i] = _entity_center(d, t)
    return pts

masses

masses() -> np.ndarray

(N,) array of length/area/volume values (via occ.getMass).

Source code in src/apeGmsh/viz/Selection.py
def masses(self) -> np.ndarray:
    """(N,) array of length/area/volume values (via ``occ.getMass``)."""
    out = np.empty(len(self._dimtags))
    for i, (d, t) in enumerate(self._dimtags):
        if d == 0:
            out[i] = 0.0
        else:
            try:
                out[i] = gmsh.model.occ.getMass(d, t)
            except Exception:
                out[i] = float('nan')
    return out

to_list

to_list() -> list[DimTag]

Return a plain list of (dim, tag) tuples.

Source code in src/apeGmsh/viz/Selection.py
def to_list(self) -> list[DimTag]:
    """Return a plain list of ``(dim, tag)`` tuples."""
    return list(self._dimtags)

to_tags

to_tags() -> list[Tag]

Return a plain list of tags (requires homogeneous dim).

Source code in src/apeGmsh/viz/Selection.py
def to_tags(self) -> list[Tag]:
    """Return a plain list of tags (requires homogeneous dim)."""
    return list(self.tags)

to_physical

to_physical(name: str, *, tag: Tag = -1) -> Tag

Promote this selection to a physical group.

Parameters

name : physical-group name tag : requested physical-group tag (-1 = auto-assign)

Returns

Tag the physical-group tag assigned by Gmsh.

Source code in src/apeGmsh/viz/Selection.py
def to_physical(self, name: str, *, tag: Tag = -1) -> Tag:
    """
    Promote this selection to a physical group.

    Parameters
    ----------
    name : physical-group name
    tag  : requested physical-group tag (``-1`` = auto-assign)

    Returns
    -------
    Tag the physical-group tag assigned by Gmsh.
    """
    if self._dim == -1:
        raise ValueError(
            "to_physical requires a homogeneous-dim selection."
        )
    if not self._dimtags:
        raise ValueError("Cannot promote an empty selection to a physical group.")
    return self._parent.physical.add(
        self._dim, list(self.tags), name=name, tag=tag,
    )

to_dataframe

to_dataframe() -> pd.DataFrame

Return a DataFrame with columns dim, tag, kind, label, x, y, z, mass.

kind is read from Model._metadata (entity metadata). label is read from g.labels (the single source of truth).

Source code in src/apeGmsh/viz/Selection.py
def to_dataframe(self) -> pd.DataFrame:
    """
    Return a DataFrame with columns
    ``dim, tag, kind, label, x, y, z, mass``.

    ``kind`` is read from ``Model._metadata`` (entity metadata).
    ``label`` is read from ``g.labels`` (the single source of truth).
    """
    reg = self._parent.model._metadata
    # Build label reverse map from g.labels
    labels_comp = getattr(self._parent, 'labels', None)
    label_map: dict[tuple[int, int], str] = {}
    if labels_comp is not None:
        try:
            label_map = labels_comp.reverse_map()
        except Exception:
            pass
    centers = self.centers() if self._dimtags else np.empty((0, 3))
    masses  = self.masses()  if self._dimtags else np.empty((0,))
    rows = []
    for i, (d, t) in enumerate(self._dimtags):
        info = reg.get((d, t), {})
        rows.append({
            'dim'   : d,
            'tag'   : t,
            'kind'  : info.get('kind'),
            'label' : label_map.get((d, t), ''),
            'x'     : centers[i, 0],
            'y'     : centers[i, 1],
            'z'     : centers[i, 2],
            'mass'  : masses[i],
        })
    return pd.DataFrame(
        rows,
        columns=['dim', 'tag', 'kind', 'label', 'x', 'y', 'z', 'mass'],
    )

g.inspect

apeGmsh.viz.Inspect.Inspect

Inspect(parent: SessionProtocol)

Composite introspection helper attached to a apeGmsh instance as self.inspect.

All geometry-query logic lives here so that apeGmsh itself stays focused on model lifecycle / IO / visualisation.

Parameters

parent : _SessionBase The owning apeGmsh instance (provides _verbose).

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

get_geometry_info

get_geometry_info() -> tuple[dict, pd.DataFrame]

Build a nested mapping with flat DataFrames and type summaries per dimension. Pure geometric introspection — no mesh, no sampling.

Returns

mapping : dict { label: { 'df' : pd.DataFrame, # flat entity table (one row per entity) 'summary' : pd.DataFrame, # type counts for this dimension 'entities': { tag: dict } # raw per-entity data } } global_summary : pd.DataFrame Single table with entity counts and types across all dimensions.

Source code in src/apeGmsh/viz/Inspect.py
def get_geometry_info(self) -> tuple[dict, pd.DataFrame]:
    """
    Build a nested mapping with flat DataFrames and type summaries per
    dimension.  Pure geometric introspection — no mesh, no sampling.

    Returns
    -------
    mapping : dict
        {
            label: {
                'df'      : pd.DataFrame,   # flat entity table (one row per entity)
                'summary' : pd.DataFrame,   # type counts for this dimension
                'entities': { tag: dict }   # raw per-entity data
            }
        }
    global_summary : pd.DataFrame
        Single table with entity counts and types across all dimensions.
    """
    entity_labels: dict[int, str] = {
        0: 'points',
        1: 'curves',
        2: 'surfaces',
        3: 'volumes',
    }

    mapping: dict[str, dict] = {
        label: {'df': None, 'summary': None, 'entities': {}}
        for label in entity_labels.values()
    }

    global_rows: list[dict] = []

    for dim, label in entity_labels.items():
        entities         = gmsh.model.getEntities(dim=dim)
        rows: list[dict] = []

        for _, tag in entities:

            entity_type: str         = gmsh.model.getType(dim, tag)
            boundary                 = gmsh.model.getBoundary([(dim, tag)], oriented=False)
            boundary_tags: list[int] = [t for _, t in boundary]
            bounds                   = gmsh.model.getParametrizationBounds(dim, tag)

            entry: dict = {
                'type'              : entity_type,
                'boundary_tags'     : boundary_tags,
                'parametric_bounds' : bounds,
            }

            # --- dim 0: points ---
            if dim == 0:
                coords: ndarray  = np.array(gmsh.model.getValue(0, tag, []))
                entry['coords']  = coords
                rows.append({
                    'name': f'Point {tag}',
                    'tag' : tag,
                    'type': entity_type,
                    'x'   : coords[0],
                    'y'   : coords[1],
                    'z'   : coords[2],
                })

            # --- dim 1: curves ---
            elif dim == 1:
                u_min: float = bounds[0][0]
                u_max: float = bounds[1][0]
                u_mid: float = 0.5 * (u_min + u_max)

                start   : ndarray = np.array(gmsh.model.getValue(1, tag, [u_min]))
                end     : ndarray = np.array(gmsh.model.getValue(1, tag, [u_max]))
                midpoint: ndarray = np.array(gmsh.model.getValue(1, tag, [u_mid]))
                length  : float   = gmsh.model.occ.getMass(1, tag)
                curvature: float  = gmsh.model.getCurvature(1, tag, [u_mid])

                entry['length']           = length
                entry['start']            = start
                entry['end']              = end
                entry['midpoint']         = midpoint
                entry['curvature_at_mid'] = curvature

                rows.append({
                    'name'            : f'Curve {tag}',
                    'tag'             : tag,
                    'type'            : entity_type,
                    'start_pt'        : boundary_tags[0] if boundary_tags else -1,
                    'end_pt'          : boundary_tags[-1] if boundary_tags else -1,
                    'length'          : length,
                    'start_x'         : start[0],
                    'start_y'         : start[1],
                    'start_z'         : start[2],
                    'end_x'           : end[0],
                    'end_y'           : end[1],
                    'end_z'           : end[2],
                    'mid_x'           : midpoint[0],
                    'mid_y'           : midpoint[1],
                    'mid_z'           : midpoint[2],
                    'curvature_at_mid': curvature,
                    'u_min'           : u_min,
                    'u_max'           : u_max,
                })

            # --- dim 2: surfaces ---
            elif dim == 2:
                u_mid_2d: list[float] = [
                    0.5 * (bounds[0][i] + bounds[1][i]) for i in range(2)
                ]

                area           = float(gmsh.model.occ.getMass(2, tag))
                center         = np.array(gmsh.model.occ.getCenterOfMass(2, tag))
                normal         = np.array(gmsh.model.getNormal(tag, u_mid_2d))
                curvature      = float(gmsh.model.getCurvature(2, tag, u_mid_2d))
                k1, k2, d1, d2          = gmsh.model.getPrincipalCurvatures(tag, u_mid_2d)

                entry['area']                 = area
                entry['center_of_mass']       = center
                entry['normal_at_mid']        = normal
                entry['curvature_at_mid']     = curvature
                entry['principal_curvatures'] = {
                    'k1': k1, 'k2': k2,
                    'd1': np.array(d1), 'd2': np.array(d2)
                }

                rows.append({
                    'name'              : f'Surface {tag}',
                    'tag'               : tag,
                    'type'              : entity_type,
                    'n_boundary_curves' : len(boundary_tags),
                    'boundary_curves'   : str(boundary_tags),
                    'area'              : area,
                    'cx'                : center[0],
                    'cy'                : center[1],
                    'cz'                : center[2],
                    'nx'                : normal[0],
                    'ny'                : normal[1],
                    'nz'                : normal[2],
                    'curvature_at_mid'  : curvature,
                    'k1'                : k1,
                    'k2'                : k2,
                })

            # --- dim 3: volumes ---
            else:
                volume = float(gmsh.model.occ.getMass(3, tag))
                center = np.array(gmsh.model.occ.getCenterOfMass(3, tag))
                inertia: ndarray = np.array(
                    gmsh.model.occ.getMatrixOfInertia(3, tag)
                ).reshape(3, 3)

                entry['volume']         = volume
                entry['center_of_mass'] = center
                entry['inertia']        = inertia

                rows.append({
                    'name'               : f'Volume {tag}',
                    'tag'                : tag,
                    'type'               : entity_type,
                    'n_boundary_surfaces': len(boundary_tags),
                    'boundary_surfaces'  : str(boundary_tags),
                    'volume'             : volume,
                    'cx'                 : center[0],
                    'cy'                 : center[1],
                    'cz'                 : center[2],
                    'ixx'                : inertia[0, 0],
                    'iyy'                : inertia[1, 1],
                    'izz'                : inertia[2, 2],
                    'ixy'                : inertia[0, 1],
                    'ixz'                : inertia[0, 2],
                    'iyz'                : inertia[1, 2],
                })

            mapping[label]['entities'][tag] = entry

        # --- flat entity DataFrame ---
        mapping[label]['df'] = (
            pd.DataFrame(rows).set_index('name') if rows else pd.DataFrame()
        )

        # --- per-dimension type summary ---
        if rows:
            mapping[label]['summary'] = (
                pd.DataFrame(rows)
                .groupby('type')
                .size()
                .reset_index(name='count')
                .set_index('type')
            )
        else:
            mapping[label]['summary'] = pd.DataFrame()

        # --- accumulate into global summary ---
        for row in rows:
            global_rows.append({
                'entity': label,
                'type'  : row.get('type', 'Vertex'),
            })

    # --- global summary ---
    global_summary: pd.DataFrame = (
        pd.DataFrame(global_rows)
        .groupby(['entity', 'type'])
        .size()
        .reset_index(name='count')
        .set_index(['entity', 'type'])
    )

    if self._parent._verbose:
        print("\n--- Global Geometry Summary ---")
        print(global_summary.to_string())
        for label, data in mapping.items():
            df: pd.DataFrame = data['df']
            if not df.empty:
                print(f"\n--- {label.capitalize()} Entities ---")
                print(df.to_string())

    return mapping, global_summary

get_mesh_info

get_mesh_info() -> tuple[dict, pd.DataFrame]

Introspect the current mesh. Returns counts, per-entity breakdowns and a per-element-type quality summary. No geometric re-sampling — only what gmsh.model.mesh can report directly.

Returns

mapping : dict { 'nodes' : {'count': int, 'df': pd.DataFrame}, 'elements' : { 'df' : pd.DataFrame, # one row per (dim, tag, elem_type) 'summary' : pd.DataFrame, # counts per element type 'quality' : pd.DataFrame, # min/mean/max SICN per elem type }, } global_summary : pd.DataFrame Single table with node / element counts indexed by dim.

Source code in src/apeGmsh/viz/Inspect.py
def get_mesh_info(self) -> tuple[dict, pd.DataFrame]:
    """
    Introspect the current mesh.  Returns counts, per-entity breakdowns
    and a per-element-type quality summary.  No geometric re-sampling —
    only what ``gmsh.model.mesh`` can report directly.

    Returns
    -------
    mapping : dict
        {
            'nodes'    : {'count': int, 'df': pd.DataFrame},
            'elements' : {
                'df'      : pd.DataFrame,  # one row per (dim, tag, elem_type)
                'summary' : pd.DataFrame,  # counts per element type
                'quality' : pd.DataFrame,  # min/mean/max SICN per elem type
            },
        }
    global_summary : pd.DataFrame
        Single table with node / element counts indexed by dim.
    """
    # --- nodes -----------------------------------------------------
    node_tags, node_xyz, _ = gmsh.model.mesh.getNodes(
        dim=-1, tag=-1, includeBoundary=True
    )
    n_nodes: int = len(node_tags)
    nodes_df: pd.DataFrame = (
        pd.DataFrame({
            'tag': node_tags,
            'x'  : node_xyz[0::3],
            'y'  : node_xyz[1::3],
            'z'  : node_xyz[2::3],
        }).set_index('tag')
        if n_nodes else pd.DataFrame()
    )

    # --- elements, per entity -------------------------------------
    elem_rows   : list[dict] = []
    quality_rows: list[dict] = []
    global_rows : list[dict] = [{'kind': 'nodes', 'dim': -1, 'count': n_nodes}]

    for dim in range(4):
        dim_elem_count = 0
        for _, tag in gmsh.model.getEntities(dim=dim):
            etypes, etags, _ = gmsh.model.mesh.getElements(dim=dim, tag=tag)
            for et, tags in zip(etypes, etags):
                name, edim, order, n_per_elem, *_ = (
                    gmsh.model.mesh.getElementProperties(et)
                )
                n = len(tags)
                dim_elem_count += n
                elem_rows.append({
                    'dim'         : dim,
                    'entity_tag'  : tag,
                    'elem_type'   : name,
                    'order'       : order,
                    'nodes_per_el': n_per_elem,
                    'count'       : n,
                })

                # quality (SICN) — only defined for dim>=2 solid/surface elements
                if dim >= 2 and n > 0:
                    try:
                        q = np.asarray(
                            gmsh.model.mesh.getElementQualities(
                                list(tags), qualityName='minSICN'
                            )
                        )
                        quality_rows.append({
                            'elem_type'   : name,
                            'count'       : n,
                            'sicn_min'    : float(q.min()),
                            'sicn_mean'   : float(q.mean()),
                            'sicn_max'    : float(q.max()),
                        })
                    except Exception:
                        pass

        global_rows.append({
            'kind': 'elements', 'dim': dim, 'count': dim_elem_count,
        })

    # --- assemble return dicts ---
    elem_df = (
        pd.DataFrame(elem_rows).set_index(['dim', 'entity_tag'])
        if elem_rows else pd.DataFrame()
    )
    elem_summary = (
        pd.DataFrame(elem_rows)
        .groupby('elem_type')['count']
        .sum()
        .reset_index()
        .set_index('elem_type')
        if elem_rows else pd.DataFrame()
    )
    quality_df = (
        pd.DataFrame(quality_rows).set_index('elem_type')
        if quality_rows else pd.DataFrame()
    )
    global_summary = pd.DataFrame(global_rows).set_index('kind')

    mapping = {
        'nodes': {'count': n_nodes, 'df': nodes_df},
        'elements': {
            'df': elem_df,
            'summary': elem_summary,
            'quality': quality_df,
        },
    }

    if self._parent._verbose:
        print("\n--- Mesh Info ---")
        print(f"Nodes: {n_nodes}")
        if not elem_summary.empty:
            print(elem_summary.to_string())
        if not quality_df.empty:
            print("\n--- Element Quality (SICN) ---")
            print(quality_df.to_string())

    return mapping, global_summary

print_summary

print_summary() -> str

Print a comprehensive model summary by introspecting the live Gmsh session. Two data sources are distinguished:

  • [gmsh] — read directly from the Gmsh API (ground truth).
  • [tracked] — recorded by apeGmsh's Mesh wrapper for settings that Gmsh exposes no getter for (transfinite, per-entity sizes, recombine, fields, per-entity algorithm).

The summary covers:

  1. Geometry — entity counts and types per dimension
  2. Physical groups — names, dimensions, member counts
  3. Mesh options — global settings readable via gmsh.option.getNumber
  4. Mesh directives — write-only settings tracked by apeGmsh
  5. Mesh statistics — node/element counts, element types, quality (if mesh has been generated)
Returns

str The formatted summary text (also printed to stdout).

Source code in src/apeGmsh/viz/Inspect.py
def print_summary(self) -> str:
    """
    Print a comprehensive model summary by introspecting the live
    Gmsh session.  Two data sources are distinguished:

    * **[gmsh]** — read directly from the Gmsh API (ground truth).
    * **[tracked]** — recorded by apeGmsh's ``Mesh`` wrapper for
      settings that Gmsh exposes no getter for (transfinite,
      per-entity sizes, recombine, fields, per-entity algorithm).

    The summary covers:

    1. **Geometry** — entity counts and types per dimension
    2. **Physical groups** — names, dimensions, member counts
    3. **Mesh options** — global settings readable via
       ``gmsh.option.getNumber``
    4. **Mesh directives** — write-only settings tracked by apeGmsh
    5. **Mesh statistics** — node/element counts, element types,
       quality (if mesh has been generated)

    Returns
    -------
    str
        The formatted summary text (also printed to stdout).
    """
    import math as _math
    lines: list[str] = []
    _hr = "=" * 72

    def _section(title: str) -> None:
        lines.append("")
        lines.append(_hr)
        lines.append(f"  {title}")
        lines.append(_hr)

    def _sub(title: str) -> None:
        lines.append(f"\n  --- {title} ---")

    _DIM_LABEL = {0: "Points", 1: "Curves", 2: "Surfaces", 3: "Volumes"}
    _ALGO_2D = {
        1: "MeshAdapt", 2: "Automatic", 3: "InitialMeshOnly",
        5: "Delaunay", 6: "Frontal-Delaunay", 7: "BAMG",
        8: "Frontal-Delaunay for Quads",
        9: "Packing of Parallelograms", 11: "Quasi-Structured Quad",
    }
    _ALGO_3D = {
        1: "Delaunay", 3: "InitialMeshOnly", 4: "Frontal",
        7: "MMG3D", 9: "R-tree", 10: "HXT",
    }

    # ==============================================================
    # 1. GEOMETRY  [gmsh]
    # ==============================================================
    _section("GEOMETRY  [gmsh]")
    total_ents = 0
    for dim in range(4):
        ents = gmsh.model.getEntities(dim=dim)
        n = len(ents)
        total_ents += n
        if n == 0:
            continue
        # Count by type
        types: dict[str, int] = {}
        for _, tag in ents:
            try:
                t = gmsh.model.getType(dim, tag)
            except Exception:
                t = "Unknown"
            types[t] = types.get(t, 0) + 1
        type_str = ", ".join(f"{v} {k}" for k, v in sorted(types.items()))
        lines.append(f"  {_DIM_LABEL[dim]:10s}: {n:>5d}  ({type_str})")

    # Bounding box of entire model
    try:
        bb = gmsh.model.getBoundingBox(-1, -1)
        lines.append(
            f"  {'BBox':10s}: "
            f"({bb[0]:.4g}, {bb[1]:.4g}, {bb[2]:.4g}) -> "
            f"({bb[3]:.4g}, {bb[4]:.4g}, {bb[5]:.4g})"
        )
        diag = float(np.linalg.norm(
            [bb[3] - bb[0], bb[4] - bb[1], bb[5] - bb[2]]
        ))
        lines.append(f"  {'Diagonal':10s}: {diag:.4g}")
    except Exception:
        pass

    lines.append(f"  {'Total':10s}: {total_ents} entities")

    # ==============================================================
    # 2. PHYSICAL GROUPS  [gmsh]
    # ==============================================================
    _section("PHYSICAL GROUPS  [gmsh]")
    pgs = gmsh.model.getPhysicalGroups()
    if not pgs:
        lines.append("  (none)")
    else:
        for dim, pg_tag in pgs:
            name = gmsh.model.getPhysicalName(dim, pg_tag) or "(unnamed)"
            try:
                members = gmsh.model.getEntitiesForPhysicalGroup(dim, pg_tag)
                n_mem = len(members)
                tags_str = ", ".join(str(t) for t in members[:8])
                if n_mem > 8:
                    tags_str += f" ... (+{n_mem - 8} more)"
            except Exception:
                n_mem = 0
                tags_str = "?"
            lines.append(
                f"  dim={dim}  pg_tag={pg_tag:>3d}  "
                f"{name!r:30s}  "
                f"{n_mem:>4d} entities  [{tags_str}]"
            )
        # Reverse: entities with no physical group
        all_ents = set(gmsh.model.getEntities())
        assigned: set[tuple[int, int]] = set()
        for dim, pg_tag in pgs:
            try:
                for t in gmsh.model.getEntitiesForPhysicalGroup(dim, pg_tag):
                    assigned.add((dim, int(t)))
            except Exception:
                pass
        unassigned = all_ents - assigned
        if unassigned:
            by_dim: dict[int, int] = {}
            for d, _ in unassigned:
                by_dim[d] = by_dim.get(d, 0) + 1
            parts = ", ".join(
                f"{n} {_DIM_LABEL[d].lower()}" for d, n in sorted(by_dim.items())
            )
            lines.append(f"\n  Unassigned entities: {len(unassigned)} ({parts})")

    # ==============================================================
    # 3. MESH OPTIONS  [gmsh]
    # ==============================================================
    _section("MESH OPTIONS  [gmsh]")
    _opts: list[tuple[str, str, str]] = [
        ("Mesh.MeshSizeMin",              "Size min",              ""),
        ("Mesh.MeshSizeMax",              "Size max",              ""),
        ("Mesh.MeshSizeFactor",           "Size factor",           ""),
        ("Mesh.MeshSizeFromCurvature",    "From curvature",        "elems per 2π"),
        ("Mesh.MeshSizeFromPoints",       "From points",           "bool"),
        ("Mesh.MeshSizeExtendFromBoundary", "Extend from boundary", "bool"),
        ("Mesh.Algorithm",                "Algorithm 2D",          "code"),
        ("Mesh.Algorithm3D",              "Algorithm 3D",          "code"),
        ("Mesh.ElementOrder",             "Element order",         ""),
        ("Mesh.RecombineAll",             "Recombine all",         "bool"),
        ("Mesh.RecombinationAlgorithm",   "Recombine algorithm",   "code"),
        ("Mesh.Optimize",                 "Optimize",              "bool"),
        ("Mesh.OptimizeNetgen",           "Optimize Netgen",       "bool"),
        ("Mesh.SubdivisionAlgorithm",     "Subdivision",           "code"),
        ("Mesh.MinimumCurveNodes",        "Min curve nodes",       ""),
    ]
    for opt_key, label, hint in _opts:
        try:
            val = gmsh.option.getNumber(opt_key)
            extra = ""
            if opt_key == "Mesh.Algorithm":
                extra = f"  ({_ALGO_2D.get(int(val), '?')})"
            elif opt_key == "Mesh.Algorithm3D":
                extra = f"  ({_ALGO_3D.get(int(val), '?')})"
            elif hint == "bool":
                extra = f"  ({'ON' if val else 'OFF'})"
            elif hint == "elems per 2π" and val > 0:
                extra = f"  ({int(val)} elements per 2π)"
            lines.append(f"  {label:28s}: {val:>12g}{extra}")
        except Exception:
            pass

    # ==============================================================
    # 4. MESH DIRECTIVES  [tracked by apeGmsh]
    # ==============================================================
    _section("MESH DIRECTIVES  [tracked by apeGmsh]")
    mesh_composite = getattr(self._parent, 'mesh', None)
    directives = getattr(mesh_composite, '_directives', []) if mesh_composite else []

    if not directives:
        lines.append("  (no directives recorded this session)")
    else:
        # Group by kind for readability
        by_kind: dict[str, list[dict]] = {}
        for d in directives:
            by_kind.setdefault(d['kind'], []).append(d)

        for kind, items in by_kind.items():
            _sub(f"{kind} ({len(items)} directive{'s' if len(items) != 1 else ''})")

            if kind == 'set_size':
                for d in items:
                    lines.append(
                        f"    dim={d['dim']}  tags={d['tags']}  "
                        f"size={d['size']}"
                    )
            elif kind == 'set_size_all_points':
                for d in items:
                    lines.append(
                        f"    size={d['size']}  "
                        f"({d['n_points']} points)"
                    )
            elif kind == 'set_size_callback':
                for d in items:
                    lines.append(f"    callback={d['func_name']}")
            elif kind == 'transfinite_curve':
                for d in items:
                    lines.append(
                        f"    curve={d['tag']}  n_nodes={d['n_nodes']}  "
                        f"type={d['mesh_type']}  coef={d['coef']}"
                    )
            elif kind == 'transfinite_surface':
                for d in items:
                    corners = d['corners'] or "auto"
                    lines.append(
                        f"    surface={d['tag']}  "
                        f"arrangement={d['arrangement']}  "
                        f"corners={corners}"
                    )
            elif kind == 'transfinite_volume':
                for d in items:
                    corners = d['corners'] or "auto"
                    lines.append(
                        f"    volume={d['tag']}  corners={corners}"
                    )
            elif kind == 'transfinite_automatic':
                for d in items:
                    angle_deg = _math.degrees(d['corner_angle'])
                    lines.append(
                        f"    corner_angle={angle_deg:.1f}°  "
                        f"recombine={d['recombine']}  "
                        f"dim_tags={d['dim_tags'] or 'all'}"
                    )
            elif kind == 'recombine':
                for d in items:
                    lines.append(
                        f"    dim={d['dim']}  tag={d['tag']}  "
                        f"angle={d['angle']}°"
                    )
            elif kind == 'smoothing':
                for d in items:
                    lines.append(
                        f"    dim={d['dim']}  tag={d['tag']}  "
                        f"passes={d['val']}"
                    )
            elif kind == 'algorithm':
                for d in items:
                    alg_name = ""
                    if d['dim'] == 2:
                        alg_name = _ALGO_2D.get(d['algorithm'], '?')
                    elif d['dim'] == 3:
                        alg_name = _ALGO_3D.get(d['algorithm'], '?')
                    lines.append(
                        f"    dim={d['dim']}  tag={d['tag']}  "
                        f"algorithm={d['algorithm']} ({alg_name})"
                    )
            elif kind == 'field_add':
                for d in items:
                    lines.append(
                        f"    field_tag={d['field_tag']}  "
                        f"type={d['field_type']}"
                    )
            elif kind == 'field_background':
                for d in items:
                    lines.append(
                        f"    background_field={d['field_tag']}"
                    )
            else:
                for d in items:
                    lines.append(f"    {d}")

    # ==============================================================
    # 5. MESH STATISTICS  [gmsh]
    # ==============================================================
    _section("MESH STATISTICS  [gmsh]")
    try:
        node_tags, _, _ = gmsh.model.mesh.getNodes(
            dim=-1, tag=-1, includeBoundary=True
        )
        n_nodes = len(node_tags)
    except Exception:
        n_nodes = 0

    if n_nodes == 0:
        lines.append("  (no mesh generated yet)")
    else:
        lines.append(f"  Total nodes: {n_nodes}")

        # Per-dim element breakdown
        _sub("Elements by type")
        type_totals: dict[str, int] = {}
        for dim in range(4):
            for _, tag in gmsh.model.getEntities(dim=dim):
                etypes, etags, _ = gmsh.model.mesh.getElements(
                    dim=dim, tag=tag,
                )
                for et, tags_arr in zip(etypes, etags):
                    name, _, order, n_per, *_ = (
                        gmsh.model.mesh.getElementProperties(et)
                    )
                    n = len(tags_arr)
                    type_totals[name] = type_totals.get(name, 0) + n

        total_elems = 0
        for ename, count in sorted(type_totals.items()):
            lines.append(f"    {ename:30s}: {count:>8d}")
            total_elems += count
        lines.append(f"    {'TOTAL':30s}: {total_elems:>8d}")

        # Per-physical-group node counts
        pgs = gmsh.model.getPhysicalGroups()
        if pgs:
            _sub("Nodes per physical group")
            for dim, pg_tag in pgs:
                name = gmsh.model.getPhysicalName(dim, pg_tag) or f"(tag={pg_tag})"
                try:
                    pg_nodes, _ = gmsh.model.mesh.getNodesForPhysicalGroup(
                        dim, pg_tag,
                    )
                    lines.append(
                        f"    dim={dim}  {name!r:30s}: "
                        f"{len(pg_nodes):>8d} nodes"
                    )
                except Exception:
                    lines.append(
                        f"    dim={dim}  {name!r:30s}: (error)"
                    )

        # Quality snapshot (aggregate by element type, dim>=2 only)
        _sub("Element quality (SICN)")
        has_quality = False
        for dim in (2, 3):
            for _, tag in gmsh.model.getEntities(dim=dim):
                etypes, etags, _ = gmsh.model.mesh.getElements(
                    dim=dim, tag=tag,
                )
                for et, tags_arr in zip(etypes, etags):
                    if len(tags_arr) == 0:
                        continue
                    name, *_ = gmsh.model.mesh.getElementProperties(et)
                    try:
                        q = np.asarray(
                            gmsh.model.mesh.getElementQualities(
                                list(tags_arr), qualityName='minSICN',
                            )
                        )
                        has_quality = True
                        lines.append(
                            f"    {name:24s}  "
                            f"n={len(tags_arr):>6d}  "
                            f"min={q.min():.4f}  "
                            f"mean={q.mean():.4f}  "
                            f"max={q.max():.4f}"
                        )
                    except Exception:
                        pass
        if not has_quality:
            lines.append("    (no 2D/3D elements to measure)")

    # ==============================================================
    # Footer
    # ==============================================================
    lines.append("")
    lines.append(_hr)

    text = "\n".join(lines)
    print(text)
    return text

VTK export

apeGmsh.viz.VTKExport.VTKExport

VTKExport(ctx: apeGmsh, dim: int | None = 2)

Convenience wrapper: accumulate fields, then write one .vtu.

vtk = VTKExport(g) # g is a apeGmsh instance vtk.add_node_scalar("T", T) vtk.add_node_vector("u", disp) vtk.add_elem_scalar("sig_xx", sig) vtk.write("results.vtu")

Source code in src/apeGmsh/viz/VTKExport.py
def __init__(self, ctx: apeGmsh, dim: int | None = 2) -> None:
    fem = ctx.mesh.queries.get_fem_data(dim=dim)
    self._node_coords = fem.nodes.coords
    self._elem_tags   = fem.elements.ids
    self._node_ids    = fem.nodes.ids
    self._fem         = fem

    # Build node-ID -> 0-based array index mapping
    tag_to_idx = {int(t): i for i, t in enumerate(fem.nodes.ids)}

    # Build per-group 0-based connectivity and VTK types
    self._cell_blocks: list[tuple[np.ndarray, int]] = []
    for group in fem.elements:
        if group.connectivity.size == 0:
            continue
        conn_0 = np.array(
            [[tag_to_idx[int(n)] for n in row]
             for row in group.connectivity])
        vtk_type = {
            (1, 2): VTK_LINE,
            (2, 3): VTK_TRIANGLE, (2, 4): VTK_QUAD,
            (3, 4): 10, (3, 6): VTK_WEDGE, (3, 8): VTK_HEXAHEDRON,
        }.get((group.dim, group.npe), VTK_QUAD)
        self._cell_blocks.append((conn_0, vtk_type))

    self._point_data: dict[str, np.ndarray] = {}
    self._cell_data:  dict[str, np.ndarray] = {}

add_node_scalar

add_node_scalar(name: str, data: ndarray) -> None

Add a nodal scalar field (one value per node).

Source code in src/apeGmsh/viz/VTKExport.py
def add_node_scalar(self, name: str, data: np.ndarray) -> None:
    """Add a nodal scalar field (one value per node)."""
    self._point_data[name] = np.asarray(data, dtype=np.float64).ravel()

add_node_vector

add_node_vector(name: str, data: ndarray) -> None

Add a nodal vector field (3 components per node).

If data is (N, 2), a zero z-component is appended automatically.

Source code in src/apeGmsh/viz/VTKExport.py
def add_node_vector(self, name: str, data: np.ndarray) -> None:
    """Add a nodal vector field (3 components per node).

    If data is (N, 2), a zero z-component is appended automatically.
    """
    arr = np.asarray(data, dtype=np.float64)
    if arr.ndim == 1:
        raise ValueError(f"Vector field '{name}' must be 2D, got shape {arr.shape}")
    if arr.shape[1] == 2:
        arr = np.column_stack([arr, np.zeros(arr.shape[0])])
    self._point_data[name] = arr

add_elem_scalar

add_elem_scalar(name: str, data: ndarray) -> None

Add an element scalar field (one value per element).

Source code in src/apeGmsh/viz/VTKExport.py
def add_elem_scalar(self, name: str, data: np.ndarray) -> None:
    """Add an element scalar field (one value per element)."""
    self._cell_data[name] = np.asarray(data, dtype=np.float64).ravel()

add_elem_vector

add_elem_vector(name: str, data: ndarray) -> None

Add an element vector field (3 components per element).

Source code in src/apeGmsh/viz/VTKExport.py
def add_elem_vector(self, name: str, data: np.ndarray) -> None:
    """Add an element vector field (3 components per element)."""
    arr = np.asarray(data, dtype=np.float64)
    if arr.shape[1] == 2:
        arr = np.column_stack([arr, np.zeros(arr.shape[0])])
    self._cell_data[name] = arr

write

write(filename: str | Path = 'results.vtu') -> Path

Write accumulated fields to a .vtu file.

Source code in src/apeGmsh/viz/VTKExport.py
def write(self, filename: str | Path = "results.vtu") -> Path:
    """Write accumulated fields to a .vtu file."""
    conn_0, vtk_type = self._primary_conn_and_type()
    return write_vtu(
        filename,
        self._node_coords,
        conn_0,
        vtk_cell_type=vtk_type,
        point_data=self._point_data,
        cell_data=self._cell_data,
    )

write_mode_series

write_mode_series(base_name: str, mode_shapes: list[ndarray], frequencies: list[float]) -> list[Path]

Write mode shapes as a time-series PVD for ParaView animation.

Source code in src/apeGmsh/viz/VTKExport.py
def write_mode_series(self, base_name: str,
                      mode_shapes: list[np.ndarray],
                      frequencies: list[float]) -> list[Path]:
    """Write mode shapes as a time-series PVD for ParaView animation."""
    steps = []
    for i, (phi, freq) in enumerate(zip(mode_shapes, frequencies)):
        phi3 = np.asarray(phi, dtype=np.float64)
        if phi3.ndim == 2 and phi3.shape[1] > 3:
            phi3 = phi3[:, :3]
        if phi3.ndim == 2 and phi3.shape[1] == 2:
            phi3 = np.column_stack([phi3, np.zeros(phi3.shape[0])])

        mag = np.sqrt(np.sum(phi3**2, axis=1))

        steps.append({
            "time": freq,
            "point_data": {
                "ModeShape": phi3,
                "Magnitude": mag,
            },
        })

    conn_0, vtk_type = self._primary_conn_and_type()
    return write_vtu_series(
        base_name,
        self._node_coords,
        conn_0,
        vtk_cell_type=vtk_type,
        steps=steps,
    )

Notebook preview

apeGmsh.viz.NotebookPreview.preview

preview(session: Any = None, *, mode: str = 'mesh', dims: list[int] | None = None, show_nodes: bool = True, browser: bool = False, return_fig: bool = False) -> Any

Unified entry point — routes to preview_model or preview_mesh.

Parameters

mode : {"model", "mesh"} Which scene to render. Default "mesh". show_nodes : bool Mesh mode only — render the full mesh-node cloud as a separate trace. Ignored in model mode. browser : bool Open in a new browser tab instead of rendering inline. return_fig : bool Skip display and return the raw plotly Figure.

Source code in src/apeGmsh/viz/NotebookPreview.py
def preview(
    session: Any = None,
    *,
    mode: str = "mesh",
    dims: list[int] | None = None,
    show_nodes: bool = True,
    browser: bool = False,
    return_fig: bool = False,
) -> Any:
    """Unified entry point — routes to ``preview_model`` or ``preview_mesh``.

    Parameters
    ----------
    mode : {"model", "mesh"}
        Which scene to render. Default ``"mesh"``.
    show_nodes : bool
        Mesh mode only — render the full mesh-node cloud as a
        separate trace. Ignored in model mode.
    browser : bool
        Open in a new browser tab instead of rendering inline.
    return_fig : bool
        Skip display and return the raw plotly ``Figure``.
    """
    if mode == "model":
        return preview_model(
            session, dims=dims, browser=browser, return_fig=return_fig,
        )
    if mode == "mesh":
        return preview_mesh(
            session, dims=dims, show_nodes=show_nodes,
            browser=browser, return_fig=return_fig,
        )
    raise ValueError(f"Unknown preview mode: {mode!r} (expected 'model' or 'mesh')")