A thin, curated, WebAssembly-native finite element analysis engine, written
in Rust, covering the subset of structural element formulations, material
models, and analysis types actually used in practice by
pysees, Carapace's companion frontend. It's designed from day
one to run efficiently inside a browser Web Worker as a .wasm module.
Carapace is heavily inspired by OpenSees / Xara but is not a port of either — it's a from-scratch reimplementation.
Both a 2D (ux, uy, rz) and 3D (ux, uy, uz, rx, ry, rz) execution
profile exist side by side as one generic implementation (const-generic
over NDIM/NDOF), not a hand-duplicated second engine — see each
element/section entry below for which profile(s) it covers. The structure and
how to extend it are described in docs/architecture.md.
Elements
- Truss (2D + 3D)
- ZeroLength — per-DOF materials and Coulomb friction coupling (2D + 3D), with optional
OpenSees-style
-orientlocal axes (default: global axes) - ElasticBeamColumn — axial, bending, torsion,
Linear/PDeltatransforms (2D + 3D) - DispBeamColumn — fiber-discretized, displacement-based (2D uniaxial, 3D biaxial)
- Tri3 (constant strain) and Quad4 (full 2x2 or enhanced incompatible-mode)
plane-stress/plane-strain continuum elements (2D), with isotropic and orthotropic
PlaneMaterials, body, edge-traction and edge-pressure loads, and Gauss-point recorders - Shell4 (MITC4 flat shell) and Shell3 (DKT/Allman flat triangle),
ElasticMembranePlatesection; 3D: nodal, pressure and self-weight loads, static and modal (lumped mass), resultant recorders; match OpenSeesShellMITC4/ShellDKGT - Shell stiffness modifiers (ETABS-style
f11 f22 f12 m11 m22 m12 v13 v23) onElasticMembranePlate, scaling the individual terms of the sectionD. Breaks OpenSees compatibility except in the uniform case (see../pysees/docs/shell-plan.md). - ZeroLengthSection — fiber section with optional independent springs (2D + 3D)
- ForceBeamColumn — fiber-discretized, force-based/flexibility method (2D uniaxial, 3D biaxial)
- 2D co-rotational transform (large-displacement geometry)
- 3D co-rotational transform — scoped, deliberately shelved: unlike planar
rotations (a single commuting scalar angle), 3D rotations don't commute, so
an objective large-rotation formulation needs persistent per-element
quaternion-tracked nodal state (updated at
commit, not just aCorotational2d-style pure function of current position) plus a dense, easy-to-mis-sign analytic geometric-stiffness Hessian (Xara/OpenSees'sCorotCrdTransf3d— itsTmatrix and fiveksigma1–ksigma5blocks). More complexity than currently justified; revisit only if a real model needs large 3D frame rotations. - Nonlinear torsion (3D frame torsion is a decoupled elastic
G*Jterm today, not fiber-derived)
Materials
- Elastic
- ElasticPP (elastic-perfectly-plastic, with permanent set)
- Gap (one-sided gap) / Ent (no-tension)
- Steel01 (bilinear kinematic + isotropic hardening)
- Steel02 (Menegotto-Pinto smooth transition curve)
- Concrete01 (Kent-Scott-Park envelope, no tension)
- Concrete02 (Concrete01 + linear tension softening)
- Hysteretic (multi-linear pinching + damage backbone)
- Pinching4 (four-point pinching + independent cyclic-damage rules)
- Parallel / Series / MinMax (material combinators — composite stress/strain coupling and strain-bound failure wrapping over any of the above)
Analysis
- DOF activation: a node DOF is an equation only if an element stiffens it, a constraint uses it as a master, it carries a mass, or it is fixed — truss-only nodes need no rotations fixed by hand, and a nodal load on a DOF nothing resists is reported as a model error instead of silently dropped
- Static analysis: load control, displacement control;
LinearandNewton(full/modified/initial-tangent, optional line search) algorithms - Arc-length continuation (
Integrator::ArcLength): scaled spherical constraint, coupled displacement/load-factor Newton through load limits, softening and snap-back, adaptive radius with transactional cutbacks, stop criteria with exact landing, bifurcation diagnostics (design and verification indocs/arclength.md) -
ConvergenceTest::Combined: per-equation force/moment equilibrium, with every Newton convergence check made at the accepted (post-update) state - Multi-phase/staged analysis with frozen (held-constant) load patterns
- Modal analysis (Lanczos eigensolver)
- Transient analysis (Newmark-β, Rayleigh damping, ground motion),
including a Newton corrector (
TransientAnalysis::with_algorithm) for nonlinear response driven dynamically —Algorithm::Linear(one effective-system solve per step, exact only for linear-material response) remains the default -
TangentStrategy(current/reuse-at-step-start/initial-tangent), optionalLineSearch(bisection/regula falsi), andKrylovNewton(stale-tangent + Krylov-subspace acceleration) — one shared iteration driver reused by both static and transient analysis (design indocs/algorithms.md§0–§6.1) - Sparse direct solver, with tangent-factorization caching
(
SparseSolver::factor/SparseFactorization) reused across iterations byTangentStrategy::ReuseAtStepStart/Initial - Event-to-event stepping (design in the archived algorithm design §9–§17, not yet implemented)
Constraints
-
equal_dof(2D + 3D) - General linear multi-point constraints (
u_slave = sum coeff * u_master, chains resolved) andrigid_link(full rigid-body coupling with lever arm), 2D + 3D - Rigid diaphragm — 2D (identity alias) and 3D (true affine constraint with lever-arm coupling, not identity aliasing)
wasm / browser boundary
-
CarapaceInputV1wire format: input decoding plus a steppedSession/advanceAPI, budget-driven for cooperative cancellation -
wasm_bindgenboundary exposingdecodeInput/advanceas JS objects and arrays viaserde-wasm-bindgen, with generated TypeScript definitions - Zero-copy transferable-typed-array wire format for
postMessage(the wasm→JS leg still converts to JS objects; the analysis-worker→storage-worker leg, entirely on thepyseesside, is already a transferredArrayBuffer) - Configurable
TangentStrategy/LineSearch/KrylovNewtonthroughCarapaceInputV1, for static and transient stages, using tagged algorithm objects. Transient stages default to linear analysis whenalgorithmis omitted. See the JS solver configuration guide. - Arc-length stages (
integrator: { kind: "arcLength", ... }) andcombinedconvergence throughCarapaceInputV1, with per-step continuation diagnostics inStepOutcome.continuation
Results / persistence — see
docs/results-storage-indexeddb.md for
the full design (IndexedDB-based, implemented on the pysees side, not
SQLite/OPFS)
- Bounded, per-
advance()-call recorder batches — the solver never retains a whole-run history in memory - IndexedDB storage worker: dense run-wide row layout, idempotent writes, interrupted-run reconciliation on reload
- Analysis-worker ↔ storage-worker
MessageChannelwiring with backpressure (bounded in-flight-byte budget) - Engine recorders: node displacement, velocity/acceleration, reactions, element local forces, and fiber stress/strain (2D + 3D)
- Engine modal frequencies/shape components and transient time histories through per-call recorder batches
- pysees compiler/storage integration for recorders beyond displacement, modal/transient stages, and spatial models. The engine supports these; the frontend compiler currently emits planar static displacement runs only.
- Results UI beyond a minimal debug panel — paged queries work, but nothing yet consumes them for real plots or deformed-shape scrubbing
- Saved-run browsing, delete, and export UI (
deleteRunandqueryResultsbackend calls exist; export and a run-management UI remain unimplemented)
Superseded planning docs (the original milestone-by-milestone implementation
plan, the 3D-profile architecture rationale, the pysees handoff contract,
algorithm and friction derivations, and a rejected Turso storage alternative) live in
docs/obsolete/ — useful for the reasoning behind past
decisions, no longer maintained as living specs.
carapace/
├── core/ # carapace-core: all FE logic. No wasm-bindgen dependency —
│ compiles and tests identically on native and wasm32 targets.
├── wasm-bridge/ # carapace-wasm: input decoding, session API, and JS adapter
└── docs/
├── architecture.md # domain model, element interface, how to extend
├── algorithms.md # current solver API and JS configuration
├── arclength.md # arc-length continuation design record
├── results-storage-indexeddb.md # current results-persistence design (pysees side)
└── obsolete/ # superseded planning docs, kept for history
# native (fast iteration, real debuggers, no browser/Node round-trip)
cargo test --workspace
# one-time tool setup (wasm-bindgen must match Cargo.lock)
cargo install wasm-bindgen-cli --version 0.2.128 --locked
# build the shipped wasm: JS glue, TypeScript types and binary in pkg/ (loads under Node and browsers)
scripts/build-wasm.sh
# check the generated package through its application API (Node 22.18+)
node wasm-bridge/tests/boundary-smoke.ts
# check the generated declarations and their use by the smoke test
npm exec --package=typescript -- tsc --strict --noEmit --target es2022 --lib esnext,dom \
--module nodenext wasm-bridge/tests/boundary-smoke.tsThe build is --target web (ES modules). A browser or worker initializes the module with the default
init() export before calling decodeInput; under Node, comparison/load_wasm.mjs loads the same files with
initSync.
Always verify a change on the native target first, wasm32 second — it isolates logic bugs from platform/toolchain bugs faster than debugging inside a wasm runtime directly.
scripts/build-wasm.sh generates everything in pkg/, including carapace_wasm.d.ts. Do not edit
generated files.
The application interface is decodeInput(input) → WasmSession, with
advance(stepBudget) → StepOutcome and currentStageId() methods.
Wire-format types in wasm-bridge/src/input_v1/ derive tsify::Tsify alongside
Serde, so fields, camelCase names, and tagged enums generate TypeScript
definitions from the Rust source. The adapter uses Ts<T> to connect those
definitions to function signatures, with explicit fallible conversion inside
each call. Malformed inputs still throw an error, and decode errors retain
their structured { kind, ... } shape (also exported as DecodeError).
The current payload uses ordinary JS objects and arrays; these declarations
do not describe the planned transferable typed-array format.
The full model/input format (tables, materials, stages, recorders, results, and errors) is documented in docs/input-format.md.
When extending the engine, update the Rust input types and decoder as needed, then rebuild to regenerate the JS types. A new binding is only needed for a new operation exposed to JavaScript; adding a material or solver behind the existing session interface does not require one.
Start with the Features checklist above for what's implemented today.
For the frontend/worker boundary and results persistence, read
docs/results-storage-indexeddb.md
— the current run lifecycle, transport, and IndexedDB results-storage design
(implemented on the pysees side).
For implemented solver settings, JS examples, and remaining event-to-event
stepping design, read
docs/algorithms.md.
For historical design rationale — the original milestone plan, the
3D-profile architecture decisions, the pysees handoff contract, friction and algorithm design rationale, and a
rejected Turso-based storage alternative — see
docs/obsolete/. These are no longer maintained as
living specs; treat them as an explanation of why, not a current what.