Engine Reference¶
The calculation engine (xdfem2d) is usable on its own, without the interface.
This page documents what it exposes: the Python API (a) and the JSON data
structure (b) that .x2d/.json files store.
Units are metres, kN, kN·m and kN/m². Angles are in degrees where an angle is
named. A model has a fixed domain — plane (frames/walls: ux, uy, θz) or
plate (slabs/grillages: w, θx, θy); the three DOF slots keep the same
positions in both, only their meaning changes.
from xdfem2d import Structure2D
s = Structure2D(domain="plane") # or "plate"
Contents¶
- Python API
- Model, solve and I/O
- Materials and sections
- Nodes and elements
- Geometry objects (parametric; meshed at solve time)
- Supports and springs
- Loads
- Analysis, combinations, dynamics
- Constraints and fields
- Section cuts
- Simplified API (the "simple" facade)
- Compatibility appendix
- Member buckling (steel EC3 / timber EC5)
- Design reports (calculation trace)
- JSON data structure
- Top level
- Notes on the slots
- See also
Python API¶
Everything below is a method of Structure2D, except "Section cuts",
"Member buckling" and "Design reports", which live as plain functions in
their own Qt-free modules (xdfem2d.cuts, xdfem2d.steel_members,
xdfem2d.design_report) — each of those sections says so again where it
starts. Signatures omit self; … -> Type is the object returned. Loads
take a positive value in the axis direction (gravity is negative). In the
plate domain the aliases fz/mx/my, w/tx/ty, kz/ktx/kty fill the same
slots as fx/fy/mz, ux/uy/tz, kx/ky/kt.
The sections below follow the order in which a model is typically built: model + solve, materials/sections, elements, geometry, supports, loads, analysis, constraints — then the simplified facade and the design layer (buckling, reports).
The
create_*/add_*convention, used throughout this document: most things can be built two ways — acreate_*shortcut (a magnitude/name, sane defaults, get-or-create where that makes sense; prefer this) or the genericadd_*call it forwards to (every field spelled out; reach for it only when a shortcut cannot express what you need — a local-frame load with both components, a trapezoidal axial load, and similar). Each subsection below lists thecreate_*layer directly above theadd_*layer it delegates to, so the two stay next to each other instead of the convention being re-explained per subsection.A few terms are used interchangeably below for the same thing: "geometry object", "surface object" and "region" all mean an item in
geometry_objects(aGeoRectangle,GeoPolygon,GeoSegment, …, meshed into bars/triangles/quads at solve time) — the canonical name in code and in the JSON structure is geometry object.
Model, solve and I/O¶
Structure2D(domain="plane")
calculate() -> dict # assemble + solve; returns results
check_references() # raise on dangling/invalid refs
domain_problems() -> list[str] # features unsupported by the domain
save(path) # write .x2d / .json
load(path) -> Structure2D # (classmethod-style) read a file
Materials and sections¶
create_* (convenience) directly above the add_* (fundamental) it
forwards to — see the convention note above.
Materials — create_* (convenience)¶
create_material(...) # == add_material, pure alias
create_rc_material(concrete="C30/37", steel="B500B",
name=None) -> Material # get-or-create, deduped by name
create_steel_material(steel="S275", name=None) -> Material
create_timber_material(timber="C24", name=None) -> Material
name=None derives the name from the class string (e.g. "C30/37").
Get-or-create: two calls with the same class dedupe to one Material.
Materials — add_* (fundamental)¶
add_material(name, elastic_modulus, unit_weight, alpha=1e-5, unit_mass=None,
material_type=None, design=None, poisson=0.2) -> Material
add_concrete_material(material_name, concrete_class, steel_class,
gamma_c=1.5, gamma_s=1.15, alpha_cc=1.0) -> ConcreteMaterial
add_concrete_material links EC2 design metadata onto an already-existing
material by name — a different job from create_rc_material, which makes
the physical material itself.
Sections — create_* (convenience)¶
create_bar_section(name, material_name, b, h, **kw) -> Section
# material_name resolves an unrecognised Eurocode class automatically first
create_area_section(name, material_name, thickness=0.2, **kw) -> TriSection
# domain-aware pair: plane -> CST (tri) / QM6 (quad), plate -> MITC3 (tri) / MITC4 (quad)
# add_area_section under the hood: a same-named TriSection AND QuadSection,
# so create_area_element resolves whichever shape it ends up building
create_rc_section(name, b, h, concrete="C30/37", steel="B500B",
cover=0.045) -> Section # material + section, one call
create_steel_section(name, profile, grade="S275") -> Section # profile e.g. "IPE300"
create_timber_bar_section(name, b, h, timber="C24",
service_class="SC1") -> Section # no add_* predecessor
create_rc_section/create_steel_section/create_timber_bar_section are
the canonical names for a material-plus-section shortcut per family;
add_rc_section/add_steel_section are kept working as aliases (see the
compatibility appendix).
Sections — add_* (fundamental)¶
add_section(name, material_name, b, h, area_override=None, inertia_override=None,
profile_name=None, section_type=None, rc_cover=0.045, rc_alpha_s=90.0,
rc_bar_phi=16.0, shape=None, tw=0.0, tf=0.0, rc_shear_min=True,
torsion_override=None) -> Section # beam / grid section (J)
add_tri_section(name, material_name, thickness=0.1, plane_strain=False,
formulation="CST", rc_cover=0.045, rc_alpha_s=90.0,
rc_bar_phi=16.0, rc_shear_min=True) -> TriSection # panel
add_plate_section(name, material_name, thickness=0.2, formulation="MITC3", **kw) -> TriSection # slab
add_quad_section(name, material_name, thickness=0.1, plane_strain=False,
formulation="MITC4", **kw) -> QuadSection
# 4-node area section. formulation: "Q4"/"QM6" (plane wall) | "MITC4"/"DKT4" (plate slab)
add_area_section(name, material_name, thickness=0.1, plane_strain=False,
formulation="CST", quad_formulation="QM6", **kw) -> TriSection
# add_tri_section + add_quad_section under one name; returns the TriSection,
# the QuadSection is self.quad_sections[name]. A bad quad_formulation rolls
# the tri half back rather than leaving an unpaired section.
The quad section fields mirror the triangle section's; a wall uses "QM6"
(recommended) or "Q4", a slab uses "MITC4" (recommended) or "DKT4". Edge,
area (pz), temperature and Winkler-spring loads all have quad targets, via the
same add_area_load / create_temperature / add_area_spring dispatchers (the
id resolves to the triangle or quad list) plus add_quad_edge_load /
add_quad_temperature_load.
add_tri_section/add_quad_section stay the exact single-shape calls they
always were — every existing caller that already knows its shape keeps using
them unchanged. add_area_section is a newer, additional entry point for a
caller (the create_* layer, or a GUI dialog) that wants "a section under
this name, whichever shape ends up used" without picking tri or quad up
front; internally the registries (self.tri_sections, self.quad_sections)
stay separate, paired only at this call.
Every field spelled out, material_name must already exist — what every
create_* call above forwards to.
Nodes and elements¶
Nodes and elements — create_* (convenience)¶
create_node(x, y, id=None) -> Node # id auto-assigned: "1", "2", ...
create_node_list(outline, id_prefix=None) -> list[Node] # flexible outline, see Geometry objects
create_bar_element(section, node_i, node_j, id=None, **kw) -> BarElement
# id auto: "B1", "B2", ...; section first, then the nodes it applies to;
# node_i/node_j: id or Node; section: name or Section
create_area_element(section, node_i, node_j, node_k, node_l=None, id=None, **kw)
-> TriElement | QuadElement
# id auto: "T1", "T2", ... (triangle) or "Q1", "Q2", ... (quad); section first;
# node_l is the only thing that decides triangle (3 nodes) vs quad (4 nodes) —
# one call covers both instead of picking add_tri_element/add_quad_element
# up front
id optional (auto-assigned), and node/section arguments accept the object
returned by another create_* call as well as a raw id/name. Section comes
first in both calls — the target it belongs to precedes the nodes it applies
to, one order across the whole create_* element layer:
n = s.create_node_list([0,0, 5,0, 10,0])
sec = s.create_rc_section("B30x60", 0.30, 0.60)
s.create_bar_element(sec, n[0], n[1])
Nodes and elements — add_* (fundamental)¶
add_node(id, x, y) -> Node
add_bar_element(id, node_i, node_j, section_name, hinge_i=False, hinge_j=False,
rc_design=False, rc_cover=0.025, rc_cotg_theta=1.0,
rc_alpha_s=90.0) -> BarElement # Beam (plane) / Grid (plate)
add_tri_element(id, node_i, node_j, node_k, section_name) -> TriElement # Panel/Slab
add_quad_element(id, node_i, node_j, node_k, node_l, section_name) -> QuadElement
# 4-node area element; nodes counter-clockwise; Panel/Slab (see add_quad_section)
add_area_element(id, node_i, node_j, node_k, node_l=None, *, section_name)
-> TriElement | QuadElement
# 3 nodes -> add_tri_element, 4 (node_l given) -> add_quad_element;
# section_name is keyword-only (its slot would otherwise shift depending
# on which branch runs)
Element ids are unique across kinds — a bar, a triangle and a quad may not share the same id. A quad is validated at creation (4 distinct nodes, counter-clockwise winding, convex, positive Jacobian); a bad quad raises rather than being silently repaired.
add_tri_element/add_quad_element stay the exact single-shape calls they
always were — the ~200 existing call sites that already know their shape
keep using them unchanged. add_area_element is a newer, additional entry
point for a caller that does not know the shape up front (the create_*
layer, or an LLM); it dispatches to one of the other two rather than
replacing them. section_name is keyword-only on add_area_element
specifically because its positional slot would otherwise shift depending on
which branch runs.
Geometry objects (parametric; meshed at solve time)¶
Geometry objects — create_area (convenience)¶
create_area(id, outline, thickness, concrete="C30/37", steel="B500B",
cover=0.045, target_size=0.5) # 2 pts → rectangle, ≥3 → polygon
Region + RC section in one call; the domain picks slab (plate) or wall
(plane). add_panel is kept working as an alias (see the compatibility
appendix). Keep the return: .node_ids are the real corner nodes
(rectangle, CCW order: bottom-left, bottom-right, top-right, top-left) — for
example to put create_node_load on one specific corner. Named edges
(support_edge, "### Supports and springs") work on this outline as long
as the 4 points are given in that same order.
outline (and any point list — including create_node_list above) is
flexible: [(x,y),…] pairs, a flat [x1,y1,x2,y2,…], {"x":…,"y":…}
dicts, or two columns [[x1,x2,…],[y1,y2,…]] (for 3+ points). Exactly 2
points build a rectangle; 3 or more build a polygon — including a rectangle
spelled out as its 4 corners.
Geometry objects — add_* (fundamental)¶
add_geo_segment(id, x1, y1, x2, y2, section_name="", divisions=1,
max_chord=0.0) -> GeoSegment
add_geo_arc(id, cx, cy, radius, start_angle, end_angle, section_name="",
divisions=8, max_chord=0.0) -> GeoArc
add_geo_arc_3pts(id, p1, pm, p2, section_name="", divisions=8, max_chord=0.0)
add_geo_multisegment(id, vertices, closed=False, section_name="", divisions=1,
max_chord=0.0) -> GeoMultisegment
add_geo_rectangle(id, corner_a, corner_c, section_name="",
target_size=0.5, prefer_quad=True) -> GeoRectangle
add_geo_polygon(id, vertices, section_name="", target_size=0.5,
prefer_quad=True) -> GeoPolygon
remove_geo_object(id)
The old names add_geo_line / add_geo_polyline / add_geo_surface remain as
aliases of add_geo_segment / add_geo_multisegment / add_geo_polygon. For
a rectangle/polygon the section argument is section_name (the old keyword
tri_section_name= still works); the stored attribute and the .x2d field
keep the name tri_section_name.
Supports and springs¶
create_*/pin/fix/roller/symm (convenience) directly above the
add_* (fundamental) each forwards to — see the convention note above.
Supports — convenience¶
pin(nodes) # plane: ux,uy · plate: w
fix(nodes) # plane: ux,uy,θz · plate: w,θx,θy
roller(nodes, free="x") # plane only — frees one translation
symm(nodes, axis="x") # plate only — symmetry line (w free, θ about axis fixed)
create_support(nodes, ux=False, uy=False, tz=False, w=None, tx=None, ty=None,
name=None)
nodes: a single id/Node, or a list of either (same list-or-single rule as
loads). pin/fix/roller/symm cover the common cases with an
auto-assigned canonical support name ("PIN", "FIXED", "ROLLER-X", "SYM-X", …);
create_support is the generic one-call create-and-assign underneath them.
True fixes that DOF, False frees it — every flag defaults to False,
so create_support(nodes) on its own restrains nothing. A model that
means to fully fix a node has to say ux=True, uy=True, tz=True (or just
call fix(nodes)); writing all three explicitly as False "to be clear"
restrains nothing and turns the structure into a mechanism — a real mistake
a script has made. w/tx/ty are the plate-domain aliases of the same
three (ux,uy,tz) slots.
Supports — fundamental¶
add_support(name, ux=False, uy=False, tz=False, w=None, tx=None, ty=None) -> Support
assign_support(node_id, support_name) -> SupportAssignment
The two calls every convenience wrapper above forwards to: create the named
restraint, then assign it to a node. Reach for these directly to reuse one
named Support across many assign_support calls without re-specifying its
flags each time.
Supports on an edge — support_edge (and pin_edge / fix_edge / …)¶
support_edge(object_id, edge, kind="pin") # the general call
pin_edge(object_id, edge) # = support_edge(…, 'pin') plate SIMPLE · plane PIN
fix_edge(object_id, edge) # = support_edge(…, 'fix') plate CLAMPED · plane FIXED
roller_edge(object_id, edge, free="x") # = support_edge(…, 'roller-x'/'roller-y') plane only
symm_edge(object_id, edge, axis="x") # = support_edge(…, 'symm-x'/'symm-y') plate only
support_object_edge(object_id, edge, support_name) # any named support / 'free'
free_edge(object_id, edge) # remove — clear that edge
support_edge is the general call; pin_edge/fix_edge/roller_edge/
symm_edge are it with a fixed kind. edge: 'all' |
'bottom'/'right'/'top'/'left' | an integer index (vertex i → i+1,
wrapping) — the same vocabulary create_edge_load (see "### Loads") uses.
kind: pin | fix | symm-x | symm-y | roller-x | roller-y. All are
domain-aware (roller-* is plane-only, symm-* plate-only, exactly as the node
methods are) and resolve to the canonical support (SIMPLE/CLAMPED/…).
They all write the object's per-edge edge_supports list, so every mesh node
the edge generates is restrained, and restraints can differ per edge (e.g.
clamped / simply / clamped / simply around a rectangle). This is the reliable
route: an earlier support_edge restrained only the two corner nodes and left
edge_support_mode to propagate the rest — the approach the per-edge list was
added to replace.
support_object_edge takes an explicit support name instead of a kind: any
support already defined (or a canonical name such as SIMPLE / CLAMPED), or
'free' to clear that edge. free_edge is the inverse of the whole family — it
frees an edge whether it was restrained through the edge_supports list or by
corner-node assignments.
In a JSON model the same thing is the object's "edge_supports" key: a list
with one entry per edge in perimeter order (a rectangle is bottom, right, top,
left), each a support name or "free". For example, a slab simply supported on
two opposite edges:
{ "kind": "rectangle", "id": "R1", "tri_section_name": "Slab",
"node_ids": ["R1.p0", "R1.p1", "R1.p2", "R1.p3"],
"edge_supports": ["SIMPLE", "free", "SIMPLE", "free"] }
When edge_supports is absent, edge_support_mode ("common" default /
"equal" / "none") instead propagates each edge from whatever supports its
two corner nodes carry — the older corner-propagation fallback, used when the
corners are supported directly (assign_support/pin on the corner nodes)
rather than through edge_supports.
Springs¶
add_node_spring(node_id, kx=0.0, ky=0.0, kt=0.0, mode_x="both", mode_y="both",
kz=None, ktx=None, kty=None) -> NodeSpring
add_element_spring(element_id, kx=0.0, ky=0.0, coord_sys="global",
mode_x="both", mode_y="both") -> ElementSpring
add_line_element_spring(object_id, kx=0.0, ky=0.0, kx_field="", ky_field="",
coord_sys="global", mode_x="both", mode_y="both")
add_area_spring(target_id, kz=0.0) # Winkler slab-on-grade (plate)
No create_* layer — springs have no single "common case" to shortcut the
way a pinned/fixed support does.
Loads¶
create_* (convenience) directly above the add_* (fundamental) it
forwards to, grouped by domain — see the convention note above. Split into
small subsections so a search for one call returns its own explanation
intact. Every load below belongs to a LoadCase, so that pair comes first.
Load cases — create_* (convenience)¶
create_load_case(id=None, self_weight_factor=0.0, action_type=None,
create_analysis_case=True) -> LoadCase
id optional — auto-assigned ("LC1", "LC2", …) when omitted.
action_type accepts the Eurocode letter ("G"/"Q"/"W"/"E"/"T"),
the word ("permanent"/"live"/"wind"/"seismic"/"temperature",
case-insensitive), or the old dashed label — whichever is written, the
stored value is always the letter. create_analysis_case=True also creates
a matching AnalysisCase with the same id (see "### Analysis, combinations,
dynamics" below) so a new load case is solvable right away, without a
separate call.
Load cases — add_* (fundamental)¶
add_load_case(id, self_weight_factor=0.0, action_type=None,
create_analysis_case=True) -> LoadCase
id required — what create_load_case above forwards to.
Bar and area loads — create_* (convenience)¶
create_bar_uniform_load(elements, load_case_id=None, q=None, direction="down") # UDL on a bar or geo line object
create_bar_distributed_load(elements, load_case_id=None, q_i=None, q_j=None, direction="down") # trapezoidal; bars only
create_bar_point_load(elements, load_case_id=None, p=None, at=0.5, direction="down") # bars only; at = span fraction
create_area_uniform_load(targets, load_case_id=None, p=None, direction="down") # slab (plate); area equivalent of the above
elements/targets: a single id/object, or a list of either (list in, list
out). load_case_id optional: omitted/None uses a default case ("LC1"),
auto-created on first use. direction: "down" (gravity: −Y plane / −Z
plate), "up", "perp" (⊥ to the bar, local) and "axial" — the last two
bars only, plane domain (create_bar_distributed_load only takes
"down"/"up"). On a geometry line id (GeoSegment/GeoMultisegment/
GeoArc), create_bar_uniform_load forwards to add_line_distributed_load
instead ("down"/"up" only — no local axis before meshing).
create_area_uniform_load is the area/region counterpart of the three bar
shortcuts above — the same "one value, spread uniformly" idea, applied to a
geometry object (every triangle/quad it meshes into) or a single tri/quad
element instead of a bar. Plate domain only; targets accepts an id,
object, or list.
It carries "uniform" in its name even though the physical quantity is a
pressure [kN/m²], not a line load [kN/m] — that's a naming choice, not a
unit mismatch. The reason: api_reference_tool's name lookup matches on
substrings of the method name, not on the docstring. An assistant whose
add_uniform_load call failed (wrong target — that's the bar method) and
who then searches for "uniform" needs the area facade to turn up by that
same word, not only by "area" or "pressure".
Bar and area loads — add_* (fundamental)¶
add_distributed_load(element_id, load_case_id, fxe=0.0, fxd=0.0, fye=0.0,
fyd=0.0, coord_sys="global", fze=None, fzd=None)
add_uniform_load(element_id, load_case_id, fx=0.0, fy=0.0, coord_sys="global", fz=None)
add_element_point_load(element_id, load_case_id, a, fx=0.0, fy=0.0, mz=0.0,
coord_sys="global", fz=None)
add_area_load(target_id, load_case_id, pz=0.0) # slab pressure (plate); create_area_uniform_load forwards to this
add_line_distributed_load(object_id, load_case_id, fx=0.0, fy=0.0, fx_field="",
fy_field="", coord_sys="global")
Signed components (fx/fy/...) in a chosen frame (coord_sys), not a
named direction — what every create_bar_* call above forwards to. Reach
for these directly for a load create_bar_* cannot express (a trapezoidal
axial load, a local-frame point load with both fx and fy).
Node and edge loads — create_* (convenience)¶
create_node_load(nodes, load_case_id=None, p=None, direction="down") -> PointLoad
create_edge_load(object_id, edge, load_case_id=None, p=None, direction="perp")
(create_area_uniform_load — a slab's uniform pressure over a whole region —
moved to "Bar and area loads" above, next to add_area_load; it is not a
node/edge call.)
create_node_load direction: "down"/"up" (both domains — plane: global
∓Y; plate: transverse, ∓Z). "left"/"right" (global ∓X) are plane
domain only. create_edge_load's edge: 'all' | 'bottom'/'right'/
'top'/'left' | an integer index — same vocabulary as support_edge.
direction: "perp" (outward normal) or "tang" (along the edge, sign
follows the edge's own winding, not a global direction — for a rectangle,
'top' is −X, not +X; use create_node_load on the corner node for an
unambiguous global push instead of guessing "tang"'s sign).
Node and edge loads — add_* (fundamental)¶
add_point_load(node_id, load_case_id, fx=0.0, fy=0.0, mz=0.0,
fz=None, mx=None, my=None) -> PointLoad
add_tri_edge_load(id, tri_id, node_a, node_b, load_case_id, fx=0.0, fy=0.0,
coord_sys="global", pn=0.0, pt=0.0)
add_surface_edge_load(id, object_id, node_a, node_b, load_case_id, fx=0.0,
fy=0.0, coord_sys="global", pn=0.0, pt=0.0)
add_point_load is the only way to put a moment (mz/mx/my) or an
in-plane force on a plate node — what neither create_node_load nor
create_area_uniform_load can express. add_tri_edge_load addresses an
already-meshed triangle's edge (post-solve ids); create_edge_load above
works pre-mesh, on the geometry object itself.
Self-weight, temperature and settlement — create_* (convenience)¶
create_self_weight(factor=1.0, load_case=None) -> LoadCase # sets/creates self_weight_factor
create_temperature(target, uniform=0.0, gradient=0.0, load_case=None)
create_support_settlement(nodes, load_case_id=None, ux=0.0, uy=0.0, tz=0.0,
w=None, tx=None, ty=None)
create_temperature dispatches by target type: bar → add_temperature_load,
triangle → add_tri_temperature_load, quad → add_quad_temperature_load,
geo line object → add_line_temperature_load, geo area object →
add_area_temperature_load. It only takes a single uniform/gradient
value, never a field: add_area_temperature_load/add_line_temperature_load
below are the ones that accept field_name/grad_field_name for a ΔT that
varies spatially (sampled per generated mesh node/bar); use them directly
for that case, the same way create_bar_distributed_load above is the one
to reach for when create_bar_uniform_load's "down"/"up"-only
direction is not enough. create_support_settlement's nodes: a single
id/Node, or a list of either; w/tx/ty are the plate-domain aliases of
ux/uy/tz.
Self-weight, temperature and settlement — add_* (fundamental)¶
add_temperature_load(bar_id, load_case_id, dt_uniform=0.0,
dt_gradient=0.0, alpha=None) # bars — alpha: accepted, no effect (see below)
add_tri_temperature_load(tri_id, load_case_id, dt_i=0.0, dt_j=0.0, dt_k=0.0,
dt_gradient=0.0) # triangles
add_quad_temperature_load(quad_id, load_case_id, dt_i=0.0, dt_j=0.0, dt_k=0.0,
dt_l=0.0, dt_gradient=0.0) # quads
add_area_temperature_load(object_id, load_case_id, dt_uniform=0.0,
dt_gradient=0.0, field_name="", grad_field_name="")
add_line_temperature_load(object_id, load_case_id, dt_uniform=0.0,
dt_gradient=0.0, field_name="", grad_field_name="")
add_support_settlement(node_id, load_case_id, ux=0.0, uy=0.0, tz=0.0,
w=None, tx=None, ty=None)
bar_id/dt_uniform/dt_gradient are the canonical names for
add_temperature_load now — matching tri_id/quad_id and
dt_uniform/dt_gradient elsewhere in the temperature-load family, instead
of the bar's old generic element_id and delta_t_uniform/
delta_t_gradient. The old element_id=/delta_t_uniform=/
delta_t_gradient= keywords still work (same pattern as tri_section_name=
above); the stored TemperatureLoad attributes and the .x2d field keep
the names element_id/delta_t_uniform/delta_t_gradient.
alpha is a different thing from a deprecated alias — it is accepted but
has no effect at all. Unlike the keywords above (each still sets the value
it names, just under an old spelling), passing alpha to
add_temperature_load changes nothing: the thermal expansion coefficient
always comes from the element's material (Material.alpha) at analysis
time. The parameter exists only so old call sites that already pass alpha
keep working: TemperatureLoad has nowhere to store it.
Analysis, combinations, dynamics¶
AnalysisCase controls how a model is solved (linear, modal, spectrum, …);
see "Load cases", in "### Loads" above, for LoadCase — what loads exist
and which case id they belong to, a different concept that only shares the
word "case". create_* (convenience) directly above the add_*
(fundamental) it forwards to — see the convention note above.
Analysis cases — create_* (convenience)¶
create_analysis_case(id=None, analysis_type="Linear", coefficients=None, **kwargs) -> AnalysisCase
id optional — auto-assigned ("AC1", "AC2", …) when omitted.
create_load_case's create_analysis_case=True (see "### Loads" above)
already creates one of these with a matching id for the common case; call
this directly only for a case that needs a different id or a non-default
analysis_type.
Analysis cases — add_* (fundamental)¶
add_analysis_case(id, analysis_type="Linear", coefficients=None, **kwargs)
# analysis_type: Linear | NonLinear | Mass | Modal | Spectrum | GeometricNonlinear
id required — what create_analysis_case above forwards to.
Combinations and dynamics¶
add_load_combination(id, coefficients, combo_type="LinearSum") -> LoadCombination
add_nodal_mass(node_id, mass_case_id, mx=0.0, my=0.0, mtz=0.0) -> NodalMass
add_spectral_function(id, description="", damping=0.05, points=None)
No create_* layer for these three — no auto-id convenience has been
needed for them yet.
Constraints and fields¶
add_rigid_link(master, slaves, id="") -> Constraint # slaves rigidly follow master
add_equal_dof(nodes, components, id="") -> Constraint # ties listed DOF components equal across nodes
add_constraint_equation(terms, value=0.0, id="") -> Constraint # general linear MPC: sum(terms) = value
add_field(name, expression="0.0") # f(x, y) sampled at nodes
Companion remove_* and rename_* methods exist for nodes, elements,
sections, materials, load cases, combinations, analysis cases and geometry
objects — plus loads (remove_point_loads, remove_distributed_loads,
remove_area_load, the remove_*_temperature_load family) and springs
(remove_node_spring, remove_element_spring, remove_area_spring,
remove_line_element_spring). The load/temperature removes take a
load_case_id="" that clears every case on the target, or one named case.
Removal facade — delete_* / free_edge¶
The intent-level inverses of the create_*/pin/support layer — the removal
twin of create_*, and what to reach for when undoing an edit:
delete_support(nodes) # unassign the support on a node / list of nodes
free_edge(object_id, edge) # free an edge (inverse of support_edge/pin_edge)
delete_area(object_id) # delete a region/line object (= remove_geo_object)
delete_load(target, load_case_id="") # node / bar / area loads on a target
delete_temperature(target, load_case_id="") # bar / triangle / quad / object
delete_spring(target) # node / element / area / line spring
Each is a forgiving wrapper over the low-level remove_*: removing what is not
there is a no-op, so one delete_load('N1') clears whatever loads N1 carries
without the caller first classifying the target. The low-level remove_* are
the by-id/by-name primitives (used by the GUI and scripts); the delete_*/
free_edge facade is the one the assistant browses and uses.
Advanced entry points (variants, construction sequences, support sets,
with_active, derive_variant, stiffness_hash) support the staging and
study features and are documented with those.
Section cuts¶
add_cut(id, x1, y1, x2, y2, name="") -> Cut
remove_cut(cut_id)
rename_cut(old_id, new_id)
A Cut is a straight segment (x1,y1) -> (x2,y2) that reports the
resultant of the internal forces crossing it — bars and areas, combined and
separately — for one case (load case, analysis case, or LinearSum
combination) at a time. Evaluation lives in the Qt-free xdfem2d.cuts
module, not on Structure2D itself:
cut_result(struc, results, cut, case, ref_point=None) -> dict
# {'domain', 'bars': [...], 'areas': [...],
# 'resultant': {'bars', 'areas', 'total'}, 'reason'}
# bars/areas per-crossing detail; resultant['total'] is what a Results
# panel shows. ref_point defaults to the cut's own midpoint.
cut_distribution(cutres, n=101) -> dict
# {'s', 'fields', 'markers'} — sampled per-length area fields (piecewise
# constant per triangle) plus bar crossings as point markers, for
# plotting the distribution along the cut.
cuts_report(struc, results, cases, ref_point=None) -> list[dict]
# one row per (cut, case): cut_id, cut_name, case, total_<N|V|M or
# V|Mb|Mt>, reason — every cut against every case id, for CSV export.
cut_bar_resultant(struc, results, cut, case, ref_point=None) -> dict
cut_area_resultant(struc, results, cut, case, ref_point=None) -> dict
# the two halves cut_result sums; called directly only if you want one
# side in isolation.
find_bar_crossings(struc, cut) -> list[dict] # {elem_id, a, s, x, y}
find_triangle_crossings(struc, cut) -> list[dict] # {tri_id, s0, s1, p0, p1}
# pure geometry — every bar/triangle the cut crosses, no results needed.
Resultant keys are N, V, M for a plane cut (axial along the cut's normal,
shear along its tangent, in-plane moment about ref_point) or V, Mb, Mt
for a plate cut (transverse shear, bending and twisting moment — the same
mn/mnt split slab results already use). struc must be the compiled
model (geo_expand.expand_geometry's output) when the model has geometry
objects, matching whatever handle the GUI already keeps for tri-field
plotting — cuts are evaluated against the solved mesh, not the editable
objects. See dev/CUT_PLAN.md for the sign conventions, the tolerance
policy at element edges/nodes, and the phase-by-phase implementation notes.
Simplified API (the "simple" facade)¶
Thin, lossless shortcuts over the generic methods for the common cases: a magnitude + a named direction instead of signed components and a reference frame, and a support created and assigned in one call. They read the model's domain and adapt (plane vs plate), and raise a clear error when a direction or support does not apply. Nothing new is stored — each forwards to the generic method, so a model built this way is identical to the long form.
Not a separate block of its own: every create_* shortcut is documented
next to the add_* call it forwards to, under that call's own domain
heading above — loads, supports, materials/sections/nodes/geometry, and
load/analysis cases each pair create_* (convenience) with add_*
(fundamental) in one place, instead of repeating either here.
Compatibility appendix¶
Every name below still works exactly as documented historically — nothing
was removed or renamed away, only given a canonical counterpart. The JSON
file format does not change: whichever name builds the model, save()
writes the same thing.
| Still works (unchanged) | Canonical name | Notes |
|---|---|---|
add_rc_section |
create_rc_section |
same signature, pure rename |
add_steel_section |
create_steel_section |
same signature, pure rename |
add_panel |
create_area |
same signature, pure rename |
add_geo_line |
add_geo_segment |
pure rename, both still work |
add_geo_polyline |
add_geo_multisegment |
pure rename, both still work |
add_geo_surface |
add_geo_polygon |
pure rename, both still work |
No name in this table is deprecated or warns — every one is a permanent
alias. add_concrete_material, add_material, add_section,
add_tri_section, add_quad_section, add_plate_section, add_node,
add_bar_element, add_tri_element, add_quad_element,
pin/fix/roller/symm, add_support_settlement, and every generic
add_* load/support/analysis call keep their original names unchanged —
they are the fundamental layer the create_* convenience methods forward
to, not aliases themselves. add_area_section/add_area_element are a
newer addition to that same fundamental layer, not a replacement for
add_tri_*/add_quad_*: they pair/dispatch to them for a caller that does
not know tri vs quad up front, while every existing tri/quad call site
keeps calling the single-shape methods directly. See
dev/XDFEM2D_ENGINE.md for the full design rationale and phased
implementation history.
Exception — the pre-existing simple-facade load calls do not follow this
rule. add_bar_load, add_bar_point_load, add_node_load,
add_pressure, add_self_weight and add_temperature are gone, with no
alias: renamed outright to create_bar_uniform_load,
create_bar_point_load, create_node_load, create_area_uniform_load,
create_self_weight and create_temperature. Unlike everything else in
this document, this is a real, unqualified rename — the project had not
shipped when it was made, so there was no installed base calling the old
names to protect.
create_area_uniform_load itself went through two further renames after
that, both for the same reason (findability by an assistant searching
api_reference_tool, whose name argument matches a substring of the
method name, not the docstring):
- From
create_pressure— "pressure" had no lexical link back to a bar's distributed/uniform load, so an assistant asked for a distributed load on a slab region had no way to find it by name. - From
create_area_load— the resulting name still had no lexical link to "uniform", so an assistant whoseadd_uniform_loadcall failed (the fundamental bar method — wrong for an area target) and who searched for "uniform" next still found no area-load facade.
See dev/XDFEM2D_ENGINE.md for the full reasoning.
Member buckling (steel EC3 / timber EC5)¶
The steel and timber member checks work on physical members — chains of
collinear bars grouped by xdfem2d.steel_members.identify_members(struc). Each
BarElement carries the per-member buckling-length factors used by the design
(buckling length = K × physical-member length); leave them None to inherit the
global default from the design preferences:
BarElement.sd_ky # K for flexural buckling about y (major / in-plane)
BarElement.sd_kz # K for flexural buckling about z (minor / out-of-plane)
BarElement.sd_klt # K for the lateral-torsional length
BarElement.sd_ltb # bool: member susceptible to lateral-torsional buckling
Free-end (cantilever) detection helpers in xdfem2d.steel_members:
member_free_ends(struc, member) -> (free_i, free_j) # a truly-free end node
member_has_free_end(struc, member) -> bool
cantilever_k_for(struc, member, prefs) -> float | None # auto K (Ky, Kz) or None
A node is a free end when nothing is attached to it (a single incident bar and no
support, node spring, triangle or constraint). When enabled (cantilever_auto,
default on), such members take cantilever_k (default 2.0) for sd_ky/sd_kz
unless an explicit value is set.
Design reports (calculation trace)¶
Every design routine can, with with_reports=True, attach a full step-by-step
calculation report for the governing case of each member / section / column /
triangle. The report content — clause, symbolic expression, LaTeX and numeric
substitution — is produced by eurocodepy through an optional trace
(eurocodepy.calc_report.CalcReport):
the check computes exactly as before and the trace only records what was
computed, so the reported numbers are, by construction, the ones used. The app
only gathers and renders; it never re-derives formulas.
The EC2 procedures are owned by eurocodepy composites, each taking an input
dataclass and an optional trace:
eurocode2_shear_check(ShearInput, v_ed, trace=None) # §6.2 shear
eurocode2_punching_check(PunchInput, n_ed, …, trace=None) # §6.4 / :2023 §8.4
eurocode2_membrane_check(MembraneInput, trace=None) # §6.109 membrane (areas)
eurocode2_slab_check(SlabInput, trace=None) # §6.1 slab flexure (areas)
calc_torsion(…, trace=None) # §6.3 grillage torsion
calc_asl_nm(…, trace=None) # §6.1 M-N flexure
Gathering and rendering live in xdfem2d.design_report (Qt-free):
build_reports(struc, results, material, prefs=None, combinations=None) -> {id: report}
# material: "steel" | "timber" | "concrete" | "concrete_area" | "punching"
filter_reports(reports, material, struc,
selected_bars=None, selected_nodes=None, selected_tris=None)
reports_from(design_output) -> {id: report} # normalise a routine's output
report_base_id(report) -> str # member/element/column/triangle id
per_combination_key(base_id, combo) / merge_per_combination({combo: {id: report}})
render_html(reports, title) / render_markdown(...) / render_pdf(...) / render_docx(...)
write_markdown_bundle(reports, md_path, title) # .md + <stem>_assets/ figures
section_figure_png(geom) # cross-section drawing (rect / I / RHS / SHS / CHS)
pandoc_available() # gates the native editable-OMML Word route
EXPECTED_SCHEMA / schema_mismatches(reports) # report-schema version guard
A report dict has the shape {schema_version, title, meta, sections:[{title,
steps:[{symbol, value, unit, clause, expr, latex, subst, note, ok}]}]}. The GUI
consumes these through the Design → Design report… side panel.
JSON data structure¶
save() writes a single JSON object; load() reads it. Every list mirrors the
matching add_* method — a list entry carries the same fields as that call's
arguments. Absent keys take their defaults, so a file stays readable as the
format grows (e.g. domain absent ⇒ plane).
Top level¶
{
"domain": "plane", // "plane" | "plate"
"project_info": { "Project name": "...", "Designer": "...", ... },
"propagate_edge_supports": true,
"propagate_edge_springs": true,
"nodes": [ {"id","x","y"} ],
"materials": [ {"name","elastic_modulus","unit_weight","alpha",
"unit_mass","material_type","poisson","design"} ],
"sections": [ {"name","material_name","b","h","area_override",
"inertia_override","torsion_override","profile_name",
"shape","tw","tf","rc_cover","rc_alpha_s","rc_bar_phi",
"rc_shear_min"} ],
"tri_sections": [ {"name","material_name","thickness","plane_strain",
"formulation","rc_cover","rc_alpha_s","rc_bar_phi",
"rc_shear_min"} ],
"quad_sections": [ {"name","material_name","thickness","plane_strain",
"formulation", ...} ], // same fields as tri_sections
"bar_elements": [ {"id","node_i","node_j","section_name","hinge_i",
"hinge_j","rc_design","rc_cover","rc_cotg_theta",
"rc_alpha_s"} ],
"tri_elements": [ {"id","node_i","node_j","node_k","section_name"} ],
"quad_elements": [ {"id","node_i","node_j","node_k","node_l","section_name"} ],
// quad loads mirror the triangle ones, keyed by "quad_id":
// quad_area_loads, quad_edge_loads, quad_temperature_loads, quad_area_springs
"geometry_objects": [ {"kind","id","node_ids", ...} ],
"supports": [ {"name","ux","uy","tz"} ],
"support_assignments": [ {"node_id","support_name"} ],
"node_springs": [ {"node_id","kx","ky","kt","mode_x","mode_y"} ],
"element_springs": [ {"element_id","kx","ky","coord_sys","mode_x","mode_y"} ],
"constraints": [ {"id","kind","terms","value","master","slaves",...} ],
"load_cases": [ {"id","self_weight_factor","action_type"} ],
"point_loads": [ {"node_id","load_case_id","fx","fy","mz"} ],
"distributed_loads":[ {"element_id","load_case_id","fxe","fxd","fye","fyd",
"coord_sys"} ],
"element_point_loads": [ {"element_id","load_case_id","a","fx","fy","mz",
"coord_sys"} ],
"tri_edge_loads": [ {"id","tri_id","node_a","node_b","load_case_id","fx",
"fy","coord_sys","pn","pt"} ],
"surface_edge_loads": [ {"id","object_id","node_a","node_b","load_case_id",
"fx","fy","coord_sys","pn","pt"} ],
"tri_area_loads": [ {"tri_id","load_case_id","pz"} ],
"surface_area_loads": [ {"object_id","load_case_id","pz"} ],
"tri_area_springs": [ {"tri_id","kz"} ],
"surface_area_springs": [ {"object_id","kz"} ],
"support_settlements": [ {"node_id","load_case_id","ux","uy","tz"} ],
"temperature_loads": [ {"element_id","load_case_id","delta_t_uniform",
"delta_t_gradient"} ],
"tri_temperature_loads": [ {"tri_id","load_case_id","dt_i","dt_j","dt_k",
"dt_gradient"} ],
"quad_temperature_loads": [ {"quad_id","load_case_id","dt_i","dt_j","dt_k",
"dt_l","dt_gradient"} ],
"area_temperature_loads": [ {"object_id","load_case_id","dt_uniform",
"field_name","dt_gradient","grad_field_name"} ],
"line_temperature_loads": [ {"object_id","load_case_id","dt_uniform",
"dt_gradient","field_name","grad_field_name"} ],
"line_distributed_loads": [ {"object_id","load_case_id","fx","fy","fx_field",
"fy_field","coord_sys"} ],
"line_element_springs": [ {"object_id","kx","ky","kx_field","ky_field",
"coord_sys","mode_x","mode_y"} ],
"cuts": [ {"id","name","x1","y1","x2","y2"} ],
"load_combinations": [ {"id","combo_type","coefficients",
"analysis_coefficients"} ],
"analysis_cases": [ {"id","analysis_type","coefficients","num_modes",
"modal_case_id","spectrum_id","combination_rule",
"direction","damping", ...} ],
"nodal_masses": [ {"node_id","mass_case_id","mx","my","mtz"} ],
"spectral_functions":[ {"id","description","damping","points"} ],
"fields": [ {"name","expression"} ]
}
Notes on the slots¶
- The three per-node components are positional: they are written with the
canonical (plane) names
ux/uy/tz,fx/fy/mz,kx/ky/ktin both domains. In a plate model read them asw/θx/θy,fz/mx/my,kz/kθx/kθy. Readers also accept the plate aliases on input. geometry_objects[].kindis one ofsegment,arc,multisegment,rectangle,polygon. The old spellingsline,polyline,surfaceare still read. Line-like objects carrysection_name,divisions,max_chord; area-like objects carrytri_section_name,target_size,edge_support_mode,edge_spring_mode.tri_sections[].formulationisCST,AllmanorES-FEM(panel / membrane) in the plane domain, or a plate-bending element in the plate domain:MITC3(shear-deformable, the default — valid for thin and thick slabs) orDKT(thin-plate / Kirchhoff).
See also¶
dev/XDFEM2D_ENGINE.md— full design rationale and phased implementation history behind thecreate_*/add_*split and the compatibility appendix (see "Compatibility appendix" above).dev/CUT_PLAN.md— section-cut sign conventions, the tolerance policy at element edges/nodes, and phase-by-phase implementation notes (see "Section cuts" above).eurocodepy.calc_report.CalcReport— the calculation-trace type consumed bywith_reports=True(see "Design reports" above).