Sections¶
This page explains what a cross-section is in apeGmsh — one word the
library uses in three related places — so you always know which layer you're
working in: sections you mesh (g.sections builds the shape as labeled
geometry), sections you declare to the solver (ops.section primitives
on the bridge, elastic or fiber), and sections you compute (the
SectionProperties analyzer, which turns a drawn face into the numbers a
frame model needs).
The layer is decided by one question: does the mesh carry the cross-section, or does an element property? In a continuum model — a wide-flange column meshed with bricks — the section is literally the geometry, and the mesh resolves stress across the flanges and web. In a frame model the member is a line, the mesh knows nothing about its shape, and the cross-section is a constitutive statement you hand to the element. And when the constants for that statement should come from drawn geometry rather than a handbook table, the third layer computes them. The three meet at the bridge, but they enter the workflow at different moments — geometry before meshing, primitives after the snapshot — which is why they live in different namespaces.
Sections you mesh: g.sections¶
g.sections is a catalog of parametric structural shapes built as real
geometry, directly in the session — no Part, no file, no import step. Ask for
a wide-flange column and you get a 3D solid, extruded along Z:
The shape itself is nothing you couldn't build from boxes and boolean cuts.
What the builder adds is structural naming: the solid arrives pre-sliced at
the flange–web boundaries, and every sub-region carries a label derived from
yours — col.top_flange, col.bottom_flange, col.web, plus col.start_face
and col.end_face for the two ends. (The returned handle spells them for you:
col.labels.web is the string "col.web".) This is the naming doctrine from
the mental model applied to structural shapes — the
sub-regions an engineer actually targets exist as names from the moment the
geometry does:
g.physical.add_surface("col.start_face", name="Base") # fix the bottom
with g.loads.case("dead"):
g.loads.gravity("col.web", density=7850) # target sub-regions
The catalog covers the common prismatic shapes — W_solid, rect_solid,
rect_hollow (HSS), pipe_solid, pipe_hollow, angle_solid,
channel_solid, tee_solid — each with the label set its shape calls for
(an angle names its horizontal_leg and vertical_leg; simple shapes get a
single body). W_shell is the mid-surface sibling: same parameters, but
flanges and web arrive as surfaces at their mid-planes, ready for shell
meshing at dim=2 instead of solids at dim=3.
Two knobs matter in practice. translate and rotate place the section
(built at the origin, using the same axis–angle convention as
g.model.transforms.rotate), and lc sets a local target element size on
the section's points — the default imposes no constraint, so the global size
governs unless you say otherwise. After placing several members you
g.parts.fragment_all() for a conformal mesh at the joints, exactly as with
any other geometry.
If that sounds close to a Part, it is — both produce labeled geometry — and the boundary is worth stating. A Part is a reusable template with its own session, persisted to a STEP file you can hand around: reach for it when one column design repeats twenty times. A section is a one-off parametric member that exists only in the assembly session: reach for it when this girder, with these plate dimensions, appears once. Parts survive session restarts; sections are rebuilt by the script that declared them — which, for parametric work, is precisely what you want.
Sections you declare: ops.section¶
In a frame model the cross-section crosses to the other side of the snapshot.
The mesh is a line of beam elements; the section is no longer geometry but a
typed primitive you build on the OpenSees bridge, under
ops.section.<Type>(...). Like every bridge primitive, it registers itself
when constructed — tags resolve automatically, and you pass the returned
handle wherever a section is consumed (a beamIntegration, a
zeroLengthSection, an element's section= field).
The fundamental choice is elastic versus fiber, and it is a modeling decision, not an API detail. An elastic section is the cross-section reduced to a handful of constants:
Give it A, E, Iz (plus Iy, G, J for the 3-D form) and it bends
forever along a straight line in the moment–curvature plane — the right
answer whenever the member is meant to stay linear. A fiber section makes the
opposite bet: instead of handing the element an Iz, you tile the
cross-section with small material cells, each carrying its own uniaxial
stress–strain law, and the section derives its response — elastic at first,
then yielding from the extreme fiber inward, then a plastic plateau:
steel = ops.uniaxialMaterial.Steel02(fy=Fy, E=E, b=0.005)
section = ops.section.Fiber(patches=(
RectPatch(material=steel, ny=4, nz=1, # top flange
yI=d/2 - tf, zI=-bf/2, yJ=d/2, zJ=bf/2),
RectPatch(material=steel, ny=24, nz=1, # web
yI=-(d/2 - tf), zI=-tw/2, yJ=d/2 - tf, zJ=tw/2),
RectPatch(material=steel, ny=4, nz=1, # bottom flange
yI=-d/2, zI=-bf/2, yJ=-(d/2 - tf), zJ=bf/2),
))
The building blocks are RectPatch (fill a rectangle with ny × nz fibers),
StraightLayer (a line of fibers — rebar), and FiberPoint (one fiber),
laid out in the section's local (y, z) frame; ops.section.W_fiber(...) is a
parametric shortcut that assembles the three-patch wide-flange for you. One
rule to internalize: build the materials and the section through the bridge
(ops.uniaxialMaterial..., ops.section.Fiber(...)), not by constructing
dataclasses standalone, so the bridge can register them and resolve every
fiber's material tag at build time.
The catalog rounds out with the shell-side sections —
ElasticMembranePlateSection for a single-layer plate, LayeredShell and
LayeredShellFiberSection for stacked layers (an RC wall with cover,
core, and smeared-rebar layers) — and Aggregator, which composes uniaxial
laws DOF-by-DOF on top of an optional base section (a shear spring bolted
onto a flexural section). To watch a fiber section reproduce first-yield and
plastic moments against hand calculations, work through
Fiber sections & moment–curvature.
Sections you compute: the analyzer¶
Between the drawn shape and the declared constants sits an obvious gap: where
do A, Iz, J, and the shear areas come from when the section isn't in
a catalog — built-up plates, an SRC column, an imported DXF outline? The
SectionProperties analyzer closes that gap in-process: mesh the
cross-section as a flat 2-D face, and it computes the full property set from
that mesh — no external tool, no re-entered dimensions.
The workflow reuses everything you already know. Every solid recipe has a
flat-face sibling (W_face, rect_face, rect_hollow_face, pipe_face,
pipe_hollow_face, angle_face, channel_face, tee_face) — though any
meshed face works; raw OCC loops and DXF or STEP imports are equal routes,
the builders are convenience, not a requirement. You mesh it like any other
model, at second order — warping and shear-area solves are FE problems in
their own right, and constant-strain triangles converge poorly on them:
g.sections.W_face(bf=400.0, tf=25.0, h=1200.0, tw=12.0, label="girder")
g.mesh.sizing.set_global_size(15.0)
g.mesh.generation.generate(dim=2)
g.mesh.generation.set_order(2) # tri6 — warping-grade
fem = g.mesh.queries.get_fem_data(dim=2)
from apeGmsh import SectionProperties
from apeGmsh.sections import SectionMaterial
sec = SectionProperties(
fem, materials={"girder": SectionMaterial(E=200e3, nu=0.3, fy=345.0)},
)
geo, warp, plas = sec.geometric(), sec.warping(), sec.plastic()
The analyzer is a declaration — frozen inputs, memoized results — and its
three analyses split by cost: geometric() is pure quadrature (area,
centroid, inertias, elastic moduli), warping() is the FE solve (torsion
constant, shear center, shear areas), plastic() locates the plastic
neutral axes (plastic moduli, Mp, shape factors). Stress recovery and a
family of matplotlib plots — von Mises contours, shear-flow quivers, a Qt
inspector via sec.viewer() — hang off the same object.
Materials make it composite-aware: materials= maps physical-group names to
SectionMaterials, so an SRC column is just a face partitioned into a
"concrete" PG and a "steel" PG. Composites impose one naming law worth
knowing at concept level — rigidity-form fields (EA, EIxx_c, GJ) are
always valid, while unprefixed accessors (Ixx_c, J) divide by the
modulus and therefore raise CompositeSectionError on a composite, where no
single modulus exists; you pick a reference explicitly with
transformed(e_ref=...). Omit materials= entirely for geometric-only mode,
the classic unit-modulus numbers.
The last step is the reason the analyzer lives in apeGmsh at all. Instead of
copying numbers into an ops.section.Elastic(...) call, you bind the
declaration to the bridge:
girder = ops.section.ComputedSection(analysis=sec) # lazy; resolves at emit
integ = ops.beamIntegration.Lobatto(section=girder, n_ip=5)
ops.element.forceBeamColumn(pg="girders", transf=transf, integration=integ)
ComputedSection holds a reference to the analyzer and resolves it at emit
time into a plain section Elastic line — byte-identical to one you'd type
by hand, so it slots into every consumer unchanged. The emitted deck
therefore always follows the drawn geometry: edit the face, re-run, and the
frame model updates with it. The lowering owns the axis mapping (authoring
x becomes the element's local z, authoring y its local y — so Ixx_c → Iz,
Iyy_c → Iy, and the shear-area ratios become alphaY/alphaZ), analyses
are memoized so many references to one analyzer cost one solve, and a
composite without explicit reference E=/G= fails loud at emit rather than
guessing a modulus. The full recipes — the SRC composite, the disconnected
multi-part policy, the gotchas — are in
Compute section properties.
Sections you author: the document¶
The three layers above all assume the section already exists — as code you wrote, as primitives you declared, as a face you meshed. The fourth layer is about building it, and about the section surviving the script that built it.
A SectionDocument is a versioned JSON description of one cross-section,
and it is the source of truth: the headless API and the Qt builder GUI are
both clients of it. It comes in the same two lanes the rest of this page
does — a continuum document (parametric shapes, freehand polygons,
booleans, per-region materials, mesh preferences) that builds into a
SectionProperties, and a fiber document (patches, layers, points, and
parametric RC templates) that builds into a resolved fiber recipe:
from apeGmsh.sections import SectionDocument
doc = SectionDocument.new(name="col500", kind="fiber", units="N, mm")
doc.set_material("conc", uniaxial=("Concrete01", {...}))
doc.set_material("bars", uniaxial=("Steel01", {"fy": 420.0, "E": 200e3, "b": 0.01}))
doc.add_template(
"rc_rect_column", materials={"concrete": "conc", "bars": "bars"},
b=500.0, h=500.0, cover=40.0, bars_x=4, bars_y=4, bar_area=491.0,
)
doc.save("col500.section.json")
Two properties make this more than a serialization format. First, RC finally
has a first-class path: rc_rect_column, rc_circ_column, and rc_beam
are stored as parameters and re-expanded on every build, so changing
cover moves every bar rather than requiring you to re-derive coordinates —
and core_split=True partitions the concrete exactly into a confined core
and a cover shell for you to assign Mander-style materials to. Second, the
document owns the composite-partition law: add_embed(outer, inner) is
cut-then-fragment as one step, so the double-cover trap that
Compute section properties warns about
is not expressible.
The builder GUI (launch_builder()) is an editor for that document, with
AutoCAD-style drafting aids for the freehand polygon tool — grid and object
snap, ortho, typed length<angle input. Its governing rule is the parity
law: every GUI action is a document mutation, so a script can do anything
the window can. What the window adds is convenience — a live properties
panel that rebuilds off the UI thread, an apeSteel catalog picker that
prefills a W_face form, and one-click moment–curvature.
That last one is also a headless API:
from apeGmsh.sections import moment_curvature
mc = moment_curvature(doc, axis="z", kappa_max=8e-5, n_steps=60, axial=-1500e3)
mc.EI0 # initial stiffness — the exact fiber sum ΣE·A·y²
mc.M_max # largest |M| reached, signed
mc.complete # False when the section lost stiffness before κ max
It builds the section on a two-node zeroLengthSection harness in-process,
which means it wipes the global OpenSees domain and must not be called
while a live analysis is open.
The document never becomes the only route. export_script() writes the
plain apeGmsh script the document is equivalent to (one-way, for reading and
for escaping the format), and handoff_snippet() writes the few lines that
put the finished section on a bridge — for a fiber document, the literal
ops.section.Fiber(...) construction; for a continuum one, a
SectionDocument.open(...) that lowers late, so numbers are never
hand-copied out of a GUI. Start at
Author a section document.
Next: Constraints.