Skip to the guide
‹ Back to the app CivilKit Buckling · Guide

Engine API (Rust / wasm)

For people calling the engine directly: the C interface of cufsm-rs, the Rust port of CUFSM, which cufsm.wasm exports. The app, the extensions' capabilities, the Python console and the cufsm-rs-py package are all built on this engine.

The crate and its builds#

cufsm-rs (on crates.io as cufsm-rs) is a dependency-free Rust port of CUFSM by B.W. Schafer and co-workers, with constrained FSM (cFSM) after Ádány and Schafer. It is tested against CUFSM's own MATLAB code run under Octave. It has three users here:

  • cufsm.wasm, the crate with its ffi feature compiled to WebAssembly: what the app, the extension capabilities and both of the console's Pythons call. This page is its interface. The app's copy is built from cufsm-rs 0.4.2.
  • cufsm-rs-py (pip install cufsm-rs-py), the crate compiled as a native CPython extension, with the Python layer the Python API reference documents.
  • Rust programs, which use the crate's Rust API directly (see its README); the C interface is off unless the ffi feature is turned on.

Build the module from the crate's source with:

cargo rustc --release --lib --features ffi --crate-type cdylib --target wasm32-unknown-unknown

ABI version#

ExportReturns
cufsm_abi_version() → u322: the buffer layouts' version. It changes only when an existing export's layout changes.
cufsm_abi_minor() → u321: which exports have been added within version 2, leaving every earlier layout as it was. A module without this export is minor 0.

Refuse a module whose version is not the one you built for. If you need the minor 1 exports, also refuse a minor below 1; the app does both.

Memory and calls#

Everything crosses the boundary as flat buffers in the module's linear memory. There is no binding library.

ExportWhat it does
cufsm_alloc(n: usize) → *mut u8n bytes of 8-aligned memory for you to fill; null (0) when the memory cannot be had.
cufsm_dealloc(p: *mut u8, n: usize)Frees a block from cufsm_alloc; n must be the byte count you allocated it with. Never free a block twice.
cufsm_last_error_ptr() → usizePointer to the last error message's UTF-8 bytes (0 when there has been none).
cufsm_last_error_len() → usizeIts length in bytes.

The conventions every analysis export follows:

  • Buffers are little-endian f64, except the boundary condition, which is UTF-8 bytes. Each buffer pointer is followed by its length, counted in values (in bytes for the boundary condition).
  • The last two arguments are out and out_cap: a buffer you allocated for the result, and how many f64 it holds. An export checks that the result fits before any work, and fails with output buffer is too small: N values needed, M given otherwise.
  • The return value is the number of f64 written, or -1 on failure, when cufsm_last_error_ptr and cufsm_last_error_len hold the message (valid until the next failing call; one slot per process, so native callers on several threads must serialise).
  • Every input is checked (shapes, finite numbers, node and material indices, flags) and refused with a message, as CUFSM's model checks are. A panic inside the analysis is caught and reported the same way natively; on wasm32-unknown-unknown, which aborts on a panic, it traps instead (a RuntimeError in JavaScript).
  • No fixed caps on longitudinal terms or eigenvalues: memory is the limit. The analysis holds dense matrices of (4 × nodes × nterms)² values; a size the platform cannot even address is refused with a message.

Model buffers#

The engine's own layouts are 0-based and differ from CUFSM's tables (which the Python package uses): no row numbers, and materials referred to by row.

BufferValues per rowColumns
mats5ex, ey, vx, vy, g. Ex, Ey and G positive, vx·vy below 1.
nodes7x, z, free_x, free_z, free_y, free_q, stress: CUFSM's node columns without the number; each free flag 0 or 1; stress is the reference stress, positive in compression.
elems4node_i, node_j, t, mat: 0-based node rows, thickness above 0, 0-based material row.
springs9 (length 0 for none)node_i, node_j, k_u, k_v, k_w, k_q, local, discrete, ys_fraction: node_j -1 for ground; local and discrete 0 or 1; a discrete spring acts at ys_fraction of the length (CUFSM v4.3's springs row).
constraints5 (length 0 for none)node_e, dof_e, coeff, node_k, dof_k, reading u_e = coeff · u_k; DOF codes 1 x, 2 z, 3 y along the member, 4 θ.
bcbytesS-S, C-C, S-C, C-F or C-G (also C-S, F-C, G-C). Exact case, except in cufsm_strip and cufsm_classify, which take lower case too.
lengths1Each positive and finite. With S-S and one term they are half-wavelengths (the signature curve); otherwise member lengths.

Mode vectors (dofs) are in CUFSM's order: per longitudinal term, 4 × nodes values, u and v interleaved for every node, then w and θ interleaved; scaled so the largest entry is 1. In the output layouts below, λ is a load factor (a multiplier on the nodes' reference stresses) and G, D, L, O are cFSM percentages.

Exports#

Arguments are listed in order; …_len follows each buffer and is omitted below. (model) stands for mats, nodes, elems.

cufsm_signature#

cufsm_signature(params, (model), bc, lengths, springs, constraints, out, out_cap) → isize

params = [terms, spaces, neigs]: terms 1 or more (the terms 1..terms at every length), spaces 0 to 15 (bits 1 G, 2 D, 4 L, 8 O: the constrained curves wanted), neigs 1 or more (checked, but only the lowest mode is solved). Output: one row of 2 + popcount(spaces) values per length, in input order: L, λ, then one column per requested space in G, D, L, O order, each from a run restricted to that space alone. A length with no positive load factor writes NaN.

cufsm_modes#

cufsm_modes(params, (model), bc, lengths, springs, constraints, out, out_cap) → isize

params as cufsm_signature (spaces unused). Output per length, a row of 2 + nterms + neigs × (5 + 4 × nodes × nterms) values: found, nterms, the terms m_1 … m_nterms, then neigs blocks of λ, G, D, L, O and the 4 × nodes × nterms dofs. Blocks past found are NaN. G, D, L, O are CUFSM's default classification (axial, vector, ST).

cufsm_props#

cufsm_props((model), out, out_cap) → isize

Gross and warping section properties. Output, 15 values: A, xcg, zcg, Ixx, Izz, Ixz, thetap (degrees), I11, I22, J, xs, zs, Cw, B1, B2.

cufsm_stresgen#

cufsm_stresgen((model), actions, out, out_cap) → isize

actions = [P, Mxx, Mzz, M11, M22, B, restrained], restrained 0 or 1. Output: one reference stress per node. The nodes' own stress column is ignored.

cufsm_yield#

cufsm_yield((model), args, out, out_cap) → isize

args = [fy, restrained, extreme_fibre], both flags 0 or 1 (extreme_fibre 1 checks the element faces, 0 the centreline). Output, 6 values: Py, Mxx, Mzz, M11, M22, By.

cufsm_stress_to_action#

cufsm_stress_to_action((model), out, out_cap) → isize

Actions fitted by least squares to the nodes' stress column. Output, 5 values: P, M11, M22, B, err.

cufsm_ftm#

cufsm_ftm(params, terms, out, out_cap) → isize

The finite tube method on one circular tube (the app's Tube view). params = [R, t, L, E, nu, N, M, T, V, base, top, p, nmodes, nth, ny]: radius, thickness, length, modulus, Poisson's ratio; the actions in N and N·mm (compression, and the moment's compression side at θ = 0, positive); base and top ends 0 free edge, 1 pinned, 2 clamped, 3 free with a stiff ring; p circumferential harmonics (1 to 80); nmodes 1 to 20; nth (4 to 720) by ny (2 to 720) grid points. terms: the longitudinal wave numbers (1 to 10,000). Output: modes found, unknowns, σN, σM, τT, τV (the reference stresses), then per mode λ, circumferential waves and its (u, v, w) at each grid point (θ = 2πi/nth, y = L·j/(ny − 1), θ outer), scaled so the largest coefficient is 1. out must hold 6 + nmodes × (2 + 3 × nth × ny) values.

Added in minor 1#

These exist so a script written for the Python package can run on the same engine. Every layout above is unchanged.

cufsm_strip#

cufsm_strip(params, (model), bc, lengths, m_all, springs, constraints, out, out_cap) → isize

params = [terms, spaces, neigs, classify] (exactly 4). With m_all empty (length 0), every length takes the terms 1..terms, as cufsm_modes. Otherwise terms must be 0 and m_all holds one list per length, each its count then its terms: [n_1, m…, n_2, m…, …], any finite values with at least one non-zero (sorted, zeros and repeats dropped, as CUFSM's msort). spaces 0 is the free analysis; otherwise its bits together are one cFSM space the analysis is restricted to (unlike cufsm_signature, where each bit is its own column). classify 1 writes the default classification, 0 writes NaN there and skips the work. Output: one cufsm_modes row per length, back to back, each sized by its own nterms (after msort). With [terms, 0, neigs, 1] and no m_all it equals cufsm_modes, value for value.

cufsm_classify#

cufsm_classify(params, (model), bc, results, springs, constraints, out, out_cap) → isize

params = [orth, norm, ospace] in CUFSM's codes: orth 1 natural, 2 axial, 3 load; norm 0 none, 1 vector, 2 strain energy, 3 work; ospace 1 ST, 2 K⁻¹, 3 Kg⁻¹, 4 the null space. CUFSM's defaults are 2, 1, 1. orth 3 reads the nodes' stress column. results holds, per length, L, nterms, m_1 … m_nterms, k, then k modes of 4 × nodes × nterms dofs. Output: G, D, L, O percent per mode, in order: 4 × (total modes) values.

cufsm_template#

cufsm_template(params, out, out_cap) → isize

params: the 23 values of CUFSM's templatecalc: shape (1 C, 2 Z), h, b1, b2, d1, d2, r1, r2, r3, r4, q1, q2, t, nh, nb1, nb2, nd1, nd2, nr1, nr2, nr3, nr4, centerline. h, b1, b2, t positive; lips and radii zero or positive; q in degrees; strip counts whole (nh, nb1, nb2 at least 1); centerline 1, or 0 for outside dimensions and inside radii. Output: nnodes, nelems, then the nodes (7 values each) and elements (4 each) in this interface's own layouts, 0-based, every node free, stress 1, material 0: ready to pass back in. out must hold 2 + 7 × (s + 1) + 4 × s values, s the sum of the nine strip counts.

cufsm_props_wn#

cufsm_props_wn((model), out, out_cap) → isize

cufsm_props' 15 values, then the warping function wn at each node: 15 + nodes values.

cufsm_signature_lengths#

cufsm_signature_lengths((model), out, out_cap) → isize

The 100 half-wavelengths of CUFSM's signature_ss.m for the model.

cufsm_signature_minima#

cufsm_signature_minima(curve, out, out_cap) → isize

curve: L, λ pairs (a NaN λ marks a length with no positive load factor; lengths positive, λ not infinite). Output: L, λ per interior local minimum, shortest first, refined by a parabola in log length: at most curve_len − 4 values.

Calling it from JavaScript#

The whole glue is WebAssembly.instantiate and typed arrays over the module's memory: allocate each input, write it, call, copy the output out, and free everything on every path. The app's own wrapper does exactly this. This example reads the section properties of a 100 × 50 × 2 mm channel (centreline dimensions, mm and MPa) in a browser module or in Node:

const bytes = await (await fetch('cufsm.wasm')).arrayBuffer();   // Node: fs.readFileSync('cufsm.wasm')
const { instance } = await WebAssembly.instantiate(bytes, {});
const e = instance.exports;
if (e.cufsm_abi_version() !== 2) throw new Error('not ABI 2');

const mats = [200000, 200000, 0.3, 0.3, 200000 / 2.6];                // ex, ey, vx, vy, g
const nodes = [[50, 0], [0, 0], [0, 50], [0, 100], [50, 100]]
  .flatMap(([x, z]) => [x, z, 1, 1, 1, 1, 1]);                        // x, z, four free flags, stress
const elems = [0, 1, 2, 3].flatMap((k) => [k, k + 1, 2, 0]);          // node_i, node_j, t, mat

const held = [];
const put = (values) => {                                             // an f64 buffer in the module
  const p = e.cufsm_alloc(values.length * 8);
  if (!p) throw new Error('out of memory');
  held.push([p, values.length * 8]);
  new Float64Array(e.memory.buffer, p, values.length).set(values);
  return p;
};
try {
  const args = [mats, nodes, elems].flatMap((v) => [put(v), v.length]);  // each pointer, then its length
  const cap = 15;
  const out = e.cufsm_alloc(cap * 8);
  held.push([out, cap * 8]);
  const n = e.cufsm_props(...args, out, cap);
  if (n < 0) {
    const msg = new Uint8Array(e.memory.buffer, e.cufsm_last_error_ptr(), e.cufsm_last_error_len());
    throw new Error(new TextDecoder().decode(msg));
  }
  const [A, xcg, zcg, Ixx] = new Float64Array(e.memory.buffer, out, n);  // read before the next call
  console.log({ A, xcg, zcg, Ixx });
} finally {
  for (const [p, size] of held) e.cufsm_dealloc(p, size);
}

Read the output before the next call: a call can grow the module's memory, which replaces e.memory.buffer and detaches views made earlier. From C, link the crate built natively with the ffi feature and call the same functions; the signatures are in the crate's src/ffi.rs, which also documents every layout.

CivilKit Buckling runs cufsm-rs, a Rust port of CUFSM, the finite strip software by Benjamin W. Schafer and co-workers (constrained FSM with Sándor Ádány), compiled to WebAssembly. An analysis tool, not a design certificate: check what you use.