Model — g.model
OCC geometry composite. Five focused sub-composites: geometry, boolean,
transforms, io, queries.
g.model
apeGmsh.core.Model.Model
Model(parent: '_SessionBase')
Bases: _HasLogging
Geometry composite attached to an apeGmsh instance as
g.model. Owns five focused sub-composites:
g.model.geometry — point / curve / surface / solid primitives
g.model.boolean — fuse, cut, intersect, fragment
g.model.transforms — translate, rotate, scale, mirror, copy,
extrude, revolve, sweep, thru_sections
g.model.io — load/save STEP, IGES, DXF, MSH, heal_shapes
g.model.queries — bounding_box, center_of_mass, mass,
boundary, adjacencies, entities_in_bounding_box, registry
Plus entity selection:
g.model.select(...) — fluent spatial entity selection
And top-level utilities on the Model itself:
g.model.sync() — flush the OCC kernel
g.model.viewer() — open the interactive Qt viewer
g.model.gui() / g.model.launch_picker() — native Gmsh viewers
Example
::
# Solid boolean workflow
box = g.model.geometry.add_box(0, 0, 0, 10, 10, 10)
hole = g.model.geometry.add_cylinder(5, 5, 0, 0, 0, 10, 2)
part = g.model.boolean.cut(box, hole)
# Wire-frame -> surface workflow
p1 = g.model.geometry.add_point(0, 0, 0)
p2 = g.model.geometry.add_point(10, 0, 0)
p3 = g.model.geometry.add_point(10, 5, 0)
p4 = g.model.geometry.add_point(0, 5, 0)
l1 = g.model.geometry.add_line(p1, p2)
l2 = g.model.geometry.add_line(p2, p3)
l3 = g.model.geometry.add_line(p3, p4)
l4 = g.model.geometry.add_line(p4, p1)
loop = g.model.geometry.add_curve_loop([l1, l2, l3, l4])
surf = g.model.geometry.add_plane_surface(loop)
Parameters
parent : _SessionBase
Owning session — used to read _verbose and name.
Source code in src/apeGmsh/core/Model.py
| def __init__(self, parent: "_SessionBase") -> None:
self._parent = parent
# (dim, tag) -> {kind, ...} (labels live in g.labels, not here)
self._metadata: dict[DimTag, dict] = {}
# Five focused sub-composites — each one holds a reference to self
self.geometry = _Geometry(self)
self.boolean = _Boolean(self)
self.transforms = _Transforms(self)
self.io = _IO(self)
self.queries = _Queries(self)
|
sync
Synchronise the OCC kernel with the gmsh model topology.
Call this explicitly when you have been batching operations with
sync=False. Returns self for chaining.
Source code in src/apeGmsh/core/Model.py
| def sync(self) -> "Model":
"""
Synchronise the OCC kernel with the gmsh model topology.
Call this explicitly when you have been batching operations with
``sync=False``. Returns ``self`` for chaining.
"""
gmsh.model.occ.synchronize()
self._log("OCC kernel synchronised")
return self
|
select
select(target=None, *, dim: int | None = None)
Start a fluent, daisy-chainable CAD-entity selection.
This is the geometry entry of the unified selection family
(docs/plans/selection-unification-v2.md). It is the single
entity-selection surface: the former
g.model.queries.select(tags, on=/crossing=...) predicate
selector and the former g.model.selection.select_* entity
composite have been removed; their behaviour is folded into the
verbs below.
select() returns an
:class:~apeGmsh.core._selection.EntitySelection (entity
family) whose verbs (in_box, in_sphere, on_plane,
crossing_plane, nearest_to, where, | & -
^) compose, and whose direct terminals .to_label() /
.to_physical() / .to_dataframe() consume it without a
.result() step. .result() is a zero-cost identity alias
yielding the :class:~apeGmsh.core._selection.Selection
payload, on which .tags() / .to_label() /
.to_physical() are also available.
Name resolution is delegated verbatim to the existing,
contract-locked geometry resolver
(:func:apeGmsh.core._helpers.resolve_to_dimtags): a string is
resolved label (Tier 1) -> physical group (Tier 2) -> part
(Tier 3); (dim, tag) / int / lists pass through with the
same semantics as everywhere else. This method re-implements
none of that tier logic, and adds no scoping or
boundary-walk behaviour of its own — what
resolve_to_dimtags returns is exactly what seeds the chain
(no silent truncation). To narrow to a specific dimension,
seed with a dimension-appropriate label / PG, or refine with a
spatial verb (.in_box / .on_plane / ...).
Parameters
target :
What to seed the chain with. Any reference
resolve_to_dimtags accepts — a label / PG / part name
string, a bare int tag, a (dim, tag) pair, or a list
thereof. None selects every entity at dim (which
then must be given), via resolve_to_dimtags(None,
default_dim=dim).
dim :
The default_dim forwarded to resolve_to_dimtags —
i.e. the dimension used for bare int tags and for
target=None. It is not a post-filter (that would be
a silent truncation the resolution contract forbids); a
multi-dim label still enumerates every dim it occupies, per
the locked contract. Defaults to 3 (the resolver's own
default) when not given.
Returns
EntitySelection
Example
::
# seed with a face physical group, then refine spatially
(g.model.select("BottomFaces")
.in_box((0, 0, 0), (1, 1, 0.5))
.to_physical("lower_faces"))
Source code in src/apeGmsh/core/Model.py
| def select(self, target=None, *, dim: int | None = None):
"""Start a fluent, daisy-chainable CAD-entity selection.
This is the **geometry entry** of the unified selection family
(``docs/plans/selection-unification-v2.md``). It is the single
entity-selection surface: the former
``g.model.queries.select(tags, on=/crossing=...)`` predicate
selector and the former ``g.model.selection.select_*`` entity
composite have been removed; their behaviour is folded into the
verbs below.
``select()`` returns an
:class:`~apeGmsh.core._selection.EntitySelection` (entity
family) whose verbs (``in_box``, ``in_sphere``, ``on_plane``,
``crossing_plane``, ``nearest_to``, ``where``, ``|`` ``&`` ``-``
``^``) compose, and whose direct terminals ``.to_label()`` /
``.to_physical()`` / ``.to_dataframe()`` consume it without a
``.result()`` step. ``.result()`` is a zero-cost identity alias
yielding the :class:`~apeGmsh.core._selection.Selection`
payload, on which ``.tags()`` / ``.to_label()`` /
``.to_physical()`` are also available.
Name resolution is delegated **verbatim** to the existing,
contract-locked geometry resolver
(:func:`apeGmsh.core._helpers.resolve_to_dimtags`): a string is
resolved label (Tier 1) -> physical group (Tier 2) -> part
(Tier 3); ``(dim, tag)`` / ``int`` / lists pass through with the
same semantics as everywhere else. This method re-implements
none of that tier logic, and adds **no** scoping or
boundary-walk behaviour of its own — what
``resolve_to_dimtags`` returns is exactly what seeds the chain
(no silent truncation). To narrow to a specific dimension,
seed with a dimension-appropriate label / PG, or refine with a
spatial verb (``.in_box`` / ``.on_plane`` / ...).
Parameters
----------
target :
What to seed the chain with. Any reference
``resolve_to_dimtags`` accepts — a label / PG / part name
string, a bare int tag, a ``(dim, tag)`` pair, or a list
thereof. ``None`` selects every entity at ``dim`` (which
then must be given), via ``resolve_to_dimtags(None,
default_dim=dim)``.
dim :
The ``default_dim`` forwarded to ``resolve_to_dimtags`` —
i.e. the dimension used for bare int tags and for
``target=None``. It is **not** a post-filter (that would be
a silent truncation the resolution contract forbids); a
multi-dim label still enumerates every dim it occupies, per
the locked contract. Defaults to ``3`` (the resolver's own
default) when not given.
Returns
-------
EntitySelection
Example
-------
::
# seed with a face physical group, then refine spatially
(g.model.select("BottomFaces")
.in_box((0, 0, 0), (1, 1, 0.5))
.to_physical("lower_faces"))
"""
# Deferred import — the established idiom (mirrors
# mesh/_mesh_structured.py). ``_selection`` is same-package, so
# this adds no eager cross-package edge
# (tests/test_import_dag_polarity.py stays green with the
# baseline unchanged).
from ._helpers import resolve_to_dimtags
from ._selection import EntitySelection
if target is None and dim is None:
raise ValueError(
"model.select(): pass a target (label / PG / part / "
"(dim, tag) / int / list) or a dim= to select every "
"entity at that dimension."
)
dimtags = resolve_to_dimtags(
target,
default_dim=3 if dim is None else dim,
session=self._parent,
)
# selection-unification-v2: the host hook returns the v2
# terminal ``EntitySelection`` (the entity-family
# chain==terminal). Same deferred-import idiom; no new eager
# cross-package edge.
return EntitySelection(dimtags, _engine=self.queries)
|
viewer
Open the interactive Qt model viewer.
Displays BRep geometry with selectable entities, parts,
physical groups, and labels. This is a geometry-only
viewer — loads, constraints, and masses are mesh-resolved
concepts and live on g.mesh.viewer() instead.
Parameters
**kwargs :
Forwarded to
:class:~apeGmsh.viewers.model_viewer.ModelViewer
(e.g. physical_group, dims, point_size,
line_width, surface_opacity).
Source code in src/apeGmsh/core/Model.py
| def viewer(self, **kwargs):
"""Open the interactive Qt model viewer.
Displays BRep geometry with selectable entities, parts,
physical groups, and labels. This is a **geometry-only**
viewer — loads, constraints, and masses are mesh-resolved
concepts and live on ``g.mesh.viewer()`` instead.
Parameters
----------
**kwargs :
Forwarded to
:class:`~apeGmsh.viewers.model_viewer.ModelViewer`
(e.g. ``physical_group``, ``dims``, ``point_size``,
``line_width``, ``surface_opacity``).
"""
# selection-unification v2 P3-R / §6.3 §4 SC-7: inline the
# former ``self.selection.picker(**kwargs)`` (SelectionComposite
# removed by M-STOP-2). ``model=self`` is the identical object
# the deleted ``picker`` passed as ``model=self._model``.
from apeGmsh.viewers.model_viewer import ModelViewer
p = ModelViewer(parent=self._parent, model=self, **kwargs)
p.show()
return p
|
preview
preview(*, dims: list[int] | None = None, browser: bool = False, return_fig: bool = False)
Interactive WebGL preview of the BRep geometry.
Zero Qt dependency — works inline in Jupyter / VS Code / Colab,
or in a dedicated browser tab when browser=True. Hover over
a cell to see its dim and tag.
Parameters
dims : list of int, optional
BRep dimensions to render. Defaults to [0, 1, 2, 3].
browser : bool
If True, open in a new browser tab (temp HTML file)
instead of rendering inline. Useful when the notebook
output is cluttered or you want a dedicated window.
return_fig : bool
If True, skip display and return the raw
:class:plotly.graph_objects.Figure for saving with
fig.write_html('path.html') or composing a notebook
layout.
Source code in src/apeGmsh/core/Model.py
| def preview(
self,
*,
dims: list[int] | None = None,
browser: bool = False,
return_fig: bool = False,
):
"""Interactive WebGL preview of the BRep geometry.
Zero Qt dependency — works inline in Jupyter / VS Code / Colab,
or in a dedicated browser tab when ``browser=True``. Hover over
a cell to see its ``dim`` and ``tag``.
Parameters
----------
dims : list of int, optional
BRep dimensions to render. Defaults to ``[0, 1, 2, 3]``.
browser : bool
If ``True``, open in a new browser tab (temp HTML file)
instead of rendering inline. Useful when the notebook
output is cluttered or you want a dedicated window.
return_fig : bool
If ``True``, skip display and return the raw
:class:`plotly.graph_objects.Figure` for saving with
``fig.write_html('path.html')`` or composing a notebook
layout.
"""
from apeGmsh.viz.NotebookPreview import preview_model
return preview_model(
self._parent,
dims=dims,
browser=browser,
return_fig=return_fig,
)
|
gui
Open the interactive Gmsh FLTK GUI window.
Source code in src/apeGmsh/core/Model.py
| def gui(self) -> None:
"""Open the interactive Gmsh FLTK GUI window."""
gmsh.fltk.run()
|
launch_picker
launch_picker(*, show_points: bool = True, show_curves: bool = True, show_surfaces: bool = True, show_volumes: bool = False, verbose: bool = True) -> None
Open Gmsh's native FLTK viewer with entity labels pre-enabled.
Source code in src/apeGmsh/core/Model.py
| def launch_picker(
self,
*,
show_points: bool = True,
show_curves: bool = True,
show_surfaces: bool = True,
show_volumes: bool = False,
verbose: bool = True,
) -> None:
"""Open Gmsh's native FLTK viewer with entity labels pre-enabled."""
gmsh.model.occ.synchronize()
gmsh.option.setNumber("Geometry.PointLabels", int(show_points))
gmsh.option.setNumber("Geometry.CurveLabels", int(show_curves))
gmsh.option.setNumber("Geometry.SurfaceLabels", int(show_surfaces))
gmsh.option.setNumber("Geometry.VolumeLabels", int(show_volumes))
gmsh.option.setNumber("Geometry.Points", 1)
gmsh.option.setNumber("Geometry.Curves", 1)
gmsh.option.setNumber("Geometry.Surfaces", 1)
if verbose:
print("[launch_picker] Opening Gmsh FLTK window.")
print(" Labels visible — read tags off the 3D view.")
print(" Close the window to return here.")
gmsh.fltk.run()
|
Sub-composites
g.model.geometry
apeGmsh.core._model_geometry._Geometry
_Geometry(model: 'Model')
Points, curves, surfaces, and solid primitive creation methods.
Source code in src/apeGmsh/core/_model_geometry.py
| def __init__(self, model: "Model") -> None:
self._model = model
|
add_point
add_point(x: float, y: float, z: float, *, mesh_size: float = 0.0, lc: float | None = None, label: str | None = None, sync: bool = True) -> Tag
Add a single point.
Parameters
x, y, z : coordinates
mesh_size : target element size at this point (0 = use global size)
lc : alias for mesh_size (Gmsh characteristic length)
Returns
int tag of the new point.
Source code in src/apeGmsh/core/_model_geometry.py
| def add_point(
self,
x: float, y: float, z: float,
*,
mesh_size: float = 0.0,
lc : float | None = None,
label : str | None = None,
sync : bool = True,
) -> Tag:
"""
Add a single point.
Parameters
----------
x, y, z : coordinates
mesh_size : target element size at this point (0 = use global size)
lc : alias for *mesh_size* (Gmsh characteristic length)
Returns
-------
int tag of the new point.
"""
if lc is not None:
mesh_size = lc
tag = gmsh.model.occ.addPoint(x, y, z, meshSize=mesh_size)
if sync:
gmsh.model.occ.synchronize()
self._model._log(f"add_point({x}, {y}, {z}) -> tag {tag}")
return self._model._register(0, tag, label, 'point')
|
add_line
add_line(start: EntityRef, end: EntityRef, *, label: str | None = None, sync: bool = True) -> Tag
Add a straight line segment between two existing points.
Parameters
start, end : point references — raw tag, label name, physical
group, part label, or (dim, tag). Each must resolve to
exactly one point.
Source code in src/apeGmsh/core/_model_geometry.py
| def add_line(
self,
start: EntityRef,
end : EntityRef,
*,
label: str | None = None,
sync : bool = True,
) -> Tag:
"""
Add a straight line segment between two existing points.
Parameters
----------
start, end : point references — raw tag, label name, physical
group, part label, or ``(dim, tag)``. Each must resolve to
exactly one point.
"""
start = self._resolve_entity_tag(start, dim=0, what="point")
end = self._resolve_entity_tag(end, dim=0, what="point")
tag = gmsh.model.occ.addLine(start, end)
if sync:
gmsh.model.occ.synchronize()
self._model._log(f"add_line({start} -> {end}) -> tag {tag}")
return self._model._register(1, tag, label, 'line')
|
add_imperfect_line
add_imperfect_line(start: EntityRef, end: EntityRef, *, magnitude: float = 0.0, direction: tuple[float, float, float], shape: Literal['kink', 'sine', 'multi_mode'] = 'kink', n_segments: int = 8, modes: list[tuple[int, float]] | None = None, label: str | None = None, sync: bool = True) -> list[Tag]
Add a line with a built-in geometric imperfection.
Used for seeding initial out-of-straightness on columns, struts,
or braces before running a corotational / nonlinear buckling
analysis. The imperfection is baked into the geometry as a
polyline through intermediate points — there is no solver-side
perturbation. The resulting line segments are a drop-in
replacement for a single :meth:add_line call and can be
grouped into a single physical group via
m.physical.add_curve(tags=[...]).
Parameters
start, end : point tags
Endpoints of the imperfect line (straight-line length L).
magnitude : float
Peak perpendicular offset of the imperfection envelope.
Typical engineering choices are L/500 to L/1000.
Ignored when shape='multi_mode' — amplitudes come from
the per-mode entries of modes.
direction : (dx, dy, dz)
Direction hint for the offset. The vector is projected onto
the plane perpendicular to the line axis and normalized.
Only the perpendicular component matters; e.g. for a
diagonal brace you can pass (0, 1, 0) to request an
out-of-plane-Y offset and the method takes care of
orthogonality. Raises ValueError if the vector is
parallel to the line axis.
shape : str
* 'kink' — single midspan intermediate point, two
line segments. Produces a triangular bent; pedagogically
clean but not physically smooth.
* 'sine' — half-sine envelope
y(s) = magnitude · sin(π·s/L) discretized into
n_segments pieces. Matches the first Euler buckling
mode exactly; recommended for quantitative work.
* 'multi_mode' — superposition of multiple sinusoidal
modes, y(s) = Σ_k a_k · sin(k·π·s/L) where
(k, a_k) pairs come from modes. Used to seed
more than one buckling mode at once.
n_segments : int
Number of line pieces the imperfect line is split into.
Only meaningful for 'sine' (default 8) and
'multi_mode' (default 8). 'kink' always uses
exactly 2 segments regardless of this value.
modes : list[(int, float)], optional
Required when shape='multi_mode'. Each entry is a
(mode_number, amplitude) pair. The mode number k
must be a positive integer; the amplitude is absolute (not
relative to magnitude).
label : str, optional
Label applied to all resulting line segments. The
intermediate interior points remain anonymous (no labels).
sync : bool
Whether to synchronise the OCC kernel after creation.
Returns
list[Tag]
Line tags in geometric order from start to end.
Pass the list directly to m.physical.add_curve.
Examples
Kinked brace with an L/1000 midspan offset in the global-Y
direction::
tags = m.model.geometry.add_imperfect_line(
p_base, p_top,
magnitude=L_brace/1000,
direction=(0, 1, 0),
shape='kink',
label='brace',
)
Half-sine imperfection discretised into 16 segments::
tags = m.model.geometry.add_imperfect_line(
p1, p2,
magnitude=L/500,
direction=(1, 0, 0),
shape='sine',
n_segments=16,
label='column',
)
First + third mode seeding::
tags = m.model.geometry.add_imperfect_line(
p1, p2,
direction=(0, 0, 1),
shape='multi_mode',
modes=[(1, L/1000), (3, L/5000)],
n_segments=24,
)
Source code in src/apeGmsh/core/_model_geometry.py
| def add_imperfect_line(
self,
start: EntityRef,
end : EntityRef,
*,
magnitude : float = 0.0,
direction : tuple[float, float, float],
shape : Literal['kink', 'sine', 'multi_mode'] = 'kink',
n_segments: int = 8,
modes : list[tuple[int, float]] | None = None,
label : str | None = None,
sync : bool = True,
) -> list[Tag]:
"""
Add a line with a built-in geometric imperfection.
Used for seeding initial out-of-straightness on columns, struts,
or braces before running a corotational / nonlinear buckling
analysis. The imperfection is baked into the **geometry** as a
polyline through intermediate points — there is no solver-side
perturbation. The resulting line segments are a drop-in
replacement for a single :meth:`add_line` call and can be
grouped into a single physical group via
``m.physical.add_curve(tags=[...])``.
Parameters
----------
start, end : point tags
Endpoints of the imperfect line (straight-line length L).
magnitude : float
Peak perpendicular offset of the imperfection envelope.
Typical engineering choices are ``L/500`` to ``L/1000``.
Ignored when ``shape='multi_mode'`` — amplitudes come from
the per-mode entries of ``modes``.
direction : (dx, dy, dz)
Direction hint for the offset. The vector is projected onto
the plane perpendicular to the line axis and normalized.
Only the perpendicular component matters; e.g. for a
diagonal brace you can pass ``(0, 1, 0)`` to request an
out-of-plane-Y offset and the method takes care of
orthogonality. Raises ``ValueError`` if the vector is
parallel to the line axis.
shape : str
* ``'kink'`` — single midspan intermediate point, two
line segments. Produces a triangular bent; pedagogically
clean but not physically smooth.
* ``'sine'`` — half-sine envelope
``y(s) = magnitude · sin(π·s/L)`` discretized into
``n_segments`` pieces. Matches the first Euler buckling
mode exactly; recommended for quantitative work.
* ``'multi_mode'`` — superposition of multiple sinusoidal
modes, ``y(s) = Σ_k a_k · sin(k·π·s/L)`` where
``(k, a_k)`` pairs come from ``modes``. Used to seed
more than one buckling mode at once.
n_segments : int
Number of line pieces the imperfect line is split into.
Only meaningful for ``'sine'`` (default 8) and
``'multi_mode'`` (default 8). ``'kink'`` always uses
exactly 2 segments regardless of this value.
modes : list[(int, float)], optional
Required when ``shape='multi_mode'``. Each entry is a
``(mode_number, amplitude)`` pair. The mode number ``k``
must be a positive integer; the amplitude is absolute (not
relative to ``magnitude``).
label : str, optional
Label applied to *all* resulting line segments. The
intermediate interior points remain anonymous (no labels).
sync : bool
Whether to synchronise the OCC kernel after creation.
Returns
-------
list[Tag]
Line tags in geometric order from ``start`` to ``end``.
Pass the list directly to ``m.physical.add_curve``.
Examples
--------
Kinked brace with an L/1000 midspan offset in the global-Y
direction::
tags = m.model.geometry.add_imperfect_line(
p_base, p_top,
magnitude=L_brace/1000,
direction=(0, 1, 0),
shape='kink',
label='brace',
)
Half-sine imperfection discretised into 16 segments::
tags = m.model.geometry.add_imperfect_line(
p1, p2,
magnitude=L/500,
direction=(1, 0, 0),
shape='sine',
n_segments=16,
label='column',
)
First + third mode seeding::
tags = m.model.geometry.add_imperfect_line(
p1, p2,
direction=(0, 0, 1),
shape='multi_mode',
modes=[(1, L/1000), (3, L/5000)],
n_segments=24,
)
"""
# ── Resolve endpoint references to point tags ───────────
start = self._resolve_entity_tag(start, dim=0, what="point")
end = self._resolve_entity_tag(end, dim=0, what="point")
# ── Resolve endpoint coordinates ────────────────────────
p0 = np.asarray(
gmsh.model.getValue(0, int(start), []), dtype=float)
p1 = np.asarray(
gmsh.model.getValue(0, int(end), []), dtype=float)
axis_vec = p1 - p0
L = float(np.linalg.norm(axis_vec))
if L <= 0.0:
raise ValueError(
f"add_imperfect_line: zero-length axis "
f"({start} -> {end}); endpoints coincide.")
axis_unit = axis_vec / L
# ── Project direction perpendicular to axis ─────────────
dir_arr = np.asarray(direction, dtype=float)
if dir_arr.shape != (3,):
raise ValueError(
f"direction must be a length-3 vector, got shape "
f"{dir_arr.shape}")
perp = dir_arr - float(np.dot(dir_arr, axis_unit)) * axis_unit
perp_mag = float(np.linalg.norm(perp))
if perp_mag < 1e-12 * max(L, 1.0):
raise ValueError(
f"direction {tuple(direction)} is (nearly) parallel to "
f"the line axis {tuple(axis_unit)}; pick a direction "
f"with a perpendicular component.")
perp_unit = perp / perp_mag
# ── Build the (arc-length fraction, offset) pairs ───────
if shape == 'kink':
s_list = [0.5]
offset_list = [magnitude]
n_interior = 1
elif shape == 'sine':
if n_segments < 2:
raise ValueError(
f"n_segments must be >= 2 for shape='sine', "
f"got {n_segments}")
s_list = [i / n_segments for i in range(1, n_segments)]
offset_list = [
magnitude * math.sin(math.pi * s) for s in s_list
]
n_interior = n_segments - 1
elif shape == 'multi_mode':
if not modes:
raise ValueError(
"shape='multi_mode' requires a non-empty 'modes' "
"list of (mode_number, amplitude) pairs.")
if n_segments < 2:
raise ValueError(
f"n_segments must be >= 2 for shape='multi_mode', "
f"got {n_segments}")
s_list = [i / n_segments for i in range(1, n_segments)]
offset_list = []
for s in s_list:
total = 0.0
for k, a in modes:
if k <= 0 or int(k) != k:
raise ValueError(
f"mode number must be a positive integer, "
f"got {k}")
total += float(a) * math.sin(int(k) * math.pi * s)
offset_list.append(total)
n_interior = n_segments - 1
else:
raise ValueError(
f"shape must be 'kink', 'sine', or 'multi_mode'; "
f"got {shape!r}")
# ── Create intermediate points + segment lines ──────────
line_tags: list[Tag] = []
prev = int(start)
for s, off in zip(s_list, offset_list):
pos = p0 + s * (p1 - p0) + off * perp_unit
pt = gmsh.model.occ.addPoint(
float(pos[0]), float(pos[1]), float(pos[2]))
ln = gmsh.model.occ.addLine(prev, pt)
line_tags.append(ln)
prev = pt
# Final segment from last interior point to the end.
ln = gmsh.model.occ.addLine(prev, int(end))
line_tags.append(ln)
if sync:
gmsh.model.occ.synchronize()
# Register metadata for every segment (kind='imperfect_line').
# Labels are skipped here: if we passed ``label`` to ``_register``
# it would call ``labels.add`` once per segment and emit a
# "duplicate label" warning on every call after the first. We
# batch them in a single ``labels.add`` below so the label PG
# contains all segments from the start.
for ln in line_tags:
self._model._register(1, ln, None, 'imperfect_line')
# Batch-label all segments under one name (if requested).
if label and getattr(self._model._parent, '_auto_pg_from_label', False):
labels_comp = getattr(self._model._parent, 'labels', None)
if labels_comp is not None:
try:
labels_comp.add(1, list(line_tags), name=label)
except Exception as exc:
import warnings as _warn
_warn.warn(
f"Label {label!r} (dim=1, tags={line_tags}) could "
f"not be created: {exc}",
stacklevel=2,
)
self._model._log(
f"add_imperfect_line({start} -> {end}, shape={shape!r}, "
f"n_interior={n_interior}, magnitude={magnitude}) "
f"-> {len(line_tags)} segment(s) {line_tags}")
return list(line_tags)
|
replace_line
replace_line(line_tag: Tag, *, magnitude: float = 0.0, direction: tuple[float, float, float], shape: Literal['kink', 'sine', 'multi_mode'] = 'kink', n_segments: int = 8, modes: list[tuple[int, float]] | None = None, sync: bool = True) -> list[Tag]
Retrofit an existing straight line with a geometric imperfection.
Use this when you built the frame with plain :meth:add_line
calls and then want to introduce an imperfection on a specific
member without rebuilding the whole geometry. The method:
- Validates that
line_tag points to a straight line
(kind='line' in the model metadata; arcs, splines, and
already-imperfect lines are rejected).
- Looks up the two endpoint points from the line's boundary.
- Records every physical group (user-facing PGs and label
PGs of the form
_label:…) that contains the line.
- Deletes the old curve — endpoints are preserved because
other geometry likely references them.
- Calls :meth:
add_imperfect_line between the same endpoints
to build the new polyline.
- Re-wires every recorded physical group: the old line tag is
swapped out and the new segment tags are spliced in, so any
PG that used to reference the straight line now references
the full imperfect polyline.
Parameters
line_tag : int
Tag of the existing straight line to replace.
magnitude, direction, shape, n_segments, modes :
Same semantics as :meth:add_imperfect_line.
sync : bool
Whether to synchronise the OCC kernel at the end.
Returns
list[Tag]
New line tags in geometric order. Same layout as
:meth:add_imperfect_line would return.
Source code in src/apeGmsh/core/_model_geometry.py
| def replace_line(
self,
line_tag : Tag,
*,
magnitude : float = 0.0,
direction : tuple[float, float, float],
shape : Literal['kink', 'sine', 'multi_mode'] = 'kink',
n_segments: int = 8,
modes : list[tuple[int, float]] | None = None,
sync : bool = True,
) -> list[Tag]:
"""
Retrofit an existing straight line with a geometric imperfection.
Use this when you built the frame with plain :meth:`add_line`
calls and then want to introduce an imperfection on a specific
member without rebuilding the whole geometry. The method:
1. Validates that ``line_tag`` points to a straight line
(``kind='line'`` in the model metadata; arcs, splines, and
already-imperfect lines are rejected).
2. Looks up the two endpoint points from the line's boundary.
3. Records every physical group (user-facing PGs **and** label
PGs of the form ``_label:…``) that contains the line.
4. Deletes the old curve — endpoints are preserved because
other geometry likely references them.
5. Calls :meth:`add_imperfect_line` between the same endpoints
to build the new polyline.
6. Re-wires every recorded physical group: the old line tag is
swapped out and the new segment tags are spliced in, so any
PG that used to reference the straight line now references
the full imperfect polyline.
Parameters
----------
line_tag : int
Tag of the existing straight line to replace.
magnitude, direction, shape, n_segments, modes :
Same semantics as :meth:`add_imperfect_line`.
sync : bool
Whether to synchronise the OCC kernel at the end.
Returns
-------
list[Tag]
New line tags in geometric order. Same layout as
:meth:`add_imperfect_line` would return.
"""
line_tag = int(line_tag)
# 1. Reject anything that isn't a plain straight line.
meta = self._model._metadata.get((1, line_tag), {})
kind = meta.get('kind')
if kind != 'line':
raise ValueError(
f"replace_line only works on straight lines created via "
f"add_line (kind='line'), got kind={kind!r} for tag "
f"{line_tag}. Rebuild the geometry explicitly if you "
f"need to replace an arc, spline, or already-imperfect "
f"line.")
# 2. Recover endpoints via the curve's boundary.
try:
bnd = gmsh.model.getBoundary(
[(1, line_tag)], oriented=False)
except Exception as exc:
raise ValueError(
f"Line {line_tag} does not exist in the OCC kernel: "
f"{exc}") from exc
point_tags = [int(t) for (d, t) in bnd if d == 0]
if len(point_tags) != 2:
raise ValueError(
f"Line {line_tag} has {len(point_tags)} boundary "
f"point(s); expected exactly 2.")
start_tag, end_tag = point_tags
# 3. Record every dim-1 PG that contains this line. Capture
# both the PG tag (for removal) and its name + entity list
# (for re-creation) in a single scan so we don't walk the PG
# table twice.
old_pgs: list[tuple[int, str, list[int]]] = []
for (pg_dim, pg_tag) in gmsh.model.getPhysicalGroups(dim=1):
ents = [
int(e) for e in
gmsh.model.getEntitiesForPhysicalGroup(pg_dim, pg_tag)
]
if line_tag not in ents:
continue
try:
name = gmsh.model.getPhysicalName(pg_dim, pg_tag)
except Exception:
name = ''
other = [e for e in ents if e != line_tag]
old_pgs.append((int(pg_tag), name, other))
# 4. Delete the old physical groups FIRST, then the line.
# Removing the PGs first prevents Gmsh's internal synchronise
# from seeing a dangling entity reference when the line is
# deleted — otherwise it fires "Unknown entity ... in physical
# group" warnings.
if old_pgs:
gmsh.model.removePhysicalGroups(
[(1, pg_tag) for (pg_tag, _n, _o) in old_pgs])
# Delete the line itself (recursive=False keeps the endpoints).
gmsh.model.occ.remove([(1, line_tag)], recursive=False)
gmsh.model.occ.synchronize()
# Drop the old line from model metadata.
self._model._metadata.pop((1, line_tag), None)
# 5. Build the new polyline. Pass ``label=None`` so no label
# PGs are auto-created — we re-create the captured PGs
# (including the ``_label:…`` one if present) below in a
# single pass. ``sync=True`` is important: the new line tags
# must be committed into Gmsh's model state before we add
# physical groups referencing them, otherwise Gmsh accepts
# the PG at the model level but warns about "Unknown entity"
# on the next OCC synchronise.
new_tags = self.add_imperfect_line(
start_tag, end_tag,
magnitude=magnitude,
direction=direction,
shape=shape,
n_segments=n_segments,
modes=modes,
label=None,
sync=True,
)
# 6. Re-create every captured PG with (other_ents ∪ new_tags)
# and the original name. We pass ``name=`` to ``addPhysicalGroup``
# in one atomic call (rather than ``addPhysicalGroup`` + a
# separate ``setPhysicalName``) to avoid a brief inconsistent-
# -state window that some Gmsh builds complain about after a
# recent ``removePhysicalGroups``.
new_tag_set = set(int(t) for t in new_tags)
for (_old_pg_tag, name, other_ents) in old_pgs:
merged = sorted(set(other_ents) | new_tag_set)
try:
gmsh.model.addPhysicalGroup(
1, merged, tag=-1, name=name or "")
except Exception as exc:
import warnings as _warn
_warn.warn(
f"replace_line: could not re-create PG "
f"{name!r}: {exc}",
stacklevel=2,
)
if sync:
gmsh.model.occ.synchronize()
self._model._log(
f"replace_line({line_tag}, shape={shape!r}, "
f"magnitude={magnitude}) -> {len(new_tags)} segment(s) "
f"{new_tags}, re-wired {len(old_pgs)} PG(s)")
return list(new_tags)
|
sweep
sweep(profile_face: Tag, path_curves: list[Tag], *, label: str | None = None, cleanup: bool = True, sync: bool = True) -> dict
Sweep a planar profile face along a chain of curves (a polyline
path) to produce a 3-D solid volume.
Wraps gmsh.model.occ.addWire + gmsh.model.occ.addPipe
and — optionally — cleans up the intermediate geometry that
would otherwise cause trouble downstream:
- The profile face that was used as the input to the pipe.
It persists as an orphan at the first station of the sweep,
which means when you try to identify the start cap by bbox
you pick up two coincident surfaces and their mesh nodes
double up.
- The path curves themselves. These live along the
centroid line of the swept solid — i.e. inside the
volume — and Gmsh happily meshes them into
line2
elements whose nodes sit interior to the tet mesh and are
not shared with any tet4 element. Those become floating
null-space DOFs if you emit them to OpenSees.
With cleanup=True (the default) both are removed after the
pipe has produced the volume, so the only surfaces left in the
model are the ones that actually bound the solid (the two end
caps + the n_path_segments * n_profile_edges ruled side
surfaces).
Parameters
profile_face : int
Tag of a planar surface that serves as the cross-section.
Most commonly built with addCurveLoop + addPlaneSurface
on a closed polyline of vertices. Must be perpendicular —
at least roughly — to the start of the path; Gmsh orients
the local frame automatically using the Frenet trihedron.
path_curves : list[int]
Ordered list of curve tags that form the path the profile
is swept along. Typically the return value of
:meth:add_imperfect_line or :meth:replace_line.
label : str, optional
If given, the resulting volume is labelled. End caps and
side surfaces remain unlabelled — run the usual
select_surfaces(in_box=…) + to_physical pass to
group them explicitly.
cleanup : bool
Remove the original profile face and path curves after
the pipe is built. Defaults to True. Set to False if you
want to preserve the profile/path for downstream use
(e.g. another sweep on a branched path).
sync : bool
Whether to synchronise the OCC kernel at the end.
Returns
dict
{'volume': tag, 'start_cap': tag, 'end_cap': tag}.
The caps are identified by scanning every new dim-2
entity's bounding box for one whose x_min == x_max ==
path_endpoint_x. If either cap cannot be identified its
entry is None.
Examples
Swept solid I-beam with a half-sine imperfection in the
weak-axis direction::
# 1. Imperfect path
p0 = g.model.geometry.add_point(0, 0, 0, lc=200)
p1 = g.model.geometry.add_point(L, 0, 0, lc=200)
path = g.model.geometry.replace_line(
g.model.geometry.add_line(p0, p1),
magnitude=L/1000, direction=(0, 1, 0),
shape='sine', n_segments=16,
)
# 2. Rectangular profile at x = 0
corners = [
(0, -t/2, -h/2), (0, +t/2, -h/2),
(0, +t/2, +h/2), (0, -t/2, +h/2),
]
pts = [gmsh.model.occ.addPoint(*c) for c in corners]
lns = [gmsh.model.occ.addLine(pts[i], pts[(i+1) % 4])
for i in range(4)]
loop = gmsh.model.occ.addCurveLoop(lns)
profile = gmsh.model.occ.addPlaneSurface([loop])
gmsh.model.occ.synchronize()
# 3. Sweep
swept = g.model.geometry.sweep(profile, path, label='beam')
# swept['volume'] — the solid tag
# swept['start_cap'] — surface tag at path start
# swept['end_cap'] — surface tag at path end
Source code in src/apeGmsh/core/_model_geometry.py
| def sweep(
self,
profile_face : Tag,
path_curves : list[Tag],
*,
label : str | None = None,
cleanup : bool = True,
sync : bool = True,
) -> dict:
"""
Sweep a planar profile face along a chain of curves (a polyline
path) to produce a 3-D solid volume.
Wraps ``gmsh.model.occ.addWire`` + ``gmsh.model.occ.addPipe``
and — optionally — cleans up the intermediate geometry that
would otherwise cause trouble downstream:
* **The profile face** that was used as the input to the pipe.
It persists as an orphan at the first station of the sweep,
which means when you try to identify the start cap by bbox
you pick up two coincident surfaces and their mesh nodes
double up.
* **The path curves** themselves. These live along the
centroid line of the swept solid — i.e. **inside** the
volume — and Gmsh happily meshes them into ``line2``
elements whose nodes sit interior to the tet mesh and are
*not* shared with any tet4 element. Those become floating
null-space DOFs if you emit them to OpenSees.
With ``cleanup=True`` (the default) both are removed after the
pipe has produced the volume, so the only surfaces left in the
model are the ones that actually bound the solid (the two end
caps + the ``n_path_segments * n_profile_edges`` ruled side
surfaces).
Parameters
----------
profile_face : int
Tag of a planar surface that serves as the cross-section.
Most commonly built with ``addCurveLoop`` + ``addPlaneSurface``
on a closed polyline of vertices. Must be perpendicular —
at least roughly — to the start of the path; Gmsh orients
the local frame automatically using the Frenet trihedron.
path_curves : list[int]
Ordered list of curve tags that form the path the profile
is swept along. Typically the return value of
:meth:`add_imperfect_line` or :meth:`replace_line`.
label : str, optional
If given, the resulting volume is labelled. End caps and
side surfaces remain unlabelled — run the usual
``select_surfaces(in_box=…)`` + ``to_physical`` pass to
group them explicitly.
cleanup : bool
Remove the original profile face and path curves after
the pipe is built. Defaults to True. Set to False if you
want to preserve the profile/path for downstream use
(e.g. another sweep on a branched path).
sync : bool
Whether to synchronise the OCC kernel at the end.
Returns
-------
dict
``{'volume': tag, 'start_cap': tag, 'end_cap': tag}``.
The caps are identified by scanning every new dim-2
entity's bounding box for one whose ``x_min == x_max ==
path_endpoint_x``. If either cap cannot be identified its
entry is ``None``.
Examples
--------
Swept solid I-beam with a half-sine imperfection in the
weak-axis direction::
# 1. Imperfect path
p0 = g.model.geometry.add_point(0, 0, 0, lc=200)
p1 = g.model.geometry.add_point(L, 0, 0, lc=200)
path = g.model.geometry.replace_line(
g.model.geometry.add_line(p0, p1),
magnitude=L/1000, direction=(0, 1, 0),
shape='sine', n_segments=16,
)
# 2. Rectangular profile at x = 0
corners = [
(0, -t/2, -h/2), (0, +t/2, -h/2),
(0, +t/2, +h/2), (0, -t/2, +h/2),
]
pts = [gmsh.model.occ.addPoint(*c) for c in corners]
lns = [gmsh.model.occ.addLine(pts[i], pts[(i+1) % 4])
for i in range(4)]
loop = gmsh.model.occ.addCurveLoop(lns)
profile = gmsh.model.occ.addPlaneSurface([loop])
gmsh.model.occ.synchronize()
# 3. Sweep
swept = g.model.geometry.sweep(profile, path, label='beam')
# swept['volume'] — the solid tag
# swept['start_cap'] — surface tag at path start
# swept['end_cap'] — surface tag at path end
"""
# Snapshot the set of existing volumes + surfaces so we can
# identify the ones the pipe creates.
before_vols = set(int(t) for (_d, t) in gmsh.model.getEntities(3))
before_surfs = set(int(t) for (_d, t) in gmsh.model.getEntities(2))
# Start position: centroid of the profile face (it sits at the
# start of the path). End position: walk the path curves and
# collect every endpoint's 3-D coord, then pick the one
# farthest from the start. This avoids having to know how
# Gmsh orients the boundary points of each curve.
prof_com = np.asarray(
gmsh.model.occ.getCenterOfMass(2, int(profile_face)),
dtype=float)
start_xyz = prof_com
all_path_pts: list[np.ndarray] = []
for t in path_curves:
try:
bnd = gmsh.model.getBoundary(
[(1, int(t))], oriented=False)
except Exception:
continue
for (d, pt) in bnd:
if d == 0:
all_path_pts.append(np.asarray(
gmsh.model.getValue(0, int(pt), []),
dtype=float))
if all_path_pts:
# End of path = point farthest from start_xyz.
end_xyz = max(
all_path_pts,
key=lambda p: float(np.linalg.norm(p - start_xyz)))
else:
end_xyz = start_xyz
# Build the wire + pipe.
wire = gmsh.model.occ.addWire(
[int(t) for t in path_curves], checkClosed=False)
gmsh.model.occ.addPipe(
[(2, int(profile_face))], wire)
gmsh.model.occ.synchronize()
# Cleanup — remove the original profile face (recursive, so
# its boundary lines/points go too) and the path curves that
# live along the volume centroid.
if cleanup:
try:
gmsh.model.occ.remove(
[(2, int(profile_face))], recursive=True)
except Exception:
pass
try:
gmsh.model.occ.remove(
[(1, int(t)) for t in path_curves], recursive=False)
except Exception:
pass
gmsh.model.occ.synchronize()
# Find the new volume and end caps.
after_vols = [
int(t) for (_d, t) in gmsh.model.getEntities(3)
if int(t) not in before_vols
]
volume_tag = after_vols[0] if after_vols else None
# Find the caps by projecting each new surface centroid onto
# the path direction and taking the extremes. The "new"
# surfaces that are actual end caps will project to approx.
# 0 (start) and |end - start| (end); the ruled side surfaces
# will project to values in between.
TOL = 0.1
start_cap = None
end_cap = None
path_vec = end_xyz - start_xyz
path_len = float(np.linalg.norm(path_vec))
if path_len > TOL:
path_unit = path_vec / path_len
cands: list[tuple[float, int]] = []
for (d, t) in gmsh.model.getEntities(2):
if int(t) in before_surfs:
continue
try:
com = np.asarray(
gmsh.model.occ.getCenterOfMass(2, int(t)),
dtype=float)
except Exception:
continue
proj = float(np.dot(com - start_xyz, path_unit))
cands.append((proj, int(t)))
if cands:
cands.sort()
start_cap = cands[0][1]
end_cap = cands[-1][1]
if volume_tag is not None and label:
self._model._register(3, volume_tag, label, 'swept_solid')
if sync:
gmsh.model.occ.synchronize()
self._model._log(
f"sweep(profile={profile_face}, n_path={len(path_curves)}) "
f"-> volume={volume_tag}, start_cap={start_cap}, "
f"end_cap={end_cap}")
return {
'volume': volume_tag,
'start_cap': start_cap,
'end_cap': end_cap,
}
|
add_arc
add_arc(start: EntityRef, center: EntityRef, end: EntityRef, *, through_point: bool = False, label: str | None = None, sync: bool = True) -> Tag
Add a circular arc defined by three existing points.
Parameters
start : point reference — start of the arc
center : point reference — interpretation depends on
through_point:
* ``through_point=False`` (default) — the **centre of the
circle** (not on the arc). All three points must be
equidistant from this centre.
* ``through_point=True`` — a point the arc **passes
through** (e.g. the apex of an arch). The circle is
fitted through ``start``, this point, and ``end``.
end : point reference — end of the arc
through_point : bool
Switches the meaning of center as above. Use
True for the common "arc through 3 points" case —
add_arc(left, apex, right, through_point=True) — where
you know a point on the arc but not the circle centre.
Each point accepts a raw tag, label name, physical group, part
label, or (dim, tag) and must resolve to exactly one point.
Note
With through_point=False the arc is the shorter of the
two possible arcs unless you reverse the start/end order.
Source code in src/apeGmsh/core/_model_geometry.py
| def add_arc(
self,
start : EntityRef,
center: EntityRef,
end : EntityRef,
*,
through_point: bool = False,
label : str | None = None,
sync : bool = True,
) -> Tag:
"""
Add a circular arc defined by three existing points.
Parameters
----------
start : point reference — start of the arc
center : point reference — interpretation depends on
``through_point``:
* ``through_point=False`` (default) — the **centre of the
circle** (not on the arc). All three points must be
equidistant from this centre.
* ``through_point=True`` — a point the arc **passes
through** (e.g. the apex of an arch). The circle is
fitted through ``start``, this point, and ``end``.
end : point reference — end of the arc
through_point : bool
Switches the meaning of ``center`` as above. Use
``True`` for the common "arc through 3 points" case —
``add_arc(left, apex, right, through_point=True)`` — where
you know a point on the arc but not the circle centre.
Each point accepts a raw tag, label name, physical group, part
label, or ``(dim, tag)`` and must resolve to exactly one point.
Note
----
With ``through_point=False`` the arc is the *shorter* of the
two possible arcs unless you reverse the start/end order.
"""
start = self._resolve_entity_tag(start, dim=0, what="point")
center = self._resolve_entity_tag(center, dim=0, what="point")
end = self._resolve_entity_tag(end, dim=0, what="point")
tag = gmsh.model.occ.addCircleArc(
start, center, end, center=not through_point)
if sync:
gmsh.model.occ.synchronize()
mid_role = "through" if through_point else "centre"
self._model._log(
f"add_arc(start={start}, {mid_role}={center}, "
f"end={end}) -> tag {tag}")
return self._model._register(1, tag, label, 'arc')
|
add_circle
add_circle(cx: float, cy: float, cz: float, radius: float, *, angle1: float = 0.0, angle2: float = 2 * math.pi, label: str | None = None, sync: bool = True) -> Tag
Add a full circle (or arc sector) as a single curve entity.
Unlike add_arc, this does not require pre-existing point
tags — it creates the circle directly from centre + radius.
Parameters
cx, cy, cz : centre
radius : radius
angle1 : start angle in radians (default 0)
angle2 : end angle in radians (default 2π = full circle)
Source code in src/apeGmsh/core/_model_geometry.py
| def add_circle(
self,
cx: float, cy: float, cz: float,
radius: float,
*,
angle1: float = 0.0,
angle2: float = 2 * math.pi,
label : str | None = None,
sync : bool = True,
) -> Tag:
"""
Add a full circle (or arc sector) as a single curve entity.
Unlike ``add_arc``, this does **not** require pre-existing point
tags — it creates the circle directly from centre + radius.
Parameters
----------
cx, cy, cz : centre
radius : radius
angle1 : start angle in radians (default 0)
angle2 : end angle in radians (default 2π = full circle)
"""
tag = gmsh.model.occ.addCircle(cx, cy, cz, radius,
angle1=angle1, angle2=angle2)
if sync:
gmsh.model.occ.synchronize()
self._model._log(
f"add_circle(centre=({cx},{cy},{cz}), r={radius}, "
f"[{math.degrees(angle1):.1f}°->{math.degrees(angle2):.1f}°]) -> tag {tag}"
)
return self._model._register(1, tag, label, 'circle')
|
add_ellipse
add_ellipse(cx: float, cy: float, cz: float, r_major: float, r_minor: float, *, angle1: float = 0.0, angle2: float = 2 * math.pi, label: str | None = None, sync: bool = True) -> Tag
Add a full ellipse (or elliptic arc) as a single curve entity.
Parameters
cx, cy, cz : centre
r_major : semi-major axis (along X before any rotation)
r_minor : semi-minor axis
angle1 : start angle in radians
angle2 : end angle in radians
Source code in src/apeGmsh/core/_model_geometry.py
| def add_ellipse(
self,
cx: float, cy: float, cz: float,
r_major: float, r_minor: float,
*,
angle1: float = 0.0,
angle2: float = 2 * math.pi,
label : str | None = None,
sync : bool = True,
) -> Tag:
"""
Add a full ellipse (or elliptic arc) as a single curve entity.
Parameters
----------
cx, cy, cz : centre
r_major : semi-major axis (along X before any rotation)
r_minor : semi-minor axis
angle1 : start angle in radians
angle2 : end angle in radians
"""
tag = gmsh.model.occ.addEllipse(cx, cy, cz, r_major, r_minor,
angle1=angle1, angle2=angle2)
if sync:
gmsh.model.occ.synchronize()
self._model._log(
f"add_ellipse(centre=({cx},{cy},{cz}), a={r_major}, b={r_minor}) -> tag {tag}"
)
return self._model._register(1, tag, label, 'ellipse')
|
add_spline
add_spline(point_tags: list[EntityRef], *, label: str | None = None, sync: bool = True) -> Tag
Add a C2-continuous spline curve through the given points
(interpolating spline).
Parameters
point_tags : ordered list of point references the spline passes
through (raw tag, label, PG, part, or (dim, tag);
each must resolve to one point). Minimum 2 points;
for a closed spline repeat the first reference at the
end.
Example
::
p1 = g.model.geometry.add_point(0, 0, 0)
p2 = g.model.geometry.add_point(1, 1, 0)
p3 = g.model.geometry.add_point(2, 0, 0)
s = g.model.geometry.add_spline([p1, p2, p3])
Source code in src/apeGmsh/core/_model_geometry.py
| def add_spline(
self,
point_tags: list[EntityRef],
*,
label: str | None = None,
sync : bool = True,
) -> Tag:
"""
Add a C2-continuous spline curve **through** the given points
(interpolating spline).
Parameters
----------
point_tags : ordered list of point references the spline passes
through (raw tag, label, PG, part, or ``(dim, tag)``;
each must resolve to one point). Minimum 2 points;
for a closed spline repeat the first reference at the
end.
Example
-------
::
p1 = g.model.geometry.add_point(0, 0, 0)
p2 = g.model.geometry.add_point(1, 1, 0)
p3 = g.model.geometry.add_point(2, 0, 0)
s = g.model.geometry.add_spline([p1, p2, p3])
"""
if len(point_tags) < 2:
raise ValueError("add_spline requires at least 2 point tags.")
point_tags = [
self._resolve_entity_tag(p, dim=0, what="point")
for p in point_tags
]
tag = gmsh.model.occ.addSpline(point_tags)
if sync:
gmsh.model.occ.synchronize()
self._model._log(f"add_spline({point_tags}) -> tag {tag}")
return self._model._register(1, tag, label, 'spline')
|
add_bspline
add_bspline(point_tags: list[EntityRef], *, degree: int = 3, weights: list[float] | None = None, knots: list[float] | None = None, multiplicities: list[int] | None = None, label: str | None = None, sync: bool = True) -> Tag
Add a B-spline curve with explicit control points.
Control points are not interpolated (the curve is attracted to
them, not forced through them), which is different from
add_spline.
Parameters
point_tags : control-point tags
degree : polynomial degree (default 3 = cubic)
weights : optional rational weights (len = len(point_tags))
knots : optional knot vector
multiplicities : optional knot multiplicities
Source code in src/apeGmsh/core/_model_geometry.py
| def add_bspline(
self,
point_tags : list[EntityRef],
*,
degree : int = 3,
weights : list[float] | None = None,
knots : list[float] | None = None,
multiplicities: list[int] | None = None,
label : str | None = None,
sync : bool = True,
) -> Tag:
"""
Add a B-spline curve with explicit control points.
Control points are **not** interpolated (the curve is attracted to
them, not forced through them), which is different from
``add_spline``.
Parameters
----------
point_tags : control-point tags
degree : polynomial degree (default 3 = cubic)
weights : optional rational weights (len = len(point_tags))
knots : optional knot vector
multiplicities : optional knot multiplicities
"""
if len(point_tags) < 2:
raise ValueError("add_bspline requires at least 2 point tags.")
point_tags = [
self._resolve_entity_tag(p, dim=0, what="point")
for p in point_tags
]
tag = gmsh.model.occ.addBSpline(
point_tags,
degree=degree,
weights=weights or [],
knots=knots or [],
multiplicities=multiplicities or [],
)
if sync:
gmsh.model.occ.synchronize()
self._model._log(f"add_bspline(ctrl_pts={point_tags}, degree={degree}) -> tag {tag}")
return self._model._register(1, tag, label, 'bspline')
|
add_bezier
add_bezier(point_tags: list[EntityRef], *, label: str | None = None, sync: bool = True) -> Tag
Add a Bézier curve.
Parameters
point_tags : control-point tags. The curve starts at the first
point and ends at the last; intermediate points are
control handles (not interpolated).
Source code in src/apeGmsh/core/_model_geometry.py
| def add_bezier(
self,
point_tags: list[EntityRef],
*,
label: str | None = None,
sync : bool = True,
) -> Tag:
"""
Add a Bézier curve.
Parameters
----------
point_tags : control-point tags. The curve starts at the first
point and ends at the last; intermediate points are
control handles (not interpolated).
"""
if len(point_tags) < 2:
raise ValueError("add_bezier requires at least 2 point tags.")
point_tags = [
self._resolve_entity_tag(p, dim=0, what="point")
for p in point_tags
]
tag = gmsh.model.occ.addBezier(point_tags)
if sync:
gmsh.model.occ.synchronize()
self._model._log(f"add_bezier({point_tags}) -> tag {tag}")
return self._model._register(1, tag, label, 'bezier')
|
add_wire
add_wire(curve_tags: list[EntityRef], *, check_closed: bool = False, label: str | None = None, sync: bool = True) -> Tag
Assemble an ordered list of curve tags into an OpenCASCADE wire
(open or closed). Wires are the path input for sweep operations
(:meth:sweep) and the section input for lofted volumes
(:meth:thru_sections).
Unlike :meth:add_curve_loop, a wire does not need to be
closed. This is what makes it suitable as a sweep path.
Parameters
curve_tags : ordered curve references (raw tag, label, PG, part,
or (dim, tag); each must resolve to one curve). A
leading '-' on a label name — or a negative tag —
reverses that curve's orientation. Curves must be connected
end-to-end but may share only geometrically identical
endpoints (OCC allows topologically distinct but coincident
points).
check_closed : if True, the underlying OCC call verifies that
the wire forms a closed loop and raises otherwise.
label : not supported — passing a non-None value raises
ValueError (see Note).
Returns
int tag of the new OCC wire. This is not a persistent,
meshable model entity — use it immediately as the path
argument to :meth:sweep or an element of wires for
:meth:thru_sections.
Note
An OpenCASCADE wire is a transient construction object, not a
model entity. Its tag is allocated in the curve tag-space and,
after synchronize(), it does not appear as its own
dim=1 entity — the tag instead aliases one of the member
curves. Consequently the wire is not added to the entity
registry, and label= is rejected: a label would silently
attach the name to an unrelated curve. To name the member
curves as a group, group the curves themselves, e.g.
m.model.select([c1, c2, c3]).to_physical(name=...).
Example
::
p0 = g.model.geometry.add_point(0, 0, 0, sync=False)
p1 = g.model.geometry.add_point(1, 0, 0, sync=False)
p2 = g.model.geometry.add_point(1, 1, 0, sync=False)
p3 = g.model.geometry.add_point(1, 1, 2, sync=False)
l1 = g.model.geometry.add_line(p0, p1, sync=False)
l2 = g.model.geometry.add_line(p1, p2, sync=False)
l3 = g.model.geometry.add_line(p2, p3, sync=False)
path = g.model.geometry.add_wire([l1, l2, l3])
g.model.transforms.sweep(section, path)
Source code in src/apeGmsh/core/_model_geometry.py
| def add_wire(
self,
curve_tags: list[EntityRef],
*,
check_closed: bool = False,
label : str | None = None,
sync : bool = True,
) -> Tag:
"""
Assemble an ordered list of curve tags into an OpenCASCADE wire
(open or closed). Wires are the path input for sweep operations
(:meth:`sweep`) and the section input for lofted volumes
(:meth:`thru_sections`).
Unlike :meth:`add_curve_loop`, a wire does **not** need to be
closed. This is what makes it suitable as a sweep path.
Parameters
----------
curve_tags : ordered curve references (raw tag, label, PG, part,
or ``(dim, tag)``; each must resolve to one curve). A
leading ``'-'`` on a label name — or a negative tag —
reverses that curve's orientation. Curves must be connected
end-to-end but may share only geometrically identical
endpoints (OCC allows topologically distinct but coincident
points).
check_closed : if True, the underlying OCC call verifies that
the wire forms a closed loop and raises otherwise.
label : **not supported** — passing a non-None value raises
``ValueError`` (see Note).
Returns
-------
int tag of the new OCC wire. This is **not** a persistent,
meshable model entity — use it immediately as the ``path``
argument to :meth:`sweep` or an element of ``wires`` for
:meth:`thru_sections`.
Note
----
An OpenCASCADE wire is a transient construction object, not a
model entity. Its tag is allocated in the curve tag-space and,
after ``synchronize()``, it does **not** appear as its own
``dim=1`` entity — the tag instead aliases one of the member
curves. Consequently the wire is **not** added to the entity
registry, and ``label=`` is rejected: a label would silently
attach the name to an unrelated curve. To name the member
curves as a group, group the curves themselves, e.g.
``m.model.select([c1, c2, c3]).to_physical(name=...)``.
Example
-------
::
p0 = g.model.geometry.add_point(0, 0, 0, sync=False)
p1 = g.model.geometry.add_point(1, 0, 0, sync=False)
p2 = g.model.geometry.add_point(1, 1, 0, sync=False)
p3 = g.model.geometry.add_point(1, 1, 2, sync=False)
l1 = g.model.geometry.add_line(p0, p1, sync=False)
l2 = g.model.geometry.add_line(p1, p2, sync=False)
l3 = g.model.geometry.add_line(p2, p3, sync=False)
path = g.model.geometry.add_wire([l1, l2, l3])
g.model.transforms.sweep(section, path)
"""
if label is not None:
raise ValueError(
"add_wire does not accept label=: an OpenCASCADE wire "
"is a transient construction object, not a persistent "
"model entity. Its tag is allocated in the curve "
"tag-space and after synchronize() aliases an existing "
"curve, so a label would silently attach the name to "
"that unrelated curve.\n"
" - To use the wire as a sweep / thru_sections path, "
"pass the returned tag directly: "
"path = add_wire([...]); sweep(profile, path)\n"
" - To name the member curves as a group, group the "
"curves themselves: "
"m.model.select([c1, c2, ...]).to_physical(name=...)"
)
curve_tags = [
self._resolve_entity_tag(c, dim=1, what="curve", allow_sign=True)
for c in curve_tags
]
tag = gmsh.model.occ.addWire(curve_tags, checkClosed=check_closed)
if sync:
gmsh.model.occ.synchronize()
self._model._log(
f"add_wire({curve_tags}, closed={check_closed}) -> tag {tag}")
return tag
|
add_curve_loop
add_curve_loop(curve_tags: list[EntityRef], *, label: str | None = None, sync: bool = True) -> Tag
Assemble an ordered list of curve references into a closed wire
(curve loop). The result is used as input to
add_plane_surface or add_surface_filling.
Parameters
curve_tags : ordered curve references forming a closed loop
(raw tag, label, PG, part, or (dim, tag); each
must resolve to one curve). Reverse a curve's
orientation with a negative tag or a leading
'-' on its label name, e.g. '-col_right'.
Example
::
loop = g.model.geometry.add_curve_loop([l1, l2, l3, l4])
surf = g.model.geometry.add_plane_surface(loop)
# by label, with one reversed curve
loop = g.model.geometry.add_curve_loop(
['col_left', 'arch', '-col_right'])
Source code in src/apeGmsh/core/_model_geometry.py
| def add_curve_loop(
self,
curve_tags: list[EntityRef],
*,
label: str | None = None,
sync : bool = True,
) -> Tag:
"""
Assemble an ordered list of curve references into a closed wire
(curve loop). The result is used as input to
``add_plane_surface`` or ``add_surface_filling``.
Parameters
----------
curve_tags : ordered curve references forming a closed loop
(raw tag, label, PG, part, or ``(dim, tag)``; each
must resolve to one curve). Reverse a curve's
orientation with a negative tag **or** a leading
``'-'`` on its label name, e.g. ``'-col_right'``.
Example
-------
::
loop = g.model.geometry.add_curve_loop([l1, l2, l3, l4])
surf = g.model.geometry.add_plane_surface(loop)
# by label, with one reversed curve
loop = g.model.geometry.add_curve_loop(
['col_left', 'arch', '-col_right'])
"""
curve_tags = [
self._resolve_entity_tag(c, dim=1, what="curve", allow_sign=True)
for c in curve_tags
]
tag = gmsh.model.occ.addCurveLoop(curve_tags)
if sync:
gmsh.model.occ.synchronize()
self._model._log(f"add_curve_loop({curve_tags}) -> tag {tag}")
return self._model._register(1, tag, label, 'curve_loop')
|
add_plane_surface
add_plane_surface(wire_tags: EntityRef | list[EntityRef], *, label: str | None = None, sync: bool = True) -> Tag
Create a planar surface bounded by one or more curve loops.
Parameters
wire_tags : reference (or list of references) to curve loops —
raw tag, label, PG, part, or (dim, tag); each
must resolve to one curve loop. The first loop is
the outer boundary; any additional loops define holes.
Example
::
outer = g.model.geometry.add_curve_loop([l1, l2, l3, l4])
hole = g.model.geometry.add_curve_loop([h1, h2, h3, h4])
surf = g.model.geometry.add_plane_surface([outer, hole])
Source code in src/apeGmsh/core/_model_geometry.py
| def add_plane_surface(
self,
wire_tags: EntityRef | list[EntityRef],
*,
label: str | None = None,
sync : bool = True,
) -> Tag:
"""
Create a planar surface bounded by one or more curve loops.
Parameters
----------
wire_tags : reference (or list of references) to curve loops —
raw tag, label, PG, part, or ``(dim, tag)``; each
must resolve to one curve loop. The first loop is
the outer boundary; any additional loops define holes.
Example
-------
::
outer = g.model.geometry.add_curve_loop([l1, l2, l3, l4])
hole = g.model.geometry.add_curve_loop([h1, h2, h3, h4])
surf = g.model.geometry.add_plane_surface([outer, hole])
"""
if not isinstance(wire_tags, list):
wire_tags = [wire_tags]
wire_tags = [
self._resolve_entity_tag(w, dim=1, what="curve loop")
for w in wire_tags
]
tag = gmsh.model.occ.addPlaneSurface(wire_tags)
if sync:
gmsh.model.occ.synchronize()
self._model._log(f"add_plane_surface(wires={wire_tags}) -> tag {tag}")
return self._model._register(2, tag, label, 'plane_surface')
|
add_surface_filling
add_surface_filling(wire_tag: EntityRef, *, label: str | None = None, sync: bool = True) -> Tag
Create a surface filling bounded by a single curve loop, using a
Coons-patch style interpolation (non-planar surfaces).
Parameters
wire_tag : reference to the bounding curve loop — raw tag,
label, PG, part, or (dim, tag); must resolve to
one curve loop.
Source code in src/apeGmsh/core/_model_geometry.py
| def add_surface_filling(
self,
wire_tag: EntityRef,
*,
label: str | None = None,
sync : bool = True,
) -> Tag:
"""
Create a surface filling bounded by a single curve loop, using a
Coons-patch style interpolation (non-planar surfaces).
Parameters
----------
wire_tag : reference to the bounding curve loop — raw tag,
label, PG, part, or ``(dim, tag)``; must resolve to
one curve loop.
"""
wire_tag = self._resolve_entity_tag(
wire_tag, dim=1, what="curve loop")
tag = gmsh.model.occ.addSurfaceFilling(wire_tag)
if sync:
gmsh.model.occ.synchronize()
self._model._log(f"add_surface_filling(wire={wire_tag}) -> tag {tag}")
return self._model._register(2, tag, label, 'surface_filling')
|
add_rectangle
add_rectangle(x: float, y: float, z: float, dx: float, dy: float, *, angles_deg: tuple[float, float, float] | None = None, angles_rad: tuple[float, float, float] | None = None, pivot: tuple[float, float, float] = (0.0, 0.0, 0.0), rounded_radius: float = 0.0, label: str | None = None, sync: bool = True) -> Tag
Add a rectangular planar surface in the XY plane.
The rectangle is created at (x, y, z) with extents dx along
X and dy along Y. Optionally rotate it in place by passing
angles_deg or angles_rad — three angles applied as
successive rotations about world X, then Y, then Z, through a
pivot point measured as an offset from the rectangle's
geometric centre (x + dx/2, y + dy/2, z).
Useful as a cutting tool for :meth:fragment — a 2D rectangle
fragmented against a 3D solid splits the solid along the
rectangle's plane.
Parameters
x, y, z : float
Corner of the rectangle.
dx, dy : float
Extents along X and Y.
angles_deg, angles_rad : (rx, ry, rz), optional
Rotation angles about world X, Y, Z, applied in that order
through pivot. Pass exactly one of the two — supplying
both raises ValueError. Either may be None (no
rotation).
pivot : (px, py, pz)
Pivot point expressed as an offset from the rectangle's
centre. (0, 0, 0) (default) rotates about the centre;
(dx/2, dy/2, 0) would rotate about the bottom-left
corner, etc. Ignored when no angles are given.
rounded_radius : float
If > 0, rounds the four corners with this radius.
label : str, optional
Human-readable label stored in the internal registry.
sync : bool
Synchronise the OCC kernel after creation (default True).
Returns
Tag
Surface tag of the new rectangle.
Example
::
# Split a solid at mid-height with a cutting plane
bb = gmsh.model.getBoundingBox(3, 1)
xmin, ymin, zmin, xmax, ymax, zmax = bb
zmid = (zmin + zmax) / 2
pad = 1.0
rect = m1.model.geometry.add_rectangle(
xmin - pad, ymin - pad, zmid,
(xmax - xmin) + 2*pad,
(ymax - ymin) + 2*pad,
)
result = m1.model.boolean.fragment(objects=[1], tools=[rect], dim=3)
# Inclined crack plane: 30 about X through the centre
m.model.geometry.add_rectangle(
-10, -10, 0, 20, 20,
angles_deg=(30, 0, 0), label='plane',
)
Source code in src/apeGmsh/core/_model_geometry.py
| def add_rectangle(
self,
x: float, y: float, z: float,
dx: float, dy: float,
*,
angles_deg: tuple[float, float, float] | None = None,
angles_rad: tuple[float, float, float] | None = None,
pivot : tuple[float, float, float] = (0.0, 0.0, 0.0),
rounded_radius: float = 0.0,
label: str | None = None,
sync : bool = True,
) -> Tag:
"""
Add a rectangular planar surface in the XY plane.
The rectangle is created at **(x, y, z)** with extents **dx** along
X and **dy** along Y. Optionally rotate it in place by passing
``angles_deg`` or ``angles_rad`` — three angles applied as
successive rotations about world X, then Y, then Z, through a
pivot point measured **as an offset from the rectangle's
geometric centre** ``(x + dx/2, y + dy/2, z)``.
Useful as a cutting tool for :meth:`fragment` — a 2D rectangle
fragmented against a 3D solid splits the solid along the
rectangle's plane.
Parameters
----------
x, y, z : float
Corner of the rectangle.
dx, dy : float
Extents along X and Y.
angles_deg, angles_rad : (rx, ry, rz), optional
Rotation angles about world X, Y, Z, applied in that order
through ``pivot``. Pass exactly one of the two — supplying
both raises ``ValueError``. Either may be ``None`` (no
rotation).
pivot : (px, py, pz)
Pivot point expressed as an **offset from the rectangle's
centre**. ``(0, 0, 0)`` (default) rotates about the centre;
``(dx/2, dy/2, 0)`` would rotate about the bottom-left
corner, etc. Ignored when no angles are given.
rounded_radius : float
If > 0, rounds the four corners with this radius.
label : str, optional
Human-readable label stored in the internal registry.
sync : bool
Synchronise the OCC kernel after creation (default True).
Returns
-------
Tag
Surface tag of the new rectangle.
Example
-------
::
# Split a solid at mid-height with a cutting plane
bb = gmsh.model.getBoundingBox(3, 1)
xmin, ymin, zmin, xmax, ymax, zmax = bb
zmid = (zmin + zmax) / 2
pad = 1.0
rect = m1.model.geometry.add_rectangle(
xmin - pad, ymin - pad, zmid,
(xmax - xmin) + 2*pad,
(ymax - ymin) + 2*pad,
)
result = m1.model.boolean.fragment(objects=[1], tools=[rect], dim=3)
# Inclined crack plane: 30 about X through the centre
m.model.geometry.add_rectangle(
-10, -10, 0, 20, 20,
angles_deg=(30, 0, 0), label='plane',
)
"""
if angles_deg is not None and angles_rad is not None:
raise ValueError(
"add_rectangle: pass either angles_deg or angles_rad, "
"not both."
)
angles = angles_rad
if angles_deg is not None:
angles = tuple(math.radians(a) for a in angles_deg)
tag = gmsh.model.occ.addRectangle(
x, y, z, dx, dy, roundedRadius=rounded_radius,
)
if angles is not None:
cx = x + dx / 2.0 + pivot[0]
cy = y + dy / 2.0 + pivot[1]
cz = z + pivot[2]
rx, ry, rz = angles
if rx:
gmsh.model.occ.rotate(
[(2, tag)], cx, cy, cz, 1, 0, 0, rx,
)
if ry:
gmsh.model.occ.rotate(
[(2, tag)], cx, cy, cz, 0, 1, 0, ry,
)
if rz:
gmsh.model.occ.rotate(
[(2, tag)], cx, cy, cz, 0, 0, 1, rz,
)
if sync:
gmsh.model.occ.synchronize()
rot_msg = ""
if angles is not None:
rot_msg = (
f", angles_rad=({angles[0]:.4f},{angles[1]:.4f},"
f"{angles[2]:.4f}), pivot_offset={pivot}"
)
self._model._log(
f"add_rectangle(origin=({x},{y},{z}), size=({dx},{dy})"
f"{f', r={rounded_radius}' if rounded_radius else ''}"
f"{rot_msg}) -> tag {tag}"
)
return self._model._register(2, tag, label, 'rectangle')
|
add_cutting_plane
add_cutting_plane(point: list[float] | ndarray, normal_vector: list[float] | ndarray, *, size: float | None = None, label: str | None = None, sync: bool = True) -> Tag
Create a square planar surface through point with the given
normal, suitable for clipping / section / visualisation views.
The surface is a plain BRep face built from 4 points + 4 lines
+ a curve loop + a plane surface, so it behaves exactly like
any other registered surface (it can be selected, meshed as a
discrete 2-D grid, exported to STEP, etc.). It is not a
Gmsh clipping plane in the rendering sense — it is real
geometry.
Parameters
point : array-like of 3 floats
A point on the plane. The square is centred here.
normal_vector : array-like of 3 floats
Plane normal. Need not be unit-length — it is normalised
internally.
size : float, optional
Edge length of the square. When None (default), size
is picked as 2 × max(model_bbox_diagonal, 1.0) so the
square comfortably overhangs the current model.
label : str, optional
Human-readable label stored in the internal registry.
sync : bool, optional
Synchronise the OCC kernel after creation (default True).
Returns
Tag
Surface tag of the new cutting plane.
Example
::
# A vertical plane through (0, 0, 0) with normal (1, 0, 0)
g.model.geometry.add_cutting_plane(
point=(0, 0, 0), normal_vector=(1, 0, 0),
)
Source code in src/apeGmsh/core/_model_geometry.py
| def add_cutting_plane(
self,
point : list[float] | ndarray,
normal_vector : list[float] | ndarray,
*,
size : float | None = None,
label : str | None = None,
sync : bool = True,
) -> Tag:
"""
Create a square planar surface through ``point`` with the given
normal, suitable for clipping / section / visualisation views.
The surface is a plain BRep face built from 4 points + 4 lines
+ a curve loop + a plane surface, so it behaves exactly like
any other registered surface (it can be selected, meshed as a
discrete 2-D grid, exported to STEP, etc.). It is *not* a
Gmsh clipping plane in the rendering sense — it is real
geometry.
Parameters
----------
point : array-like of 3 floats
A point on the plane. The square is centred here.
normal_vector : array-like of 3 floats
Plane normal. Need not be unit-length — it is normalised
internally.
size : float, optional
Edge length of the square. When ``None`` (default), size
is picked as ``2 × max(model_bbox_diagonal, 1.0)`` so the
square comfortably overhangs the current model.
label : str, optional
Human-readable label stored in the internal registry.
sync : bool, optional
Synchronise the OCC kernel after creation (default True).
Returns
-------
Tag
Surface tag of the new cutting plane.
Example
-------
::
# A vertical plane through (0, 0, 0) with normal (1, 0, 0)
g.model.geometry.add_cutting_plane(
point=(0, 0, 0), normal_vector=(1, 0, 0),
)
"""
p = np.asarray(point, dtype=float)
n = np.asarray(normal_vector, dtype=float)
n_norm = float(np.linalg.norm(n))
if n_norm == 0.0:
raise ValueError("normal_vector must be non-zero")
n = n / n_norm
# Pick size from the current model bounding box when not given.
# ``getBoundingBox`` requires a synchronised OCC state, so this
# is the only hard reason for a pre-sync. When the caller
# supplies an explicit ``size``, we do not sync until the end.
if size is None:
gmsh.model.occ.synchronize()
xmin, ymin, zmin, xmax, ymax, zmax = gmsh.model.getBoundingBox(-1, -1)
diag = float(np.linalg.norm([xmax - xmin, ymax - ymin, zmax - zmin]))
size = 2.0 * max(diag, 1.0)
# Orthonormal basis (u, v) spanning the plane. v does not need
# explicit normalisation: if ``n`` and ``u`` are unit and
# orthogonal, then ``cross(n, u)`` is also unit.
ref = (
np.array([1.0, 0.0, 0.0]) if abs(n[0]) < 0.9
else np.array([0.0, 1.0, 0.0])
)
u = np.cross(n, ref)
u /= np.linalg.norm(u)
v = np.cross(n, u)
half = size / 2.0
corner_coeffs = [(-half, -half), (half, -half), (half, half), (-half, half)]
corners = [p + a * u + b * v for a, b in corner_coeffs]
# Delegate to the existing BRep primitives so the new points,
# lines, loop, and surface all flow through ``_register`` /
# ``_log`` with the correct kinds. Defer every sub-sync so we
# sync exactly once at the end (or zero times if sync=False).
pt_tags = [
self.add_point(float(c[0]), float(c[1]), float(c[2]), sync=False)
for c in corners
]
ln_tags = [
self.add_line(pt_tags[i], pt_tags[(i + 1) % 4], sync=False)
for i in range(4)
]
loop_tag = self.add_curve_loop(ln_tags, sync=False)
# Defer the label until AFTER the sync below: a label PG bind
# via gmsh.model.addPhysicalGroup requires the entity to be
# visible to Gmsh, which only happens post-synchronize.
tag = self.add_plane_surface(loop_tag, sync=False)
if sync:
gmsh.model.occ.synchronize()
self._model._log(
f"add_cutting_plane(point={tuple(p)}, normal={tuple(n)}, "
f"size={size}) -> tag {tag}"
)
# Re-register as a cutting plane (overwrites the
# ``plane_surface`` metadata kind from add_plane_surface) and,
# now that the surface is synced, bind the label PG. When
# sync=False the caller has opted out of syncing, so we only
# update the metadata kind and skip label registration to
# avoid the "Unknown entity" warning.
if sync:
self._model._register(2, tag, label, 'cutting_plane')
else:
self._model._metadata[(2, tag)] = {'kind': 'cutting_plane'}
entry = self._model._metadata.get((2, tag))
if entry is not None:
entry['point'] = tuple(float(x) for x in p)
entry['normal'] = tuple(float(x) for x in n)
return tag
|
add_axis_cutting_plane
add_axis_cutting_plane(axis: Literal['x', 'y', 'z'], offset: float = 0.0, *, origin: list[float] | ndarray | None = None, rotation: float = 0.0, rotation_about: Literal['x', 'y', 'z'] | None = None, label: str | None = None, sync: bool = True) -> Tag
Add an axis-aligned cutting plane, optionally tilted by a rotation.
Convenience wrapper around :meth:add_cutting_plane. The plane is
initially defined as normal to axis (so axis='z' produces
a horizontal XY-plane). It is then:
- Offset along its base normal by
offset.
- Rotated by
rotation degrees about rotation_about
(if both are given), producing a tilted plane through the same
anchor point.
Parameters
axis : {'x', 'y', 'z'}
Axis the plane is normal to. 'z' -> XY plane, etc.
offset : float, optional
Signed distance along the base normal from origin
(or from the global origin if origin is None).
origin : array-like of 3 floats, optional
Anchor point before the offset is applied. Defaults to (0, 0, 0).
rotation : float, optional
Rotation angle in degrees. Requires rotation_about
to have any effect — passing rotation without
rotation_about raises ValueError so silent
no-ops do not sneak through.
rotation_about : {'x', 'y', 'z'}, optional
Axis about which the base normal is rotated. Must differ
from axis for the rotation to have any effect.
label : str, optional
Human-readable label stored in the internal registry.
sync : bool, optional
Synchronise the OCC kernel after creation (default True).
Returns
Tag
Surface tag of the new cutting plane.
Examples
Horizontal plane at z = 3::
g.model.geometry.add_axis_cutting_plane('z', offset=3.0)
Vertical YZ-plane passing through x = 1.5::
g.model.geometry.add_axis_cutting_plane('x', offset=1.5)
Horizontal plane tilted 15° about the y-axis::
g.model.geometry.add_axis_cutting_plane(
'z', offset=0.0,
rotation=15.0, rotation_about='y',
)
Source code in src/apeGmsh/core/_model_geometry.py
| def add_axis_cutting_plane(
self,
axis : Literal['x', 'y', 'z'],
offset : float = 0.0,
*,
origin : list[float] | ndarray | None = None,
rotation : float = 0.0,
rotation_about: Literal['x', 'y', 'z'] | None = None,
label : str | None = None,
sync : bool = True,
) -> Tag:
"""
Add an axis-aligned cutting plane, optionally tilted by a rotation.
Convenience wrapper around :meth:`add_cutting_plane`. The plane is
initially defined as **normal to** ``axis`` (so ``axis='z'`` produces
a horizontal XY-plane). It is then:
1. Offset along its base normal by ``offset``.
2. Rotated by ``rotation`` degrees about ``rotation_about``
(if both are given), producing a tilted plane through the same
anchor point.
Parameters
----------
axis : {'x', 'y', 'z'}
Axis the plane is **normal to**. ``'z'`` -> XY plane, etc.
offset : float, optional
Signed distance along the base normal from ``origin``
(or from the global origin if ``origin`` is None).
origin : array-like of 3 floats, optional
Anchor point before the offset is applied. Defaults to (0, 0, 0).
rotation : float, optional
Rotation angle in **degrees**. Requires ``rotation_about``
to have any effect — passing ``rotation`` without
``rotation_about`` raises ``ValueError`` so silent
no-ops do not sneak through.
rotation_about : {'x', 'y', 'z'}, optional
Axis about which the base normal is rotated. Must differ
from ``axis`` for the rotation to have any effect.
label : str, optional
Human-readable label stored in the internal registry.
sync : bool, optional
Synchronise the OCC kernel after creation (default True).
Returns
-------
Tag
Surface tag of the new cutting plane.
Examples
--------
Horizontal plane at z = 3::
g.model.geometry.add_axis_cutting_plane('z', offset=3.0)
Vertical YZ-plane passing through x = 1.5::
g.model.geometry.add_axis_cutting_plane('x', offset=1.5)
Horizontal plane tilted 15° about the y-axis::
g.model.geometry.add_axis_cutting_plane(
'z', offset=0.0,
rotation=15.0, rotation_about='y',
)
"""
if axis not in _AXIS_UNIT_VEC:
raise ValueError(
f"axis must be one of 'x', 'y', 'z'; got {axis!r}"
)
if rotation != 0.0 and rotation_about is None:
raise ValueError(
"rotation was provided without rotation_about — pass "
"rotation_about='x'|'y'|'z' to tilt the plane, or drop "
"the rotation argument to keep it axis-aligned."
)
base_normal = _AXIS_UNIT_VEC[axis].copy()
if rotation_about is not None and rotation != 0.0:
if rotation_about not in _AXIS_UNIT_VEC:
raise ValueError(
f"rotation_about must be one of 'x', 'y', 'z'; "
f"got {rotation_about!r}"
)
if rotation_about == axis:
self._model._log(
f"add_axis_cutting_plane: rotation_about='{rotation_about}' "
f"equals axis='{axis}'; rotation has no effect."
)
else:
theta = np.deg2rad(rotation)
k = _AXIS_UNIT_VEC[rotation_about]
c, s = np.cos(theta), np.sin(theta)
# Rodrigues' rotation formula — k is a unit vector.
base_normal = (
base_normal * c
+ np.cross(k, base_normal) * s
+ k * np.dot(k, base_normal) * (1.0 - c)
)
if origin is None:
origin_arr = np.zeros(3)
else:
origin_arr = np.asarray(origin, dtype=float)
if origin_arr.shape != (3,):
raise ValueError(
f"origin must be a length-3 vector; got shape {origin_arr.shape}"
)
point = origin_arr + offset * base_normal
self._model._log(
f"add_axis_cutting_plane(axis={axis!r}, offset={offset}, "
f"origin={tuple(origin_arr)}, rotation={rotation}, "
f"rotation_about={rotation_about!r}) "
f"-> point={tuple(point)}, normal={tuple(base_normal)}"
)
return self.add_cutting_plane(
point=point,
normal_vector=base_normal,
label=label,
sync=sync,
)
|
cut_by_surface
cut_by_surface(solid: Tag | str | list[Tag | str] | None, surface, *, keep_surface: bool = True, remove_original: bool = True, label: str | None = None, sync: bool = True) -> list[Tag]
Split one or more solids with an arbitrary cutting surface.
Uses OCC's fragment operation under the hood, which splits
every input shape at its intersections and keeps all
resulting sub-shapes. Unlike :meth:cut_by_plane, this method
does not classify the output pieces — callers that need
"above/below" semantics should use :meth:cut_by_plane (which
delegates here and adds the classification step).
Parameters
solid : Tag, list[Tag], or None
Volume(s) to cut. When None, every registered volume
in the model is cut against the surface.
surface : Tag, str, or (2, tag)
The cutting surface. Can be any registered 2-D entity —
a plane from :meth:add_cutting_plane, a STEP-imported
trimmed surface, a Coons patch, etc. Accepts a raw tag,
a label or PG name, or an explicit (2, tag) tuple.
Must resolve to exactly one surface.
keep_surface : bool, default True
Leave the (now-trimmed) surface in the model after the
cut. Useful when you want to mesh the cut interface as a
shared face for conformal ties. Set to False to
delete it.
remove_original : bool, default True
Consume the original solid(s) so only the cut pieces
remain. When False, OCC keeps the originals alongside
the pieces, which usually produces overlapping geometry
and is rarely what you want.
label : str, optional
Label applied to every new volume fragment in the
registry. Pass None to leave the fragments unlabelled.
sync : bool, default True
Synchronise the OCC kernel after the cut.
Returns
list[Tag]
Solid tags of the fragments produced by the cut, in the
order OCC returns them. An empty list means the cut
produced nothing new (shouldn't happen unless the surface
misses every input solid entirely).
Example
::
box = g.model.geometry.add_box(0, 0, 0, 1, 1, 1)
plane = g.model.geometry.add_axis_cutting_plane('z', offset=0.5)
pieces = g.model.geometry.cut_by_surface(box, plane)
Source code in src/apeGmsh/core/_model_geometry.py
| def cut_by_surface(
self,
solid : Tag | str | list[Tag | str] | None,
surface,
*,
keep_surface : bool = True,
remove_original: bool = True,
label : str | None = None,
sync : bool = True,
) -> list[Tag]:
"""
Split one or more solids with an arbitrary cutting surface.
Uses OCC's ``fragment`` operation under the hood, which splits
every input shape at its intersections and keeps **all**
resulting sub-shapes. Unlike :meth:`cut_by_plane`, this method
does not classify the output pieces — callers that need
"above/below" semantics should use :meth:`cut_by_plane` (which
delegates here and adds the classification step).
Parameters
----------
solid : Tag, list[Tag], or None
Volume(s) to cut. When ``None``, every registered volume
in the model is cut against the surface.
surface : Tag, str, or (2, tag)
The cutting surface. Can be any registered 2-D entity —
a plane from :meth:`add_cutting_plane`, a STEP-imported
trimmed surface, a Coons patch, etc. Accepts a raw tag,
a label or PG name, or an explicit ``(2, tag)`` tuple.
Must resolve to exactly one surface.
keep_surface : bool, default True
Leave the (now-trimmed) surface in the model after the
cut. Useful when you want to mesh the cut interface as a
shared face for conformal ties. Set to ``False`` to
delete it.
remove_original : bool, default True
Consume the original solid(s) so only the cut pieces
remain. When ``False``, OCC keeps the originals alongside
the pieces, which usually produces overlapping geometry
and is rarely what you want.
label : str, optional
Label applied to every new volume fragment in the
registry. Pass ``None`` to leave the fragments unlabelled.
sync : bool, default True
Synchronise the OCC kernel after the cut.
Returns
-------
list[Tag]
Solid tags of the fragments produced by the cut, in the
order OCC returns them. An empty list means the cut
produced nothing new (shouldn't happen unless the surface
misses every input solid entirely).
Example
-------
::
box = g.model.geometry.add_box(0, 0, 0, 1, 1, 1)
plane = g.model.geometry.add_axis_cutting_plane('z', offset=0.5)
pieces = g.model.geometry.cut_by_surface(box, plane)
"""
solid_tags = self._normalize_solid_input(solid, self._collect_volume_tags)
# Sync before resolving: the dim auto-detector queries
# gmsh.model.getEntities, which only sees synced OCC state.
# Without this, an unsynced surface tag can be misread as a
# stale curve sharing the same numeric tag value.
from ._helpers import resolve_to_single_dimtag
gmsh.model.occ.synchronize()
surf_dim, surf_tag = resolve_to_single_dimtag(
surface, default_dim=2, session=self._model._parent,
what="cutting surface",
)
if surf_dim != 2:
raise ValueError(
f"cut_by_surface: surface ref {surface!r} resolved to "
f"dim={surf_dim} (tag {surf_tag}); expected a 2-D entity."
)
obj_dt = [(3, int(t)) for t in solid_tags]
tool_dt = [(2, int(surf_tag))]
# Collect the original labels BEFORE the boolean so we can
# propagate them to fragments afterwards.
inherited_label = label
if inherited_label is None and remove_original:
labels_comp = getattr(self._model._parent, 'labels', None)
if labels_comp is not None:
original_labels: set[str] = set()
for t in solid_tags:
original_labels.update(
labels_comp.labels_for_entity(3, int(t))
)
if len(original_labels) == 1:
inherited_label = original_labels.pop()
with pg_preserved() as pg:
out_dimtags, result_map = gmsh.model.occ.fragment(
obj_dt,
tool_dt,
removeObject=remove_original,
removeTool=not keep_surface,
)
# Always sync before PG remap — remap needs topology visible.
gmsh.model.occ.synchronize()
pg.set_result(obj_dt + tool_dt, result_map)
new_volume_tags: list[Tag] = [
int(t) for (d, t) in out_dimtags if d == 3
]
if remove_original:
for t in solid_tags:
self._model._metadata.pop((3, int(t)), None)
# Register metadata for new fragments. Label= is only passed
# when the user explicitly provided one; inherited labels are
# already handled by the snapshot-remap above.
for t in new_volume_tags:
self._model._register(3, t, label, 'cut_fragment')
if keep_surface:
surviving_surfaces = [
int(t) for (d, t) in out_dimtags if d == 2
]
for t in surviving_surfaces:
if (2, t) not in self._model._metadata:
self._model._register(2, t, None, 'cut_interface')
self._model._log(
f"cut_by_surface(solids={solid_tags}, surface={int(surf_tag)}) "
f"-> {len(new_volume_tags)} volume fragment(s): {new_volume_tags}"
)
return new_volume_tags
|
cut_by_plane
cut_by_plane(solid: Tag | str | list[Tag | str] | None, plane, *, keep_plane: bool = True, remove_original: bool = True, above_direction: list[float] | ndarray | None = None, label_above: str | None = None, label_below: str | None = None, sync: bool = True) -> tuple[list[Tag], list[Tag]]
Split one or more solids with a plane and classify the
resulting pieces by which side of the plane they sit on.
Thin wrapper around :meth:cut_by_surface that additionally
computes which fragments are "above" (same side as the plane
normal) vs "below" the plane. The normal direction is
resolved from, in order of priority:
- An explicit
above_direction argument.
- The
normal and point stashed in the registry by
:meth:add_cutting_plane / :meth:add_axis_cutting_plane.
gmsh.model.getNormal sampled at the parametric centre
of the plane surface.
Parameters
solid : Tag, list[Tag], or None
Volume(s) to cut. None = every registered volume.
plane : Tag
Planar surface to cut with. Accepts a raw tag, a label
or PG name, or an explicit (2, tag) tuple — must
resolve to exactly one 2-D entity. Ideally built by
:meth:add_cutting_plane so its normal and point are in
the registry; other planar surfaces work too but require
an explicit above_direction or fall back to querying
Gmsh.
keep_plane : bool, default True
Leave the trimmed plane in the model as a registered
surface (useful for meshing the cut interface).
remove_original : bool, default True
Consume the original solid(s).
above_direction : array-like of 3 floats, optional
Override the plane's normal direction. Pieces whose
centroid dotted with this vector (relative to the plane
point) is positive are classified as "above".
label_above, label_below : str, optional
Labels applied to the above / below fragment solids.
sync : bool, default True
Synchronise the OCC kernel after the cut.
Returns
tuple[list[Tag], list[Tag]]
(above_tags, below_tags) — solid tags on each side of
the plane, classified by the sign of
(centroid - plane_point) · normal.
Example
::
col = g.model.geometry.add_box(0, 0, 0, 1, 1, 3)
pl = g.model.geometry.add_axis_cutting_plane('z', offset=1.5)
top, bot = g.model.geometry.cut_by_plane(
col, pl,
label_above="col_upper", label_below="col_lower",
)
Source code in src/apeGmsh/core/_model_geometry.py
| def cut_by_plane(
self,
solid : Tag | str | list[Tag | str] | None,
plane,
*,
keep_plane : bool = True,
remove_original: bool = True,
above_direction: list[float] | ndarray | None = None,
label_above : str | None = None,
label_below : str | None = None,
sync : bool = True,
) -> tuple[list[Tag], list[Tag]]:
"""
Split one or more solids with a plane and classify the
resulting pieces by which side of the plane they sit on.
Thin wrapper around :meth:`cut_by_surface` that additionally
computes which fragments are "above" (same side as the plane
normal) vs "below" the plane. The normal direction is
resolved from, in order of priority:
1. An explicit ``above_direction`` argument.
2. The ``normal`` and ``point`` stashed in the registry by
:meth:`add_cutting_plane` / :meth:`add_axis_cutting_plane`.
3. ``gmsh.model.getNormal`` sampled at the parametric centre
of the plane surface.
Parameters
----------
solid : Tag, list[Tag], or None
Volume(s) to cut. ``None`` = every registered volume.
plane : Tag
Planar surface to cut with. Accepts a raw tag, a label
or PG name, or an explicit ``(2, tag)`` tuple — must
resolve to exactly one 2-D entity. Ideally built by
:meth:`add_cutting_plane` so its normal and point are in
the registry; other planar surfaces work too but require
an explicit ``above_direction`` or fall back to querying
Gmsh.
keep_plane : bool, default True
Leave the trimmed plane in the model as a registered
surface (useful for meshing the cut interface).
remove_original : bool, default True
Consume the original solid(s).
above_direction : array-like of 3 floats, optional
Override the plane's normal direction. Pieces whose
centroid dotted with this vector (relative to the plane
point) is positive are classified as "above".
label_above, label_below : str, optional
Labels applied to the above / below fragment solids.
sync : bool, default True
Synchronise the OCC kernel after the cut.
Returns
-------
tuple[list[Tag], list[Tag]]
``(above_tags, below_tags)`` — solid tags on each side of
the plane, classified by the sign of
``(centroid - plane_point) · normal``.
Example
-------
::
col = g.model.geometry.add_box(0, 0, 0, 1, 1, 3)
pl = g.model.geometry.add_axis_cutting_plane('z', offset=1.5)
top, bot = g.model.geometry.cut_by_plane(
col, pl,
label_above="col_upper", label_below="col_lower",
)
"""
# Sync before resolving: the dim auto-detector queries
# gmsh.model.getEntities, which only sees synced OCC state.
# Without this, an unsynced plane tag can be misread as a
# stale curve sharing the same numeric tag value.
from ._helpers import resolve_to_single_dimtag
gmsh.model.occ.synchronize()
plane_dim, plane_tag = resolve_to_single_dimtag(
plane, default_dim=2, session=self._model._parent,
what="cutting plane",
)
if plane_dim != 2:
raise ValueError(
f"cut_by_plane: plane ref {plane!r} resolved to "
f"dim={plane_dim} (tag {plane_tag}); expected a 2-D entity."
)
plane_tag = int(plane_tag)
normal, point = self._resolve_plane_normal(
plane_tag, above_direction,
)
# Perform the actual cut via the general surface method.
# sync=True so the PG remap and classify step see synced topology.
fragments = self.cut_by_surface(
solid,
plane_tag,
keep_surface=keep_plane,
remove_original=remove_original,
label=None, # we re-label by side below
sync=True,
)
above_tags, below_tags = self._classify_fragments(
fragments, normal, point,
label_above=label_above,
label_below=label_below,
)
if not above_tags or not below_tags:
self._model._log(
f"cut_by_plane: WARNING plane {plane_tag} produced "
f"only one side ({len(above_tags)} above, "
f"{len(below_tags)} below) — the plane may not "
f"intersect the solid(s)"
)
if sync:
gmsh.model.occ.synchronize()
self._model._log(
f"cut_by_plane(plane={plane_tag}) -> "
f"above={above_tags}, below={below_tags}"
)
return above_tags, below_tags
|
slice
slice(solid: Tag | str | list[Tag | str] | None = None, *, axis: Literal['x', 'y', 'z'], offset: float = 0.0, classify: bool = False, label: str | None = None, sync: bool = True) -> list[Tag] | tuple[list[Tag], list[Tag]]
Slice solids at an axis-aligned plane in one atomic call.
Internally creates a temporary cutting plane, fragments the
solids, removes the cutting plane (and any trimmed surfaces
it left behind), and returns the volume fragments. No
orphaned geometry is left in the model.
Parameters
solid : Tag, list[Tag], or None
Volume(s) to slice. None slices every registered
volume in the model.
axis : {'x', 'y', 'z'}
Axis the plane is normal to. 'z' slices with
a horizontal XY-plane, etc.
offset : float, default 0.0
Signed distance along the axis from the origin.
classify : bool, default False
When True, returns (positive_side, negative_side)
classified by the plane's normal direction (the positive
axis direction). When False (default), returns all
fragments as a flat list.
label : str, optional
Label applied to every fragment in the registry.
sync : bool, default True
Synchronise the OCC kernel after the operation.
Returns
list[Tag]
All volume fragments (when classify=False).
tuple[list[Tag], list[Tag]]
(positive_side, negative_side) fragments classified
by which side of the plane each piece's centroid sits on
(when classify=True).
Example
::
# Slice a box at y = 0.5
box = g.model.geometry.add_box(0, 0, 0, 1, 1, 1)
pieces = g.model.geometry.slice(box, axis='y', offset=0.5)
# Slice and classify
top, bot = g.model.geometry.slice(
box, axis='z', offset=0.5, classify=True,
)
# Slice all volumes at x = 0
g.model.geometry.slice(axis='x', offset=0.0)
Source code in src/apeGmsh/core/_model_geometry.py
| def slice(
self,
solid : Tag | str | list[Tag | str] | None = None,
*,
axis : Literal['x', 'y', 'z'],
offset : float = 0.0,
classify: bool = False,
label : str | None = None,
sync : bool = True,
) -> list[Tag] | tuple[list[Tag], list[Tag]]:
"""
Slice solids at an axis-aligned plane in one atomic call.
Internally creates a temporary cutting plane, fragments the
solids, removes the cutting plane (and any trimmed surfaces
it left behind), and returns the volume fragments. No
orphaned geometry is left in the model.
Parameters
----------
solid : Tag, list[Tag], or None
Volume(s) to slice. ``None`` slices every registered
volume in the model.
axis : {'x', 'y', 'z'}
Axis the plane is **normal to**. ``'z'`` slices with
a horizontal XY-plane, etc.
offset : float, default 0.0
Signed distance along the axis from the origin.
classify : bool, default False
When True, returns ``(positive_side, negative_side)``
classified by the plane's normal direction (the positive
axis direction). When False (default), returns all
fragments as a flat list.
label : str, optional
Label applied to every fragment in the registry.
sync : bool, default True
Synchronise the OCC kernel after the operation.
Returns
-------
list[Tag]
All volume fragments (when ``classify=False``).
tuple[list[Tag], list[Tag]]
``(positive_side, negative_side)`` fragments classified
by which side of the plane each piece's centroid sits on
(when ``classify=True``).
Example
-------
::
# Slice a box at y = 0.5
box = g.model.geometry.add_box(0, 0, 0, 1, 1, 1)
pieces = g.model.geometry.slice(box, axis='y', offset=0.5)
# Slice and classify
top, bot = g.model.geometry.slice(
box, axis='z', offset=0.5, classify=True,
)
# Slice all volumes at x = 0
g.model.geometry.slice(axis='x', offset=0.0)
"""
# Snapshot ALL entities before creating the cutting plane.
# After the slice, anything that was created during the
# operation but isn't a surviving volume fragment gets
# removed. This catches the cutting-plane corner points,
# edges, curve loops, and trimmed surfaces that the old
# registry-based cleanup missed.
pre_entities: dict[int, set[int]] = {}
for d in range(4):
pre_entities[d] = {t for _, t in gmsh.model.getEntities(d)}
plane_tag = self.add_axis_cutting_plane(
axis, offset=offset, sync=False,
)
# add_axis_cutting_plane just produced a 2-D surface but didn't
# synchronise — pass an explicit (2, plane_tag) dimtag so the
# downstream resolve_dim() doesn't need to query getEntities(2).
plane_dt: DimTag = (2, plane_tag)
if classify:
above, below = self.cut_by_plane(
solid, plane_dt,
keep_plane=False,
label_above=label,
label_below=label,
sync=False,
)
result: list[Tag] | tuple[list[Tag], list[Tag]] = (above, below)
else:
fragments = self.cut_by_surface(
solid, plane_dt,
keep_surface=False,
label=label,
sync=False,
)
result = fragments
# Determine which volume tags are the real output.
if classify:
keep_vol_tags = set(above) | set(below)
else:
keep_vol_tags = set(fragments)
self._cleanup_slice_orphans(pre_entities, keep_vol_tags)
if sync:
gmsh.model.occ.synchronize()
self._model._log(
f"slice(axis={axis!r}, offset={offset}, classify={classify})"
)
return result
|
add_box
add_box(x: float, y: float, z: float, dx: float, dy: float, dz: float, *, label: str | None = None, sync: bool = True) -> Tag
Add an axis-aligned box.
Parameters
x, y, z : origin corner
dx, dy, dz : extents along X, Y, Z
Source code in src/apeGmsh/core/_model_geometry.py
| def add_box(
self,
x: float, y: float, z: float,
dx: float, dy: float, dz: float,
*,
label: str | None = None,
sync : bool = True,
) -> Tag:
"""
Add an axis-aligned box.
Parameters
----------
x, y, z : origin corner
dx, dy, dz : extents along X, Y, Z
"""
tag = gmsh.model.occ.addBox(x, y, z, dx, dy, dz)
return self._add_solid(
tag, 'box', f"add_box(origin=({x},{y},{z}), size=({dx},{dy},{dz}))",
label=label, sync=sync,
)
|
add_sphere
add_sphere(cx: float, cy: float, cz: float, radius: float, *, label: str | None = None, sync: bool = True) -> Tag
Add a sphere centred at (cx, cy, cz) with the given radius.
Source code in src/apeGmsh/core/_model_geometry.py
| def add_sphere(
self,
cx: float, cy: float, cz: float,
radius: float,
*,
label: str | None = None,
sync : bool = True,
) -> Tag:
"""Add a sphere centred at (cx, cy, cz) with the given radius."""
tag = gmsh.model.occ.addSphere(cx, cy, cz, radius)
return self._add_solid(
tag, 'sphere', f"add_sphere(centre=({cx},{cy},{cz}), r={radius})",
label=label, sync=sync,
)
|
add_cylinder
add_cylinder(x: float, y: float, z: float, dx: float, dy: float, dz: float, radius: float, *, angle: float = 2 * math.pi, label: str | None = None, sync: bool = True) -> Tag
Add a cylinder.
Parameters
x, y, z : base-circle centre
dx, dy, dz : axis direction vector (length = height of cylinder)
radius : base radius
angle : sweep angle in radians (default 2π = full cylinder)
Source code in src/apeGmsh/core/_model_geometry.py
| def add_cylinder(
self,
x: float, y: float, z: float,
dx: float, dy: float, dz: float,
radius: float,
*,
angle: float = 2 * math.pi,
label: str | None = None,
sync : bool = True,
) -> Tag:
"""
Add a cylinder.
Parameters
----------
x, y, z : base-circle centre
dx, dy, dz : axis direction vector (length = height of cylinder)
radius : base radius
angle : sweep angle in radians (default 2π = full cylinder)
"""
tag = gmsh.model.occ.addCylinder(x, y, z, dx, dy, dz, radius, angle=angle)
return self._add_solid(
tag, 'cylinder',
f"add_cylinder(base=({x},{y},{z}), axis=({dx},{dy},{dz}), r={radius})",
label=label, sync=sync,
)
|
add_cone
add_cone(x: float, y: float, z: float, dx: float, dy: float, dz: float, r1: float, r2: float, *, angle: float = 2 * math.pi, label: str | None = None, sync: bool = True) -> Tag
Add a cone / truncated cone.
Parameters
x, y, z : base-circle centre
dx, dy, dz : axis vector
r1 : base radius
r2 : top radius (0 = sharp cone)
angle : sweep angle in radians
Source code in src/apeGmsh/core/_model_geometry.py
| def add_cone(
self,
x: float, y: float, z: float,
dx: float, dy: float, dz: float,
r1: float, r2: float,
*,
angle: float = 2 * math.pi,
label: str | None = None,
sync : bool = True,
) -> Tag:
"""
Add a cone / truncated cone.
Parameters
----------
x, y, z : base-circle centre
dx, dy, dz : axis vector
r1 : base radius
r2 : top radius (0 = sharp cone)
angle : sweep angle in radians
"""
tag = gmsh.model.occ.addCone(x, y, z, dx, dy, dz, r1, r2, angle=angle)
return self._add_solid(
tag, 'cone', f"add_cone(base=({x},{y},{z}), r1={r1}, r2={r2})",
label=label, sync=sync,
)
|
add_torus
add_torus(cx: float, cy: float, cz: float, r1: float, r2: float, *, angle: float = 2 * math.pi, label: str | None = None, sync: bool = True) -> Tag
Add a torus.
Parameters
cx, cy, cz : centre
r1 : major radius (axis to tube centre)
r2 : minor radius (tube cross-section)
angle : sweep angle in radians
Source code in src/apeGmsh/core/_model_geometry.py
| def add_torus(
self,
cx: float, cy: float, cz: float,
r1: float, r2: float,
*,
angle: float = 2 * math.pi,
label: str | None = None,
sync : bool = True,
) -> Tag:
"""
Add a torus.
Parameters
----------
cx, cy, cz : centre
r1 : major radius (axis to tube centre)
r2 : minor radius (tube cross-section)
angle : sweep angle in radians
"""
tag = gmsh.model.occ.addTorus(cx, cy, cz, r1, r2, angle=angle)
return self._add_solid(
tag, 'torus', f"add_torus(centre=({cx},{cy},{cz}), R={r1}, r={r2})",
label=label, sync=sync,
)
|
add_wedge
add_wedge(x: float, y: float, z: float, dx: float, dy: float, dz: float, ltx: float, *, label: str | None = None, sync: bool = True) -> Tag
Add a right-angle wedge.
Parameters
x, y, z : origin corner
dx, dy, dz : extents
ltx : top X extent (0 = sharp wedge)
Source code in src/apeGmsh/core/_model_geometry.py
| def add_wedge(
self,
x: float, y: float, z: float,
dx: float, dy: float, dz: float,
ltx: float,
*,
label: str | None = None,
sync : bool = True,
) -> Tag:
"""
Add a right-angle wedge.
Parameters
----------
x, y, z : origin corner
dx, dy, dz : extents
ltx : top X extent (0 = sharp wedge)
"""
tag = gmsh.model.occ.addWedge(x, y, z, dx, dy, dz, ltx=ltx)
return self._add_solid(
tag, 'wedge',
f"add_wedge(origin=({x},{y},{z}), size=({dx},{dy},{dz}), ltx={ltx})",
label=label, sync=sync,
)
|
g.model.boolean
apeGmsh.core._model_boolean._Boolean
Boolean-operation sub-composite extracted from Model.
Source code in src/apeGmsh/core/_model_boolean.py
| def __init__(self, model: "Model") -> None:
self._model = model
|
fuse
fuse(objects: EntityRefs, tools: EntityRefs, *, dim: int = 3, remove_object: bool = True, remove_tool: bool = True, sync: bool = True, label: str | None = None) -> list[Tag]
Boolean union (A ∪ B). Returns surviving volume tags.
When label= is supplied, the labels carried by the inputs
are dropped from the result and the new label is attached
instead. Without label=, all input labels survive on the
merged volume.
Example
result = g.model.boolean.fuse(box, sphere, label='merged')
Source code in src/apeGmsh/core/_model_boolean.py
| def fuse(
self,
objects : EntityRefs,
tools : EntityRefs,
*,
dim : int = 3,
remove_object : bool = True,
remove_tool : bool = True,
sync : bool = True,
label : str | None = None,
) -> list[Tag]:
"""
Boolean union (A \u222a B). Returns surviving volume tags.
When ``label=`` is supplied, the labels carried by the inputs
are dropped from the result and the new ``label`` is attached
instead. Without ``label=``, all input labels survive on the
merged volume.
Example
-------
``result = g.model.boolean.fuse(box, sphere, label='merged')``
"""
return self._bool_op(
'fuse', objects, tools, dim,
remove_object, remove_tool, sync, label=label,
)
|
cut
cut(objects: EntityRefs, tools: EntityRefs, *, dim: int = 3, remove_object: bool = True, remove_tool: bool = True, sync: bool = True, label: str | None = None) -> list[Tag]
Boolean difference (A − B). Returns surviving volume tags.
When label= is supplied, the object's label is dropped
from the result and the new label is attached instead.
Example
result = g.model.boolean.cut(box, cylinder, label='holey')
Source code in src/apeGmsh/core/_model_boolean.py
| def cut(
self,
objects : EntityRefs,
tools : EntityRefs,
*,
dim : int = 3,
remove_object : bool = True,
remove_tool : bool = True,
sync : bool = True,
label : str | None = None,
) -> list[Tag]:
"""
Boolean difference (A \u2212 B). Returns surviving volume tags.
When ``label=`` is supplied, the object's label is dropped
from the result and the new ``label`` is attached instead.
Example
-------
``result = g.model.boolean.cut(box, cylinder, label='holey')``
"""
return self._bool_op(
'cut', objects, tools, dim,
remove_object, remove_tool, sync, label=label,
)
|
intersect
intersect(objects: EntityRefs, tools: EntityRefs, *, dim: int = 3, remove_object: bool = True, remove_tool: bool = True, sync: bool = True, label: str | None = None) -> list[Tag]
Boolean intersection (A ∩ B). Returns surviving volume tags.
When label= is supplied, the input labels are dropped from
the intersection and the new label is attached instead.
Source code in src/apeGmsh/core/_model_boolean.py
| def intersect(
self,
objects : EntityRefs,
tools : EntityRefs,
*,
dim : int = 3,
remove_object : bool = True,
remove_tool : bool = True,
sync : bool = True,
label : str | None = None,
) -> list[Tag]:
"""Boolean intersection (A \u2229 B). Returns surviving volume tags.
When ``label=`` is supplied, the input labels are dropped from
the intersection and the new ``label`` is attached instead.
"""
return self._bool_op(
'intersect', objects, tools, dim,
remove_object, remove_tool, sync, label=label,
)
|
fragment
fragment(objects: EntityRefs, tools: EntityRefs, *, dim: int = 3, remove_object: bool = True, remove_tool: bool = True, cleanup_free: bool = True, sync: bool = True) -> list[Tag]
Boolean fragment — splits all shapes at their intersections and
preserves all sub-volumes (useful for conformal meshing).
Parameters
objects : tag(s) of the entities to fragment.
tools : tag(s) of the cutting entities (e.g. rectangles).
Dimensions are auto-resolved from the registry, so bare
integer tags work even when tools have a different dimension
than dim.
dim : target dimension for bare integer tags in objects
(default 3).
remove_object, remove_tool : passed to OCC (default True).
cleanup_free : bool
When True (default), remove any "free" surfaces that do not
bound a volume after the fragment operation. This cleans up
exterior remnants of cutting planes that fall outside the
solid. Set to False to keep all surface fragments.
sync : synchronise the OCC kernel (default True).
Returns
list[Tag]
Tags of all surviving entities at the target dimension.
Source code in src/apeGmsh/core/_model_boolean.py
| def fragment(
self,
objects : EntityRefs,
tools : EntityRefs,
*,
dim : int = 3,
remove_object : bool = True,
remove_tool : bool = True,
cleanup_free : bool = True,
sync : bool = True,
) -> list[Tag]:
"""
Boolean fragment \u2014 splits all shapes at their intersections and
preserves all sub-volumes (useful for conformal meshing).
Parameters
----------
objects : tag(s) of the entities to fragment.
tools : tag(s) of the cutting entities (e.g. rectangles).
Dimensions are auto-resolved from the registry, so bare
integer tags work even when tools have a different dimension
than *dim*.
dim : target dimension for bare integer tags in *objects*
(default 3).
remove_object, remove_tool : passed to OCC (default True).
cleanup_free : bool
When True (default), remove any "free" surfaces that do not
bound a volume after the fragment operation. This cleans up
exterior remnants of cutting planes that fall outside the
solid. Set to False to keep all surface fragments.
sync : synchronise the OCC kernel (default True).
Returns
-------
list[Tag]
Tags of all surviving entities at the target dimension.
"""
result = self._bool_op(
'fragment', objects, tools, dim,
remove_object, remove_tool, sync,
)
# ``cleanup_free`` removes dim=2 surfaces that have no upward
# adjacency to a volume — useful after a 3D fragment to drop
# stray cutting-plane remnants. In a 2D-only model every
# surface has no volume neighbour (there ARE no volumes), so
# the sweep would destroy every surface in the model. Skip
# the cleanup when no 3D entities exist.
# An embedded interior surface (e.g. a future crack plane) is
# also adjacency-free, so we keep any free surface whose
# centroid falls inside some volume's bounding box; only
# surfaces clearly outside every volume are deleted.
if cleanup_free and gmsh.model.getEntities(3):
vol_bboxes = [
gmsh.model.getBoundingBox(3, vt)
for _, vt in gmsh.model.getEntities(3)
]
free: list[tuple[int, int]] = []
for _, tag_s in gmsh.model.getEntities(2):
up, _ = gmsh.model.getAdjacencies(2, tag_s)
if len(up) != 0:
continue
cx, cy, cz = gmsh.model.occ.getCenterOfMass(2, tag_s)
inside_any = any(
xmin <= cx <= xmax
and ymin <= cy <= ymax
and zmin <= cz <= zmax
for xmin, ymin, zmin, xmax, ymax, zmax in vol_bboxes
)
if not inside_any:
free.append((2, tag_s))
if free:
gmsh.model.occ.remove(free, recursive=True)
if sync:
gmsh.model.occ.synchronize()
for dt in free:
self._model._metadata.pop(dt, None)
cleanup_label_pgs(free)
self._model._log(
f"fragment cleanup: removed {len(free)} free surface(s)"
)
return result
|
_Transforms(model: 'Model')
Transform and extrusion/revolution sub-composite extracted from Model.
Source code in src/apeGmsh/core/_model_transforms.py
| def __init__(self, model: "Model") -> None:
self._model = model
|
translate(tags: EntityRefs, dx: float, dy: float, dz: float, *, dim: int = 3, sync: bool = True) -> '_Transforms'
Translate entities by (dx, dy, dz).
tags accepts int / label / PG name / (dim, tag) / list
of any mix. dim is the fallback for bare ints.
::
g.model.transforms.translate(box, 5, 0, 0)
g.model.transforms.translate("col.body", 0, 0, 5)
Source code in src/apeGmsh/core/_model_transforms.py
| def translate(
self,
tags: EntityRefs,
dx: float, dy: float, dz: float,
*,
dim : int = 3,
sync: bool = True,
) -> "_Transforms":
"""
Translate entities by (dx, dy, dz).
``tags`` accepts int / label / PG name / ``(dim, tag)`` / list
of any mix. *dim* is the fallback for bare ints.
Example
-------
::
g.model.transforms.translate(box, 5, 0, 0)
g.model.transforms.translate("col.body", 0, 0, 5)
"""
with pg_preserved_identity():
gmsh.model.occ.translate(self._resolve_dt(tags, dim), dx, dy, dz)
if sync:
gmsh.model.occ.synchronize()
self._model._log(f"translate by ({dx}, {dy}, {dz})")
return self
|
rotate(tags: EntityRefs, angle: float, *, ax: float = 0.0, ay: float = 0.0, az: float = 1.0, cx: float = 0.0, cy: float = 0.0, cz: float = 0.0, dim: int = 3, sync: bool = True) -> '_Transforms'
Rotate entities around an axis through (cx, cy, cz) with direction
(ax, ay, az) by angle radians.
tags accepts any flexible-ref form (see :meth:translate).
g.model.transforms.rotate(box, math.pi / 4, az=1)
Source code in src/apeGmsh/core/_model_transforms.py
| def rotate(
self,
tags : EntityRefs,
angle : float,
*,
ax: float = 0.0, ay: float = 0.0, az: float = 1.0,
cx: float = 0.0, cy: float = 0.0, cz: float = 0.0,
dim : int = 3,
sync: bool = True,
) -> "_Transforms":
"""
Rotate entities around an axis through (cx, cy, cz) with direction
(ax, ay, az) by ``angle`` radians.
``tags`` accepts any flexible-ref form (see :meth:`translate`).
Example
-------
``g.model.transforms.rotate(box, math.pi / 4, az=1)``
"""
with pg_preserved_identity():
gmsh.model.occ.rotate(
self._resolve_dt(tags, dim),
cx, cy, cz,
ax, ay, az,
angle,
)
if sync:
gmsh.model.occ.synchronize()
self._model._log(
f"rotate {math.degrees(angle):.2f}\u00b0 about axis=({ax},{ay},{az}) "
f"through ({cx},{cy},{cz})"
)
return self
|
scale(tags: EntityRefs, sx: float, sy: float, sz: float, *, cx: float = 0.0, cy: float = 0.0, cz: float = 0.0, dim: int = 3, sync: bool = True) -> '_Transforms'
Scale (dilate) entities by (sx, sy, sz) from centre (cx, cy, cz).
tags accepts any flexible-ref form (see :meth:translate).
g.model.transforms.scale(box, 2, 2, 2) # uniform double
Source code in src/apeGmsh/core/_model_transforms.py
| def scale(
self,
tags: EntityRefs,
sx: float, sy: float, sz: float,
*,
cx: float = 0.0, cy: float = 0.0, cz: float = 0.0,
dim : int = 3,
sync: bool = True,
) -> "_Transforms":
"""
Scale (dilate) entities by (sx, sy, sz) from centre (cx, cy, cz).
``tags`` accepts any flexible-ref form (see :meth:`translate`).
Example
-------
``g.model.transforms.scale(box, 2, 2, 2)`` # uniform double
"""
with pg_preserved_identity():
gmsh.model.occ.dilate(
self._resolve_dt(tags, dim),
cx, cy, cz,
sx, sy, sz,
)
if sync:
gmsh.model.occ.synchronize()
self._model._log(f"scale ({sx},{sy},{sz}) about ({cx},{cy},{cz})")
return self
|
mirror(tags: EntityRefs, a: float, b: float, c: float, d: float, *, dim: int = 3, sync: bool = True) -> '_Transforms'
Mirror entities through the plane ax + by + cz + d = 0.
tags accepts any flexible-ref form (see :meth:translate).
g.model.transforms.mirror(box, 1, 0, 0, 0) # reflect through YZ plane
Source code in src/apeGmsh/core/_model_transforms.py
| def mirror(
self,
tags: EntityRefs,
a: float, b: float, c: float, d: float,
*,
dim : int = 3,
sync: bool = True,
) -> "_Transforms":
"""
Mirror entities through the plane ax + by + cz + d = 0.
``tags`` accepts any flexible-ref form (see :meth:`translate`).
Example
-------
``g.model.transforms.mirror(box, 1, 0, 0, 0)`` # reflect through YZ plane
"""
with pg_preserved_identity():
gmsh.model.occ.mirror(self._resolve_dt(tags, dim), a, b, c, d)
if sync:
gmsh.model.occ.synchronize()
self._model._log(f"mirror through plane {a}x + {b}y + {c}z + {d} = 0")
return self
|
copy(tags: EntityRefs, *, dim: int = 3, sync: bool = True) -> list[Tag]
Duplicate entities. Returns the tags of the new copies.
tags accepts any flexible-ref form (see :meth:translate).
copies = g.model.transforms.copy([box, sphere])
Source code in src/apeGmsh/core/_model_transforms.py
| def copy(
self,
tags: EntityRefs,
*,
dim : int = 3,
sync: bool = True,
) -> list[Tag]:
"""
Duplicate entities. Returns the tags of the new copies.
``tags`` accepts any flexible-ref form (see :meth:`translate`).
Example
-------
``copies = g.model.transforms.copy([box, sphere])``
"""
with pg_preserved_identity():
new_dimtags = gmsh.model.occ.copy(self._resolve_dt(tags, dim))
if sync:
gmsh.model.occ.synchronize()
new_tags = [t for _, t in new_dimtags]
self._model._log(f"copy -> new tags {new_tags}")
return new_tags
|
extrude(tags: EntityRefs, dx: float, dy: float, dz: float, *, dim: int = 2, num_elements: list[int] | None = None, heights: list[float] | None = None, recombine: bool = False, sync: bool = True) -> list[DimTag]
Linear extrusion — sweeps entities along (dx, dy, dz).
Creates new geometry one dimension up: point -> curve,
curve -> surface, surface -> volume.
tags : entities to extrude.
dx, dy, dz : extrusion vector.
dim : default dimension for bare integer tags (default 2).
num_elements : structured layer counts, e.g. [10] for
10 layers. Empty list (default) = unstructured.
heights : relative heights per layer, e.g. [0.3, 0.7].
Must sum to 1.0 when provided. Empty = uniform layers.
recombine : if True, produce hex/quad elements instead of
tet/tri (requires structured layers).
sync : synchronise OCC kernel after extrusion (default True).
list[DimTag]
All generated (dim, tag) pairs. For a surface -> volume
extrusion the list contains the top face, the volume, and
the lateral faces — index into it to assign physical groups.
::
surf = g.model.geometry.add_plane_surface(loop)
out = g.model.transforms.extrude(surf, 0, 0, 3.0, num_elements=[10])
# out[0] = (2, top_face), out[1] = (3, volume), ...
Source code in src/apeGmsh/core/_model_transforms.py
| def extrude(
self,
tags: EntityRefs,
dx: float, dy: float, dz: float,
*,
dim : int = 2,
num_elements : list[int] | None = None,
heights : list[float] | None = None,
recombine : bool = False,
sync : bool = True,
) -> list[DimTag]:
"""
Linear extrusion \u2014 sweeps entities along (dx, dy, dz).
Creates new geometry one dimension up: point -> curve,
curve -> surface, surface -> volume.
Parameters
----------
tags : entities to extrude.
dx, dy, dz : extrusion vector.
dim : default dimension for bare integer tags (default 2).
num_elements : structured layer counts, e.g. ``[10]`` for
10 layers. Empty list (default) = unstructured.
heights : relative heights per layer, e.g. ``[0.3, 0.7]``.
Must sum to 1.0 when provided. Empty = uniform layers.
recombine : if True, produce hex/quad elements instead of
tet/tri (requires structured layers).
sync : synchronise OCC kernel after extrusion (default True).
Returns
-------
list[DimTag]
All generated (dim, tag) pairs. For a surface -> volume
extrusion the list contains the top face, the volume, and
the lateral faces \u2014 index into it to assign physical groups.
Example
-------
::
surf = g.model.geometry.add_plane_surface(loop)
out = g.model.transforms.extrude(surf, 0, 0, 3.0, num_elements=[10])
# out[0] = (2, top_face), out[1] = (3, volume), ...
"""
dt = self._resolve_dt(tags, dim)
ne = num_elements if num_elements is not None else []
ht = heights if heights is not None else []
with pg_preserved_identity():
result: list[tuple[int, int]] = gmsh.model.occ.extrude(
dt, dx, dy, dz,
numElements=ne,
heights=ht,
recombine=recombine,
)
if sync:
gmsh.model.occ.synchronize()
for d, t in result:
self._model._register(d, t, None, 'extrude')
self._model._log(
f"extrude({dt}, ({dx},{dy},{dz})) -> {len(result)} entities"
)
return result
|
revolve(tags: EntityRefs, angle: float, *, x: float = 0.0, y: float = 0.0, z: float = 0.0, ax: float = 0.0, ay: float = 0.0, az: float = 1.0, dim: int = 2, num_elements: list[int] | None = None, heights: list[float] | None = None, recombine: bool = False, sync: bool = True) -> list[DimTag]
Revolution — sweeps entities around an axis.
tags : entities to revolve.
angle : sweep angle in radians (2π for full revolution).
x, y, z : point on the rotation axis.
ax, ay, az : direction vector of the rotation axis.
dim : default dimension for bare integer tags (default 2).
num_elements, heights, recombine : same as :meth:extrude.
sync : synchronise OCC kernel (default True).
list[DimTag]
All generated (dim, tag) pairs.
::
# Revolve a cross-section 360° around the Y axis
out = g.model.transforms.revolve(profile, 2 * math.pi, ay=1)
Source code in src/apeGmsh/core/_model_transforms.py
| def revolve(
self,
tags : EntityRefs,
angle : float,
*,
x : float = 0.0, y : float = 0.0, z : float = 0.0,
ax: float = 0.0, ay: float = 0.0, az: float = 1.0,
dim : int = 2,
num_elements : list[int] | None = None,
heights : list[float] | None = None,
recombine : bool = False,
sync : bool = True,
) -> list[DimTag]:
"""
Revolution \u2014 sweeps entities around an axis.
Parameters
----------
tags : entities to revolve.
angle : sweep angle in radians (2\u03c0 for full revolution).
x, y, z : point on the rotation axis.
ax, ay, az : direction vector of the rotation axis.
dim : default dimension for bare integer tags (default 2).
num_elements, heights, recombine : same as :meth:`extrude`.
sync : synchronise OCC kernel (default True).
Returns
-------
list[DimTag]
All generated (dim, tag) pairs.
Example
-------
::
# Revolve a cross-section 360\u00b0 around the Y axis
out = g.model.transforms.revolve(profile, 2 * math.pi, ay=1)
"""
dt = self._resolve_dt(tags, dim)
ne = num_elements if num_elements is not None else []
ht = heights if heights is not None else []
with pg_preserved_identity():
result: list[tuple[int, int]] = gmsh.model.occ.revolve(
dt, x, y, z, ax, ay, az, angle,
numElements=ne,
heights=ht,
recombine=recombine,
)
if sync:
gmsh.model.occ.synchronize()
for d, t in result:
self._model._register(d, t, None, 'revolve')
self._model._log(
f"revolve({dt}, angle={math.degrees(angle):.1f}\u00b0, "
f"axis=({ax},{ay},{az}) through ({x},{y},{z})) "
f"-> {len(result)} entities"
)
return result
|
sweep(profiles: EntityRefs, path: Tag, *, dim: int = 2, trihedron: str = 'DiscreteTrihedron', label: str | None = None, sync: bool = True) -> list[DimTag]
Sweep one or more profile entities along an arbitrary wire.
This is the "constant-section sweep" operation: a single profile
(point, curve, or surface) is translated along path, generating
geometry one dimension up — point -> curve, curve -> surface,
surface -> volume. Unlike :meth:extrude the path does not have
to be a straight line: it can be any OCC wire built from lines,
arcs, splines, or a mix, assembled via
:meth:~Model.add_wire.
profiles : entity or entities to sweep. For a solid you
normally pass a plane surface.
path : tag of an OCC wire to sweep along (use
:meth:~Model.add_wire to build it). A curve_loop can be
used for closed paths.
dim : default dimension for bare integer tags in profiles
(default 2).
trihedron : how the profile frame is transported along the
path. One of "DiscreteTrihedron" (default),
"CorrectedFrenet", "Fixed", "Frenet",
"ConstantNormal", "Darboux", "GuideAC",
"GuidePlan", "GuideACWithContact",
"GuidePlanWithContact". Most structural workflows
want the default; use "Frenet" for smooth curves
without inflection and "Fixed" to keep the profile's
orientation constant in world space.
label : optional label applied to the highest-dimension
survivor of the sweep (the volume for a surface sweep).
sync : synchronise the OCC kernel after the call (default
True).
list[DimTag]
All generated (dim, tag) pairs. Index into the list
to grab the volume, the lateral faces, or the end caps and
assign them to physical groups.
::
section = g.model.geometry.add_plane_surface(loop, label="I_section")
path = g.model.geometry.add_wire([arc1, line1, arc2])
out = g.model.transforms.sweep(section, path, label="curved_beam")
Source code in src/apeGmsh/core/_model_transforms.py
| def sweep(
self,
profiles : EntityRefs,
path : Tag,
*,
dim : int = 2,
trihedron : str = "DiscreteTrihedron",
label : str | None = None,
sync : bool = True,
) -> list[DimTag]:
"""
Sweep one or more profile entities along an arbitrary wire.
This is the "constant-section sweep" operation: a single profile
(point, curve, or surface) is translated along *path*, generating
geometry one dimension up — point -> curve, curve -> surface,
surface -> volume. Unlike :meth:`extrude` the path does not have
to be a straight line: it can be any OCC wire built from lines,
arcs, splines, or a mix, assembled via
:meth:`~Model.add_wire`.
Parameters
----------
profiles : entity or entities to sweep. For a solid you
normally pass a plane surface.
path : tag of an OCC wire to sweep along (use
:meth:`~Model.add_wire` to build it). A curve_loop can be
used for closed paths.
dim : default dimension for bare integer tags in *profiles*
(default 2).
trihedron : how the profile frame is transported along the
path. One of ``"DiscreteTrihedron"`` (default),
``"CorrectedFrenet"``, ``"Fixed"``, ``"Frenet"``,
``"ConstantNormal"``, ``"Darboux"``, ``"GuideAC"``,
``"GuidePlan"``, ``"GuideACWithContact"``,
``"GuidePlanWithContact"``. Most structural workflows
want the default; use ``"Frenet"`` for smooth curves
without inflection and ``"Fixed"`` to keep the profile's
orientation constant in world space.
label : optional label applied to the highest-dimension
survivor of the sweep (the volume for a surface sweep).
sync : synchronise the OCC kernel after the call (default
True).
Returns
-------
list[DimTag]
All generated ``(dim, tag)`` pairs. Index into the list
to grab the volume, the lateral faces, or the end caps and
assign them to physical groups.
Example
-------
::
section = g.model.geometry.add_plane_surface(loop, label="I_section")
path = g.model.geometry.add_wire([arc1, line1, arc2])
out = g.model.transforms.sweep(section, path, label="curved_beam")
"""
dt = self._resolve_dt(profiles, dim)
with pg_preserved_identity():
result: list[tuple[int, int]] = gmsh.model.occ.addPipe(
dt, int(path), trihedron=trihedron,
)
if sync:
gmsh.model.occ.synchronize()
if result and label is not None:
max_dim = max(d for d, _ in result)
for d, t in result:
lbl = label if d == max_dim else None
self._model._register(d, t, lbl, 'sweep')
else:
for d, t in result:
self._model._register(d, t, None, 'sweep')
self._model._log(
f"sweep({dt}, path={path}, trihedron={trihedron!r}) "
f"-> {len(result)} entities"
)
return result
|
thru_sections(wires: list[Tag], *, make_solid: bool = True, make_ruled: bool = False, max_degree: int = -1, continuity: str = '', parametrization: str = '', smoothing: bool = False, label: str | None = None, sync: bool = True) -> list[DimTag]
Variable-section sweep — loft a volume (or surface shell)
through an ordered list of wires.
This is the right operation when the cross-section changes
along the sweep: a tapered column, a transition piece between
two different flange shapes, a blended nozzle. Each wire
defines one intermediate section; OCC builds a smooth surface
that interpolates between them and (optionally) caps the ends
to produce a solid.
All wires should be topologically similar (same number of
sub-curves in the same order) for reliable lofting. Open wires
produce a skin; closed wires with make_solid=True produce a
solid.
wires : ordered list of wire tags (build each one with
:meth:~Model.add_wire). At least two wires are required.
make_solid : if True (default), cap the ends and return a
solid; if False, return only the skinned surface(s).
make_ruled : if True, force the lateral faces to be ruled
surfaces (linear interpolation between adjacent sections).
max_degree : maximum degree of the resulting surface
(-1 = OCC default).
continuity : "C0", "G1", "C1", "G2", "C2",
"C3", or "CN" ("" = OCC default).
parametrization : "ChordLength", "Centripetal", or
"IsoParametric" ("" = OCC default).
smoothing : if True, apply a smoothing pass to the resulting
surface.
label : optional label applied to the highest-dimension
survivor (the volume when make_solid=True).
sync : synchronise the OCC kernel after the call (default
True).
list[DimTag]
All generated (dim, tag) pairs.
::
w_base = g.model.geometry.add_wire([lb1, lb2, lb3, lb4])
w_top = g.model.geometry.add_wire([lt1, lt2, lt3, lt4])
out = g.model.transforms.thru_sections(
[w_base, w_top],
make_solid=True,
label="tapered_column",
)
Source code in src/apeGmsh/core/_model_transforms.py
| def thru_sections(
self,
wires : list[Tag],
*,
make_solid : bool = True,
make_ruled : bool = False,
max_degree : int = -1,
continuity : str = "",
parametrization: str = "",
smoothing : bool = False,
label : str | None = None,
sync : bool = True,
) -> list[DimTag]:
"""
Variable-section sweep — loft a volume (or surface shell)
through an ordered list of wires.
This is the right operation when the cross-section *changes*
along the sweep: a tapered column, a transition piece between
two different flange shapes, a blended nozzle. Each wire
defines one intermediate section; OCC builds a smooth surface
that interpolates between them and (optionally) caps the ends
to produce a solid.
All wires should be topologically similar (same number of
sub-curves in the same order) for reliable lofting. Open wires
produce a skin; closed wires with ``make_solid=True`` produce a
solid.
Parameters
----------
wires : ordered list of wire tags (build each one with
:meth:`~Model.add_wire`). At least two wires are required.
make_solid : if True (default), cap the ends and return a
solid; if False, return only the skinned surface(s).
make_ruled : if True, force the lateral faces to be ruled
surfaces (linear interpolation between adjacent sections).
max_degree : maximum degree of the resulting surface
(``-1`` = OCC default).
continuity : ``"C0"``, ``"G1"``, ``"C1"``, ``"G2"``, ``"C2"``,
``"C3"``, or ``"CN"`` (``""`` = OCC default).
parametrization : ``"ChordLength"``, ``"Centripetal"``, or
``"IsoParametric"`` (``""`` = OCC default).
smoothing : if True, apply a smoothing pass to the resulting
surface.
label : optional label applied to the highest-dimension
survivor (the volume when ``make_solid=True``).
sync : synchronise the OCC kernel after the call (default
True).
Returns
-------
list[DimTag]
All generated ``(dim, tag)`` pairs.
Example
-------
::
w_base = g.model.geometry.add_wire([lb1, lb2, lb3, lb4])
w_top = g.model.geometry.add_wire([lt1, lt2, lt3, lt4])
out = g.model.transforms.thru_sections(
[w_base, w_top],
make_solid=True,
label="tapered_column",
)
"""
if len(wires) < 2:
raise ValueError(
"thru_sections requires at least two wires (got "
f"{len(wires)})."
)
with pg_preserved_identity():
result: list[tuple[int, int]] = gmsh.model.occ.addThruSections(
list(wires),
makeSolid = make_solid,
makeRuled = make_ruled,
maxDegree = max_degree,
continuity = continuity,
parametrization= parametrization,
smoothing = smoothing,
)
if sync:
gmsh.model.occ.synchronize()
if result and label is not None:
max_dim = max(d for d, _ in result)
for d, t in result:
lbl = label if d == max_dim else None
self._model._register(d, t, lbl, 'thru_sections')
else:
for d, t in result:
self._model._register(d, t, None, 'thru_sections')
self._model._log(
f"thru_sections(wires={list(wires)}, solid={make_solid}) "
f"-> {len(result)} entities"
)
return result
|
g.model.io
apeGmsh.core._model_io._IO
IO sub-composite — import/export IGES, STEP, DXF, MSH.
Source code in src/apeGmsh/core/_model_io.py
| def __init__(self, model: "Model") -> None:
self._model = model
|
load_iges
load_iges(file_path: Path | str, *, highest_dim_only: bool = True, sync: bool = True) -> dict[int, list[Tag]]
Import an IGES file into the current model.
All imported entities are registered and their tags are returned so
you can immediately use them in boolean ops or transforms.
Parameters
highest_dim_only : bool
If True (default) only the highest-dimension entities are
returned and registered (volumes for solids, surfaces for
surface models). Set to False to capture every sub-entity
(faces, edges, vertices) as well.
Returns
dict[int, list[Tag]]
{dim: [tag, ...]} indexed by dimension.
Example
::
imported = g.model.io.load_iges("part.iges")
bodies = imported[3] # all imported volume tags
flange = bodies[0] # first imported volume
boss = g.model.geometry.add_cylinder(10, 10, 0, 0, 0, 5, 3)
result = g.model.boolean.fuse(flange, boss)
Source code in src/apeGmsh/core/_model_io.py
| def load_iges(
self,
file_path : Path | str,
*,
highest_dim_only: bool = True,
sync : bool = True,
) -> dict[int, list[Tag]]:
"""
Import an IGES file into the current model.
All imported entities are registered and their tags are returned so
you can immediately use them in boolean ops or transforms.
Parameters
----------
highest_dim_only : bool
If True (default) only the highest-dimension entities are
returned and registered (volumes for solids, surfaces for
surface models). Set to False to capture every sub-entity
(faces, edges, vertices) as well.
Returns
-------
dict[int, list[Tag]]
``{dim: [tag, ...]}`` indexed by dimension.
Example
-------
::
imported = g.model.io.load_iges("part.iges")
bodies = imported[3] # all imported volume tags
flange = bodies[0] # first imported volume
boss = g.model.geometry.add_cylinder(10, 10, 0, 0, 0, 5, 3)
result = g.model.boolean.fuse(flange, boss)
"""
return self._import_shapes(
Path(file_path), 'iges', highest_dim_only, sync
)
|
load_step
load_step(file_path: Path | str, *, highest_dim_only: bool = True, sync: bool = True) -> dict[int, list[Tag]]
Import a STEP file into the current model.
All imported entities are registered and their tags are returned so
you can immediately use them in boolean ops or transforms.
Parameters
highest_dim_only : bool
If True (default) only the highest-dimension entities are
returned and registered. Set to False to include all
sub-entities.
Returns
dict[int, list[Tag]]
{dim: [tag, ...]} indexed by dimension.
Example
::
imported = g.model.io.load_step("assembly.step")
bodies = imported[3]
g.model.transforms.translate(bodies, 0, 0, 50) # lift the whole import
Source code in src/apeGmsh/core/_model_io.py
| def load_step(
self,
file_path : Path | str,
*,
highest_dim_only: bool = True,
sync : bool = True,
) -> dict[int, list[Tag]]:
"""
Import a STEP file into the current model.
All imported entities are registered and their tags are returned so
you can immediately use them in boolean ops or transforms.
Parameters
----------
highest_dim_only : bool
If True (default) only the highest-dimension entities are
returned and registered. Set to False to include all
sub-entities.
Returns
-------
dict[int, list[Tag]]
``{dim: [tag, ...]}`` indexed by dimension.
Example
-------
::
imported = g.model.io.load_step("assembly.step")
bodies = imported[3]
g.model.transforms.translate(bodies, 0, 0, 50) # lift the whole import
"""
return self._import_shapes(
Path(file_path), 'step', highest_dim_only, sync
)
|
heal_shapes
heal_shapes(tags: TagsLike | None = None, *, dim: int = 3, tolerance: float = 1e-08, fix_degenerated: bool = True, fix_small_edges: bool = True, fix_small_faces: bool = True, sew_faces: bool = True, make_solids: bool = True, sync: bool = True) -> _IO
Heal topology issues in imported CAD geometry (STEP / IGES).
Wraps gmsh.model.occ.healShapes which fixes common issues
such as degenerate edges, tiny faces, gaps between faces, and
open shells that should be solids.
Parameters
tags : entities to heal (default: all entities in the model).
dim : default dimension for bare integer tags.
tolerance : healing tolerance (default 1e-8).
fix_degenerated : fix degenerate edges/faces.
fix_small_edges : remove edges smaller than tolerance.
fix_small_faces : remove faces smaller than tolerance.
sew_faces : reconnect open shells at shared edges.
make_solids : close healed shells into solids.
sync : synchronise OCC kernel (default True).
Returns
self — for method chaining.
Example
::
imported = g.model.io.load_step("legacy_part.step")
g.model.io.heal_shapes(tolerance=1e-3)
Source code in src/apeGmsh/core/_model_io.py
| def heal_shapes(
self,
tags: TagsLike | None = None,
*,
dim : int = 3,
tolerance : float = 1e-8,
fix_degenerated : bool = True,
fix_small_edges : bool = True,
fix_small_faces : bool = True,
sew_faces : bool = True,
make_solids : bool = True,
sync : bool = True,
) -> _IO:
"""
Heal topology issues in imported CAD geometry (STEP / IGES).
Wraps ``gmsh.model.occ.healShapes`` which fixes common issues
such as degenerate edges, tiny faces, gaps between faces, and
open shells that should be solids.
Parameters
----------
tags : entities to heal (default: all entities in the model).
dim : default dimension for bare integer tags.
tolerance : healing tolerance (default 1e-8).
fix_degenerated : fix degenerate edges/faces.
fix_small_edges : remove edges smaller than tolerance.
fix_small_faces : remove faces smaller than tolerance.
sew_faces : reconnect open shells at shared edges.
make_solids : close healed shells into solids.
sync : synchronise OCC kernel (default True).
Returns
-------
self — for method chaining.
Example
-------
::
imported = g.model.io.load_step("legacy_part.step")
g.model.io.heal_shapes(tolerance=1e-3)
"""
if tags is not None:
dt = self._model._as_dimtags(tags, dim)
else:
dt = [] # empty = heal everything
out: list[tuple[int, int]] = gmsh.model.occ.healShapes(
dimTags=dt,
tolerance=tolerance,
fixDegenerated=fix_degenerated,
fixSmallEdges=fix_small_edges,
fixSmallFaces=fix_small_faces,
sewFaces=sew_faces,
makeSolids=make_solids,
)
if sync:
gmsh.model.occ.synchronize()
for d, t in out:
if (d, t) not in self._model._metadata:
self._model._register(d, t, None, 'healed')
self._model._log(
f"heal_shapes(tol={tolerance}) -> {len(out)} entities output"
)
return self
|
save_iges
save_iges(file_path: Path | str) -> None
Export the current model to IGES.
The .iges extension is appended automatically if omitted.
Source code in src/apeGmsh/core/_model_io.py
| def save_iges(self, file_path: Path | str) -> None:
"""
Export the current model to IGES.
The ``.iges`` extension is appended automatically if omitted.
"""
file_path = Path(file_path).with_suffix('.iges')
gmsh.write(str(file_path))
self._model._log(f"saved IGES -> {file_path}")
|
save_step
save_step(file_path: Path | str) -> None
Export the current model to STEP.
The .step extension is appended automatically if omitted.
Source code in src/apeGmsh/core/_model_io.py
| def save_step(self, file_path: Path | str) -> None:
"""
Export the current model to STEP.
The ``.step`` extension is appended automatically if omitted.
"""
file_path = Path(file_path).with_suffix('.step')
gmsh.write(str(file_path))
self._model._log(f"saved STEP -> {file_path}")
|
load_dxf
load_dxf(file_path: Path | str, *, point_tolerance: float = 1e-06, create_physical_groups: bool = True, sync: bool = True) -> dict[str, dict[int, list[Tag]]]
Import a DXF file into the current model.
Uses ezdxf to parse the DXF (supports all AutoCAD versions
from R12 to 2024+), then builds Gmsh geometry through the OCC
kernel. AutoCAD layers become Gmsh physical groups
automatically.
Supported DXF entity types: LINE, ARC, CIRCLE,
LWPOLYLINE, POLYLINE, SPLINE, POINT.
Parameters
file_path : Path or str
Path to the .dxf file.
point_tolerance : float
Distance below which two DXF endpoints are considered
coincident and share a single Gmsh point. Default 1e-6.
create_physical_groups : bool
If True (default), a physical group is created for each DXF
layer. If False, entities are created but no physical groups
are made (useful when you want to assign groups manually).
sync : bool
Synchronise the OCC kernel after import (default True).
Returns
dict[str, dict[int, list[Tag]]]
{layer_name: {dim: [tag, ...]}}
Each key is a DXF layer name. Values map entity dimension
to lists of Gmsh tags created from that layer.
Example
::
# AutoCAD drawing with layers: "C80x80", "V30x50"
layers = g.model.io.load_dxf("frame_2D.dxf")
# layers == {
# "C80x80": {1: [1, 2, 3, 4]},
# "V30x50": {1: [5, 6, 7, 8, 9]},
# }
# Physical groups are already created — ready for meshing.
# Access beam curves:
beam_curves = layers["V30x50"][1]
Source code in src/apeGmsh/core/_model_io.py
| def load_dxf(
self,
file_path: Path | str,
*,
point_tolerance: float = 1e-6,
create_physical_groups: bool = True,
sync: bool = True,
) -> dict[str, dict[int, list[Tag]]]:
"""
Import a DXF file into the current model.
Uses ``ezdxf`` to parse the DXF (supports all AutoCAD versions
from R12 to 2024+), then builds Gmsh geometry through the OCC
kernel. AutoCAD **layers** become Gmsh physical groups
automatically.
Supported DXF entity types: ``LINE``, ``ARC``, ``CIRCLE``,
``LWPOLYLINE``, ``POLYLINE``, ``SPLINE``, ``POINT``.
Parameters
----------
file_path : Path or str
Path to the ``.dxf`` file.
point_tolerance : float
Distance below which two DXF endpoints are considered
coincident and share a single Gmsh point. Default ``1e-6``.
create_physical_groups : bool
If True (default), a physical group is created for each DXF
layer. If False, entities are created but no physical groups
are made (useful when you want to assign groups manually).
sync : bool
Synchronise the OCC kernel after import (default True).
Returns
-------
dict[str, dict[int, list[Tag]]]
``{layer_name: {dim: [tag, ...]}}``
Each key is a DXF layer name. Values map entity dimension
to lists of Gmsh tags created from that layer.
Example
-------
::
# AutoCAD drawing with layers: "C80x80", "V30x50"
layers = g.model.io.load_dxf("frame_2D.dxf")
# layers == {
# "C80x80": {1: [1, 2, 3, 4]},
# "V30x50": {1: [5, 6, 7, 8, 9]},
# }
# Physical groups are already created — ready for meshing.
# Access beam curves:
beam_curves = layers["V30x50"][1]
"""
importer = _DXFImporter(self._model, point_tolerance)
return importer.run(Path(file_path), create_physical_groups, sync)
|
save_dxf
save_dxf(file_path: Path | str) -> None
Export the current model to DXF.
The .dxf extension is appended automatically if omitted.
Source code in src/apeGmsh/core/_model_io.py
| def save_dxf(self, file_path: Path | str) -> None:
"""
Export the current model to DXF.
The ``.dxf`` extension is appended automatically if omitted.
"""
file_path = Path(file_path).with_suffix('.dxf')
gmsh.write(str(file_path))
self._model._log(f"saved DXF -> {file_path}")
|
save_msh
save_msh(file_path: Path | str) -> None
Export the current model to Gmsh's native MSH format.
Unlike STEP/IGES, this preserves everything: geometry, mesh,
physical groups, and partition data.
The .msh extension is appended automatically if omitted.
Source code in src/apeGmsh/core/_model_io.py
| def save_msh(self, file_path: Path | str) -> None:
"""
Export the current model to Gmsh's native MSH format.
Unlike STEP/IGES, this preserves **everything**: geometry, mesh,
physical groups, and partition data.
The ``.msh`` extension is appended automatically if omitted.
"""
file_path = Path(file_path).with_suffix('.msh')
gmsh.option.setNumber("Mesh.SaveAll", 1)
gmsh.write(str(file_path))
self._model._log(f"saved MSH -> {file_path}")
|
load_msh
load_msh(file_path: Path | str) -> dict[int, list[Tag]]
Import a Gmsh .msh file using gmsh.merge.
Unlike load_iges / load_step, this preserves physical
groups, mesh data, and partition info — because .msh is
Gmsh's native format.
Parameters
file_path : Path or str
Path to the .msh file.
Returns
dict[int, list[Tag]]
{dim: [tag, ...]} of all entities after merge.
Source code in src/apeGmsh/core/_model_io.py
| def load_msh(
self,
file_path: Path | str,
) -> dict[int, list[Tag]]:
"""
Import a Gmsh ``.msh`` file using ``gmsh.merge``.
Unlike ``load_iges`` / ``load_step``, this preserves physical
groups, mesh data, and partition info — because ``.msh`` is
Gmsh's native format.
Parameters
----------
file_path : Path or str
Path to the ``.msh`` file.
Returns
-------
dict[int, list[Tag]]
``{dim: [tag, ...]}`` of all entities after merge.
"""
file_path = Path(file_path)
if not file_path.exists():
raise FileNotFoundError(f"MSH file not found: {file_path}")
gmsh.merge(str(file_path))
result: dict[int, list[Tag]] = {}
for d in range(4):
for dim, tag in gmsh.model.getEntities(d):
result.setdefault(dim, []).append(tag)
dim_summary = {d: len(ts) for d, ts in result.items()}
self._model._log(f"loaded MSH <- {file_path.name} {dim_summary}")
return result
|
load_geo
load_geo(file_path: Path | str) -> dict[int, list[Tag]]
Import a Gmsh .geo script using gmsh.merge.
The script is executed in the active model, so any Mesh N;
statements inside the file will run. The CAD kernel used for
synchronization is auto-detected by scanning the file head for
SetFactory("OpenCASCADE"):
- found ->
gmsh.model.occ.synchronize()
- absent ->
gmsh.model.geo.synchronize()
Parameters
file_path : Path or str
Path to the .geo file.
Returns
dict[int, list[Tag]]
{dim: [tag, ...]} of all entities after merge.
Source code in src/apeGmsh/core/_model_io.py
| def load_geo(
self,
file_path: Path | str,
) -> dict[int, list[Tag]]:
"""
Import a Gmsh ``.geo`` script using ``gmsh.merge``.
The script is executed in the active model, so any ``Mesh N;``
statements inside the file will run. The CAD kernel used for
synchronization is auto-detected by scanning the file head for
``SetFactory("OpenCASCADE")``:
- found -> ``gmsh.model.occ.synchronize()``
- absent -> ``gmsh.model.geo.synchronize()``
Parameters
----------
file_path : Path or str
Path to the ``.geo`` file.
Returns
-------
dict[int, list[Tag]]
``{dim: [tag, ...]}`` of all entities after merge.
"""
file_path = Path(file_path)
if not file_path.exists():
raise FileNotFoundError(f"GEO file not found: {file_path}")
head = file_path.read_text(encoding="utf-8", errors="ignore")[:4096]
use_occ = "SetFactory(\"OpenCASCADE\")" in head
gmsh.merge(str(file_path))
if use_occ:
gmsh.model.occ.synchronize()
kernel = "occ"
else:
gmsh.model.geo.synchronize()
kernel = "geo"
result: dict[int, list[Tag]] = {}
for d in range(4):
for dim, tag in gmsh.model.getEntities(d):
result.setdefault(dim, []).append(tag)
dim_summary = {d: len(ts) for d, ts in result.items()}
self._model._log(
f"loaded GEO <- {file_path.name} [{kernel}] {dim_summary}"
)
return result
|
g.model.queries
apeGmsh.core._model_queries._Queries
Queries sub-composite — remove, topology queries, and registry.
Source code in src/apeGmsh/core/_model_queries.py
| def __init__(self, model: "Model") -> None:
self._model = model
|
remove
remove(tags: TagsLike, *, dim: int = 3, recursive: bool = False, sync: bool = True) -> None
Delete entities from the model.
Parameters
recursive : bool
If True, also delete all lower-dimensional entities that are
exclusively owned by these entities.
Source code in src/apeGmsh/core/_model_queries.py
| def remove(
self,
tags : TagsLike,
*,
dim : int = 3,
recursive: bool = False,
sync : bool = True,
) -> None:
"""
Delete entities from the model.
Parameters
----------
recursive : bool
If True, also delete all lower-dimensional entities that are
exclusively owned by these entities.
"""
dim_tags = self._model._as_dimtags(tags, dim)
gmsh.model.occ.remove(dim_tags, recursive=recursive)
if sync:
gmsh.model.occ.synchronize()
for dt in dim_tags:
self._model._metadata.pop(dt, None)
cleanup_label_pgs(dim_tags)
self._model._log(f"removed {dim_tags} (recursive={recursive})")
|
remove_duplicates
remove_duplicates(*, tolerance: float | None = None, sync: bool = True) -> _Queries
Merge all coincident OCC entities in the current model.
Calls gmsh.model.occ.removeAllDuplicates(), which walks every
dimension (points -> curves -> surfaces -> volumes) and collapses
entities that are geometrically identical within the OCC tolerance.
The internal registry is then reconciled so only entities that
survive the merge are tracked.
This is the recommended post-processing step after importing IGES
or STEP files, which routinely produce coincident points and
overlapping curves at shared frame joints.
Parameters
tolerance : float | None
Geometric merge tolerance. When provided, temporarily overrides
Geometry.Tolerance and Geometry.ToleranceBoolean for the
duration of this call, then restores the previous values.
Use this when the IGES exporter introduced small coordinate
imprecisions (e.g. tolerance=1e-3 for mm-scale models).
When None (default), the current Gmsh tolerance is used unchanged.
sync : bool
Synchronise the OCC kernel after merging (default True).
Set to False only if you intend to call model.sync()
manually as part of a larger batch operation.
Returns
self — for method chaining
Example
::
imported = g.model.io.load_iges("Frame3D.iges", highest_dim_only=False)
g.model.queries.remove_duplicates(tolerance=1e-3)
g.plot.geometry(label_tags=True)
Source code in src/apeGmsh/core/_model_queries.py
| def remove_duplicates(
self,
*,
tolerance: float | None = None,
sync : bool = True,
) -> _Queries:
"""
Merge all coincident OCC entities in the current model.
Calls ``gmsh.model.occ.removeAllDuplicates()``, which walks every
dimension (points -> curves -> surfaces -> volumes) and collapses
entities that are geometrically identical within the OCC tolerance.
The internal registry is then reconciled so only entities that
survive the merge are tracked.
This is the recommended post-processing step after importing IGES
or STEP files, which routinely produce coincident points and
overlapping curves at shared frame joints.
Parameters
----------
tolerance : float | None
Geometric merge tolerance. When provided, temporarily overrides
``Geometry.Tolerance`` and ``Geometry.ToleranceBoolean`` for the
duration of this call, then restores the previous values.
Use this when the IGES exporter introduced small coordinate
imprecisions (e.g. ``tolerance=1e-3`` for mm-scale models).
When None (default), the current Gmsh tolerance is used unchanged.
sync : bool
Synchronise the OCC kernel after merging (default True).
Set to False only if you intend to call ``model.sync()``
manually as part of a larger batch operation.
Returns
-------
self — for method chaining
Example
-------
::
imported = g.model.io.load_iges("Frame3D.iges", highest_dim_only=False)
g.model.queries.remove_duplicates(tolerance=1e-3)
g.plot.geometry(label_tags=True)
"""
before = {d: len(gmsh.model.getEntities(d)) for d in range(4)}
with _temporary_tolerance(tolerance):
gmsh.model.occ.removeAllDuplicates()
if sync:
gmsh.model.occ.synchronize()
# Reconcile metadata — drop any (dim, tag) pairs that no longer
# exist in the gmsh model after the merge.
surviving: set[tuple[int, int]] = {
(dim, tag)
for dim in range(4)
for _, tag in gmsh.model.getEntities(dim)
}
stale_dts = [dt for dt in self._model._metadata if dt not in surviving]
for dt in stale_dts:
del self._model._metadata[dt]
# Reconcile label PGs — removeAllDuplicates doesn't provide a
# result_map, so we walk all label PGs and drop dead entity tags.
reconcile_label_pgs()
after = {d: len(gmsh.model.getEntities(d)) for d in range(4)}
removed = {d: before[d] - after[d] for d in range(4) if before[d] != after[d]}
tol_str = f"tolerance={tolerance}" if tolerance is not None else ""
self._model._log(
f"remove_duplicates({tol_str}): merged {removed} entities "
f"(before={before}, after={after})"
)
return self
|
make_conformal(*, dims: list[int] | None = None, tolerance: float | None = None, sync: bool = True) -> _Queries
Fragment all entities against each other to produce a conformal model.
IGES/STEP files exported from CAD tools often create topologically
disconnected entities at shared joints — for example, column endpoints
and beam endpoints that are coincident in space but belong to separate
BRep objects with no shared vertex. A conformal model is required for
FEM meshing because elements must share nodes at junctions rather than
having two independent nodes at the same location.
This method calls gmsh.model.occ.fragment() with all entities of
the requested dimensions as both objects and tools. OCC computes all
intersections, splits curves at shared points, and merges coincident
vertices — leaving a single connected topology.
dims : list[int] | None
Dimensions to fragment. Defaults to all non-empty dimensions
present in the model (typically [1] for wireframe frames,
[1, 2] for mixed models). Pass [1] explicitly to
restrict to curves only and avoid fragmenting surfaces.
tolerance : float | None
Geometric tolerance for OCC's intersection / coincidence detection.
Temporarily overrides Geometry.ToleranceBoolean for the duration
of the fragment call, then restores the original value.
Use this when curves only touch at endpoints (no proper crossing)
and the default OCC tolerance is too tight to detect them —
e.g. tolerance=1.0 for mm-scale models.
When None (default), the current Gmsh tolerance is used unchanged.
sync : bool
Synchronise the OCC kernel after fragmenting (default True).
self — for method chaining
::
m1.model.io.load_iges("Frame3D.iges", highest_dim_only=False)
m1.remove_duplicates(tolerance=1.0)
m1.model.queries.make_conformal(dims=[1], tolerance=1.0)
m1.plot.geometry(label_tags=True)
Source code in src/apeGmsh/core/_model_queries.py
| def make_conformal(
self,
*,
dims : list[int] | None = None,
tolerance: float | None = None,
sync : bool = True,
) -> _Queries:
"""
Fragment all entities against each other to produce a conformal model.
IGES/STEP files exported from CAD tools often create topologically
disconnected entities at shared joints — for example, column endpoints
and beam endpoints that are coincident in space but belong to separate
BRep objects with no shared vertex. A conformal model is required for
FEM meshing because elements must share nodes at junctions rather than
having two independent nodes at the same location.
This method calls ``gmsh.model.occ.fragment()`` with all entities of
the requested dimensions as both objects and tools. OCC computes all
intersections, splits curves at shared points, and merges coincident
vertices — leaving a single connected topology.
Parameters
----------
dims : list[int] | None
Dimensions to fragment. Defaults to all non-empty dimensions
present in the model (typically ``[1]`` for wireframe frames,
``[1, 2]`` for mixed models). Pass ``[1]`` explicitly to
restrict to curves only and avoid fragmenting surfaces.
tolerance : float | None
Geometric tolerance for OCC's intersection / coincidence detection.
Temporarily overrides ``Geometry.ToleranceBoolean`` for the duration
of the fragment call, then restores the original value.
Use this when curves only touch at endpoints (no proper crossing)
and the default OCC tolerance is too tight to detect them —
e.g. ``tolerance=1.0`` for mm-scale models.
When None (default), the current Gmsh tolerance is used unchanged.
sync : bool
Synchronise the OCC kernel after fragmenting (default True).
Returns
-------
self — for method chaining
Example
-------
::
m1.model.io.load_iges("Frame3D.iges", highest_dim_only=False)
m1.remove_duplicates(tolerance=1.0)
m1.model.queries.make_conformal(dims=[1], tolerance=1.0)
m1.plot.geometry(label_tags=True)
"""
before = {d: len(gmsh.model.getEntities(d)) for d in range(4)}
if dims is None:
dims = [d for d in range(4) if gmsh.model.getEntities(d)]
all_dimtags: list[tuple[int, int]] = [
(d, tag)
for d in dims
for _, tag in gmsh.model.getEntities(d)
]
if not all_dimtags:
self._model._log("make_conformal(): no entities found, nothing to do")
return self
with pg_preserved() as pg, \
_temporary_tolerance(tolerance, keys=("Geometry.ToleranceBoolean",)):
_, result_map = gmsh.model.occ.fragment(
all_dimtags, [], removeObject=True, removeTool=True,
)
if sync:
gmsh.model.occ.synchronize()
pg.set_result(all_dimtags, result_map)
# Rebuild metadata from scratch — fragment renumbers entities.
# Only kind/normal/point metadata is preserved; labels live
# in g.labels (Gmsh PGs) and are remapped by remap_physical_groups.
old_metadata = dict(self._model._metadata)
self._model._metadata.clear()
for d in range(4):
for _, tag in gmsh.model.getEntities(d):
old_entry = old_metadata.get((d, tag))
if old_entry:
self._model._metadata[(d, tag)] = old_entry
else:
self._model._metadata[(d, tag)] = {'kind': 'fragment'}
# Remap Instance.entities if the session has a parts registry.
# Without this, instances track stale tags after make_conformal.
parts = getattr(self._model._parent, 'parts', None)
if parts is not None:
# Build (dim, old_tag) → [new_tags at same dim]
dt_remap: dict[tuple[int, int], list[int]] = {}
for old_dt, new_dts in zip(all_dimtags, result_map):
od, ot = int(old_dt[0]), int(old_dt[1])
dt_remap[(od, ot)] = [int(t) for d, t in new_dts if int(d) == od]
for inst in parts._instances.values():
for d in list(inst.entities.keys()):
old_tags = inst.entities.get(d, [])
new_tags: list[int] = []
for ot in old_tags:
mapped = dt_remap.get((d, ot))
if mapped is not None:
new_tags.extend(mapped)
else:
new_tags.append(ot)
inst.entities[d] = new_tags
after = {d: len(gmsh.model.getEntities(d)) for d in range(4)}
delta = {d: after[d] - before[d] for d in range(4) if before[d] != after[d]}
tol_str = f", tolerance={tolerance}" if tolerance is not None else ""
self._model._log(
f"make_conformal(dims={dims}{tol_str}): entity delta={delta} "
f"(before={before}, after={after})"
)
return self
|
bounding_box
bounding_box(tag, *, dim: int = 3) -> tuple[float, float, float, float, float, float]
Return the axis-aligned bounding box of an entity.
tag accepts an int, a label, a PG name, or a (dim, tag)
tuple. Must resolve to exactly one entity. When tag is a
bare int, dim is honoured as an explicit dimension hint
(no live-model lookup) — important because Gmsh tag spaces are
per-dimension, so the same int can refer to different entities
at different dims.
Returns
(xmin, ymin, zmin, xmax, ymax, zmax)
Example
xmin, ymin, zmin, xmax, ymax, zmax = g.model.queries.bounding_box("box")
Source code in src/apeGmsh/core/_model_queries.py
| def bounding_box(
self,
tag,
*,
dim: int = 3,
) -> tuple[float, float, float, float, float, float]:
"""
Return the axis-aligned bounding box of an entity.
``tag`` accepts an int, a label, a PG name, or a ``(dim, tag)``
tuple. Must resolve to exactly one entity. When ``tag`` is a
bare int, ``dim`` is honoured as an explicit dimension hint
(no live-model lookup) — important because Gmsh tag spaces are
per-dimension, so the same int can refer to different entities
at different dims.
Returns
-------
(xmin, ymin, zmin, xmax, ymax, zmax)
Example
-------
``xmin, ymin, zmin, xmax, ymax, zmax = g.model.queries.bounding_box("box")``
"""
if isinstance(tag, int) and not isinstance(tag, bool):
return gmsh.model.getBoundingBox(dim, tag)
from ._helpers import resolve_to_single_dimtag
d, t = resolve_to_single_dimtag(
tag, default_dim=dim, session=self._model._parent,
what="bounding_box target",
)
return gmsh.model.getBoundingBox(d, t)
|
center_of_mass
center_of_mass(tag, *, dim: int = 3) -> tuple[float, float, float]
Return the center of mass of an entity.
tag accepts an int, a label, a PG name, or a (dim, tag)
tuple. Must resolve to exactly one entity. Bare ints are
interpreted at dim directly (no live-model lookup).
Example
cx, cy, cz = g.model.queries.center_of_mass("box")
Source code in src/apeGmsh/core/_model_queries.py
| def center_of_mass(
self,
tag,
*,
dim: int = 3,
) -> tuple[float, float, float]:
"""
Return the center of mass of an entity.
``tag`` accepts an int, a label, a PG name, or a ``(dim, tag)``
tuple. Must resolve to exactly one entity. Bare ints are
interpreted at ``dim`` directly (no live-model lookup).
Example
-------
``cx, cy, cz = g.model.queries.center_of_mass("box")``
"""
if isinstance(tag, int) and not isinstance(tag, bool):
return gmsh.model.occ.getCenterOfMass(dim, tag)
from ._helpers import resolve_to_single_dimtag
d, t = resolve_to_single_dimtag(
tag, default_dim=dim, session=self._model._parent,
what="center_of_mass target",
)
return gmsh.model.occ.getCenterOfMass(d, t)
|
mass
mass(tag, *, dim: int = 3) -> float
Return the mass (volume for 3D, area for 2D, length for 1D)
of an entity.
tag accepts an int, a label, a PG name, or a (dim, tag)
tuple. Must resolve to exactly one entity. Bare ints are
interpreted at dim directly (no live-model lookup).
Example
vol = g.model.queries.mass("box")
Source code in src/apeGmsh/core/_model_queries.py
| def mass(
self,
tag,
*,
dim: int = 3,
) -> float:
"""
Return the mass (volume for 3D, area for 2D, length for 1D)
of an entity.
``tag`` accepts an int, a label, a PG name, or a ``(dim, tag)``
tuple. Must resolve to exactly one entity. Bare ints are
interpreted at ``dim`` directly (no live-model lookup).
Example
-------
``vol = g.model.queries.mass("box")``
"""
if isinstance(tag, int) and not isinstance(tag, bool):
return gmsh.model.occ.getMass(dim, tag)
from ._helpers import resolve_to_single_dimtag
d, t = resolve_to_single_dimtag(
tag, default_dim=dim, session=self._model._parent,
what="mass target",
)
return gmsh.model.occ.getMass(d, t)
|
boundary
boundary(tags: TagsLike, *, dim: int = 3, oriented: bool = False, combined: bool = True, recursive: bool = False) -> list[DimTag]
Return the boundary entities of the given entities.
Parameters
tags : int, label, PG name, (dim, tag), or list thereof.
Strings are resolved as label first (Tier 1, g.labels),
then user physical-group name (Tier 2, g.physical).
dim : default dimension for bare integer tags or string refs.
oriented : if True, return oriented boundary (signs on tags).
combined : if True, return the boundary of the combined entities.
recursive : if True, recurse down to dimension 0.
Returns
list[DimTag]
Boundary entities as (dim, tag) pairs.
Example
::
faces = g.model.queries.boundary(vol_tag) # by tag
edges = g.model.queries.boundary("Plate", dim=2) # by label
Source code in src/apeGmsh/core/_model_queries.py
| def boundary(
self,
tags: TagsLike,
*,
dim : int = 3,
oriented : bool = False,
combined : bool = True,
recursive: bool = False,
) -> list[DimTag]:
"""
Return the boundary entities of the given entities.
Parameters
----------
tags : int, label, PG name, ``(dim, tag)``, or list thereof.
Strings are resolved as label first (Tier 1, ``g.labels``),
then user physical-group name (Tier 2, ``g.physical``).
dim : default dimension for bare integer tags or string refs.
oriented : if True, return oriented boundary (signs on tags).
combined : if True, return the boundary of the combined entities.
recursive : if True, recurse down to dimension 0.
Returns
-------
list[DimTag]
Boundary entities as (dim, tag) pairs.
Example
-------
::
faces = g.model.queries.boundary(vol_tag) # by tag
edges = g.model.queries.boundary("Plate", dim=2) # by label
"""
if isinstance(tags, str):
from ._helpers import _resolve_string_to_dimtags
dt = _resolve_string_to_dimtags(
tags, default_dim=dim, session=self._model._parent,
)
elif (
isinstance(tags, list)
and tags
and all(isinstance(t, str) for t in tags)
):
from ._helpers import _resolve_string_to_dimtags
dt = []
for name in tags:
dt.extend(_resolve_string_to_dimtags(
name, default_dim=dim, session=self._model._parent,
))
else:
dt = self._model._as_dimtags(tags, dim)
return gmsh.model.getBoundary(
dt,
combined=combined,
oriented=oriented,
recursive=recursive,
)
|
boundary_curves
boundary_curves(tag) -> list[DimTag]
Return all unique curves (dim = 1) on the boundary of an entity.
Wraps the two-step query needed to get a volume's edges:
boundary(vol) skips straight to vertices when recursive=True,
so the correct pattern is to fetch faces first, then walk each face's
boundary individually with combined=False (so shared edges are not
cancelled), and deduplicate the result.
Parameters
tag : int, label, PG name, (dim, tag) tuple, or list thereof.
Returns
list[DimTag]
(1, curve_tag) pairs, deduplicated.
Example
::
edges = g.model.queries.boundary_curves('box') # 12 edges
edges = g.model.queries.boundary_curves(surf) # 4 edges of a face
Source code in src/apeGmsh/core/_model_queries.py
| def boundary_curves(self, tag) -> list[DimTag]:
"""
Return all unique curves (dim = 1) on the boundary of an entity.
Wraps the two-step query needed to get a volume's edges:
``boundary(vol)`` skips straight to vertices when ``recursive=True``,
so the correct pattern is to fetch faces first, then walk each face's
boundary individually with ``combined=False`` (so shared edges are not
cancelled), and deduplicate the result.
Parameters
----------
tag : int, label, PG name, ``(dim, tag)`` tuple, or list thereof.
Returns
-------
list[DimTag]
``(1, curve_tag)`` pairs, deduplicated.
Example
-------
::
edges = g.model.queries.boundary_curves('box') # 12 edges
edges = g.model.queries.boundary_curves(surf) # 4 edges of a face
"""
owners = self._resolve_to_dimtags(tag)
# If the entities are already curves, return them deduplicated.
if all(d == 1 for d, _ in owners):
return list(dict.fromkeys(owners))
# Surfaces → walk one more level with combined=False to keep shared edges.
if all(d == 2 for d, _ in owners):
return list(dict.fromkeys(
self.boundary(owners, combined=False, oriented=False)
))
# Volumes (or mixed): faces first, then their individual boundaries.
faces = self.boundary(owners, oriented=False)
return list(dict.fromkeys(
self.boundary(faces, combined=False, oriented=False)
))
|
boundary_points
boundary_points(tag) -> list[DimTag]
Return all unique points (dim = 0) on the boundary of an entity.
Equivalent to boundary(tag, recursive=True) for volumes —
Gmsh's recursive walk goes straight to dim=0 — but provided as a
named alias for symmetry with boundary_curves.
Example
::
corners = g.model.queries.boundary_points('box') # 8 corners
Source code in src/apeGmsh/core/_model_queries.py
| def boundary_points(self, tag) -> list[DimTag]:
"""
Return all unique points (dim = 0) on the boundary of an entity.
Equivalent to ``boundary(tag, recursive=True)`` for volumes —
Gmsh's recursive walk goes straight to dim=0 — but provided as a
named alias for symmetry with ``boundary_curves``.
Example
-------
::
corners = g.model.queries.boundary_points('box') # 8 corners
"""
owners = self._resolve_to_dimtags(tag)
return list(dict.fromkeys(
dt for dt in self.boundary(owners, oriented=False, recursive=True)
if dt[0] == 0
))
|
adjacencies
adjacencies(tag: Tag, *, dim: int = 3) -> tuple[list[Tag], list[Tag]]
Return entities adjacent to the given entity.
Returns
(upward, downward)
upward — tags of entities of dim + 1 that contain
this entity.
downward — tags of entities of dim - 1 on this
entity's boundary.
Example
::
up, down = g.model.queries.adjacencies(face_tag, dim=2)
# up = volumes bounded by this face
# down = curves on this face's boundary
Source code in src/apeGmsh/core/_model_queries.py
| def adjacencies(
self,
tag: Tag,
*,
dim: int = 3,
) -> tuple[list[Tag], list[Tag]]:
"""
Return entities adjacent to the given entity.
Returns
-------
(upward, downward)
``upward`` — tags of entities of ``dim + 1`` that contain
this entity.
``downward`` — tags of entities of ``dim - 1`` on this
entity's boundary.
Example
-------
::
up, down = g.model.queries.adjacencies(face_tag, dim=2)
# up = volumes bounded by this face
# down = curves on this face's boundary
"""
d = self._model._resolve_dim(tag, dim)
up, down = gmsh.model.getAdjacencies(d, tag)
return list(up), list(down)
|
entities_in_bounding_box
entities_in_bounding_box(xmin: float, ymin: float, zmin: float, xmax: float, ymax: float, zmax: float, *, dim: int = -1) -> list[DimTag]
Return all entities inside a bounding box.
Parameters
xmin, ymin, zmin, xmax, ymax, zmax : box limits.
dim : restrict to this dimension (-1 = all dimensions).
Returns
list[DimTag]
Example
::
# Find all entities in a region
found = g.model.queries.entities_in_bounding_box(
0, 0, 0, 10, 10, 10, dim=3
)
Source code in src/apeGmsh/core/_model_queries.py
| def entities_in_bounding_box(
self,
xmin: float, ymin: float, zmin: float,
xmax: float, ymax: float, zmax: float,
*,
dim: int = -1,
) -> list[DimTag]:
"""
Return all entities inside a bounding box.
Parameters
----------
xmin, ymin, zmin, xmax, ymax, zmax : box limits.
dim : restrict to this dimension (-1 = all dimensions).
Returns
-------
list[DimTag]
Example
-------
::
# Find all entities in a region
found = g.model.queries.entities_in_bounding_box(
0, 0, 0, 10, 10, 10, dim=3
)
"""
return gmsh.model.getEntitiesInBoundingBox(
xmin, ymin, zmin, xmax, ymax, zmax, dim,
)
|
plane
plane(*args, **kwargs) -> Plane
Construct a :class:Plane for use with
m.model.select(...).crossing_plane(plane, mode=...) (or any
other API that accepts a plane spec).
plane(z=0) / plane(x=5)
Axis-aligned plane.
plane(p1, p2, p3)
Plane through three non-collinear points.
plane(normal=(0, 0, 1), through=(0, 0, 5))
Direct construction from a normal and an anchor point.
Example
::
mid = m.model.queries.plane(z=2.5)
faces_cut = m.model.select(faces, dim=2).crossing_plane(
mid, mode="crossing")
below = m.model.select(faces, dim=2).crossing_plane(
mid, mode="not_crossing")
Source code in src/apeGmsh/core/_model_queries.py
| def plane(self, *args, **kwargs) -> Plane:
"""
Construct a :class:`Plane` for use with
``m.model.select(...).crossing_plane(plane, mode=...)`` (or any
other API that accepts a plane spec).
Forms accepted
--------------
``plane(z=0)`` / ``plane(x=5)``
Axis-aligned plane.
``plane(p1, p2, p3)``
Plane through three non-collinear points.
``plane(normal=(0, 0, 1), through=(0, 0, 5))``
Direct construction from a normal and an anchor point.
Example
-------
::
mid = m.model.queries.plane(z=2.5)
faces_cut = m.model.select(faces, dim=2).crossing_plane(
mid, mode="crossing")
below = m.model.select(faces, dim=2).crossing_plane(
mid, mode="not_crossing")
"""
if args and not kwargs:
if len(args) == 3:
return Plane.through(*args)
raise ValueError(
"plane(*args) needs 3 points; got "
f"{len(args)}. Did you mean plane(z=0)?"
)
if kwargs and not args:
if 'normal' in kwargs and 'through' in kwargs:
import numpy as _np
n = _np.asarray(kwargs['normal'], dtype=float)
n = n / _np.linalg.norm(n)
a = _np.asarray(kwargs['through'], dtype=float)
return Plane(normal=n, anchor=a)
return Plane.at(**kwargs)
raise ValueError(
"plane() takes either an axis kwarg (z=0), 3 positional points, "
"or normal=/through= kwargs."
)
|
registry
registry() -> pd.DataFrame
Return a DataFrame of all entities created through this helper.
Indexed by (dim, tag) — matching Gmsh's identity model where
tags are only unique within a dimension.
Columns: kind, label
The label column is populated from g.labels (the single
source of truth), not from the metadata dict.
Source code in src/apeGmsh/core/_model_queries.py
| def registry(self) -> pd.DataFrame:
"""
Return a DataFrame of all entities created through this helper.
Indexed by ``(dim, tag)`` — matching Gmsh's identity model where
tags are only unique within a dimension.
Columns: ``kind``, ``label``
The ``label`` column is populated from ``g.labels`` (the single
source of truth), not from the metadata dict.
"""
if not self._model._metadata:
return pd.DataFrame(columns=['dim', 'tag', 'kind', 'label'])
# Build label reverse map from g.labels
labels_comp = getattr(self._model._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
rows = []
for (dim, tag), info in self._model._metadata.items():
row = {'dim': dim, 'tag': tag, **info}
row['label'] = label_map.get((dim, tag), '')
rows.append(row)
return (
pd.DataFrame(rows)
.set_index(['dim', 'tag'])
.sort_index()
)
|
Fluent selection — g.model.select()
g.model.select(...) is the single entity-selection surface and
the geometry entry of the unified, daisy-chainable
selection idiom. The former
g.model.queries.select(...) predicate selector and the former
g.model.selection.select_* entity composite have been removed;
their behaviour is folded into the verbs below. select() returns an
EntitySelection (entity
family) with direct terminals .to_label() / .to_physical() /
.to_dataframe(); .result() is a zero-cost identity alias yielding
the Selection payload (retained
by architecture as the entity-side terminal type).
(g.model.select("Faces") # tiered name resolve
.in_box((-0.1, -0.1, -0.1), (1.1, 1.1, 1.1)) # gmsh BRep containment
.on_plane((0, 0, 0), (0, 0, 1), tol=1e-6)
.to_physical("Base"))
Entity-family in_box is gmsh BRep containment and rejects
inclusive= with a TypeError (it is point-family only). See
Selection for the full idiom, the verb surface, and
the point-vs-entity family contract.
Geometric predicates — cheat sheet
The straddle predicates are reached two ways: as the
.crossing_plane(spec, *, mode=) verb on the
EntitySelection chain, or
as the .select(on=/crossing=/not_on=/not_crossing=) refinement method
on the Selection payload (after
.result()):
| Predicate |
Where |
Dim |
Example |
Keeps entities that… |
mode="on" / on= |
.crossing_plane() verb / .select() kwarg |
any |
on={"z": 0} |
lie entirely on the plane |
mode="crossing" / crossing= |
.crossing_plane() verb / .select() kwarg |
any |
crossing={"z": 0} |
straddle the plane |
mode="not_on" / not_on= |
.crossing_plane() verb / .select() kwarg |
any |
not_on={"z": 0} |
are not entirely on the plane |
mode="not_crossing" / not_crossing= |
.crossing_plane() verb / .select() kwarg |
any |
not_crossing={"z": 0} |
lie entirely on one side |
.parallel_to(...) |
method on Selection |
1 (curves) |
edges.parallel_to("z") |
are curves whose chord direction is parallel to it |
.normal_along(...) |
method on Selection |
2 (surfaces) |
faces.normal_along("z") |
are surfaces whose normal is along it |
| Form |
Meaning |
{"z": 0} / {"x": 5} / {"y": -3} |
Axis-aligned plane |
[(x1,y1,z1), (x2,y2,z2)] |
Infinite line through 2 points (for curves in 2-D) |
[(x1,y1,z1), (x2,y2,z2), (x3,y3,z3)] |
Infinite plane through 3 points (for surfaces / volumes) |
m.model.queries.plane(...) |
Plane object — axis-aligned, 3-point, or normal=/through= |
| Form |
Meaning |
"x", "y", "z" |
Axis alias |
(1, 0, 0) / (1, 1, 0) |
Any non-zero 3-vector (normalized internally) |
angle_tol=2.0 |
Tolerance in degrees; default 1.0. Anti-parallel counts as parallel. |
Seeding a selection
| Call |
Returns |
g.model.select(dim=N) |
every entity at dimension N (point=0, curve=1, surface=2, volume=3) |
g.model.select(name_or_dimtags, dim=N) |
by PG / label / part name, or from an explicit (dim, tag) set |
Selection — .result() payload of select()
g.model.select(...) returns an EntitySelection; its .result()
yields a Selection — a chainable list of (dim, tag) pairs. No
import is needed. The Selection payload still carries the position
predicates (.select(on=/crossing=/not_on=/not_crossing=, tol=)),
direction filters, and set-algebra.
curves = m.model.queries.boundary(surf, oriented=False) # -> Selection
# axis-aligned plane
bottom = curves.select(on={'y': 0})
# 2-point line
mid = curves.select(crossing=[(0,5,0),(5,5,0)])
# chain to narrow further
left_bottom = curves.select(on={'y': 0}).select(on={'x': 0})
# extract bare tags for downstream calls
m.mesh.structured.set_transfinite_curve(bottom.tags(), n=11)
Starting from every entity of a dimension
When parsing an imported .geo / STEP file with no labels yet, seed
with g.model.select(dim=N) (no target):
# Every volume in the model
g.model.select(dim=3).to_physical("solids")
# Volumes the plane z = -15 slices through
(g.model.select(dim=3)
.crossing_plane({"z": -15}, mode="crossing")
.to_physical("crossers"))
# The floor (surfaces lying on z = 0)
(g.model.select(dim=2)
.crossing_plane({"z": 0}, mode="on")
.to_physical("base"))
dim=0 points, dim=1 curves, dim=2 surfaces, dim=3 volumes.
Direction-based filters — parallel_to and normal_along
For dim-restricted filtering by direction (not position), the
Selection payload offers two methods:
# Curves: keep only edges whose chord is along a direction
edges = g.model.select("box", dim=1).result()
verticals = edges.parallel_to("z") # axis alias
diagonals = edges.parallel_to((1, 1, 0), angle_tol=2) # arbitrary vector
# Surfaces: keep only faces whose normal is along a direction
faces = g.model.select("box", dim=2).result()
horizontals = faces.normal_along("z")
Both accept axis aliases ("x" / "y" / "z") or any non-zero 3-vector
(normalized internally; anti-parallel counts as parallel). Default
angle_tol is 1.0°. The methods are dim-restricted: parallel_to
raises if the Selection contains non-curve entities, normal_along raises
for non-surface entities — with a fix-it suggestion in the error.
They chain with the position predicates:
# Vertical edges on the x = 0 wall
(g.model.select("box", dim=1).result()
.parallel_to("z")
.select(on={"x": 0}))
Combining selections — |, &, -
Two Selections can be combined with set-algebra operators. Semantics are
set-like with deduplication — a (dim, tag) pair never appears
twice in the result, so downstream calls like to_physical register each
entity once.
Each operation has both an operator form (terse, for one-liners)
and a named-method form (discoverable via autocomplete, keeps the
chain fluent — important when you don't want to break out to a
variable).
| Operator |
Method |
Meaning |
Example |
a \| b |
a.union(b) |
entities in either |
sides = nx.union(ny) |
a & b |
a.intersect(b) |
entities in both |
edge = top.intersect(front) |
a - b |
a.difference(b) |
in a, not in b |
lateral = all.difference(horizontal) |
surf = g.model.select(dim=2).result()
# Three equivalent ways to grab the lateral sides of an axis-aligned box:
(surf.normal_along("x") | surf.normal_along("y")).to_physical("sides")
(surf - surf.normal_along("z")).to_physical("sides")
surf.normal_along("x").union(surf.normal_along("y")).to_physical("sides")
# Intersection — curves shared by two faces (the edge between them)
top_edges = m.model.queries.boundary("top", dim=2, oriented=False)
front_edges = m.model.queries.boundary("front", dim=2, oriented=False)
shared_edge = top_edges.intersect(front_edges)
Why | and not +? Selection subclasses list, where +
already means concatenation with duplicates preserved. The | family
follows Python's set / dict convention for combining-with-dedup,
which is the right semantics for selection sets — combining the xmin
faces with the ymin faces should give each shared corner edge once, not
twice.
Resolve-only select(...) — no predicate required
g.model.select("name", dim=N) with no spatial verb returns
the entities under that name as a chainable selection — useful as an
entry point into the method-style filters:
g.model.select("box", dim=1).result().parallel_to("z").to_physical("verticals")
apeGmsh.core._selection.Selection
Selection(dimtags: Iterable[DimTag] = (), *, _queries: '_Queries | None' = None)
Bases: list
A filtered list of (dim, tag) pairs — the payload yielded by
the .result() terminal of an :class:EntitySelection (itself
returned by m.model.select(...)). Retained by architecture
as the entity-side terminal payload; it is not a legacy or
backward-compat type.
A Selection is a list subclass, so it iterates as (dim, tag)
pairs and supports indexing. It is also chainable — every method
that narrows or combines returns a new Selection.
Refine (narrow what you have)
============================== ==========================================
.select(...) position predicates: on, crossing,
not_on, not_crossing
.parallel_to(direction) curves whose chord is along a direction
.normal_along(direction) surfaces whose normal is along a direction
.partition_by(axis=None) group entities by dominant BB axis
============================== ==========================================
Combine (set algebra on two Selections)
Set semantics with deduplication — appropriate for (dim, tag)
pairs, where logical identity matters and duplicates would cause
downstream calls (e.g. to_physical) to register the same
entity twice.
Each operation has both an operator form (terse, for one-liners)
and a named-method form (discoverable via autocomplete, keeps the
chain fluent).
===================== ===================== ===================== ===========================
Operator Method Meaning Example
===================== ===================== ===================== ===========================
a | b a.union(b) union nx | ny
a & b a.intersect(b) intersection top & front
a - b a.difference(b) set difference all - horizontal
===================== ===================== ===================== ===========================
Why | and not +: Selection subclasses list, where
+ is concatenation with duplicates preserved. | follows
the set / dict convention for combining-with-dedup, and is
the right semantics for selection sets.
Consume (turn a Selection into something else)
================================ ===========================================
.tags() bare integer tags (drops dim)
.to_label(name) register entities as a label
.to_physical(name) register entities as a physical group
================================ ===========================================
Example
::
surf = m.model.select(dim=2).result()
# Lateral sides of an axis-aligned box — three equivalent forms:
(surf.normal_along("x") | surf.normal_along("y")).to_physical("sides")
(surf - surf.normal_along("z")).to_physical("sides")
surf.normal_along("x").union(surf.normal_along("y")).to_physical("sides")
# Chain refine → consume
(m.model.select(curves, dim=1).result()
.select(on={'z': 0})
.select(on={'x': 0})
.to_label("bottom_left_edge"))
Source code in src/apeGmsh/core/_selection.py
| def __init__(self, dimtags: Iterable[DimTag] = (), *,
_queries: "_Queries | None" = None) -> None:
super().__init__(dimtags)
self._queries = _queries
|
select
select(*, on=None, crossing=None, not_on=None, not_crossing=None, tol: float = 1e-06) -> 'Selection'
Filter this selection further by position predicates
(on / crossing / not_on / not_crossing).
Source code in src/apeGmsh/core/_selection.py
| def select(self, *, on=None, crossing=None, not_on=None, not_crossing=None,
tol: float = 1e-6) -> "Selection":
"""Filter this selection further by position predicates
(``on`` / ``crossing`` / ``not_on`` / ``not_crossing``)."""
return _select_impl(self, on=on, crossing=crossing,
not_on=not_on, not_crossing=not_crossing,
tol=tol, _queries=self._queries)
|
Return bare integer tags (drops dim).
Source code in src/apeGmsh/core/_selection.py
| def tags(self) -> list[int]:
"""Return bare integer tags (drops dim)."""
return [t for _, t in self]
|
to_label
to_label(name: str) -> 'Selection'
Register every entity in this selection as a label.
Groups by dimension before calling session.labels.add so a
mixed-dim Selection is handled correctly. Returns self for
chaining.
Example
::
(m.model.select(curves, dim=1).result()
.select(on={'x': 0})
.select(on={'y': 5})
.to_label('left_top_edge'))
m.mesh.sizing.set_size('left_top_edge', size=0.1)
Source code in src/apeGmsh/core/_selection.py
| def to_label(self, name: str) -> "Selection":
"""
Register every entity in this selection as a label.
Groups by dimension before calling ``session.labels.add`` so a
mixed-dim Selection is handled correctly. Returns ``self`` for
chaining.
Example
-------
::
(m.model.select(curves, dim=1).result()
.select(on={'x': 0})
.select(on={'y': 5})
.to_label('left_top_edge'))
m.mesh.sizing.set_size('left_top_edge', size=0.1)
"""
import warnings
if self._queries is None:
raise RuntimeError(
"Selection.to_label()/.to_physical() requires a Selection "
"bound to the model-queries engine — build it via "
"m.model.select(...).result(). This Selection has "
"_queries=None (constructed standalone), so it has no "
"session to register the label/physical-group on."
)
session = self._queries._model._parent
dims = sorted({d for d, _ in self})
with warnings.catch_warnings():
# Re-using the same name across multiple dims is the documented
# intent here, not a mistake — silence the labels-composite warning
# so a mixed-dim selection labels cleanly.
if len(dims) > 1:
warnings.filterwarnings(
"ignore", message=r".*already exists at dim.*",
)
for d in dims:
tags = [t for dim, t in self if dim == d]
session.labels.add(d, tags, name=name)
return self
|
to_physical
to_physical(name: str) -> 'Selection'
Register every entity in this selection as a physical group.
Groups by dimension before calling session.physical.add so a
mixed-dim Selection is handled correctly. Returns self for
chaining.
Example
::
(m.model.select(faces, dim=2).result()
.select(on={'z': 0})
.to_physical('Base'))
g.constraints.fix('Base', dofs=[1, 2, 3])
Source code in src/apeGmsh/core/_selection.py
| def to_physical(self, name: str) -> "Selection":
"""
Register every entity in this selection as a physical group.
Groups by dimension before calling ``session.physical.add`` so a
mixed-dim Selection is handled correctly. Returns ``self`` for
chaining.
Example
-------
::
(m.model.select(faces, dim=2).result()
.select(on={'z': 0})
.to_physical('Base'))
g.constraints.fix('Base', dofs=[1, 2, 3])
"""
if self._queries is None:
raise RuntimeError(
"Selection.to_label()/.to_physical() requires a Selection "
"bound to the model-queries engine — build it via "
"m.model.select(...).result(). This Selection has "
"_queries=None (constructed standalone), so it has no "
"session to register the label/physical-group on."
)
session = self._queries._model._parent
for d in sorted({d for d, _ in self}):
tags = [t for dim, t in self if dim == d]
session.physical.add(d, tags, name=name)
return self
|
parallel_to
parallel_to(direction: 'str | tuple[float, float, float] | np.ndarray', *, angle_tol: float = 1.0) -> 'Selection'
Keep curves whose endpoint chord is parallel to direction.
Only meaningful for curves (dim=1). Raises ValueError if the
Selection contains entities of any other dim.
Parameters
direction : str or 3-vector
"x", "y", "z" for axis aliases, or any non-zero
3-vector for an arbitrary direction. Anti-parallel matches
count as parallel — a z-edge with reversed endpoint order is
still a z-edge.
angle_tol : float, default 1.0
Maximum angle (in degrees) between the curve's chord direction
and direction for the curve to be kept.
Returns
Selection
New Selection of curves that match.
Example
::
edges = m.model.select("layer_1", dim=1).result()
verticals = edges.parallel_to("z")
obliques = edges.parallel_to((1, 1, 0), angle_tol=2.0)
m.mesh.structured.set_transfinite_curve(verticals.tags(), n=21)
Source code in src/apeGmsh/core/_selection.py
| def parallel_to(
self,
direction: "str | tuple[float, float, float] | np.ndarray",
*,
angle_tol: float = 1.0,
) -> "Selection":
"""Keep curves whose endpoint chord is parallel to ``direction``.
Only meaningful for curves (dim=1). Raises ``ValueError`` if the
Selection contains entities of any other dim.
Parameters
----------
direction : str or 3-vector
``"x"``, ``"y"``, ``"z"`` for axis aliases, or any non-zero
3-vector for an arbitrary direction. Anti-parallel matches
count as parallel — a z-edge with reversed endpoint order is
still a z-edge.
angle_tol : float, default 1.0
Maximum angle (in degrees) between the curve's chord direction
and ``direction`` for the curve to be kept.
Returns
-------
Selection
New Selection of curves that match.
Example
-------
::
edges = m.model.select("layer_1", dim=1).result()
verticals = edges.parallel_to("z")
obliques = edges.parallel_to((1, 1, 0), angle_tol=2.0)
m.mesh.structured.set_transfinite_curve(verticals.tags(), n=21)
"""
_require_dim(self, 1, method="parallel_to")
target = _parse_direction(direction)
cos_tol = math.cos(math.radians(angle_tol))
kept = [
dt for dt in self
if abs(float(_chord_direction(dt) @ target)) >= cos_tol
]
return Selection(kept, _queries=self._queries)
|
normal_along
normal_along(direction: 'str | tuple[float, float, float] | np.ndarray', *, angle_tol: float = 1.0) -> 'Selection'
Keep surfaces whose face normal is along direction.
Only meaningful for surfaces (dim=2). Raises ValueError if
the Selection contains entities of any other dim.
Same direction grammar and tolerance as :meth:parallel_to. The
normal is computed from three boundary points — exact for flat
faces, an approximation for curved faces (prefer on= for those).
Anti-parallel matches count as parallel.
Example
::
faces = m.model.select("layer_1", dim=2).result()
horizontals = faces.normal_along("z")
verticals = faces.normal_along("x").select(...)
Source code in src/apeGmsh/core/_selection.py
| def normal_along(
self,
direction: "str | tuple[float, float, float] | np.ndarray",
*,
angle_tol: float = 1.0,
) -> "Selection":
"""Keep surfaces whose face normal is along ``direction``.
Only meaningful for surfaces (dim=2). Raises ``ValueError`` if
the Selection contains entities of any other dim.
Same direction grammar and tolerance as :meth:`parallel_to`. The
normal is computed from three boundary points — exact for flat
faces, an approximation for curved faces (prefer ``on=`` for those).
Anti-parallel matches count as parallel.
Example
-------
::
faces = m.model.select("layer_1", dim=2).result()
horizontals = faces.normal_along("z")
verticals = faces.normal_along("x").select(...)
"""
_require_dim(self, 2, method="normal_along")
target = _parse_direction(direction)
cos_tol = math.cos(math.radians(angle_tol))
kept = [
dt for dt in self
if abs(float(_face_normal(dt) @ target)) >= cos_tol
]
return Selection(kept, _queries=self._queries)
|
union
union(other) -> 'Selection'
Alias for self | other. See :meth:__or__.
Source code in src/apeGmsh/core/_selection.py
| def union(self, other) -> "Selection":
"""Alias for ``self | other``. See :meth:`__or__`."""
return self.__or__(other)
|
intersect
intersect(other) -> 'Selection'
Alias for self & other. See :meth:__and__.
Source code in src/apeGmsh/core/_selection.py
| def intersect(self, other) -> "Selection":
"""Alias for ``self & other``. See :meth:`__and__`."""
return self.__and__(other)
|
difference
difference(other) -> 'Selection'
Alias for self - other. See :meth:__sub__.
Source code in src/apeGmsh/core/_selection.py
| def difference(self, other) -> "Selection":
"""Alias for ``self - other``. See :meth:`__sub__`."""
return self.__sub__(other)
|
partition_by
partition_by(axis: str | None = None)
Group entities by their dominant bounding-box axis.
Returns
If axis is None: dict[str, Selection] keyed by 'x',
'y', 'z'.
If axis is one of 'x', 'y', 'z': a single
Selection for that axis only.
Semantics by entity dimension
- dim = 1 (curves) — dominant axis is the largest BB extent
(the direction the curve runs along).
- dim = 2 (surfaces) — dominant axis is the smallest BB extent
(the surface normal — for axis-aligned faces this picks the
perpendicular direction).
- Mixed dims partition independently per dim using the right rule.
Example
::
curves = m.model.queries.boundary_curves('box')
groups = curves.partition_by()
m.mesh.structured.set_transfinite_curve(groups['x'].tags(), nx)
m.mesh.structured.set_transfinite_curve(groups['y'].tags(), ny)
m.mesh.structured.set_transfinite_curve(groups['z'].tags(), nz)
Source code in src/apeGmsh/core/_selection.py
| def partition_by(self, axis: str | None = None):
"""
Group entities by their dominant bounding-box axis.
Returns
-------
If ``axis`` is ``None``: ``dict[str, Selection]`` keyed by ``'x'``,
``'y'``, ``'z'``.
If ``axis`` is one of ``'x'``, ``'y'``, ``'z'``: a single
``Selection`` for that axis only.
Semantics by entity dimension
-----------------------------
- **dim = 1 (curves)** — dominant axis is the **largest** BB extent
(the direction the curve runs along).
- **dim = 2 (surfaces)** — dominant axis is the **smallest** BB extent
(the surface normal — for axis-aligned faces this picks the
perpendicular direction).
- Mixed dims partition independently per dim using the right rule.
Example
-------
::
curves = m.model.queries.boundary_curves('box')
groups = curves.partition_by()
m.mesh.structured.set_transfinite_curve(groups['x'].tags(), nx)
m.mesh.structured.set_transfinite_curve(groups['y'].tags(), ny)
m.mesh.structured.set_transfinite_curve(groups['z'].tags(), nz)
"""
if axis is not None and axis not in ('x', 'y', 'z'):
raise ValueError(f"axis must be 'x', 'y', or 'z', got {axis!r}")
groups: dict[str, list] = {'x': [], 'y': [], 'z': []}
AXES = ('x', 'y', 'z')
for d, t in self:
xmin, ymin, zmin, xmax, ymax, zmax = gmsh.model.getBoundingBox(d, t)
spans = [xmax - xmin, ymax - ymin, zmax - zmin]
if d == 1:
# Curve → direction of largest extent
idx = int(np.argmax(spans))
elif d == 2:
# Surface → axis with smallest extent (≈ normal direction)
idx = int(np.argmin(spans))
elif d == 3:
# Volume → largest extent (most useful for transfinite hints)
idx = int(np.argmax(spans))
else:
continue # dim 0 — points have no axis
groups[AXES[idx]].append((d, t))
if axis is not None:
return Selection(groups[axis], _queries=self._queries)
return {ax: Selection(items, _queries=self._queries)
for ax, items in groups.items()}
|
Geometric primitives (internal)
These classes are constructed automatically by select() from raw input.
You never instantiate them directly, but their docstrings describe the
accepted formats.
apeGmsh.core._selection.Plane
dataclass
Plane(normal: ndarray, anchor: ndarray)
Infinite plane defined by a unit normal and an anchor point.
at
classmethod
Axis-aligned plane. E.g. Plane.at(z=0), Plane.at(x=5).
Source code in src/apeGmsh/core/_selection.py
| @classmethod
def at(cls, **kwargs) -> "Plane":
"""Axis-aligned plane. E.g. ``Plane.at(z=0)``, ``Plane.at(x=5)``."""
if len(kwargs) != 1:
raise ValueError("Plane.at() takes exactly one keyword, e.g. z=0")
axis, value = next(iter(kwargs.items()))
axes = {'x': 0, 'y': 1, 'z': 2}
if axis not in axes:
raise ValueError(f"Unknown axis {axis!r}. Use 'x', 'y', or 'z'.")
normal = np.zeros(3)
normal[axes[axis]] = 1.0
anchor = np.zeros(3)
anchor[axes[axis]] = float(value)
return cls(normal=normal, anchor=anchor)
|
through
classmethod
through(p1, p2, p3) -> 'Plane'
Plane through three non-collinear points.
Source code in src/apeGmsh/core/_selection.py
| @classmethod
def through(cls, p1, p2, p3) -> "Plane":
"""Plane through three non-collinear points."""
p1, p2, p3 = np.array(p1, float), np.array(p2, float), np.array(p3, float)
n = np.cross(p2 - p1, p3 - p1)
norm = np.linalg.norm(n)
if norm < 1e-14:
raise ValueError("Points are collinear — cannot define a plane.")
return cls(normal=n / norm, anchor=p1)
|
signed_distances
signed_distances(bb: tuple) -> np.ndarray
Signed distance of each bounding-box corner from this plane.
Source code in src/apeGmsh/core/_selection.py
| def signed_distances(self, bb: tuple) -> np.ndarray:
"""Signed distance of each bounding-box corner from this plane."""
corners = _bb_corners(bb) # (8, 3)
return (corners - self.anchor) @ self.normal # (8,)
|
apeGmsh.core._selection.Line
dataclass
Line(normal: ndarray, anchor: ndarray)
Infinite line used to cut 2-D geometry.
The 'signed distance' is computed as the component of each bounding-box
corner along the line's in-plane normal — the axis perpendicular to the
line direction projected onto the dominant plane (XY, XZ, or YZ).
through
classmethod
through(p1, p2) -> 'Line'
Line through two points.
Source code in src/apeGmsh/core/_selection.py
| @classmethod
def through(cls, p1, p2) -> "Line":
"""Line through two points."""
p1, p2 = np.array(p1, float), np.array(p2, float)
d = p2 - p1
norm = np.linalg.norm(d)
if norm < 1e-14:
raise ValueError("Points are coincident — cannot define a line.")
d = d / norm
# Build a normal perpendicular to d in the plane that best contains it
# Try cross with Z, then Y, then X to avoid degeneracy
for ref in (np.array([0., 0., 1.]), np.array([0., 1., 0.]), np.array([1., 0., 0.])):
n = np.cross(d, ref)
if np.linalg.norm(n) > 1e-6:
break
n = n / np.linalg.norm(n)
return cls(normal=n, anchor=p1)
|