Migrating to pyGmsh → apeGmsh v1.0¶
v1.0 bundles four breaking changes into a single release:
- Package rename:
pyGmsh→apeGmsh - Model API restructure:
g.model.*methods split into five focused sub-composites (geometry,boolean,transforms,io,queries). - Mesh API restructure:
g.mesh.*methods split into seven focused sub-composites (generation,sizing,field,structured,editing,queries,partitioning). - OpenSees API restructure:
g.opensees.*methods split into five focused sub-composites (materials,elements,ingest,inspect,export), withset_modelandbuildstaying flat.
Plus several smaller cleanups (g.mass → g.masses, lifecycle aliases,
legacy deprecation removals).
This guide is a drop-in find/replace checklist. Most codebases can migrate in one pass with a single search across the project.
If you were on v0.3.0 (last pyGmsh release), apply all sections below.
If you were on v0.3.1 (the transitional apeGmsh rename), skip section 1.
1. Package rename: pyGmsh → apeGmsh¶
-from pyGmsh import pyGmsh
+from apeGmsh import apeGmsh
-from pyGmsh import Part, PartsRegistry, Instance
+from apeGmsh import Part, PartsRegistry, Instance
-from pyGmsh.viewers.ui.theme import STYLESHEET
+from apeGmsh.viewers.ui.theme import STYLESHEET
-g = pyGmsh(model_name="bridge")
+g = apeGmsh(model_name="bridge")
The class name stays lowercase to match the package (from apeGmsh import apeGmsh).
Install: uninstall the old wheel (pip uninstall pyGmsh) and install the
new one (pip install -e . from the repo root).
Companion app rename: the standalone results viewer was also renamed
from pyGmshViewer → apeGmshViewer (directory + package + console
script). Update imports and command-line invocations accordingly:
-from pyGmshViewer import show
+from apeGmshViewer import show
-python -m pyGmshViewer results.vtu
+python -m apeGmshViewer results.vtu
-pygmsh-viewer results.vtu
+apegmsh-viewer results.vtu
2. Model methods → sub-composites¶
Model is now a composition of five focused sub-composites. Every geometry
method moved behind a categorical namespace.
Geometry primitives (g.model.geometry.*)¶
-p = g.model.add_point(0, 0, 0)
+p = g.model.geometry.add_point(0, 0, 0)
-l = g.model.add_line(p1, p2)
+l = g.model.geometry.add_line(p1, p2)
-box = g.model.add_box(0, 0, 0, 1, 1, 1)
+box = g.model.geometry.add_box(0, 0, 0, 1, 1, 1)
-cyl = g.model.add_cylinder(0, 0, 0, 0, 0, 10, 0.5)
+cyl = g.model.geometry.add_cylinder(0, 0, 0, 0, 0, 10, 0.5)
All 19 methods move: add_point, add_line, add_arc, add_circle,
add_ellipse, add_spline, add_bspline, add_bezier, add_wire,
add_curve_loop, add_plane_surface, add_surface_filling,
add_rectangle, add_box, add_sphere, add_cylinder, add_cone,
add_torus, add_wedge.
Boolean operations (g.model.boolean.*)¶
-result = g.model.fuse(a, b)
+result = g.model.boolean.fuse(a, b)
-part = g.model.cut(box, hole)
+part = g.model.boolean.cut(box, hole)
-common = g.model.intersect(a, b)
+common = g.model.boolean.intersect(a, b)
-pieces = g.model.fragment(objects=[1], tools=[2])
+pieces = g.model.boolean.fragment(objects=[1], tools=[2])
Transforms & sweeps (g.model.transforms.*)¶
-g.model.translate(v, 0, 0, 5)
+g.model.transforms.translate(v, 0, 0, 5)
-g.model.rotate(v, angle=np.pi/4, ax=0, ay=0, az=1)
+g.model.transforms.rotate(v, angle=np.pi/4, ax=0, ay=0, az=1)
-g.model.scale(v, 2, 2, 2)
+g.model.transforms.scale(v, 2, 2, 2)
-g.model.mirror(v, 1, 0, 0, 0)
+g.model.transforms.mirror(v, 1, 0, 0, 0)
-copies = g.model.copy(v)
+copies = g.model.transforms.copy(v)
-ext = g.model.extrude(surf, 0, 0, 10)
+ext = g.model.transforms.extrude(surf, 0, 0, 10)
-rev = g.model.revolve(surf, np.pi, ax=0, ay=1, az=0)
+rev = g.model.transforms.revolve(surf, np.pi, ax=0, ay=1, az=0)
-swept = g.model.sweep(profile, path)
+swept = g.model.transforms.sweep(profile, path)
-loft = g.model.thru_sections([w1, w2])
+loft = g.model.transforms.thru_sections([w1, w2])
Fluent chaining still works on sub-composites:
Import / export (g.model.io.*)¶
-g.model.load_step("bracket.step")
+g.model.io.load_step("bracket.step")
-g.model.save_step("output.step")
+g.model.io.save_step("output.step")
-g.model.load_iges("file.iges")
+g.model.io.load_iges("file.iges")
-g.model.load_dxf("plan.dxf")
+g.model.io.load_dxf("plan.dxf")
-g.model.save_msh("mesh.msh")
+g.model.io.save_msh("mesh.msh")
-g.model.heal_shapes(tolerance=1e-6)
+g.model.io.heal_shapes(tolerance=1e-6)
Queries & cleanup (g.model.queries.*)¶
-bb = g.model.bounding_box(tag)
+bb = g.model.queries.bounding_box(tag)
-cm = g.model.center_of_mass(tag)
+cm = g.model.queries.center_of_mass(tag)
-vol = g.model.mass(tag)
+vol = g.model.queries.mass(tag)
-bdry = g.model.boundary(tags)
+bdry = g.model.queries.boundary(tags)
-up, down = g.model.adjacencies(tag)
+up, down = g.model.queries.adjacencies(tag)
-ents = g.model.entities_in_bounding_box(0, 0, 0, 1, 1, 1)
+ents = g.model.queries.entities_in_bounding_box(0, 0, 0, 1, 1, 1)
-g.model.remove([(3, 1)])
+g.model.queries.remove([(3, 1)])
-g.model.remove_duplicates()
+g.model.queries.remove_duplicates()
-g.model.make_conformal()
+g.model.queries.make_conformal()
-df = g.model.registry()
+df = g.model.queries.registry()
What stays on Model directly¶
These methods remain flat on g.model:
g.model.sync()g.model.viewer(fem=...)g.model.gui()g.model.launch_picker()g.model.select(...)— fluent entity selection (theg.model.selectionsub-composite was later removed by selection-unification v2; see §6a)
And the existing g.parts, g.physical, g.constraints, g.loads,
g.mesh_selection composites are unchanged in location.
3. Mesh methods → sub-composites¶
Mesh is now a thin composition container. Every action method moved
behind one of seven focused sub-composites. The old flat methods
(g.mesh.generate(3), g.mesh.set_global_size(0.5), ...) are gone
— there are no shortcuts, and no backwards-compatible aliases.
The mapping is mechanical — every old method went to exactly one sub-composite. Pick the right namespace, prefix the method, done.
Generation & algorithm (g.mesh.generation.*)¶
-g.mesh.generate(3)
+g.mesh.generation.generate(3)
-g.mesh.set_order(2)
+g.mesh.generation.set_order(2)
-g.mesh.refine()
+g.mesh.generation.refine()
-g.mesh.optimize("Netgen", niter=3)
+g.mesh.generation.optimize("Netgen", niter=3)
-g.mesh.set_algorithm(surf_tag, "frontal_delaunay_quads")
+g.mesh.generation.set_algorithm(surf_tag, "frontal_delaunay_quads")
-g.mesh.set_algorithm_by_physical("Flanges", "quads")
+g.mesh.generation.set_algorithm_by_physical("Flanges", "quads")
Size control (g.mesh.sizing.*)¶
-g.mesh.set_global_size(6000)
+g.mesh.sizing.set_global_size(6000)
-g.mesh.set_size_global(min_size=15, max_size=25)
+g.mesh.sizing.set_size_global(min_size=15, max_size=25)
-g.mesh.set_size_sources(from_points=False)
+g.mesh.sizing.set_size_sources(from_points=False)
-g.mesh.set_size([p1, p2], 0.05)
+g.mesh.sizing.set_size([p1, p2], 0.05)
-g.mesh.set_size_all_points(6000)
+g.mesh.sizing.set_size_all_points(6000)
-g.mesh.set_size_callback(my_size_fn)
+g.mesh.sizing.set_size_callback(my_size_fn)
-g.mesh.set_size_by_physical("Corners", 0.05)
+g.mesh.sizing.set_size_by_physical("Corners", 0.05)
g.mesh.field (the FieldHelper) is unchanged — it was already a
sub-composite and lives alongside g.mesh.sizing.
Structured meshing (g.mesh.structured.*)¶
-g.mesh.set_transfinite_curve(c, 20, coef=1.3)
+g.mesh.structured.set_transfinite_curve(c, 20, coef=1.3)
-g.mesh.set_transfinite_surface(s, arrangement="Left")
+g.mesh.structured.set_transfinite_surface(s, arrangement="Left")
-g.mesh.set_transfinite_volume(v, corners=[...])
+g.mesh.structured.set_transfinite_volume(v, corners=[...])
-g.mesh.set_transfinite_automatic()
+g.mesh.structured.set_transfinite_automatic()
-g.mesh.set_transfinite_by_physical("Web", dim=1, n_nodes=40)
+g.mesh.structured.set_transfinite_by_physical("Web", dim=1, n_nodes=40)
-g.mesh.set_recombine(s)
+g.mesh.structured.set_recombine(s)
-g.mesh.recombine()
+g.mesh.structured.recombine()
-g.mesh.set_recombine_by_physical("Flanges")
+g.mesh.structured.set_recombine_by_physical("Flanges")
-g.mesh.set_smoothing(s, 5)
+g.mesh.structured.set_smoothing(s, 5)
-g.mesh.set_smoothing_by_physical("Web", 5)
+g.mesh.structured.set_smoothing_by_physical("Web", 5)
-g.mesh.set_compound(2, [s1, s2, s3])
+g.mesh.structured.set_compound(2, [s1, s2, s3])
-g.mesh.remove_constraints()
+g.mesh.structured.remove_constraints()
Editing (g.mesh.editing.*)¶
Topology editing, periodicity, STL → discrete pipeline, and embed.
-g.mesh.embed(crack_surf, body_tag, dim=2, in_dim=3)
+g.mesh.editing.embed(crack_surf, body_tag, dim=2, in_dim=3)
-g.mesh.set_periodic([2], [1], transform)
+g.mesh.editing.set_periodic([2], [1], transform)
-g.mesh.clear()
+g.mesh.editing.clear()
-g.mesh.reverse()
+g.mesh.editing.reverse()
-g.mesh.relocate_nodes()
+g.mesh.editing.relocate_nodes()
-g.mesh.remove_duplicate_nodes()
+g.mesh.editing.remove_duplicate_nodes()
-g.mesh.remove_duplicate_elements()
+g.mesh.editing.remove_duplicate_elements()
-g.mesh.affine_transform(matrix)
+g.mesh.editing.affine_transform(matrix)
-g.mesh.import_stl()
+g.mesh.editing.import_stl()
-g.mesh.classify_surfaces(math.radians(30))
+g.mesh.editing.classify_surfaces(math.radians(30))
-g.mesh.create_geometry()
+g.mesh.editing.create_geometry()
Queries (g.mesh.queries.*)¶
Every read-only extractor. get_fem_data lives here — this is the
single most-touched rewrite in most projects.
-nodes = g.mesh.get_nodes(dim=2)
+nodes = g.mesh.queries.get_nodes(dim=2)
-elems = g.mesh.get_elements(dim=3)
+elems = g.mesh.queries.get_elements(dim=3)
-props = g.mesh.get_element_properties(4)
+props = g.mesh.queries.get_element_properties(4)
-fem = g.mesh.get_fem_data(dim=3)
+fem = g.mesh.queries.get_fem_data(dim=3)
-q = g.mesh.get_element_qualities(tags, "minSICN")
+q = g.mesh.queries.get_element_qualities(tags, "minSICN")
-df = g.mesh.quality_report()
+df = g.mesh.queries.quality_report()
Partitioning & renumbering (g.mesh.partitioning.*)¶
MPI-style partitioning and node/element renumbering now live in the same namespace — they are the "reorganise DOFs" surface.
-g.mesh.partition(4)
+g.mesh.partitioning.partition(4)
-g.mesh.unpartition()
+g.mesh.partitioning.unpartition()
-g.mesh.renumber_mesh(method="rcm", base=1)
+result = g.mesh.partitioning.renumber(dim=2, method="rcm", base=1)
The split compute_renumbering / renumber_nodes / renumber_elements
surface from v0 has been collapsed into a single
partitioning.renumber(dim, *, method, base) call (see
mesh/_mesh_partitioning.py:134), which returns a RenumberResult
with the old→new maps for both nodes and elements.
What stays on Mesh directly¶
Only two entry points remain flat on g.mesh — both open interactive
windows and neither makes sense inside a sub-composite:
g.mesh.viewer(**kwargs)— open the interactive mesh viewerg.mesh.results_viewer(...)— open the results viewer
Plus the FieldHelper sub-composite which was already in place:
g.mesh.field.*— unchanged from v0.x
Chaining inside a composite¶
Every non-query method on a sub-composite returns self (the
sub-composite) so chaining works the same way it used to — just inside
one composite at a time:
(g.mesh.sizing
.set_size_sources(from_points=False)
.set_global_size(6000))
(g.mesh.generation
.set_algorithm(0, "hxt", dim=3)
.generate(3)
.set_order(2))
To chain across composites, break the chain — the cross-composite
g.mesh.sizing.set_global_size(...).generate(...) form does not
work, because generate is on generation, not sizing.
4. OpenSees methods → sub-composites¶
OpenSees is now a thin composition container. Every declaration
method moved behind one of five focused sub-composites, and several
long-winded names were shortened in the process.
Two methods stay flat on g.opensees — the lifecycle verbs that
every workflow calls exactly once or twice:
g.opensees.set_model(ndm, ndf)— unchangedg.opensees.build()— unchanged
Materials (g.opensees.materials.*)¶
Three material-like registries live here. Names kept as-is — the
composite prefix already provides the disambiguation you used to get
from the add_*_material suffix.
-g.opensees.add_nd_material("Concrete", "ElasticIsotropic", E=30e9, nu=0.2)
+g.opensees.materials.add_nd_material("Concrete", "ElasticIsotropic", E=30e9, nu=0.2)
-g.opensees.add_uni_material("Steel", "Steel01", Fy=250e6, E=200e9, b=0.01)
+g.opensees.materials.add_uni_material("Steel", "Steel01", Fy=250e6, E=200e9, b=0.01)
-g.opensees.add_section("Slab", "ElasticMembranePlateSection",
- E=30e9, nu=0.2, h=0.2, rho=2400)
+g.opensees.materials.add_section("Slab", "ElasticMembranePlateSection",
+ E=30e9, nu=0.2, h=0.2, rho=2400)
Elements (g.opensees.elements.*)¶
Geometric transformations, element assignments, and single-point constraints — everything that attaches to a physical group during model declaration.
Renamed: assign_element → assign (the composite name already
says "elements", so elements.assign_element would stutter).
-g.opensees.add_geom_transf("Cols", "Linear", vecxz=[0, 0, 1])
+g.opensees.elements.add_geom_transf("Cols", "Linear", vecxz=[0, 0, 1])
-g.opensees.assign_element("Body", "FourNodeTetrahedron", material="Concrete")
+g.opensees.elements.assign("Body", "FourNodeTetrahedron", material="Concrete")
-g.opensees.assign_element("Cols", "elasticBeamColumn",
- geom_transf="Cols", A=0.04, E=200e9, ...)
+g.opensees.elements.assign("Cols", "elasticBeamColumn",
+ geom_transf="Cols", A=0.04, E=200e9, ...)
-g.opensees.fix("Base", dofs=[1, 1, 1])
+g.opensees.elements.fix("Base", dofs=[1, 1, 1])
Ingest from FEMData (g.opensees.ingest.*)¶
The two consume_*_from_fem methods are renamed — the composite
name "ingest" already says what they do, and fem is the argument,
so the method names drop everything but the noun.
-g.opensees.consume_loads_from_fem(fem)
+g.opensees.ingest.loads(fem)
-g.opensees.consume_masses_from_fem(fem)
+g.opensees.ingest.masses(fem)
Inspect (g.opensees.inspect.*)¶
Post-build tables and the human-readable summary. Names unchanged.
-df = g.opensees.node_table()
+df = g.opensees.inspect.node_table()
-df = g.opensees.element_table()
+df = g.opensees.inspect.element_table()
-print(g.opensees.summary())
+print(g.opensees.inspect.summary())
Export (g.opensees.export.*)¶
Script emission. Renamed: export_tcl → tcl, export_py →
py (same reasoning as elements.assign).
-g.opensees.export_tcl("model.tcl")
+g.opensees.export.tcl("model.tcl")
-g.opensees.export_py("model.py")
+g.opensees.export.py("model.py")
Both return the composite so they still chain:
Full before/after¶
# Declaration
-g.opensees.set_model(ndm=3, ndf=6)
-g.opensees.add_nd_material("Concrete", "ElasticIsotropic", E=30e9, nu=0.2)
-g.opensees.add_geom_transf("Cols", "Linear", vecxz=[0, 0, 1])
-g.opensees.assign_element("Body", "FourNodeTetrahedron", material="Concrete")
-g.opensees.assign_element("Cols", "elasticBeamColumn", geom_transf="Cols",
- A=0.04, E=200e9, G=77e9, Jx=1e-4, Iy=2e-4, Iz=2e-4)
-g.opensees.fix("Base", dofs=[1, 1, 1, 1, 1, 1])
+g.opensees.set_model(ndm=3, ndf=6)
+g.opensees.materials.add_nd_material("Concrete", "ElasticIsotropic", E=30e9, nu=0.2)
+g.opensees.elements.add_geom_transf("Cols", "Linear", vecxz=[0, 0, 1])
+g.opensees.elements.assign("Body", "FourNodeTetrahedron", material="Concrete")
+g.opensees.elements.assign("Cols", "elasticBeamColumn", geom_transf="Cols",
+ A=0.04, E=200e9, G=77e9, Jx=1e-4, Iy=2e-4, Iz=2e-4)
+g.opensees.elements.fix("Base", dofs=[1, 1, 1, 1, 1, 1])
# Build + inspect + export
fem = g.mesh.queries.get_fem_data(dim=3)
-g.opensees.consume_loads_from_fem(fem)
-g.opensees.consume_masses_from_fem(fem)
+g.opensees.ingest.loads(fem).masses(fem)
g.opensees.build()
-print(g.opensees.summary())
-g.opensees.export_tcl("model.tcl").export_py("model.py")
+print(g.opensees.inspect.summary())
+g.opensees.export.tcl("model.tcl").py("model.py")
Chaining inside a composite¶
Like Mesh, every non-query method on an OpenSees sub-composite
returns self (the sub-composite) so chaining works inside one
namespace at a time:
(g.opensees.materials
.add_nd_material("Concrete", "ElasticIsotropic", E=30e9, nu=0.2)
.add_nd_material("Soil", "DruckerPrager", K=80e6, G=60e6, sigmaY=20e3))
(g.opensees.elements
.add_geom_transf("Cols", "Linear", vecxz=[0, 0, 1])
.assign("Cols", "elasticBeamColumn", geom_transf="Cols", ...)
.fix("Base", dofs=[1, 1, 1, 1, 1, 1]))
To chain across composites, break the chain — the cross-composite form does not work.
5. Rename: g.mass → g.masses¶
Every other composite is plural. The last outlier is fixed.
-g.mass.volume("concrete", density=2400)
+g.masses.volume("concrete", density=2400)
-g.mass.point("tip", mass=500)
+g.masses.point("tip", mass=500)
-g.mass.line("beam", linear_density=80)
+g.masses.line("beam", linear_density=80)
-g.mass.surface("slab", areal_density=300)
+g.masses.surface("slab", areal_density=300)
The FEMData field is also renamed:
fem = g.mesh.queries.get_fem_data(dim=3)
-print(fem.mass.total_mass())
+print(fem.nodes.masses.total_mass())
The class names (MassesComposite, MassDef, MassRecord, MassSet)
are unchanged — only the session attribute and FEMData field renamed.
Not renamed (intentionally): r.mass on MassRecord is the value field,
d.mass on PointMassDef, g.model.queries.mass(tag) (geometric mass),
and ops.mass(...) (OpenSees solver command).
6. Removed legacy aliases and deprecated methods¶
These had lived as shims or DeprecationWarning for one or more releases.
v1.0 deletes them entirely.
Session lifecycle¶
-g.initialize() # old alias
+g.begin()
-g.finalize() # old alias
+g.end()
-g._initialized # old property
+g.is_active
-g.model_name # old property alias for g.name
+g.name
Fast/slow viewer split¶
Parts / PhysicalGroups separation¶
-# Old: auto 1:1 mapping from parts to physical groups
-g.parts.add_physical_groups()
+# New: be explicit about which entities join which group
+inst = g.parts.get("i_beam")
+g.physical.add_volume(inst.entities[3], name="steel")
Parts track assembly identity ("which geometry belongs to which part"). Physical groups tag regions for solver purposes ("which entities get this material / BC"). A part can have many groups; a group can span parts. The auto-mapping collapsed this richness.
OpenSees bridge¶
-g.opensees.add_nodal_load(
- "Wind", "WindwardFace", force=[1e4, 0, 0]
-)
+with g.loads.pattern("Wind"):
+ g.loads.point("WindwardFace", force_xyz=(1e4, 0, 0))
# Then `fem.nodes.loads` / `fem.elements.loads` are auto-populated by get_fem_data(),
# and the OpenSees bridge consumes it via consume_loads_from_fem()
# (auto-called from build_from_fem()).
Mesh selection parameter alias¶
The v1.0-era nearest_to→closest_to alias applied to
g.mesh_selection.add_nodes / add_elements. Those spatial
registrars were removed entirely by selection-unification v2
(§6a) — use g.mesh_selection.select(...).nearest_to(...) instead;
there is no add_nodes to pass either keyword to.
Convenience delegates on the session¶
-g.remove_duplicates() # apeGmsh convenience delegate
+g.model.queries.remove_duplicates()
-g.make_conformal()
+g.model.queries.make_conformal()
6a. Selection-unification v2 — full removal (BREAKING, post-v1.0)¶
Selection-unification v2 collapsed every selection surface onto a
single fluent idiom and hard-removed the legacy surface
(no shim, no deprecation window). The fluent .select() family is now
the only way to select; the old entry points raise AttributeError /
ImportError. Migration table:
| Removed | Replacement |
|---|---|
g.model.queries.select(on=/crossing=/not_on=/not_crossing=), g.model.queries.line(...) |
g.model.select(target, dim=) → EntitySelection; straddle via .crossing_plane(spec, mode=) or .result().select(on=...) |
g.model.queries.select_all*() |
g.model.select(dim=N) (no target) |
g.model.selection / g.model.selection.select_* (SelectionComposite) |
g.model.select(...) (see capability gap below) |
fem.nodes.get/get_ids/get_coords, fem.elements.get/get_ids/get_coords/resolve |
fem.nodes.select(...) / fem.elements.select(...) → MeshSelection; terminals .ids / .coords / .connectivity / .groups() / .result() / .resolve() |
g.mesh_selection.add_nodes / add_elements (spatial registrars) |
g.mesh_selection.select(...).save_as(name) (live engine); g.mesh_selection.add(dim, ids, name=) / from_physical(...) retained for explicit ids. filter_set / sort_set / union / intersection / difference are retained. |
results.<level>.select(...).get(component=) chain terminal; the chain .values() |
results.<level>.select(...).values(component=) → forwards to the retained typed reader results.<sub>.get(component=, ids=, pg=, label=, selection=) |
package exports of core._selection.Selection / viz.Selection |
the classes are RETAINED by architecture (.result() payload / viewer pick-result type) — only the exports were dropped; reach them via g.model.select(...).result() / viewer.selection |
Two capability gaps with no v2 successor (document, do not paper over):
g.mesh_selection.from_geometric(...)+viz.Selection.to_mesh_*(the "geometric entity selection → named mesh-selection" path) — both ends were removed; there is no v2 replacement.- The
SelectionComposite.select_*rich filter grammar (labels=fnmatch,kinds=,length/area/volume_range=,predicate=fn,exclude_tags=,physical=,at_point=) —EntitySelectionhas only spatial verbs + set-ops +to_label/to_physical/to_dataframe. The retainedviz.Selection.filter()carries that grammar but is viewer-pick only, not ag.model.selectmigration path.
The other behavioural changes you must know about: one breaking box
default (S2), and a fail-loud end-state on three formerly-silent
paths (S5) that you may now hit at runtime. Only one of the three S5
paths (the loads/masses __ms__ consumer) is a behavior change
landing with the v1.0 release; the other two were already loud /
merged ahead of it (attributed below).
S2 — g.mesh_selection box default: closed → half-open (BREAKING)¶
g.mesh_selection's in_box filter is now half-open on the upper
side by default (xmin <= xyz < xmax per axis), matching the
results side which was always half-open. Previously it was
closed-closed. A coordinate exactly equal to an upper bound is now
excluded — adjacent boxes no longer double-count a shared face.
# A node exactly on the z = 10 upper face:
-g.mesh_selection.select().in_box((-5,-5,0), (5,5,10)).save_as("z") # half-open: EXCLUDES it
+g.mesh_selection.select().in_box((-5,-5,0), (5,5,10),
+ inclusive=True).save_as("z") # closed: INCLUDES it
The point-family in_box(lo, hi) is half-open [lo, hi) by default;
pass inclusive=True for the closed [lo, hi] box (restores the
pre-S2 closed upper bound). If you relied on a tight box capturing
on-face nodes, either pad the box by a small ε or pass
inclusive=True. results.*.in_box is unchanged (it was always
half-open) — this only reconciles the mesh side to match.
S5 — fail-loud end-state on three formerly-silent paths¶
Each of these used to silently produce a wrong (usually empty or
corrupted) result; they now fail loud with a directive message. The
end-state below is what is true on main. Only path 2 (the
loads/masses __ms__ consumer) is the behavior change landing with
this release — paths 1 and 3 were already loud / merged ahead of
it and are shown for the complete picture.
# 1. Results selection= against an import-origin FEMData (ALREADY
# LOUD on main, locked by a characterization pin — NOT this release)
# (from_msh / MPCO / native — these have mesh_selection=None)
results = Results.from_native("run.h5", fem=fem_from_msh)
-results.nodes.get(selection="my_set") # silently → empty slab
+results.nodes.get(selection="my_set") # RuntimeError: selection= requires
+ # fem.mesh_selection to be present
# 2. A load bound to a mesh-selection name whose set is missing
# (the __ms__ consumer in LoadsComposite — THE CHANGE IN THIS RELEASE)
-g.loads.point("missing_ms_name", force_xyz=(0,0,-1)) # silently bound to 0 nodes
+g.loads.point("missing_ms_name", force_xyz=(0,0,-1)) # KeyError: the mesh-
+ # selection set is empty/missing
# 3. Element centroid with a connectivity id absent from the node set
# (ALREADY MERGED SEPARATELY, ahead of this release — NOT introduced
# here; backs results.elements.in_box / nearest_to / on_plane AND
# the results.elements.select(...) chain)
-results.elements.in_box(lo, hi, component="stress_xx") # silent corrupted centroid
+results.elements.in_box(lo, hi, component="stress_xx") # KeyError: element N
+ # references node M not
+ # in the FEM node set
Fixes, in order: bind a session-origin FEMData (one that actually
carries the g.mesh_selection sets); give the load a target that
resolves to a real, non-empty mesh selection; bind the FEMData whose
mesh matches the results file. None of these were valid results
before — the change only surfaces a bug you already had.
For a mechanical find-replace across your own codebase, this Python script handles every transform in this guide — including the Model, Mesh, and OpenSees sub-composite splits:
#!/usr/bin/env python3
"""Migrate pyGmsh v0.x → apeGmsh v1.0 across a project."""
import re, os, sys
# --- Model methods → sub-composites ---
MODEL_GEOMETRY = {
'add_point', 'add_line', 'add_arc', 'add_circle', 'add_ellipse',
'add_spline', 'add_bspline', 'add_bezier', 'add_wire',
'add_curve_loop', 'add_plane_surface', 'add_surface_filling',
'add_rectangle', 'add_box', 'add_sphere', 'add_cylinder',
'add_cone', 'add_torus', 'add_wedge',
}
MODEL_BOOLEAN = {'fuse', 'cut', 'intersect', 'fragment'}
MODEL_TRANSFORMS = {
'translate', 'rotate', 'scale', 'mirror', 'copy',
'extrude', 'revolve', 'sweep', 'thru_sections',
}
MODEL_IO = {
'load_iges', 'load_step', 'load_dxf', 'load_msh',
'save_iges', 'save_step', 'save_dxf', 'save_msh', 'heal_shapes',
}
MODEL_QUERIES = {
'remove', 'remove_duplicates', 'make_conformal', 'fragment_all',
'bounding_box', 'center_of_mass', 'mass', 'boundary',
'adjacencies', 'entities_in_bounding_box', 'registry',
}
# --- Mesh methods → sub-composites ---
MESH_MAPPING = {
# generation
'generate': 'generation', 'set_order': 'generation',
'refine': 'generation', 'optimize': 'generation',
'set_algorithm': 'generation',
'set_algorithm_by_physical': 'generation',
# sizing
'set_global_size': 'sizing', 'set_size_sources': 'sizing',
'set_size_global': 'sizing', 'set_size': 'sizing',
'set_size_all_points': 'sizing', 'set_size_callback': 'sizing',
'set_size_by_physical': 'sizing',
# structured
'set_transfinite_curve': 'structured',
'set_transfinite_surface': 'structured',
'set_transfinite_volume': 'structured',
'set_transfinite_automatic': 'structured',
'set_transfinite_by_physical': 'structured',
'set_recombine': 'structured',
'set_recombine_by_physical': 'structured',
'recombine': 'structured', 'set_smoothing': 'structured',
'set_smoothing_by_physical': 'structured',
'set_compound': 'structured',
'remove_constraints': 'structured',
# editing
'embed': 'editing', 'set_periodic': 'editing',
'import_stl': 'editing', 'classify_surfaces': 'editing',
'create_geometry': 'editing', 'clear': 'editing',
'reverse': 'editing', 'relocate_nodes': 'editing',
'remove_duplicate_nodes': 'editing',
'remove_duplicate_elements': 'editing',
'affine_transform': 'editing',
# queries
'get_nodes': 'queries', 'get_elements': 'queries',
'get_element_properties': 'queries',
'get_fem_data': 'queries',
'get_element_qualities': 'queries',
'quality_report': 'queries',
# partitioning
'partition': 'partitioning', 'unpartition': 'partitioning',
'compute_renumbering': 'partitioning',
'renumber_nodes': 'partitioning',
'renumber_elements': 'partitioning',
'renumber_mesh': 'partitioning',
}
# --- OpenSees methods → sub-composites ---
# Value format: (composite, new_method_name_or_None_to_keep)
OPENSEES_MAPPING = {
# materials
'add_nd_material': ('materials', None),
'add_uni_material': ('materials', None),
'add_section': ('materials', None),
# elements (assign_element renamed to assign)
'add_geom_transf': ('elements', None),
'assign_element': ('elements', 'assign'),
'fix': ('elements', None),
# ingest (consume_*_from_fem renamed)
'consume_loads_from_fem': ('ingest', 'loads'),
'consume_masses_from_fem': ('ingest', 'masses'),
# inspect
'node_table': ('inspect', None),
'element_table': ('inspect', None),
'summary': ('inspect', None),
# export (export_tcl → tcl, export_py → py)
'export_tcl': ('export', 'tcl'),
'export_py': ('export', 'py'),
}
def model_sub_for(m):
if m in MODEL_GEOMETRY: return 'geometry'
if m in MODEL_BOOLEAN: return 'boolean'
if m in MODEL_TRANSFORMS: return 'transforms'
if m in MODEL_IO: return 'io'
if m in MODEL_QUERIES: return 'queries'
return None
pat_model = re.compile(r'(\.model\.)([a-zA-Z_][a-zA-Z_0-9]*)')
# NB: (?<!model\.) skips `gmsh.model.mesh.<method>(`
pat_mesh = re.compile(
r'(?<!model\.)(?<!_)(\bmesh)\.(' +
'|'.join(sorted(MESH_MAPPING, key=len, reverse=True)) +
r')(?=\()'
)
pat_opensees = re.compile(
r'(\bopensees)\.(' +
'|'.join(sorted(OPENSEES_MAPPING, key=len, reverse=True)) +
r')(?=\()'
)
def transform(src):
def model_repl(m):
method = m.group(2)
sub = model_sub_for(method)
return f'.model.{sub}.{method}' if sub else m.group(0)
def mesh_repl(m):
return f'mesh.{MESH_MAPPING[m.group(2)]}.{m.group(2)}'
def opensees_repl(m):
old = m.group(2)
comp, new_name = OPENSEES_MAPPING[old]
name = new_name if new_name else old
return f'opensees.{comp}.{name}'
out = pat_model.sub(model_repl, src)
out = pat_mesh.sub(mesh_repl, out)
out = pat_opensees.sub(opensees_repl, out)
# pyGmsh → apeGmsh (the longer viewer name is caught by the
# prefix match: `from pyGmshViewer` → `from apeGmshViewer`)
out = out.replace('from pyGmsh', 'from apeGmsh')
out = out.replace('import pyGmsh', 'import apeGmsh')
out = re.sub(r'\bpyGmsh\(', 'apeGmsh(', out)
# Viewer-specific names the line above does not catch
out = out.replace('-m pyGmshViewer', '-m apeGmshViewer')
out = out.replace('pygmsh-viewer', 'apegmsh-viewer')
# Renames elsewhere in the v1.0 API
out = re.sub(r'\bg\.mass\.', 'g.masses.', out)
out = re.sub(r'\bfem\.mass\b', 'fem.masses', out)
# FEMData sub-broker restructure
out = re.sub(r'\bfem\.node_ids\b', 'fem.nodes.ids', out)
out = re.sub(r'\bfem\.node_coords\b', 'fem.nodes.coords', out)
out = re.sub(r'\bfem\.element_ids\b', 'fem.elements.ids', out)
out = re.sub(r'\bfem\.connectivity\b', 'fem.elements.connectivity', out)
out = re.sub(r'\bfem\.physical\b', 'fem.nodes.physical', out)
out = re.sub(r'\bfem\.labels\b', 'fem.nodes.labels', out)
out = re.sub(r'\bfem\.constraints\b', 'fem.nodes.constraints', out)
out = re.sub(r'\bfem\.loads\b', 'fem.nodes.loads', out)
out = re.sub(r'\bg\.initialize\(\)', 'g.begin()', out)
out = re.sub(r'\bg\.finalize\(\)', 'g.end()', out)
out = re.sub(r'\bg\.model_name\b', 'g.name', out)
return out
SKIP = {'.git', '__pycache__', 'node_modules', '.venv', 'venv'}
for dirpath, dirnames, filenames in os.walk(sys.argv[1] if len(sys.argv) > 1 else '.'):
dirnames[:] = [d for d in dirnames if d not in SKIP]
for fn in filenames:
if not (fn.endswith('.py') or fn.endswith('.ipynb') or fn.endswith('.md')):
continue
path = os.path.join(dirpath, fn)
with open(path, 'r', encoding='utf-8') as fh:
src = fh.read()
new = transform(src)
if new != src:
with open(path, 'w', encoding='utf-8', newline='') as fh:
fh.write(new)
print(f' updated: {path}')
Save as migrate_v1.py and run: python migrate_v1.py /path/to/your/project
Caveats:
- Backup first. Run it on a clean git branch.
- The (?<!model\.) lookbehind on pat_mesh intentionally skips
raw gmsh.model.mesh.<method>( calls — those are the real Gmsh API
and must stay untouched.
- The regex \bg\.mass\. requires a dot after mass — it will catch
g.mass.point(...) but not a bare g.mass reference. Grep manually
for any leftover bare references.
- gmsh.initialize() / gmsh.finalize() at the raw Gmsh level are
preserved — the script only rewrites g.initialize (our wrapper).
- If your project has custom subclasses of Model or Mesh that
referenced the old mixin classes directly, those need manual updates
(the mixins are gone — replaced with _Geometry, _Generation,
etc. that take a parent reference in __init__).
8. What didn't change¶
- The session lifecycle contract (
g.begin()/g.end()/ context manager) g.parts,g.physical,g.constraints,g.loads,g.mesh_selectioncomposite APIsfem.nodes.ids,fem.nodes.coords,fem.elements.ids,fem.elements.connectivity,fem.nodes.physical,fem.nodes.constraints,fem.elements.constraints,fem.nodes.loads,fem.elements.loads— restructured underfem.nodesandfem.elementssub-brokers (see Section 8a)PartandPartsRegistryAPIs (only docstring examples were swept)g.mesh.field.*— the FieldHelper sub-composite was already in placeg.mesh.viewer()andg.mesh.results_viewer()— the two interactive entry points stay flat ong.meshg.opensees.set_model()andg.opensees.build()— the two OpenSees lifecycle verbs stay flatSelection surface — REMOVED, not unchanged. Earlier revisions of this guide stated the legacy selection surface (
g.model.selection.*,g.model.queries.select(on=/crossing=),g.mesh_selection.add_nodes/add_elements,fem.nodes/elements.get/resolve, the chainresults.*.select().get) was "purely additive, nothing removed". That is no longer true: selection-unification v2 hard-removed all of it. See §6a for the full removal, the migration table, and the two capability gaps. Thecore._selection.Selectionandviz.Selectionclasses are retained by architecture (only their package exports were dropped); the retained typed readerresults.<sub>.get(component=)is the successor to the chain.values()terminal.
9. Full example — old vs new¶
Old (v0.x pyGmsh)¶
from pyGmsh import pyGmsh
g = pyGmsh(model_name="cantilever")
g.initialize()
v = g.model.add_box(0, 0, 0, 1, 0.2, 5)
g.model.translate(v, 0.5, 0, 0)
bb = g.model.bounding_box(v)
g.physical.add_volume([v], name="concrete")
with g.loads.pattern("dead"):
g.loads.gravity("concrete", g=(0, 0, -9.81), density=2400)
g.mass.volume("concrete", density=2400)
g.mesh.set_global_size(0.5)
g.mesh.generate(3)
fem = g.mesh.get_fem_data(3)
print(f"total mass: {fem.mass.total_mass():.0f} kg")
g.finalize()
New (v1.0 apeGmsh)¶
from apeGmsh import apeGmsh
with apeGmsh(model_name="cantilever") as g:
v = g.model.geometry.add_box(0, 0, 0, 1, 0.2, 5)
g.model.transforms.translate(v, 0.5, 0, 0)
bb = g.model.queries.bounding_box(v)
g.physical.add_volume([v], name="concrete")
with g.loads.pattern("dead"):
g.loads.gravity("concrete", g=(0, 0, -9.81), density=2400)
g.masses.volume("concrete", density=2400)
g.mesh.sizing.set_global_size(0.5)
g.mesh.generation.generate(3)
g.mesh.partitioning.renumber(method="rcm", base=1)
fem = g.mesh.queries.get_fem_data(dim=3)
print(f"total mass: {fem.nodes.masses.total_mass():.0f} kg")
Compared to v0.x:
with apeGmsh(...) as g:replacesg.initialize()/g.finalize().- Model methods now live under
geometry,transforms,queries. - Mesh methods now live under
sizing,generation,partitioning,queries. g.massis nowg.masses;fem.massis nowfem.nodes.masses.