Skip to content

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

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 — a create_* shortcut (a magnitude/name, sane defaults, get-or-create where that makes sense; prefer this) or the generic add_* 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 the create_* layer directly above the add_* 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 (a GeoRectangle, 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):

  1. 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.
  2. From create_area_load — the resulting name still had no lexical link to "uniform", so an assistant whose add_uniform_load call 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/kt in both domains. In a plate model read them as w/θx/θy, fz/mx/my, kz/kθx/kθy. Readers also accept the plate aliases on input.
  • geometry_objects[].kind is one of segment, arc, multisegment, rectangle, polygon. The old spellings line, polyline, surface are still read. Line-like objects carry section_name, divisions, max_chord; area-like objects carry tri_section_name, target_size, edge_support_mode, edge_spring_mode.
  • tri_sections[].formulation is CST, Allman or ES-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) or DKT (thin-plate / Kirchhoff).

See also

  • dev/XDFEM2D_ENGINE.md — full design rationale and phased implementation history behind the create_*/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 by with_reports=True (see "Design reports" above).