FERS — structural FEM

com.ferscloudPublished by ferscloud.comOfficial publisherv2.83.0Updated Nov 7, 2026

Structural FEM for AI agents: frames, plates, buckling, seismic and Eurocode checks, solved locally.

VerifiedStreamable HTTPWeb executableFiles & StorageDeveloper ToolsData & Analytics

Overview

AI-generated overview

FERS is a finite-element structural-engineering solver that lets an assistant build, validate and solve beam, frame, truss and plate models with Eurocode…

What you can do
  • Build and edit models: beams, portal frames, arbitrary node/member graphs, sections, supports, member releases and scissor hinges, plate surfaces and openings
  • Define loading and analysis: load cases, loads, factored combinations, EN 1990 combination sets, imperfections, second-order, modal, buckling and seismic options
  • Solve and check: run the solver for displacements, forces and reactions, run unity checks, or check a steel beam to EN 1993-1-1 in one call
  • Inspect results: summarize plate results, render an HTML hand-calc report, and read usage/tier
  • Look up references: section catalogues, material presets, model and check schemas, and template models
  • Manage models: stash a model behind a…
When to use it
Use it when an assistant needs real structural numbers rather than estimates: beam and column checks, frame or slab analysis, deflections, buckling factors, natural frequencies, seismic response, or Eurocode unity checks with a traceable calculation.
Requirements
Runs as a remote endpoint or as a local npm package started with npx. OAuth authentication is required for the remote endpoint; the optional FERS_API_KEY environment variable unlocks Pro limits. No key means Free tier: non-commercial use only, up to 100 members. A key makes each solve verify the licence with ferscloud.com.
Before you install
The Free tier is non-commercial only and capped at 100 members, so business or client work needs a Pro key. With a key, each solve contacts ferscloud.com for a licence handshake. Authoring tools mutate and return model documents, and save_model writes to the local FERS model folder. Results are engineering output and should be reviewed by a qualified engineer.

Installation

In SourceWeft

  1. Open FERS — structural FEM in the dashboard and add it to a workspace.
  2. Connect the required credentials. They are stored privately and never shown on public pages.
  3. Enable the server for the chats that should use its tools.

Web executable via Streamable HTTP. Remote servers run from the web runtime once configured in a workspace.

Use with an agent

Paste this prompt into Claude Code, Codex, Cursor or another agent — or into SourceWeft chat. The agent reads this server's install guide, shows you its publisher, endpoint and sign-in, and adds it to your client once you agree. Keys never go into a command.

Read https://sourceweft.com/mcp/com.ferscloud%2Ffers/install.md and connect the MCP server it describes to my client. Before changing any config, show me its publisher, endpoint and sign-in method and wait for my OK. Never put an API key in a command or in a file I would commit; ask me where it should come from.

Read the install guide the agent follows

Other MCP clients

Add this to your client's mcpServers config. The client completes OAuth sign-in on first connect.

{
  "mcpServers": {
    "fers-structural-fem": {
      "type": "http",
      "url": "https://ferscloud.com/api/mcp"
    }
  }
}

README

FERS: structural analysis in Python

FERS: structural finite-element analysis (3D frames, trusses, plates, Eurocode 3 checks) for engineers and AI agents. This is its Python SDK and the fers command. The solver is a compiled engine that runs on your machine, with no account and no network call: free for non-commercial use up to 100 members. This SDK is open source (BSD-3-Clause) at github.com/Jeroen124/FERS_core; the engine's licence is under Licence.

It computes linear and second-order (P-Delta) static analysis, natural frequencies, elastic buckling load factors (α_cr), response-spectrum seismic analysis, and EN 1993-1-1 steel member checks, flexural and lateral-torsional buckling included. A model is JSON in and JSON out, written from Python, by the fers command, or by an AI agent through the FERS MCP server, the Claude Code plugin or the agent skill.

bash
pip install FERS
python
from fers_core import check_beam
# IPE200 in S235, simply supported over 6 m, 10 kN/m. SI units: m, N, Pa.# check_beam adds an EN 1993-1-1 member check on a ULS combination of 1.35 x the load.model = check_beam(6.0, "IPE200", udl=10_000.0)model.settings.analysis_options.include_member_deflected_shape = Truemodel.run_analysis()
beam = model.resultsbundle.loadcases["Q"].member_results["1"]x_frac, (_, dy, _) = beam.member_displacement_peakec3 = model.unity_check_results()[0]print(f"M {beam.local_minimums.mz / 1e3:.1f} kNm, sag {dy * 1e3:.1f} mm at x/L {x_frac:.2f}")print(f"EC3 utilization {ec3['max_utilization']:.2f}")
text
M -45.0 kNm, sag -41.8 mm at x/L 0.50EC3 utilization 2.85

The sag is 41.3 mm of bending (5wL⁴/384EI) plus 0.4 mm of shear deformation, which the engine includes. The beam fails: the ULS moment of 60.75 kNm exceeds its bending resistance of 51.9 kNm, and with nothing restraining its compression flange, lateral-torsional buckling takes the utilization to 2.85.

  • Agents and scripts: fers solve, fers check and fers validate take a model JSON file, print one line (or --json) and say in their exit code whether the numbers can be used; see From the terminal and from coding agents. fers skill --install . gives a coding agent the FERS skill, and MCP clients can use the FERS MCP server. In Claude Code, both come as one plugin: /plugin install fers --marketplace Jeroen124/FERS_core.
  • Extras: plots (pip install "FERS[plot]"), section properties meshed from your own dimensions ("FERS[sections]") and DXF import ("FERS[dxf]"); "FERS[all]" installs everything 0.1.x did. Catalogue sections (IPE200, HEB300, UB 457x191x67, W14X30, ...) need no extra.
  • Licence: the SDK is BSD-3-Clause; the engine it installs is free for non-commercial use (Licence).

Solving a model

Building a model by hand, create the FERS object first, then its nodes, members and loads. Creating a FERS restarts the id counters, so a node made before it would share an id with one made after it, and the solver refuses node ids that do not run from 1 without gaps.

Reading results

run_analysis() puts the results on model.resultsbundle. Load cases and load combinations are keyed by name. Within each, nodes and members are keyed by their id as a string, because results round-trip through JSON.

python
case = model.resultsbundle.loadcases["Load"]    # or .loadcombinations["ULS 1"]
case.displacement_nodes["5"].dy                  # dx dy dz rx ry rz warpcase.reaction_nodes["1"].nodal_forces.fy         # fx fy fz mx my mz bwbeam = case.member_results["4"]beam.local_maximums.mz, beam.local_minimums.mz   # envelope along the memberbeam.local_start_forces.fy                       # end forces, member axes

start_node_forces, end_node_forces, maximums and minimums are in the global frame; the local_* forces, section_forces and internal_force_series are in the member's own axes, with mz the strong-axis moment. Read a frame's moments from the local ones: outside the XY plane the global mz is not the bending moment, and in a frame in the YZ plane it reads 0.

Since 0.1.98, member_results and displacement_nodes are read-only tables rather than dicts of objects. That is what keeps a large result small in memory. They read like dicts: [], get, in, len, .items(), and iteration in the solver's order. Each lookup builds a fresh object, and nothing in it can be changed:

python
beam.local_maximums.mz = 0.0                     # AttributeError: read-onlyeditable = case.member_results.copy()            # a plain dict of editable objectseditable["4"].local_maximums.mz = 0.0            # fine

section_forces, internal_force_series and member_displacements come back as tuples. To test for a table, use isinstance(x, collections.abc.Mapping), not dict. reaction_nodes and plate_results are plain dicts, as before.

Saving and loading

python
from fers_core import FERS
model.save_to_json("beam.json")                  # the model and its resultsmodel = FERS.from_json("beam.json")

FERS.from_json reads any file the solver or the SDK wrote, one entry at a time. A typical result needs less memory than its file to load, and at most about twice the file's size; before 0.1.98 it needed 15 to 20 times. For a large model, let the solver write the file itself. The result then never passes through Python as one string:

python
model.run_analysis_to_file("beam_results.json")solved = FERS.from_json("beam_results.json")

fers_core/examples/809_Reading_Results.py runs all of this end to end.

Saving a loaded model writes back everything the solver reads, with the same ids. That includes what the file defines but nothing uses yet, such as a section an editor keeps in its library. Nodes, members and the rest created after loading get ids the loaded model does not use, so saving never lets a new object replace a loaded one.

Only the results you read (engine 0.2.68)

AnalysisOptions(result_filter=[...]) keeps or drops whole result blocks. result_requests goes further. Each ResultRequest names a block and narrows it to chosen members or nodes, load combinations, components and one member end:

python
from fers_core import ResultBlock, ResultRequestfrom fers_core.builders import check_beamfrom fers_core.result_requests import nodes_offrom fers_core.unity_checks import classification
model = check_beam(5.0, "IPE300", udl=10000.0)    # one "Beam", one ULS combinationbeams = classification("Beam")model.settings.analysis_options.result_requests = [    ResultRequest(ResultBlock.LOCAL_ENVELOPES, members=[beams],                  limit_state="ULS", components=["fx", "my", "mz"]),    ResultRequest(ResultBlock.NODE_DISPLACEMENTS, nodes=nodes_of(beams),                  limit_state="ULS", components=["dy"]),    ResultRequest(ResultBlock.REACTIONS, limit_state="ULS"),]model.run_analysis()

The values load into the same member_results, displacement_nodes and reaction_nodes, so code that reads them keeps working. Anything that was not requested reads as None, not zero. A check that reads a value nobody asked for therefore fails instead of passing on a zero.

  • members takes the unity checks' selectors: classification(...), members([...]), member_sets([...]) and all_members(). Several selectors form a union.
  • nodes takes nodes([...]), or nodes_of(*selectors) for the end nodes of the selected members.
  • Load combinations are matched on their limit_state, as unity checks match them, or by load_combination_ids. To select by limit state, set limit_state on each LoadCombination; the solver does not read situation.
  • A request with neither limit_state nor load_combination_ids also covers the load cases.
  • end="start" or end="end" keeps one end of the end-force and end-displacement blocks.
  • The solver refuses a request it cannot meet, before it starts. That includes section_forces, internal_force_series, a component the block does not have, a limit state no combination carries, and result_filter set as well.

The values themselves are on model.resultsbundle.selections, one entry per request and group of result sets. Each entry's values is a read-only numpy array shaped (result_sets, ids, fields, components), with each axis listed beside it:

python
sel = model.resultsbundle.selections[0]sel["group"]                     # "loadcombinations" or "loadcases"sel["result_sets"], sel["ids"]   # combination names, member or node idssel["fields"], sel["components"] # e.g. ["local_maximums", "local_minimums"], ["fx", "my", "mz"]my = sel["values"][..., sel["components"].index("my")]   # every set, member and field

save_to_json writes the selections back as the solver wrote them, so a saved result reloads unchanged. fers_core/examples/810_Result_Requests.py runs this end to end.

Scissor hinges: a continuous beam that turns on what it crosses (engine 0.2.68)

A rail running over beams and resting on each one is continuous along its length, but free to turn on every beam. A MemberHinge releases one member end, so releasing both rail ends at a beam splits the rail into two spans. Put the rail's members in a member set of their own and give the set a ScissorHinge instead. No link member or extra node is needed:

python
from fers_core import FERS, DistributedLoad, Material, Member, MemberSet, NodalSupport, Node, ScissorHinge, Sectionfrom fers_core.supports.supportcondition import SupportCondition as C
model = FERS()steel = Material(name="S355", e_mod=210e9, g_mod=81e9, density=7850.0, yield_stress=355e6)rail_section = Section(name="rail", material=steel, i_y=1e-6, i_z=1e-6, j=1e-7, area=1e-3)beam_section = Section(name="beam", material=steel, i_y=1e-4, i_z=1e-4, j=1e-4, area=5e-3)
turns = {"X": C.fixed(), "Y": C.free(), "Z": C.free()}a, m, b = Node(0.0, 0.0, 0.0), Node(1.0, 0.0, 0.0), Node(2.0, 0.0, 0.0)a.nodal_support = NodalSupport(rotation_conditions=turns)b.nodal_support = NodalSupport(    displacement_conditions={"X": C.free(), "Y": C.fixed(), "Z": C.fixed()}, rotation_conditions=turns)beam_start, beam_end = Node(1.0, 0.0, -0.5), Node(1.0, 0.0, 0.5)beam_start.nodal_support = beam_end.nodal_support = NodalSupport()  # clamped
rail_1 = Member(start_node=a, end_node=m, section=rail_section)rail_2 = Member(start_node=m, end_node=b, section=rail_section)model.add_member_set(MemberSet(    members=[rail_1, rail_2],    scissor_hinge=ScissorHinge(rotational_release_y=0.0, rotational_release_z=0.0),))model.add_member_set(MemberSet(members=[    Member(start_node=beam_start, end_node=m, section=beam_section),    Member(start_node=m, end_node=beam_end, section=beam_section),]))
span = model.create_load_case(name="span")DistributedLoad(member=rail_1, load_case=span, magnitude=1000.0, end_magnitude=1000.0, direction=(0, -1, 0))model.run_analysis()mz = model.resultsbundle.loadcases["span"].member_results[str(rail_1.id)].local_end_forces.mzprint(f"{abs(mz):.1f} Nm")  # 62.4; a knife edge gives qL²/16 = 62.5, a rigid joint 122.6
  • Where it applies: at every node where a member of the set meets a member outside the set, or a plate. There the set's members stay joined to each other and move relative to the node along and about the released axes. Nothing is released where the set meets only itself or a support. So the continuous beam's members belong in a set of their own; a set holding every member meets nothing, and the solver refuses it.
  • Only some of those nodes: MemberSet(scissor_hinge_nodes=[...]) lists where the hinge applies, for a beam that turns on some of the members it meets and is joined to the rest.
  • Each value works as a MemberHinge release does: None holds it, 0.0 frees it, and a positive value is a spring between the set and the node, in force per length for a translation (translational_release_x/_y/_z) and moment per radian for a rotation (rotational_release_x/_y/_z).
  • The axes are global unless the hinge names its own: ScissorHinge(..., axes=ReleaseAxes.user(x=..., y=...)) takes the first axis along x, the second along y made perpendicular to it, and the third as their cross product, as a rack turned in plan needs.
  • Supports and nodal loads at a released node act on the node, meaning the members outside the set. A node's reported displacement is theirs. The set's own motion there is in its members' local_displacement_start_node and local_displacement_end_node.
  • Two hinged sets crossing with nothing else are each released from the node, so every axis either one releases is free between them.
  • A set of one member gives a release about the hinge's axes at that member's ends.
  • Second order: as with member hinges, a model with scissor hinges takes the corotational correction only when NonlinearMethod.COROTATIONAL is named.

A rail that may also slide over its posts frees that translation. Here it slides over the first post only, so the second takes its share of the pull:

python
from fers_core import (    FERS, DistributedLoad, Material, Member, MemberPointLoad, MemberSet, NodalSupport, Node,    ScissorHinge, Section,)
model = FERS()steel = Material(name="S355", e_mod=210e9, g_mod=81e9, density=7850.0, yield_stress=355e6)frame = Section(name="frame", material=steel, i_y=4e-6, i_z=4e-6, j=1e-7, area=2e-3)anchor, top_1, top_2 = Node(0.0, 0.0, 0.0), Node(2.0, 0.0, 0.0), Node(4.0, 0.0, 0.0)foot_1, foot_2 = Node(2.0, -1.5, 0.0), Node(4.0, -1.5, 0.0)anchor.nodal_support = foot_1.nodal_support = foot_2.nodal_support = NodalSupport()
rail_1 = Member(start_node=anchor, end_node=top_1, section=frame)rail_2 = Member(start_node=top_1, end_node=top_2, section=frame)model.add_member_set(MemberSet(    members=[rail_1, rail_2],    scissor_hinge=ScissorHinge(translational_release_x=0.0),  # slides along X    scissor_hinge_nodes=[top_1],                               # over the first post only))for foot, top in ((foot_1, top_1), (foot_2, top_2)):    model.add_member_set(MemberSet(members=[Member(start_node=foot, end_node=top, section=frame)]))
pull = model.create_load_case(name="pull")DistributedLoad(member=rail_1, load_case=pull, magnitude=2000.0, end_magnitude=2000.0, direction=(0, -1, 0))MemberPointLoad(member=rail_2, load_case=pull, position=0.5, magnitude=1000.0, direction=(1, 0, 0))model.run_analysis()reactions = model.resultsbundle.loadcases["pull"].reaction_nodestakes = [abs(reactions[str(foot.id)].nodal_forces.fx) for foot in (foot_1, foot_2)]print(f"post 1 takes {takes[0]:.1f} N, post 2 {takes[1]:.1f} N")  # 0.0 N and 67.2 N

A model that uses scissor hinges is written with schema_version 3, so a solver older than 0.2.68 refuses it instead of solving every connection as rigid. Without them, a model is written exactly as before. fers_core/examples/054_Scissor_Hinge_Rail_On_Beam.py compares the rigid connection, member hinges, the scissor hinge and a spring, and fers_core/examples/055_Scissor_Hinge_Sliding_Rail_And_Release_Axes.py slides the rail, limits the hinge to one post, and turns the rack in plan.

Member hinges in global or user axes (engine 0.2.68)

A MemberHinge releases its rotations about its member's own axes. A beam at an angle in plan, pinned about the girder it hangs from, needs a pin about the girder's axis instead, which in the beam's own axes mixes its bending with its torsion. rotation_axes names the axes the rotational releases, their stiffness curves and their moment–rotation diagrams use:

python
from fers_core import MemberHinge, ReleaseAxes
pin_about_x = MemberHinge(rotational_release_mx=0.0, rotation_axes=ReleaseAxes.GLOBAL)

ReleaseAxes.LOCAL (the default) keeps the member's axes; ReleaseAxes.user(x, y) names a frame as above. A curve on such a hinge reads the end moment about the same axes; the member's results stay in its local axes. Translational releases stay member-local. A model whose member hinges release in other axes is written with schema_version 3 as well. Release axes are fixed in space, so a corotational solve does not turn them with the structure.

A member's self-weight

The solver works out each member's self-weight, density·area·g, and puts it in the load case that self_weight_load_case_id names. A load combination carries the self-weight only if it includes that case, so give it your permanent-load case: create_load_case("G", self_weight=True) creates G and sets both options.

python
from fers_core.builders import create_beam
model = create_beam(6.0, "IPE300", udl=5_000.0, load_case_name="Q")   # 5 kN/m imposed loadimposed = model.load_cases[0]dead = model.create_load_case("G", self_weight=True)                   # G takes the self-weightmodel.create_load_combination("ULS", {dead: 1.35, imposed: 1.5}, limit_state="ULS")model.run_analysis()
uls = model.resultsbundle.loadcombinations["ULS"]print(round(sum(r.nodal_forces.fy for r in uls.reaction_nodes.values()), 1))   # 48357.4

The ULS reactions are 1.5 × 30 kN plus 1.35 × the beam's 2.49 kN. With enable_self_weight set and no self_weight_load_case_id, the solver puts the self-weight in a load case of its own, "Self-weight", which combinations built from your load cases do not include. run_analysis refuses such a model when it has combinations, rather than solve every one of them without self-weight, and the solver warns with self_weight_not_in_combinations on each combination. A model without combinations solves, and its "Self-weight" case is a result of its own, as below.

The solver also works out each member's mass for modal and seismic analysis from the section. A member that carries more, such as cladding or a services run, can override both:

python
from fers_core.builders import create_beam
model = create_beam(5.0, "IPE180")                  # no load but its own weightmodel.settings.analysis_options.enable_self_weight = Truefor member in model.members:    member.weight_override = 1500.0                 # N/m, gravity includedmodel.run_analysis()
self_weight = model.resultsbundle.loadcases["Self-weight"]print(round(sum(r.nodal_forces.fy for r in self_weight.reaction_nodes.values()), 1))   # 7500.0

The solver applies 1500 N/m in place of the section's 184 N/m, and takes 1500 / 9.81 kg/m as the member's mass. weight_override is in the model's force/length units. Member.weight is a different thing: the member's mass as the SDK computes it, density·area·length, which is never sent to the solver.

Seismic analysis

add_seismic_analysis asks the solver for a modal response-spectrum analysis (the default), the lateral-force method, or both. The spectrum helpers build EN 1998-1 design spectra. Accelerations are in m/s² and periods in seconds, whatever the model's units; nodal masses are in kg.

python
from fers_core import (    FERS, Member, MemberSet, MaterialLibrary, NodalMass, NodalSupport, Node, Section,    SeismicAnalysisSettings,)
model = FERS()                                                    # first: it restarts the idssteel = MaterialLibrary.S235()column = Section.from_name("HEB200", steel)nodes = [Node(0.0, float(height), 0.0) for height in range(7)]    # a 6 m mastnodes[0].nodal_support = NodalSupport()                           # fixed basemodel.add_member_set(MemberSet(members=[Member(a, b, section=column) for a, b in zip(nodes, nodes[1:])]))model.add_nodal_mass(NodalMass(nodes[-1], 2000.0))                # 2 t of equipment on top
model.add_seismic_analysis(    SeismicAnalysisSettings.eurocode_spectrum(ag=2.5, ground_type="C", q=1.5),    directions=["X"],    num_modes=4,)model.run_analysis()
x = model.resultsbundle.seismic["modal_response_spectrum"]["per_direction"][0]print(f"{x['participating_mass_ratio']:.0%} of the mass, base shear {x['base_shear'] / 1000:.1f} kN")

mass_sources=[(load_case, psi)] turns gravity load cases into seismic mass (ψE = 1.0 for permanent loads). direct_spectrum takes S, T_B, T_C and T_D directly for a national annex, and custom_spectrum a table of periods and accelerations. The results are plain dicts in resultsbundle.seismic, one entry per method. fers_core/examples/151_Seismic_Response_Spectrum.py runs a portal frame through both methods.

Unity-check reports

The solver can embed one HTML report of every unity check in the result:

python
from fers_core.builders import check_beam
model = check_beam(5.0, "IPE300", udl=10000.0)model.settings.analysis_options.include_report_html = Truemodel.run_analysis()html = model.unity_report_html()                    # a complete HTML page

render_unity_reports = True also fills each checked member's rendered_report from its check's report_template, and adds those narratives to the HTML report. Both are off by default, which keeps the result small.

Premium solves and timeouts

Without an API key the solver runs at Free-tier limits (100 members) and makes no network call at all. The free tier is for non-commercial use; commercial use needs a Pro key at any model size. Passing api_key raises those limits, to 10,000 members with a Pro key and 1,000,000 with an Enterprise one (granted by agreement):

python
model.run_analysis(api_key=KEY)

A key means each solve first verifies the licence with ferscloud.com, so that handshake is bounded. If the licence server does not answer within license_timeout seconds (default 30) the call raises FersTimeoutError before the solve starts — so retrying is always safe:

python
import fers_calculations
try:    model.run_analysis(api_key=KEY, license_timeout=60)except fers_calculations.FersTimeoutError:    ...   # transient stall — retry or skip this modelexcept RuntimeError:    ...   # definitive — bad key, non-premium account, or a rejected model

Keeping those two arms apart is the point: retrying a RuntimeError fails identically, while collapsing both into except Exception turns a retryable blip into a lost result. See fers_core/examples/203_Premium_Batch_Solve.py for the full unattended-batch pattern.

FersTimeoutError subclasses the built-in TimeoutError, so a plain except TimeoutError catches it too.

Set FERS_LICENSE_TIMEOUT (seconds) to change the default without editing any call sites. It must be a real environment variable — .env files are not read by the engine.

license_timeout bounds the licence handshake, not the solve. A solve is native code with no cancellation point, so no in-process timeout can interrupt it. For a hard ceiling on total wall time per model, run each solve in a killable subprocess — that also reclaims its memory.

Requires engine fers_calculations >= 0.2.53.

From the terminal and from coding agents

pip install FERS puts a fers command on the path: a thin layer over the objects above and the engine, for shells, scripts, CI jobs and coding agents. Every large output goes to a file; stdout carries one line per command, or a JSON object with --json.

bash
fers validate model.json                 # the input schema: the failing field, and keys the engine ignoresfers solve model.json --out results.json # the engine streams the results to the filefers check model.json                    # solve, print each unity check's utilization and a verdictfers report results.json --out report.htmlfers schema                              # the schema's definitions; `fers schema Member` prints onefers sections get IPE200                 # catalogue properties in SI; `sections list` names the seriesfers materials listfers examples list                       # the numbered examples; `examples show 001`, `examples run 001`fers skill --install .                   # the skill for coding agents, into .claude/skillsfers skill --zip fers-skill.zip          # the same skill as a zip to upload to Claude.aifers doctor                              # versions, and whether the key is accepted

fers solve sends the file to the engine as it is, and reads the results back only for its summary line: load cases, combinations, recorded failures, non-converged result sets and messages. A model that writes hundreds of megabytes costs this process nothing.

The exit code says whether to use the numbers:

CodeMeaning
0done; for check, every check passes
1the engine refused or failed the solve
2bad input: a missing file, invalid JSON, a model that fails the schema (with validate --strict, also a key the schema does not know)
3licence: no key where one is needed, a rejected key, a licence timeout
4solved, but do not use the numbers: a result set has errors (a mechanism is singular_model), did not converge, or failures were recorded. The output says why.
5check: nothing decided: a check is Indeterminate, left combinations unsolved, or the model has no checks
6check: a check fails (utilization above 1)

A check that meets several of these exits with the first of 4, 6, 5. With --json, solve and check add a verdict and its reasons. After "DO NOT USE these results" comes the reason: the solver's first error, or the error a load case or combination recorded when it failed outright. fers check names what governs each check, governing member 4 (Column, HEA300) in "ULS 1" (combination 2), and --json carries the same as governing_entity_label and governing_combination_name.

FERS_API_KEY supplies the key for Premium limits when --api-key is not given; without a key the engine runs at Free-tier limits and makes no network call, as above. --solve-token (or FERS_SOLVE_TOKEN) takes a short-lived Premium token from FERS Cloud instead, which the engine checks offline: Premium limits on a machine that cannot reach ferscloud.com. In Python it is run_analysis(solve_token=...).

For an agent, the skill is the working loop on one page: which route to take (MCP tools, a shell or code sandbox, or a file for FERS Studio), validate, solve, check, and the conventions that change answers, with runnable models in reference/api.md. It ships in the package (fers_core/skills/fers/); fers skill --install . copies it to .claude/skills/fers/, where Claude Code looks, fers skill --zip fers-skill.zip packs it for upload to Claude.ai, and fers skill --path names SKILL.md for any other agent. MCP clients can run the same engine as a local MCP server instead (npx -y @ferscloud/fers-mcp, which solves on the user's machine); clients that cannot start a program use the hosted server at https://ferscloud.com/api/mcp, which returns a browser-solve link. Setup for each client: https://ferscloud.com/mcp.

fers_core/examples/811_Command_Line_Workflow.py runs the loop end to end.

Licence

This package, FERS, is open source under the BSD 3-Clause License (LICENSE).

The solver engine it installs, fers_calculations, is licensed separately. From 0.2.70 it is under the PolyForm Strict License 1.0.0: free for any non-commercial purpose, such as study, teaching, research and hobby projects. Commercial use needs a paid FERS Cloud plan, which includes the commercial licence.

Source: README.md at commit 30bd0e3

Tools

36

solve_model

Solve a structural model

Run the FERS FEM solver on a structural model. Input is the full model JSON (as a string). Returns displacements, internal forces and reactions. Runs on this machine; nothing is metered. Results are returned at `detail: standard` by default — ask for 'summary' when you only need peak values, and 'full' only when you genuinely need every sample point.

Parametersmodeldetailmodel_refresult_filterinclude_reportidempotency_key

validate_model

Validate a model

Check a FERS model against the solver's own schema WITHOUT solving it — free. Reports field-level errors (missing or wrong-typed fields, invalid enum values), broken id references (a member pointing at a node that does not exist, a load on a deleted member, duplicate ids), and warns about unknown fields under analysis.* — which matter because the engine silently ignores those and gives you a default instead of an error. Always run this before solve_model on a hand-authored model.

Parametersmodelmodel_ref

run_unity_checks

Run code checks

Solve a model and return its code-check (unity-check) results: per-entity utilizations, status, and the calculation trace behind each rule. The model's analysis.unity_checks must already be defined (see add_unity_check). Runs on this machine; nothing is metered. Set include_report=true to also get the rendered HTML hand-calc report.

Parametersmodeldetailmodel_refresult_filterinclude_reportidempotency_key

check_beam

Check a steel beam (EN 1993-1-1)

Check a steel beam to EN 1993-1-1 (Eurocode 3). Builds the beam + a ULS load combination + an EC3 member check over one member, solves it, and returns the governing utilization (<= 1.0 = OK) with the per-rule breakdown: bending, shear, combined N+M, and lateral-torsional buckling, plus a short `advice` string, and `deflection`: the peak deflection under the unfactored (characteristic) load in mm, where it occurs (at_m) and span/deflection. The deflection is reported, not checked against a limit. Requires a named library section: European (IPE/HEA/HEB/HEM/UPE/RHS/SHS/CHS), UK (UB/UC/PFC) or US (W/HP/HSS/Pipe). Accepts common parameter aliases (span/length for span_m, load_kN_per_m/uniform_load for udl). Set restrained=true if the compression flange is continuously laterally restrained (skips the LTB check). Runs on this machine; nothing is metered. Units: m, kN, kN/m.

Parametersudlspanlengthspan_msectionrequiredsupportmaterialpoint_loadrestraineduls_factoruniform_loadload_kN_per_m+2 more

create_beam

Build a beam model

Generate a solver-ready FERS model JSON for one beam. Use a named library section (IPE300, HEA200, 'RHS 100x50x5', 'UB 457x191x67', 'W10X22', …) or a solid rectangle. Returns model JSON to pass to solve_model. Accepts common parameter aliases (span/length for span_m, load/udl/uniform_load for load_kn; point_load implies load_type 'point'). Units: metres, kN, kN/m. The model carries two load combinations: 1 'SLS characteristic (auto)' at factor 1.0 and 2 'ULS (auto)' at 1.35, so ULS unity checks evaluate factored loads. Free.

Parametersudlloadspanlengthspan_mload_knsectionsupportmaterialwidth_mmheight_mmload_type+5 more

create_frame

Build a portal frame

Generate a solver-ready FERS model JSON for a 2-D rectangular portal frame (columns + beams). Returns model JSON to pass to solve_model. Units: metres, kN, kN/m. Columns are turned 90° so their depth (column_height_mm) resists sway in the frame plane. Load combinations as create_beam: 1 'SLS characteristic (auto)' at 1.0, 2 'ULS (auto)' at 1.35. Free.

Parametersbasebaysstoreysmateriallateral_knbay_width_mbeam_width_mmbeam_height_mmcolumn_width_mmstorey_height_mcolumn_height_mmroof_udl_kn_per_m

stash_model

Park a model, get a handle

Park a model and get back a short handle (model_ref). Every authoring tool, and solve_model, accepts `model_ref` instead of `model` — so a multi-step build stops shipping the whole document through your context on every call, which on a real model is thousands of tokens per step. Tools called with a handle return the same handle, so the chain stays cheap. Handles are private to you and expire after 24 hours; they are NOT saved models and never appear in list_models (use save_model for that). Free.

Parametersnamemodelrequired

set_analysis

Configure the analysis

Configure HOW a model is solved, and switch on the analyses the engine supports beyond plain statics. Writes analysis.options (first- vs second-order, P-Delta, self-weight, gravity, tolerances) and the three optional blocks: analysis.modal (natural frequencies), analysis.buckling (linear eigenvalue alpha_cr) and analysis.seismic (EN 1998-1 response spectrum / lateral force). Pass only what you want to change — each sub-object is merged into the existing block, so load cases, combinations and unity checks are preserved. Returns the updated model for solve_model. KEY FACTS: models from create_beam, create_frame and build_model are LINEAR (the engine itself has no default and requires order; the Python SDK defaults to NONLINEAR), so set order:"NONLINEAR" for second-order/P-Delta. Buckling needs exactly one reference form (reference / references / all_combinations) and reports alpha_cr as results.buckling.modes[].critical_load_factor. Seismic directions are GLOBAL AXES and these models are Y-up, so the two horizontal directions are usually X and Z. Unknown field names are REJECTED here on purpose: the solver would accept them, ignore them, and silently give you a default. Call get_schema("BucklingAnalysisSettings") etc. for the exact fields.

Parametersclearmodalmodeloptionsseismicbucklingmodel_ref

add_load_case

Add a load case

Add an empty load case — a named bucket of UNFACTORED loads of one origin (dead, snow, wind, imposed). Put loads in it with add_load, then combine cases with partial factors using add_load_combination. A load case on its own never reaches a code check: checks envelope over COMBINATIONS only. Returns the model plus the new load_case_id. Free.

Parametersnamerequiredmodelmodel_ref

add_load

Add a load

Add ONE load of any kind to an existing load case. Choose `kind`: "nodal_force" (force at a node), "nodal_moment" (moment at a node — `direction` is the moment AXIS, right-hand rule), "distributed" (line load along a member; covers uniform, TRAPEZOIDAL/TRIANGULAR via end_magnitude, and PARTIAL via start_frac/end_frac), "member_point_force" / "member_point_moment" (at a fractional position along a member, in global or local axes), "surface" (pressure over a convex planar polygon, distributed onto the coplanar members inside it), "plate_pressure" (pressure on a meshed plate surface). SIGN: direction_x/y/z is a GLOBAL vector — gravity is direction_y = -1. The direction is normalised for you and the whole load stays in `magnitude` (the raw engine multiplies component-wise, so an unnormalised [0,-2,0] would silently double the load). Distributed intensity is per unit of MEMBER length, not of horizontal projection. Units are the model's own (default N, m) EXCEPT plate_pressure, which must always be in SI pascals. Free.

Parametersaxeskindrequirednodemodelmemberpolygonend_fracpositionmagnitudemodel_refprojectedstart_frac+10 more

add_load_combination

Add a load combination

Add ONE factored load combination — a weighted sum of load cases at a given limit state. This is what the solver designs against: unity checks envelope over COMBINATIONS, never over raw load cases, so a load case no combination references is invisible to every check. ALWAYS set limit_state: it is the only field of a combination the solver reads, and leaving it null makes every SLS deflection check return an empty result that reads as a pass. Free.

Parametersnamerequiredcheckmodelfactorsrequiredmodel_refsituationlimit_state

generate_load_combinations_ec0

Generate EN 1990 combinations

Generate a complete EN 1990 (Eurocode 0) combination set from the model's load cases in one call, instead of adding them one at a time. Classify each load case as an action type — G permanent, Q imposed, S snow, W wind — and this emits ULS expression 6.10 (each variable action taken as leading in turn, with psi_0 on the accompanying ones) and the SLS characteristic combinations, including the favourable-permanent (gamma_G = 1.0) variants that govern wind uplift. Every generated combination carries a limit_state, so unity checks actually evaluate against them. Any load case you do not classify is guessed from its name. Free.

Parametersmodelactionsrequiredmodel_refinclude_slsinclude_ulsreplace_existinginclude_favorable

add_imperfection_case

Add a geometric imperfection

Add a geometric imperfection — a global sway (out-of-plumb lean, EN 1993-1-1 §5.3.2) and/or a member bow — bound to specific load combinations. The solver perturbs the geometry before solving those combinations, so it feeds both first-order and P-Delta results. Give the sway as a ratio denominator (200 means a 1/200 lean); it is stored in radians. IMPORTANT: the engine ignores an unknown combination id SILENTLY, so a typo would apply no imperfection at all — this tool rejects unknown ids instead. Free.

Parametersmodelmodel_refsway_axis_xsway_axis_ysway_axis_zheight_direction_xheight_direction_yheight_direction_zsway_magnitude_radtranslation_axis_xtranslation_axis_ytranslation_axis_z+4 more

build_model

Build a model from a node/member graph

Build 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 w

Parametersloadsnodesrequiredorderplanemembersrequiredmaterialsectionssupportsself_weightproject_namedefault_sectionload_combinations

create_section

Add a cross-section

Add or replace a cross-section on an existing FERS model and optionally assign it to members. Takes the model JSON, mutates model.sections (and model.materials when a new preset is introduced), returns the model. THREE WAYS TO DEFINE IT: 1) LIBRARY — shape:"library" with library_name (or just `name`) set to a library profile — European IPE/HEA/HEB/HEM/UPE/RHS/SHS/CHS/L or UK UB/UC/PFC, e.g. "IPE300", "HEB240", "CHS 114.3x5", "RHS 100x50x5", "UB 457x191x67". This is the ONLY way to get exact rolled-profile properties (root fillets included) and it carries every EC3 modulus. Prefer it whenever a standard profile fits. 2) PARAMETRIC — shape:"rect" (h_mm, b_mm), "circle" (d_mm), "chs" (d_mm, t_mm), "rhs"/"shs"/"box" (h_mm, b_mm, t_mm) or "i" (h_mm, b_mm, tf_mm, tw_mm), dimensions in MILLIMETRES. rect/circle/chs are exact and rhs area+inertias are exact (its Bredt torsion J is ~2% low), but the "i" generator assumes SHARP fillets: against a real rolled IPE300 it is 3.6% low on area, 4.3% low on i_z and 22% low on J. Use "i" for WELDED plate girders, not for rolled profiles. 3) CUSTOM — shape:"custom" with your own SI numbers. area, i_y, i_z and j are all MANDATORY and strictly positive; a missing one is a hard parse error and a MISTYPED one is silently ignored (Section has no deny_unknown_fields), giving a wrong answer that still solves. AXIS CONVENTION: local z is the STRONG axis. For depth h and width b, i_z = b·h³/12 (strong, resists gravity on a horizontal member) and i_y = h·b³/12 (weak). Getting these the wrong way round is the most common authoring error and the solver cannot detect it. EC3 CHECKS NEED MORE THAN STIFFNESS. An Ec3Steel check on a section lacking wel_y/wpl_y/i_w does not fail — it returns utilization 0.0, status Green and an explanatory message, which reads as a clean pass. So also give wel_y, wel_z, wpl_y, wpl_z and i_w (warping constant, m⁶); the parametric shapes compute these for you. Optionally add ec3 {"section_class":1,"buckling_curve_y":"

Parametersjec3i_wi_yi_za_sya_szareab_mmd_mmh_mmnamerequired+13 more

set_support

Set a support condition

Write a nodal support (boundary condition) and point one or more nodes at it: creates or overwrites an entry in model.nodal_supports and sets node.nodal_support on every node you name. Returns the updated model — feed it to solve_model. All 7 DOFs are set independently, in the GLOBAL frame: X/Y/Z (translations), RX/RY/RZ (rotations about the global axes) and warping (the 7th DOF, only live when analysis.options.include_warping is true and the section has i_w). Condition types are case-sensitive: Fixed (rigid), Free (no restraint), Spring (needs the matching <DOF>_stiffness > 0 — N/m for translations, N*m/rad for rotations), PositiveOnly (one-way: may only push in the +global direction, i.e. lift-off) and NegativeOnly (the mirror image). A one-way condition may also carry a <DOF>_stiffness, which from engine 0.2.65 is the bedding stiffness it acts through while in contact — a base plate on a slab is not infinitely stiff. Released, it is free regardless. On 0.2.64 and earlier that number was accepted, range-checked and then silently discarded, so the model solved as rigid contact. THERE IS NO "FREE BY OMISSION". Once a node references a support, any DOF you leave out is treated by the solver as FULLY FIXED — a support of literally {"id":1} is an encastre, and the OpenAPI text claiming an absent field means free is wrong. This tool therefore always writes all six translation/rotation conditions explicitly, and anything you do not name is written as Free. Use `preset` to fill them in one go: fixed (encastre), pinned (X/Y/Z fixed, rotations free), free, roller_x/roller_y/roller_z (ONLY that translation fixed, everything else free), ground_contact_y (X/Z fixed, Y PositiveOnly, rotations free — a base plate that can lift off). Per-DOF fields are applied on top of the preset. Warping is the one exception: leaving it out really does mean free. ONE-WAY SUPPORTS have a prerequisite this tool checks for you: analysis.options.rigid_strategy must be RigidMember (LinearMpc is a

ParametersXYZRXRYRZmodelpresetwarpingnode_idsrequiredmodel_refsupport_id+10 more

set_member

Set member behaviour

Set structural behaviour on one or more existing members: element type, end releases (hinges), rigid end offsets, cross-section rotation/mirroring and cable parameters. Mutates model.members (and model.member_hinges when a release is authored) and returns the model. member_type: Normal (full 6-DOF beam), Truss (axial only — carries exactly zero moment), Tension (tie that drops out in compression), Compression (strut that drops out in tension), Rigid (near-rigid link; produces NO entry in results.member_results at all) or Cable (analytic elastic catenary; forces the whole solve onto the nonlinear path and only sags when analysis.options.enable_self_weight is true). Tension/Compression drop-out is governed by analysis.options.axial_slack in FORCE units — a member switches off when its axial force passes 0.5*axial_slack on the wrong side — so this tool lowers axial_slack to 1e-3 when it writes one (set keep_axial_slack to skip). With a coarse axial_slack the member never drops out and quietly behaves like a plain Truss. To build a truss use member_type Truss: two double-hinged Normal members meeting at a free node give a singular stiffness matrix. END RELEASES ARE A TRISTATE and that is the whole point: for each release DOF, null (or simply not naming it) = RIGID, 0.0 = FULLY RELEASED, and a positive number = a spring of that stiffness (force/length for vx/vy/vz, force*length/rad for mx/my/mz/warp). Release DOFs are in the MEMBER LOCAL frame. Verified on an encastre beam under UDL: mz null gives support moment wL^2/12 and midspan wL^2/24; mz 0.0 gives 0 and wL^2/8, i.e. exactly simply supported. Each release object fully defines that end's hinge — DOFs you do not name are written as rigid — and passing "rigid" clears the release. A rotational release is rejected when it would leave a node with NO rotational stiffness at all, because that is a hard "Stiffness matrix is singular or near-singular": either a dead-end node (no other member frames in and the support does n

Parametersmodelmirrormodel_refend_offsetmember_idsrequiredpretensionend_releasemember_typestart_offsetstart_releasekeep_axial_slackrotation_angle_deg+1 more

set_scissor_hinge

Let a continuous beam turn on what it crosses

Let a continuous beam turn or slide on the members it crosses (a scissor hinge, engine 0.2.68), with no link member or extra node: a rail resting on beams, a bracing pair crossing at a bolt. Name the beam by member_ids (or an existing member_set_id) and give the releases. Returns the model. releases is an object over tx, ty, tz (translations) and rx, ry, rz (rotations): 0.0 FREES that axis, a positive number is a spring between the beam and the node (force/length for t*, force*length/rad for r*), and an axis you do not name is HELD — the same tristate as a member hinge. {"rz":0.0} lets a beam in the XY plane turn on what it crosses; {"tx":0.0} lets it slide along X. The hinge goes on a MEMBER SET and applies wherever one of the set's members meets a member OUTSIDE the set (or a plate). Inside the set the members stay joined to each other, so the beam stays continuous. A set holding every member meets nothing and the solver refuses it, so when no set holds exactly these members they leave their sets for a new one; info lists the sets they were taken from. at_nodes limits the hinge to those nodes of the beam (a rail that slides over its first post only); omit it for every crossing. axes: "Global" (the default) or {"User":{"x":[...],"y":[...]}} for a rack turned in plan — the first axis runs along x, the second along y made square to it. "Local" is refused: the set's members need not share one. Supports and nodal loads at a released node act on the members outside the set, and a node's reported displacement is theirs; the beam's own motion there is in its members' local end displacements. The solver refuses a member point load or moment exactly at a released end of the beam. In second order, a model with scissor hinges takes the corotational correction only when analysis.options.nonlinear_method is COROTATIONAL. The model is written with schema_version 3, so an engine older than 0.2.68 refuses it instead of solving every crossing rigid. scissor_hinge_id reuses (and

Parametersaxesclearmodelat_nodesreleasesmodel_refmember_idsmember_set_idscissor_hinge_id

set_member_design

Set buckling / design data

Set the design (code-check) data that lives on a MemberSet — buckling restraints, buckling lengths, LTB length, effective-length factors — plus the per-member chi override. THESE FIELDS CHANGE NOTHING IN THE STATICS. Displacements, reactions and member forces come back bit-identical; the values only feed the EN 1993-1-1 unity-check evaluator. If the model has no Ec3Steel unity check and a matching load combination you will see no effect at all anywhere in the results, so add one with add_unity_check first. A MemberSet is one physical beam: the ordered chain of collinear FE members that make it up. Design lengths resolve per axis with this precedence: explicit buckling_length_* > effective_length_factor_* > buckling_restraints spacing > member length. WATCH THE TWO BASELINES — an explicit length and a restraint spacing are measured along the WHOLE chain, but effective_length_factor_y/z multiplies the length of the INDIVIDUAL FE member. Verified on a 6 m column split into 4 x 1.5 m members: buckling_length = 6.0 reproduces the default exactly, buckling_length = 12.0 quarters N_cr, but effective_length_factor = 2.0 gives L_cr = 3.0 m (2 x 1.5 m), not 12 m. buckling_restraints are named by the member LOCAL axis they hold (restrains_local_y / restrains_local_z / restrains_torsion), not strong/weak — which one is strong depends on the section orientation and rotation_angle. A restraint's node_id must actually lie on one of the set's own members or the whole model fails validation. chi is a per-MEMBER override of the EN 1993-1-1 flexural-buckling reduction factor. It replaces the section-curve chi for BOTH flexural axes (the slenderness stays physical) and is written onto every member of the targeted sets; pass "clear" to remove it.

Parameterschimodelmodel_refltb_lengthclassificationmember_set_idsrequiredbuckling_length_tbuckling_length_ybuckling_length_zset_classificationbuckling_restraintseffective_length_factor_y+1 more

add_plate_surface

Add a plate or slab

Add a meshed plate/shell surface (slab, wall, deck, panel) to a FERS model. You give the corner points; the tool creates the boundary nodes, optionally the full edge-node grid with a support, and appends the PlateSurface to model.plate_surfaces. The SOLVER meshes it at solve time — never author plate_elements for a surface yourself. Load it with add_load kind:"plate_pressure" (surface_id), not with distributed_loads. MESHING DECIDES WHETHER YOUR MODEL IS CORRECT. The mesher only ever creates NODES and PLATE ELEMENTS: it never splits members and never adds supports, so a plate connects to the rest of the structure ONLY through nodes it shares by exact coordinate. (1) Leave the mesh unset and the surface becomes TWO TRIANGLES spanning the corners — it still solves and reactions still equal q x A, so nothing looks wrong; this tool rejects that instead. (2) A supported or member-connected EDGE only exists where a node sits at an exact mesh-grid coordinate: structured meshing has predictable positions so edge_support can pre-create them, unstructured/Delaunay does not, so edge_support with unstructured meshing is rejected. (3) If a beam runs along a plate edge, split that beam into segments ending on the grid, or the plate hangs off its two end nodes only (measured 4.2x too flexible, beam moment 2.5x too low) with no warning. THEORY — THE ENGINE DEFAULT IS WRONG FOR THIN PLATES. PlateTheory "Auto" is resolved unconditionally to Mindlin (the OpenAPI text claiming otherwise is stale). Mindlin (DSG3) shear-locks: on a simply-supported square slab at span/thickness 80 the centre-deflection error was -86.1% / -55.2% / -8.1% / -0.4% at 4x4 / 8x8 / 16x16 / 32x32, where Kirchhoff (DKT, rewritten and patch-test-exact in 0.2.44) gave -5.5% / -1.4% / -0.3% / -0.03%. So: span/thickness >= 20 -> "kirchhoff"; span/thickness < 10 -> "mindlin" plus a fine mesh (it correctly adds the ~10% shear deflection Kirchhoff misses). theory:"auto" (the default) makes that choice from the SMALLER

Parametersnamemodeloffsettheorybehaviormodel_refthicknesssurface_iddivisions_udivisions_vmaterial_idmesh_method+12 more

add_plate_opening

Cut an opening in a plate

Cut a hole (stairwell, duct, riser) in an existing PlateSurface: creates the hole's boundary nodes and appends a PlateOpening to that surface's `openings`. The hole ring must be coplanar with, and lie strictly inside, the outer boundary; elements whose centroid falls in the hole are dropped, so the applied load drops with the removed area (verified: a 1x1 m hole in a 4x4 m slab under 10 kPa took the total reaction from 160000 N to exactly 150000 N). HARD CONSTRAINT: openings only work with the Delaunay mesher. The structured mesher rejects them outright ("PlateSurface N Structured meshing does not support openings."), so this tool switches the surface to mesh method "Unstructured" with a target_size and tells you it did. That makes the surface's edge-node positions unpredictable, which is why the tool REFUSES to convert a surface whose structured edge grid is carrying a support: those pre-created edge nodes would be stranded off the new mesh and the plate would silently fall back to hanging on its corners (measured 6x too flexible, with the load still balancing exactly). If you need both an opening and a supported edge, author the surface as unstructured from the start and support it by other means. TARGET SIZE IS A WHOLE-SURFACE SETTING, so a SMALL hole is the expensive case, not the cheap one: left to default (half the shortest hole edge) a 200 mm duct in a 6 m x 6 m slab asks for ~6100 mesh nodes and the engine aborts on that with a raw wasm trap. The default is therefore clamped to what the node budget allows — which means the hole is resolved coarsely and its stress concentration is NOT converged; model the region around the opening as its own smaller surface if you need it sharp. An explicit target_size that busts the budget is refused rather than clamped.

Parametersmodelmodel_refopening_idsurface_idtarget_sizecorner_pointsboundary_node_ids

add_plate_grid_elements

Add an explicit plate mesh

Author plate elements EXPLICITLY (model.plate_elements) as a regular quad grid over four corner points, instead of letting the solver mesh a PlateSurface. Creates the nodes, optionally supports the perimeter, and writes one 4-node PlateElement per cell. Verified to give bit-identical results to the equivalent structured surface mesh (16x16: max|dz| 4.31048e-3 m either way). USE THIS WHEN YOU NEED PLATE UNITY CHECKS. In engine 0.2.52 a unity check targeting plates (AllPlates / PlateSurfaces / PlateElements with a PlateStress variable) returns an EMPTY per_entity for anything meshed from a PlateSurface — status "Green", max_utilization 0, no warning — because the mesh is generated on an internal working copy while the checks run against the original document. Explicit plate elements survive in that document and are checked normally (verified 256/256 entities). ALSO USE IT when you need stable, known plate element ids: mesh-generated ids start at max(explicit id)+1 and are never echoed back, so an element-targeted pressure on a generated id is a silent no-op. COST: verbosity. An N x N grid emits (N+1)^2 nodes and N^2 elements inline, and the engine allocates a dense (7 x nodes)^2 block, so keep total model nodes under ~2000. Load the grid with ONE add_load kind:"surface" whose polygon covers it, rather than N^2 individual pressures. Same theory guidance as add_plate_surface: PlateTheory "Auto" resolves to Mindlin, which shear-locks (-55% at 8x8 on a span/thickness 80 slab), so prefer "kirchhoff" at span/thickness >= 20.

Parametersmodeltheorybehaviormodel_refthicknessdivisions_udivisions_vmaterial_idplane_stateedge_supportcorner_pointsrequiredfirst_element_id+1 more

summarize_plate_results

Summarize plate results

Reduce a solved FERS document to the plate numbers an engineer actually reads, and derive extreme-fibre stresses. READ-ONLY: it returns a summary, not a model. You need this because plate results cannot be read back through the model. The mesh is generated on an internal working copy, so the returned document has model.plate_elements = [] and plate_surfaces[].generated_plate_element_ids = [], and the mesh node ids in displacement_nodes have no coordinates anywhere in model.nodes. The only georeferenced plate output is results.<case|combination>.plate_results, keyed by element id, carrying centroid {X,Y,Z}, centroid_displacement_global/local and the stress resultants. UNITS AND CONVENTION, from the engine's own denormalisation: nx, ny, nxy, qx, qy are membrane forces and transverse shears per unit width, in force/length (N/m); mx, my, mxy are moments per unit width, in force (N.m/m, numerically N). mx is the moment PRODUCING sigma_x (conjugate to curvature about local y), not "the moment about x" — on a rectangular slab spanning the short way with local_x along the long side, the large resultant is my. The sign of mx/my/mxy follows the element normal, which follows the boundary/connectivity ordering, so reversing it flips all three; von Mises and |principal| are immune. qx and qy are identically ZERO for Kirchhoff/DKT plates by construction, so non-zero transverse shear is itself proof the Mindlin element was used. results.<case>.summary.total_plate_forces is a COUNT of plate results, not a force. Give `thickness` and the tool derives the extreme-fibre stresses exactly as the engine's own plate unity check does: sigma_x = nx/t +/- 6mx/t^2, sigma_y = ny/t +/- 6my/t^2, tau_xy = nxy/t +/- 6mxy/t^2, evaluated at both fibres with the governing (larger von Mises) one reported, then vonMises = sqrt(sx^2 - sx.sy + sy^2 + 3txy^2) and the in-plane principals. Add `yield_stress` for a utilisation.

Parameterstop_nresultsrequiredthicknessquantitiesyield_stresscase_or_combination

render_report

Render the hand-calc report

Render the human-readable HTML hand-calc report from an ALREADY-SOLVED document (the full JSON you got back from solve_model or run_unity_checks, including its `results`). No re-solve, and free — solve lean with include_report=false, then call this only when you need a report to show someone.

Parameterssolved_modelrequired

list_sections

List steel sections

List the named steel sections FERS knows: European (IPE, HEA, HEB, HEM, UPE, RHS, SHS, CHS, L), UK (UB, UC, PFC) and US (W, HP, HSS, Pipe). check_beam and create_beam require one of these names, so call this rather than guessing. Returns names only — use get_section for a section's properties. Free.

Parametersfamily

get_section

Section properties

Get a named section's full properties in SI units (m, m², m⁴): area, second moments i_y/i_z, torsion constant j, warping constant i_w, shear areas, and the elastic/plastic moduli wel_y/wel_z/wpl_y/wpl_z that an EN 1993-1-1 check needs. Free.

Parametersnamerequired

list_materials

List materials

List the named material presets the model builders accept, with their E, G, density and yield stress in SI units. The solver itself accepts ANY isotropic material — see get_schema('Material') to author one inline. Free.

Takes no parameters.

get_schema

Look up the model schema

Look up the FERS model input contract — the authoritative JSON Schema the solver itself is generated from. Call with NO arguments to get an index of the useful roots (geometry, loads, analysis, checks) with a one-line description of each; then call again with a `root` to get that schema plus everything it references. Use this instead of guessing field names: the engine silently ignores unknown keys under analysis.*, so a typo becomes a wrong answer rather than an error. Free.

Parametersroot

get_check_schema

Code-check schema

Get the canonical schema for authoring unity (code) checks: the UnityCheckDefinition schema and everything it references, read straight from the solver's OpenAPI, plus a worked EN 1993 example. Call this before add_unity_check. Free.

Takes no parameters.

add_unity_check

Add a code check

Append a unity (code) check to a model's analysis.unity_checks. `check` is a UnityCheckDefinition JSON (see get_check_schema). Returns the updated model JSON to pass to run_unity_checks. Free.

Parameterscheckrequiredmodelmodel_ref

save_model

Save a model to your account

Save a model to this machine's FERS model folder. Solver results are never stored. Returns the model id.

Parametersnamerequiredmodelmodel_refdescription

load_model

Load a saved model

Load one of your saved models' full JSON by id (owner only).

Parametersidrequired

list_models

List saved models

List your saved models (id, name, description, last-updated time).

Takes no parameters.

list_templates

List starter models

List all available template structural models. Returns name and description for each template.

Takes no parameters.

get_template

Get a starter model

Retrieve the full JSON of a template structural model by name. Use list_templates first to see available names.

Parametersnamerequired

get_usage

Check usage

Check your FERS tier and how solves run on this server: member limits, licence state and where results come from.

Takes no parameters.

Version history

7
  1. v2.83.0LatestNov 4, 2026
  2. v2.82.0Nov 3, 2026
  3. v2.80.0Nov 3, 2026
  4. v2.79.0Nov 1, 2026
  5. v2.77.0Oct 30, 2026
  6. v2.76.0Oct 29, 2026
  7. v2.1.0Oct 21, 2026

Score

100% of the applicable checks met

How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.

  • Reached in the last 30 daysRequired25 pts
  • Auth declaredRequired10 pts
  • Tools listed15 pts
  • DescriptionRequired15 pts
  • Logo10 pts
  • Documentation15 pts
  • License10 pts