Changelog¶
v1.6.0 — Selection-unification v2: one .select() idiom · legacy selection surface REMOVED (BREAKING) · half-open mesh box · loads/masses fail-loud¶
Selection / resolution unification (v2). A single
daisy-chainable .select() idiom is now the only selection idiom
at all four levels — geometry, the live mesh, the FEM broker, and
results. This is a BREAKING change: the legacy selection surface
was removed with no deprecation shim (project-owner-ratified full
removal — v1's backward-compat constraint was explicitly dropped).
Removed: fem.nodes.get / fem.elements.get / .resolve (the
selection accessors), the results.*.select(...).values() chain path,
g.mesh_selection.add_nodes / add_elements / from_geometric,
g.model.queries.select / queries.line / select_all*,
g.model.selection (SelectionComposite), the four legacy *Chain
modules + GeometryChain, and the Selection / SelectionComposite
package exports. The classes core._selection.Selection and
viz.Selection are retained by architecture as internal
terminal-payload / viewer-pick-result types — not user entry
points; only their exports were dropped.
Migrate every legacy call per the old→v2 table and the two
incomplete-unification gaps in
api/selection.md (ADR 0017 supersedes the earlier
ADR-0016 "accepted gap" framing — these are owed v2 successors, not
WONTFIX). Gap 1 (geometric-selection → named mesh-selection): the
capability is intact via the retained 2-call route
(g.model.select(...).to_physical(name) then
g.mesh_selection.from_physical(...)) — only the one-call ergonomic
was lost. Gap 2 (the SelectionComposite filter grammar): a genuine
unique-capability loss for which a v2-native EntitySelection
successor is owed/planned (not a resurrected SelectionComposite).
Two behavior changes ride along. The g.mesh_selection box filter
moves closed → half-open by default to match the results side
(breaking — see below; inclusive=True restores the old closed
box). And the loads/masses __ms__ consumer now fails loud
instead of silently binding to zero nodes — the one remaining member
of a three-path fail-loud end-state whose other two paths landed
earlier (see below).
This release is selection plumbing only — no viewer or solver-bridge surface changed.
ADDED — One fluent .select() idiom at all four levels¶
A new canonical, daisy-chainable selection chain. Entry points, returning a chain that composes fluently:
| Entry point | Terminal | Family | Result |
|---|---|---|---|
g.model.select(target, *, dim=) |
EntitySelection |
entity | .to_label/.to_physical/.to_dataframe/.result() (→ the retained Selection payload) |
fem.nodes.select(...) |
MeshSelection |
point | .result() → NodeResult; .ids/.coords |
fem.elements.select(...) |
MeshSelection |
point | .groups()/.result()/.resolve() → GroupResult |
results.nodes.select(...) / results.elements.select(...) |
MeshSelection |
point | .values(component=, time=, stage=) → slab (forwards onto the retained results.<level>.get(...) reader) |
g.mesh_selection.select(*, level=, dim=, ids=, name=) |
MeshSelection |
point | .result() → same dict as get_nodes/get_elements; .ids; .save_as(name) (live-mesh only) |
- Refining verbs (identical names on every chain):
.in_box(lo, hi, *, inclusive=False),.in_sphere(center, radius),.on_plane(point, normal, *, tol=),.nearest_to(point, *, count=1),.where(predicate). Each returns a new chain of the same concrete type, so they daisy-chain: - Set algebra:
|&-^(and the named aliases.union/.intersect/.difference), insertion-order preserving with one fixed dedup law. Combining two chains of different type or bound to different engines (differentFEMData/Results/ session) raisesTypeError— cross-level mixing is loud, never a silent empty set. - Seeding reuses the existing contract-locked resolvers, never a
re-implementation. Broker chains take the same selectors as
.get(target/pg/label/tag/dim/partition,element_typefor elements) plusids=; no-arg seeds the whole domain. Results chains takepg=/label=/selection=/ids=.g.mesh_selection.selecttakesids=orname=(an existingg.mesh_selectionset, seeded id-for-id by delegating verbatim to the existingget_tag/get_nodes/get_elementssurface — no new resolver, read-only, fail-loud on an unknown name); no-arg seeds the full live-mesh universe.g.model.selectdelegates string resolution to the same label → PG → part tier as everywhere else. - Two spatial families, honestly different — not interchangeable:
- point family (
fem.*,results.*,g.mesh_selection):.in_boxis half-open[lo, hi)by default;inclusive=Truegives the closed box[lo, hi]. Operates on node coordinates (node chains) or element centroids (element chains). - entity family (
g.model.select):.in_boxdelegates to Gmsh'sgetEntitiesInBoundingBox— BRep bounding-box containment (the whole entity bbox must lie inside the query box, expanded byGeometry.Tolerance≈ 1e-8). There is no half-open notion, so passinginclusive=(or any keyword) raisesTypeErrorrather than being silently ignored. For an exact geometric on/crossing predicate useg.model.select(target).crossing_plane(spec, mode="on"|"crossing")(the v2 successor;g.model.queries.selectwas removed).
The two families share verb names and set algebra but never
share .in_box behavior; do not assume a cross-family result is
identical.
- The classes core/_selection.Selection and viz/Selection.Selection
are retained by architecture (the EntitySelection.result()
terminal payload and the viewer pick-result type, respectively) —
structurally distinct internal types, not user entry points;
only their package exports were dropped.
g.model.select(...).result() returns the retained core
Selection payload (.to_label / .to_physical / .to_dataframe
are also direct terminals).
- Named persistence is .save_as(name) on a MeshSelection
(live-mesh engine only), or the retained explicit-ids registrar
g.mesh_selection.add(dim, ids, name=) → FEMData snapshot →
results(selection=...). The removed
g.mesh_selection.add_nodes(..., name=...) / from_geometric
two-step keeps its capability via the retained 2-call route
(g.model.select(...).to_physical(name) →
g.mesh_selection.from_physical(...)); only the one-call ergonomic
was lost (incomplete-unification Gap 1 — see
api/selection.md).
BREAKING — g.mesh_selection box is now half-open by default¶
g.mesh_selection.add_nodes(in_box=), add_elements(in_box=), and
filter_set(in_box=) (the _mesh_filters.nodes_in_box engine)
flipped from closed [lo, hi] to half-open [lo, hi) on the
upper side, per axis. This matches the results side, which was
already half-open — the two diverged on main; this reconciles them
on the canonical (half-open) semantics.
Effect: a node/element coordinate (or centroid) lying exactly on an upper box face is now excluded by default. Adjacent boxes no longer double-count a shared face.
Migration — pass inclusive=True to restore the old closed box:
# Before (pre-v1.6.0): closed [lo, hi] — upper face INCLUDED
sid = g.mesh_selection.add_nodes(in_box=(0, 0, 0, 1, 1, 1))
# v1.6.0 default: half-open [lo, hi) — upper face EXCLUDED.
# On a 3x3x3 unit-cube lattice this drops 27 nodes -> 8.
sid = g.mesh_selection.add_nodes(in_box=(0, 0, 0, 1, 1, 1))
# To keep the exact pre-v1.6.0 result, opt back into the closed box:
sid = g.mesh_selection.add_nodes(
in_box=(0, 0, 0, 1, 1, 1), inclusive=True, # 27 nodes (closed)
)
inclusive= is also accepted on add_elements and filter_set.
Audit any g.mesh_selection box query that intentionally relied on
catching on-the-upper-face nodes; pad the upper corner outward or pass
inclusive=True. Pure-interior boxes are unaffected.
CHANGED — Loads/masses __ms__ now fails loud (completing a three-path end-state)¶
Three paths that once returned a quietly-wrong result now raise; each is safer (a plausible-looking wrong answer becomes an explicit, located error). This release ships only path 2. Paths 1 and 3 were already loud / merged ahead of it and are described here for the complete end-state:
results(...)selection=on an import-origin FEM (already loud — locked). AFEMDatabuilt fromfrom_msh/ MPCO / native input has nomesh_selection(it isNone). Passingselection=against such a FEM raisesRuntimeError("selection= requires fem.mesh_selection to be present.") on both the node and element resolution arms instead of resolving to an empty set. This path was already loud; it is held by a characterization pin and is not changed by this release. Build the selection on the session (g.mesh_selection) so it travels into the snapshot.- Loads / masses
__ms__consumer binding to zero nodes (the change in this release).LoadsComposite._target_nodes(and theMassesCompositecounterpart) hit aif info is None: return set()arm that silently bound a load/mass to zero nodes when a named mesh selection it referenced was gone or the store was inconsistent. It now raisesKeyError("... Refusing to silently bind this load to zero nodes (fail loud)."). A load/mass that resolves to nothing is a model error, not a no-op. This is the one code behavior shipping with this release (with a flipped characterization pin and a dedicated regression test). results._element_centroidscorrupting a centroid (already merged separately — not this release). This routine usednp.clipto map a connectivity node id that is absent from the FEM node set to the last node — silently corrupting that element's centroid. It now raisesKeyError(detecting both past-the-end and in-range-wrong-id, strictly stronger than the old clip), which also makes the legacyresults.elements.in_box/nearest_to/on_planehelpers fail loud on a broken connectivity instead of returning garbage-located elements. The new chain centroids were already fail-loud; this brought the legacy helper to parity. This fix merged independently, ahead of this release — it is not introduced here.
INTERNAL — deduped Loads/Masses target resolver¶
MassesComposite._resolve_target was a byte-for-byte clone of
LoadsComposite._resolve_target. Both now delegate to one shared
core/_resolution.resolve_target engine. Behavior is byte-identical
(tier order, the ("__ms__", dim, tag) sentinel, expected_dim
scoping, multi-dim-PG ValueError, the verbatim per-noun error
messages). No public API change; not a migration. The library-wide
resolvers (FEMData._resolve_one_target, _helpers._resolve_string)
are deliberately untouched.
Follow-ups (tracked, not yet shipped)¶
Consciously deferred; mentioned so they are not mistaken for available surface:
- Results sub-composite
.select()—results.nodes/.elementshave.select(); the five element sub-composites (gauss/fibers/layers/line_stations/springs) do not yet (they need per-terminal kwarg forwarding).
(The g.mesh_selection.select() name-seed that previously sat here
has shipped — see the .select() ADDED section above.)
v1.5.0 — Applied loads + reactions diagrams · geometry-scoped gate¶
One PR landing on top of v1.4.0's post-release work. The ResultsViewer gains two new diagram kinds in the Add layer dropdown — Applied loads (constant force arrows, one diagram per load pattern) and Reactions (recorded reaction forces and moments, auto-scaling per step with the time slider). The composition gate is also tightened so adding a brand-new Geometry no longer leaves the previous Geometry's diagrams visible.
PR in this release: #92.
ADDED — Applied loads diagram (#92)¶
Add layer → Applied loads lists every fem.nodes.loads pattern
that carries at least one non-zero force record. Each diagram
renders force arrows at the resolved nodes for one pattern. Reference
magnitudes only — the broker does not carry the OpenSees
timeSeries function, so step scaling is intentionally deferred
(the diagram's update_to_step is a documented no-op until
timeSeries metadata lands in the broker). Moments are not drawn
yet; they need a different glyph and will follow.
ADDED — Reactions diagram (#92)¶
Add layer → Reactions (enabled iff the file has any
reaction_force_* or reaction_moment_* recordings). Forces render
as straight arrows; moments use the existing moment_glyph curved
arrow. Each family auto-fits its own scale because forces and
torques have different units. The diagram is step-resolved — every
time-slider move re-reads the slab and rebuilds the glyphs. An
auto-filter drops nodes whose magnitude max-over-time is below
zero_tol × global_max, so free-interior nodes don't pollute the
scene with near-zero arrows.
FIXED — Composition gate scoped to active Geometry (#92)¶
The gate previously hid layers using the flat diagram registry, so
adding a new (empty) Geometry kept the previous Geometry's diagrams
visible — active_comp is None set show_all=True and turned every
actor on regardless of which Geometry owned it. The visible-layer
set is now restricted to compositions of the active Geometry; an
empty Geometry shows nothing, an existing one with no active
composition still shows its own layers (preserving the prior
single-Geometry "show all" intent within the active Geometry).
v1.4.0 — ResultsViewer dock split · new-layer attach + lifecycle fixes · import banner¶
Six PRs landing on top of v1.3.0. The post-solve viewer's right rail
splits into dedicated Diagram and Geometry docks (tabified
with Details and Session), with a new floating "?" shortcut HUD on
the viewport. New-layer attach now pushes the active step + re-fires
deformation sync so freshly-added line-force / vector-glyph layers
land aligned with the rest of the scene instead of paint-then-drift.
A series of selection-related composition-gate fixes (Esc, outline
Geometry-row click, stale session restore) stop diagrams from
silently disappearing when the user navigates the outline. The HDF5
reader is now released on viewer close, so re-running a capture
script in the same kernel no longer hits PermissionError. The
package prints an ASCII banner + __version__ on import (suppress
with APEGMSH_QUIET=1) so the running version is unambiguous.
PRs in this release: [#69], [#70], [#71], [#72], [#73], [#74].
ADDED — Diagram / Geometry dock split ([#69])¶
The right rail's single Details dock — which previously stacked the DiagramSettingsTab and GeometrySettingsPanel inside a QStackedWidget — is split into two dedicated docks:
- Diagram dock hosts
DiagramSettingsTab.widgetdirectly. - Geometry dock hosts
GeometrySettingsPanel.widgetdirectly. - Details dock is reserved for future canvas-driven contextual content (contour scalebar edits, picked-node readouts).
- Outline routing: clicking a Composition row raises the Diagram dock; clicking a Geometry row raises the Geometry dock.
- The "+ Add layer" button is now on the Diagram dock's title row.
- Layout schema bumped to v5 — saved v4 layouts are discarded so users land in the new arrangement on first launch.
ADDED — Floating shortcut help HUD ([#69])¶
ShortcutHelpHUD — small "?" button in the viewport's bottom-right
corner. Click pops a list of mapped keyboard shortcuts (Esc, Ctrl+H,
Q, N/E/G, Shift+LMB, Shift+click, F2).
ADDED — Banner + __version__ on import ([#73])¶
import apeGmsh prints the ASCII banner + the installed version to
stderr. __version__ is now exposed at the package root, sourced
from importlib.metadata (single source of truth = pyproject.toml).
Set APEGMSH_QUIET=1 to suppress for tests / CI / piped scripts.
FIXED — New-layer attach ([#69])¶
After registry.add(...), the director now pushes the active step
to the new layer and re-fires _apply_deformation. Resolves:
- Line-force diagrams that rendered as collapsed slivers because step-0 internal forces are zero (the polydata was correct, the values were wrong).
- Vector glyphs that landed at undeformed positions until the user manually scrubbed the time slider.
FIXED — HDF5 reader released on viewer close ([#70])¶
ResultsViewer._on_close now calls self._results.close() so the
NativeReader's file handle is released. Re-running a capture script
in the same Jupyter kernel — which deletes and recreates the same
.h5 path — no longer hits PermissionError: [WinError 32] The
process cannot access the file because it is being used by another
process.
FIXED — Composition gate no longer silently hides diagrams ([#71], [#72], [#74])¶
Three trigger paths surfaced the same symptom — layers visible in the dock with checkboxes still checked, but nothing painting in the viewport — because the composition gate hides every actor when no composition is "active":
- #71: Esc previously called
compositions.set_active(None), which fired the gate. Esc now only clears probe markers + element / GP highlights and leaves composition state alone. - #72: Clicking a Geometry row in the outline previously called
compositions.set_active(None)for the same reason. Selecting a Geometry row is now a navigation gesture; composition state is left unchanged. - #74: The session JSON persists
active_composition_id. Pre-#71 / pre-#72 sessions easily savednull. On restore, the gate then hid every layer until the user clicked a Composition row. Two-part fix: heal stale sessions by defaulting to the first composition when the saved active id is null; relax the gate to "show all" when no composition is active anywhere.
v1.3.0 — ResultsViewer B++ redesign · live recorder + MPCO emission · spatial filters¶
Nine PRs landing on top of v1.2.0. The ResultsViewer ships a full B++
redesign — outline tree, plot pane, viewport HUDs (probe palette top-
right, pick-readout top-left), inline kind picker, style presets,
density toggle — replacing the right-dock tab strip. The recorder
spec gains two new in-process execution strategies (emit_recorders
and emit_mpco), so one declarative spec now drives five backend
paths. The read side picks up nearest_to / in_box / in_sphere /
on_plane spatial filters plus an element_type= selector, all
composing additively with the existing pg= / label= /
selection= / ids= vocabulary. Elastic beams (ElasticBeam{2d,3d},
ElasticTimoshenkoBeam{2d,3d}, ModElasticBeam2d) gain a synthesised
2-station line-stations slab via the live capture path, matching the
existing MPCO behaviour. Documentation gets a 6-card landing, grouped
navigation, an 8-notebook curated examples gallery rendered inline
via mkdocs-jupyter, a Recorder reference page, and a Reading &
filtering results guide. All 9 PRs merged green.
PRs in this release: #43, #44, #46, #47, #48, #50, #51, #52, #49.
ADDED — ResultsViewer B++ redesign (#43, #46)¶
Closes the B++ Implementation Guide. The right-dock tab strip (Stages / Diagrams / Settings / Inspector / Probes) is retired in favour of a 3×3 grid layout: title-bar row (40 px), three-column body (left rail · viewport · right rail), scrubber row (84 px).
ResultsWindowshell (#43 B0). WrapsViewerWindowwith the 3×3 grid central widget. Hidden left (260 px) and right (380 px) columns reserve space for the upcoming widgets.OutlineTree(#43 B1). Left-rail single navigator with four groups: Stages, Diagrams, Probes, Plots. Replaces the StagesTab and DiagramsTab. Visibility checkboxes toggle render; clicks drive the details panel.PlotPane+DetailsPanel(#43 B2). Right-rail vertical-list tabs (dot · label · ×). Re-homes the fiber-section, layer- thickness, and time-history panels that previously floated asQDockWidgets on the main-window edges. The DetailsPanel below hosts contextual content (DiagramSettingsTab when a diagram row is selected).ProbePaletteHUD(#43 B3). Floating panel in the viewport's top-right corner with three mode buttons (Point / Line / Slice) + Stop / Clear. Repositions on viewport resize via a Qt event filter. RetiresProbesTab.PickReadoutHUD(#46). Floating glass card in the viewport's top-left corner. Subscribes toProbeOverlay.on_point_resultand Director step / stage changes; renders the picked node id, snapped coords, and one mono-typed line per active component value. RetiresInspectorTab.- Shift-click → time-history plot (#46).
ShiftClickPickerregisters a low-priority VTK observer onLeftButtonPressEventthat fires only when shift is held; opens (or focuses) aTimeHistoryPanelas a closable plot-pane tab. The default component prefers the active diagram's selector. - Title-bar utility strip (#46). Three decorative stop-light
dots, breadcrumb label, right-aligned icon strip with theme cycle,
clipboard screenshot, density toggle, help dialog. Theme cycles
through every palette in
PALETTES. - Density toggle (#46). New
DensityManagersingleton mirroringThemeManager. Persists viaQSettings.DensityTokenscarryrow_h,pad_x,pad_y,gap,fs_body,fs_head. The global stylesheet picks them up; toggling triggers a full restyle. - Two-way tree ↔ plot-tab binding (#46). The Plots group in the outline tree mirrors the plot-pane tab list; clicking a Plots row activates the matching tab. Empty Plots group falls back to a hint placeholder.
- Inline 2×4 kind picker (#46). Clicking the outline tree's
"+ Insert" button reveals a 2×4 grid of diagram-kind shortcuts
directly under the header. Selecting a kind opens
AddDiagramDialogpre-selected for that kind. - Diagram picker pre-flight (#46). The Add Diagram dialog now
greys out kinds whose topology has no data anywhere in the
Results file (
— no datasuffix). The Component combo placeholder distinguishes "no data in file" from "no data in selected stage". - Style presets (#46). New module
[
viewers/diagrams/_style_presets.py] withstyle_to_dict/style_from_dictcodec,KIND_TO_STYLE_CLASSregistry, and aStylePresetStore(CRUD under<QSettings AppConfigLocation>/ apeGmsh/style_presets/). Add Diagram dialog gains a Preset combo;DiagramSettingsTabgains a Save…/Apply footer. Path-traversal sanitiser refuses unsafe names. - Theme + global-preferences reachability (#46). The
ResultsWindow help dialog promotes from
QMessageBoxto a properQDialogwith footer buttons that open the Theme editor and Global preferences dialogs (the dock-strip path the other viewers use is gone in B5). - Theme integration for the new shell (#43 B4). Hardcoded
inline stylesheets removed; the global
build_stylesheetpicks up object-name selectors for every new widget (#ResultsTitleBar,#OutlineHeader,#PlotPaneHeader,#DetailsPanel,#ProbeHUD,#OutlineKindPicker,#OutlineKindBtn, etc.). All four palettes (catppuccin_mocha, neutral_studio, catppuccin_latte, paper) render cleanly.
ADDED — Live recorder + MPCO emission strategies (#48)¶
Two new in-process consumers on the recorder spec — same seam, two new code paths:
spec.emit_recorders(out_dir)— classic recorders pushed live into theopsdomain viaops.recorder()calls, withbegin_stage/end_stagescoping and per-stage filename prefixes (<stage>__<record>_<token>).spec.emit_mpco(path)— single in-process MPCO recorder with a build-gate that raises with a clear remediation pointer when the active openseespy build doesn't include MPCO.- Threads
stage_idthrough emit → cache → transcoder →from_recorders(defaultNonepreserves byte-for-byteexport.tcl/pycompatibility). to_ops_args/mpco_ops_argsare the live-emit equivalents offormat_python/emit_mpco_python. Both flow through the existingLogicalRecorderdataclass so source-form and tuple-form share one source of truth.- Architecture doc rewritten:
apeGmsh_results_obtaining.mdcovers the spec-as-seam pattern with the five-strategy comparison table. - New user-facing guide:
guide_obtaining_results.mdwith worked recipes per strategy + decision flowchart + pitfalls. - 46 new tests; full recorder/live/mpco sweep at 495 passing.
ADDED — Spatial filters on every read-side composite (#51)¶
Ergonomic spatial selection lands on every composite that returns slabs:
| Filter | Semantics |
|---|---|
nearest_to(point, component=…) |
Single nearest entity to the query point |
in_box(box_min, box_max, …) |
Half-open on the upper side: [box_min, box_max) so adjacent boxes don't double-count shared faces. Use np.inf to relax an axis |
in_sphere(center, radius, …) |
Closed ball |
on_plane(point_on_plane, normal, tolerance, …) |
Absolute distance ≤ tolerance |
Available on results.nodes plus the six element-level composites
(results.elements, .elements.gauss, .line_stations, .fibers,
.layers, .springs) via the shared _ElementGeometryMixin.
Element-side queries use centroids computed lazily from the FEM's
node coordinates + per-type connectivity, robust to mixed-type
meshes.
- Filters compose additively. Spatial primitives intersect with
each named selector (
pg=/label=/selection=/ids=/element_type=) the same way: element_type=selector on every element-level composite. Restricts the candidate set by broker element-type name ("Tet4","Hex8","Quad4", etc.). Resolves viafem.elements.typesandfem.elements.resolve(element_type=…).- Verbose parameter names per project preference:
point(wasxyz),box_min/box_max(wasp_min/p_max),center,radius,point_on_plane,normal,tolerance. - New user-facing guide:
guide_results_filtering.md— 12 sections covering the composite tree, selectors menu, geometric helpers, additive composition, slab shapes, time slicing, stage scoping, discovery API, worked recipes, pitfalls, and what's queued. - 21 spatial tests + 18 existing composite tests pass.
ADDED — Elastic-beam line-stations synthesis (#47)¶
The Phase 11b live-capture path previously required a force- or
disp-based beam-column section.force probe; closed-form elastic
beams (ElasticBeam{2d,3d}, ElasticTimoshenkoBeam{2d,3d},
ModElasticBeam2d) were silently dropped because they have no
integration points. The MPCO read path already synthesises a
2-station slab from the localForce bucket — this commit ports the
same synthesis to the live DomainCapture path.
- New
synthesize_line_station_layout_for_elastic_beam(class_name)in [solvers/_element_response.py] — builds a 2-stationResponseLayoutat ξ ∈ {-1, +1} for any class with aNODAL_FORCE_CATALOGentry, using canonical line-station component names. Companionis_line_station_synthesis_cataloguedpredicate. _LineStationGroupgains amodefield;_probe_elementsplits into_probe_section_force(existing) and_probe_local_force_synthesis(new)._step_local_forcereadsops.eleResponse(eid, "localForce")once per step and applies the standard sign flip on station 2 so the slab matches section-force convention (mirrors the MPCO path).- Loud skip warning.
DomainCapture.end_stagenow emits a single consolidatedUserWarningif any elements were dropped from line-stations recording, listing the breakdown by reason and a sample of element IDs. Steers the user to MPCO or to rebuilding asForceBeamColumninstead of letting the silent skip turn into a confusing empty diagram. examples/EOS Examples/example_buckleUP_v2.ipynbgains arecs.line_stations(...)call so itselasticBeamColumnbraces / columns / beams produce a real line-stations slab theLineForceDiagramcan render.
ADDED — Recorder vocabulary discovery (#50)¶
The recorder vocabulary is now discoverable from three surfaces, all sharing one source of truth:
- Method docstrings on
Recorders.{nodes, elements, line_stations, gauss, fibers, layers, modal}enumerate every canonical component, every shorthand expansion, the selector vocabulary, the cadence options, coverage caveats per execution strategy, and a worked example. mkdocstrings auto-renders these onapi/opensees.md, so the API page now shows the full menu. - Static introspection methods: Useful at the REPL, in notebooks, for validation messages.
- New reference page at Guides › Results › Recorder reference
(
guide_recorders_reference.md) — single-page menu with categories at a glance, shared selectors and cadence, then per-method tables of components and shorthands. - 16 new introspection tests; full recorder sweep at 158 passing.
DOCS — 6-card landing, grouped nav, examples gallery (#49, #52, #44, [affa81d])¶
- 6-card landing (#49) replaces the 2-card grid on the docs home page. Cards are organised by user intent: First steps, Quickstart & Examples, Build a model, Run & read results, Architecture, API reference. Plus a 3-card "What's new" band.
- Grouped navigation (#49).
mkdocs.ymlreorganises the bloated Guides and Architecture sections into collapsible sub-headings: Getting started / Building models / Physics / Solver bridge / Results / Reference under Guides; Foundations / Subsystems / Gmsh background under Architecture. No content moves. - Curated examples gallery (#49) — 8 EOS notebooks rendered
inline via mkdocs-jupyter at
/examples/notebooks/<name>/.docs/_hooks.pycopies notebooks fromexamples/EOS Examples/todocs/examples/notebooks/on every build (mtime-aware so incremental rebuilds are fast); source of truth stays inexamples/EOS Examples/. - 8 hero notebooks modernised (#52) — single-flow apeGmsh-Results pedagogical template:
- Imports + parameters
- Geometry (apeGmsh)
- Physical groups
- Mesh
- OpenSees model — vanilla openseespy
- Declare recorders — apeGmsh
- Run analysis with
spec.capture(...)orspec.emit_recorders(...) - Read results back via
Results - Plot in-notebook
- Optional viewer (subprocess,
APEGMSH_SKIP_VIEWERhonoured)
Strategy assignments mixed across the curriculum:
01_hello_plate, 04_portal_frame_2D, 05_labels_and_pgs,
12_interface_tie, 17_modal_analysis, 19_pushover_elastoplastic
use spec.capture; 02_cantilever_beam_2D, 10b_part_assembly
use spec.emit_recorders. All 8 verified end-to-end via
nbconvert --execute against closed-form solutions.
- Gallery page refresh (affa81d). Each card calls out the
strategy used (spec.capture vs spec.emit_recorders), names the
verification target, and points at the specific pedagogical
moment that notebook teaches that others don't. New "Common
shape across every notebook" section makes the unified template
visible at the gallery level.
- 31 EOS notebooks wired (#44) with a "Capture results"
section before g.end(), providing two pedagogical paths:
manual NativeWriter and declarative
Recorders().nodes(...) → DomainCapture context. New
scripts/wire_eos_notebook.py (auto-wiring) and
scripts/migrate_eos_legacy_api.py (API-drift migrator) for
future notebooks. Both paths gate the viewer launch on
APEGMSH_SKIP_VIEWER for headless / nbconvert / CI runs.
Notebooks with pre-existing breakage moved to to_review/ with a
README explaining each.
Test coverage¶
| PR | New tests | Notes |
|---|---|---|
| #43 | — | Reorganises existing test infra (B0–B4 widget tests track the migration) |
| #46 | ~30 | HUD construction + callbacks, density manager, style presets CRUD, picker pre-flight |
| #47 | 4 | Elastic-beam round trip 3D / 2D, skip-warning fires once, clean-recording no-warning |
| #48 | 46 | Recorder/live/mpco sweep at 495 passing |
| #50 | 16 | Static introspection — categories / components_for / shorthands_for |
| #51 | 21 | nearest_to / in_box / in_sphere / on_plane / element_type semantics + additive intersection |
| Total | ~117 |
v1.2.0 — Results viewer: Gauss contour, scrubber animation, shape-function catalog¶
Six PRs landing on top of v1.1.0's Results rebuild. ContourDiagram
gains two new rendering paths so element-level Gauss data flows
straight through the dialog → diagram → substrate pipeline; the time
scrubber gets real Play / FPS / loop controls; the shape-function
catalog grows from 5 to 12 element types so quadratic and prism
meshes are first-class. All 6 PRs merged green — 377 viewer
tests + 1876 non-viewer tests pass on main.
PRs in this release: #35, #36, #37, #38, #39, #42.
ADDED — ContourDiagram Gauss paths¶
- Element-constant Gauss contour (#37). New
gauss_cellrendering path activated whenn_gp == 1per element (CST / tri31, hex8 with one-point integration, etc.). Reads viaresults.elements.gauss.get(component=...), paintscell_dataon a substrate submesh extracted by element IDs. Removes the manual nodal-averaging step the plate notebook was using. - GP→nodal extrapolation for higher-order integration (#39).
New
gauss_nodepath activated whenn_gp > 1. The slab is projected onto the linear-corner shape functions via the Moore–Penrose pseudo-inverse (pinv(N)), then accumulated into a per-node sum + count for cross-element averaging. New module [apeGmsh.results._gauss_extrapolation] with two public entry points:extrapolate_gauss_slab_to_nodes(slab, fem)andper_element_max_gp_count(slab). ContourStyle.topologyfield (#37). User-facing knob with three values:"auto"(default) — prefer nodal data when both composites have the requested component; fall through to Gauss otherwise."nodes"— force the nodal-scalar path (point data)."gauss"— force the Gauss path; cell-vs-node sub-decision is made internally based onn_gp.
- Topology dropdown in the Add Diagram dialog (#38). Visible
only when the selected kind is Contour. The Component combo
populates from the union of nodes + gauss components under
"auto", and from the picked composite under"nodes"/"gauss".
ADDED — Time scrubber animation (#36)¶
- Play button drives a
QTimerat1000 / fpsms; each tick advances one step viadirector.set_step(...). The scrubber stays slider-passive — it only updates the slider via the Director'son_step_changedcallback, never directly. - FPS spinner (1–60, default 30). Live — changing it while playing updates the timer interval without disturbing the run.
- Loop modes combo:
"once"(stop at last step),"loop"(wrap to step 0),"bounce"(reverse direction at boundaries, never wraps). - Stops on stage change automatically; a fresh stage may have a different step count, so the scrubber refreshes and waits for the user to press Play again.
ADDED — Shape-function catalog expansion (#42)¶
SHAPE_FUNCTIONS_BY_GMSH_CODE grows from 5 entries to 12. New types
covering everything you'd hit by setting
gmsh.model.mesh.setOrder(2) plus wedge6 for extruded / layered
meshes:
| Code | Type | Notes |
|---|---|---|
| 6 | wedge6 | Linear prism (tri × line tensor product) |
| 9 | tri6 | Quadratic triangle |
| 10 | quad9 | Lagrangian biquadratic quad |
| 11 | tet10 | Quadratic tet |
| 12 | hex27 | Lagrangian triquadratic hex |
| 16 | quad8 | Serendipity quadratic quad |
| 17 | hex20 | Serendipity quadratic hex |
Node orderings match Gmsh's published convention (cross-checked
against the ASCII diagrams in gmsh-4.15.1/src/geo/M{Triangle,
Quadrangle,Tetrahedron,Hexahedron,Prism}.h), so a connectivity row
read straight from a Gmsh-generated mesh works without any
reordering. Pyramids (pyr5 / pyr14) and line3 are deliberately
out of scope — pyramids have a known apex singularity worth avoiding
for a first pass, and line3 is rare in OpenSees output.
For GP→nodal extrapolation the higher-order types fall back to
their linear counterpart (tri6 → tri3, quad8/9 → quad4,
tet10 → tet4, hex20/27 → hex8). Reasons: the substrate is built
from linear cells (mid-side / face / center nodes are dropped in
build_fem_scene), so non-corner extrapolations are never painted;
and pinv on the full higher-order N matrix produces a
non-constant nodal field for a constant GP input
(minimum-norm regularization of the under-determined system),
which is wrong for visualization.
REFACTORED — single-source diagram topology routing (#35)¶
- New
Diagram.topology: strclass attribute; each subclass declares it next tokind. The Add Diagram dialog's_KIND_TO_TOPOLOGYtable is now derived from those attributes: Previously the dict was hand-maintained alongside the per-class composite-reader calls and could drift.
CHANGED — ContourDiagram internals¶
- Three internal effective-topology values replace the previous two:
"nodes"/"gauss_cell"/"gauss_node". Dispatch is decided at attach time after a single step-0 read used both for the n_gp probe and the initial scatter. - Cross-element discontinuities are smoothed by the nodal averaging
in the
gauss_nodepath. Standard post-processor behaviour (STKO, ParaView). A future per-element subdivision path can preserve discontinuities — out of scope here, thegauss_cellscaffolding stays in place to keep that door open.
Test coverage¶
| PR | New tests | Notes |
|---|---|---|
| #35 | 2 | Pinning test for the eight kind→topology mappings |
| #36 | 10 | State-machine + timer + stage-change-stop coverage |
| #37 | 10 | Auto resolution, attach, in-place mutation, multi-GP rejection (later refactored to extrapolation in #39) |
| #38 | 11 | Visibility per kind, component listing per topology, end-to-end run() spec construction |
| #39 | 11 | Linear-field round-trip on hex8 + 2×2×2 GPs (atol=1e-12), shared-face averaging, time-axis preservation, in-place mutation on the new path |
| #42 | 37 | 5 invariants × 7 types: Kronecker delta, partition of unity, linear precision, dN-sum, FD cross-check |
| Total | 81 |
Known follow-ups (not scheduled)¶
- Discontinuity-preserving Gauss contour — subdivides each multi-GP element into linear sub-cells, samples at sub-vertices, renders cell-data per sub-cell. Preserves jumps at material interfaces. ~500–800 LOC, design-discussion-first decision.
- Hex27 face/center node ordering — verified self-consistent in
the shape-function math, but the assumed Gmsh ordering for nodes
20–26 isn't independently validated against a real Gmsh hex27
mesh. One-row fix in
_HEX27_LAGRANGE_INDEXif a real mesh surfaces a mismatch. - Pyramid shape functions —
pyr5andpyr14. Add when needed.
v1.1.0 — Results: backend-agnostic FEM post-processing system rebuild¶
Wholesale rebuild of the apeGmsh.results module. The legacy
in-memory Results carrier (a thin VTK-feeder bound to live numpy
arrays) is replaced by a lazy disk-backed reader plus a unified
composite API that mirrors FEMData. Recording flows through three
execution strategies — Tcl/Py recorders, in-process domain capture,
and an MPCO bridge — all driven by one declarative
g.opensees.recorders spec. 987 tests pass end-to-end including
the El Ladruno OpenSees Tcl subprocess integration. See PR #12 and
internal_docs/Results_architecture.md for full design.
ADDED — apeGmsh.results¶
- Native HDF5 schema + I/O.
NativeWriter/NativeReaderround-trip nodes, Gauss points, fibers, layers, line stations, and per-element forces. Stages are first-class (kind="transient"/"static"/"mode"). Multi-partition stitching transparent to the reader. Embedded FEMData snapshot in/model/— includingMeshSelectionStore— so result files are self-contained. Resultscomposite class mirroringFEMData. Selection vocabularypg=/label=/selection=/ids=. Stage scoping viaresults.stage(name); mode access viaresults.modes. Soft FEM coupling with hash-validatedbind().compute_snapshot_id(fem)deterministic content hash — ties recorder specs ↔ result files ↔ FEMData snapshots.- MPCO reader.
Results.from_mpco(path)reads existing STKO.mpcofiles through the same composite API. Partial FEMData synthesis from MPCOMODEL/group (nodes + elements + region-derived PGs). g.opensees.recordersdeclarative spec composite. Standalone class, no parent ref, no gmsh dependency..nodes,.elements,.line_stations,.gauss,.fibers,.layers,.modaldeclaration methods.spec.resolve(fem, ndm, ndf)expands shorthand components, validates per category, locksfem_snapshot_id.- Three execution strategies driven by the spec:
- Strategy A —
g.opensees.export.tcl/py(..., recorders=spec)emitsrecorder Node/Element ...commands + HDF5 manifest sidecar.Results.from_recorders(spec, output_dir, fem=fem)parses output.outfiles into native HDF5 with a cache layer at<project_root>/results/. - Strategy B —
with spec.capture(path, fem) as cap:wraps an openseespy analyze loop, queryingops.nodeDispetc. per record. Multi-record merge with NaN-fill when records target disjoint node sets.cap.capture_modes()writes one mode-kind stage perops.eigenmode. - Strategy C —
recorders_file_format="mpco"dispatches to a singlerecorder mpcoline aggregating all records. Validated via subprocess against the El Ladruno OpenSees Tcl build. - Element capability flags on
_ElemSpec:has_gauss,has_fibers,has_layers,has_line_stationsplus asupports(category)helper. All 16 entries in_ELEM_REGISTRYannotated. MeshSelectionStorename-based lookups:names(),node_ids(name),element_ids(name)— mirrorsPhysicalGroupSet's API.- Architecture doc
internal_docs/Results_architecture.md(single canonical reference).
CHANGED — Results module API (BREAKING)¶
Results.from_fem(fem, point_data=..., cell_data=...)removed. UseResults.from_native(...),from_mpco(...),from_recorders(...), or hand-construct viaNativeWriterfor the in-memory case.fem.viewer()raisesNotImplementedErroruntil the viewer rebuild project. The new flow will go through the rebuilt composite API.g.mesh.viewer(point_data=..., cell_data=...)raisesNotImplementedError. The mesh-only paths (g.mesh.viewer()andg.mesh.viewer(results=path)for a.vtu/.pvdfile) still work unchanged.- Public exports under
apeGmsh.results:Results,ResultsReader,NativeReader,MPCOReader,ResultLevel,StageInfo,BindError, and the slab dataclasses (NodeSlab,ElementSlab,LineStationSlab,GaussSlab,FiberSlab,LayerSlab).
DEFERRED — element-level transcoding¶
Element-level records (gauss / fibers / layers /
line_stations / elements) work end-to-end on the declaration
and emission side. The read/decode side is stubbed:
MPCOReader.read_gauss/fibers/layers/...returns empty slabs.RecorderTranscoderskips element records silently.DomainCapture.step()raisesNotImplementedErrorfor element categories.
All three need the same missing piece: a per-element-class
response-metadata catalog. Plan in
internal_docs/plan_element_transcoding.md (Phase 11a).
Nodal results work everywhere today.
NEW FIXTURE¶
tests/fixtures/results/elasticFrame.mpco— 400 KB binary, 12 nodes / 11 elastic frame elements / 10 transient steps / 2 model stages. Used by the MPCO reader + integration tests.
v1.0.9 — Viewer: higher-order rendering + filter overhaul (WIP)¶
Lands the viewer fixes and refactor scaffolding from PR #11. Higher- order elements (Q8/Q9, Tri6, Tet10, etc.) no longer render as VTK's sub-triangle tessellation fans. The dim filter actually hides actors now, and node display scopes to the dim filter. Step 5 (corner / midside / bubble node differentiation) is deferred to the next release.
FIXED — viewer¶
- Q9 / higher-order surface fill — the fill layer is now built from
linearized corner-only cells (
mesh_scene.GMSH_LINEAR), so a Q9 quad renders as a single quad and a Tri6 as a single triangle. 31 element types covered including P3 / P4 and bubble variants; unknown types warn instead of being silently dropped. - Dim filter (1D/2D/3D checkboxes) — was overridden in
mesh_viewer._on_mesh_filtersetup so it only set the pick mask; now also flips fill / wire / node-cloud actor visibility per dim. - Phantom wireframe on Reveal —
VisibilityManager._rebuild_actorsnow rebuilds the wire actor alongside the fill on hide / reveal, so hidden entities lose their edges and revealed entities regain them. - BRep surface fill for higher-order meshes —
brep_scenegot the same linearization treatment for Tri6 / Quad8 / Quad9.
CHANGED — viewer¶
- New wireframe layer built via
extract_all_edges()per dim>=2, registered asEntityRegistry.dim_wire_actors. Replaces VTK's built-inshow_edges(which rendered the higher-order cell tessellation, not the FE element boundary). Clipping plane, visibility manager, and dim filter all participate. - Per-dim node clouds — single global
node_actorreplaced byEntityRegistry.dim_node_actorskeyed by dim, each containing the nodes used by entities of that dim (withincludeBoundary=True). The dim filter now scopes node display: unchecking 1D drops 1D-only nodes, but boundary nodes shared with a visible 2D dim stay. - Tree right-click Hide / Isolate / Reveal-all — added to BRep
SelectionTreePanelandBrowserTab(group + entity rows). Backed by newVisibilityManager.hide_dts(dts)/isolate_dts(dts)programmatic counterparts of the pick-drivenhide()/isolate().
ADDED — viewers.core.visibility doc¶
- Spelled out the filter state model in the module docstring:
cosmetic dim toggle (
SetVisibility), entity hide (VisibilityManager._hidden), and clipping (render-time) are three independent layers, intentionally not unified.
v1.0.8 — Embedded-node constraint resolver (ASDEmbeddedNodeElement)¶
Closes Phase 11b. Replaces the NotImplementedError on
ConstraintsComposite._resolve_embedded with a working resolver, so
embedded-rebar and similar non-conforming inclusions can be expressed
without fragmenting the host mesh.
ADDED — solvers/_constraint_resolver.py¶
_barycentric_tri3(p, p0, p1, p2)— barycentric coordinates ofpinside a 2D triangle, with projection onto the triangle's plane for off-plane points._barycentric_tet4(p, p0, p1, p2, p3)— same for a 3D tetrahedron.ConstraintResolver.resolve_embedded(...)— given embedded nodes and host element connectivity, locates each embedded node in its host via KD-tree spatial indexing + barycentric coordinates, then emitsInterpolationRecordshape-function couplings that matchASDEmbeddedNodeElementkinematics.
ADDED — integration¶
ConstraintsComposite._resolve_embeddednow dispatches to the new resolver, collects host element connectivity from a labeled master region (tri3 / tet4), filters out embedded nodes that coincide with host corners, and returns the coupling records.examples/EOS Examples/15_embedded_rebar.pyrewritten to use the embedded path instead of the old fragment-based conformal rebar.
ADDED — tests¶
tests/test_constraint_resolver.py— 4 cases (tri3 interior + corner, tet4 centroid, multi-element search).
ADDED — regression coverage¶
tests/test_target_resolution.py— locks inFEMData.nodes.get()/.elements.get()target=precedence (label > PG) and raw[(dim, tag)]passthrough.tests/test_boolean_ops.py— guards the 2Dfragment(cleanup_free=True)bug so it can't regress.tests/test_parts_advanced.py— coversg.parts.add(part, label=..., translate=...)on an unlabeled Part (no sidecar).
ADDED — infrastructure¶
pyproject.toml[tool.pytest.ini_options]withpythonpath = ["src"], so pytest run from a worktree picks up the worktree's source instead of the editable install pointing at the main checkout.
v1.0.7 — Selection upgrades + set_transfinite_box¶
Polish pass on the v1.0.6 selection API. Eliminates the hand-rolled
patterns that kept showing up in scripts (two-step boundary queries,
_apply_hex helpers, manual node-count-by-axis loops) and adds the
predicates and combinators users were reaching for.
ADDED — boundary helpers¶
g.model.queries.boundary_curves(tag)— returns every unique curve on the boundary of an entity. Encapsulates the two-step query (faces → individual face boundaries withcombined=False→ deduplicate) that's needed because Gmsh'sgetBoundary(vol, recursive=True)skips dim=1 and goes straight to vertices. Accepts a label, PG name, int tag, dimtag, or list of any.g.model.queries.boundary_points(tag)— symmetric helper for the eight corner points of a volume.
ADDED — select() upgrades¶
select()now accepts a label string with adim=keyword:select('box', dim=2, on={'z': 0})resolves the label, walks to dim 2, and applies the predicate — no manualboundary()call beforehand.not_on=andnot_crossing=negation predicates. Same signed-distance computation as the positive forms; useful for all faces except the bottom style queries.- The
Selection.to_label()call on a mixed-dim selection no longer triggers the labels-composite collision warning — using the same name across multiple dims is the documented intent here.
ADDED — Selection ergonomics¶
- Set operations:
selection | other(union with deduplication),selection & other(intersection),selection - other(difference). All three preserve the back-reference to_Queriesso chaining keeps working. selection.partition_by(axis=None)groups entities by their dominant bounding-box axis. Returns adict[str, Selection]keyed by'x','y','z', or — ifaxis=is given — a singleSelection. Semantics are dim-aware:- curves group by the largest extent (curve direction);
- surfaces group by the smallest extent (perpendicular / normal direction).
ADDED — primitive factories¶
g.model.queries.plane(z=0),plane(p1, p2, p3), andplane(normal=..., through=...)build aPlaneyou can pass to anyselect(on=..., crossing=...)call (positive or negated).g.model.queries.line(p1, p2)builds aLine.- Define a primitive once, reuse across many selections — useful when the same cutting plane appears in several queries.
ADDED — set_transfinite_box¶
g.mesh.structured.set_transfinite_box(vol, *, size=None, n=None, recombine=True)— collapses the full transfinite-hex setup (curve node counts, face transfinite + recombine, volume transfinite) into a single call. Accepts eithersize=(target element size; node counts derived per edge fromround(length / size) + 1) orn=(uniform node count per edge). Passrecombine=Falsefor a transfinite tet mesh instead of hex.
CHANGED¶
examples/EOS Examples/22_geometric_selection.ipynbrewrites the 3-D section to useset_transfinite_box,select('box', dim=2, ...), theplane()factory,not_on=, set operations,partition_by, and chainedto_label / to_physical— every v1.0.7 feature is exercised.
FIXED¶
g.model.queries.bounding_box(tag, dim=N),center_of_mass(tag, dim=N), andmass(tag, dim=N)now honourdimas an explicit hint whentagis a bare integer. Previously these went throughresolve_to_single_dimtag→resolve_dim, which always searches dimensions 3 → 0 and returns the first match — so on a model containing both volume 1 and curve 1,bounding_box(1, dim=1)silently returned the volume's bounding box. Bare ints are now passed straight to the corresponding Gmsh OCC call at the requested dim. String labels and(dim, tag)tuples still go through resolution.g.model.geometry.slicenow passes its plane reference as an explicit(2, plane_tag)dimtag to the downstreamcut_by_surface/cut_by_planecalls. Previously it passed a bare int, which triggeredresolve_dimto scan the live Gmsh model — and becauseadd_axis_cutting_planeis called withsync=False, the new plane wasn't yet visible togetEntities(2), causing the resolver to fall through to the curves and fail with"surface ref N resolved to dim=1". Together these two fixes recover ≈14 previously-failing tests intest_geometry_cutting,test_sections, andtest_part_anchors.
INTERNAL¶
- New
_Queries._resolve_to_dimtags(tag)helper consolidates the string / int / dimtag resolution path used byboundary_curves,boundary_points, and the newselect()label-string branch. _select_implnow takesnot_on/not_crossingand inverts the predicate result (hit ^ invert). The four kwargs are mutually exclusive — exactly one must be passed.Selectionis parameterised onDimTag(atuple[int, int]alias). Method signatures use proper type hints throughout.- 29 new test cases in
tests/test_selection.pycovering the new helpers, the negation predicates, set operations,partition_byfor both curves and surfaces, the primitive factories, andset_transfinite_box. Total: 55 cases passing.
v1.0.6 — Geometric selection API (g.model.queries.select)¶
ADDED¶
g.model.queries.select(entities, on=..., crossing=...)filters curves, surfaces, or volumes by a geometric predicate. Replaces the noisyentities_in_bounding_box(xmin,ymin,zmin,xmax,ymax,zmax)pattern with a readable description of what you want.- Predicates work on the bounding-box corners of each candidate:
on=— every corner withintolof the primitive (entity lies on it).crossing=— corners exist on both sides of the primitive (entity straddles it). Same signed-distance computation underlies both.- Primitive formats — no imports needed:
{'z': 0}→ axis-aligned plane z = 0.[(p1), (p2)](2 points) → infinite line, for cutting 2-D geometry.[(p1), (p2), (p3)](3 points) → infinite plane through 3 non-collinear points. Use for surfaces and volumes.select()returns aSelection— alistsubclass with three chainable methods:.select(...)— filter further (AND logic when stacked)..to_label(name)— register every entity as a label, grouped by dimension..to_physical(name)— register every entity as a physical group, grouped by dimension. Each returnsselfso you can keep chaining:select → label → select again → physical.Selection.__repr__describes the count by dimension and reminds the user how to chain — IDE autocomplete + the repr are the only discovery surface needed.- New curriculum notebook
examples/EOS Examples/22_geometric_selection.ipynbwalks the full workflow: predicate intro → stacking → unstructured baseline → transfinite quad mesh of a plate → 3-D hex of a box. - Companion script
examples/example_unstructured_and_transfinite.pyshows the unstructured-vs-transfinite contrast for two adjacent boxes. - API docs page extended at
docs/api/model.mdwithSelection,Plane, andLine(the latter two documented as internal but exposed so the format reference is auto-generated from docstrings).
INTERNAL¶
- New module
src/apeGmsh/core/_selection.pyholdsPlane,Line, the_parse_primitivedispatcher, the_select_implcore, and theSelectionclass.PlaneandLineare not part of the public API — they are only constructed by_parse_primitivefrom raw user input passed toselect(). Selectioncarries a back-reference to the originating_Queriesso.select(),.to_label(), and.to_physical()can route to the session'slabels/physicalcomposites without the user having to thread context.- Tests in
tests/test_selection.py(26 cases) cover predicates in 2-D and 3-D, primitive parsing (including degenerate / collinear / coincident input), the Selection chain, label / PG registration for both single-dim and mixed-dim selections, and error paths.
v1.0.5 — Line loads with normal=True (radial / curve-perpendicular pressure)¶
ADDED¶
g.loads.line(..., normal=True)applies a pressure perpendicular to each edge instead of along a fixed direction. Useful for internal/external pressure on curved 2-D boundaries (Lamé-style problems, fluid-loaded arcs, etc.) where the cartesian direction varies along the curve.- Default sign comes from Gmsh's surface boundary orientation —
gmsh.model.getBoundary([(2, surface)], oriented=True)tells the composite which side of the curve the structure sits on, somagnitude > 0always pushes into the structure (matchingg.loads.surface(..., normal=True)). - Optional
away_from=(x0, y0, z0)reference point overrides the Gmsh path — flips the in-plane normal so it points away from that point. Use for ambiguous cases (a curve bounding two surfaces, or a free curve not bounding any surface) or when you want to be explicit. - Both
reduction="tributary"(default) andreduction="consistent"honournormal=True. - New worked example
examples/EOS Examples/example_plate_pyGmsh_v2.ipynbrewrites the thick-walled-cylinder Lamé problem on top of the new API — replaces ~30 lines of manual consistent-force integration on the inner arc with a singleg.loads.line(pg='Pressure', magnitude=p, normal=True)declaration.
INTERNAL¶
- New resolver methods
LoadResolver.resolve_line_per_edge_tributaryandresolve_line_per_edge_consistentaccept a list of(edge, q_xyz)items so the composite can pre-compute per-edge force-per-length vectors (which vary along curved boundaries). Constant-direction line loads still use the originalresolve_line_*methods unchanged. - Per-edge normal computation lives in
LoadsComposite(it needs Gmsh queries for the boundary-orientation path); the resolver stays pure-math.
v1.0.4 — Low-level booleans preserve Instances + accept label/PG refs¶
FIXED¶
g.model.boolean.{fuse,cut,intersect,fragment}now keepInstance.entitiesconsistent when called directly on tags that happen to belong to a tracked Instance. Previously the remap only ran insideg.parts.fragment_all/fragment_pair/fuse_group, so a low-level boolean left Instance entries pointing at consumed tags. The remap-from-result walk has been extracted intoPartsRegistry._remap_from_resultand every OCC boolean call site (both_bool_opand the Parts-level methods) now routes through that single implementation.
ADDED¶
g.model.boolean.*accepts label names and user physical-group names inobjects=/tools=, matching the input shape ofg.physical.add. Strings resolve via the shared resolver: label (Tier 1) first, then user PG (Tier 2). Raw tags, dimtags, and mixed lists still work.
INTERNAL¶
- New
resolve_to_dimtagshelper inapeGmsh.core._helpers— companion toresolve_to_tagsthat emits(dim, tag)pairs. Handles labels / PGs that span multiple dimensions without the caller having to coerce a single dim. - Plan B (
Instance.entitiesas a computed label-backed property) was weighed against this conservative fix and deferred; seeinternal_docs/plan_instance_computed_view.mdfor the signals that would trigger revisiting it.
v1.0.0 — Clean Architecture (breaking)¶
v1.0 bundles two breaking changes: the package rename and the Model
composition refactor. A full find-replace migration guide is at
internal_docs/MIGRATION_v1.md.
BREAKING¶
- Package renamed:
pyGmsh→apeGmsh from pyGmsh import pyGmsh→from apeGmsh import apeGmshclass pyGmsh(_SessionBase)→class apeGmsh(_SessionBase)-
Companion app
apeGmshViewerkeeps its name (only its internal imports of our theme module were updated) -
Model methods split into five sub-composites (composition replaces mixin inheritance):
g.model.geometry.*— 19 primitive builders (add_point, add_line, add_box, add_cylinder, etc.)g.model.boolean.*— fuse, cut, intersect, fragmentg.model.transforms.*— translate, rotate, scale, mirror, copy, extrude, revolve, sweep, thru_sectionsg.model.io.*— load/save STEP, IGES, DXF, MSH, heal_shapesg.model.queries.*— bounding_box, center_of_mass, mass, boundary, adjacencies, entities_in_bounding_box, remove, remove_duplicates, make_conformal, registry-
g.model.sync(),g.model.viewer(),g.model.gui(),g.model.launch_picker(),g.model.selectionstay flat on Model -
Rename
g.mass→g.massesfor consistency with the other plural composites (g.loads,g.parts,g.physical,g.constraints,g.mesh_selection) fem.mass→fem.masses-
Class names (
MassesComposite,MassSet,MassDef,MassRecord) unchanged -
Removed legacy aliases:
g.initialize()/g.finalize()→ useg.begin()/g.end()g._initialized→ useg.is_active-
g.model_name→ useg.name -
Removed deprecated methods:
g.model.viewer_fast()/g.mesh.viewer_fast()→ useviewer()(always fast now)g.parts.add_physical_groups()→ explicitg.physical.add_volume(inst.entities[3], name=...)g.opensees.add_nodal_load()→ useg.loads.point()in ag.loads.pattern()block-
g.mesh_selection.add_nodes(nearest_to=...)→closest_to= -
Removed convenience delegates on the session:
g.remove_duplicates()→g.model.queries.remove_duplicates()-
g.make_conformal()→g.model.queries.make_conformal() -
Removed property on
_SessionBase: _parent.model_name→_parent.name
FIXED¶
- Pylance / static analyzers no longer lose track of Model methods.
Composition makes every method statically discoverable through
the sub-composite classes (
_Geometry,_Boolean,_Transforms,_IO,_Queries), each of which is a concrete class with explicit methods. No MRO walking across 5 mixin files.
INTERNAL¶
_GeometryMixin→_Geometry_BooleanMixin→_Boolean_TransformsMixin→_Transforms_IOMixin→_IO_QueriesMixin→_Queries
Each sub-composite now takes a model reference in __init__ and
accesses Model state via self._model._log(...),
self._model._register(...), self._model._as_dimtags(...),
self._model._registry, instead of inheriting state.
MIGRATION¶
See internal_docs/MIGRATION_v1.md for the complete
find-replace table and an automated migration script.
v0.3.0 is the last pyGmsh release (pre-rename safety tag).
v0.3.1 is the transitional apeGmsh release (rename only).
v1.0.0 is the new architecture (composition + cleanups).
v0.3.1 — Package rename (transitional)¶
- Renamed
pyGmshpackage directory toapeGmsh/on disk - All internal imports, class name, config entries updated
- Examples and docs still reference old API (deferred to v1.0)
- Safety release — rename is isolated from the architectural refactor that follows in v1.0
v0.3.0 — Last pyGmsh release (safety tag)¶
Safety checkpoint before the package rename and v1.0 refactor. This
is the final tag under the pyGmsh name. If you need the old API,
pin to this version.
v0.2.x — Loads, Masses, Viewer overlays¶
- New
g.loadscomposite with pattern context managers - New
g.masscomposite (renamed tog.massesin v1.0) fem.loads/fem.massauto-resolved byget_fem_data()- Loads/masses/constraints overlays live on
g.mesh.viewer(fem=...)— they are mesh-resolved concepts and never landed ong.model.viewer()
v0.2.0 — Composites architecture¶
- Assembly absorbed into
apeGmshas composites: g.parts(PartsRegistry)g.constraints(ConstraintsComposite)- MeshSelectionSet + _mesh_filters spatial query engine
- Viewer rebuild: BRep / mesh viewers unified around EntityRegistry, PickEngine, ColorManager, VisibilityManager
- Catppuccin Mocha theme across all viewers