agentleFS
skycivskyciv/skyciv-ai-skills/analysis-results/SKILL.md

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.

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
  1. Distinctive:

    Reactions only list supported nodes: Don't iterate a model's full node list expecting a reaction entry for each — nodes without a support …

    line 234

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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 (see s3d-api). Inside an S3D App, the user solves via the S3D UI itself — see s3d-apps for the app runtime this skill's client-side functions run in.


Two ways to get results — same object, different calls

ContextCall sequenceNotes
Server APIS3D.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

KeyTypeDescription
namestringDisplay name. The only reliable way to distinguish envelope sub-types — see the type gotcha below.
typestringuser_defined (a combo from load_combinations), load_case, load_group (e.g. self-weight SW1 solved alone), or envelope.
reactionsobjectSupport reactions — node id → {Fx, Fy, Fz, Mx, My, Mz}. Only nodes that actually have a support appear as keys.
member_displacements, member_forces, member_stressesobjectRaw per-station member results. Tier 1 of 3 — see below.
member_lengths, member_stations, member_discontinuitiesobjectSupporting per-member data — see below.
member_minimums, member_maximumsobjectPer-member min/max summaries. Tier 2 of 3.
member_peak_resultsobjectGlobal min/max across every member. Tier 3 of 3.
plate_displacements, plate_forces, plate_stressesobjectRaw per corner-node plate results. Tier 1 of 3.
plate_element_forces, plate_element_stressesobject | absentSame 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_maximumsobjectPer-plate min/max summaries. Tier 2 of 3.
plate_peak_resultsobjectGlobal min/max across every plate. Tier 3 of 3.
bucklingstring | 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 (see s3d-api) — read that once per model rather than assuming mm/kN/MPa.

Member results — three tiers, same pattern every time

  1. 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 not settings.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: read member_stations["<id>"].length, or iterate the station keys of the result object itself.
  2. 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.
  3. 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)
ObjectKeys
member_displacementsdisplacement_x/y/z, displacement_sum, displacement_local_x/y/z, displacement_local_sum, rotation_x/y/z, rotation_local_x/y/z
member_forcesaxial_force, shear_force_y, shear_force_z, bending_moment_y, bending_moment_z, torsion
member_stressesaxial_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):

KeyShapeDescription
member_lengthsmember id → floatMember length.
member_stationsmember 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_discontinuitiesmember id → integerCount 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

  1. 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's nodes order (3 entries triangular, 4 quad).
  2. Per-plate min/max (plate_minimums / plate_maximums): result key → plate id → single scalar.
  3. 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}
ObjectKeys
plate_displacementsdisplacement_x/y/z, displacement_sum
plate_forcesforce_x/y/xy/xz/yz, moment_x/y/xy
plate_stressesstress_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, and Envelope Absolute Max all report type: "envelope" — use name to distinguish them. This also means the lc_filter: ["envelope_abs_max"] option on S3D.model.solve / S3D.results.get selects by a filter keyword that does not match the type field you get back — filter by keyword going in, read name coming out.
  • User-defined combinations report type: "user_defined", not "load_combo" — despite lc_filter accepting "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_y and axial_force jump there while bending_moment_z stays 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 with member_discontinuities > 0 — except combination 10, 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)) returns NaN, and value > limit coerces instead of throwing. Flatten the pairs first, or read member_minimums/member_maximums, which are always scalars.
  • member_discontinuities is 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 (not settings.evaluation_points, and not the same number as its neighbours) and may carry [left, right] pairs. For the actual count, read member_stations["<id>"].length.
  • plate_element_forces/plate_element_stresses may 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 four user_defined combinations and present as {} on the six load-case, load-group and envelope ones. So Object.keys(combo.plate_element_forces) throws a TypeError on exactly the combinations you are most likely to read first — guard with ?. or || {}. The same holds for any key dropped by result_filter on 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_stations gives full-precision positions (e.g. 4.166666702); the matching key in member_forces etc. 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.set to 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-design checks (all concrete design, connections, or member codes that Member Design doesn't support) consume the peak forces/stresses from here (typically member_peak_results or member_maximums per member) as their design-load inputs.
  • A Quick Design calc pack's s3d_integration.js reads the legacy S3D.results.getAll(true) array (not the API shape above) through StructureHelpers.ezDesignForces; see the S3D integration guide in build-quick-design-calculator.
  • qa-engineer reviews should cross-check S3D.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 in s3d-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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.