analysis-results
Skill11 starsChanged 33 days agoMITView on GitHub
Read and interpret the SkyCiv S3D analysis results object returned after a solve — reactions, per-station member forces/stresses/displacements, plate results, min/max summaries. Use after S3D.model.solve, or when reading results client-side in an S3D App.
In this repository17 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- baseplateSkill
- build-quick-design-calculatorSkill
- cloudcad-apiSkill
- foundation-apiSkill
- load-combinationsSkill
- load-gen-apiSkill
- qa-engineerSkill
- rendererSkill
- run-quick-designSkill
- s3d-apiSkill
- s3d-appsSkill
- schema-agentSkill
- section-selectorSkill
- skill-writerSkill
- skyciv-api-v3Skill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 234
Reactions only list supported nodes: Don't iterate a model's full node list expecting a reaction entry for each — nodes without a support …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Analysis Results Skill
How to fetch, read, and interpret S3D structural analysis results — the object returned once a model has been solved — whether you're calling the SkyCiv API or building a client-side S3D App that reads results from an already-open session.
Prerequisite: A model must be solved before results exist. Over the API, that's
S3D.model.solve(sees3d-api). Inside an S3D App, the user solves via the S3D UI itself — sees3d-appsfor the app runtime this skill's client-side functions run in.
Two ways to get results — same object, different calls
| Context | Call sequence | Notes |
| Server API | S3D.model.set → S3D.model.solve (or S3D.model.solve then S3D.results.get) | Returns the analysis results object directly, already in API format. |
| S3D App / UI (client-side, inside an open S3D session) | S3D.solver.isSolved() → S3D.results.get() / S3D.results.getAll(true, cb) → S3D.API.output.S3D2API(...) | S3D.results.* return S3D's internal/legacy format, not the API shape. Always pipe through S3D.API.output.S3D2API to get the same object documented below. |
Do not skip the conversion step in an App. S3D.results.get() / getAll() alone return a
different, older internal shape — if your app was built to read the documented results schema
(the one below, matching the API), reading S3D.results.getAll()'s raw output directly will look
similar but have different/missing keys and silently produce wrong values. Always wrap it:
S3D.API.output.S3D2API(S3D.results.getAll(true))Checking solve status first
if (!S3D.solver.isSolved()) {
// Nothing to fetch yet — prompt the user to solve, or solve programmatically first.
} else {
// S3D.results.get() is the current load combination only, and always instantly available.
const currentComboResults = S3D.API.output.S3D2API(S3D.results.get());
// S3D.results.getAll(true, cb) returns every load combination, but may take a moment to
// download — do the real work inside the callback, not immediately after the call.
S3D.results.getAll(true, function () {
// Served from the local cache this time, not re-downloaded.
const allResults = S3D.API.output.S3D2API(S3D.results.getAll(true));
});
}S3D.results.getAll's first argument (downloadAll) asks for every load combination rather than
only the ones already held locally. Pass true: the results are cached once downloaded, so the
second getAll(true) inside the callback reads the local copy and does not re-fetch. That is why
the example calls it twice — once to trigger the download, once to read the result — and why doing
so costs nothing.
Reading solve messages/warnings
S3D.solver.getLastSolveInfo() returns the same info/warning messages shown in the S3D solve
dialog — general messages plus per-load-group/load-case/load-combination breakdowns and a
severity-bucketed warnings list:
{
"msg": "Solved Successfully!",
"info": ["Linear Static Analysis Completed"],
"info_categorised": { "general": ["Linear Static Analysis Completed"], "load_groups": {}, "load_cases": {}, "load_combinations": {} },
"warnings": [],
"warnings_categorised": {
"general": { "minor": [], "moderate": [], "severe": [] },
"load_groups": {}, "load_cases": {}, "load_combinations": {}
},
"err_code": 0
}Surface msg and any non-empty warnings/warnings_categorised buckets in your UI before
presenting numeric results — a warning (e.g. a mechanism, an unstable member) means the results
may not be trustworthy even though a solve technically completed. err_code !== 0 means the solve
failed outright.
The analysis results object
Whichever path you used above, you get an object keyed by load combination id — a string,
matching the id used in the model's load_combinations (or an auto-numbered id for individual
load cases/groups/envelopes). IDs are not necessarily sequential or starting at 1
(e.g. 1, 2, 8, 9, 10, 11, 12, 13, 14, 15 is a normal set — gaps are expected, matching whichever
load_combinations/load_cases/load_groups ids and auto-generated envelope ids exist on that
solve). A full worked solve — 10 combinations, 16 members, 20 plates — is at
assets/example1-api-format.json. Every shape and
number in this skill is taken from it, so the tables below are the fastest way to read it.
At 5.6 MB (~1.37M tokens) never open the file, and there is no safe slice either — one
combination alone is ~57k tokens. If you need a value the tables do not give, print just that
path with a one-line script; never open the file to find it.
{
"1": { "name": "...", "type": "...", /* ... */ },
"2": { /* ... */ }
}Per-combination entry
| Key | Type | Description |
name | string | Display name. The only reliable way to distinguish envelope sub-types — see the type gotcha below. |
type | string | user_defined (a combo from load_combinations), load_case, load_group (e.g. self-weight SW1 solved alone), or envelope. |
reactions | object | Support reactions — node id → {Fx, Fy, Fz, Mx, My, Mz}. Only nodes that actually have a support appear as keys. |
member_displacements, member_forces, member_stresses | object | Raw per-station member results. Tier 1 of 3 — see below. |
member_lengths, member_stations, member_discontinuities | object | Supporting per-member data — see below. |
member_minimums, member_maximums | object | Per-member min/max summaries. Tier 2 of 3. |
member_peak_results | object | Global min/max across every member. Tier 3 of 3. |
plate_displacements, plate_forces, plate_stresses | object | Raw per corner-node plate results. Tier 1 of 3. |
plate_element_forces, plate_element_stresses | object | absent | Same as above, per meshed element. Not guaranteed to be present — absent entirely on some combinations, {} on others, until the plates are meshed (S3D.model.mesh). Guard before reading; see the gotcha below. |
plate_minimums, plate_maximums | object | Per-plate min/max summaries. Tier 2 of 3. |
plate_peak_results | object | Global min/max across every plate. Tier 3 of 3. |
buckling | string | object | "disabled" unless the solve used analysis_type: "buckling". |
Dynamic/response-spectrum results are separate. Modes, frequencies, and mass participation live in
S3D.results.getDynamicFreq, not in this per-combination object.
Units are never embedded in results. Every number follows the model's
s3d_model.settings.units(sees3d-api) — read that once per model rather than assuming mm/kN/MPa.
Member results — three tiers, same pattern every time
- Raw per-station (
member_displacements/member_forces/member_stresses): result key → member id → station position (string, 0–100% of the member's length, rounded to 1 decimal) → a float, or a 2-element[left, right]pair at a discontinuity — see the gotcha below. Station count varies per member and is notsettings.evaluation_points— in the reference solve it ranges from 9 to 57, rising with the member's discontinuity count. Never assume a count or compute a station index from a percentage: readmember_stations["<id>"].length, or iterate the station keys of the result object itself. - Per-member min/max (
member_minimums/member_maximums): result key → member id → a single scalar — the min/max of that quantity anywhere along that one member. - Global min/max (
member_peak_results): result key →{ "min": ..., "max": ... }— the extremes across every member in the combination (no member id, no station).
member_forces.axial_force["1"]["0.0"] // -0.001 (station-level)
member_minimums.axial_force["1"] // -0.00139328 (per-member min)
member_peak_results.axial_force // {"min": -0.00267385, "max": 0.427625} (global)
// Member 1 has two discontinuities, so some of its stations hold a [left, right] pair —
// and only for the quantities that actually step there:
member_forces.axial_force["1"]["33.3"] // [-0.001, 0.003]
member_forces.shear_force_y["1"]["33.3"] // [-0.057, 0.047]
member_forces.bending_moment_z["1"]["33.3"] // -0.004 (continuous — still a scalar)| Object | Keys |
member_displacements | displacement_x/y/z, displacement_sum, displacement_local_x/y/z, displacement_local_sum, rotation_x/y/z, rotation_local_x/y/z |
member_forces | axial_force, shear_force_y, shear_force_z, bending_moment_y, bending_moment_z, torsion |
member_stresses | axial_stress, top_bending_stress_z/y, btm_bending_stress_z/y, shear_stress_z/y, shear_stress_total, torsion_stress, max_combined_normal_stress, min_combined_normal_stress |
member_minimums / member_maximums (extra keys beyond the above three) | displacement_relative, displacement_relative_local_x/y/z, displacement_relative_2, displacement_relative_local_x/y/z_2, displacement_span_ratio, displacement_span_ratio_2 — relative-to-chord deflection and span/deflection ratio, e.g. for span/250 serviceability checks. |
Supporting per-member data (not results, but needed to interpret them):
| Key | Shape | Description |
member_lengths | member id → float | Member length. |
member_stations | member id → [float] | The exact station positions (0–100%) the raw results are evaluated at. Length varies per member and is not settings.evaluation_points — this array is the authoritative station count for a member; don't derive one. |
member_discontinuities | member id → integer | Count of internal discontinuity points detected along the member (e.g. from a mid-span point load) — a count, not an array of positions. Governs both how many stations the member has and whether any of its station values are [left, right] pairs. |
Plate results — the same three tiers
- Raw per corner-node (
plate_displacements/plate_forces/plate_stresses): result key → plate id → an array of values, one per corner node in the plate'snodesorder (3 entries triangular, 4 quad). - Per-plate min/max (
plate_minimums/plate_maximums): result key → plate id → single scalar. - Global min/max (
plate_peak_results): result key →{ "min": ..., "max": ... }across every plate.
plate_stresses.stress_von_mises_top["1"] // [0.357414, 0.363112, 0.790236, 0.482214]
plate_minimums.stress_von_mises_top["1"] // 0.357414
plate_peak_results.displacement_x // {"min": -0.00747437, "max": -0.00738231}| Object | Keys |
plate_displacements | displacement_x/y/z, displacement_sum |
plate_forces | force_x/y/xy/xz/yz, moment_x/y/xy |
plate_stresses | stress_xx/yy/xy, stress_major_principal, stress_minor_principal, stress_von_mises, stress_max_shear — each suffixed _top and _btm |
plate_element_forces / plate_element_stresses mirror plate_forces / plate_stresses but
keyed per meshed element instead of per corner node.
Gotchas (verified against assets/example1-api-format.json)
type: "envelope"doesn't tell you which envelope.Envelope Min,Envelope Max, andEnvelope Absolute Maxall reporttype: "envelope"— usenameto distinguish them. This also means thelc_filter: ["envelope_abs_max"]option onS3D.model.solve/S3D.results.getselects by a filter keyword that does not match thetypefield you get back — filter by keyword going in, readnamecoming out.- User-defined combinations report
type: "user_defined", not"load_combo"— despitelc_filteraccepting"load_combo"as a filter keyword. Same filter-in/name-or-type-out mismatch as above. - A station value can be a
[left, right]pair, not a number. Where a member has a discontinuity (a mid-span point load, an intermediate support), that station holds the value either side of the step —member_forces.shear_force_y["1"]["33.3"]is[-0.057, 0.047]. It varies per result key at the same station:shear_force_yandaxial_forcejump there whilebending_moment_zstays continuous and scalar. 2,254 station values across the reference solve's ten combinations are pairs (190–261 in each), always on the six members withmember_discontinuities > 0— except combination10, which has none at all. So don't feature-detect on one combination: a series that is all scalars there can hold pairs in the next. This fails silently rather than loudly —Math.max(...Object.values(series))returnsNaN, andvalue > limitcoerces instead of throwing. Flatten the pairs first, or readmember_minimums/member_maximums, which are always scalars. member_discontinuitiesis a count, not a results array — despite sitting alongside per-station result objects,member_discontinuities["<id>"]is a single integer (how many discontinuity points were detected along that member), not a station-keyed object. Treat it as a flag rather than a formula: a member with a non-zero count has more stations than a plain one (notsettings.evaluation_points, and not the same number as its neighbours) and may carry[left, right]pairs. For the actual count, readmember_stations["<id>"].length.plate_element_forces/plate_element_stressesmay be missing, not merely empty. They populate only once the model's plates have been meshed (S3D.model.mesh), but an unmeshed solve is inconsistent about how it says so: in the reference asset the keys are absent entirely from the fouruser_definedcombinations and present as{}on the six load-case, load-group and envelope ones. SoObject.keys(combo.plate_element_forces)throws aTypeErroron exactly the combinations you are most likely to read first — guard with?.or|| {}. The same holds for any key dropped byresult_filteron a solve: no key in the table above is guaranteed to be present.- Reactions only list supported nodes. Don't iterate a model's full node list expecting a reaction entry for each — nodes without a support are simply absent as keys.
- Station keys are rounded strings, not the raw floats in
member_stations.member_stationsgives full-precision positions (e.g.4.166666702); the matching key inmember_forcesetc. is that same position rounded to 1 decimal as a string ("4.2"). Don't try to match on float equality — read the key set directly, or round your own lookup the same way.
Downstream use
- Pass this object straight to
S3D.results.setto load results computed elsewhere (a different solve, a cached run) back into a session for reporting. - For steel, cold-formed steel and timber member design, prefer
S3D.design.member.check. It reads these results itself, across every load combination, so you don't extract forces by hand. run-quick-designchecks (all concrete design, connections, or member codes that Member Design doesn't support) consume the peak forces/stresses from here (typicallymember_peak_resultsormember_maximumsper member) as their design-load inputs.- A Quick Design calc pack's
s3d_integration.jsreads the legacyS3D.results.getAll(true)array (not the API shape above) throughStructureHelpers.ezDesignForces; see the S3D integration guide inbuild-quick-design-calculator. qa-engineerreviews should cross-checkS3D.solver.getLastSolveInfo()warnings alongside the numeric peaks here before signing off a result set.- For a downloadable report instead of raw data, see
S3D.results.getAnalysisReport— a separate, much slower call; don't bundle it into a normal analyze/results pipeline (see the caution ins3d-api).
<!-- Maintainers only — not part of the agent workflow. -->Refreshing this skill. assets/make-shape.py walks the whole reference asset and prints a
shape manifest (every key, how each object is keyed, sample values, the pair/station counts quoted
above). Run it after the asset is refreshed or the API changes, fold anything new into the tables
here, then delete its output — it is gitignored. Don't run it as part of reading this skill: the
tables above already carry everything it finds, at a third of the tokens.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

