Results
Post-processing container.
Construction — model= is required
Every Results constructor requires a model, so the post-processing
container always has a read-side broker to resolve names, PGs, and
connectivity against:
from apeGmsh import Results
from apeGmsh.opensees import OpenSeesModel
# Native apeGmsh HDF5 (a Composed file carrying results + model is common)
model = OpenSeesModel.from_h5("run.h5")
results = Results.from_native("run.h5", model=model) # model= REQUIRED
# STKO .mpco — model_h5= points at the sibling model archive
results = Results.from_mpco("run.mpco", model_h5="model.h5") # model_h5= REQUIRED
# Live recorders — fem= and model=
results = Results.from_recorders(spec, "out/", fem=fem, model=model)
Omitting model= (or model_h5= for from_mpco) raises TypeError.
results.model is never None on a constructed Results (ADR 0020
INV-1). Reach the neutral FEMData zone through the broker chain:
osm = results.model # OpenSeesModel broker (never None)
fem = results.model.fem # neutral FEMData zone
results.fem is the locally-bound snapshot and may differ from
results.model.fem after a .bind().
Lineage — results.lineage warns, never raises
results.lineage returns a Lineage(fem_hash, model_hash,
results_hash, warnings) describing the git-style fem → model →
results hash chain. Stored-vs-recomputed mismatches surface as
[lineage] ... strings in lineage.warnings; the property itself
never raises. Call lineage.assert_clean() to escalate any
warnings to LineageError.
BindError is gone
Construction no longer rejects a mismatched FEM with BindError
(deleted in the three-broker refactor). results.bind(fem)
performs no hash validation — pairing a FEM with the right run is
the user's responsibility, reported through lineage.warnings.
Fluent selection — .nodes.select() / .elements.select()
results.nodes.select(...) and results.elements.select(...) are the
results entries of the unified, daisy-chainable
selection idiom. .select() returns a
MeshSelection (point
family); the terminal is .values(component=...), which forwards to
the retained results.<level>.get(...) reader and returns the
same slab (NodeSlab / ElementSlab) with id/value parity.
slab = (results.nodes.select(pg="Base")
.in_box(lo, hi) # half-open [lo, hi)
.on_plane((0, 0, 0), (0, 0, 1), tol=1e-6)
.values(component="displacement_x")) # -> NodeSlab
A bare results selection needs a component — .result() raises
RuntimeError; use .values(component=...). Element spatial verbs
operate on element centroids.
S5 — formerly-silent results paths now raise
results with selection= on an import-origin
(from_msh/MPCO/native) FEMData now raises RuntimeError
instead of resolving to an empty set; results element-centroid
computation raises KeyError on an unknown connectivity node,
which also makes the legacy
results.elements.in_box/nearest_to/on_plane helpers fail
loud. See the changelog.
See Selection for the full idiom; results
sub-composite .select() (gauss/fibers/layers/line_stations/
springs) is a tracked, not-yet-shipped follow-up.
apeGmsh.results.Results.Results
Results(reader: ResultsReader, *, fem: 'Optional[FEMData]' = None, stage_id: Optional[str] = None, path: Optional[Path] = None, model: 'OpenSeesModel', model_path: Optional[Path] = None)
Top-level results object. Returned by Results.from_* constructors.
Stage scoping
Instances may be unscoped (top-level — accesses any stage) or
scoped to one stage (returned by .stage(name),
.modes[i]). Scoped instances expose stage metadata as
properties (.kind, .time, .n_steps); mode-scoped
instances additionally expose .eigenvalue, .frequency_hz,
.period_s, .mode_index.
Source code in src/apeGmsh/results/Results.py
| def __init__(
self,
reader: ResultsReader,
*,
fem: "Optional[FEMData]" = None,
stage_id: Optional[str] = None,
path: Optional[Path] = None,
model: "OpenSeesModel",
model_path: Optional[Path] = None,
) -> None:
self._reader = reader
self._fem = fem
self._stage_id = stage_id
self._path = path
# ADR 0020 INV-1 (Phase 8 prune) — ``_model`` is required and
# never None. The three public constructors validate the
# contract and raise :class:`TypeError` on missing supply;
# internal callers (``_derive``) propagate the existing handle.
self._model = model
# Sibling-archive path for readers that carry no embedded
# ``/opensees/`` zone (MPCO). ``None`` when ``self._path``
# already is the model archive (native Composed file). The
# subprocess viewer reads this to forward ``--model-h5`` to
# the child process — without it, ``__main__.py`` exits(2)
# on ``.mpco`` paths.
self._model_path = model_path
self._stages_cache: Optional[list[StageInfo]] = None
# Composites
self.nodes = NodeResultsComposite(self)
self.elements = ElementResultsComposite(self)
self.inspect = ResultsInspect(self)
self._plot: Optional["ResultsPlot"] = None
|
fem
property
The bound FEMData snapshot, or None if not bound.
model
property
The bound :class:OpenSeesModel broker.
Phase 8 (ADR 0020 INV-1) — always non-None on a constructed
:class:Results. The chain-forward handle from which the
OpenSeesModel and its embedded FEMData can be reached.
lineage
property
Phase-6 lineage chain — git-style fem → model → results.
ADR 0021 defines a three-link hash chain fem_hash →
model_hash → results_hash where each layer's hash includes
its parent's hash (one-directional, tamper-evident).
Mismatches between stored and recomputed hashes surface as
[lineage] ... warnings in :attr:Lineage.warnings; they
never raise from this property (INV-2).
Phase-8 derivation order:
- Inherit
fem_hash + model_hash + accumulated
warnings from :attr:model.lineage (the broker
recomputes against the same file).
- Read the stored
/meta/lineage/results_hash via the
reader's results_lineage_attrs helper and recompute
from /stages/... via recompute_results_hash;
append a drift warning on mismatch.
Readers that don't implement the Phase-6 result-layer
protocol methods are tolerated via getattr cushions:
their lineage stays at the model layer, no warning emitted.
stages
property
All stages in the file (scoped instances also list them).
modes
property
Stages with kind='mode' as a list of mode-scoped Results.
Order is the order the modes were written (typically by
ascending mode_index). For a stable lookup by index, sort:
sorted(results.modes, key=lambda m: m.mode_index).
eigen_modes
property
eigen_modes: list[EigenMode]
Mode-kind stages as lightweight :class:EigenMode snapshots.
Each :class:EigenMode carries only the four scalar fields
(mode_index, eigenvalue, frequency_hz, period_s)
— no file handle, no mode-shape arrays. Use this when you
need the eigenvalue spectrum but not the per-node shapes
(e.g. an LTB Mcr probe, a pickle-able report, or a return
value from a function whose Results context is about to be
closed).
For the per-node mode shape arrays, use the mode-scoped
:class:Results from :attr:modes instead and query via
mode.nodes.get(component="displacement_x", ...).
Order matches :attr:modes. For a stable lookup by index,
sort: sorted(results.eigen_modes, key=lambda m: m.mode_index).
plot
property
results.plot — static matplotlib renderer.
Mirrors the interactive viewer's diagram catalog as headless,
publication-ready matplotlib figures::
results.plot.contour("displacement_z", step=-1)
results.plot.deformed(step=-1, scale=50, component="stress_xx")
results.plot.history(node=412, component="displacement_x")
Requires the [plot] extra (matplotlib).
from_native
classmethod
from_native(path: str | Path, *, fem: 'Optional[FEMData]' = None, model: 'Optional[OpenSeesModel]' = None, model_path: 'Optional[str | Path]' = None) -> 'Results'
Open an apeGmsh native HDF5 results file.
Phase 8 (ADR 0020 INV-1) — model= is required. Missing
supply raises :class:TypeError. Pass
model=OpenSeesModel.from_h5(path_to_model_h5) (often the
same path as path when the file is a Composed-file
per ADR 0020).
If fem is omitted, the embedded /model/ snapshot is
used as the bound FEMData.
model_path records the on-disk archive the model was read
from, for when it is not path itself — e.g. results whose
embedded /model zone is not independently readable. The
non-blocking subprocess viewer forwards it as --model-h5 so the
child re-reads the model from there instead of from path.
Source code in src/apeGmsh/results/Results.py
| @classmethod
def from_native(
cls,
path: str | Path,
*,
fem: "Optional[FEMData]" = None,
model: "Optional[OpenSeesModel]" = None,
model_path: "Optional[str | Path]" = None,
) -> "Results":
"""Open an apeGmsh native HDF5 results file.
Phase 8 (ADR 0020 INV-1) — ``model=`` is required. Missing
supply raises :class:`TypeError`. Pass
``model=OpenSeesModel.from_h5(path_to_model_h5)`` (often the
same path as ``path`` when the file is a Composed-file
per ADR 0020).
If ``fem`` is omitted, the embedded ``/model/`` snapshot is
used as the bound FEMData.
``model_path`` records the on-disk archive the ``model`` was read
from, for when it is *not* ``path`` itself — e.g. results whose
embedded ``/model`` zone is not independently readable. The
non-blocking subprocess viewer forwards it as ``--model-h5`` so the
child re-reads the model from there instead of from ``path``.
"""
if model is None:
raise TypeError(_MODEL_REQUIRED_MESSAGE)
from .readers._native import NativeReader
reader = NativeReader(path)
bound_fem = _resolve_fem(reader, fem)
bound_model = resolve_bound_model(reader, model)
# ``resolve_bound_model`` always returns ``model`` here since
# we just asserted it is non-None, but route through the helper
# to keep the resolution semantics in one place.
assert bound_model is not None
return cls(
reader, fem=bound_fem, path=Path(path), model=bound_model,
model_path=Path(model_path) if model_path is not None else None,
)._with_autoloaded_definitions()
|
from_recorders
classmethod
from_recorders(spec, output_dir: str | Path, *, fem: 'FEMData', cache_root: str | Path | None = None, stage_name: str = 'analysis', stage_kind: str = 'transient', file_format: str = 'out', stage_id: str | None = None, model: 'Optional[OpenSeesModel]' = None) -> 'Results'
Open the result of an OpenSees run driven by Tcl/Py recorders.
Phase 8 (ADR 0020 INV-1) — model= is required. Missing
supply raises :class:TypeError. The model's /opensees/
zone is embedded into the transcoded native h5 (the
Composed-file pattern); downstream
:meth:Results.from_native then auto-resolves the broker
from the same file.
Parses the .out / .xml files emitted at
output_dir (matching what spec.emit_recorders(...) or
the apeGmsh OpenSees bridge's Tcl/Py emit produced) into an
apeGmsh native HDF5, caches the result at
cache_root, and opens it through NativeReader.
Caching: subsequent calls with unchanged input files return
the cached HDF5 directly (file mtime + size + spec
snapshot_id form the cache key). See
writers/_cache.py.
stage_id matches the per-stage filename prefix used by
:meth:ResolvedRecorderSpec.emit_recorders together with
begin_stage(stage_id, ...). When set, only files prefixed
with <stage_id>__ are read; stage_name defaults to
stage_id if not overridden. None (default) keeps the
legacy flat-naming used by Tcl/Py exports.
Phase 6 v1 supports nodal records only; element-level records
in the spec are skipped with a note. The capture flow
(Phase 7) handles modal recorders.
Source code in src/apeGmsh/results/Results.py
| @classmethod
def from_recorders(
cls,
spec,
output_dir: str | Path,
*,
fem: "FEMData",
cache_root: str | Path | None = None,
stage_name: str = "analysis",
stage_kind: str = "transient",
file_format: str = "out",
stage_id: str | None = None,
model: "Optional[OpenSeesModel]" = None,
) -> "Results":
"""Open the result of an OpenSees run driven by Tcl/Py recorders.
Phase 8 (ADR 0020 INV-1) — ``model=`` is required. Missing
supply raises :class:`TypeError`. The model's ``/opensees/``
zone is embedded into the transcoded native h5 (the
Composed-file pattern); downstream
:meth:`Results.from_native` then auto-resolves the broker
from the same file.
Parses the ``.out`` / ``.xml`` files emitted at
``output_dir`` (matching what ``spec.emit_recorders(...)`` or
the apeGmsh OpenSees bridge's Tcl/Py emit produced) into an
apeGmsh native HDF5, caches the result at
``cache_root``, and opens it through ``NativeReader``.
Caching: subsequent calls with unchanged input files return
the cached HDF5 directly (file mtime + size + spec
``snapshot_id`` form the cache key). See
``writers/_cache.py``.
``stage_id`` matches the per-stage filename prefix used by
:meth:`ResolvedRecorderSpec.emit_recorders` together with
``begin_stage(stage_id, ...)``. When set, only files prefixed
with ``<stage_id>__`` are read; ``stage_name`` defaults to
``stage_id`` if not overridden. ``None`` (default) keeps the
legacy flat-naming used by Tcl/Py exports.
Phase 6 v1 supports nodal records only; element-level records
in the spec are skipped with a note. The capture flow
(Phase 7) handles modal recorders.
"""
if model is None:
raise TypeError(_MODEL_REQUIRED_MESSAGE)
from .schema._versions import PARSER_VERSION
from .transcoders import RecorderTranscoder
from .writers import _cache
if fem is None:
raise TypeError(
"Results.from_recorders(...) requires fem= "
"(the spec's snapshot_id must match)."
)
# When stage_id is provided and stage_name was left at its
# default, mirror stage_id so the resulting Results stage is
# named meaningfully (otherwise everything ends up as
# "analysis" regardless of which stage the user loaded).
if stage_id is not None and stage_name == "analysis":
stage_name = stage_id
out_dir = Path(output_dir)
cache_dir = _cache.resolve_cache_root(cache_root)
source_files = _cache.list_source_files(
spec, out_dir, file_format=file_format, stage_id=stage_id,
)
key = _cache.compute_cache_key(
source_files,
parser_version=PARSER_VERSION,
fem_snapshot_id=fem.snapshot_id,
)
cached_h5, _ = _cache.cache_paths(cache_dir, key)
if not cached_h5.exists():
# Materialise the model's ``/opensees/`` zone alongside the
# transcoded results. ``OpenSeesModel.to_h5`` (the public,
# schema-authority-respecting writer) shapes the source;
# NativeWriter copies the ``/opensees/`` group at open
# time (Composed-file pattern, ADR 0020 INV-3 preserved).
model_h5_src: Path = cached_h5.with_suffix(".model.h5")
model.to_h5(model_h5_src)
transcoder = RecorderTranscoder(
spec, out_dir, cached_h5, fem,
stage_name=stage_name,
stage_kind=stage_kind,
file_format=file_format,
stage_id=stage_id,
model_h5_src=model_h5_src,
)
transcoder.run()
return cls.from_native(cached_h5, fem=fem, model=model)
|
from_mpco
classmethod
from_mpco(path: 'str | Path | list[str | Path]', *, fem: 'Optional[FEMData]' = None, merge_partitions: bool = True, model_h5: 'Optional[str | Path]' = None) -> 'Results'
Open a STKO .mpco HDF5 results file.
Phase 8 (ADR 0020 INV-1) — model_h5= is required. Missing
supply raises :class:TypeError. The broker is loaded via
:meth:OpenSeesModel.from_h5 and attached to the resulting
:class:Results; INV-3 — the broker is held in memory only
(no derived results.h5 is written copying the
/opensees/ zone in).
Single-file mode (default for non-partitioned analyses): pass
the path of one .mpco file. Synthesizes a partial FEMData
from the MPCO MODEL/ group if fem is omitted.
Multi-partition mode (parallel OpenSees runs): pass either
- a single
<stem>.part-<N>.mpco path — siblings are
discovered automatically by globbing <stem>.part-*.mpco
in the same directory and merged into one virtual reader;
- an explicit list of partition paths.
Boundary nodes deduplicate by ID (first-occurrence wins);
elements concatenate (disjoint by partition); slabs stitch
across partitions transparently. Stage and time vectors must
match across partitions or construction raises.
Pass merge_partitions=False to opt out of auto-discovery
and read only the file at path even if it follows the
.part-N naming convention.
Source code in src/apeGmsh/results/Results.py
| @classmethod
def from_mpco(
cls,
path: "str | Path | list[str | Path]",
*,
fem: "Optional[FEMData]" = None,
merge_partitions: bool = True,
model_h5: "Optional[str | Path]" = None,
) -> "Results":
"""Open a STKO ``.mpco`` HDF5 results file.
Phase 8 (ADR 0020 INV-1) — ``model_h5=`` is required. Missing
supply raises :class:`TypeError`. The broker is loaded via
:meth:`OpenSeesModel.from_h5` and attached to the resulting
:class:`Results`; INV-3 — the broker is held *in memory only*
(no derived ``results.h5`` is written copying the
``/opensees/`` zone in).
Single-file mode (default for non-partitioned analyses): pass
the path of one ``.mpco`` file. Synthesizes a partial FEMData
from the MPCO ``MODEL/`` group if ``fem`` is omitted.
Multi-partition mode (parallel OpenSees runs): pass either
- a single ``<stem>.part-<N>.mpco`` path — siblings are
discovered automatically by globbing ``<stem>.part-*.mpco``
in the same directory and merged into one virtual reader;
- an explicit list of partition paths.
Boundary nodes deduplicate by ID (first-occurrence wins);
elements concatenate (disjoint by partition); slabs stitch
across partitions transparently. Stage and time vectors must
match across partitions or construction raises.
Pass ``merge_partitions=False`` to opt out of auto-discovery
and read only the file at ``path`` even if it follows the
``.part-N`` naming convention.
"""
if model_h5 is None:
raise TypeError(_MODEL_H5_REQUIRED_MESSAGE)
from .readers._mpco import MPCOReader
from .readers._mpco_multi import (
MPCOMultiPartitionReader, discover_partition_files,
)
if isinstance(path, (list, tuple)):
paths = [Path(p) for p in path]
reader = (
MPCOMultiPartitionReader(paths)
if len(paths) > 1
else MPCOReader(paths[0])
)
anchor = paths[0]
else:
anchor = Path(path)
if merge_partitions:
discovered = discover_partition_files(anchor)
else:
discovered = [anchor]
if len(discovered) > 1:
reader = MPCOMultiPartitionReader(discovered)
else:
reader = MPCOReader(discovered[0])
bound_fem = _resolve_fem(reader, fem)
# Per INV-3, this is an in-memory rehydrate from the sibling
# file; we never copy the zone into a derived h5.
from ..opensees.opensees_model import OpenSeesModel
bound_model = OpenSeesModel.from_h5(model_h5)
# ADR 0043 slice 1.3 — MPCO buckets key element results by the
# OpenSees ops tag; the results API speaks fem_eid. Whenever the
# bound model carries a real element_meta pairing, the reader must
# relabel ops↔fem through it. This is NOT compose-only: gmsh
# numbers lower-dimensional elements first, so almost any solid
# model with surface physical groups has fem_eid != ops_tag even
# uncomposed (the former compose-provenance gate silently
# scrambled / dropped element results for exactly that case —
# fixed 2026-08-04). A deliberately-unrelated stub ``model_h5=``
# (common in tests) stays safe through the translator's
# all-or-nothing `_relabel` contract: ids the model does not
# fully describe pass through untranslated.
from .readers._tag_translation import ElementTagTranslator
_tag_map = ElementTagTranslator.from_model(bound_model)
if not _tag_map.is_empty:
reader.attach_tag_map(_tag_map)
return cls(
reader, fem=bound_fem, path=anchor, model=bound_model,
model_path=Path(model_h5),
)._with_autoloaded_definitions()
|
from_ladruno
classmethod
from_ladruno(path: 'str | Path | list[str | Path]', *, fem: 'Optional[FEMData]' = None, merge_partitions: bool = True, model_h5: 'Optional[str | Path]' = None) -> 'Results'
Open a Ladruno .ladruno HDF5 results file.
The Ladruno recorder is the fork's canonical recorder. Unlike
.mpco (and unlike :meth:from_mpco, which requires
model_h5=), a .ladruno is self-sufficient — it carries
its own geometry, regions and beam local axes (schema Principle 0:
"this is the native path; no sibling file"). So model_h5= is
optional:
- omitted → the broker is built in-memory from the file's own
MODEL group (geometry + inferred ndm/ndf; bridge
record zones empty). This is read-time interpretation, not a
transcode.
- supplied → the richer broker is loaded via
:meth:
OpenSeesModel.from_h5 (full bridge records + lineage),
and — whenever the model records an element_meta pairing —
the fem_eid↔ops-tag translator is attached (ADR 0043; required
for composed models AND for any sparsely-renumbered mesh, e.g.
a gmsh solid whose 2-D boundary elements consumed the low ids).
Keys on INFO/GENERATOR="Ladruno" + a supported
FORMAT_VERSION (the reader rejects a .mpco / foreign file
or an out-of-window version loudly).
Multi-partition merge: a parallel run writes one
<stem>.part-<N>.ladruno per rank. Passing one partition path
auto-discovers its siblings (<stem>.part-*.ladruno) and merges
them into one virtual reader (node-union + element-concat);
passing a list merges exactly those paths. merge_partitions=False
opts out of sibling auto-discovery.
Source code in src/apeGmsh/results/Results.py
| @classmethod
def from_ladruno(
cls,
path: "str | Path | list[str | Path]",
*,
fem: "Optional[FEMData]" = None,
merge_partitions: bool = True,
model_h5: "Optional[str | Path]" = None,
) -> "Results":
"""Open a Ladruno ``.ladruno`` HDF5 results file.
The Ladruno recorder is the fork's *canonical* recorder. Unlike
``.mpco`` (and unlike :meth:`from_mpco`, which **requires**
``model_h5=``), a ``.ladruno`` is **self-sufficient** — it carries
its own geometry, regions and beam local axes (schema Principle 0:
"this *is* the native path; no sibling file"). So ``model_h5=`` is
**optional**:
* omitted → the broker is built in-memory from the file's own
``MODEL`` group (geometry + inferred ``ndm``/``ndf``; bridge
record zones empty). This is read-time interpretation, not a
transcode.
* supplied → the richer broker is loaded via
:meth:`OpenSeesModel.from_h5` (full bridge records + lineage),
and — whenever the model records an element_meta pairing —
the fem_eid↔ops-tag translator is attached (ADR 0043; required
for composed models AND for any sparsely-renumbered mesh, e.g.
a gmsh solid whose 2-D boundary elements consumed the low ids).
Keys on ``INFO/GENERATOR="Ladruno"`` + a supported
``FORMAT_VERSION`` (the reader rejects a ``.mpco`` / foreign file
or an out-of-window version loudly).
Multi-partition merge: a parallel run writes one
``<stem>.part-<N>.ladruno`` per rank. Passing one partition path
auto-discovers its siblings (``<stem>.part-*.ladruno``) and merges
them into one virtual reader (node-union + element-concat);
passing a list merges exactly those paths. ``merge_partitions=False``
opts out of sibling auto-discovery.
"""
from .readers._ladruno import LadrunoReader
from .readers._ladruno_multi import (
LadrunoMultiPartitionReader, discover_partition_files,
)
if isinstance(path, (list, tuple)):
paths = [Path(p) for p in path]
reader = (
LadrunoMultiPartitionReader(paths)
if len(paths) > 1
else LadrunoReader(paths[0])
)
anchor = paths[0]
else:
anchor = Path(path)
discovered = (
discover_partition_files(anchor)
if merge_partitions else [anchor]
)
reader = (
LadrunoMultiPartitionReader(discovered)
if len(discovered) > 1
else LadrunoReader(discovered[0])
)
bound_fem = _resolve_fem(reader, fem)
bound_model: "Optional[OpenSeesModel]"
if model_h5 is not None:
from ..opensees.opensees_model import OpenSeesModel
bound_model = OpenSeesModel.from_h5(model_h5)
# Attach the fem_eid↔ops-tag translator whenever the model
# carries a real element_meta pairing — see the twin comment
# in :meth:`from_mpco` (the pairing diverges for ANY sparsely
# renumbered mesh, not just composed models).
from .readers._tag_translation import ElementTagTranslator
_tag_map = ElementTagTranslator.from_model(bound_model)
if not _tag_map.is_empty:
reader.attach_tag_map(_tag_map)
model_path: "Optional[Path]" = Path(model_h5)
else:
# Self-sufficient path — minimal broker from the file itself.
bound_model = resolve_bound_model(reader, None)
model_path = None
assert bound_model is not None
return cls(
reader, fem=bound_fem, path=anchor, model=bound_model,
model_path=model_path,
)._with_autoloaded_definitions()
|
from_fem
classmethod
from_fem(fem: 'FEMData', path: 'str | Path | list[str | Path]', *, kind: str = 'auto', merge_partitions: bool = True, cache_root: 'str | Path | None' = None) -> 'Results'
Open a results file against a bare :class:FEMData snapshot.
The one-call route to :class:Results / :meth:viewer for a
model that did not go through the apeSees bridge — e.g. a
physical-group model where fem = g.mesh.queries.get_fem_data()
drove a hand-written OpenSees deck. Without this the only routes
were the bridge-bound constructors (which need a bridge-emitted
model.h5 / OpenSeesModel) or the self-describing
:meth:from_ladruno.
from_fem materialises a neutral-only model.h5 from fem
(cached, keyed by fem.snapshot_id) and binds it to the reader
for path. Materialising a file — rather than an in-memory
model — is deliberate: it sets model_path so the non-blocking
/ web viewers work (they forward --model-h5), not just data
access.
Parameters
fem
The bound snapshot (from g.mesh.queries.get_fem_data() or
FEMData.from_h5). A composed fem is refused — see
below.
path
The results file (or a partition list).
kind
"mpco" / "ladruno" / "native", or "auto"
(default) to detect from the suffix (.mpco / .ladruno;
a native .h5 must pass kind="native").
merge_partitions
Forwarded to :meth:from_mpco / :meth:from_ladruno for
.part-N auto-discovery.
cache_root
Where the materialised model.h5 is written — under
<cache_root>/from_fem/ (default <cwd>/results/from_fem/
or $APEGMSH_RESULTS_DIR).
Raises
ValueError
When fem is composed (g.compose / from_h5
assembly). Element / Gauss results are relabelled through the
fem_eid ↔ ops-tag map that only a real bridge run records;
a neutral-only model.h5 carries none, so a composed model
would silently mislabel every element result. Build the model
through apeSees and pass its model.h5 (e.g.
apeSees(fem).h5(...) / g.save →
from_mpco(path, model_h5=...)).
Notes
A bare fem carries no envelope ndf (MeshInfo has none), so
the cached model's ndf is 0. This is harmless for reading
results and for the viewer; it only matters for deck re-emit
(model.build(...)), which is not this path's purpose.
Source code in src/apeGmsh/results/Results.py
| @classmethod
def from_fem(
cls,
fem: "FEMData",
path: "str | Path | list[str | Path]",
*,
kind: str = "auto",
merge_partitions: bool = True,
cache_root: "str | Path | None" = None,
) -> "Results":
"""Open a results file against a bare :class:`FEMData` snapshot.
The one-call route to :class:`Results` / :meth:`viewer` for a
model that did **not** go through the ``apeSees`` bridge — e.g. a
physical-group model where ``fem = g.mesh.queries.get_fem_data()``
drove a hand-written OpenSees deck. Without this the only routes
were the bridge-bound constructors (which need a bridge-emitted
``model.h5`` / ``OpenSeesModel``) or the self-describing
:meth:`from_ladruno`.
``from_fem`` materialises a neutral-only ``model.h5`` from ``fem``
(cached, keyed by ``fem.snapshot_id``) and binds it to the reader
for ``path``. Materialising a file — rather than an in-memory
model — is deliberate: it sets ``model_path`` so the non-blocking
/ web viewers work (they forward ``--model-h5``), not just data
access.
Parameters
----------
fem
The bound snapshot (from ``g.mesh.queries.get_fem_data()`` or
``FEMData.from_h5``). A **composed** fem is refused — see
below.
path
The results file (or a partition list).
kind
``"mpco"`` / ``"ladruno"`` / ``"native"``, or ``"auto"``
(default) to detect from the suffix (``.mpco`` / ``.ladruno``;
a native ``.h5`` must pass ``kind="native"``).
merge_partitions
Forwarded to :meth:`from_mpco` / :meth:`from_ladruno` for
``.part-N`` auto-discovery.
cache_root
Where the materialised ``model.h5`` is written — under
``<cache_root>/from_fem/`` (default ``<cwd>/results/from_fem/``
or ``$APEGMSH_RESULTS_DIR``).
Raises
------
ValueError
When ``fem`` is **composed** (``g.compose`` / ``from_h5``
assembly). Element / Gauss results are relabelled through the
``fem_eid ↔ ops-tag`` map that only a real bridge run records;
a neutral-only ``model.h5`` carries none, so a composed model
would silently mislabel every element result. Build the model
through ``apeSees`` and pass its ``model.h5`` (e.g.
``apeSees(fem).h5(...)`` / ``g.save`` →
``from_mpco(path, model_h5=...)``).
Notes
-----
A bare fem carries no envelope ``ndf`` (``MeshInfo`` has none), so
the cached model's ``ndf`` is ``0``. This is harmless for reading
results and for the viewer; it only matters for deck **re-emit**
(``model.build(...)``), which is not this path's purpose.
"""
# ADR 0043 slice 1.3 — element results relabel through the
# bridge-emitted element_meta. A neutral-only model.h5 carries
# none, but ``composed_from`` round-trips, so from_mpco /
# from_ladruno would see a "composed" model, attach an
# element-less (empty) tag translator, and silently mislabel
# every element/gauss/fiber result. Refuse loudly.
if len(getattr(fem, "composed_from", ()) or ()) > 0:
raise ValueError(
"Results.from_fem: the FEMData is composed (g.compose / "
"from_h5). Element/Gauss results need the bridge's "
"fem_eid<->ops-tag map, which a neutral-only model.h5 "
"cannot provide. Build the model through apeSees and pass "
"its model.h5 — e.g. Results.from_mpco(path, "
"model_h5='model.h5')."
)
resolved_kind = _resolve_results_kind(kind, path)
# Materialise a neutral-only model.h5, cached by content hash.
from .writers._cache import resolve_cache_root
cache_dir = resolve_cache_root(cache_root) / "from_fem"
cache_dir.mkdir(parents=True, exist_ok=True)
snap = str(getattr(fem, "snapshot_id", "") or "model")
cached = cache_dir / f"{snap}.model.h5"
if not cached.exists():
fem.to_h5(str(cached))
if resolved_kind == "mpco":
return cls.from_mpco(
path, fem=fem, model_h5=cached,
merge_partitions=merge_partitions,
)
if resolved_kind == "ladruno":
return cls.from_ladruno(
path, fem=fem, model_h5=cached,
merge_partitions=merge_partitions,
)
# native — pass the rehydrated neutral-only model + its path so
# the subprocess viewer can forward --model-h5.
from ..opensees.opensees_model import OpenSeesModel
return cls.from_native(
path, fem=fem, model=OpenSeesModel.from_h5(cached),
model_path=cached,
)
|
energy
energy(*, region: 'Optional[int]' = None, stage: Optional[str] = None) -> 'Any'
Energy-balance time history — Ladruno-recorder feature.
Returns a :class:pandas.DataFrame of the closure components
KE / IE / DW / ULW / RES / ERR indexed by
simulation time, written by the recorder's -G energy verb.
region=None → whole-domain balance (ON_DOMAIN).
region=<tag> → the per-region balance (ON_REGIONS) for
the OpenSees region tag.
ERR (the normalized energy-balance error %) is the headline
solution-quality diagnostic for explicit runs. Raises
:class:TypeError on a non-Ladruno results object (MPCO / native
carry no energy balance) and ValueError if energy was not
recorded / the region is unknown.
results.plot.energy(...) renders this as a matplotlib
time-history figure.
Source code in src/apeGmsh/results/Results.py
| def energy(
self,
*,
region: "Optional[int]" = None,
stage: Optional[str] = None,
) -> "Any":
"""Energy-balance time history — **Ladruno-recorder feature**.
Returns a :class:`pandas.DataFrame` of the closure components
``KE`` / ``IE`` / ``DW`` / ``ULW`` / ``RES`` / ``ERR`` indexed by
simulation time, written by the recorder's ``-G energy`` verb.
* ``region=None`` → whole-domain balance (``ON_DOMAIN``).
* ``region=<tag>`` → the per-region balance (``ON_REGIONS``) for
the OpenSees region tag.
``ERR`` (the normalized energy-balance error %) is the headline
solution-quality diagnostic for explicit runs. Raises
:class:`TypeError` on a non-Ladruno results object (MPCO / native
carry no energy balance) and ``ValueError`` if energy was not
recorded / the region is unknown.
``results.plot.energy(...)`` renders this as a matplotlib
time-history figure.
"""
read_energy = getattr(self._reader, "read_energy", None)
if read_energy is None:
raise TypeError(
"Results.energy() is a Ladruno-recorder feature. Open a "
".ladruno via Results.from_ladruno(...) recorded with the "
"'-G energy' verb; MPCO / native results carry no energy "
"balance."
)
sid = self._resolve_stage(stage)
cols, values, time = read_energy(sid, region=region)
import pandas as pd
return pd.DataFrame(
values, columns=cols, index=pd.Index(time, name="time"),
)
|
energy_regions
energy_regions(*, stage: Optional[str] = None) -> 'list[int]'
OpenSees region tags with a recorded per-region energy balance.
The bridge auto-allocates an integer region tag when a Ladruno
recorder is given energy_pg= (or a value filter + energy);
that tag is opaque to the author. This lists the tags actually
present in ON_REGIONS/energyBalance so you can pick one to pass
to :meth:energy — e.g. r.energy(region=r.energy_regions()[0]).
Returns [] when only the whole-model balance was recorded
(read it with energy(), no region=). Ladruno-recorder
feature; raises :class:TypeError on MPCO / native results.
Source code in src/apeGmsh/results/Results.py
| def energy_regions(self, *, stage: Optional[str] = None) -> "list[int]":
"""OpenSees region tags with a recorded per-region energy balance.
The bridge auto-allocates an integer region tag when a Ladruno
recorder is given ``energy_pg=`` (or a value filter + ``energy``);
that tag is opaque to the author. This lists the tags actually
present in ``ON_REGIONS/energyBalance`` so you can pick one to pass
to :meth:`energy` — e.g. ``r.energy(region=r.energy_regions()[0])``.
Returns ``[]`` when only the whole-model balance was recorded
(read it with ``energy()``, no ``region=``). Ladruno-recorder
feature; raises :class:`TypeError` on MPCO / native results.
"""
available = getattr(self._reader, "available_energy_regions", None)
if available is None:
raise TypeError(
"Results.energy_regions() is a Ladruno-recorder feature. "
"Open a .ladruno via Results.from_ladruno(...); MPCO / "
"native results carry no energy balance."
)
sid = self._resolve_stage(stage)
return available(sid)
|
node_envelope
node_envelope(component: str, *, stage: Optional[str] = None) -> 'Any'
Per-node time-reduced extremes — Ladruno -envelope feature.
When a .ladruno is recorded with the recorder's -envelope
flag, each node channel stores componentwise running extremes
(MIN/MAX/ABSMAX and the step at which the abs-extreme
occurred) instead of a time series — the cheap way to capture peak
response over a long run without keeping every step.
Returns a :class:pandas.DataFrame indexed by node id with columns
min / max / absmax / arg_step for component (a
neutral name like "displacement_x"). Raises :class:TypeError
on a non-Ladruno results object, and :class:ValueError if the file
was not recorded with -envelope or the component is absent.
results.plot.node_envelope(...) paints a chosen measure on
the mesh as a matplotlib figure.
Source code in src/apeGmsh/results/Results.py
| def node_envelope(
self,
component: str,
*,
stage: Optional[str] = None,
) -> "Any":
"""Per-node time-reduced extremes — **Ladruno ``-envelope`` feature**.
When a ``.ladruno`` is recorded with the recorder's ``-envelope``
flag, each node channel stores componentwise running extremes
(``MIN``/``MAX``/``ABSMAX`` and the step at which the abs-extreme
occurred) *instead of* a time series — the cheap way to capture peak
response over a long run without keeping every step.
Returns a :class:`pandas.DataFrame` indexed by node id with columns
``min`` / ``max`` / ``absmax`` / ``arg_step`` for ``component`` (a
neutral name like ``"displacement_x"``). Raises :class:`TypeError`
on a non-Ladruno results object, and :class:`ValueError` if the file
was not recorded with ``-envelope`` or the component is absent.
``results.plot.node_envelope(...)`` paints a chosen measure on
the mesh as a matplotlib figure.
"""
read_node_envelope = getattr(self._reader, "read_node_envelope", None)
if read_node_envelope is None:
raise TypeError(
"Results.node_envelope() is a Ladruno-recorder feature. Open "
"a single-file .ladruno via Results.from_ladruno(...) recorded "
"with the '-envelope' flag. (MPCO / native results and "
"partitioned .ladruno envelope merges are not supported.)"
)
sid = self._resolve_stage(stage)
env = read_node_envelope(sid, component)
import pandas as pd
return pd.DataFrame(
{
"min": env.min,
"max": env.max,
"absmax": env.absmax,
"arg_step": env.arg_step,
},
index=pd.Index(env.node_ids, name="node_id"),
)
|
bind
bind(fem: 'FEMData') -> 'Results'
Re-bind to fem.
Useful when you've re-built the same mesh in a fresh session
and want labels / Parts that the embedded snapshot doesn't
carry. No hash validation is performed — pairing the FEMData
with a results file from the same run is the user's
responsibility.
Source code in src/apeGmsh/results/Results.py
| def bind(self, fem: "FEMData") -> "Results":
"""Re-bind to ``fem``.
Useful when you've re-built the same mesh in a fresh session
and want labels / Parts that the embedded snapshot doesn't
carry. No hash validation is performed — pairing the FEMData
with a results file from the same run is the user's
responsibility.
"""
bound = _resolve_fem(self._reader, fem)
return self._derive(fem=bound)
|
stage
stage(name_or_id: str) -> 'Results'
Return a Results scoped to a stage (matched by id or name).
Source code in src/apeGmsh/results/Results.py
| def stage(self, name_or_id: str) -> "Results":
"""Return a Results scoped to a stage (matched by id or name)."""
info = self._lookup_stage(name_or_id)
return self._derive(stage_id=info.id)
|
close
Close the underlying reader (releases the HDF5 file handle).
Source code in src/apeGmsh/results/Results.py
| def close(self) -> None:
"""Close the underlying reader (releases the HDF5 file handle)."""
if hasattr(self._reader, "close"):
self._reader.close()
|
demo
classmethod
demo(**kwargs) -> 'Results'
Return a ready-to-view demo :class:Results (cantilever pushover).
Zero-setup sample data so Results.demo().show_web() (or
.viewer()) renders without supplying an .mpco /
model.h5 pair — handy for docs, smoke tests, and trying the
viewer. A real apeSees-emitted model with a synthetic, ramped
cantilever deflection (no OpenSees solve). See
:func:apeGmsh.results.make_demo_results for the keyword options
(length / n_elements / n_steps / tip_drift /
path).
Source code in src/apeGmsh/results/Results.py
| @classmethod
def demo(cls, **kwargs) -> "Results":
"""Return a ready-to-view demo :class:`Results` (cantilever pushover).
Zero-setup sample data so ``Results.demo().show_web()`` (or
``.viewer()``) renders without supplying an ``.mpco`` /
``model.h5`` pair — handy for docs, smoke tests, and trying the
viewer. A real ``apeSees``-emitted model with a synthetic, ramped
cantilever deflection (no OpenSees solve). See
:func:`apeGmsh.results.make_demo_results` for the keyword options
(``length`` / ``n_elements`` / ``n_steps`` / ``tip_drift`` /
``path``).
"""
from .demo import make_demo_results
return make_demo_results(**kwargs)
|
assess
assess(*, figures: bool = False, out_dir: 'str | Path | None' = None) -> 'AssessmentReport'
Compile a v1 :class:~apeGmsh.assess.AssessmentReport.
figures=True calls :meth:render_pack. Default is False.
Source code in src/apeGmsh/results/Results.py
| def assess(
self,
*,
figures: bool = False,
out_dir: "str | Path | None" = None,
) -> "AssessmentReport":
"""Compile a v1 :class:`~apeGmsh.assess.AssessmentReport`.
``figures=True`` calls :meth:`render_pack`. Default is ``False``.
"""
from apeGmsh.assess import assess_results
return assess_results(self, figures=figures, out_dir=out_dir)
|
session
The presentation session for these results (ADR 0098 §1).
Presentation with no window: a ResultsSession (from
apeGmsh.results.session) bound to this broker, booted with
the default picture — ONE empty mesh view (grey analysis mesh,
no slots, no legends). Configure it (slots, deform, time), then
s.render("a.png") for a still; the Qt client (s.show())
arrives at S2 and viewer() flips onto it at S6.
Persisted section cuts boot as view clips (ADR 0098 S6b).
The retired section_cut diagram kind took its auto-load
contract with it, but not the contract itself: cuts persisted
under /opensees/cuts/ come back on the booted view as
clips. Only the ones that translate honestly do — a cut that
named a strict subset of the model's elements, or that carries
a bounding polygon, cuts LESS than a view clip does, so it is
skipped with one [session] line rather than silently
widening what disappears from the screen. Reading the cuts can
never fail this call: a bad zone is a line, not a traceback.
Source code in src/apeGmsh/results/Results.py
| def session(self):
"""The presentation session for these results (ADR 0098 §1).
Presentation with no window: a ``ResultsSession`` (from
``apeGmsh.results.session``) bound to this broker, booted with
the default picture — ONE empty mesh view (grey analysis mesh,
no slots, no legends). Configure it (slots, deform, time), then
``s.render("a.png")`` for a still; the Qt client (``s.show()``)
arrives at S2 and ``viewer()`` flips onto it at S6.
**Persisted section cuts boot as view clips** (ADR 0098 S6b).
The retired ``section_cut`` diagram kind took its auto-load
contract with it, but not the contract itself: cuts persisted
under ``/opensees/cuts/`` come back on the booted view as
clips. Only the ones that translate honestly do — a cut that
named a strict subset of the model's elements, or that carries
a bounding polygon, cuts LESS than a view clip does, so it is
skipped with one ``[session]`` line rather than silently
widening what disappears from the screen. Reading the cuts can
never fail this call: a bad zone is a line, not a traceback.
"""
from .session import ResultsSession
from .session._cuts import attach_persisted_cuts
s = ResultsSession(results=self)
view = s.add_view()
try:
notices = attach_persisted_cuts(self, view)
except Exception as exc: # noqa: BLE001 - the session always boots
notices = (
f"persisted section cuts could not be loaded as view "
f"clips: {type(exc).__name__}: {exc}",
)
for notice in notices:
print(f"[session] {notice}")
return s
|
viewer
viewer(*, blocking: 'Optional[bool]' = None, title: Optional[str] = None, restore_session: 'bool | str' = 'prompt', save_session: bool = True)
Open the post-solve results window on a ResultsSession.
Sugar for :meth:session + show() (ADR 0098 §1, flipped at
S6a). The one-liner is unchanged; what it opens is not. A
:class:~apeGmsh.results.session.ResultsSession is the
document — tiled mesh and plot panes, each with the closed §4
slot catalog — and the window is a client that projects it. The
retired Geometry / Composition / Diagram window is gone from
this door. Everything the window does, a script can do to the
same object::
s = results.session() # the document, no window
s.render("a.png") # a still, no Qt
results.viewer() # the human one-liner
Parameters
blocking
None (default) — auto: True in scripts and the
plain CLI, False inside a Jupyter / IPython ZMQ kernel,
where the blocking Qt loop would freeze (often kill) the
kernel. An in-memory Results in a notebook cannot spawn a
subprocess and falls back to :meth:show_web. Either
notebook path announces itself with one line.
True — open the window in-process and block the calling
thread until it closes. Matches the signature of
:meth:g.mesh.viewer and :meth:g.model.viewer.
False — spawn a subprocess via
python -m apeGmsh.viewers <path> so the notebook /
kernel can keep running. Requires that the Results was
opened from disk (self._path is set); raises
:class:RuntimeError for in-memory Results.
title
Optional window title; defaults to "Results — <filename>".
restore_session
What to do with a session snapshot saved beside the results
file. True restores silently, False ignores it,
"prompt" (default) asks. No effect for in-memory
Results, which have no file to sit beside.
save_session
If True (default), the session — panes, slots, pose,
time link, selection — is written to
<results>.viewer-session.json when the window closes.
False disables auto-save. Auto-save also disarms itself
for a window that could not read an existing file there, so
the unreadable file survives (INV-SESSION-OPEN, see
apeGmsh.results.session._boot).
Returns
ResultsSession
The session the window projected, after the window
closes (blocking). Still live: query it, render stills off
it, snapshot it.
subprocess.Popen
The spawned process handle (non-blocking). Deliberately not
unified with the blocking return — a session in this
process is not what the child window is showing.
WebViewer
The :meth:show_web handle (auto mode, in-memory Results
in a notebook). The web client is a later client of the
same session; until it lands this hatch keeps today's path.
None
If APEGMSH_SKIP_VIEWER is set in the environment. This
lets the same cell run under jupyter nbconvert --execute
or in CI without spawning a GUI window.
Notes
The v13 <results>.viewer-session.json written by the retired
window is not restorable (ADR 0098 Consequences). The first
flipped open says so in one line and renames it aside to
.legacy — never overwriting it, and never overwriting an
aside that is already there.
cuts= is retired with the diagram ontology (§1): a cut plane
is clip state on a view. Build cuts with :mod:apeGmsh.cuts and
add them as clips — results.session() then view.add_clip.
Source code in src/apeGmsh/results/Results.py
| def viewer(
self,
*,
blocking: "Optional[bool]" = None,
title: Optional[str] = None,
restore_session: "bool | str" = "prompt",
save_session: bool = True,
):
"""Open the post-solve results window on a ``ResultsSession``.
Sugar for :meth:`session` + ``show()`` (ADR 0098 §1, flipped at
S6a). The one-liner is unchanged; what it opens is not. A
:class:`~apeGmsh.results.session.ResultsSession` is the
document — tiled mesh and plot panes, each with the closed §4
slot catalog — and the window is a client that projects it. The
retired Geometry / Composition / Diagram window is gone from
this door. Everything the window does, a script can do to the
same object::
s = results.session() # the document, no window
s.render("a.png") # a still, no Qt
results.viewer() # the human one-liner
Parameters
----------
blocking
``None`` (default) — auto: ``True`` in scripts and the
plain CLI, ``False`` inside a Jupyter / IPython ZMQ kernel,
where the blocking Qt loop would freeze (often kill) the
kernel. An in-memory Results in a notebook cannot spawn a
subprocess and falls back to :meth:`show_web`. Either
notebook path announces itself with one line.
``True`` — open the window in-process and block the calling
thread until it closes. Matches the signature of
:meth:`g.mesh.viewer` and :meth:`g.model.viewer`.
``False`` — spawn a subprocess via
``python -m apeGmsh.viewers <path>`` so the notebook /
kernel can keep running. Requires that the Results was
opened from disk (``self._path`` is set); raises
:class:`RuntimeError` for in-memory Results.
title
Optional window title; defaults to ``"Results — <filename>"``.
restore_session
What to do with a session snapshot saved beside the results
file. ``True`` restores silently, ``False`` ignores it,
``"prompt"`` (default) asks. No effect for in-memory
Results, which have no file to sit beside.
save_session
If ``True`` (default), the session — panes, slots, pose,
time link, selection — is written to
``<results>.viewer-session.json`` when the window closes.
``False`` disables auto-save. Auto-save also disarms itself
for a window that could not read an existing file there, so
the unreadable file survives (INV-SESSION-OPEN, see
``apeGmsh.results.session._boot``).
Returns
-------
ResultsSession
The session the window projected, **after** the window
closes (blocking). Still live: query it, render stills off
it, snapshot it.
subprocess.Popen
The spawned process handle (non-blocking). Deliberately not
unified with the blocking return — a session in this
process is not what the child window is showing.
WebViewer
The :meth:`show_web` handle (auto mode, in-memory Results
in a notebook). The web client is a later client of the
same session; until it lands this hatch keeps today's path.
None
If ``APEGMSH_SKIP_VIEWER`` is set in the environment. This
lets the same cell run under ``jupyter nbconvert --execute``
or in CI without spawning a GUI window.
Notes
-----
The v13 ``<results>.viewer-session.json`` written by the retired
window is not restorable (ADR 0098 Consequences). The first
flipped open says so in one line and renames it aside to
``.legacy`` — never overwriting it, and never overwriting an
aside that is already there.
``cuts=`` is retired with the diagram ontology (§1): a cut plane
is clip state on a view. Build cuts with :mod:`apeGmsh.cuts` and
add them as clips — ``results.session()`` then ``view.add_clip``.
"""
import os
if os.environ.get("APEGMSH_SKIP_VIEWER"):
print("[skip viewer] APEGMSH_SKIP_VIEWER set")
return None
if blocking is None:
if not _in_notebook_kernel():
blocking = True
elif self._path is None:
print(
"[viewer] notebook kernel detected and this Results "
"is in-memory — opening the web viewer instead of "
"blocking the kernel (pass blocking=True to force "
"the Qt window)."
)
return self.show_web()
else:
print(
"[viewer] notebook kernel detected — spawning the "
"viewer as a separate process so the kernel keeps "
"running (pass blocking=True to open it in-process)."
)
blocking = False
if not blocking:
handle = self._spawn_viewer_subprocess(
title=title,
restore_session=restore_session,
save_session=save_session,
)
# The subprocess opens its own NativeReader against the
# path; the parent kernel's reader is no longer needed for
# rendering. Close it here so the user can re-run a capture
# script (which deletes / recreates the same .h5) without
# hitting ``PermissionError: file is being used by another
# process`` — Windows refuses to unlink a file that any
# process has open, even read-only.
#
# If the user wants to keep querying ``results`` after the
# spawn, they can re-bind via ``Results.from_native(path)``.
try:
self.close()
except Exception:
pass
return handle
return self._show_session_window(
title=title,
restore_session=restore_session,
save_session=save_session,
)
|
render
render(path: 'str | Path', *, view: str = 'contour', component: Optional[str] = None, step: int = -1, deform: 'Optional[Any]' = None, camera: 'Optional[str]' = None, window_size: tuple[int, int] = (1280, 720)) -> 'Optional[Path]'
Write one offscreen still (ADR 0094 S1).
VTK offscreen from the viewer scene / diagram pipeline — no
Qt window, no event loop, no setup(plotter, director).
view is a closed set: mesh / contour / deformed
/ reactions.
camera= defaults to xy for a planar model, iso
otherwise (ADR 0094 Amendment 3); pass it explicitly to
override.
Returns the written :class:~pathlib.Path, or None (and
prints the [skip viewer] notice) under
APEGMSH_SKIP_VIEWER=1 or with no GL.
Source code in src/apeGmsh/results/Results.py
| def render(
self,
path: "str | Path",
*,
view: str = "contour",
component: Optional[str] = None,
step: int = -1,
deform: "Optional[Any]" = None,
camera: "Optional[str]" = None,
window_size: tuple[int, int] = (1280, 720),
) -> "Optional[Path]":
"""Write one offscreen still (ADR 0094 S1).
VTK offscreen from the viewer scene / diagram pipeline — no
Qt window, no event loop, no ``setup(plotter, director)``.
``view`` is a closed set: ``mesh`` / ``contour`` / ``deformed``
/ ``reactions``.
``camera=`` defaults to ``xy`` for a planar model, ``iso``
otherwise (ADR 0094 Amendment 3); pass it explicitly to
override.
Returns the written :class:`~pathlib.Path`, or ``None`` (and
prints the ``[skip viewer]`` notice) under
``APEGMSH_SKIP_VIEWER=1`` or with no GL.
"""
from apeGmsh.viewers.render import render_results
return render_results(
self, path,
view=view, component=component, step=step,
deform=deform, camera=camera, window_size=window_size,
)
|
render_pack
render_pack(out_dir: 'str | Path', *, camera: 'Optional[str]' = None, window_size: tuple[int, int] = (1280, 720)) -> tuple[Path, ...]
Write the canned report pack (ADR 0094 S3).
Returns the tuple of written paths, or () under
APEGMSH_SKIP_VIEWER=1 / no GL (and prints the
[skip viewer] notice). Closed view= set only; no
setup(). There is no fem.render_pack.
camera= defaults to xy for a planar model, iso
otherwise (ADR 0094 Amendment 3); pass it explicitly to
override.
Source code in src/apeGmsh/results/Results.py
| def render_pack(
self,
out_dir: "str | Path",
*,
camera: "Optional[str]" = None,
window_size: tuple[int, int] = (1280, 720),
) -> tuple[Path, ...]:
"""Write the canned report pack (ADR 0094 S3).
Returns the tuple of written paths, or ``()`` under
``APEGMSH_SKIP_VIEWER=1`` / no GL (and prints the
``[skip viewer]`` notice). Closed ``view=`` set only; no
``setup()``. There is no ``fem.render_pack``.
``camera=`` defaults to ``xy`` for a planar model, ``iso``
otherwise (ADR 0094 Amendment 3); pass it explicitly to
override.
"""
from apeGmsh.viewers.render import render_pack as _render_pack
return _render_pack(
self, out_dir, camera=camera, window_size=window_size,
)
|
export_animation
export_animation(path: 'str | Any', *, fps: int = 30, step_stride: int = 1, stage: 'Optional[str]' = None, deform: 'Optional[Any]' = None, camera: 'Optional[Any]' = None, window_size: 'Optional[tuple[int, int]]' = (1280, 720), setup: 'Optional[Any]' = None)
Render the time history to a video / GIF without a GUI session.
Builds the full results viewer off-screen (so deformation,
contours, camera, and theming are pixel-identical to the
interactive viewer), walks every step capturing a frame, and
encodes to the format chosen by path's suffix — .mp4
(H.264, needs the apegmsh[animation] extra) or .gif
(Pillow, no extra). The viewer window is shown briefly while
rendering (the OpenGL context requires a realized surface) but
no blocking event loop is entered.
Parameters
path
Output file. Suffix selects the format (.mp4 / .gif).
fps
Frames per second of the output.
step_stride
Capture every N-th step (plus always the last). Useful to
keep long histories short.
stage
Stage id/name to animate. Defaults to the active stage.
deform
Deformed-shape scaling. A number applies that scale to the
"displacement" field; a (field, scale) pair selects
another field. None (default) renders the undeformed
mesh.
camera
Optional value assigned to plotter.camera_position (e.g.
"iso", "xy", or an explicit position triple) before
rendering. None keeps the auto-framed camera.
window_size
(width, height) of the rendered frames. None keeps
the viewer's default size.
setup
Optional callback(plotter, director) invoked after the
scene is built and before capture — the escape hatch for
adding contours / section cuts / custom camera work via the
same APIs the interactive viewer uses.
Returns
pathlib.Path
The resolved output path, or None when
APEGMSH_SKIP_VIEWER is set in the environment.
Source code in src/apeGmsh/results/Results.py
| def export_animation(
self,
path: "str | Any",
*,
fps: int = 30,
step_stride: int = 1,
stage: "Optional[str]" = None,
deform: "Optional[Any]" = None,
camera: "Optional[Any]" = None,
window_size: "Optional[tuple[int, int]]" = (1280, 720),
setup: "Optional[Any]" = None,
):
"""Render the time history to a video / GIF without a GUI session.
Builds the full results viewer off-screen (so deformation,
contours, camera, and theming are pixel-identical to the
interactive viewer), walks every step capturing a frame, and
encodes to the format chosen by ``path``'s suffix — ``.mp4``
(H.264, needs the ``apegmsh[animation]`` extra) or ``.gif``
(Pillow, no extra). The viewer window is shown briefly while
rendering (the OpenGL context requires a realized surface) but
no blocking event loop is entered.
Parameters
----------
path
Output file. Suffix selects the format (``.mp4`` / ``.gif``).
fps
Frames per second of the output.
step_stride
Capture every N-th step (plus always the last). Useful to
keep long histories short.
stage
Stage id/name to animate. Defaults to the active stage.
deform
Deformed-shape scaling. A number applies that scale to the
``"displacement"`` field; a ``(field, scale)`` pair selects
another field. ``None`` (default) renders the undeformed
mesh.
camera
Optional value assigned to ``plotter.camera_position`` (e.g.
``"iso"``, ``"xy"``, or an explicit position triple) before
rendering. ``None`` keeps the auto-framed camera.
window_size
``(width, height)`` of the rendered frames. ``None`` keeps
the viewer's default size.
setup
Optional ``callback(plotter, director)`` invoked after the
scene is built and before capture — the escape hatch for
adding contours / section cuts / custom camera work via the
same APIs the interactive viewer uses.
Returns
-------
pathlib.Path
The resolved output path, or ``None`` when
``APEGMSH_SKIP_VIEWER`` is set in the environment.
"""
import os
if os.environ.get("APEGMSH_SKIP_VIEWER"):
print("[skip viewer] APEGMSH_SKIP_VIEWER set")
return None
from ..viewers.results_viewer import ResultsViewer
viewer = ResultsViewer(
self, restore_session=False, save_session=False,
)
# Borrow this live Results — don't close its HDF5 handle on
# teardown (the caller keeps using it). Set BEFORE show() so a
# build failure that triggers teardown still leaves it open.
viewer._own_results_close = False # noqa: SLF001
try:
# show() is inside the try so a failed off-screen realize
# (GL / pixel-format error) still hits ``viewer.close()`` —
# otherwise a half-built, possibly-visible window leaks.
viewer.show(run_loop=False, window_size=window_size)
director = viewer.director
plotter = viewer.plotter
if stage is not None:
director.set_stage(stage)
if deform is not None:
if isinstance(deform, (tuple, list)):
d_field, d_scale = deform[0], float(deform[1])
else:
d_field, d_scale = "displacement", float(deform)
geoms = director.geometries
active = geoms.active or (
geoms.geometries[0] if geoms.geometries else None
)
if active is not None:
geoms.set_deformation(
active.id, enabled=True,
field=d_field, scale=d_scale,
)
if camera is not None:
try:
plotter.camera_position = camera
except Exception:
pass
if setup is not None:
setup(plotter, director)
return viewer.export_animation(
path, fps=fps, step_stride=step_stride,
)
finally:
viewer.close()
|
show_web
show_web(*, stage: 'Optional[str]' = None, show: bool = True, controls: bool = True, render_mode: str = 'client')
Open the view-only web / Jupyter results viewer (ADR 0042 R-C).
Renders the FEM substrate plus any diagrams the director holds
through a pyvista.trame backend — the kernel-safe path that
replaces the blocking Qt :meth:viewer in a notebook. View-only
(picking is deferred to R-D), but with a step slider + per-layer
visibility checkboxes when ipywidgets is available.
No results file handy? Results.demo().show_web() renders a
zero-setup cantilever-pushover sample.
Parameters
stage
Stage id or name to activate; defaults to the first stage.
show
When True (default), display inline immediately. When
False, return the :class:~apeGmsh.viewers.web_viewer.WebViewer
unshown so diagrams can be added via viewer.director first.
controls
When True (default), stack an ipywidgets control panel
(step slider + layer toggles) above the view. Degrades to a
bare view if ipywidgets is absent.
render_mode
"client" (default) renders in the browser via WebGL — fast
camera interaction. "server" renders on the kernel and
streams images (laggy, most VTK-feature-complete; for very
large models). "hybrid" is pyvista's trame backend with
a local/remote toggle in the toolbar.
Returns
WebViewer
The viewer handle (.director / .set_step / .show).
Source code in src/apeGmsh/results/Results.py
| def show_web(
self,
*,
stage: "Optional[str]" = None,
show: bool = True,
controls: bool = True,
render_mode: str = "client",
):
"""Open the view-only web / Jupyter results viewer (ADR 0042 R-C).
Renders the FEM substrate plus any diagrams the director holds
through a ``pyvista.trame`` backend — the kernel-safe path that
replaces the blocking Qt :meth:`viewer` in a notebook. View-only
(picking is deferred to R-D), but with a step slider + per-layer
visibility checkboxes when ``ipywidgets`` is available.
No results file handy? ``Results.demo().show_web()`` renders a
zero-setup cantilever-pushover sample.
Parameters
----------
stage
Stage id or name to activate; defaults to the first stage.
show
When ``True`` (default), display inline immediately. When
``False``, return the :class:`~apeGmsh.viewers.web_viewer.WebViewer`
unshown so diagrams can be added via ``viewer.director`` first.
controls
When ``True`` (default), stack an ``ipywidgets`` control panel
(step slider + layer toggles) above the view. Degrades to a
bare view if ``ipywidgets`` is absent.
render_mode
``"client"`` (default) renders in the browser via WebGL — fast
camera interaction. ``"server"`` renders on the kernel and
streams images (laggy, most VTK-feature-complete; for very
large models). ``"hybrid"`` is pyvista's ``trame`` backend with
a local/remote toggle in the toolbar.
Returns
-------
WebViewer
The viewer handle (``.director`` / ``.set_step`` / ``.show``).
"""
from ..viewers.web_viewer import show_web as _show_web
return _show_web(
self, stage=stage, show=show, controls=controls,
render_mode=render_mode,
)
|
serve_web
serve_web(*, stage: 'Optional[str]' = None, render_mode: str = 'client', port: 'Optional[int]' = None, open_browser: bool = True, title: str = 'apeGmsh', **start_kwargs)
Serve the results as a standalone trame web app (ADR 0042 R-C).
The non-Jupyter counterpart of :meth:show_web: builds a vuetify3
single-page app (the FEM view plus a step slider and per-layer
switches) and serves it at a local URL, opening a browser tab and
blocking until stopped (Ctrl-C). In a notebook use :meth:show_web
instead.
Parameters
stage
Stage id or name to activate; defaults to the first stage.
render_mode
"client" (default), "server", or "hybrid" — see
:meth:show_web.
port
Port to serve on; None lets trame pick one.
open_browser
Open a browser tab at the served URL.
title
App title shown in the toolbar.
**start_kwargs
Passed through to the trame server.start (e.g.
exec_mode).
Returns
WebViewer
The viewer handle.
Source code in src/apeGmsh/results/Results.py
| def serve_web(
self,
*,
stage: "Optional[str]" = None,
render_mode: str = "client",
port: "Optional[int]" = None,
open_browser: bool = True,
title: str = "apeGmsh",
**start_kwargs,
):
"""Serve the results as a standalone trame web app (ADR 0042 R-C).
The non-Jupyter counterpart of :meth:`show_web`: builds a vuetify3
single-page app (the FEM view plus a step slider and per-layer
switches) and serves it at a local URL, opening a browser tab and
blocking until stopped (Ctrl-C). In a notebook use :meth:`show_web`
instead.
Parameters
----------
stage
Stage id or name to activate; defaults to the first stage.
render_mode
``"client"`` (default), ``"server"``, or ``"hybrid"`` — see
:meth:`show_web`.
port
Port to serve on; ``None`` lets trame pick one.
open_browser
Open a browser tab at the served URL.
title
App title shown in the toolbar.
**start_kwargs
Passed through to the trame ``server.start`` (e.g.
``exec_mode``).
Returns
-------
WebViewer
The viewer handle.
"""
from ..viewers.web_viewer import serve_web as _serve_web
return _serve_web(
self, stage=stage, render_mode=render_mode, port=port,
open_browser=open_browser, title=title, **start_kwargs,
)
|
save_definitions
save_definitions(path: 'Optional[str | Path]' = None) -> Path
Persist this Results' custom scalar definitions to a JSON
sidecar (default <results>.defs.json).
Reloaded automatically by :meth:from_native / :meth:from_mpco
/ :meth:from_ladruno, and carried to the subprocess viewer.
Raises for an in-memory Results with no path= given.
Source code in src/apeGmsh/results/Results.py
| def save_definitions(self, path: "Optional[str | Path]" = None) -> Path:
"""Persist this Results' custom scalar definitions to a JSON
sidecar (default ``<results>.defs.json``).
Reloaded automatically by :meth:`from_native` / :meth:`from_mpco`
/ :meth:`from_ladruno`, and carried to the subprocess viewer.
Raises for an in-memory Results with no ``path=`` given.
"""
import json
if path is None:
if self._path is None:
raise RuntimeError(
"in-memory Results has no default sidecar path; "
"pass save_definitions(path=...)."
)
path = self._default_defs_path(self._path)
path = Path(path)
path.write_text(
json.dumps(self._definitions_payload(), indent=2), encoding="utf-8",
)
return path
|
load_definitions
load_definitions(path: 'Optional[str | Path]' = None) -> int
Load and register custom scalar definitions from a JSON sidecar
(default <results>.defs.json). Returns the count applied; a
missing file is a no-op returning 0. Idempotent / best-effort —
see :meth:_apply_definitions_payload.
Source code in src/apeGmsh/results/Results.py
| def load_definitions(self, path: "Optional[str | Path]" = None) -> int:
"""Load and register custom scalar definitions from a JSON sidecar
(default ``<results>.defs.json``). Returns the count applied; a
missing file is a no-op returning 0. Idempotent / best-effort —
see :meth:`_apply_definitions_payload`."""
import json
if path is None:
if self._path is None:
return 0
path = self._default_defs_path(self._path)
path = Path(path)
if not path.exists():
return 0
payload = json.loads(path.read_text(encoding="utf-8"))
return self._apply_definitions_payload(payload)
|
Slabs
Tabular dataclasses returned by reader queries.
apeGmsh.results._slabs
Slab dataclasses returned by ResultsReader implementations.
A slab carries one component's values plus enough location metadata
that the caller can interpret each row without re-deriving it. They
are numpy-native and immutable; the viewer wraps them in xarray
when it wants labeled axes.
Shape conventions (single-stage, post-stitching across partitions):
================ ======================== =================================================
Slab values shape Location index fields
================ ======================== =================================================
NodeSlab (T, N) node_ids: (N,)
ElementSlab (T, E, npe) element_ids: (E,)
LineStationSlab (T, sum_S) element_index, station_natural_coord: (sum_S,)
GaussSlab (T, sum_GP) element_index: (sum_GP,),
natural_coords: (sum_GP, dim)
FiberSlab (T, sum_F) element_index, gp_index, y, z, area,
material_tag: (sum_F,),
station_natural_coord: (sum_F,) | None
LayerSlab (T, sum_L) element_index, gp_index, layer_index,
sub_gp_index, thickness: (sum_L,)
================ ======================== =================================================
For a single time step (time_slice was a scalar), T is 1 and
the leading axis is preserved — the caller can squeeze if desired.
LocalAxes
dataclass
LocalAxes(element_ids: ndarray, quaternions: ndarray)
Per-element local coordinate frames (beam / shell orientation).
Read from a .ladruno MODEL/LOCAL_AXES group — the orientation a
.mpco does not carry for beam-columns (so apeGmsh can orient
line / section-force diagrams straight from .ladruno for wired
element classes, instead of the native vecxz path).
quaternions are scalar-first (w, x, y, z), mapping global →
element-local at the reference configuration. The local axes are the
rows of the rotation matrix (OpenSees quatFromMat stores the
transpose convention), so matrices[k] has row 0 = local x, row 1 =
local y, row 2 = local z — each expressed in global coordinates.
Elements with no recorded frame get the identity quaternion.
matrices
property
(n, 3, 3) per-element rotations; rows are the local axes
in global coordinates.
x_axis
property
(n, 3) — each element's local x-axis (beam axis) in global coords.
y_axis
property
(n, 3) — each element's local y-axis in global coords.
z_axis
property
(n, 3) — each element's local z-axis in global coords.
NodeSlab
dataclass
NodeSlab(component: str, values: ndarray, node_ids: ndarray, time: ndarray)
Node-level result values.
ElementSlab
dataclass
ElementSlab(component: str, values: ndarray, element_ids: ndarray, time: ndarray)
Per-element-node values (e.g. globalForce / localForce).
LineStationSlab
dataclass
LineStationSlab(component: str, values: ndarray, element_index: ndarray, station_natural_coord: ndarray, time: ndarray, local_axes_quaternion: Optional[ndarray] = None)
Beam line-diagram values per integration station.
GaussSlab
dataclass
GaussSlab(component: str, values: ndarray, element_index: ndarray, natural_coords: ndarray, local_axes_quaternion: Optional[ndarray], time: ndarray)
Continuum Gauss-point values.
natural_coords are in parent space [-1, +1]. To get
global coordinates, call slab.global_coords(fem) — interpolates
through the bound FEMData's element shape functions for hex8 /
quad4, falling back to a centroid + bbox-scaled approximation for
element types that don't yet have explicit shape-fn support.
global_coords
global_coords(fem) -> ndarray
Map per-GP natural coords to (sum_GP, 3) world coords.
Uses element shape functions for supported types (hex8, quad4);
falls back to centroid + 0.5 * bbox_span * natural for
others — visualization-faithful for axis-aligned elements.
Source code in src/apeGmsh/results/_slabs.py
| def global_coords(self, fem) -> ndarray:
"""Map per-GP natural coords to ``(sum_GP, 3)`` world coords.
Uses element shape functions for supported types (hex8, quad4);
falls back to ``centroid + 0.5 * bbox_span * natural`` for
others — visualization-faithful for axis-aligned elements.
"""
from ._gauss_world_coords import compute_global_coords
return compute_global_coords(self, fem)
|
FiberSlab
dataclass
FiberSlab(component: str, values: ndarray, element_index: ndarray, gp_index: ndarray, y: ndarray, z: ndarray, area: ndarray, material_tag: ndarray, time: ndarray, station_natural_coord: Optional[ndarray] = None)
Fiber-level values within fiber-section GPs.
LayerSlab
dataclass
LayerSlab(component: str, values: ndarray, element_index: ndarray, gp_index: ndarray, layer_index: ndarray, sub_gp_index: ndarray, thickness: ndarray, local_axes_quaternion: ndarray, time: ndarray)
Layered shell layer values (one row per (elem, surf_gp, layer, sub_gp)).
SpringSlab
dataclass
SpringSlab(component: str, values: ndarray, element_index: ndarray, time: ndarray)
Zero-length spring values (one column per element, one spring index).
component encodes which spring is represented (e.g.
"spring_force_0" for the force in the first configured spring
direction). Each column in values corresponds to one element;
element_index carries the raw OpenSees element tag so the
caller can correlate columns with elements without needing a
separate ID array.
================ ========================
values (T, E)
element_index (E,)
================ ========================
Readers
Reader protocol and supporting types shared by every backend.
apeGmsh.results.readers._protocol
Reader protocol — backend-agnostic contract for the composite layer.
Two implementations:
NativeReader reads apeGmsh native HDF5 (Phase 1).
MPCOReader reads STKO MPCO HDF5 (Phase 3).
The composite layer above (Results.nodes.get(...) etc.) talks
only to this protocol — it never branches on backend type.
ResultLevel
Bases: Enum
The topology level a component lives at.
StageInfo
dataclass
StageInfo(id: str, name: str, kind: str, n_steps: int, eigenvalue: Optional[float] = None, frequency_hz: Optional[float] = None, period_s: Optional[float] = None, mode_index: Optional[int] = None)
Stage metadata returned by ResultsReader.stages().
For kind="mode", the eigenvalue/frequency/period/index
fields are populated and n_steps is 1. For other kinds,
the mode-only fields are None.
EigenMode
dataclass
EigenMode(mode_index: int, eigenvalue: float, frequency_hz: float, period_s: float)
Lightweight snapshot of one eigenmode — no file handle.
Returned by :attr:apeGmsh.results.Results.eigen_modes. Mirrors
the mode-only fields of :class:StageInfo but is detachable from
the Results context, so it is safe to pickle, pass between
processes, or return from a function whose Results was already
closed.
For the mode shape (per-node displacement vectors), use the
mode-scoped :class:apeGmsh.results.Results from results.modes
and query via results.nodes.get(component="displacement_x", ...)
— that path holds the file handle and reads the underlying arrays.
omega_rad_s
property
Angular frequency omega = sqrt(eigenvalue) in rad/s.
Returns 0.0 for non-positive (rigid-body or numerical
zero) eigenvalues — matches capture_modes' behaviour.
ResultsReader
Bases: Protocol
Backend-agnostic reader protocol.
Implementations must support all six topology levels even if a
given file has no data at some of them; they should return empty
component lists from available_components() in that case
rather than raising.
stages
stages() -> list[StageInfo]
All stages in this file, in write order.
Source code in src/apeGmsh/results/readers/_protocol.py
| def stages(self) -> list[StageInfo]:
"""All stages in this file, in write order."""
...
|
time_vector
time_vector(stage_id: str) -> ndarray
Time vector for a stage. Shape (n_steps,).
Source code in src/apeGmsh/results/readers/_protocol.py
| def time_vector(self, stage_id: str) -> ndarray:
"""Time vector for a stage. Shape ``(n_steps,)``."""
...
|
partitions
partitions(stage_id: str) -> list[str]
Partition IDs for a stage (always at least one).
Source code in src/apeGmsh/results/readers/_protocol.py
| def partitions(self, stage_id: str) -> list[str]:
"""Partition IDs for a stage (always at least one)."""
...
|
fem
fem() -> 'Optional[FEMData]'
Embedded / synthesized FEMData snapshot.
NativeReader: reconstructs from /model/ (always available).
MPCOReader: synthesizes a partial FEMData from /MODEL/
(no apeGmsh labels, no Part provenance).
Source code in src/apeGmsh/results/readers/_protocol.py
| def fem(self) -> "Optional[FEMData]":
"""Embedded / synthesized FEMData snapshot.
- ``NativeReader``: reconstructs from ``/model/`` (always available).
- ``MPCOReader``: synthesizes a partial FEMData from ``/MODEL/``
(no apeGmsh labels, no Part provenance).
"""
...
|
opensees_model
opensees_model() -> 'Optional[OpenSeesModel]'
Embedded :class:OpenSeesModel from the file's /opensees/ zone.
Phase 4 (ADR 0020) — the Composed-file pattern. Native readers
auto-resolve from the file when the zone is present (silent, no
warning); third-party file readers (MPCO) return None.
Returns
OpenSeesModel | None
The rehydrated broker when the file carries the zone,
None otherwise. INV-1 — Phase 4 callers must tolerate
None; the value becomes required only in Phase 8.
Source code in src/apeGmsh/results/readers/_protocol.py
| def opensees_model(self) -> "Optional[OpenSeesModel]":
"""Embedded :class:`OpenSeesModel` from the file's ``/opensees/`` zone.
Phase 4 (ADR 0020) — the Composed-file pattern. Native readers
auto-resolve from the file when the zone is present (silent, no
warning); third-party file readers (MPCO) return ``None``.
Returns
-------
OpenSeesModel | None
The rehydrated broker when the file carries the zone,
``None`` otherwise. INV-1 — Phase 4 callers must tolerate
``None``; the value becomes required only in Phase 8.
"""
...
|
available_components
available_components(stage_id: str, level: ResultLevel) -> list[str]
Canonical component names available at the given level.
Source code in src/apeGmsh/results/readers/_protocol.py
| def available_components(
self, stage_id: str, level: ResultLevel,
) -> list[str]:
"""Canonical component names available at the given level."""
...
|
Live capture
Recorder wiring used during a live OpenSees analysis.
LiveRecorders
apeGmsh.results.live._recorders.LiveRecorders
LiveRecorders(spec: 'ResolvedRecorderSpec', output_dir: 'str | Path', *, file_format: str = 'out', ops=None)
Context manager that owns stage-scoped OpenSees recorders.
Parameters
spec
The :class:ResolvedRecorderSpec whose records to emit.
output_dir
Directory the recorder .out / .xml files land in.
Created on __enter__ if missing.
file_format
"out" (text) or "xml". Defaults to "out".
ops
The openseespy module (or a stand-in for testing). Defaults
to openseespy.opensees resolved lazily on __enter__.
Raises
RuntimeError
On __enter__ if the spec contains any modal records.
Source code in src/apeGmsh/results/live/_recorders.py
| def __init__(
self,
spec: "ResolvedRecorderSpec",
output_dir: "str | Path",
*,
file_format: str = "out",
ops=None,
) -> None:
self._spec = spec
self._output_dir = str(output_dir) if output_dir else ""
self._file_format = file_format
self._ops = ops
self._opened = False
self._exited = False
self._current_stage: Optional[StageRecord] = None
self._current_tags: list[int] = []
self._stages: list[StageRecord] = []
|
stages
property
stages: tuple[StageRecord, ...]
All completed stages, in the order they ran.
Recorder tags issued so far across all stages (read-only).
begin_stage
begin_stage(name: str, kind: str = 'transient') -> None
Issue recorders for a new stage. Files are prefixed <name>__.
kind is forwarded to Results.from_recorders(...,
stage_kind=kind) when the stage is read back; valid values
are "transient" or "static".
Source code in src/apeGmsh/results/live/_recorders.py
| def begin_stage(self, name: str, kind: str = "transient") -> None:
"""Issue recorders for a new stage. Files are prefixed ``<name>__``.
``kind`` is forwarded to ``Results.from_recorders(...,
stage_kind=kind)`` when the stage is read back; valid values
are ``"transient"`` or ``"static"``.
"""
self._require_opened()
if self._current_stage is not None:
raise RuntimeError(
f"begin_stage({name!r}) called while stage "
f"{self._current_stage.name!r} is still open. Call "
f"end_stage() first."
)
if not name:
raise ValueError("Stage name must be a non-empty string.")
if "__" in name:
raise ValueError(
f"Stage name {name!r} contains '__', which collides "
f"with the stage/record filename separator."
)
self._current_stage = StageRecord(name=name, kind=kind)
self._current_tags = []
for record in self._spec.records:
if record.category in _SUPPORTED_CATEGORIES:
for logical in emit_logical(
record,
output_dir=self._output_dir,
file_format=self._file_format,
stage_id=name,
):
args = to_ops_args(logical)
tag = self._ops.recorder(*args)
if isinstance(tag, int):
self._current_tags.append(tag)
continue
if record.category in _NON_RECORDER_CATEGORIES:
warnings.warn(
f"LiveRecorders: skipping record {record.name!r} "
f"(category={record.category!r}); fiber/layer "
f"data can't be emitted via classic recorders. "
f"Use spec.capture(...) for in-process capture "
f"or spec.emit_mpco(...) for the STKO MPCO "
f"recorder.",
stacklevel=2,
)
continue
# Modal already raised in __enter__, but defend in depth.
if record.category in _MODAL_CATEGORIES:
continue
warnings.warn(
f"LiveRecorders: skipping record {record.name!r} "
f"with unrecognised category={record.category!r}.",
stacklevel=2,
)
|
end_stage
Remove the current stage's recorders and flush their files.
Source code in src/apeGmsh/results/live/_recorders.py
| def end_stage(self) -> None:
"""Remove the current stage's recorders and flush their files."""
self._require_opened()
if self._current_stage is None:
raise RuntimeError(
"end_stage() called without a matching begin_stage()."
)
for tag in self._current_tags:
try:
self._ops.remove("recorder", tag)
except Exception: # noqa: BLE001 — best-effort flush
warnings.warn(
f"LiveRecorders: failed to remove recorder tag "
f"{tag}; output file may not be flushed.",
stacklevel=2,
)
completed = StageRecord(
name=self._current_stage.name,
kind=self._current_stage.kind,
tags=tuple(self._current_tags),
)
self._stages.append(completed)
self._current_stage = None
self._current_tags = []
|
LiveMPCO
apeGmsh.results.live._mpco.LiveMPCO
LiveMPCO(spec: 'ResolvedRecorderSpec', path: 'str | Path', *, ops=None)
Context manager that owns a single in-process MPCO recorder.
Parameters
spec
The :class:ResolvedRecorderSpec whose records to emit.
path
Output .mpco HDF5 file path. Parent directory created on
__enter__ if missing.
ops
The openseespy module (or a stand-in for testing). Defaults
to openseespy.opensees resolved lazily on __enter__.
Raises
RuntimeError
On __enter__ if ops.recorder('mpco', ...) fails — most
commonly because the openseespy build does not include the
MPCO recorder.
Source code in src/apeGmsh/results/live/_mpco.py
| def __init__(
self,
spec: "ResolvedRecorderSpec",
path: "str | Path",
*,
ops=None,
) -> None:
self._spec = spec
self._path = Path(path)
self._ops = ops
self._opened = False
self._exited = False
self._tag: Optional[int] = None
|
tag
property
The recorder tag returned by ops.recorder, if any.
DomainCapture
apeGmsh.results.capture._domain.DomainCapture
DomainCapture(spec: 'ResolvedDomainCaptureSpec', path: str | Path, fem: 'FEMData', *, ops: Any = None, bridge: Any = None, tag_map: Any = None)
Context manager for in-process result capture.
Constructed live via ops.domain_capture(spec, path=...)
(bridge-attached) or off-line via :meth:DomainCapture.from_h5
(ndm / ndf sourced from a model.h5 /meta). The user
drives the analysis loop and calls step(t) to capture a
snapshot.
Source code in src/apeGmsh/results/capture/_domain.py
| def __init__(
self,
spec: "ResolvedDomainCaptureSpec",
path: str | Path,
fem: "FEMData",
*,
ops: Any = None,
bridge: Any = None,
tag_map: Any = None,
) -> None:
self._spec = spec
self._path = Path(path)
# ADR 0043 slice 1.3 follow-up — fem_eid↔ops-tag translator for a
# composed model. ``None`` (uncomposed / no model) ⇒ no
# translation. Set here when supplied (``from_h5``); otherwise
# built from the bridge sidecar at ``__enter__``.
self._tag_map = tag_map
self._fem = fem
# Per Phase 9 D8 ``ndm`` / ``ndf`` ride on the resolved spec —
# set at resolve time by ``ops.domain_capture`` (live bridge)
# or :meth:`from_h5` (file ``/meta``). The user never passes
# them directly.
self._ndm = spec.ndm
self._ndf = spec.ndf
self._ops = ops # injected for testing; lazy-loaded otherwise
# Phase 4 (ADR 0020) — when supplied, the live :class:`apeSees`
# bridge writes a sidecar ``model.h5`` at __enter__ time whose
# ``/opensees/`` zone is composed into the run h5 via
# NativeWriter (Composed-file pattern). ``None`` keeps the
# pre-Phase-4 single-zone (``/meta`` + ``/model/`` only)
# output shape.
self._bridge = bridge
self._writer = None
self._current_stage: Optional[str] = None
self._stage_kind: Optional[str] = None
self._buffers: list[_NodesCapturer] = []
# Element-level capturers — gauss (Phase 11a), line_stations +
# nodal_forces (Phase 11b), fibers (Phase 11e). Layered shells
# remain deferred (see __enter__).
self._gauss_capturers: list[_GaussCapturer] = []
self._line_station_capturers: list[_LineStationCapturer] = []
self._nodal_force_capturers: list[_NodalForcesCapturer] = []
self._fiber_capturers: list[_FiberCapturer] = []
self._layer_capturers: list[_LayerCapturer] = []
# Records that fall through every supported capturer — surface
# in step() for visibility.
self._element_level_records: list = []
# Whether any nodes record needs reactions per-step.
self._needs_reactions: bool = False
# Phase 4 — sidecar model.h5 path the bridge wrote to, kept so
# __exit__ can clean it up after NativeWriter copied the zone
# into the run file.
self._model_h5_sidecar: Optional[Path] = None
|
close
Finalise any open stage and close the writer.
Source code in src/apeGmsh/results/capture/_domain.py
| def close(self) -> None:
"""Finalise any open stage and close the writer."""
if self._current_stage is not None:
self.end_stage()
if self._writer is not None:
self._writer.close()
self._writer = None
# Phase 4 (ADR 0020) — once the Composed file's ``/opensees/``
# zone has been written, the bridge-emitted sidecar model.h5
# is redundant (the run file is self-contained). Remove it so
# the user's directory stays clean. Tolerate missing-file: a
# failed __enter__ before the bridge wrote the sidecar would
# leave ``_model_h5_sidecar`` pointing at a non-existent path.
if self._model_h5_sidecar is not None:
try:
self._model_h5_sidecar.unlink(missing_ok=True)
except OSError:
# Best-effort cleanup; never let it mask a real
# exception from the analysis loop.
pass
self._model_h5_sidecar = None
|
from_h5
classmethod
from_h5(model_path: 'str | _Path', *, spec: 'DomainCaptureSpec', fem: 'FEMData', output: 'str | _Path', ops: Any = None) -> 'DomainCapture'
Construct a DomainCapture sourcing ndm / ndf from a model.h5.
Reads /meta/ndm and /meta/ndf from the supplied
model_path, resolves spec against fem using those
values, and returns a ready-to-use context manager writing
to output. Use this entry point when you have a saved
model.h5 but no live :class:apeSees bridge in the
current process (per Phase 9 D8 the user never types
ndm / ndf explicitly).
Parameters
model_path
Path to a bridge-emitted model.h5 whose /meta
carries ndm and ndf.
spec
Declarative :class:DomainCaptureSpec. Standalone (no
bridge attached) is fine — this method supplies
ndm / ndf from the file rather than from a bridge.
fem
The :class:FEMData to resolve selectors against. Must
correspond to the same mesh that produced model_path.
output
Path the resulting :class:DomainCapture will write to.
ops
Optional openseespy module (or test stand-in). Defaults
to lazy-loading openseespy.opensees.
Returns
DomainCapture
Ready to be used as a context manager.
Notes
Phase 4 (ADR 0020) — this entry point does not accept a
bridge= kwarg because there is no live bridge in scope.
For the Composed-file pattern with a file-based workflow,
the caller can pre-build the bridge themselves and pass it
via :meth:DomainCapture (the constructor) instead of
from_h5.
Source code in src/apeGmsh/results/capture/_domain.py
| @classmethod
def from_h5(
cls,
model_path: "str | _Path",
*,
spec: "DomainCaptureSpec",
fem: "FEMData",
output: "str | _Path",
ops: Any = None,
) -> "DomainCapture":
"""Construct a DomainCapture sourcing ``ndm`` / ``ndf`` from a model.h5.
Reads ``/meta/ndm`` and ``/meta/ndf`` from the supplied
``model_path``, resolves ``spec`` against ``fem`` using those
values, and returns a ready-to-use context manager writing
to ``output``. Use this entry point when you have a saved
``model.h5`` but no live :class:`apeSees` bridge in the
current process (per Phase 9 D8 the user never types
``ndm`` / ``ndf`` explicitly).
Parameters
----------
model_path
Path to a bridge-emitted ``model.h5`` whose ``/meta``
carries ``ndm`` and ``ndf``.
spec
Declarative :class:`DomainCaptureSpec`. Standalone (no
bridge attached) is fine — this method supplies
``ndm`` / ``ndf`` from the file rather than from a bridge.
fem
The :class:`FEMData` to resolve selectors against. Must
correspond to the same mesh that produced ``model_path``.
output
Path the resulting :class:`DomainCapture` will write to.
ops
Optional openseespy module (or test stand-in). Defaults
to lazy-loading ``openseespy.opensees``.
Returns
-------
DomainCapture
Ready to be used as a context manager.
Notes
-----
Phase 4 (ADR 0020) — this entry point does not accept a
``bridge=`` kwarg because there is no live bridge in scope.
For the Composed-file pattern with a file-based workflow,
the caller can pre-build the bridge themselves and pass it
via :meth:`DomainCapture` (the constructor) instead of
``from_h5``.
"""
from ...opensees.emitter import h5_reader
with h5_reader.open(str(model_path)) as model:
meta = model.meta()
try:
ndm = int(meta["ndm"])
ndf = int(meta["ndf"])
except KeyError as exc:
raise RuntimeError(
f"DomainCapture.from_h5: {model_path!s} has no "
f"ndm/ndf attrs in /meta (got {sorted(meta)!r})."
) from exc
resolved = spec._resolve_with_explicit_ndm_ndf(
fem, ndm=ndm, ndf=ndf,
)
# ADR 0043 slice 1.3 follow-up — when the model.h5 is composed,
# build the fem_eid↔ops-tag translator so capturers query the
# live ops domain with the right tags. ``None`` for an uncomposed
# model (translation would be a no-op).
tag_map = _maybe_build_capture_tag_map(model_path)
return cls(resolved, output, fem, ops=ops, tag_map=tag_map)
|
begin_stage
begin_stage(name: str, kind: str = 'transient') -> str
Open a new stage. Returns the stage_id.
Source code in src/apeGmsh/results/capture/_domain.py
| def begin_stage(self, name: str, kind: str = "transient") -> str:
"""Open a new stage. Returns the stage_id."""
if self._writer is None:
raise RuntimeError(
"DomainCapture is not open. Use as a context manager."
)
if self._current_stage is not None:
raise RuntimeError(
f"Stage {self._current_stage!r} still open — call end_stage()."
)
# Reset per-stage buffers
for cap in self._buffers:
cap.reset()
for gc in self._gauss_capturers:
gc.reset()
for lc in self._line_station_capturers:
lc.reset()
for nfc in self._nodal_force_capturers:
nfc.reset()
for fc in self._fiber_capturers:
fc.reset()
for lc in self._layer_capturers:
lc.reset()
# Stage gets a placeholder time vector that we'll fill at end_stage.
# We can't pre-create the time dataset here because we don't yet
# know how many steps. We start the stage with an empty time
# vector and write everything (including time) at end_stage.
self._current_stage = name
self._stage_kind = kind
return name
|
step
Capture one snapshot at simulation time t.
Source code in src/apeGmsh/results/capture/_domain.py
| def step(self, t: float) -> None:
"""Capture one snapshot at simulation time ``t``."""
if self._current_stage is None:
raise RuntimeError("No stage open — call begin_stage() first.")
if self._element_level_records:
cats = sorted({r.category for r in self._element_level_records})
raise NotImplementedError(
f"DomainCapture does not support element-level "
f"records of category {cats}. Supported categories: "
f"``nodes`` (Phase 3), ``modal`` (Phase 3), ``gauss`` "
f"(Phase 11a), ``line_stations`` (Phase 11b), "
f"``elements`` per-element-node forces (Phase 11b). "
f"Fibers and layers are MPCO-only (Phase 11c) — see "
f"the module docstring."
)
ops = self._lazy_ops()
if self._needs_reactions:
# Refresh the cached reactions in the domain.
ops.reactions()
for cap in self._buffers:
cap.step(t, ops)
for gc in self._gauss_capturers:
gc.step(t, ops)
for lc in self._line_station_capturers:
lc.step(t, ops)
for nfc in self._nodal_force_capturers:
nfc.step(t, ops)
for fc in self._fiber_capturers:
fc.step(t, ops)
for lc in self._layer_capturers:
lc.step(t, ops)
|
end_stage
Flush buffered data for the current stage to disk.
Multiple nodes records may target different node subsets
(e.g. displacements on all nodes + reactions on fixed nodes
only). The native schema has one _ids per partition, so
we merge: take the union of node IDs across records, fill
each component with NaN at slots the record didn't visit.
Source code in src/apeGmsh/results/capture/_domain.py
| def end_stage(self) -> None:
"""Flush buffered data for the current stage to disk.
Multiple ``nodes`` records may target different node subsets
(e.g. displacements on all nodes + reactions on fixed nodes
only). The native schema has one ``_ids`` per partition, so
we merge: take the union of node IDs across records, fill
each component with ``NaN`` at slots the record didn't visit.
"""
if self._current_stage is None:
raise RuntimeError("No stage open.")
assert self._writer is not None
# Time vector — they must all match (same step cadence).
time_vec = np.array([], dtype=np.float64)
for cap in self._buffers:
if cap._times:
time_vec = np.array(cap._times, dtype=np.float64)
break
if time_vec.size == 0:
for gc in self._gauss_capturers:
if gc._times:
time_vec = np.array(gc._times, dtype=np.float64)
break
if time_vec.size == 0:
for lc in self._line_station_capturers:
if lc._times:
time_vec = np.array(lc._times, dtype=np.float64)
break
if time_vec.size == 0:
for nfc in self._nodal_force_capturers:
if nfc._times:
time_vec = np.array(nfc._times, dtype=np.float64)
break
if time_vec.size == 0:
for fc in self._fiber_capturers:
if fc._times:
time_vec = np.array(fc._times, dtype=np.float64)
break
if time_vec.size == 0:
for lc in self._layer_capturers:
if lc._times:
time_vec = np.array(lc._times, dtype=np.float64)
break
sid = self._writer.begin_stage(
name=self._current_stage,
kind=self._stage_kind or "transient",
time=time_vec,
)
try:
self._flush_nodes_merged(sid, time_vec)
self._flush_gauss(sid)
self._flush_line_stations(sid)
self._flush_nodal_forces(sid)
self._flush_fibers(sid)
self._flush_layers(sid)
finally:
# Always close the stage — even if the merge raised, we
# want the stage closed so subsequent ``begin_stage`` works.
self._writer.end_stage()
self._current_stage = None
self._stage_kind = None
# ── Skip-summary warnings ─────────────────────────────────
# Surface any elements the line-stations capturers had to
# drop (typically disp-based beams whose IP coords aren't
# introspectable from openseespy in OpenSees v3.7.x). Until
# this warning lands the user had no signal that their
# recorder spec was partially honoured.
self._warn_about_skipped_line_station_elements()
# Same for gauss captures: an element class with no
# RESPONSE_CATALOG entry (e.g. LadrunoUP, whose effective
# stress is not yet catalogued) is silently dropped from the
# gauss group build — without this the record's dataset comes
# out empty with zero user-facing signal.
self._warn_about_skipped_gauss_elements()
|
capture_modes
capture_modes(n_modes: Optional[int] = None) -> None
Run ops.eigen() and write one mode-kind stage per mode.
n_modes defaults to the maximum across all modal
records in the spec. Pass an explicit value to override.
Source code in src/apeGmsh/results/capture/_domain.py
| def capture_modes(self, n_modes: Optional[int] = None) -> None:
"""Run ``ops.eigen()`` and write one mode-kind stage per mode.
``n_modes`` defaults to the maximum across all ``modal``
records in the spec. Pass an explicit value to override.
"""
modal_records = [
r for r in self._spec.records if r.category == "modal"
]
if n_modes is None:
if not modal_records:
return
n_modes = max(r.n_modes for r in modal_records)
if n_modes <= 0:
return
ops = self._lazy_ops()
eigenvalues = ops.eigen(n_modes)
# ``ops.eigen`` returns a list of eigenvalues (length n_modes).
node_ids = np.asarray(self._fem.nodes.ids, dtype=np.int64)
for mode_idx, lam in enumerate(eigenvalues, start=1):
if lam > 0:
omega = math.sqrt(lam)
else:
# A non-positive eigenvalue is physically meaningful — a
# rigid-body mode / mechanism or an unconverged eigensolve.
# Don't silently bury it as a 0 Hz mode; warn so the user
# treats the shape with suspicion (the stage is still
# written, with frequency/period = 0).
warnings.warn(
f"Mode {mode_idx} has a non-positive eigenvalue "
f"({lam:.6g}); this indicates a spurious/unstable mode "
f"(rigid-body mechanism or unconverged eigensolve). "
f"Writing frequency=0 / period=0 for it.",
UserWarning,
stacklevel=2,
)
omega = 0.0
freq_hz = omega / (2.0 * math.pi)
period_s = (2.0 * math.pi / omega) if omega > 0 else 0.0
sid = self._writer.begin_stage(
name=f"mode_{mode_idx}",
kind="mode",
time=np.array([0.0]),
eigenvalue=float(lam),
frequency_hz=float(freq_hz),
period_s=float(period_s),
mode_index=mode_idx,
)
components: dict[str, ndarray] = {}
axes = ("x", "y", "z")
n_trans = min(3, self._ndm)
want_rotations = self._ndf >= 6
# ONE call per node, then slice — never a call per (node,
# DOF) index.
#
# Asking `nodeEigenvector(nid, mode, dof)` for dof 4-6 gated
# on the model ENVELOPE ndf cannot read a mixed-DOF model at
# all: in any solid + frame assembly the envelope is 6 while
# the solid nodes carry 3, and OpenSees answers "dofTag? too
# large" and raises. Every such model — the whole class this
# capture exists for — failed at the first mode.
#
# A node's own vector is as long as ITS ndf, so slicing it is
# correct per node with no envelope guesswork, and a node
# with no rotational DOFs contributes 0.0 to a rotation
# component that, for it, does not exist.
trans = np.zeros((n_trans, node_ids.size), dtype=np.float64)
rot = np.zeros((3, node_ids.size), dtype=np.float64)
for col, nid in enumerate(node_ids):
vec = ops.nodeEigenvector(int(nid), mode_idx)
for axis_idx in range(min(n_trans, len(vec))):
trans[axis_idx, col] = vec[axis_idx]
if want_rotations and len(vec) >= 6:
for axis_idx in range(3):
rot[axis_idx, col] = vec[3 + axis_idx]
for axis_idx in range(n_trans):
components[f"displacement_{axes[axis_idx]}"] = (
trans[axis_idx][None, :]
)
if want_rotations:
for axis_idx in range(3):
components[f"rotation_{axes[axis_idx]}"] = (
rot[axis_idx][None, :]
)
self._writer.write_nodes(
sid, "partition_0",
node_ids=node_ids,
components=components,
)
self._writer.end_stage()
|
Inspect
apeGmsh.results._inspect.ResultsInspect
ResultsInspect(results: 'Results')
results.inspect — what's available.
Source code in src/apeGmsh/results/_inspect.py
| def __init__(self, results: "Results") -> None:
self._r = results
|
summary
Multi-line human-readable summary.
Source code in src/apeGmsh/results/_inspect.py
| def summary(self) -> str:
"""Multi-line human-readable summary."""
r = self._r
lines = [f"Results: {r._reader_path()!s}"]
fem = r.fem
if fem is not None:
lines.append(
f" FEM: {len(fem.nodes.ids)} nodes, "
f"{sum(len(g) for g in fem.elements)} elements "
f"(snapshot_id={fem.snapshot_id})"
)
else:
lines.append(" FEM: not bound")
stages = r.stages
if not stages:
lines.append(" Stages: (none)")
else:
lines.append(f" Stages ({len(stages)}):")
for s in stages:
detail = f"steps={s.n_steps}, kind={s.kind}"
if s.kind == "mode":
detail += (
f", f={s.frequency_hz:.4g} Hz, "
f"T={s.period_s:.4g} s, "
f"mode_index={s.mode_index}"
)
lines.append(f" - {s.id} ({s.name}): {detail}")
return "\n".join(lines)
|
components
components(*, stage: str | None = None) -> dict[str, list[str]]
Available components per topology level for one stage.
If no stage is given, defaults to the only stage when there is
exactly one; otherwise raises.
Source code in src/apeGmsh/results/_inspect.py
| def components(
self, *, stage: str | None = None,
) -> dict[str, list[str]]:
"""Available components per topology level for one stage.
If no stage is given, defaults to the only stage when there is
exactly one; otherwise raises.
"""
sid = self._r._resolve_stage(stage)
return {
level.value: self._r._reader.available_components(sid, level)
for level in ResultLevel
}
|
diagnose
diagnose(component: str, *, stage: str | None = None) -> str
Explain where a component lives (or doesn't) in this stage.
When a viewer or downstream consumer asks for a component and
gets nothing back, this is the routing-side answer to "why
is the slab empty?". Walks every topology, calls each
composite's available_components(), and returns a
human-readable report that shows where component was
found and what's actually available at each level.
Parameters
component
Canonical component name (e.g. "axial_force",
"displacement_z", "stress_xx").
stage
Stage id or name. Defaults to the only stage when there
is exactly one.
Returns
str
Multi-line report. Print it or include it in an error
message.
Source code in src/apeGmsh/results/_inspect.py
| def diagnose(
self,
component: str,
*,
stage: str | None = None,
) -> str:
"""Explain where a component lives (or doesn't) in this stage.
When a viewer or downstream consumer asks for a component and
gets nothing back, this is the routing-side answer to "why
is the slab empty?". Walks every topology, calls each
composite's ``available_components()``, and returns a
human-readable report that shows where ``component`` was
found and what's actually available at each level.
Parameters
----------
component
Canonical component name (e.g. ``"axial_force"``,
``"displacement_z"``, ``"stress_xx"``).
stage
Stage id or name. Defaults to the only stage when there
is exactly one.
Returns
-------
str
Multi-line report. Print it or include it in an error
message.
"""
try:
sid = self._r._resolve_stage(stage)
except Exception as exc:
return f"diagnose({component!r}): could not resolve stage: {exc}"
lines = [
f"diagnose({component!r}) — stage={sid!r}",
]
per_level: list[tuple[str, list[str], bool]] = []
errors: list[tuple[str, str]] = []
found: list[str] = []
for level in ResultLevel:
try:
comps = self._r._reader.available_components(sid, level)
except Exception as exc:
errors.append((level.value, f"{type(exc).__name__}: {exc}"))
continue
is_match = component in comps
per_level.append((level.value, comps, is_match))
if is_match:
found.append(level.value)
if found:
lines.append(f" FOUND in: {', '.join(found)}")
else:
lines.append(" NOT FOUND in any topology level.")
# Per-level preview — same for found and not-found, so the user
# always sees what's actually present at each level.
for level_value, comps, is_match in per_level:
preview = ", ".join(comps[:6])
if len(comps) > 6:
preview += f", … (+{len(comps) - 6} more)"
available = preview if comps else "(empty — no buckets present)"
marker = "✓" if is_match else " "
lines.append(
f" {marker} {level_value:16s} available: {available}"
)
for level_value, msg in errors:
lines.append(f" {level_value:16s} error: {msg}")
if not found:
lines.append("")
lines.append(
" If you expected the component above, try:"
)
lines.append(
" * Check spelling against ``results.inspect.components()``."
)
lines.append(
" * Check the recorder declared this component in this stage."
)
lines.append(
" * For MPCO files: confirm the underlying recorder "
"wrote a bucket the reader knows about (section.force, "
"localForce, etc.)."
)
return "\n".join(lines)
|
Vocabulary
Canonical result names and shorthand expansion.
apeGmsh.results._vocabulary
apeGmsh.results._vocabulary — deprecation shim (Phase 9).
The canonical vocabulary moved to :mod:apeGmsh._vocabulary so the
OpenSees bridge (declaration-side) and the results module
(consumer-side) can both import without a layering inversion.
This shim fires a :class:DeprecationWarning once on first import
and re-exports the canonical names for one release cycle. Internal
apeGmsh code imports from :mod:apeGmsh._vocabulary directly; only
external callers see the warning.
ALL_CANONICAL
module-attribute
ALL_CANONICAL: frozenset[str] = frozenset(NODAL_KINEMATICS + NODAL_FORCES + PER_ELEMENT_NODAL_FORCES + LINE_DIAGRAMS + LINE_STATION_DEFORMATIONS + SHELL_STRESS_RESULTANTS + SHELL_GENERALIZED_STRAINS + STRESS + STRAIN + PLASTIC_STRAIN + DERIVED_SCALARS + FIBER + SPRING + MATERIAL_STATE)
expand_shorthand
expand_shorthand(name: str, *, ndm: int = 3, ndf: int = 6) -> tuple[str, ...]
Expand a shorthand or pass through a canonical name.
Translational shorthands clip to ndm axes (e.g. ndm=2 →
displacement_x/y only). Rotational shorthands require
rotational DOFs in the active ndf and return () if there
are none. Tensor shorthands ("stress", "strain") clip to
3 components in ndm=2 (xx, yy, xy) and 6 in ndm=3.
Raises ValueError if name is neither a known shorthand
nor a canonical name.
Source code in src/apeGmsh/_vocabulary.py
| def expand_shorthand(
name: str, *, ndm: int = 3, ndf: int = 6,
) -> tuple[str, ...]:
"""Expand a shorthand or pass through a canonical name.
Translational shorthands clip to ``ndm`` axes (e.g. ``ndm=2`` →
``displacement_x/y`` only). Rotational shorthands require
rotational DOFs in the active ``ndf`` and return ``()`` if there
are none. Tensor shorthands (``"stress"``, ``"strain"``) clip to
3 components in ``ndm=2`` (xx, yy, xy) and 6 in ``ndm=3``.
Raises ``ValueError`` if ``name`` is neither a known shorthand
nor a canonical name.
"""
if is_canonical(name):
return (name,)
if name in _SHORTHAND_TRANSLATIONAL:
full = _SHORTHAND_TRANSLATIONAL[name]
return _clip_translational(full, ndm)
if name in _SHORTHAND_ROTATIONAL:
full = _SHORTHAND_ROTATIONAL[name]
return _clip_rotational(full, ndm, ndf)
if name in _SHORTHAND_TENSOR:
full = _SHORTHAND_TENSOR[name]
return _clip_tensor(full, ndm)
if name in _SHORTHAND_LINE_STATION:
# Line-station shorthands are not clipped by ``ndm``/``ndf``:
# the catalog declares per-element which subset is emitted,
# and ``Results`` returns empty slabs for components a given
# element doesn't expose. Clipping here would hide tokens
# users genuinely want (e.g. asking for ``section_force`` on
# a 3D model and missing ``torsion``).
return _SHORTHAND_LINE_STATION[name]
if name in _SHORTHAND_SHELL:
# Not clipped either: the eight resultants are one fixed
# response layout, and which subset a given shell class emits
# is the catalog's answer, not ``ndm``'s.
return _SHORTHAND_SHELL[name]
if name == "reaction":
forces = _clip_translational(_SHORTHAND_REACTION[:3], ndm)
moments = _clip_rotational(_SHORTHAND_REACTION[3:], ndm, ndf)
return forces + moments
raise ValueError(
f"Unknown component '{name}'. Must be a canonical name "
f"(e.g. 'displacement_x', see ALL_CANONICAL) or a known shorthand "
f"({sorted(ALL_SHORTHANDS)})."
)
|
is_canonical
is_canonical(name: str) -> bool
True if name is a known canonical component name.
Source code in src/apeGmsh/_vocabulary.py
| def is_canonical(name: str) -> bool:
"""True if ``name`` is a known canonical component name."""
if name in ALL_CANONICAL:
return True
# Pattern: state_variable_<integer>.
if name.startswith("state_variable_"):
suffix = name[len("state_variable_"):]
return suffix.isdigit()
# Patterns: fiber_stress_<integer> / fiber_strain_<integer>.
# Layered shells write a vector per layer (e.g. 5-component
# plane-stress + transverse-shear), not a scalar; the canonical
# split is index-based when META labels are generic.
for stem in ("fiber_stress_", "fiber_strain_"):
if name.startswith(stem):
suffix = name[len(stem):]
if suffix.isdigit():
return True
# Patterns: spring_force_<integer> / spring_deformation_<integer>.
# ZeroLength elements can have N springs; each gets an indexed
# canonical tied to its position in the configured direction list.
for stem in ("spring_force_", "spring_deformation_"):
if name.startswith(stem):
suffix = name[len(stem):]
if suffix.isdigit():
return True
return False
|