The OpenSees Bridge¶
apeGmsh ships with a first-class bridge to OpenSees. The bridge takes a
FEMData snapshot from an apeGmsh session, combines it with material
definitions, element assignments, boundary conditions, and explicitly
re-declared loads/masses, and produces ready-to-run Tcl or openseespy
scripts. You never touch raw node numbering or element connectivity --
the bridge resolves physical groups against the snapshot.
The legacy in-session g.opensees.* composite (and the apeGmsh.solvers
package) was removed in the Phase-8 teardown (ADR 0009 -- no
back-compat shim). The OpenSees surface is now a single class,
apeSees, constructed after the session from a FEMData snapshot:
from apeGmsh.opensees import apeSees
fem = g.mesh.queries.get_fem_data(dim=3)
ops = apeSees(fem)
ops.model(ndm=3, ndf=3)
apeSees exposes typed namespaces and explicit declaration verbs:
| Namespace / verb | Purpose |
|---|---|
ops.nDMaterial, ops.uniaxialMaterial, ops.section |
typed material / section primitives |
ops.geomTransf, ops.beamIntegration |
beam local frame + integration |
ops.element.<Type>(pg=…) |
element assignment by physical group |
ops.fix, ops.mass |
homogeneous SP + lumped mass (explicit) |
ops.timeSeries, ops.pattern |
load patterns + prescribed SP (explicit) |
ops.recorder.<Type> |
recorder declarations |
ops.tcl / .py / .h5 / .run / .analyze |
emit / run |
Plus the lifecycle entry points: ops.model(ndm=, ndf=) (first) and
ops.build() (usually implicit -- each emit builds internally).
The big behavioural change: no ingest. The old bridge had an
ingeststep that pulled session-declaredg.loads/g.masses/g.constraintsinto the deck.apeSeeshas no ingest and no auto-resolution. It reads thefemsnapshot only to resolvepg=/label=selectors to node/element tags and to get coordinates/connectivity. Loads, masses and SPs you want in OpenSees must be re-declared explicitly onops(§4). Session declarations still flow into themodel.h5neutral zone for the viewer /Results-- they are just not in the runnable Tcl/Py/Live deck.
The overall pipeline is:
geometry --> mesh --> FEMData snapshot
--> apeSees(fem) --> typed materials/elements
--> explicit fix/mass/patterns --> emit (tcl/py/h5/run)
1. Model Dimensions -- ops.model¶
Tell the bridge how many spatial dimensions and DOFs per node your model has:
Both arguments are keyword-only. Typical combinations:
ndm |
ndf |
Use case |
|---|---|---|
| 2 | 2 | 2-D solid (ux, uy) |
| 2 | 3 | 2-D frame (ux, uy, rz) |
| 3 | 3 | 3-D solid (ux, uy, uz) |
| 3 | 6 | 3-D frame or shell (ux, uy, uz, rx, ry, rz) |
ops.model(...) must be the first call -- materials, elements,
fix, mass, and patterns all depend on it. ndf sets the required
length of the tuples you pass to ops.fix(dofs=…),
ops.mass(values=…), and p.load(forces=…). build() raises early
if any assigned element type is incompatible with the declared
ndm/ndf.
apeSees(fem, default_orientation=Cartesian()) sets a model-wide
geomTransf orientation default (Z-up). Pass None for 2-D models;
pass a custom Orientation for a Y-up CAD import.
2. Materials¶
Three typed namespaces mirror how OpenSees organises material-like
objects. Every method has an explicit, fully-typed signature (no
**kwargs) and returns a handle -- there are no string names and
no registry. Capture the handle in a variable and pass it by reference
to the element constructor; handles auto-register on the bridge.
2.1 nDMaterial -- continuum solids¶
Use ops.nDMaterial.<Type> for any element that takes an nDMaterial
in OpenSees: tetrahedra, bricks, quads, and triangles.
The returned conc handle is passed later as
ops.element.FourNodeTetrahedron(..., material=conc). The method name
is the OpenSees nDMaterial type -- ElasticIsotropic,
J2Plasticity, DruckerPrager, etc.
2.2 uniaxialMaterial -- trusses and springs¶
Use ops.uniaxialMaterial.<Type> for trusses, corotational trusses,
zeroLength springs, and fiber-section beams.
Steel02-family uses fy= (the legacy Fy= is gone).
2.3 Section -- shells and fiber sections¶
Use ops.section.<Type> for shell elements and fiber sections for
beams. The most common shell section is ElasticMembranePlateSection,
combining membrane and bending behaviour.
slab = ops.section.ElasticMembranePlateSection(
E=30e9, nu=0.2, h=0.2, rho=2400,
)
from apeGmsh.opensees.section.fiber import FiberPoint
sec = ops.section.Fiber(
fibers=(FiberPoint(material=steel, y=0.0, z=0.0, area=0.01),),
)
Which namespace: ops.nDMaterial -> solid elements;
ops.uniaxialMaterial -> truss / spring / fibre-section beams;
ops.section -> shells and fiber sections for beams. The constructors
are not chainable across namespaces -- write a separate statement
for each handle.
3. Element Assignment¶
ops.element.<Type>(pg="PG", ...) writes every mesh element in that
physical group as <Type>. PG resolution is "FEM-direct" against
fem. The call returns a typed ElementGroup.
3.1 Assigning element types -- ops.element.<Type>¶
ops.element.<Type>(
pg="PG", # physical-group or label name
*,
material=…, # nDMaterial / uniaxialMaterial handle
section=…, # section handle (shells / fiber beams)
transf=…, # geomTransf handle (beams only)
integration=…, # beamIntegration handle (force-based beams)
body_force=…, # element body/gravity force (solids)
pressure=…, # 2-D solids
**scalars, # element-specific scalar parameters
)
pg= resolves against both physical groups and apeGmsh labels (labels
resolve automatically -- no promote_to_physical). Keep PG names
dimension-unique; an ambiguous pg= that exists at multiple dimensions
is an error.
Material / section / transf / integration are passed as handles (the variables the constructors returned), not string names:
- Solid elements need an
nDMaterialhandle (ops.nDMaterial.*) - Truss elements need a
uniaxialMaterialhandle (ops.uniaxialMaterial.*) - Shell elements need a
sectionhandle (ops.section.*) - Beam elements take a
transfhandle plus either section properties as scalar kwargs (elasticBeamColumn) or abeamIntegrationhandle (forceBeamColumn)
Examples:
# 3-D solid tetrahedron -- gravity is an ELEMENT body_force param
ops.element.FourNodeTetrahedron(
pg="Body", material=conc,
body_force=(0.0, 0.0, -9.81 * 2400),
)
# 2-D plane-stress quad
ops.element.quad(
pg="Plate", material=steel_2d,
thick=0.01, eleType="PlaneStress",
)
# Truss with cross-section area
ops.element.corotTruss(pg="Diagonals", material=steel, A=3.14e-4)
# Elastic beam-column (3-D)
t = ops.geomTransf.PDelta(vecxz=(0, 0, 1))
ops.element.elasticBeamColumn(
pg="Columns", transf=t,
A=0.04, E=200e9, G=77e9, J=1e-4, Iy=2e-4, Iz=2e-4,
)
# Force-based beam-column with a fiber section
integ = ops.beamIntegration.Lobatto(section=sec, n_ip=5)
ops.element.forceBeamColumn(pg="Cols", transf=t, integration=integ)
# Shell (section-based)
ops.element.ShellMITC4(pg="SlabSurface", section=slab)
There is no eleLoad pattern verb -- distributed/body loads are
element parameters (body_force=, pressure=), not loads.
3.2 Supported element types¶
| Family | Types |
|---|---|
| 3-D solid | FourNodeTetrahedron, TenNodeTetrahedron, stdBrick, bbarBrick, SSPbrick |
| 2-D solid | quad, tri31, SSPquad |
| Shell | ShellMITC3, ShellMITC4, ShellDKGQ, ASDShellQ4 |
| Truss | truss, corotTruss |
| Beam | elasticBeamColumn, forceBeamColumn, ElasticTimoshenkoBeam |
If your Gmsh mesh uses second-order elements but the assigned OpenSees
type only supports first-order, build() issues a UserWarning and
discards mid-side nodes automatically.
3.3 Geometric transformations -- ops.geomTransf¶
Beam elements require a geometric transformation defining their local
coordinate system. The constructor returns a handle you pass to
ops.element.* as transf=:
t = ops.geomTransf.PDelta(vecxz=(0, 0, 1)) # or .Linear / .Corotational
ops.element.elasticBeamColumn(pg="Columns", transf=t, A=…, E=…, …)
vecxz-- the local x-z plane vector (3-D only; omit for 2-D models)- the namespace method name is the transform type:
.Linear,.PDelta,.Corotational
3.4 Boundary conditions -- ops.fix¶
Apply homogeneous single-point constraints to every node in a physical group:
ops.fix(pg="BasePlate", dofs=(1, 1, 1))
ops.fix(pg="PinnedBase", dofs=(1, 1, 1, 0, 0, 0)) # ndf=6
ops.fix(nodes=[101, 102], dofs=(1, 1, 1)) # explicit-node form
The dofs tuple must have exactly ndf entries (1 = fixed, 0 =
free). Homogeneous SPs are model-level here; non-zero prescribed
displacements go inside a pattern via p.sp (§4).
Solid faces: For solid meshes where you declared a face SP via
g.loads.face_sp(...) on the session, that record resolves into
fem.nodes.sp but is not ingested. Re-declare it explicitly:
homogeneous -> ops.fix(pg=…, dofs=…); prescribed -> p.sp(...)
inside a pattern. See guide_loads.md §11.
4. Loads, Masses, and SP -- Re-declared Explicitly¶
There is no ingest step. apeSees does not read the session's
g.loads / g.masses / g.constraints, nor the resolved
fem.nodes.loads / fem.nodes.masses / fem.nodes.sp records. To put
loads / masses / SP in the runnable deck you re-declare them explicitly
on ops. (The session declarations still resolve into FEMData and
persist into the model.h5 neutral zone for the viewer / Results --
they are simply not pulled into the deck.)
4.1 Masses -- ops.mass¶
values is an ndf-length tuple. No pattern grouping, no ordering
concerns -- one lumped-mass declaration per physical group.
4.2 Loads and prescribed SP -- patterns¶
Nodal loads and non-zero prescribed displacements are pattern-scoped:
ts = ops.timeSeries.Linear() # also Constant/Path/Trig/Pulse
with ops.pattern.Plain(series=ts) as p: # also UniformExcitation
p.load(pg="Tip", forces=(0.0, 0.0, -5e4))
p.sp(pg="LoadingPin", dof=3, value=0.01) # prescribed displacement
p.load / p.sp fan a pg= across the group's nodes at build time;
node= takes an explicit tag or a Node from ops.nodes.get(...).
Each session load pattern you declared with g.loads.pattern(...)
becomes a separate ops.pattern.* block here, each with its own
timeSeries. The forces= tuple length must match the model ndf.
Distributed/body loads (gravity, surface pressure) are not patterns
-- they are element parameters (body_force=, pressure=) on the
ops.element.* call (§3.1).
4.3 Migration of the old ingest call¶
Old g.opensees.ingest.X(fem) |
New |
|---|---|
.loads(fem) |
with ops.pattern.Plain(series=ts) as p: p.load(pg=…, forces=…) |
.masses(fem) |
ops.mass(pg=…, values=…) |
.sp(fem) (homogeneous face_sp) |
ops.fix(pg=…, dofs=…) |
.sp(fem) (prescribed face_sp) |
p.sp(pg=…, dof=…, value=…) |
gravity via g.loads.gravity(...) |
element body_force=(b1,b2,b3) param |
.constraints(fem, tie_penalty=) |
deferred -- no path (§4.4) |
4.4 Multi-point constraints are DEFERRED¶
⚠️ Multi-point constraints (
tie/rigid_link/equal_dof/rigid_diaphragm/node_to_surface/tied_contact/mortar, and embedded rebar) are deferred inapeSees-- there is no OpenSees emission path. TheEmitterprotocol exposes onlynode / fix / mass / element / sp; there is noequalDOF,rigidLink,rigidDiaphragm, orASDEmbeddedNodeElement. This is deferred by design (ADR 0009;src/apeGmsh/opensees/architecture/_DEFERRED.md).
Consequences:
- Declare the constraints on the session as usual -- they resolve into
FEMDataand are persisted into themodel.h5neutral zone byops.h5(path), so the viewer /Resultsrender them. - They are not written to any runnable Tcl/Py/Live deck. A model whose load path depends on a tie / rigid link will be wrong if you run the emitted deck as-is.
- To run such a model today, hand-emit the constraint commands by
iterating
fem.nodes.constraints/fem.elements.constraintsinto raw openseespy yourself, or wait for the deferred feature.
Homogeneous fixities (ops.fix) and prescribed displacements
(p.sp) do have a path -- only the multi-point couplings are
deferred.
5. Building the Model -- build()¶
ops.build() resolves every typed declaration and pg= selector
against the bound fem snapshot and returns an immutable
BuiltModel that the emitters consume. You rarely call it directly
-- ops.tcl / py / h5 / run each build internally.
build() raises early with a pointed error if the model is
inconsistent: a missing geomTransf on a beam, an ndm/ndf mismatch,
an ambiguous pg=, or a dofs mask whose length is not ndf.
6. Emit / Run¶
apeSees writes the built model to disk or runs it in process. These
are separate statements -- not a fluent chain. Each tcl / py / h5
/ run calls build() internally.
6.1 Tcl script -- ops.tcl(path)¶
Produces a complete OpenSees Tcl input file: model builder, nodes, materials/sections/transforms, element connectivity with physical-group comments, fix commands, nodal masses, and load patterns. Multi-point constraints are not emitted (deferred -- §4.4).
6.2 openseespy script -- ops.py(path)¶
Produces an equivalent Python script using
openseespy.opensees as ops. The structure mirrors the Tcl output.
ops.tcl(...) and ops.py(...) are independent statements -- write
them on separate lines, not as a chain:
6.3 Native HDF5 + live run¶
ops.h5("model.h5") # bridge /opensees/ zone + broker neutral zone
ops.run() # in-process openseespy (LiveOpsEmitter)
ops.analyze(steps=10, dt=0.01) # drive the analysis chain
ops.h5(path) writes the canonical model.h5: the bridge's emitted
/opensees/ zone plus the broker neutral zone (which carries the
session's loads/masses/constraints for the viewer / Results).
6.4 Recorders¶
Recorder declarations live on ops.recorder.<Type>. They are emitted
alongside the model in tcl / py / h5. See
guide_recorders_reference.md.
7. Inspection¶
Inspection is broker-side or post-emit -- there is no inspect
sub-composite on the bridge.
7.1 Summary¶
Returns a multi-line string from the broker: registered record sets, node/element counts by type and physical group. The quickest sanity check before emitting.
7.2 Broker tables¶
DataFrames of the broker's nodes and elements -- coordinates, connectivity, physical-group membership. Useful for verifying placement before emitting.
7.3 Post-emit reader¶
The reference reader inspects an emitted model.h5 -- the
ground-truth check of what actually went into the deck.
8. Complete Example¶
End-to-end: create a concrete block, mesh, take the snapshot, declare materials/BCs/loads explicitly on the bridge, and emit.
from apeGmsh import apeGmsh
from apeGmsh.opensees import apeSees
# ── Session ───────────────────────────────────────────────
with apeGmsh("concrete_block") as g:
# ── Geometry ──────────────────────────────────────────
# A simple 2m x 1m x 1m block
g.model.geometry.add_box(0, 0, 0, 2.0, 1.0, 1.0, label="Block")
# Label the base surface for boundary conditions
g.model.selection.select_surfaces(on_plane=("z", 0.0)).label("Base")
# Label the top surface for loading
g.model.selection.select_surfaces(on_plane=("z", 1.0)).label("Top")
g.model.geometry.synchronize()
# Promote the labels the solver needs into physical groups
g.physical.from_label("Block", name="Block")
g.physical.from_label("Base", name="Base")
g.physical.from_label("Top", name="Top")
# ── Mesh ──────────────────────────────────────────────
g.mesh.generation.set_size_global(0.15)
g.mesh.generation.generate(3)
# ── FEMData snapshot ──────────────────────────────────
fem = g.mesh.queries.get_fem_data(dim=3)
# ── OpenSees -- post-session, explicit declarations ──────
ops = apeSees(fem)
ops.model(ndm=3, ndf=3)
# ── Material ──────────────────────────────────────────────
conc = ops.nDMaterial.ElasticIsotropic(E=30e9, nu=0.2, rho=2400)
# ── Element assignment (gravity is an element body_force) ─
ops.element.FourNodeTetrahedron(
pg="Block", material=conc,
body_force=(0.0, 0.0, -9.81 * 2400),
)
# ── Boundary conditions ──────────────────────────────────
ops.fix(pg="Base", dofs=(1, 1, 1))
# ── Loads -- RE-DECLARED explicitly (no ingest) ──────────
with ops.pattern.Plain(series=ops.timeSeries.Linear()) as p:
p.load(pg="Top", forces=(0.0, 0.0, -50e3))
# ── Sanity check + emit ──────────────────────────────────
fem.inspect.summary()
ops.tcl("block_model.tcl")
ops.py("block_model.py")
The emitted files are self-contained model definitions. Run them with
opensees block_model.tcl or python block_model.py after appending
your analysis commands (the bridge intentionally omits analysis setup).
9. Practical Advice¶
Declaration order¶
ops.model(...) must come first. After that, materials, elements,
fix, mass, and patterns can be declared in the order that reads best;
they are resolved at build() (implicit in emit). Capture every
material/section/transf/integration handle in a variable before the
element call that consumes it.
Physical groups vs. labels¶
pg= resolves either a Gmsh physical-group name or an apeGmsh label --
labels resolve automatically (FEM-direct), so promote_to_physical is
not required for the OpenSees workflow. Keep PG names dimension-unique
so pg= is never ambiguous.
Beam elements need geomTransf¶
Beam elements do not use a material registry. Their section properties
are scalar kwargs (A=0.04, E=200e9) for elasticBeamColumn, or a
beamIntegration handle for forceBeamColumn, but they always require
a geomTransf handle. Forgetting it is the most common beam-model
error -- build() catches it.
Shell elements need ndf=6¶
Shell elements require six DOFs per node. If your model mixes shells
with solids, set ndf=6 for the entire model.
The deck stops at model definition¶
The emitted scripts omit analysis commands (integrator, algorithm,
system). Append your analysis block or source/import the generated
file from a driver script. Recorders, if you declared them on
ops.recorder.*, are emitted.
Tie / non-conformal interfaces are DEFERRED¶
You can declare ties for non-matching meshes on the session:
The tie resolves into FEMData and persists into model.h5 for the
viewer / Results, but apeSees has no OpenSees emission path
for it (deferred -- §4.4). A model whose load path depends on a tie
will be wrong if you run the emitted deck as-is. To run such a model
today, hand-emit the constraint commands from fem.nodes.constraints /
fem.elements.constraints into raw openseespy yourself, or wait for
the deferred feature.
Loads/masses/SP are not ingested¶
The single biggest behavioural change from the old bridge: there is no
ingest step and no auto-resolution. apeSees does not read
g.loads / g.masses / g.constraints or the resolved
fem.nodes.* records. Re-declare loads/masses/SP explicitly on ops
(§4). The session declarations are still preserved for the viewer /
Results via the model.h5 neutral zone -- they just do not enter
the runnable deck automatically.
Emit calls are separate statements¶
ops.tcl(...), ops.py(...), ops.h5(...), ops.run() are not
fluent. Write each on its own line; each builds internally.