Build a model from a node/member graph
build_modelBuild a complete, solver-ready FERS model from an arbitrary node/member graph: 3-D frames, trusses, pitched roofs, bracing, towers — anything create_beam/create_frame cannot express. Returns a full FERS JSON document ready for solve_model.
GEOMETRY (1-BASED). nodes is a JSON array of [X, Y, Z] triples in METRES, Y up (gravity is −Y). The index you use to refer to a node IS its solver node id: nodes[0] is node 1, so results (displacement_nodes, reaction_nodes) come back under exactly the numbers you wrote. Index 0 is rejected. members is a JSON array of [startNode, endNode] / [startNode, endNode, "SectionName"] / [startNode, endNode, "SectionName", {options}]; member ids are 1..N in the order given. Member options: rotation_angle (DEGREES, rolls the section about the member axis), type ("Truss" for a pin-jointed axial-only bar; also Tension/Compression/Cable/Rigid), hinge ("start"|"end"|"both" — releases both bending moments there; at a joint only ONE of the members may be released, because a node whose every member end is released has no rotational stiffness left and the solve then fails outright), classification (free-text group name; one member_set per distinct value, and unity checks can target it).
COLUMN ORIENTATION — READ THIS. FERS's strong bending axis is the LOCAL z axis (i_z > i_y for an I-section), and the member local frame is built from a global-Y reference vector. A HORIZONTAL member therefore bends strong-axis under gravity automatically, but a VERTICAL member gets local y = global Z and local z = global X, so it resists sway along global X with its WEAK axis i_y. Verified: a 6 m cantilever column pushed 20 kN in +X deflects 1.1357 m at rotation_angle 0 (= PL³/3E·i_y) and 0.0820 m at rotation_angle 90 (= PL³/3E·i_z) — a 13.8× difference the solver will never warn about. In plane:"XY" this tool sets rotation_angle 90 on vertical bending members for you (reported in info.auto_rotation), because sway there is along X; in "3D" it only warns, since which axis you want depends on the structure — set rotation_angle explicitly.
SUPPORTS. supports maps a 1-based node number to a preset or an explicit DOF object: "fixed" (all 6 DOFs), "pinned" (X,Y,Z fixed, rotations free), "roller_x"/"roller_y"/"roller_z" (free to slide along that global axis), "free". For anything else pass {"X":"fixed","Y":"fixed","Z":"free","RX":"free","RY":"free","RZ":2.5e6} — a NUMBER is a spring stiffness (N/m for X/Y/Z, N·m/rad for RX/RY/RZ), and that node then settles by exactly force/stiffness. Watch the sign when you read results: a SPRING reaction comes back with the OPPOSITE sign to a rigid support carrying the same force (measured: +30 kN at the pin and −30 kN at the spring of one beam), so do not add the two up. Any DOF you leave out of an explicit object is written as Free (never omitted: in the raw JSON an absent DOF means FIXED, which is the opposite of what everyone expects).
PLANE. plane is "3D" (default) or "XY". "XY" requires every node to have Z = 0 and pins Z, RX and RY at EVERY node, which is what makes a planar model well-posed — essential for a planar TRUSS, where the bars carry exactly zero out-of-plane stiffness. Note that analysis.options.dimensionality is cosmetic: the solver never reads it, so only supports can make a model planar. An under-restrained model whose loads reach the free direction is reported, not solved: that load case's errors_and_warnings carries singular_model and its converged flag is false. Read errors_and_warnings before using any number.
SECTIONS. Give a library name ("IPE300", "HEA200", "CHS 114.3x5", "RHS 100x50x5", "UB 457x191x67", …) in the member row and it is looked up and inserted with all EC3 moduli. For a rectangle or your own numbers, declare it in sections (same object shape as create_section) and reference it by name. A member naming no section gets the first declared section, else default_section (IPE300).
LOADS are in kN, kN/m and kNm and are converted to newtons internally. loads is a list of load cases: {"name":"G","udl":[[member, w]],"nodal_loads":[[node, Fx, Fy, Fz]],"nodal_moments":[[node, Mx, My, Mz]],"point_loads":[[member, position0to1, Fx, Fy, Fz]]}. A 2-entry udl row [member, w] is w kN/m acting DOWN (−Y); a 4-entry row [member, wx, wy, wz] is a full vector. Combine them with load_combinations: [{"name":"ULS","factors":{"G":1.35,"W":1.5}}], referencing load cases BY NAME; omit it for two automatic ones over every case: combination 1 "SLS characteristic (auto)" at factor 1.0 and combination 2 "ULS (auto)" at 1.35. A combination you name gets limit_state "ULS" unless you give one, because a null limit_state makes every later unity check silently evaluate nothing and report a pass.
LIMITS: 100 members on the Free tier (hard "LimitExceeded" above that), so keep element counts modest.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| loads | No | JSON array of load cases in kN / kN·m: [{"name":"G","udl":[[member, w]],"nodal_loads":[[node,Fx,Fy,Fz]],"nodal_moments":[[node,Mx,My,Mz]],"point_loads":[[member,pos0to1,Fx,Fy,Fz]]}]. A 2-entry udl row is kN/m acting DOWN. | |
| nodes | Yes | JSON array of [X, Y, Z] node coordinates in metres, Y up. The 1-based array position IS the node id: the first triple is node 1, so results come back under the numbers you wrote. Minimum 2 nodes. | |
| order | No | "LINEAR" (default) or "NONLINEAR" for second-order. | |
| plane | No | "3D" (default) or "XY". "XY" declares a planar model (every node at Z=0) and pins Z/RX/RY at EVERY node, which is what makes a planar model well-posed — an under-restrained one comes back as singular_model (converged:false), with no usable numbers. | |
| members | Yes | JSON array of member rows: [start, end] / [start, end, "IPE300"] / [start, end, "IPE300", {options}]. Node numbers are 1-based; member ids are 1..N in this order. Options: rotation_angle (DEGREES — use 90 on the columns of an XY-plane frame or they resist sway with their WEAK axis), type (Truss/Tension/Compression/Cable/Rigid), hinge (start|end|both), classification. | |
| material | No | steel_S355 (default), steel_S235, aluminium_6061 or concrete_C30. | |
| sections | No | JSON array of section definitions (same shape as create_section), referenced by name from the member rows. | |
| supports | No | JSON object mapping a 1-based node number to "fixed" / "pinned" / "roller_x" / "roller_y" / "roller_z" / "free", or to an explicit DOF object {"X":"fixed","Y":"fixed","Z":"free","RX":"free","RY":"free","RZ":1.0e7} where a NUMBER means a spring stiffness. Required — a model with no supports is rejected. | |
| self_weight | No | Add self-weight as its own load case and factor it into the combinations. | |
| project_name | No | Label for the model. | |
| default_section | No | Section for members that name none. Default IPE300. | |
| load_combinations | No | JSON array referencing load cases by NAME: [{"name":"ULS","factors":{"G":1.35,"W":1.5},"limit_state":"ULS"}]. Omit for two automatic ones over every case: 1 "SLS characteristic (auto)" at 1.0 and 2 "ULS (auto)" at 1.35. |