Skip to content

Migrating to pyGmsh → apeGmsh v1.0

v1.0 bundles four breaking changes into a single release:

  1. Package rename: pyGmshapeGmsh
  2. Model API restructure: g.model.* methods split into five focused sub-composites (geometry, boolean, transforms, io, queries).
  3. Mesh API restructure: g.mesh.* methods split into seven focused sub-composites (generation, sizing, field, structured, editing, queries, partitioning).
  4. OpenSees API restructure: g.opensees.* methods split into five focused sub-composites (materials, elements, ingest, inspect, export), with set_model and build staying flat.

Plus several smaller cleanups (g.massg.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: pyGmshapeGmsh

-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 pyGmshViewerapeGmshViewer (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:

(g.model.transforms
    .translate(v, 5, 0, 0)
    .rotate(v, angle=np.pi/2, ax=0, ay=0, az=1))

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 (the g.model.selection sub-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 viewer
  • g.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) — unchanged
  • g.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_elementassign (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_tcltcl, export_pypy (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:

g.opensees.export.tcl("model.tcl").py("model.py")

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

-g.model.viewer_fast()   # always fast now
+g.model.viewer()

-g.mesh.viewer_fast()
+g.mesh.viewer()

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_toclosest_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=) — EntitySelection has only spatial verbs + set-ops + to_label/to_physical/to_dataframe. The retained viz.Selection.filter() carries that grammar but is viewer-pick only, not a g.model.select migration 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_selection composite APIs
  • fem.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 under fem.nodes and fem.elements sub-brokers (see Section 8a)
  • Part and PartsRegistry APIs (only docstring examples were swept)
  • g.mesh.field.* — the FieldHelper sub-composite was already in place
  • g.mesh.viewer() and g.mesh.results_viewer() — the two interactive entry points stay flat on g.mesh
  • g.opensees.set_model() and g.opensees.build() — the two OpenSees lifecycle verbs stay flat

    Selection 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 chain results.*.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. The core._selection.Selection and viz.Selection classes are retained by architecture (only their package exports were dropped); the retained typed reader results.<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: replaces g.initialize() / g.finalize().
  • Model methods now live under geometry, transforms, queries.
  • Mesh methods now live under sizing, generation, partitioning, queries.
  • g.mass is now g.masses; fem.mass is now fem.nodes.masses.