OpenSees bridge¶
apeGmsh's OpenSees deck is constructed via the explicit-constructor pattern after the session closes:
from apeGmsh.opensees import apeSees
fem = g.mesh.queries.get_fem_data(dim=3)
ops = apeSees(fem)
ops.model(ndm=3, ndf=6)
# … typed-primitive declarations, explicit fix / mass / patterns …
ops.tcl("model.tcl") # or ops.py(...), ops.h5(...), ops.run()
The legacy g.opensees session composite and its sub-composites
(materials / elements / ingest / inspect /
export) were removed in Phase 8 of the bridge teardown
(ADR 0009).
apeSees brings the session in three ways (ADR 0051): MP
constraints (g.constraints.*) auto-emit; loads (g.loads.*)
and prescribed displacements (g.displacements.*) are opt-in —
a resolved load case reaches the deck only when a bridge pattern
imports it with p.from_model(case) (or you author it with
p.load(...)); masses and support fixities / SPs are
re-declared explicitly on ops
(ops.fix(pg=, dofs=), ops.mass(pg=, values=)).
Loads do not auto-emit. A
g.loads.*case reaches the solver only viap.from_model(case)inside a pattern (or an ad-hocp.load). Nothing auto-emits, so there is no double-count trap; the deck is authoritative — the bridge applies exactly the cases you import and does not audit the geometry's declared cases, so a case you don't import is simply not applied. A staged model must keep every pattern stage-scoped (s.pattern(series=...)) — a global pattern +ops.stage(...)raisesBridgeError.
Since the teardown, the bridge has been progressively widened:
- Loads are opt-in (ADR 0051). A
g.loads.*case reaches the runnable Tcl/Py deck (and the live/run path) only when a bridge pattern imports it withp.from_model(case)— or you author the load directly withp.load(...). Nothing auto-emits; the deck is authoritative (see the note above). - MP constraints emit automatically from
fem.nodes.constraints/fem.elements.constraints(ADR 0022, Phase 7b) —equalDOF/rigidLink/rigidDiaphragm/ASDEmbeddedNodeElementlines land in the runnable Tcl/Py deck without aningeststep. TheapeSees.h5(path)write target persists per-record details under/opensees/constraints/(additive minor schema bump 2.6.0 → 2.7.0). Auto-emits aTransformationconstraint handler when MP constraints are present and the user has not declared one. - Staged analysis ships via
ops.stage(name)— see Staged analysis below for the user- surface walkthrough, and the in-repo internals doc at staged-analysis.md for the per-stage emit pipeline. - Read-side broker.
OpenSeesModel.from_h5(path, fem_root=)provides a frozen read-only view of the persisted/opensees/zone with the embeddedFEMDatalazily attached (ADR 0019). Re-emit viaom.build("tcl", path)/om.build("py", path)/om.build("live")without rehydrating the apeSees primitives.
For the full user-facing surface (typed materials, sections, elements, recorders, patterns, analysis chain, staged analysis, SSI helpers, cuts and sweeps), see the in-repo api-design.md.
Public surface¶
apeGmsh.opensees.apeSees ¶
apeSees(fem: 'FEMData', *, default_orientation: Orientation | None | _UnsetType = _UNSET, opensees: 'OpenSeesTarget | None' = None)
The OpenSees bridge.
Construct with a :class:~apeGmsh.mesh.FEMData snapshot:
.. code-block:: python
ops = apeSees(fem)
ops.model(ndm=3, ndf=6)
steel = ops.uniaxialMaterial.Steel02(fy=420e6, E=200e9, b=0.01)
...
The bridge holds declared state. apeSees.build() returns a
:class:BuiltModel (immutable) that emitters consume.
Parameters¶
fem
The FEM snapshot the bridge is built against.
default_orientation
Orientation field substituted on any
ops.geomTransf.<Type>() call where the user supplied
neither orientation= nor vecxz=. Defaults to
Cartesian() (Z-up) which matches the prevailing structural
convention. Pass an explicit None for 2D models, where
vecxz is omitted at emit time and an orientation field makes
no sense. Pass a custom orientation (e.g.
Cartesian(reference_axis=(0,1,0)) for a Y-up CAD import)
to set the model-wide default once.
Source code in src/apeGmsh/opensees/apesees.py
7846 7847 7848 7849 7850 7851 7852 7853 7854 7855 7856 7857 7858 7859 7860 7861 7862 7863 7864 7865 7866 7867 7868 7869 7870 7871 7872 7873 7874 7875 7876 7877 7878 7879 7880 7881 7882 7883 7884 7885 7886 7887 7888 7889 7890 7891 7892 7893 7894 7895 7896 7897 7898 7899 7900 7901 7902 7903 7904 7905 7906 7907 7908 7909 7910 7911 7912 7913 7914 7915 7916 7917 7918 7919 7920 7921 7922 7923 7924 7925 7926 7927 7928 7929 7930 7931 7932 7933 7934 7935 7936 7937 7938 7939 7940 7941 7942 7943 7944 7945 7946 7947 7948 7949 7950 7951 7952 7953 7954 7955 7956 7957 7958 7959 7960 7961 7962 7963 7964 7965 7966 7967 7968 7969 7970 7971 7972 7973 7974 7975 7976 7977 7978 7979 | |
opensees
property
¶
The :class:OpenSeesTarget bound on construction, or None.
all_fix_records
property
¶
All fix records — global + every stage's pool.
Returns a tuple of (origin, record) pairs where origin
is either "global" or f"stage {stage.name!r}". Order:
global pool first (in registration order), then each stage in
stage_records order, then each record within a stage in
registration order.
all_mass_records
property
¶
All mass records — global + every stage's pool.
Same shape as :attr:all_fix_records.
all_region_records
property
¶
All region records — global + every stage's pool.
Phase SSI-2.D PR-C introspection symmetry (matches the
:attr:all_fix_records / :attr:all_mass_records shape).
Validator V3 (PR-A) guarantees no name= collision across
scopes, so the user-facing name is unambiguous per
(origin, record) pair.
all_recorder_specs
property
¶
All recorder specs — global + every stage's pool.
Global recorders are sourced from self._primitives
filtered to :class:Recorder instances and EXCLUDING any
spec claimed by s.recorder(...); the per-stage entries
come from each :class:StageRecord's recorder_specs.
Origin is "global" or f"stage {stage.name!r}".
capabilities ¶
Probe the in-process openseespy build (live path).
Imports openseespy in the active interpreter and reports whether
it looks like the Ladruno fork (has_fork), exposes the
fork-only profiler command, its version() string, and its
build stamp (the exact git hash the binary was compiled from,
on fork builds that ship ladrunoBuild).
Raises if openseespy is not installed. This introspects the
live runtime only — the subprocess paths bind their own
interpreter / binary via :class:OpenSeesTarget.
Source code in src/apeGmsh/opensees/apesees.py
model ¶
Set the model dimensionality (ndm) and the envelope ndf.
Per-node ndf is inferred from the declared element
classes (ADR 0048) — ndf here is only the OpenSees model
envelope (model BasicBuilder -ndm K -ndf N) and the
fallback for nodes inference cannot see: element-less /
decoupled nodes, and nodes touched only by adaptive elements
(the zeroLength family). Element-attached nodes get their
inferred value as a per-node -ndf override, emitted only
where it differs from this envelope. There is no per-node
ndf to declare on the geometry session — g.node_ndf
was removed; the elements you declare determine it.
Source code in src/apeGmsh/opensees/apesees.py
domain_capture ¶
domain_capture(spec: 'DomainCaptureSpec', *, path: 'str | Path', ops: Any = None) -> 'DomainCapture'
Open a :class:DomainCapture for in-process recording.
Live entry point that resolves the supplied
:class:DomainCaptureSpec against the bridge's fem
snapshot using the bridge's ndm / ndf, then returns a
:class:DomainCapture context manager writing to path.
Per Phase 9 D8 ndm / ndf are sourced implicitly from
the bridge — the user must have called ops.model(ndm=,
ndf=) first. Use :meth:DomainCapture.from_h5 instead when
no live bridge is available (sources ndm / ndf from a
model.h5 /meta block).
Example::
ops.model(ndm=3, ndf=6)
spec = DomainCaptureSpec(opensees=ops)
spec.nodes(pg="Top", components=["displacement"])
with ops.domain_capture(spec, path="run.h5") as cap:
cap.begin_stage("gravity", kind="static")
for _ in range(n):
ops.analyze(1, 1.0)
cap.step(t=ops.getTime())
cap.end_stage()
Raises¶
RuntimeError
If ops.model(ndm=, ndf=) has not been called yet.
Source code in src/apeGmsh/opensees/apesees.py
8127 8128 8129 8130 8131 8132 8133 8134 8135 8136 8137 8138 8139 8140 8141 8142 8143 8144 8145 8146 8147 8148 8149 8150 8151 8152 8153 8154 8155 8156 8157 8158 8159 8160 8161 8162 8163 8164 8165 8166 8167 8168 8169 8170 8171 8172 8173 8174 8175 8176 8177 8178 8179 8180 8181 8182 8183 8184 8185 8186 8187 8188 8189 8190 8191 8192 8193 8194 8195 | |
fix ¶
fix(*, pg: str | None = None, nodes: Iterable[int | Node] | None = None, dofs: tuple[int, ...]) -> None
Apply homogeneous SP constraints (fix).
Exactly one of pg / nodes must be supplied. nodes
accepts a mix of plain integer tags and :class:Node
instances (from ops.nodes.get(...)); both are normalized
to tags. The build pipeline expands pg to a per-node
fan-out at emit time.
Source code in src/apeGmsh/opensees/apesees.py
mass ¶
mass(*, pg: str | None = None, nodes: Iterable[int | Node] | None = None, values: tuple[float, ...], overwrite: bool = False) -> None
Attach lumped nodal mass.
Exactly one of pg / nodes must be supplied. nodes
accepts plain integers or :class:Node instances.
overwrite (Phase SSI-2.E) opts the record out of validator
V2's cross-tier duplicate-mass check. Rare at the global tier
but kept for symmetry with the stage-bound :meth:_StageBuilder.mass
— see that method for the typical use case.
Source code in src/apeGmsh/opensees/apesees.py
mass_from_model ¶
Stream per-node lumped masses straight from the model snapshot.
Equivalent to looping ops.mass(nodes=[m.node_id], values=m.mass)
over every entry in fem.nodes.masses (e.g. the per-node tributary
masses produced by g.masses.volume(...)), but without
materializing one bridge MassRecord per node — the snapshot
masses are streamed at emit time. On a multi-million-node model this
avoids a multi-GB resident list and millions of small objects (ADR
0065 Tier 2). Emits byte-identical deck lines and honours per-node
ndf via the same fit_dof_vector as :meth:mass.
Model-wide declaration (no arguments). May be combined with explicit
:meth:mass calls only on disjoint node sets — overlap raises at
emit (nodal mass is additive under MP assembly). Deck/live emit only;
the H5 archival emitter rejects it (masses already persist in
model.h5 via fem.nodes.masses).
Source code in src/apeGmsh/opensees/apesees.py
ndf ¶
State the per-node ndf of an element-LESS decoupled node
(ADR 0049 — the sole explicit per-node ndf channel).
Every other node's ndf is inferred from its incident element
classes (ADR 0048). ops.ndf exists only for nodes inference cannot
reach — a spring/dashpot ground, a control node, or a mass anchor
created via g.decouple_node(...) that no element touches.
Parameters¶
target
The decoupled-node handle returned by g.decouple_node(...) (a
DecoupledNodeDef) or its integer node tag. The handle is
resolved to its tag at build time (so a handle materialized
after meshing resolves correctly); a still-unmeshed handle fails
loud at build.
ndf
The DOF count to assign the node.
Raises (at build) :class:BridgeError if target is a mesh node, an
element-touched node (its ndf is inferred — restating it would create
a two-headed model), or an unresolved handle. The stated value is also
checked by gates G1–G3 (adaptive endpoints, constraint masters,
referenced fix/mass/load/sp DOFs).
Source code in src/apeGmsh/opensees/apesees.py
initial_stress ¶
initial_stress(*, name: str, pg: str | None = None, elements: Iterable[int] | None = None, sigma_xx: float, sigma_yy: float, sigma_zz: float, ramp_steps: int, lambda_install: float = 1.0) -> 'InitialStressRecord'
Initialize an in-situ stress tensor on ASDPlasticMaterial3D elements.
Emits the OpenSees parameter / addToParameter /
updateParameter ramp pattern that STKO uses to inject a
pre-stressed state without applying gravity-driven body loads.
The factor ramps linearly 0 → 1 over ramp_steps analyze
calls and plateaus at 1.0 thereafter; the target stress baked
into the ramp is sigma_* × lambda_install, so passing
lambda_install < 1.0 produces a partial-installation
(convergence-confinement) result.
Exactly one of pg / elements must be supplied. This
primitive is declarative only — the actual stress
advancement happens at analyze time, via the per-step
dispatcher this primitive registers with. Call
ops.analyze(steps=ramp_steps, dt=...) or pass
analyze_steps=ramp_steps to :meth:tcl / :meth:py for
the ramp to take effect.
Parameters¶
name
Unique Tcl-identifier-safe label. Used to name the
emitted proc / state container.
pg
Physical group whose elements receive the ramped stress.
elements
Explicit list of FEM element ids. XOR with pg.
sigma_xx, sigma_yy, sigma_zz
Target Cauchy stress per component (compression negative).
ramp_steps
Number of analyze steps over which the factor reaches 1.0.
Must be >= 1.
lambda_install
Fraction of target to install (default 1.0). Must be in
(0, 1].
Source code in src/apeGmsh/opensees/apesees.py
8320 8321 8322 8323 8324 8325 8326 8327 8328 8329 8330 8331 8332 8333 8334 8335 8336 8337 8338 8339 8340 8341 8342 8343 8344 8345 8346 8347 8348 8349 8350 8351 8352 8353 8354 8355 8356 8357 8358 8359 8360 8361 8362 8363 8364 8365 8366 8367 8368 8369 8370 8371 8372 8373 8374 8375 8376 8377 8378 8379 8380 8381 | |
convergence_confinement ¶
convergence_confinement(*, name: str, pg: str | None = None, elements: Iterable[int] | None = None, sigma_xx: float = 0.0, sigma_yy: float = 0.0, sigma_zz: float = 0.0, lambda_target: float, n_steps: int) -> 'InitialStressRecord'
Convergence-confinement helper (Phase SSI-3).
Thin wrapper over :meth:initial_stress for the tunnelling
convergence-confinement pattern: ramp a target stress on a
boundary region to lambda_target × sigma over
n_steps analyze steps. Matches the
_stressCtrl_11-style proc from
SSI/Interaccion/analysis_steps.tcl:19753-19767.
Differs from :meth:initial_stress in two cosmetic ways:
lambda_target(renamed fromlambda_install) — more natural reading at the call site for confinement / relaxation contexts.n_steps(renamed fromramp_steps) — matches the spec's naming.
At least one of sigma_xx / sigma_yy / sigma_zz must
be non-zero (typically only one — single-component relaxation
is the canonical SSI use case).
Returns the underlying :class:InitialStressRecord; pass it to
s.add(...) inside a stage block to bind to that stage.
Parameters¶
name
Unique Tcl-identifier-safe label.
pg, elements
Same XOR semantics as :meth:initial_stress.
sigma_xx, sigma_yy, sigma_zz
Target Cauchy stress per component (compression negative).
At least one must be non-zero.
lambda_target
Fraction of target stress to install — i.e. the relaxation
(or confinement) coefficient. Must be in (0, 1].
n_steps
Number of analyze steps over which the factor reaches 1.0
internally. After the cap, the cumulative is
sigma × lambda_target.
Source code in src/apeGmsh/opensees/apesees.py
8383 8384 8385 8386 8387 8388 8389 8390 8391 8392 8393 8394 8395 8396 8397 8398 8399 8400 8401 8402 8403 8404 8405 8406 8407 8408 8409 8410 8411 8412 8413 8414 8415 8416 8417 8418 8419 8420 8421 8422 8423 8424 8425 8426 8427 8428 8429 8430 8431 8432 8433 8434 8435 8436 8437 8438 8439 8440 8441 8442 8443 8444 8445 8446 8447 8448 8449 8450 | |
imposed_displacement ¶
imposed_displacement(*, pg: str | None = None, nodes: Iterable[int] | None = None, ux: float | None = None, uy: float | None = None, uz: float | None = None, pattern_factor: float = 1.0, series: 'TimeSeries | None' = None) -> 'Plain'
Imposed-displacement pattern helper (Phase SSI-3).
Emits one pattern Plain containing sp NODE DOF VALUE
prescribed-displacement entries for every (node, dof) pair
where the corresponding ux / uy / uz is non-None.
Used for fault-slip kinematics, support-settlement scenarios,
and any other prescribed-displacement driver.
STKO equivalent:
pattern Plain N tsTag -fact F { sp NODE DOF VAL ... }
from SSI/Interaccion y Falla/analysis_steps.tcl:22832-23253.
Where STKO uses -fact F on the pattern, this helper folds
the same scaling into the auto-created Linear(factor=F)
time series — numerically identical, simpler API.
Parameters¶
pg, nodes
XOR: exactly one of pg (physical-group name) or
nodes (iterable of FEM node ids) must be supplied.
ux, uy, uz
Scalar broadcast: every targeted node gets the same
prescribed displacement in this DOF. None (default)
skips the DOF. At least one of the three must be set.
pattern_factor
Multiplier folded into the auto-created Linear time
series. Default 1.0 (no scaling). Matches STKO's
-fact F semantics: the actual applied displacement
at simulation-time t is
value × pattern_factor × t.
series
Optional explicit :class:TimeSeries to use. Must be
already registered with the bridge. When supplied,
pattern_factor is ignored — the user is in full
control of the time-history shape.
Returns¶
Plain
The registered :class:Plain pattern. This is a global
(non-staged) pattern: it is valid only in a non-staged deck
(global pattern + ops.analyze). Per ADR 0051 §5 a model
may not mix a global pattern with stages — combining this
with ops.stage(...) raises :class:BridgeError at build.
For prescribed motion inside a staged deck, author the sp
on a stage pattern instead (with s.pattern(series=...) as
p: p.sp(...)).
Notes¶
Per-node-varying displacements are NOT supported in v1 —
every targeted node gets the same scalar. For different
values per node, call imposed_displacement multiple times
with disjoint nodes= lists, or construct the Plain
pattern manually via ops.pattern.Plain(...).
Source code in src/apeGmsh/opensees/apesees.py
8452 8453 8454 8455 8456 8457 8458 8459 8460 8461 8462 8463 8464 8465 8466 8467 8468 8469 8470 8471 8472 8473 8474 8475 8476 8477 8478 8479 8480 8481 8482 8483 8484 8485 8486 8487 8488 8489 8490 8491 8492 8493 8494 8495 8496 8497 8498 8499 8500 8501 8502 8503 8504 8505 8506 8507 8508 8509 8510 8511 8512 8513 8514 8515 8516 8517 8518 8519 8520 8521 8522 8523 8524 8525 8526 8527 8528 8529 8530 8531 8532 8533 8534 8535 8536 8537 8538 8539 8540 8541 8542 8543 8544 8545 8546 8547 8548 8549 8550 8551 8552 8553 8554 8555 8556 8557 8558 8559 8560 8561 8562 8563 8564 8565 8566 8567 8568 8569 8570 8571 8572 8573 8574 8575 8576 8577 8578 8579 | |
stage ¶
Open a staged-analysis block (Phase SSI-2.A).
Nested with ops.stage(...) blocks are NOT supported —
opening a second stage builder while another is still open
raises RuntimeError. The lexical-vs-emit-order semantics
would otherwise be confusing (the inner builder's exit
fires first, registering the inner stage BEFORE the outer in
_stage_records, which is the opposite of what readers
expect).
Usage::
with ops.stage(name="insitu") as s:
s.add(ops.initial_stress(name="rock", ..., ramp_steps=10))
s.analysis(
test=ops.test.NormDispIncr(tol=1e-4, max_iter=150),
algorithm=ops.algorithm.Newton(),
integrator=ops.integrator.LoadControl(dlam=0.1),
constraints=ops.constraints.Plain(),
numberer=ops.numberer.RCM(),
system=ops.system.UmfPack(),
analysis=ops.analysis.Static(),
)
s.run(n_increments=10, dt=0.1)
Each stage emits its own analysis-chain primitives, its own
analyze loop (hook-wrapped if any s.add(initial_stress(...))
registered a ramp), and a between-stages cleanup block
(loadConst -time 0.0 + wipeAnalysis + hook-list clear).
Multiple with ops.stage(...) blocks accumulate in
registration order; they emit in that order at deck-emit time.
Validation happens on with exit: every stage must have a
complete analysis chain (all six chain kwargs + the analysis
directive) and an s.run(...) call.
Returns¶
_StageBuilder
Context manager that collects per-stage records and emits
a :class:StageRecord to the bridge on close.
Source code in src/apeGmsh/opensees/apesees.py
region ¶
Assign nodes to a named OpenSees Region.
Each name collects all nodes registered against it
(across multiple calls, across explicit nodes= and
pg= resolutions) and emits a single
region $tag -node n1 n2 ... line at build time with a
freshly allocated region tag. Useful for damping
assignments and any future recorder that filters by region.
Exactly one of pg / nodes must be supplied; nodes
accepts a mix of plain integer tags and :class:Node
instances (matching :meth:fix / :meth:mass).
End users typically call this through :meth:Node.region or
:meth:NodeSet.region rather than directly.
Source code in src/apeGmsh/opensees/apesees.py
analyze ¶
analyze(*, steps: int, dt: float | None = None, strategy: 'Ladder | None' = None, profile: str | None = None, profile_run: str | None = None, profile_deep: bool = False, profile_memory: bool = False, profile_per_step: bool = False) -> int
Build + emit + run the analysis chain via the live emitter.
Builds a :class:BuiltModel, drives a
:class:~apeGmsh.opensees.emitter.live.LiveOpsEmitter end-to-
end, then issues the analyze call. Returns the openseespy
analyze return value (0 on success).
strategy (ADR 0057 Phase A) attaches a solution-strategy
ladder to the analyze loop — on a failed increment the live
runner escalates through the ladder's algorithm rungs (the
declared chain algorithm is rung 0), restoring rung 0 after a
rescue and logging escalations to the live emitter's
strategy_events. Exhaustion returns the failing rc.
When profile is given, the live run is bracketed by the Ladruno
fork's stack profiler: profiler start [flags] before the analyze
loop and profiler report <profile> [-run profile_run] after,
with profile_deep / profile_memory / profile_per_step
toggling the start flags. Requires the fork build — the live
emitter raises a clear error on stock openseespy. (Deck-mode
profiling uses the explicit ops.profiler.* verbs instead, and
does NOT consume the profile= kwargs here.)
Raises :class:BridgeError if the analysis chain is incomplete
(one or more of constraints / numberer / system / test /
algorithm / integrator / analysis is missing).
Phase SSI-2.A: staged models (ops.stage(...) blocks
declared) are NOT supported by live execution. Emit a Tcl
or Py deck via :meth:tcl / :meth:py and run it via the
OpenSees binary / openseespy subprocess instead.
Source code in src/apeGmsh/opensees/apesees.py
8698 8699 8700 8701 8702 8703 8704 8705 8706 8707 8708 8709 8710 8711 8712 8713 8714 8715 8716 8717 8718 8719 8720 8721 8722 8723 8724 8725 8726 8727 8728 8729 8730 8731 8732 8733 8734 8735 8736 8737 8738 8739 8740 8741 8742 8743 8744 8745 8746 8747 8748 8749 8750 8751 8752 8753 8754 8755 8756 8757 8758 8759 8760 8761 8762 8763 8764 8765 8766 8767 8768 8769 8770 8771 8772 8773 8774 8775 8776 8777 8778 8779 8780 8781 8782 8783 8784 8785 8786 8787 8788 8789 8790 8791 8792 | |
ladruno_projection_tie_force ¶
Tie force f = M(a_raw - a_proj) at (node, dof) from the last
projection step (≈ LS-DYNA *DATABASE_NCFORC).
Recovers the interface force a non-matching equation-tied interface
(g.constraints.tie(..., enforce="equation")) carries, via the fork
ladrunoProjectionTieForce query (ADR-30 P3 / ADR 0068 P5). dof
is 1-based (OpenSees convention).
Requires a prior live :meth:analyze with a LadrunoProjection
constraint handler active. Fork-only: a stock build raises
RuntimeError (see :data:~apeGmsh.opensees.emitter.live.
_TIE_FORCE_FORK_REQUIRED).
For a recorded time history of the tie force instead of a single
post-run value, use the recorder route:
ops.recorder.Ladruno(nodal_responses=("constraintTieForce",)) and
read it back with
results.nodes.get(component="constraint_tie_force_x") (explicit
analyses only — the recorder channel is scattered by the explicit
CentralDifferenceLadruno integrator).
Source code in src/apeGmsh/opensees/apesees.py
ladruno_contact_force ¶
Total normal contact-force magnitude on an NTS slave node.
The sum over that node's active master-segment pairs of
tn = kn·<−gap>₊ (fork ladrunoContactForce, ADR-39 B3). Works in
2-D and 3-D. Requires a prior live :meth:analyze; fork-only.
Four limits, none of which the return value can tell you about — read them before using this number:
- NTS lane only. It is fed exclusively from the segment / end-cap
branch, so a mortar or rigid-plane slave always reads
0.0. Those lanes have no force query at all; recover their forces from reactions or the penalty-depth identity instead. - A magnitude, not a vector. Near a corner or the 2-D D4 end-cap the pair normal is not axis-aligned, so this does not equal any single global force component. The fork's own guide says so.
- Zero is ambiguous.
0.0means "not in contact" and "no contact engine in this domain". Call :meth:ladruno_contact_infoand checktotal_contactsto tell them apart — notn_contacts, which counts the NTS lane only and reads0on a perfectly live mortar-only model. - A released 3-D pair reports its last-active force forever — a
known, deferred fork defect (reproduced at
f_query = 1000.0againstf_true = 0.0). The 2-D lane carries the fix.
Source code in src/apeGmsh/opensees/apesees.py
ladruno_contact_info ¶
Engine counters — (n_contacts, n_commits, n_reverts,
n_mortar_contacts) (fork ladrunoContactInfo).
Mostly useful as the disambiguator for the other three queries: they
all return 0.0 both for "nothing happening here" and for "no
contact engine at all". Use info.total_contacts, the sum of the two
lane counters — n_contacts and n_mortar_contacts are disjoint
lanes, not a total and a subset, so a mortar-only model reports
n_contacts == 0 with a live engine (measured on fork
b17e8bd82). Requires a prior live :meth:analyze; fork-only.
Source code in src/apeGmsh/opensees/apesees.py
ladruno_mortar_penetration ¶
Max KKT-active normal penetration over all mortar slave nodes
(fork ladrunoMortarPenetration, ADR-41 C2.2).
A length, not a force — dimension-blind, and unaffected by the
mortar thickness=. It is the mortar lane's ALM convergence measure:
the quantity a held-load augmentation loop watches to decide it has
augmented enough. 0.0 with no mortar contact. Requires a prior live
:meth:analyze; fork-only.
Source code in src/apeGmsh/opensees/apesees.py
ladruno_mortar_tie_residual ¶
Max weighted relative-displacement bond residual over all mortar
tie slave nodes (fork ladrunoMortarTieResidual, ADR-41 C4).
The tie's ALM convergence measure, the counterpart of
:meth:ladruno_mortar_penetration for tie=True. 0.0 with no
tie declared. Requires a prior live :meth:analyze; fork-only.
Source code in src/apeGmsh/opensees/apesees.py
eigen ¶
Build + emit + run a one-shot eigen solve via the live emitter.
Builds a :class:BuiltModel, drives a
:class:~apeGmsh.opensees.emitter.live.LiveOpsEmitter end-to-
end (model + nodes + elements + bcs + mass), then issues the
single eigen call and returns an :class:EigenResult
carrying the eigenvalues plus a back-reference to the live
emitter for lazy mode-shape access.
Unlike :meth:analyze, eigen does NOT require an analysis
chain (constraints / numberer / system / test / algorithm /
integrator / analysis): it only needs the assembled stiffness
and mass matrices.
Partitioned models — serial-gather stopgap (ADR 0077 Tier 0).
On a partition-authored model this runs the eigensolve serially
on the full, gathered model in one process (the live emitter has
supports_partitions = False): the modes are exact, but the
whole model is assembled on one rank, so it does not scale the
eigensolve. There is no distributed modal path yet — never run a
bare eigen under OpenSeesMP (it solves each rank's LOCAL
subdomain → wrong modes; ADR 0077 refuted v1). Distributed FEAST
(ADR 0077 Tier 1) is gated on the classic-Tcl -feast unlock.
Parameters¶
num_modes
Number of modes to compute. Must be >= 1.
solver
OpenSees eigen-solver flag, one of -genBandArpack
(default), -symmBandLapack, -fullGenLapack,
-frequency, -standard. Passed through verbatim to
ops.eigen(solver, num_modes).
Returns¶
EigenResult
Carries eigenvalues (λ_i = ω_i²) plus derived
omega / freq / periods and a
:meth:EigenResult.mode_shape accessor.
Raises¶
ValueError
If num_modes < 1.
NotImplementedError
If the model has any registered stages — live execution
of staged models is unsupported (Phase SSI-2.A).
Source code in src/apeGmsh/opensees/apesees.py
8907 8908 8909 8910 8911 8912 8913 8914 8915 8916 8917 8918 8919 8920 8921 8922 8923 8924 8925 8926 8927 8928 8929 8930 8931 8932 8933 8934 8935 8936 8937 8938 8939 8940 8941 8942 8943 8944 8945 8946 8947 8948 8949 8950 8951 8952 8953 8954 8955 8956 8957 8958 8959 8960 8961 8962 8963 8964 8965 8966 8967 8968 8969 8970 8971 8972 8973 8974 8975 8976 8977 8978 8979 8980 8981 8982 8983 8984 8985 8986 8987 8988 8989 8990 | |
modal_properties ¶
modal_properties(num_modes: int, *, solver: str = '-genBandArpack', unorm: bool = False) -> 'ModalPropertiesResult'
Build + emit + run eigen + modalProperties live.
Like :meth:eigen, drives a
:class:~apeGmsh.opensees.emitter.live.LiveOpsEmitter end-to-end
and needs no analysis chain; after the eigen solve it issues
modalProperties -return (upstream DomainModalProperties)
and wraps the returned dict in a
:class:~apeGmsh.opensees.analysis.modal.ModalPropertiesResult
carrying participation factors, modal masses, and mass ratios
per mode and per global component.
The properties are also stored on the OpenSees Domain, which is the prerequisite state for the Ladruno fork's modal-response commands (fork ADR 44).
Partitioned models — serial-gather stopgap (ADR 0077 Tier 0).
Runs serially on the full, gathered model (see :meth:eigen), so
participation factors / effective modal mass are correct here.
This is the only correct way to get modal properties on a
partition-authored model today: the distributed path (ADR 0077
Tier 1) has no participation surface — upstream
modalProperties is MPI-blind — so a distributed run would
return wrong effective mass. It does not scale the eigensolve
(whole model on one rank).
Parameters¶
num_modes
Number of modes to compute. Must be >= 1.
solver
OpenSees eigen-solver flag, passed through verbatim (see
:meth:eigen). Use -fullGenLapack on tiny models —
ARPACK needs num_modes < n_dof.
unorm
Request the displacement-normalized eigenvector scaling
(modalProperties -unorm).
Raises¶
ValueError
If num_modes < 1.
NotImplementedError
If the model has any registered stages — live execution
of staged models is unsupported (Phase SSI-2.A).
Source code in src/apeGmsh/opensees/apesees.py
8992 8993 8994 8995 8996 8997 8998 8999 9000 9001 9002 9003 9004 9005 9006 9007 9008 9009 9010 9011 9012 9013 9014 9015 9016 9017 9018 9019 9020 9021 9022 9023 9024 9025 9026 9027 9028 9029 9030 9031 9032 9033 9034 9035 9036 9037 9038 9039 9040 9041 9042 9043 9044 9045 9046 9047 9048 9049 9050 9051 9052 9053 9054 9055 9056 9057 9058 9059 9060 9061 9062 9063 9064 9065 9066 9067 9068 9069 9070 9071 9072 9073 9074 | |
eigen_feast ¶
Band-targeted FEAST eigensolve via the live emitter.
Fork-only (Ladruno ADR-43): eigen -feast fmin fmax
returns all modes whose natural frequency lies in
[f_min, f_max] Hz — the mode count is an output
(len(result.eigenvalues)), not an input, which is why this
is a separate method and not an :meth:eigen solver flag.
certify=True adds the fork's Sturm/inertia completeness
certificate: the band content is independently counted via
LDLᵀ inertia at the two band edges and the solve REFUSES on a
mismatch with FEAST's count.
Parameters¶
f_min, f_max
Frequency band in Hz; needs 0 <= f_min < f_max.
certify
Emit -certify (the completeness certificate).
Returns¶
EigenResult
The standard eigen result (possibly zero modes if the band
is empty) with lazy mode_shape access.
Source code in src/apeGmsh/opensees/apesees.py
9076 9077 9078 9079 9080 9081 9082 9083 9084 9085 9086 9087 9088 9089 9090 9091 9092 9093 9094 9095 9096 9097 9098 9099 9100 9101 9102 9103 9104 9105 9106 9107 9108 9109 9110 9111 9112 9113 9114 9115 9116 9117 9118 9119 9120 9121 9122 9123 9124 9125 9126 9127 9128 9129 9130 9131 9132 9133 9134 9135 9136 9137 9138 9139 9140 9141 9142 9143 9144 9145 | |
complex_eigen ¶
complex_eigen(num_modes: int, *, solver: str = '-genBandArpack', tol: float | None = None, closed_form: bool = False) -> 'ComplexEigenResult'
Complex / state-space modal analysis via the live emitter.
Fork-only (Ladruno ADR-46, complexEigen): true per-mode
damping ratios ζ_k, damped frequencies ω_d,k, and phased mode
shapes for non-classically damped models (localized
dashpots, bearings, radiation damping). Builds + emits a fresh
live domain, runs the real eigen (the projection basis),
then complexEigen and parses the flat 7-per-mode return
into a :class:ComplexEigenResult.
The default route projects the model's actual M and C
(element getDamp()/getMass() + nodal mass/alphaM) —
exactly the C a transient analysis feels. closed_form=True
uses the fast global-Rayleigh diagonal closed form instead
(refuses betaKinit/betaKcomm; blind to scoped
Rayleigh).
Contract traps (fork guide): damping that does not flow through
getDamp() is invisible (modalDamping, HHT-α numerical
damping, elements whose -doRayleigh defaults OFF — the
Truss/zeroLength families); the projection spans only
the retained num_modes real modes; complex mode shapes are
recorded via Node-recorder raw=("complexEigenRe<k>",) /
Im<k> tokens, not carried on this result.
Parameters¶
num_modes
Real modes to extract as the projection basis (retain
enough to cover the band of interest;
-fullGenLapack on tiny models).
tol
Optional residual tolerance (fork default 1e-8).
closed_form
Use the closed-form Rayleigh route (Route A).
Source code in src/apeGmsh/opensees/apesees.py
9147 9148 9149 9150 9151 9152 9153 9154 9155 9156 9157 9158 9159 9160 9161 9162 9163 9164 9165 9166 9167 9168 9169 9170 9171 9172 9173 9174 9175 9176 9177 9178 9179 9180 9181 9182 9183 9184 9185 9186 9187 9188 9189 9190 9191 9192 9193 9194 9195 9196 9197 9198 9199 9200 9201 9202 9203 9204 9205 9206 9207 9208 9209 9210 9211 9212 9213 9214 9215 9216 9217 9218 9219 9220 9221 9222 9223 9224 9225 9226 9227 9228 9229 9230 9231 9232 | |
modal_response_history ¶
modal_response_history(*, dt: float, n_steps: int, num_modes: int, base_accel: 'TimeSeries | str | None' = None, direction: int | None = None, load: 'Plain | str | None' = None, series: 'TimeSeries | str | None' = None, damp: float | None = None, rayleigh: tuple[float, float] | None = None, modal_damp: Sequence[float] | None = None, modes: Sequence[int] | None = None, t0: float = 0.0, solver: str = '-genBandArpack') -> 'ModalHistoryResult'
Run the fork's exact modal-superposition transient live.
Fork-only (Ladruno ADR-44 P1a, modalResponseHistory).
Builds + emits a fresh live domain, issues eigen +
modalProperties, then advances each retained mode by the
closed-form piecewise-linear recurrence — no iteration, no
factorization. One domain step is committed per station
(t0 … t0 + n_steps·dt), so every recorder declared on the
model captures the history exactly as in a direct run.
Linear models only — superposition is invalid under any
material or geometric nonlinearity (use analyze then).
Parameters¶
dt, n_steps
Time step and station count (n_steps + 1 commits).
num_modes
Modes to extract for the superposition basis (retain
enough to cover the band of interest).
base_accel, direction
Ground-acceleration channel: a registered
ops.timeSeries.* handle (or name) sampled at the
stations, plus the global excitation direction (1-based).
Response is relative to the moving base. Make the
record extend at least one sample past t0 + n_steps·dt.
load, series
Nodal-force channel P(t) = s(t)·P: an
ops.pattern.Plain handle (or name) whose plain nodal
loads give the reference shape P (the pattern's own
timeSeries is IGNORED by the fork), and the scalar
s(t) timeSeries. Response is absolute. Mutually
exclusive with the base-acceleration channel.
damp, rayleigh, modal_damp
Exactly one damping channel (ADR 0075): uniform ratio /
Rayleigh (a0, a1) / per-mode ratios.
modes
Optional 1-based subset of the extracted modes.
t0
Start time (base accel sampled at t0 + k·dt).
solver
Eigen-solver flag (-fullGenLapack on tiny models).
Source code in src/apeGmsh/opensees/apesees.py
9251 9252 9253 9254 9255 9256 9257 9258 9259 9260 9261 9262 9263 9264 9265 9266 9267 9268 9269 9270 9271 9272 9273 9274 9275 9276 9277 9278 9279 9280 9281 9282 9283 9284 9285 9286 9287 9288 9289 9290 9291 9292 9293 9294 9295 9296 9297 9298 9299 9300 9301 9302 9303 9304 9305 9306 9307 9308 9309 9310 9311 9312 9313 9314 9315 9316 9317 9318 9319 9320 9321 9322 9323 9324 9325 9326 9327 9328 9329 9330 9331 9332 9333 9334 9335 9336 9337 9338 9339 9340 9341 9342 9343 9344 9345 9346 9347 9348 9349 9350 9351 9352 9353 9354 9355 9356 9357 9358 9359 9360 9361 9362 9363 9364 | |
response_spectrum_analysis ¶
response_spectrum_analysis(direction: int, *, periods: Sequence[float], accels: Sequence[float], combine: str, num_modes: int, damp: float | None = None, modal_damp: Sequence[float] | None = None, solver: str = '-genBandArpack') -> 'ResponseSpectrumResult'
Run a response-spectrum analysis with native combination.
Fork-only (Ladruno ADR-44 P1b): the -combine stage on
responseSpectrumAnalysis. Builds + emits a fresh live
domain, issues eigen + modalProperties, computes the
per-mode modal displacements against the (periods, accels)
design spectrum, and commits the combined nodal design
displacement field, read back via
:meth:ResponseSpectrumResult.node_disp.
Combination is per-quantity and nonlinear — do NOT derive combined element forces / drifts from the combined displacements (combine those quantities' own per-mode peaks instead).
Parameters¶
direction
Global excitation direction (1-based).
periods, accels
The design spectrum Sa(Tn) as parallel lists.
periods must be non-negative and strictly increasing;
a leading T = 0 PGA anchor is legal (the fork clamps
T <= Tn[0] to Sa[0]).
combine
"SRSS" | "CQC" | "ABS" | "TenPercent".
CQC and TenPercent weight closely-spaced modes; CQC
requires a damping channel.
num_modes
Modes to extract; the combination spans all of them
(-combine and -mode are mutually exclusive — the
bridge never emits -mode).
damp, modal_damp
Optional damping channel (uniform ratio or per-mode).
Required for CQC.
Source code in src/apeGmsh/opensees/apesees.py
9366 9367 9368 9369 9370 9371 9372 9373 9374 9375 9376 9377 9378 9379 9380 9381 9382 9383 9384 9385 9386 9387 9388 9389 9390 9391 9392 9393 9394 9395 9396 9397 9398 9399 9400 9401 9402 9403 9404 9405 9406 9407 9408 9409 9410 9411 9412 9413 9414 9415 9416 9417 9418 9419 9420 9421 9422 9423 9424 9425 9426 9427 9428 9429 9430 9431 9432 9433 9434 9435 9436 9437 9438 9439 9440 9441 9442 9443 9444 9445 9446 9447 9448 9449 9450 9451 9452 9453 9454 9455 9456 9457 9458 9459 9460 9461 9462 9463 9464 9465 9466 9467 9468 9469 9470 9471 9472 9473 9474 9475 9476 | |
frequency_response ¶
frequency_response(*, f_min: float, f_max: float, n_freq: int, node: 'int | Node', dof: int, num_modes: int, grid: str = 'lin', base_accel_dir: int | None = None, load: 'Plain | str | None' = None, amp: float = 1.0, damp: float | None = None, rayleigh: tuple[float, float] | None = None, modal_damp: Sequence[float] | None = None, resp: str = 'disp', modes: Sequence[int] | None = None, out: str | None = None, solver: str = '-genBandArpack') -> 'FrequencyResponseResult'
Compute the complex modal FRF of one response DOF, live.
Fork-only (Ladruno ADR-44 P2, frequencyResponse): for a
harmonic excitation amp·e^{iΩt} the steady response is a
dense post-processor on the mode basis — no time stepping.
Returns a :class:FrequencyResponseResult (frequencies in Hz
+ complex FRF).
Excitation: base_accel_dir= for uniform harmonic base
acceleration along a global direction (no timeSeries — the
sweep is per amp; relative response) XOR load= for
harmonic nodal forces amp·P·e^{iΩt} from a plain-nodal-load
pattern (absolute response).
grid: "lin" / "log" / "biased" — biased adds a
±5 % cluster around each in-band modal frequency so sharp
low-damping peaks are not stepped over.
resp: "disp" | "vel" (iΩ·û) | "accel"
(−Ω²·û). out= additionally writes the table to an
ASCII file.
Source code in src/apeGmsh/opensees/apesees.py
steady_state_dynamics ¶
steady_state_dynamics(*, f_min: float, f_max: float, n_freq: int, node: 'int | Node', dof: int, num_modes: int, grid: str = 'lin', base_accel_dir: int | None = None, load: 'Plain | str | None' = None, amp: float = 1.0, damp: float | None = None, rayleigh: tuple[float, float] | None = None, modal_damp: Sequence[float] | None = None, resp: str = 'disp', modes: Sequence[int] | None = None, out: str | None = None, solver: str = '-genBandArpack') -> 'SteadyStateResult'
Steady-state harmonic response amplitude |response| per
sweep frequency — the magnitude companion of
:meth:frequency_response (same flags, fork ADR-44 P2).
Source code in src/apeGmsh/opensees/apesees.py
random_response ¶
random_response(*, f_min: float, f_max: float, n_freq: int, node: 'int | Node', dof: int, num_modes: int, input_psd: 'TimeSeries | str', grid: str = 'biased', base_accel_dir: int | None = None, load: 'Plain | str | None' = None, damp: float | None = None, rayleigh: tuple[float, float] | None = None, modal_damp: Sequence[float] | None = None, resp: str = 'disp', modes: Sequence[int] | None = None, stats: bool = False, duration: float | None = None, out: str | None = None, solver: str = '-genBandArpack') -> 'RandomResponseResult'
Stationary random response RMS on the modal FRF, live.
Fork-only (Ladruno ADR-44 P3, randomResponse):
input_psd is a one-sided PSD G(f) in Hz ((excitation)²/
Hz), supplied as a registered timeSeries sampled at f in Hz
(Path with f→G breakpoints, Constant for white noise).
With base_accel_dir= it is the base-acceleration PSD; with
load= the PSD of the scalar multiplying the pattern's
nodal-load shape (fully correlated).
grid defaults to "biased" — the RMS is a band integral
and a linear grid mis-integrates sharp resonances (fork guide
P3). The band [f_min, f_max] must cover the input's
support and every resonance carrying response power; the fork
refuses zero-damped in-band modes and a rigid-body mode with
f_min = 0.
stats= adds ν₀ (mean zero-upcrossing rate, Hz) and the
spectral moments m0 / m2; duration= additionally
appends the Davenport expected peak over that exposure.
Source code in src/apeGmsh/opensees/apesees.py
9725 9726 9727 9728 9729 9730 9731 9732 9733 9734 9735 9736 9737 9738 9739 9740 9741 9742 9743 9744 9745 9746 9747 9748 9749 9750 9751 9752 9753 9754 9755 9756 9757 9758 9759 9760 9761 9762 9763 9764 9765 9766 9767 9768 9769 9770 9771 9772 9773 9774 9775 9776 9777 9778 9779 9780 9781 9782 9783 9784 9785 9786 9787 9788 9789 9790 9791 9792 9793 9794 9795 9796 9797 9798 9799 9800 9801 9802 9803 9804 9805 9806 9807 9808 9809 9810 9811 9812 | |
critical_time_step ¶
Query the active explicit integrator's critical time step dt_cr.
Fork-only (Ladruno): builds + emits a throwaway live model
(like :meth:eigen), primes one tiny step to trigger the
integrator's dt_cr computation, then returns the usable
(Noh-Bathe) limit.
Requires a complete analysis chain with an explicit
integrator constructed with cfl=True (e.g.
ops.integrator.ExplicitBathe(cfl=True)), a Transient
analysis, and element mass density (-rho / -mass) —
the dt_cr eigensolve uses element mass+stiffness, not
ops.mass nodal mass.
Raises¶
BridgeError
If the analysis chain is incomplete.
NotImplementedError
If the model has registered stages (live execution of staged
models is unsupported — emit Tcl/Py instead).
ValueError
If dt_cr is not usable (no cfl flag, a non-explicit
integrator, or a pure nodal-mass model).
Source code in src/apeGmsh/opensees/apesees.py
analyze_explicit ¶
analyze_explicit(*, duration: float, safety: float = 0.9, dt_max: float | None = None) -> 'ExplicitRunResult'
Run an explicit transient over duration, auto-sized to dt_cr.
Fork-only (Ladruno) driver implementing the explicit-dynamics
sub-stepping recipe (ADR D5): build + emit, prime one tiny step,
query the critical time step, then integrate duration in
n = ceil(duration / (safety * dt_cr)) equal sub-steps via a
single analyze(n, duration / n).
.. warning::
dt_cr is queried once, on the initial stiffness. For a
model whose tangent stiffens mid-run (contact closing,
geometric / material stiffening) the true critical step shrinks
and a fixed dt can go supercritical and diverge. Guard such
runs by constructing the integrator with cfl_abort=True (and
recompute=N) so a recomputed CFL violation aborts the run —
this method then re-raises that abort as an error rather than
returning silently. A one-shot run with an unguarded integrator
emits :class:OpenSeesExplicitSolverWarning.
Parameters¶
duration
Total physical time to integrate (> 0).
safety
Fraction of dt_cr used as the step (0 < safety <= 1;
default 0.9). Scales the value criticalTimeStep()
returns — do not re-base it on any larger Noh-Bathe bound.
dt_max
Optional upper bound on the sub-step — use a step finer than
stability requires (e.g. for output resolution). > 0.
Returns¶
ExplicitRunResult
(n, dt, dt_cr) — the sub-step count, the step actually used,
and the queried critical time step.
Raises¶
BridgeError / NotImplementedError / ValueError
As for :meth:critical_time_step, plus ValueError for an
out-of-range duration / safety / dt_max.
RuntimeError
If the explicit analyze returns non-zero (divergence, or a
mid-run -cflAbort when the integrator is guarded).
Source code in src/apeGmsh/opensees/apesees.py
9977 9978 9979 9980 9981 9982 9983 9984 9985 9986 9987 9988 9989 9990 9991 9992 9993 9994 9995 9996 9997 9998 9999 10000 10001 10002 10003 10004 10005 10006 10007 10008 10009 10010 10011 10012 10013 10014 10015 10016 10017 10018 10019 10020 10021 10022 10023 10024 10025 10026 10027 10028 10029 10030 10031 10032 10033 10034 10035 10036 10037 10038 10039 10040 10041 10042 10043 10044 10045 10046 10047 10048 10049 10050 10051 10052 10053 10054 10055 10056 10057 10058 10059 10060 10061 10062 10063 | |
tcl ¶
tcl(path: str, *, run: bool = False, bin: str | None = None, analyze_steps: int | None = None, analyze_dt: float | None = None, split: bool = False, per_rank: bool = False, flat: bool = False, stream: bool = False, verbose: bool = False, log: str | None = None, progress: bool = True) -> None
Emit a Tcl deck to path; optionally subprocess OpenSees.
When run=True the OpenSees subprocess output is always
tee'd to a log file — log (a path) overrides, otherwise it
is <path>.log next to the deck. Console output is opt-in via
verbose: False (default) prints begin / op / end only;
True adds a live step counter (parsed from the
APEGMSH_PROGRESS markers progress=True injects into the
analyze loop) plus streamed warning lines. A non-zero exit
raises RuntimeError carrying the log tail + path, never the
whole buffer. verbose / log / progress are inert
when run=False.
When analyze_steps is supplied, an analyze line is
appended to the deck after every other primitive — wrapped in
a hook-dispatching for-loop if any
:meth:initial_stress calls registered step hooks (Phase
SSI-1). Without analyze_steps, the emitted deck declares
the model but does not drive an analysis.
split=True (ADR 0043 slice 1.1, mode A) writes a driver
deck at path plus one parts/<module>.tcl fragment per
composed module (g.compose); the driver sources each
fragment. The split is canonical — by compose module, no
free-form carve — and changes only the on-disk layout: the
default split=False writes the single self-contained deck,
byte-identical to the pre-0043 output. Requires a composed
model; partitioned / staged / initial_stress models are not
supported under split.
per_rank=True (ADR 0061) writes a driver deck at path
plus one ranks/rank<K>_<seq>.tcl fragment per
if {[getPID] == K} { ... } block; the driver guards each
fragment behind a one-line source so every MPI rank parses
only the driver plus its own fragments — O(global + model/np)
instead of O(model) per rank. Layout-only: the deck semantics
(including the single-process rank-0 fallback) are unchanged.
Requires a partitioned model (len(fem.partitions) > 1);
mutually exclusive with split.
flat=True forces the single-domain (serial) emit even when
the model carries partitions — e.g. a composed model, which is
auto-partitioned one-rank-per-module (ADR 0038 §"Rank model")
and would otherwise take the per-rank fan-out. The deck
declares the whole model in one domain with no getPID
brackets, exactly as the live in-process runner and modal decks
emit it. This is the Tcl route for serial-only records
(g.embed ties; fork contact before ADR 0092 S4 landed
partitioned emit, and still the escape hatch for the contact
cases the partitioned path refuses) on a composed model.
Mutually exclusive with per_rank and split; a no-op on
an already-unpartitioned model.
stream=True (ADR 0065 Tier 2 / plan_emit_memory_columnar.md
A1–A3) writes the deck through a live file sink instead of
accumulating the line buffer, so peak emit memory stops scaling
with deck size. Output is byte-identical to the default list
mode, including under per_rank=True, where the fragment
files are live-routed (partition_open switches the sink)
rather than sliced post-hoc. Everything goes to .tmp
siblings promoted atomically on clean completion — a mid-emit
exception never leaves a half-written deck. Not supported with
split=True (v1).
Source code in src/apeGmsh/opensees/apesees.py
10065 10066 10067 10068 10069 10070 10071 10072 10073 10074 10075 10076 10077 10078 10079 10080 10081 10082 10083 10084 10085 10086 10087 10088 10089 10090 10091 10092 10093 10094 10095 10096 10097 10098 10099 10100 10101 10102 10103 10104 10105 10106 10107 10108 10109 10110 10111 10112 10113 10114 10115 10116 10117 10118 10119 10120 10121 10122 10123 10124 10125 10126 10127 10128 10129 10130 10131 10132 10133 10134 10135 10136 10137 10138 10139 10140 10141 10142 10143 10144 10145 10146 10147 10148 10149 10150 10151 10152 10153 10154 10155 10156 10157 10158 10159 10160 10161 10162 10163 10164 10165 10166 10167 10168 10169 10170 10171 10172 10173 10174 10175 10176 10177 10178 10179 10180 10181 10182 10183 10184 10185 10186 10187 10188 10189 10190 10191 10192 10193 10194 10195 10196 10197 10198 10199 10200 10201 10202 10203 10204 10205 10206 10207 10208 10209 10210 10211 10212 10213 10214 10215 10216 10217 10218 10219 10220 10221 10222 10223 10224 10225 10226 10227 10228 10229 10230 10231 10232 10233 10234 10235 10236 10237 10238 10239 10240 10241 10242 10243 10244 10245 10246 10247 10248 10249 10250 10251 10252 10253 10254 10255 10256 10257 10258 10259 10260 10261 10262 10263 10264 10265 10266 10267 10268 10269 10270 10271 10272 10273 | |
modal_deck ¶
modal_deck(path: str, *, solver: str = 'feast', band: 'tuple[float, float] | None' = None, num_modes: int | None = None, certify: bool = False, target: str = 'tcl', out: str = 'eigenvalues.out') -> None
Emit a distributed modal deck (ADR 0077 Tier 1) — two backends.
solver="feast" (default) emits the replicated FEAST deck
described below; solver="arpack" emits the partitioned
ARPACK deck (Tier 1B) — see
:meth:_modal_deck_arpack for that half. They invert each
other on the two facts that matter (flat vs partitioned emit;
system inert vs load-bearing), so the docs are kept apart
rather than merged.
Which to use. "arpack" when the model does not fit on one
node: it is the only backend where both the storage and the
factorization are distributed. "feast" when you want a
frequency window rather than the lowest N, or -certify
completeness. Neither for a model that fits on one node —
Tier 0 (:meth:eigen / :meth:modal_properties on the
unpartitioned build) is faster at every size measured and is the
only route to correct participation factors and effective modal
mass.
FEAST backend (solver="feast", needs band=)¶
Writes a flat Tcl deck — every MPI rank builds the FULL model —
that runs band-targeted FEAST under OpenSeesMP: eigen -feast
band[0] band[1] -rci routes each contour solve through the
distributed dmumps kernel (fork ADR 43, L3-only: every rank
holds the full (K, M) CSR and the kernel slices the 2n block
system's triplets across ranks). Distribution lives inside the RCI
kernel, not in domain decomposition — a partitioned
if {[getPID]==K} deck fails FeastEigenSOE::setSize (P2
live finding), so partitions on the model are ignored here (the
deck is emitted flat) and the deck's system line plays no part
in the FEAST solve. RAM trade-off: the full model is assembled on
every rank (the documented L3 regime, ~1e5–1e6 DOF).
The band (Hz) defines the mode count; there is no num_modes.
The deck is the HPC entry point (ops.run_remote /
Cluster.submit, ADR 0060) and also runs single-process under
plain OpenSees (serial FEAST — the getPID shim makes the
rank-0 write-out unconditional).
modalProperties is not emitted: it is MPI-blind upstream
(wrong effective mass under any multi-rank run; ADR 0077 INV-2).
For participation factors run the single-process
:meth:modal_properties (Tier 0). Harvest with
:meth:ParallelModalResult.from_job — eigenvalues from the
rank-0 write-out plus mode shapes (ADR 0077 P3): the deck
records one mode_shape_<k>.out per found mode from rank 0
(the replicated model puts ALL nodes on every rank) with a
mode_shapes.json sidecar pinning the node→column map
(sorted mesh node tags × ndf DOFs).
Parameters¶
path
Deck output path.
solver
"feast" (default, replicated band solve) or "arpack"
(partitioned lowest-N solve, Tier 1B).
band
(f_min, f_max) frequency band in Hz; needs
0 <= f_min < f_max. FEAST only — rejected for
solver="arpack", whose selection axis is a mode count.
num_modes
Number of modes to extract. ARPACK only — rejected for
solver="feast", where the contour is the band and the
found count is dynamic.
certify
Emit -certify (fork Sturm/inertia completeness check).
FEAST only.
target
"tcl" (the classic-Tcl deck). Each solver needs its own
fork build: FEAST the classic-Tcl -feast parity build
(fork PR #578), ARPACK the MP eigen wiring (fork PR #668,
5a522b03b). "pymp" (an
OpenSeesMP-Python deck, ADR 0077 unlock 2a) raises for both
solvers — for ARPACK because the modern interpreter still
builds its ArpackSOE bare (the same latent F1 defect;
#668 is classic-Tcl only).
out
Rank-0 eigenvalue write-out filename (read by
:meth:ParallelModalResult.from_job).
Raises¶
ValueError
If solver is unknown, if the band / mode-count arguments
do not match the chosen solver, if band is invalid, if
solver="arpack" is given an unpartitioned model, or if a
non-Mumps system is declared on an ARPACK deck.
NotImplementedError
If target != "tcl" or the model has registered stages.
Source code in src/apeGmsh/opensees/apesees.py
10275 10276 10277 10278 10279 10280 10281 10282 10283 10284 10285 10286 10287 10288 10289 10290 10291 10292 10293 10294 10295 10296 10297 10298 10299 10300 10301 10302 10303 10304 10305 10306 10307 10308 10309 10310 10311 10312 10313 10314 10315 10316 10317 10318 10319 10320 10321 10322 10323 10324 10325 10326 10327 10328 10329 10330 10331 10332 10333 10334 10335 10336 10337 10338 10339 10340 10341 10342 10343 10344 10345 10346 10347 10348 10349 10350 10351 10352 10353 10354 10355 10356 10357 10358 10359 10360 10361 10362 10363 10364 10365 10366 10367 10368 10369 10370 10371 10372 10373 10374 10375 10376 10377 10378 10379 10380 10381 10382 10383 10384 10385 10386 10387 10388 10389 10390 10391 10392 10393 10394 10395 10396 10397 10398 10399 10400 10401 10402 10403 10404 10405 10406 10407 10408 10409 10410 10411 10412 10413 10414 10415 10416 10417 10418 10419 10420 10421 10422 10423 10424 10425 10426 10427 10428 10429 10430 10431 10432 10433 10434 10435 10436 10437 10438 10439 10440 10441 10442 10443 10444 10445 10446 10447 10448 10449 10450 10451 10452 10453 10454 10455 10456 10457 10458 10459 10460 10461 10462 10463 10464 10465 10466 10467 | |
py ¶
py(path: str, *, run: bool = False, analyze_steps: int | None = None, analyze_dt: float | None = None, split: bool = False, python: str | None = None, stream: bool = False, verbose: bool = False, log: str | None = None, progress: bool = True) -> None
Emit an openseespy Python deck to path; optionally run it.
run=True streams the openseespy subprocess exactly like
:meth:tcl — full output always tee'd to a log (log path
override, else <path>.log), console opt-in via verbose,
a live step counter from the progress markers, and a
tail-only RuntimeError on a non-zero exit. verbose /
log / progress are inert when run=False.
analyze_steps / analyze_dt semantics mirror :meth:tcl
(Phase SSI-1).
split=True (ADR 0043 slice 1.1, mode A) writes a driver
script at path plus one parts/<module>.py fragment per
composed module; each fragment exposes def build(ops): ...
and the driver loads + calls them. The default split=False
writes the single self-contained script, byte-identical to the
pre-0043 output. Same composed-model requirement as
:meth:tcl.
stream=True is out of scope for the Python deck emitter
(v1) and fails loud — the HPC path is Tcl (ADR 0065 Tier 2 /
plan_emit_memory_columnar.md A1–A3); use
ops.tcl(path, stream=True).
Source code in src/apeGmsh/opensees/apesees.py
10694 10695 10696 10697 10698 10699 10700 10701 10702 10703 10704 10705 10706 10707 10708 10709 10710 10711 10712 10713 10714 10715 10716 10717 10718 10719 10720 10721 10722 10723 10724 10725 10726 10727 10728 10729 10730 10731 10732 10733 10734 10735 10736 10737 10738 10739 10740 10741 10742 10743 10744 10745 10746 10747 10748 10749 10750 10751 10752 10753 10754 10755 10756 10757 10758 10759 10760 10761 10762 10763 10764 10765 10766 10767 10768 10769 10770 10771 10772 10773 10774 10775 10776 10777 10778 10779 10780 10781 10782 | |
run ¶
Drive an in-process LiveOpsEmitter through the full deck.
This emits every primitive but does NOT call analyze —
that is the user's call (or :meth:analyze's). Useful when
the user wants to declare a model, populate openseespy state,
and then run their own analysis driver.
Source code in src/apeGmsh/opensees/apesees.py
run_remote ¶
run_remote(job_dir: str, *, cluster: 'str | Cluster', np: int | None = None, name: str | None = None, deck: str = 'main.tcl', binary: str | None = None, walltime: str | None = None, analyze_steps: int | None = None, analyze_dt: float | None = None, wait: bool = True, poll: float = 15.0, timeout: float | None = None, overwrite: bool = False) -> 'Job'
Emit the Tcl deck and run it on a SLURM cluster (ADR 0060 sugar).
One call for the whole loop: emit into job_dir -> push ->
sbatch -> poll to completion -> fetch results back into
job_dir. Wraps :class:apeGmsh.hpc.Cluster /
:class:apeGmsh.hpc.Job; use those directly for finer control
(or pass wait=False to get the live :class:Job handle back
right after submission).
Parameters¶
job_dir
Local directory the deck is emitted into and results are
fetched back into. Created if missing.
cluster
Cluster name in ~/.apegmsh/clusters.toml (e.g.
"esmeralda") or a constructed Cluster.
np
MPI ranks. Defaults to the model's partition count
(len(fem.partitions)), or 1 for a flat model.
analyze_steps / analyze_dt
Forwarded to :meth:tcl — appends the analyze drive
line exactly as the local emit would.
wait
True (default) blocks until the job ends and fetches.
False returns the submitted Job immediately;
poll/fetch it yourself (it survives sessions via
Job.load(job_dir)).
Raises¶
HPCError
If the job ends in any state other than COMPLETED
(results and logs are still fetched first; the message
carries the stderr tail).
Source code in src/apeGmsh/opensees/apesees.py
10798 10799 10800 10801 10802 10803 10804 10805 10806 10807 10808 10809 10810 10811 10812 10813 10814 10815 10816 10817 10818 10819 10820 10821 10822 10823 10824 10825 10826 10827 10828 10829 10830 10831 10832 10833 10834 10835 10836 10837 10838 10839 10840 10841 10842 10843 10844 10845 10846 10847 10848 10849 10850 10851 10852 10853 10854 10855 10856 10857 10858 10859 10860 10861 10862 10863 10864 10865 10866 10867 10868 10869 10870 10871 10872 10873 10874 10875 10876 10877 10878 10879 10880 10881 10882 10883 10884 10885 10886 10887 10888 | |
h5 ¶
h5(path: str, *, model_name: str | None = None, cuts: 'Sequence[SectionCutDef]' = (), sweeps: 'Sequence[SectionSweepDef]' = ()) -> None
Emit a model-definition HDF5 archive at path.
Phase 8.5 composes the file in two layers:
- The broker (
self._fem) writes/meta+ the neutral zone (/nodes,/elements/{type},/physical_groups,/labels,/constraints/{kind},/loads/{kind}/{pattern},/masses). Broker writers live in :mod:apeGmsh.mesh._femdata_h5_io. - The bridge (an :class:
H5Emitterdriven through the :class:BuiltModel) appends/opensees/...enrichment. - apeGmsh.cuts v4: if
cutsand / orsweepsare supplied, they're persisted under/opensees/cuts/and/opensees/sweeps/(writer in :mod:apeGmsh.cuts._h5_io).
If self._fem does not expose a real :class:FEMData
surface (e.g. integration tests using a hand-rolled stub),
the broker step is skipped: the file ends up with the
bridge's own /meta plus /opensees/..., but no neutral
zone. Real callers always get the full file shape.
Parameters¶
path
File path to write the HDF5 archive to.
model_name
Optional human-readable name written to /meta/model_name.
Defaults to the path's stem.
cuts
Optional sequence of :class:apeGmsh.cuts.SectionCutDef
to persist under /opensees/cuts/cut_{i}. Each cut
travels with the model definition; the viewer auto-loads
them from the file the next time Results.viewer(...)
is opened against a results.h5 carrying the same
/opensees/ zone (Phase 8 / ADR 0020 Composed-file
pattern).
sweeps
Optional sequence of :class:apeGmsh.cuts.SectionSweepDef
to persist under /opensees/sweeps/sweep_{i}. Each
sweep group carries its own cuts/ sub-group in sweep
order (see apeGmsh/cuts/ARCHITECTURE.md "## v4").
Source code in src/apeGmsh/opensees/apesees.py
10890 10891 10892 10893 10894 10895 10896 10897 10898 10899 10900 10901 10902 10903 10904 10905 10906 10907 10908 10909 10910 10911 10912 10913 10914 10915 10916 10917 10918 10919 10920 10921 10922 10923 10924 10925 10926 10927 10928 10929 10930 10931 10932 10933 10934 10935 10936 10937 10938 10939 10940 10941 10942 10943 10944 10945 10946 10947 10948 10949 10950 10951 10952 10953 10954 10955 10956 10957 10958 10959 10960 10961 10962 10963 10964 10965 10966 10967 10968 10969 10970 10971 10972 10973 10974 10975 10976 10977 10978 10979 10980 10981 10982 10983 10984 10985 10986 10987 10988 10989 10990 10991 10992 10993 10994 10995 10996 10997 10998 10999 11000 11001 11002 11003 11004 11005 11006 11007 11008 11009 11010 11011 11012 11013 11014 11015 11016 11017 11018 11019 11020 11021 11022 11023 11024 | |
register ¶
tag_for ¶
build ¶
Freeze the declarations into a :class:BuiltModel.
Source code in src/apeGmsh/opensees/apesees.py
Staged analysis¶
Multi-stage workflows (in-situ stress install → excavate →
lining install → dynamic shake, or any other sequence of
analyze blocks with Domain mutations between them) use
the ops.stage(name) context manager:
with ops.stage(name="excavate") as s:
s.activate(pgs=["Lining"]) # bring new elements online
s.fix(pg="LiningAnchor", dofs=(1, 1, 1)) # stage-bound BC
s.embedded(name="lining_embed") # claim MP constraint by name
s.analysis(test=…, algorithm=…, integrator=…,
constraints=…, numberer=…, system=…, analysis=…)
s.run(n_increments=20, dt=0.05)
Each stage emits its own analysis chain + analyze loop with an
explicit inter-stage cleanup (loadConst -time 0.0 +
wipeAnalysis). Between-stage Domain mutators (s.remove_sp
/ s.remove_element / s.set_time / s.set_creep /
s.reset / s.mass(overwrite=True)) lift the append-only
constraint from earlier phases and unlock the atomic-replace
pattern (release prior support + re-fix the same DOF in one
stage).
Five validators gate stage-bound BCs at build time (H1 / V1 / V2
/ V3 / V4) and two more cover the SSI-2.E removal verbs (V5 /
V6). Each raises BridgeError with a clear offender list when
a stage references topology that doesn't yet exist or has
already been removed.
Tcl + Py text emit are the supported execution paths for staged
decks today. Live execution (ops.analyze / ops.eigen)
refuses staged models with NotImplementedError — emit via
ops.tcl(p, run=True) / ops.py(p, run=True) for the
OpenSees subprocess. H5 archival of staged structure is also
deferred (apeSees.h5(path) is fail-loud on a staged build per
PR #313).
The full lifecycle table, builder verbs, validator surface, MP partitioned + staged emit (Phase SSI-2.C), and the SSI-1 initial-stress ramp live in architecture/api-design.md §"Staged analysis"; the internals (deck layout, ownership computation, hook dispatcher, per-emitter dialect divergence, cleanup contract) live in architecture/staged-analysis.md and architecture/emitter.md.
Orientation helpers¶
Used as the orientation= argument on the typed geom_transf
primitives (Linear / PDelta / Corotational).
apeGmsh.opensees.Cartesian ¶
Constant Cartesian triad. reference_axis defines e3;
e1 and e2 are picked deterministically from the global
axis least aligned with e3.
The default reference_axis = (0, 0, 1) reproduces the legacy
"Z up" convention: horizontal beams get vecxz = (0, 0, 1) and
vertical columns fall back to vecxz = (-1, 0, 0) (the sign
follows the tangent direction; see :ref:shoebuckle).
Parameters¶
reference_axis : 3-vector
The axis e3. Need not be unit length.
Example¶
::
from apeGmsh.opensees import Cartesian
# Standard structural convention: Z is vertical
orientation = Cartesian() # reference_axis = +Z
# Mechanical CAD convention: Y is vertical
orientation = Cartesian(reference_axis=(0, 1, 0))
Source code in src/apeGmsh/opensees/_orientation.py
apeGmsh.opensees.Cylindrical ¶
Cylindrical orientation about an axis of revolution.
At a point p:
e1= radial outward, perpendicular toaxise2= circumferential,axis × e1e3=axis(constant) ← reference axis for the rule
Use this for ring beams, tank stiffeners, and any beam set whose natural "vertical" is the axis of revolution.
Parameters¶
origin : 3-vector Any point on the axis of revolution. axis : 3-vector Direction of the axis of revolution. Need not be unit length.
Example¶
::
from apeGmsh.opensees import Cylindrical
# Vertical tank
orientation = Cylindrical(origin=(0, 0, 0), axis=(0, 0, 1))
Source code in src/apeGmsh/opensees/_orientation.py
apeGmsh.opensees.Spherical ¶
Spherical orientation about a fixed origin. Polar axis is global +Z.
At a point p (with r = |p − origin|):
e1=e_θ— along the meridian (south at the equator)e2=e_φ— along the parallel (east)e3=e_r— outward radial ← reference axis for the rule
Useful for fan vaults, geodesic ribs, and any beam network with
natural radial structure. Note: for a planar curved beam (e.g.
a vertical-plane arch), :class:Cartesian with reference_axis
in the plane gives the same answer with less ceremony.
Parameters¶
origin : 3-vector Centre of the sphere.
Example¶
::
from apeGmsh.opensees import Spherical
orientation = Spherical(origin=(0, 0, 0))
Source code in src/apeGmsh/opensees/_orientation.py
Recorders¶
Standalone recorder declaration helper. Recorder declarations live on
ops.recorder.* in the apeSees bridge (typed primitives —
Node / Element / MPCO / declarative fan-out via
ops.recorder.declare(...)).
apeGmsh.opensees.recorder ¶
Typed recorder primitives.
Phase 3B ships three concrete recorder classes mirroring the OpenSees
recorder command:
- :class:
Node—recorder Node ... - :class:
Element—recorder Element ... - :class:
MPCO—recorder mpco ...(HDF5)
Each class is a @dataclass(frozen=True, kw_only=True, slots=True);
the matching :class:apeGmsh.opensees._internal.ns.recorder._RecorderNS
methods take the same kwargs and call self._bridge._register(Cls(...)).
Recorders never compose other primitives (dependencies() returns
()). They are leaves in the dependency graph; the build pipeline
emits them after the topology + analysis chain so that each recorder
command sees fully-allocated node and element tags.
The pg= form (physical-group fan-out into node/element tags) is
materialized at build time by
:func:apeGmsh.opensees._internal.build.emit_recorder_spec, which
resolves pg through the FEM snapshot, rewrites the spec to its
explicit nodes= / elements= form via :func:dataclasses.replace,
and then delegates to _emit. End users drive this through
apeSees(fem).tcl(...) / .py(...) / .run() — never call _emit
directly with a pg spec, which raises :class:NotImplementedError
as a defense-in-depth guard.
OpenSees command shapes¶
::
recorder Node -file fname [-time] [-dT dT] [-node n...]
-dof d... response
recorder Element -file fname [-time] [-dT dT] [-ele e...]
response_tokens...
recorder mpco fname.mpco [-N nodal_responses...]
[-E elem_responses...]
[-T dt $dt | -T nsteps $n]
The -time flag (when time_format="dt") instructs OpenSees to
include the simulation-time column in the output file. The default
time_format="step" writes only the response columns.
Node
dataclass
¶
Node(*, file: str, response: str, nodes: tuple[int, ...] | None = None, pg: str | None = None, dofs: tuple[int, ...], dT: float | None = None, time_format: str = 'step')
Bases: Recorder
recorder Node — record nodal response history.
OpenSees command::
recorder Node -file fname [-time] [-dT dT]
(-node n1 n2 ... | -nodeRange first last)
-dof d1 d2 ... response
Exactly one of nodes= (explicit list) or pg= (physical-group
label) must be supplied. The bridge build pipeline materializes the
pg= form against the FEM snapshot before driving _emit;
direct _emit calls on a pg= spec raise
:class:NotImplementedError as a defense-in-depth guard.
Parameters¶
file
Output file path.
response
OpenSees response token ("disp", "vel", "accel",
"reaction", "unbalance", ...).
nodes
Explicit tuple of node tags. Mutually exclusive with pg.
pg
Physical-group label whose nodes the recorder targets.
Mutually exclusive with nodes. Resolved by the bridge
build pipeline at emit time.
dofs
DOF indices (1-based, OpenSees convention). At least one
required.
dT
Optional cadence — record only every dT simulation
seconds. None records every step.
time_format
"step" (default) writes only response columns;
"dt" emits the OpenSees -time flag, prepending the
simulation-time column.
Element
dataclass
¶
Element(*, file: str, response: tuple[str, ...], elements: tuple[int, ...] | None = None, pg: str | None = None, dT: float | None = None, time_format: str = 'step')
Bases: Recorder
recorder Element — record element-level response history.
OpenSees command::
recorder Element -file fname [-time] [-dT dT]
(-ele e1 e2 ... | -eleRange first last)
response_tokens...
response is a tuple of OpenSees response tokens — the simplest
case is ("globalForce",) or ("stresses",); element types
that nest responses (e.g. fiber sections) take multi-token forms
such as ("section", "1", "force").
Exactly one of elements= (explicit list) or pg= (physical-
group label) must be supplied. pg= is resolved against the FEM
snapshot by the bridge build pipeline before driving _emit;
direct _emit calls on a pg= spec raise
:class:NotImplementedError as a defense-in-depth guard.
Parameters¶
file
Output file path.
response
Tuple of OpenSees response tokens (at least one).
elements
Explicit tuple of element tags. Mutually exclusive with pg.
pg
Physical-group label whose elements the recorder targets.
Mutually exclusive with elements. Resolved by the bridge
build pipeline at emit time.
dT
Optional cadence — record only every dT simulation
seconds. None records every step.
time_format
"step" (default) writes only response columns;
"dt" emits the OpenSees -time flag.
FilterableRecorder
dataclass
¶
FilterableRecorder(*, file: str, nodal_responses: tuple[str, ...] = (), elem_responses: tuple[str, ...] = (), dT: float | None = None, nsteps: int | None = None, nodes: tuple[int, ...] | None = None, nodes_pg: str | None = None, elements: tuple[int, ...] | None = None, elements_pg: str | None = None, _region_tag: int | None = None)
Bases: Recorder
Base for HDF5 recorders that share MPCO's region-filter surface.
Carries the four mutually-paired selectors (nodes / nodes_pg
/ elements / elements_pg) and the region-emit machinery that
turns them into an OpenSees region $tag -node ... -ele ... line
plus a -R $tag on the recorder command. :class:MPCO and
:class:Ladruno both inherit it; they differ only in the recorder
kind token and (Ladruno) the trailing -G energy channel.
Carries the shared value channels too (file + nodal_responses
/ elem_responses -N/-E + dT/nsteps -T cadence),
so :meth:_value_channel_args builds the common -N ... -E ... -T ...
-R ... tail once for both subclasses — only the recorder kind
token and (Ladruno) the trailing -G energy are subclass-specific.
Subclasses own only their required-response rule (MPCO needs nodal or
element responses; Ladruno also accepts energy); the cadence
mutex (:meth:_validate_cadence) and the four selector guards
(:meth:_validate_filter) are shared. The partition-aware build
pipeline (ADR 0027 INV-4) keys its per-rank region pass on
isinstance(spec, FilterableRecorder) via :meth:has_filter /
:meth:resolve_filter_ids.
has_filter ¶
True iff any node/element selector was supplied.
Used by the partition-aware build pipeline (ADR 0027 INV-4) to
decide whether the recorder needs a per-rank region pass — a
whole-model recorder (no filter) emits one recorder line
and nothing else.
Source code in src/apeGmsh/opensees/recorder.py
resolve_filter_ids ¶
resolve_filter_ids(fem: 'FEMData', fem_eid_to_ops_tag: 'FemToOpsTagMap | dict[int, int] | None' = None) -> tuple[tuple[int, ...], tuple[int, ...]]
Resolve nodes / nodes_pg / elements / elements_pg
to explicit id tuples — no emission, no tag allocation.
Returns (node_ids, elem_ids). Either may be empty when its
side was not requested; an empty result on a requested side
(e.g. nodes_pg="X" resolving to zero nodes) raises
:class:BridgeError to mirror the OpenSees runtime rejection of
an empty region.
fem_eid_to_ops_tag is the bridge-built {fem_eid: ops_tag}
map for element fan-out. When supplied AND elements_pg is
set, the resolved FEM eids are translated to OpenSees element
tags before they flow into the region's -ele arg list
(same drift as the Element recorder, closed by
:meth:Element.materialize). Lookup miss → :class:BridgeError.
When None (legacy direct callers) the FEM eids are returned
verbatim — the partition orchestrator uses this form so it can
intersect per-rank in FEM-eid space, then translate at the
final region-emit step.
This is the partition-aware split-point of the legacy single-
pass :meth:materialize: the partition orchestrator calls
resolve_filter_ids once globally to determine the full
filter id set, then intersects per-rank before emitting the
region. Whole-model recording (has_filter() is False) is
a no-op pass-through — callers should not invoke this method
in that case.
Source code in src/apeGmsh/opensees/recorder.py
513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 | |
materialize ¶
materialize(emitter: 'Emitter', fem: 'FEMData', tags: 'TagAllocator | None', fem_eid_to_ops_tag: 'FemToOpsTagMap | dict[int, int] | None' = None) -> 'FilterableRecorder'
Resolve filter selectors against the FEM and emit the region.
Whole-model recording (no filter selectors) is a no-op pass-
through. When any of nodes / nodes_pg / elements /
elements_pg is set, this method:
- Resolves
*_pgto explicit id tuples via the bridge's PG-expansion helpers; refuses empty resolutions with :class:BridgeError(an empty OpenSees region is rejected at runtime). - Allocates one fresh region tag from
tags(must be supplied — the bridge build pipeline forwards theTagAllocator). - Emits one
region $tag -node ... -ele ...line onemitter. - Returns a clone with the filter selectors cleared and
_region_tagpopulated, so the subsequent_emitappends-R $tagto the recorder command.
Used by the flat / unpartitioned emit path. The partitioned
emit path (ADR 0027 INV-4) invokes :meth:resolve_filter_ids
once and emits the per-rank region line itself; it then
injects _region_tag= onto the spec via
:func:dataclasses.replace directly, bypassing this method.
Source code in src/apeGmsh/opensees/recorder.py
622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 | |
MPCO
dataclass
¶
MPCO(*, file: str, nodal_responses: tuple[str, ...] = (), elem_responses: tuple[str, ...] = (), dT: float | None = None, nsteps: int | None = None, nodes: tuple[int, ...] | None = None, nodes_pg: str | None = None, elements: tuple[int, ...] | None = None, elements_pg: str | None = None, _region_tag: int | None = None)
Bases: FilterableRecorder
recorder mpco — write a single HDF5 .mpco file.
OpenSees command::
recorder mpco fname.mpco [-N nodal_responses...]
[-E elem_responses...]
[-T dt $dt | -T nsteps $n]
[-R $regTag]
The MPCO recorder captures the full response tensor for each
requested token (no per-DOF selection at write time); STKO /
apeGmsh consumers filter at read time. At least one of
nodal_responses or elem_responses must be non-empty.
Cadence is selected by exactly one of dT (seconds) or
nsteps (analysis steps). Supplying both raises ValueError;
supplying neither records every analysis step.
Filtering — MPCO records the whole model by default. To
restrict output to a subset of nodes/elements, supply any of
nodes= / nodes_pg= / elements= / elements_pg=:
the build pipeline auto-emits an OpenSees region $tag -node ...
-ele ... command before the recorder and passes -R $tag to
MPCO. nodes= is mutually exclusive with nodes_pg=; the
same applies to the element pair. When all four are None
(the default) MPCO records the whole model and no region is
emitted.
Parameters¶
file
Output .mpco (HDF5) file path.
nodal_responses
Tuple of MPCO -N tokens (e.g. ("displacement",
"reactionForce")). Empty tuple means no nodal recording.
elem_responses
Tuple of MPCO -E tokens (e.g. ("stresses",
"section.fiber.stress")). Empty tuple means no element
recording.
dT
Optional time-based cadence (seconds). Mutually exclusive
with nsteps.
nsteps
Optional step-based cadence (every N analysis steps).
Mutually exclusive with dT.
nodes
Explicit tuple of node tags to include in the region filter.
Mutually exclusive with nodes_pg.
nodes_pg
Physical-group label whose nodes the region filter targets.
Mutually exclusive with nodes. Resolved by the bridge
build pipeline at emit time.
elements
Explicit tuple of element tags to include in the region
filter. Mutually exclusive with elements_pg.
elements_pg
Physical-group label whose elements the region filter
targets. Mutually exclusive with elements. Resolved by
the bridge build pipeline at emit time.
Note¶
The bridge does not interpret -R-bearing MPCO arg tails when
_emit is called directly (outside the build pipeline); the
pg= form is materialised by
:func:apeGmsh.opensees._internal.build.emit_recorder_spec,
which resolves selectors, allocates a region tag, emits the
region, and replaces the spec via :func:dataclasses.replace
with explicit nodes=/elements= before driving _emit.
Ladruno
dataclass
¶
Ladruno(*, file: str, nodal_responses: tuple[str, ...] = (), elem_responses: tuple[str, ...] = (), dT: float | None = None, nsteps: int | None = None, nodes: tuple[int, ...] | None = None, nodes_pg: str | None = None, elements: tuple[int, ...] | None = None, elements_pg: str | None = None, _region_tag: int | None = None, energy: bool = False, energy_pg: str | None = None, _energy_region_tags: tuple[int, ...] = ())
Bases: FilterableRecorder
recorder ladruno — write a single HDF5 .ladruno file.
Fork-only. The ladruno recorder exists only in the Ladruno
fork build of OpenSees (nmorabowen/OpenSees@ladruno); stock
openseespy does not have it. Per the opt-in contract, emission
works on any build (the deck line is just recorder ladruno
...); the fork requirement bites only when the deck actually runs
(ops.run() / the live emitter). Gate at the point of use, never
at import.
OpenSees command (value channels + region filter + energy balance)::
recorder ladruno fname.ladruno [-N nodal_responses...]
[-E elem_responses...]
[-T dt $dt | -T nsteps $n]
[-R $regTag]
[-G energy]
The .ladruno recorder is forked from the frozen MPCORecorder
and shares its value-channel command grammar (the -N/-E/
-T channels reproduce the frozen recorder to 1e-12), so this
dataclass mirrors :class:MPCO for those channels and inherits the
same region-filter machinery from :class:FilterableRecorder. It
diverges only in the recorder kind token (ladruno vs mpco),
the output extension (.ladruno), and the trailing -G energy
channel.
Cadence is selected by exactly one of dT (seconds) or nsteps
(analysis steps). Supplying both raises ValueError; supplying
neither records every analysis step.
Filtering — like MPCO, Ladruno records the whole model by
default. Supply any of nodes= / nodes_pg= / elements= /
elements_pg= to restrict output: the build pipeline auto-emits an
OpenSees region $tag -node ... -ele ... command before the
recorder and passes -R $tag to ladruno. nodes= is
mutually exclusive with nodes_pg= (same for the element pair).
A node-only filter cannot be combined with elem_responses (and
vice versa) — the auto-region would carry no entries on the other
side and produce an empty stream. The -R $tag is emitted
before -G energy so the energy flag stays last.
energy=True adds the fork's whole-model energy-balance channel
(-G energy → RESULTS/ON_DOMAIN/energyBalance, components
KE/IE/DW/ULW/RES/ERR), read back via Results.energy(). The
flag is emitted last: the fork's -G parser eagerly consumes
trailing region-tag integers and cannot rewind past a following
flag, so -G energy -T nsteps 10 is a parse error while
-T nsteps 10 -G energy runs (run-verified on the fork build).
Energy balance. Three forms, all run-verified on the fork build
(which always writes the whole-model balance,
RESULTS/ON_DOMAIN/energyBalance, and adds a per-region balance,
RESULTS/ON_REGIONS/energyBalance, whenever a region tag is given):
energy=Truealone → whole-model (-G energy, no tag).energy=True+ a value filter → energy over the same region the-Rfilter targets, reusing the filter's already-allocated tag (-G energy $filterTag). The coupled form.energy_pg="X"→ energy over an independent region X (-G energy $tagX), decoupled from the-Rvalue filter. X may differ from (or exist without) the value-channel filter; it gets its own region tag. The decoupled form.energy_pgtakes precedence over the coupled form, and implies energy recording (no need to also setenergy=True).
All three ride the flat / staged / partitioned region plumbing — the
decoupled region gets its own per-rank fan-out under partitioning, just
like the value filter (ADR 0064 §4). Read any region's balance via
Results.energy(region=<tag>); whole-model via Results.energy().
Parameters¶
file
Output .ladruno (HDF5) file path.
nodal_responses
Tuple of -N tokens (e.g. ("displacement",
"reactionForce")). Empty tuple means no nodal recording.
elem_responses
Tuple of -E tokens (e.g. ("stresses",
"section.fiber.stress")). Empty tuple means no element
recording.
dT
Optional time-based cadence (seconds). Mutually exclusive with
nsteps.
nsteps
Optional step-based cadence (every N analysis steps). Mutually
exclusive with dT.
energy
Record the energy balance (-G energy). Whole-model when no
filter is set; per-region (over the -R filter region, plus
whole-model) when combined with a nodes=/elements= filter.
energy_pg
Physical-group label for a decoupled energy region — records
energy over that PG (-G energy $tag) independent of the value
filter. Implies energy recording. Resolved by the bridge build
pipeline; gets its own auto-emitted region (per-rank under
partitioning).
nodes
Explicit tuple of node tags for the region filter. Mutually
exclusive with nodes_pg.
nodes_pg
Physical-group label whose nodes the region filter targets.
Mutually exclusive with nodes. Resolved by the bridge build
pipeline at emit time.
elements
Explicit tuple of element tags for the region filter. Mutually
exclusive with elements_pg.
elements_pg
Physical-group label whose elements the region filter targets.
Mutually exclusive with elements. Resolved by the bridge
build pipeline at emit time.
resolve_energy_ids ¶
resolve_energy_ids(fem: 'FEMData', fem_eid_to_ops_tag: 'FemToOpsTagMap | dict[int, int] | None' = None) -> tuple[int, ...]
Resolve energy_pg to element ids for the energy region.
Mirrors the element side of :meth:resolve_filter_ids: returns
FEM eids verbatim when fem_eid_to_ops_tag is None (the
partition orchestrator intersects per-rank in FEM-eid space),
else translates to OpenSees element tags. Empty resolution →
:class:BridgeError. Energy is an element quantity, so the
region carries -ele only (the fork auto-derives the nodes).
Source code in src/apeGmsh/opensees/recorder.py
materialize ¶
materialize(emitter: 'Emitter', fem: 'FEMData', tags: 'TagAllocator | None', fem_eid_to_ops_tag: 'FemToOpsTagMap | dict[int, int] | None' = None) -> 'FilterableRecorder'
Emit the value-filter region (base) and the decoupled
energy region (energy_pg), each as its own OpenSees region.
The energy region is independent of the -R value filter (the
fork's -G energy <tag> list is orthogonal to -R); it gets
its own tag, recorded in _energy_region_tags and referenced by
_emit as -G energy $tag. Used by the flat path; the
partitioned path builds the equivalent spec in
:meth:BuiltModel._plan_partitioned_mpco_recorders.
Source code in src/apeGmsh/opensees/recorder.py
Monitor
dataclass
¶
Monitor(*, sink: str, dofs: tuple[int, ...], nodes: tuple[int, ...] | None = None, pg: str | None = None, resp: str = 'disp', every: int | None = None, hz: float | None = None)
Bases: Recorder
recorder Monitor — live SWMR-HDF5 nodal-telemetry sink (fork-only).
Fork-only. The Monitor recorder exists only in the Ladruno fork
build; stock openseespy does not have it. Emission works on any
build (the deck line is just recorder Monitor ...); the fork
requirement bites only when the deck runs. Gate at the point of use.
Unlike the canonical :class:Ladruno recorder, the Monitor is a
lightweight live-telemetry sidecar: it streams a few selected nodal
scalars to a small SWMR-HDF5 file a viewer process can tail while the
analysis is still running. The same file is a valid at-rest result
once the run finishes — read both via
:func:apeGmsh.results.read_monitor / :func:apeGmsh.results.tail_monitor.
OpenSees command::
recorder Monitor (-node n1 n2 ... | -region tag) -dof d1 d2 ...
[-resp disp|vel|accel|reaction]
-sink fname.h5 [-every K] [-hz H]
Exactly one of nodes= (explicit tags) or pg= (physical-group
label, resolved against the FEM snapshot by the bridge build pipeline)
must be supplied. The recorded channels are the cartesian product of
nodes × dofs, labelled node<N>.<resp>.dof<D> in node-major order.
Parameters¶
sink
Output .h5 SWMR sink path.
dofs
DOF indices (1-based, OpenSees convention). At least one required.
nodes
Explicit tuple of node tags. Mutually exclusive with pg.
pg
Physical-group label whose nodes are monitored. Mutually exclusive
with nodes; resolved at emit time.
resp
Nodal response — one of disp / vel / accel /
reaction (the fork v1 set). Default disp.
every
Step decimation — emit a frame every K analysis steps. None
records every step.
hz
Wall-clock throttle — emit at most H frames per second of real
time (the first frame always passes). None means no throttle.
Independent of every; both may bound the stream.
RecorderRecord
dataclass
¶
RecorderRecord(*, category: str, components: tuple[str, ...] = (), raw: tuple[str, ...] = (), pg: tuple[str, ...] = (), label: tuple[str, ...] = (), selection: tuple[str, ...] = (), ids: tuple[int, ...] | None = None, dt: float | None = None, n_steps: int | None = None, name: str | None = None, n_modes: int | None = None, element_class_name: str | None = None)
One category-level declaration entry within a RecorderDeclaration.
Stores already-expanded canonical components (or raw OpenSees
tokens via the raw= escape hatch). Shorthand expansion
("displacement" → displacement_x/y/z) happens at
construction in the namespace method (Phase 9 commit 3), not in
this dataclass — by the time a record is built, components are
fully expanded.
Parameters¶
category
One of :data:ALL_RECORDER_CATEGORIES.
components
Tuple of canonical component names. Validated against
:data:_CATEGORY_CANONICALS per category, plus indexed
canonicals (state_variable_<n>, fiber_stress_<n>,
spring_force_<n>) recognized via :func:is_canonical.
raw
Escape hatch for non-canonical OpenSees tokens (e.g. a
custom recorder response). Bypasses canonical validation.
pg / label / selection / ids
Target selectors. ids= is mutually exclusive with the
named selectors. Resolution against FEMData happens at
emit time (commit 3).
dt / n_steps
Recording cadence. At most one may be set; both None
records every step.
name
Optional user-supplied name for this record; auto-generated
when None.
n_modes
Required for category="modal"; rejected for other
categories.
element_class_name
Optional OpenSees C++ class name override for element-level
records. Used by the .out transcoder to disambiguate
elements that share a flat response size (e.g. tri31 vs
SSPquad). Carried from the legacy Recorders.elements
contract.
RecorderDeclaration
dataclass
¶
RecorderDeclaration(*, records: tuple[RecorderRecord, ...], name: str = 'default', ndm: int = 3, ndf: int = 6, file_root: str = '.')
Bases: Recorder
A bundle of recorder records, registered as a single Primitive.
Captures the bridge's ndm and ndf at construction time
(Phase 9 D8 — implicit source-of-truth binding). Drives the
file-emit path via :func:emit_recorder_spec in
:mod:apeGmsh.opensees._internal.build.
Parameters¶
records
Tuple of :class:RecorderRecord entries. Each is one
category-level declaration; emit fans them out into one or
more concrete OpenSees recorder commands.
name
Identifier for this declaration (defaults to "default").
Multiple named declarations can coexist on one bridge.
ndm, ndf
Snapshot of the bridge's ndm/ndf at construction time.
Used downstream for shorthand expansion and validation. The
bridge passes these in (Phase 9 D8 — user never repeats
ops.model(ndm=, ndf=) values).
file_root
Directory prefix for emitted .out files. Each record fans
out to <file_root>/<decl.name>__<record_name>__<token>.out.
Defaults to "." (current working directory).
build_recorder_declaration ¶
build_recorder_declaration(*, ndm: int, ndf: int, nodes: 'Iterable[str] | str' = (), elements: 'Iterable[str] | str' = (), line_stations: 'Iterable[str] | str' = (), gauss: 'Iterable[str] | str' = (), raw_nodes: 'Iterable[str] | str | None' = None, raw_elements: 'Iterable[str] | str | None' = None, raw_line_stations: 'Iterable[str] | str | None' = None, raw_gauss: 'Iterable[str] | str | None' = None, pg: 'str | Iterable[str] | None' = None, label: 'str | Iterable[str] | None' = None, selection: 'str | Iterable[str] | None' = None, ids: 'Iterable[int] | None' = None, dt: float | None = None, n_steps: int | None = None, name: str = 'default', record_name: str | None = None, element_class_name: str | None = None, file_root: str = '.') -> RecorderDeclaration
Construct a :class:RecorderDeclaration from declarative kwargs.
Single source of truth for shorthand expansion ("displacement"
→ displacement_x/y/z via the bound ndm/ndf) and
per-category record construction. Shared by
:meth:apeGmsh.opensees._internal.ns.recorder._RecorderNS.declare
(bridge-owned models) and
:meth:apeGmsh.opensees.ModelData.recorders (hand-written decks).
ndm / ndf are supplied by the caller (the bridge or
ModelData binds them at declaration time, Phase 9 D8 — the user
never repeats ndm=/ndf= here).
Source code in src/apeGmsh/opensees/recorder.py
1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 | |
Numberer¶
apeGmsh.mesh._numberer.Numberer ¶
Renumbers a FEM mesh for solver consumption.
Parameters¶
fem_data : dict
Output of Mesh.get_fem_data(). Must contain:
node_tags, node_coords, elem_tags,
connectivity, used_tags.
Source code in src/apeGmsh/mesh/_numberer.py
renumber ¶
Produce a solver-ready mesh with contiguous IDs.
Parameters¶
method : "simple" or "rcm"
"simple" — preserves relative order, just makes IDs
contiguous. Fast, no optimisation.
``"rcm"`` — Reverse Cuthill-McKee bandwidth minimisation.
Reorders nodes so that the assembled stiffness matrix has
minimal bandwidth. Recommended for direct solvers.
int
Starting ID (default 1 = Fortran/OpenSees convention; use 0 for C/Python convention).
bool
If True (default), only include nodes that appear in at least one element (skip orphan nodes). Set False to include all nodes from the mesh.
Returns¶
NumberedMesh
Source code in src/apeGmsh/mesh/_numberer.py
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 | |
apeGmsh.mesh._numberer.NumberedMesh
dataclass
¶
NumberedMesh(node_ids: ndarray, node_coords: ndarray, elem_ids: ndarray, connectivity: ndarray, n_nodes: int = 0, n_elems: int = 0, bandwidth: int = 0, method: str = 'simple', gmsh_to_solver_node: dict[int, int] = dict(), solver_to_gmsh_node: dict[int, int] = dict(), gmsh_to_solver_elem: dict[int, int] = dict(), solver_to_gmsh_elem: dict[int, int] = dict())
Solver-ready mesh with contiguous IDs and bidirectional maps.
All IDs are 1-based (the standard in structural FEM solvers
like OpenSees, Abaqus, SAP2000). Set base=0 in
:meth:Numberer.renumber for 0-based if your solver needs it.
Attributes¶
node_ids : ndarray(N,)
New contiguous node IDs.
node_coords : ndarray(N, 3)
Nodal coordinates, same order as node_ids.
elem_ids : ndarray(E,)
New contiguous element IDs.
connectivity : ndarray(E, npe)
Element connectivity in terms of new node IDs.
n_nodes : int
n_elems : int
bandwidth : int
Semi-bandwidth of the resulting adjacency.
method : str
Numbering method used ("simple" or "rcm").
Maps ~~~~ gmsh_to_solver_node : dict[int, int] Gmsh node tag -> solver node ID. solver_to_gmsh_node : dict[int, int] Solver node ID -> Gmsh node tag. gmsh_to_solver_elem : dict[int, int] Gmsh element tag -> solver element ID. solver_to_gmsh_elem : dict[int, int] Solver element ID -> Gmsh element tag.