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 itsffifeature 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
ffifeature 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#
| Export | Returns |
|---|---|
cufsm_abi_version() → u32 | 2: the buffer layouts' version. It changes only when an existing export's layout changes. |
cufsm_abi_minor() → u32 | 1: 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.
| Export | What it does |
|---|---|
cufsm_alloc(n: usize) → *mut u8 | n 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() → usize | Pointer to the last error message's UTF-8 bytes (0 when there has been none). |
cufsm_last_error_len() → usize | Its 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
outandout_cap: a buffer you allocated for the result, and how manyf64it holds. An export checks that the result fits before any work, and fails withoutput buffer is too small: N values needed, M givenotherwise. - The return value is the number of
f64written, or -1 on failure, whencufsm_last_error_ptrandcufsm_last_error_lenhold 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 (aRuntimeErrorin 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.
| Buffer | Values per row | Columns |
|---|---|---|
mats | 5 | ex, ey, vx, vy, g. Ex, Ey and G positive, vx·vy below 1. |
nodes | 7 | x, 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. |
elems | 4 | node_i, node_j, t, mat: 0-based node rows, thickness above 0, 0-based material row. |
springs | 9 (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). |
constraints | 5 (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 θ. |
bc | bytes | S-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. |
lengths | 1 | Each 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.