← Carapace

Test report

What the test suite verifies, and whether it passes. Commit a607ac2, 2026-10-13.

388tests
387passing
0failing
1ignored

Integration tests

analysis54 tests

Static analysis: solvers, algorithms, load patterns, displacement control, arc-length.

algorithms · 5
ok
Krylov newton converges with far fewer factorizations than full newtonkrylov_newton_converges_with_far_fewer_factorizations_than_full_newton

Algorithm::KrylovNewton (stale Initial tangent + acceleration) converges to the same equilibrium as full Current-tangent Newton, while factoring far less often than once per iteration — the whole point of accelerating a stale tangent instead of re-forming it.

ok
Tangent strategy current factors once per iterationtangent_strategy_current_factors_once_per_iteration

TangentStrategy::Current re-forms and factors the tangent every iteration. On a step that crosses yield (elastic-perfectly-plastic spring parallel to a truss) the answer matches the closed-form displacement, the factorization count equals the iteration count, and more than one iteration is needed.

ok
Line search converges to the same answer as plain newtonline_search_converges_to_the_same_answer_as_plain_newton

LineSearch must not change the converged answer versus plain Newton — only how it gets there. Both variants checked against the same Current-tangent, no-line-search baseline.

ok
Tangent strategy reuse at step start refactors once per steptangent_strategy_reuse_at_step_start_refactors_once_per_step

TangentStrategy::ReuseAtStepStart: factors once per step() call (unlike Initial's once-ever), so a second step gets its own fresh factorization at the state that step started from — distinguishing this from tangent_strategy_initial_never_refactors_across_multiple_steps above is the whole point of these being two different enum variants.

ok
Tangent strategy initial never refactors across multiple stepstangent_strategy_initial_never_refactors_across_multiple_steps

TangentStrategy::Initial: forms the tangent once, from the model's very first (undeformed, elastic) state, and never re-factors again for the rest of the Analysis's lifetime — proven here across two separate step() calls, not just within one, since docs/algorithms.md §3.1 specifically calls out "once ever," not "once per step" (TangentStrategy::ReuseAtStepStart is the once-per-step variant). Converges to the same answer as Current (linear convergence rate, more iterations, but the same equilibrium point), while never paying for a second factorization.

arclength_continuation · 19

docs/arclength.md §9: arc-length acceptance tests beyond the degrading-strength fixtures — elastic closed forms in both profiles, an exactly singular tangent, smooth snap-back, geometric snap-through, bifurcation diagnostics, stop criteria, auto-scaling, cutbacks and rollback, frozen loads, constraints, scale invariance and invalid configurations.

ok
Auto scale with singular first tangent fails initializationauto_scale_with_singular_first_tangent_fails_initialization

With ArcScales::Auto the displacement scale is derived from the first tangent. Starting exactly at the peak of a softening spring the tangent is singular, so no scale can be derived and the first step fails with SingularSystem rather than continuing with a bad scale.

ok
Chord and step caps and reopening with new criteriachord_and_step_caps_and_reopening_with_new_criteria

Stop criteria: a maximum chord length ends the phase after the expected number of steps, and reopening the phase with new criteria (a step cap of 3) keeps the direction and chord history and stops after exactly three more steps.

ok
Domain mut discards history and revalidatesdomain_mut_discards_history_and_revalidates

Mutating the domain between steps (here adding a constant load) discards the continuation history and re-validates the entry state; the broken equilibrium is reported as InitialStateNotInEquilibrium.

ok
Elastic closed form planar needs no corrector and honours directionelastic_closed_form_planar_needs_no_corrector_and_honours_direction

On a linear spring the arc-length path is the straight line lambda = k*u, so each step advances by the closed-form increment dlambda = s / sqrt((1/(k u*))^2 + (1/lambda*)^2), in both the increasing and decreasing directions. The predictor is already exact, so no corrector solves are needed, the determinant sign stays positive and no bifurcation is flagged.

ok
Elastic closed form spatialelastic_closed_form_spatial

3D (Domain3) counterpart of the planar elastic closed-form test: a linear ZeroLength3 spring continued by arc-length follows the exact straight path lambda = k*u.

ok
Auto scale derives the hand computed scale and reproduces the pathauto_scale_derives_the_hand_computed_scale_and_reproduces_the_path

The automatic displacement scale equals the hand-computed u* = lambda* p / K (0.01 here), and a run with ArcScales::Auto reproduces the path of the same run with that scale given explicitly.

ok
Displacement target lands exactly and further steps are refuseddisplacement_target_lands_exactly_and_further_steps_are_refused

A displacement stop target with exact: true on the softening spring lands exactly on the target (u = 0.02, load factor 6.0, zero overshoot), and further steps after the stop are refused without changing the state.

ok
Equal dof reduced coordinates match the equivalent single node modelequal_dof_reduced_coordinates_match_the_equivalent_single_node_model

Two parallel degrading springs on nodes tied with equal_dof are equivalent to one spring of twice the strength on a single node: the two arc-length paths agree, so continuation works in the constraint-reduced coordinates.

ok
Exactly singular tangent needs a seed and then continues past the peakexactly_singular_tangent_needs_a_seed_and_then_continues_past_the_peak

Starting in equilibrium exactly at the peak of a softening spring (tangent exactly zero, closed-form peak force k*a/e), continuation needs a seed direction and then continues past the peak along the softening branch.

ok
Determinant sign change without load reversal flags a bifurcationdeterminant_sign_change_without_load_reversal_flags_a_bifurcation

A perfectly straight corotational cantilever under axial compression: the primary (straight) path keeps loading through the Euler load, where one eigenvalue of K crosses zero. lambda never reverses, so the determinant sign change must be flagged as a suspected bifurcation.

ok
Invalid entry states and load definitions are rejectedinvalid_entry_states_and_load_definitions_are_rejected

Entry validation: a model that is not in equilibrium, an active Path load series, and a model with no continued load are each rejected with a structured error instead of producing results.

ok
Frozen gravity stays constant while lateral load is continuedfrozen_gravity_stays_constant_while_lateral_load_is_continued

Gravity is applied under load control and frozen with hold_pattern_constant, then the lateral pattern is continued by arc-length. The frozen gravity displacement must stay constant while only the lateral load factor evolves.

ok
Invalid options and unsupported combinations are rejectedinvalid_options_and_unsupported_combinations_are_rejected

Every invalid arc-length option (non-finite scales, bad radii or tolerances) and unsupported algorithm/convergence-test combination is reported as InvalidOption naming the offending field.

ok
Exhausted cutbacks restore everything and leave history untouchedexhausted_cutbacks_restore_everything_and_leave_history_untouched

A step that fails after exhausting its cutbacks (an unreachable tolerance on the step that crosses the kink) restores the domain and leaves the continuation history untouched, so retrying with a valid tolerance reproduces the clean reference run.

ok
Oversized radius cuts back and the accepted path stays on the oracleoversized_radius_cuts_back_and_the_accepted_path_stays_on_the_oracle

On a smooth exponential-softening spring with only two corrector solves allowed per step, an oversized radius needs cutbacks. Every step still converges, the radius stays within its bounds, and each accepted point lies on the exact force-displacement curve.

ok
Load factor target and zero crossingload_factor_target_and_zero_crossing

Load-factor stop criteria on an elastic spring: an exact target (lambda = 1) is landed on, then reversing direction restarts the history at lambda = 1 and the zero-crossing criterion ends the phase.

ok
Unit and reference load changes with matching scales recover the same pathunit_and_reference_load_changes_with_matching_scales_recover_the_same_path

The path is invariant to units and reference-load scaling: the same degrading spring in millimetres with a reference load of 4 and matching arc scales recovers the metre/unit-load path after rescaling.

ok
Smooth snap back through nonsingular displacement turning pointssmooth_snap_back_through_nonsingular_displacement_turning_points

A smooth softening spring in series with a stiffer linear one gives snap-back: displacement turns back while the tangent stays nonsingular. With both the secant and tangent predictors the path is traced through the turning points, with the load decreasing and then recovering.

ok
Geometric snap through matches the two bar oraclegeometric_snap_through_matches_the_two_bar_oracle

Shallow two-bar (von Mises) truss of corotational elastic members, pinned at the supports and at the apex (two coincident apex nodes tied in translation by equal_dof, so each bar is pin-ended and carries no moment), apex x fixed as the symmetry constraint. Oracle: P(v) = 2 EA (L0 - L)/L0 * (h - v)/L, L = sqrt(b^2 + (h - v)^2).

arclength_softening · 4

docs/arclength.md §8: arc-length continuation through a degrading- strength peak (one Hysteretic spring) and through an actual snap-back (the same spring in series with an elastic one), against closed-form backbone oracles at every accepted step.

ok
Degrading spring is traced past peak strengthdegrading_spring_is_traced_past_peak_strength

Arc-length continuation traces a degrading-strength spring through its peak and down the softening branch. Every accepted point lies on the independent backbone curve (lambda = backbone(u)), the support reaction balances the applied load, and the load decreases after the peak.

ok
Load control fails past the peak and rolls back exactlyload_control_fails_past_the_peak_and_rolls_back_exactly

Baseline for the arc-length tests: load control converges before the peak but fails to converge past it, and the failed step rolls the node displacement back exactly.

ok
Reverse initial direction follows the negative backbonereverse_initial_direction_follows_the_negative_backbone

Starting arc-length in the decreasing direction follows the mirror-image negative backbone: displacement decreases monotonically and every point satisfies lambda = -backbone(-u).

ok
Series spring snap back is traced with decreasing load and displacementseries_spring_snap_back_is_traced_with_decreasing_load_and_displacement

The degrading spring in series with a stiff elastic spring produces a true snap-back (both load and displacement decrease after the peak). Arc-length continuation traces it, including the step that crosses the backbone kink.

displacement_control · 1
ok
Displacement control converges across yield with each tangent strategydisplacement_control_converges_across_yield_with_each_tangent_strategy

A yielding ground spring in series with an elastic spring exercises both the controlled DOF and an unconstrained equilibrium correction. The post-yield load sensitivity differs from the initial one.

dof_activation · 10

DOF activation: a node DOF is an equation only if an element stiffens it, a constraint uses it as a master, it carries a nodal mass, or the user fixed it. Models therefore no longer need to fix the DOFs an element type never uses (a truss's rotations), and a load on a DOF nothing resists is reported instead of silently dropped.

ok
A nodal load on a dof nothing stiffens is a model errora_nodal_load_on_a_dof_nothing_stiffens_is_a_model_error

A nodal load on a DOF that no element stiffens (a moment on a truss-only node) is a model error, LoadOnInactiveDof, reported by validate and returned as a value by try_build.

ok
Arc length needs no rotation scale when rotations are inactivearc_length_needs_no_rotation_scale_when_rotations_are_inactive

Arc length needs a rotation scale only when the model has rotational unknowns; with the rotations inactive it must not demand one.

ok
Build panics on an invalid modelbuild_panics_on_an_invalid_model

build panics on a model that fails validation (a moment applied to a truss-only node), since that is a model-construction error rather than a runtime failure.

ok
Constraint masters are active without any elementconstraint_masters_are_active_without_any_element

A diaphragm's retained node typically has no element at all; the DOFs the constraint ties to must still be equations.

ok
Fixed dofs are active even when nothing stiffens themfixed_dofs_are_active_even_when_nothing_stiffens_them

Explicit fixity wins: fixing a DOF that no element uses (a truss node's rotation) is legal, keeps the DOF active, and gives it no equation number.

ok
Modal analysis of a truss needs no fixed rotationsmodal_analysis_of_a_truss_needs_no_fixed_rotations

A modal analysis of a 2D truss SDOF with its rotations left free works and gives the closed-form frequency sqrt(k/m), with k = EA/L and m = rho*A*L/2.

ok
Nodal mass activates its dofnodal_mass_activates_its_dof

A nodal mass is inertia even with no stiffness, so it activates its DOF (a free mass is legal in a transient analysis); a mass on a stiffened DOF is unchanged.

ok
Joint of beam and truss keeps rotation only where a beam needs itjoint_of_beam_and_truss_keeps_rotation_only_where_a_beam_needs_it

A beam shares a node with a brace truss: the joint's rotation is an unknown (the beam stiffens it) while a truss-only node has none.

ok
Spatial truss needs no fixed rotationsspatial_truss_needs_no_fixed_rotations

A 3D skew truss with no rotations fixed solves, with only the translational DOFs as unknowns.

ok
Truss only model needs no fixed rotations and matches the fixed modeltruss_only_model_needs_no_fixed_rotations_and_matches_the_fixed_model

A truss-only 2D model solves without fixing rotations: only the 3 translational DOFs become equations, and the displacements are identical to the same model with rotations fixed by hand.

cyclic_protocol · 1
ok
Set integrator drives a cyclic protocol with permanent set at zero displacementset_integrator_drives_a_cyclic_protocol_with_permanent_set_at_zero_displacement

Exercises Analysis::set_integrator driving a cyclic DisplacementControl protocol — many small legs on one Analysis (no rebuild) — the mechanism a real quasi-static cyclic-loading protocol (e.g. FEMA 461/ ATC-24-style excursions) is meant to use: ramp past yield, back through zero to a negative excursion, then back to zero — and check that the material remembers it yielded (nonzero residual force at zero net displacement — the hysteresis/permanent-set signature a path-independent material could never produce).

load_patterns · 3

Load patterns: frozen patterns across phases and Path series walked by LoadControl.

ok
Load control walks a path series pattern through its prescribed load factorsload_control_walks_a_path_series_pattern_through_its_prescribed_load_factors

A LoadSeries::Path pattern driven by Integrator::LoadControl walking pseudo-time along the path's own control points — the load-controlled equivalent of Xara's dedicated Integrator::LoadPath (a StaticIntegrator that walks a prescribed load-factor vector one entry per step, see SRC/analysis/integrator/Static/LoadPath.h): no separate Integrator variant needed here, since a Path series already is a prescribed load-factor-vs-pseudo-time path, and LoadControl already knows how to step pseudo-time forward by a fixed amount each step.

More

A single elastic Truss makes this an exact, closed-form check at every step (u = F/k, k = E*A/L), not just a "did it run" smoke test — the load factor at pseudo-time t is exactly path.factor(t) (piecewise- linear interpolation over the control points), so the expected displacement at each step is computable directly from the same control points the test hands to the pattern.

ok
Frozen gravity pattern survives a second phase untouchedfrozen_gravity_pattern_survives_a_second_phase_untouched

A cantilever beam is loaded axially by a gravity pattern, which is then frozen, and a separate lateral pattern is applied in a second phase. The axial displacement from gravity must be nonzero after phase one and unchanged by the lateral phase, since the frozen pattern no longer ramps.

ok
Pdelta softening holds across a frozen gravity phase boundarypdelta_softening_holds_across_a_frozen_gravity_phase_boundary

Same two-phase (frozen gravity → lateral) protocol, run once with GeomTransf::Linear and once with PDelta: with zero gravity the two must match exactly (no axial force means PDelta's geometric-stiffness correction vanishes), and with compressive gravity PDelta must soften the beam relative to Linear — the same physical checks elements/elastic_beam.rs makes within a single Analysis, but here driven by two genuinely separate analysis phases sharing one evolving Domain.

newton · 3
ok
Displacement control recovers the load that produces the target displacementdisplacement_control_recovers_the_load_that_produces_the_target_displacement

Integrator::DisplacementControl on the truss case: prescribing the same target displacement (rather than the equivalent load) should drive the analysis to the same load factor the linear truss test verifies directly (P=50).

ok
Newton raphson resolves elastic perfectly plastic regime crossingnewton_raphson_resolves_elastic_perfectly_plastic_regime_crossing

Newton-Raphson resolves a step that crosses a regime change inside the step, where a one-shot linear solve cannot. An elastic truss in parallel with an elastic-perfectly-plastic spring, loaded past yield, must give the closed-form displacement u = (F - fy) / k_truss.

ok
Norm disp incr and energy incr also converge to the same resultnorm_disp_incr_and_energy_incr_also_converge_to_the_same_result

The displacement-increment and energy-increment convergence tests reach the same equilibrium as the unbalance-norm test on the yielding parallel system: the closed-form u = (F - fy) / k_truss.

prescribed_displacement · 5
ok
Committed history persists across targetscommitted_history_persists_across_targets

Prescribed-displacement steps keep committed material history. An elastic-perfectly-plastic probe is driven past +yield, then past -yield, then back to -0.0005; the force must be +100, -100 and then +50 (the elastic unloading branch from the permanent set), where a path-independent material would give -50.

ok
Elastic probe follows prescribed strainelastic_probe_follows_prescribed_strain

The uniaxial probe (a zero-length spring with one prescribed DOF and no free DOFs) follows a prescribed strain history on an elastic material: displacement equals the target and force equals E times strain, going through zero and negative strains.

ok
Invalid targets change nothinginvalid_targets_change_nothing

Invalid prescribed targets (NaN, infinity, or a DOF that is free) are rejected with InvalidConstraint and leave the previous displacement untouched.

ok
Free dofs respond to a support displacementfree_dofs_respond_to_a_support_displacement

Support settlement: two equal-stiffness trusses in series with the end node displaced by 0.5. The free middle node moves half of that, 0.25.

ok
Zero tangent branch reports zero forcezero_tangent_branch_reports_zero_force

Ent carries no tension: tangent and force are both zero at positive strain, which is exactly where DisplacementControl has nothing to divide by.

reaction · 2
ok
A shared fixed node sums every touching elements contributiona_shared_fixed_node_sums_every_touching_elements_contribution

A fixed node shared by two elements (a two-span beam continuous over its middle support) sums each element's own contribution to that node's reaction — proves reaction iterates every element touching the node, not just the first (or last) one found, by checking the fixed end's reaction against simple statics: with a point load at the free tip of a cantilever built from two collinear elements in series, the root reaction moment must equal -load * total_length regardless of where the elements are split, and the root shear must equal -load — neither of which could be right if only one of the two elements were being summed.

ok
Fixed free truss reaction exactly opposes the applied loadfixed_free_truss_reaction_exactly_opposes_the_applied_load

Domain::reaction's simplest closed-form check: a fixed-free truss pulled at its free end (same case elements/truss.rs uses for displacement) must show a reaction at the fixed support exactly opposite the applied load — global equilibrium (sum(reactions) + sum(applied loads) == 0) for a single-DOF system with only one load and one support.

sparse_solver · 1
ok
Long cantilever chain matches closed form under sparse solvelong_cantilever_chain_matches_closed_form_under_sparse_solve

A correctness + scale check for the SparseSolver swap to faer . A single 2-node ElasticBeamColumn is exact for a simply-supported UDL case; this does the equivalent check for a chain of many elements, which exercises the sparse assembly/solve path at a non-trivial system size (150 free DOFs) with a real banded/sparse pattern, rather than the single- or few-element systems every other test uses. Chaining n identical prismatic Euler-Bernoulli elements under only a tip point load (no interior element loads) is exact against the classical cantilever closed form delta = P*L^3/(3*E*I) regardless of n — cubic Hermite elements reproduce a cubic deflection field exactly, so subdividing adds no discretization error.

constraints24 tests

Equal-DOF, rigid diaphragm and general linear constraints.

diaphragm_equal_dof · 3

equal_dof and 2D rigid_diaphragm constraints.

ok
Plain handler rejects a domain with multi point constraintsplain_handler_rejects_a_domain_with_multi_point_constraints

ConstraintHandler::Plain can't resolve multi-point constraints — using it with a domain that has one is a model-construction error caught at build, not a silently-wrong solve.

ok
Equal dof couples two nodes x translation like parallel springsequal_dof_couples_two_nodes_x_translation_like_parallel_springs

equal_dof tying two otherwise-unconnected nodes' x DOF should make two separate ground-trusses act exactly like two springs in parallel on a single node (same closed form analysis/newton.rs's parallel-EPP/Truss case uses for a single shared node, but here the "sharing" is via constraint aliasing across two distinct NodeIds instead of two elements on one node) — the real thing under test is that Domain::number_dofs gives the constrained node's x DOF the same equation number as the retained node's, so both trusses' stiffness contributions land in the same row/ column of the assembled system.

ok
Rigid diaphragm couples every constrained node to the retained noderigid_diaphragm_couples_every_constrained_node_to_the_retained_node

rigid_diaphragm ties every constrained node's x DOF to the retained node's — same aliasing mechanism as equal_dof, exercised across more than one constrained node at once (the 2D-frame simplification: only x-translation is tied, not rotation/lever-arm kinematics — see Domain::rigid_diaphragm's doc comment).

linear · 17

General linear multi-point constraints: u_slave = sum(coeff * u_master) with masters on any nodes. Covers the generated kinds (equal_dof, rigid diaphragms, rigid links), arbitrary coefficients, chains of any depth, normalization, every structural error, the refusal to prescribe a constraint master, and reading slave motion back out of free-DOF vectors.

ok
A diaphragm can hang off a slavea_diaphragm_can_hang_off_a_slave

A rigid diaphragm whose retained node is itself a slave of another rigid link (a chained diaphragm) used to panic.

ok
A coefficient of two is honored not treated as an aliasa_coefficient_of_two_is_honored_not_treated_as_an_alias

u_slave = 2 * u_master: a ground spring k on the slave and a load P on the master give a master stiffness of 4k, so u_m = P / (4k).

ok
A slave defined twice is rejecteda_slave_defined_twice_is_rejected

Constraining the same slave DOF twice is rejected with DuplicateSlave.

ok
A fixed slave is rejecteda_fixed_slave_is_rejected

A constraint whose slave DOF is also fixed is rejected by validate with SlaveIsFixed.

ok
A master fixed at a nonzero value is rejecteda_master_fixed_at_a_nonzero_value_is_rejected

A master DOF fixed at a nonzero initial displacement is rejected with ConstraintOnPrescribedDof, since constrained slaves cannot follow a prescribed value.

ok
Cycles and self references are rejectedcycles_and_self_references_are_rejected

Constraint cycles (A depends on B and B on A) and a slave that references itself are rejected with ConstraintCycle.

ok
An inconsistent nonzero initial slave state is rejectedan_inconsistent_nonzero_initial_slave_state_is_rejected

A slave whose initial displacement disagrees with its constraint equation is rejected with InconsistentInitialState.

ok
Mass on a slave that combines several unknowns is an error not a panicmass_on_a_slave_that_combines_several_unknowns_is_an_error_not_a_panic

Nodal mass on a slave DOF that is a combination of several unknowns (a rigid-diaphragm slave with a lever arm) cannot be lumped. validate and TransientAnalysis::new report MassOnConstrainedDof as an error instead of panicking.

ok
Planar rigid link matches the hand assembled master stiffnessplanar_rigid_link_matches_the_hand_assembled_master_stiffness

2D: springs kx, ky on a slave offset (dx, dy) from a loaded master. Master stiffness is T^T K T with u_s = [ux - theta*dy, uy + theta*dx].

ok
Equal dof chains and general constraints agreeequal_dof_chains_and_general_constraints_agree

The same tie written as equal_dof and as a general constraint solves identically, and chains resolve whatever order the constraints were added in.

ok
Mode shapes report slave motion through the constraintmode_shapes_report_slave_motion_through_the_constraint

A constrained node's motion in a mode shape comes from value_at, not from the (absent) equation of the slave: a mass on a rigid-link master with a spring on the slave moves the slave by the lever-arm kinematics.

ok
Mass on a scaled single term slave contributes coefficient squaredmass_on_a_scaled_single_term_slave_contributes_coefficient_squared

A single scaled unknown (u = c * q) can carry mass: c^2 m lands on q.

ok
Repeated masters merge and zero terms do not activate their dofrepeated_masters_merge_and_zero_terms_do_not_activate_their_dof

Repeated masters merge and zero coefficients vanish, so a term with a zero coefficient does not even activate its DOF.

ok
Prescribing a constraint master is refused and leaves state unchangedprescribing_a_constraint_master_is_refused_and_leaves_state_unchanged

A fixed DOF that is a constraint master cannot be prescribed (its slaves would not follow), including the first link of a chain, and a refused step leaves the domain untouched.

ok
Rigid translation field satisfies links diaphragms and chainsrigid_translation_field_satisfies_links_diaphragms_and_chains

A rigid translation of the whole model satisfies every constraint, so the ground-motion influence vector must read back as exactly 1 on every translation of every node — including slaves — and 0 on rotations.

ok
Zero slave state is derived from the masters and consistent state passeszero_slave_state_is_derived_from_the_masters_and_consistent_state_passes

A slave left at the default zero takes the value its masters imply (in dependency order along a chain); a consistent nonzero value is accepted.

ok
Spatial rigid link matches the hand assembled master stiffnessspatial_rigid_link_matches_the_hand_assembled_master_stiffness

3D: translational springs on an offset slave, u_s = u_m + theta x d, so the master's 6x6 stiffness is T^T K T with T = [I | -[d]x].

rigid_diaphragm_3d · 4
ok
Rigid diaphragm about z normal matches opensees rigid diaphragm equationsrigid_diaphragm_about_z_normal_matches_opensees_rigid_diaphragm_equations

rigid_diaphragm_about's explicit normal parameter, checked against Xara/OpenSees's own documented RigidDiaphragm constraint equations for their default perpDirn = 3 (z-normal, diaphragm plane x-y): Ux_c = Ux_r - Rz_r*(Yc-Yr), Uy_c = Uy_r + Rz_r*(Xc-Xr) — literally, not via this crate's general a/b/normal formula (Domain3:: rigid_diaphragm's doc comment shows the two agree, but this test doesn't rely on that derivation being right). Same kinematics-only approach as the vertical/rotation test above.

ok
Rigid diaphragm never couples vertical translation or any rotationrigid_diaphragm_never_couples_vertical_translation_or_any_rotation

Rigid-rotation invariance: a diaphragm-constrained node's out-of-plane translation (uy, never tied by rigid_diaphragm — see its doc comment: "never vertical") and every one of its rotational DOFs (likewise never tied — "never rotational") must be completely unaffected by the master node's rotation, however large. Only the two in-plane translations respond to the lever arm. This is the direct acceptance check for the "translational constraint only, never rotational, never vertical" scope the constraint is deliberately limited to.

More

Driven through TransientAnalysis::new (public API) rather than the crate-private scatter_state directly: it computes a consistent initial acceleration from equilibrium, which exercises Domain:: scatter_state's affine-derived-dof pass (apply_displacement_ increment's twin — see its doc comment) with zero stiffness coupling in play (ZeroLength3 with no material assigned on any of the DOFs under test has zero stiffness there), isolating pure kinematics. The constrained node's untied DOFs (uy, rx, ry, rz) start at distinct nonzero values via Node3::with_initial_displacement; if the diaphragm's affine transform ever touched them, they'd be overwritten to something derived from the master node's state instead of surviving unchanged.

ok
Nodal mass on a diaphragm tied translation panics rather than silently dropping inertianodal_mass_on_a_diaphragm_tied_translation_panics_rather_than_silently_dropping_inertia

Documented limitation: nodal mass directly on a rigid-diaphragm-tied translation can't be represented by the lumped mass diagonal assemble_mass_diagonal returns — a real rigid diaphragm's tributary mass there would need an off-diagonal (rotational-inertia-at-the- master) term, which this crate's dynamics don't support yet. A clear panic, not a silently-wrong lumped mass, is the intended failure mode — same "loud, not silent" reasoning GeomTransf3::local_axes's degenerate-input asserts already use. (Models that go through validate, AnalysisBuilder, modal_analysis or TransientAnalysis get the same condition as a ModelError instead; see constraints/linear.rs.)

ok
Asymmetric shear walls reproduce the standard torsional diaphragm stiffness matrixasymmetric_shear_walls_reproduce_the_standard_torsional_diaphragm_stiffness_matrix

The classical "asymmetric rigid diaphragm" acceptance case: two lateral springs ("shear walls") at different offsets from the diaphragm's master (retained) node, tied to it via Domain3::rigid_diaphragm (default normal Axis3::Y, so the diaphragm plane is x-z — see Domain3::rigid_diaphragm's doc comment for the kinematics). A lateral load applied at the master node (the diaphragm's center) still induces rotation whenever the walls' combined stiffness isn't itself centered there — exactly the mechanism the lever-arm term exists for; an identity alias (what planar rigid_diaphragm/equal_dof do) could never produce this coupling.

More

Each wall is a ZeroLength3 spring (ground → wall node) stiff only in local x (SpatialDof::Ux), attached to a diaphragm-constrained wall node offset from the master node in z. Assembling this through Domain3's general affine-constraint machinery (dof_terms's coefficient-product distribution in assemble_stiffness_triplets) produces exactly the standard rigid-diaphragm torsional stiffness matrix used throughout seismic engineering practice:

text [ sum(k_i) sum(k_i*z_i) ] [U] [P] [ sum(k_i*z_i) sum(k_i*z_i^2)] [theta] = [M]

(U = master's Ux, theta = master's Ry, z_i each wall's z offset from the master) — derived independently here from first principles (not copied from the implementation) and solved by hand (M = 0: the load is applied at the master node itself, not offset).

continuum21 tests

Plane-stress/strain Tri3 and Quad4 elements: patch tests and beam-theory benchmarks.

loads · 5

Element loads on continuum elements: accumulation, compatibility checks, consistent nodal forces (total force and pressure sign), self-weight, and a patch test driven entirely by boundary tractions.

ok
Loads accumulate field by field and expose a fixed component layoutloads_accumulate_field_by_field_and_expose_a_fixed_component_layout

ElementLoad values add and scale field by field (beam uniform, body force, edge traction, edge pressure), and expose a fixed 16-component layout via component(i), which is what element-load recorders index into.

ok
Incompatible loads are a model error before anything is accumulatedincompatible_loads_are_a_model_error_before_anything_is_accumulated

Loads that do not fit their element (beam loads on continuum elements, continuum loads on a beam, edges out of range) are reported as model errors by validate, before any load is accumulated.

ok
Several body force patterns sum with their factorsseveral_body_force_patterns_sum_with_their_factors

Body-force loads from several patterns on the same Quad4 add up with each pattern's load factor and scale: the displacement from split patterns equals that from one pattern carrying the summed load.

ok
Self weight column reaction equals the total weightself_weight_column_reaction_equals_the_total_weight

A column of n stacked quads, base fixed, self-weight as a body force: the base reaction is the total weight b t A, and the column compresses.

ok
Boundary tractions alone reproduce the exact constant stress fieldboundary_tractions_alone_reproduce_the_exact_constant_stress_field

A patch test driven entirely by boundary edge loads (no prescribed boundary displacements) reproduces the exact constant stress field, for Quad4 (full and enhanced) and Tri3.

mixed · 6

Continuum elements together with frame elements, constraints, and the dynamic and nonlinear analysis paths.

ok
Element mass on a rigid link slave is a structured errorelement_mass_on_a_rigid_link_slave_is_a_structured_error

A mass-bearing element on a rigid-link slave cannot be lumped onto a diagonal mass vector; that is a ModelError, not a panic inside the mass assembly.

ok
Modal shapes of a beam rigidly linked to a quad edge include slave motionmodal_shapes_of_a_beam_rigidly_linked_to_a_quad_edge_include_slave_motion

A rigid link between the end of a beam (master) and a Q4 corner (slave): every constrained mode shape reports the slave's motion through the constraint, equal to the master's plus the lever-arm term.

ok
Panel and beam sharing nodes stiffen in parallelpanel_and_beam_sharing_nodes_stiffen_in_parallel

A Q4 panel with a frame element along its bottom edge sharing the panel's nodes: under a uniform axial stretch (nu = 0) the stiffness is E A_panel / L + E A_beam / L exactly. Proves activation and assembly across element kinds (the beam's rotation is an equation, the panel's is not).

ok
A linear continuum model converges immediately on every solver patha_linear_continuum_model_converges_immediately_on_every_solver_path

A linear continuum model through every nonlinear path needs one corrector iteration (or none for the arc-length predictor): Newton in each tangent mode, with line search, and the arc-length integrator.

ok
Cantilever strip frequency approaches beam theory under refinementcantilever_strip_frequency_approaches_beam_theory_under_refinement

Cantilever strip: the fundamental frequency approaches Euler-Bernoulli theory from below (shear and rotary inertia lower it by about one percent at this slenderness), and the total mass is rho t A in each direction.

ok
Undamped block conserves energy under newmarkundamped_block_conserves_energy_under_newmark

Undamped free vibration of a Q4 block under Newmark average acceleration conserves 1/2 v.M.v + 1/2 u.K.u.

patch · 3

Patch tests for the continuum elements: a mesh with irregular interior nodes, boundary displacements from a linear field, interior nodes free. A converged element must reproduce the field exactly at the interior nodes and the constant stress D * strain at every Gauss point.

ok
A continuum model with no rotation fixed solvesa_continuum_model_with_no_rotation_fixed_solves

Plane elements do not stiffen the rotation DOF, so a continuum model needs no rotational fixity, and a load on that rotation is a LoadOnInactiveDof error.

ok
Tri3 constant strain patch test is exact on an irregular meshtri3_constant_strain_patch_test_is_exact_on_an_irregular_mesh

Constant-strain patch test: Tri3 elements on an irregular mesh with boundary displacements taken from a linear field must reproduce the field exactly at the interior nodes and a constant stress in every element, for plane stress, plane strain and orthotropic materials (tolerance 1e-12).

ok
Quad4 patch test is exact on a distorted mesh for both formulationsquad4_patch_test_is_exact_on_a_distorted_mesh_for_both_formulations

The same patch test for Quad4 on a distorted mesh, for both the full and enhanced formulations and for plane stress, plane strain and orthotropic materials: exact displacement field and constant stress (tolerance 1e-12).

quad4 · 7

Plain Quad4 against beam theory and the thick-cylinder (Lame) solution.

ok
Enhanced cantilever converges faster than the plain elementenhanced_cantilever_converges_faster_than_the_plain_element

Cantilever tip load against the Timoshenko tip deflection on a 10x2 mesh: the enhanced element is within 3% of the exact answer and at least 0.2 closer than the plain element.

ok
Enhanced pure bending is exact on rectangles and beats the plain elementenhanced_pure_bending_is_exact_on_rectangles_and_beats_the_plain_element

Pure bending on a rectangular mesh: the enhanced Quad4 gives the exact beam-theory tip deflection for any mesh (1, 4 or 10 elements along the span), while the plain element is too stiff, and a single plain element locks (less than 70% of the exact answer).

ok
Enhanced thick cylinder is accurate even at nu 4999enhanced_thick_cylinder_is_accurate_even_at_nu_4999

Thick cylinder under internal pressure in plane strain with Poisson's ratio 0.4999 (nearly incompressible): the enhanced element stays within 1% of the Lame solution, while the plain element locks (more than 50% error).

ok
Macneal harder results are recordedmacneal_harder_results_are_recorded

MacNeal-Harder cantilever results, recorded (not asserted to be better than the plain element): rectangles are near beam theory, distorted meshes are not, for either formulation. The numbers are regression values and are compared with OpenSees' enhancedQuad in comparison/.

ok
Thick cylinder matches lame on a coarse mesh and convergesthick_cylinder_matches_lame_on_a_coarse_mesh_and_converges

Thick-walled cylinder against the Lame closed-form radial and hoop stresses, for plane stress and plane strain. A coarse mesh is already within a few percent, and refining the mesh reduces the error by the expected factors.

ok
Cantilever tip deflection converges to timoshenko beam theorycantilever_tip_deflection_converges_to_timoshenko_beam_theory

The plain element locks in bending: with one element through the depth the tip deflection is far too small (documented, not hidden), and the ratio converges to the Timoshenko value at roughly second order under refinement.

ok
Cook membrane at nu 4999 records the plain elements lockingcook_membrane_at_nu_4999_records_the_plain_elements_locking

Nearly incompressible plane strain (nu = 0.4999) on Cook's membrane. The plain element locks completely; the enhanced element, which is not a mixed formulation and carries no volumetric-locking guarantee, is observed to behave much better here and in the thick-cylinder test. Recorded as observations (the 64x64 enhanced result is the reference), not as a property of the formulation.

dynamics17 tests

Modal and transient analysis against SDOF closed forms.

free_vibration · 8

Element mass and Newmark free vibration (undamped and Rayleigh) against SDOF closed forms, plus 3D modal and transient SDOFs.

ok
Beam element mass matches sdof closed form frequencybeam_element_mass_matches_sdof_closed_form_frequency

Same check for ElasticBeamColumn's axial mode (transverse bending DOFs are fixed here so this exercises the same k = E*A/L axial stiffness against the beam's form_mass, independent of the truss test above).

ok
Newmark undamped sdof matches closed form free vibrationnewmark_undamped_sdof_matches_closed_form_free_vibration

Zero damping (RayleighDamping::NONE) should match undamped SDOF free vibration u(t) = u0*cos(omega*t) exactly (to Newmark discretization error) — a sanity check that RayleighDamping::NONE really contributes nothing, isolated from the damped case above.

ok
Spatial truss mass matches sdof closed form frequency via modal analysis3spatial_truss_mass_matches_sdof_closed_form_frequency_via_modal_analysis3

Spatial counterpart to dynamics/free_vibration.rs's truss_element_mass_matches_ sdof_closed_form_frequency — modal_analysis/TransientAnalysis generalized to the same <NDIM, NDOF, NId, E> profile as Domain/Analysis run through the real Domain3/Analysis3 stack rather than asserted by analogy. Every non-axial DOF at the free node is fixed, so this is a genuine SDOF (k = E*A/L, m = density*A*L/2, half the truss's total mass — the fixed node's half never enters the free-DOF mass diagonal) with the exact same closed form as the planar test.

ok
Newmark rayleigh damped sdof matches closed form free vibrationnewmark_rayleigh_damped_sdof_matches_closed_form_free_vibration

Newmark + Rayleigh damping against the classical damped-SDOF free-vibration closed form. ZeroLength+Elastic (k=1) between a fixed ground and a node carrying Node::mass = 1 (m=1, so omega=1 rad/s), mass-proportional-only Rayleigh damping (alpha_m = 2*xi*omega, beta_k = 0) for an exact damping ratio xi = 5%, released from u0 = 1, v0 = 0.

More

Closed form: u(t) = exp(-xi*omega*t) * u0 * [cos(omega_d*t) + (xi*omega/omega_d)*sin(omega_d*t)], omega_d = omega*sqrt(1-xi^2).

Newmark's average-acceleration scheme is second-order accurate, not exact, so the tolerance here (1e-4) reflects real (small, at this dt/period ratio) discretization error — unlike the modal tests, which check exact closed forms to solver precision.

ok
Truss element mass matches sdof closed form frequencytruss_element_mass_matches_sdof_closed_form_frequency

Element-consistent lumped mass. A single Truss with a nonzero density, fixed at one end, has its total mass (density*area*length) split half to each node's translational DOFs (Element::form_mass) — since the fixed node's half never enters the free-DOF mass diagonal, the free node carries exactly half the truss's total mass. This is a genuine SDOF (stiffness k = E*A/L, mass m = density*A*L/2), so its fundamental frequency has an exact closed form: omega = sqrt(k/m).

ok
Spatial rayleigh damped sdof matches closed form free vibration via transient analysis3spatial_rayleigh_damped_sdof_matches_closed_form_free_vibration_via_transient_analysis3

Spatial Rayleigh-damped counterpart — same closed form and tolerance as dynamics/free_vibration.rs's newmark_rayleigh_damped_sdof_matches_closed_form_ free_vibration, run on a single spatial translational DOF (uy this time, not ux, so the two Newmark tests above don't both happen to exercise the same DOF index).

ok
Orthogonal ground motions drive independent spatial sdof responsesorthogonal_ground_motions_drive_independent_spatial_sdof_responses

"3D ground motion": two independent GroundMotions active at once, one per translational direction (ux, uz), driving one spatial mass node whose spring stiffnesses differ per direction and which has no coupling between them (independent ZeroLength3 materials per DOF — see ZeroLength3's doc comment). Because the assembled system is diagonal, each direction's response must match the same single-direction step-response closed form dynamics/ground_motion.rs already verifies (u(t) = -(ag0/omega^2)*(1-cos(omega*t))), independently and simultaneously — the spatial generalization of "ground motion in more than one direction doesn't cross-talk between DOFs".

ok
Spatial zero length sdof matches closed form free vibration via transient analysis3spatial_zero_length_sdof_matches_closed_form_free_vibration_via_transient_analysis3

Spatial counterpart to dynamics/free_vibration.rs's newmark_undamped_sdof_ matches_closed_form_free_vibration — TransientAnalysis3 (Newmark stepping through Domain3) against the same undamped free-vibration closed form, u(t) = u0*cos(omega*t), released from rest at u0=1. Every DOF but ux at the mass node is fixed, isolating one spatial translational DOF exactly like the planar SDOF case.

ground_motion · 3
ok
Constant ground acceleration matches undamped step response closed formconstant_ground_acceleration_matches_undamped_step_response_closed_form

Undamped SDOF (m=1, k=1, omega=1), at rest, under a constant ground acceleration ag0 starting at t=0 (LoadSeries::Constant — factor(t) = 1.0 for every t, so GroundMotion::acceleration is just ag0 throughout). Standard step-response closed form (e.g. Chopra): u(t) = -(ag0/omega^2) * (1 - cos(omega*t)). Same closed-form-vs-Newmark comparison style as dynamics/free_vibration.rs's newmark_undamped_sdof_matches_ closed_form_free_vibration, but forced (nonzero right-hand side) rather than free vibration — isolates that GroundMotion's effective force term (-M*ι*ag(t)) is wired correctly into both the initial- acceleration equilibrium and every Newmark step's f_eff.

ok
Ground motion matches an equivalent mass proportional load patternground_motion_matches_an_equivalent_mass_proportional_load_pattern

GroundMotion's effective force (-M*ι*ag(t), mass-proportional) must be mathematically identical to applying an ordinary nodal LoadPattern whose reference load at the mass DOF is -m and whose series is ag(t) directly (load(t) = series.factor(t) * (-m) = -m*ag(t)) — the same physics, two different code paths (GroundMotion's direction_incidence vs. an ordinary pattern). Checked here with a LoadSeries::Path (piecewise-linear — an accelerogram shape), rather than re-deriving a closed form for a forced-vibration response to a nontrivial pulse: if both paths agree to near machine precision at every step, GroundMotion is correctly wired.

ok
Beam with massless rotation matches condensed sdofbeam_with_massless_rotation_matches_condensed_sdof

A cantilever's massless rotation must satisfy equilibrium through the Newmark effective stiffness, without adding artificial inertia. Compared step by step with the condensed SDOF (k = 3EI/L^3), under the same ground motion, to absolute tolerance 1e-10.

modal · 4
ok
Cantilever with massless rotation matches closed formcantilever_with_massless_rotation_matches_closed_form

Rotations and axial DOFs with no assigned mass are statically condensed, not an error: a cantilever column with a lumped tip mass in the transverse direction only has omega = sqrt(3 E I / (m L^3)) exactly (the elastic beam-column's stiffness is the Euler-Bernoulli one).

ok
Mass spring chain matches golden ratio closed formmass_spring_chain_matches_golden_ratio_closed_form

Eigenvalues of a small known model match closed form. This is the classic 2-DOF "1-1-1-1" mass-spring chain (unit masses, unit spring stiffnesses in series): ground -k1- m1 -k2- m2, giving K = [[2,-1],[-1,1]], M = I. Its natural frequencies have an exact closed form in the golden ratio: omega = 1/phi and omega = phi, where phi = (1+sqrt(5))/2 — from the characteristic polynomial lambda^2 - 3*lambda + 1 = 0.

ok
Massless free dof is reported as a singular systemmassless_free_dof_is_reported_as_a_singular_system

A node with a free DOF but no assigned mass makes the generalized eigenproblem singular (physically: a massless DOF has infinite natural frequency) — modal_analysis should report that, not silently divide by zero.

ok
Requesting fewer modes than free dofs returns the lowest onesrequesting_fewer_modes_than_free_dofs_returns_the_lowest_ones

Asking for fewer modes than the model has free DOFs is the whole point of the Lanczos solver — a dense full-spectrum eigensolve can't do partial-spectrum extraction at all. A 5-mass chain (5 free DOFs) asking for just the lowest 2 modes should return exactly those 2, still matching this chain's true lowest frequencies (checked against a num_modes = 5 full solve of the same system).

transient_corrector · 2
ok
Newton corrector gives a different displacement than algorithm linear once yielding is engaged mid stepnewton_corrector_gives_a_different_displacement_than_algorithm_linear_once_yielding_is_engaged_mid_step

The actual new capability: Algorithm::Linear forms its one effective tangent at step start — still fully elastic here, since the spring hasn't yielded yet at u_n — and never re-checks it for the rest of the step, even once the true (trial) state has crossed the ElasticPP spring's yield breakpoint. The resisting force element_local_force reads back is always evaluated through the material's own real law (Material::trial_stress_tangent, a pure function of committed history + current displacement, independent of which Algorithm chose that displacement) — so both runs report a force clamped to fy. What Algorithm::Linear actually gets wrong is the displacement itself: having solved against the stale elastic tangent for the whole step, its u doesn't satisfy the true (regime-updated) dynamic equilibrium the way the Newton corrector's converged u does — so the two algorithms must land on visibly different displacements once yielding is engaged mid-step, even though both "solve" without error.

ok
Newton corrector matches algorithm linear bit for bit on a linear systemnewton_corrector_matches_algorithm_linear_bit_for_bit_on_a_linear_system

docs/algorithms.md §6.1's load-bearing correctness check, run as an actual test rather than only hand-verified on paper: for a linear system, the new Newton-corrector arm (TangentStrategy::ReuseAtStepStart — form the effective operator once, at step start, same as the old closed form did implicitly) reproduces the old one-shot closed form's answer bit-for-bit — proving the restructure is a strict superset, not a behavior change, for the case that already worked. Same undamped SDOF free-vibration system dynamics/free_vibration.rs's newmark_undamped_sdof_ matches_closed_form_free_vibration already checks against the closed form; this test checks the two algorithms against each other instead.

More

Exactly two iterations, not one: ConvergenceTest::check (shared with static Analysis) tests the residual that produced the just-applied correction, not a freshly re-formed one — so iteration 0's large step-start unbalance never self-certifies, even though the single linear solve it produces is already exact. Iteration 1 re-forms the residual at that already-exact state (now ~0, since a linear Op solved exactly satisfies equilibrium after one correction) and that's what converges. This is the same convention Algorithm::Newton already used for static analysis — not a new wrinkle introduced by the transient corrector.

elements40 tests

Element formulations (truss, beams, zero-length) against closed forms and each other.

disp_beam · 3

DispBeamColumn / DispBeamColumn3 reproduce the elastic beam exactly for an elastic fiber section.

ok
Lobatto integration gives the same exact result as legendrelobatto_integration_gives_the_same_exact_result_as_legendre

Sanity check that the exact match above isn't an artifact of a specific point count or quadrature family — Lobatto (which includes the endpoints, unlike Legendre) must give the identical exact answer too, since the underlying integrand's polynomial degree doesn't care which family of points samples it.

ok
Four corner fiber section matches elastic beam column3 exactly in both planesfour_corner_fiber_section_matches_elastic_beam_column3_exactly_in_both_planes

DispBeamColumn3 (biaxial fiber-discretized, displacement-based) must exactly reproduce ElasticBeamColumn3's closed-form linear-elastic response in both bending planes plus axial and (decoupled) torsion — not approximately, exactly, to solver precision. Four corner fibers at (±hy, ±hz) reproduce EA, EIy, EIz exactly with zero product-of-inertia coupling (FiberSection3's four_corner_elastic_fibers_reproduce_ea_ eiy_eiz_with_zero_cross_coupling unit test checks this piece in isolation). DispBeamColumn3::strain_displacement's shape functions are the same cubic-Hermite/linear-axial fields ElasticBeamColumn3 is built from, so for constant EA/EIy/EIz any >=2-point Gauss rule integrates the (at most quadratic) integrand exactly.

ok
Two fiber elastic section matches elastic beam column exactlytwo_fiber_elastic_section_matches_elastic_beam_column_exactly

DispBeamColumn (fiber-discretized, displacement-based) must exactly reproduce ElasticBeamColumn's closed-form linear-elastic response — not approximately, exactly, to solver precision. Two symmetric elastic fibers at y = ±sqrt(Iz/A), area A/2 each, reproduce EA = E*A and EI = E*Iz exactly (FiberSection's two_symmetric_elastic_fibers_reproduce_ea_ei_exactly unit test checks this piece in isolation). The element-level strain-displacement field (DispBeamColumn::strain_displacement) uses the same cubic-Hermite/ linear-axial shape functions as ElasticBeamColumn, so for constant EA/EI any >=2-point Gauss rule integrates the (at most quadratic) integrand exactly — the two elements' stiffness matrices must therefore be identical, not just close.

disp_beam_loads · 3

DispBeamColumn / DispBeamColumn3 uniform element loads: elastic equivalence with the closed-form beams, and a yielding cantilever benchmarked against OpenSeesPy.

ok
Planar disp beam matches elastic beam under axial and transverse udlplanar_disp_beam_matches_elastic_beam_under_axial_and_transverse_udl

A fiber disp beam with two symmetric elastic fibers reproduces E·A/E·I exactly, so under the same uniform load it must match ElasticBeamColumn: same nodal displacements and same member end forces (the latter only if the load's fixed-end effect is subtracted identically).

ok
Spatial disp beam matches elastic beam under triaxial udlspatial_disp_beam_matches_elastic_beam_under_triaxial_udl

Spatial counterpart: four fibers at (±h, ±h) give Iy = Iz; wx, wy and wz all nonzero on a member along global x.

ok
Yielding disp cantilever under udl matches openseesyielding_disp_cantilever_under_udl_matches_opensees

A yielding DispBeamColumn cantilever under a uniform load, run with 1 and 8 elements and 3 and 5 Lobatto points, matches OpenSeesPy's tip deflection and rotation to a relative tolerance of 1e-6. The reference values were produced with OpenSeesPy using the same fiber section, integration rule and 10 load steps.

elastic_beam · 9

ElasticBeamColumn / ElasticBeamColumn3 through the full stack: closed-form UDL and combined-load deflections, PDelta against Linear, and axial/transverse element loads.

ok
Axis aligned biaxial udl matches closed form end rotations in both planesaxis_aligned_biaxial_udl_matches_closed_form_end_rotations_in_both_planes

Spatial counterpart to elements/elastic_beam.rs's simply_supported_beam_udl_ matches_closed_form_end_rotation — ElasticBeamColumn3's new local wy/wz uniform transverse load (ElementLoad3::uniform, the spatial generalization of planar ElementLoad::Uniform; see the 3D beam-load design), exercised through the full Domain3/Analysis3 stack with a member aligned along global x (vec_xz = [0,0,1] makes local [x,y,z] equal global [x,y,z] exactly, isolating the load formula itself from GeomTransf3's rotation — same isolation strategy as elastic_beam_column.rs's spatial_tests).

More

Both bending planes are loaded simultaneously (wy and wz both nonzero) to confirm they're independent (no cross term) and each matches the same closed form as the planar case, theta = w*L^3/(24*E*I), with Iz governing the wy-driven rz rotation and Iy governing the wz-driven ry rotation.

Boundary conditions: node_i is a pin (Ux,Uy,Uz fixed) plus torsion fixed (Rx, since a doubly-free torsion DOF pair is a zero-energy rigid rotation about the member axis — see GeomTransf3's discussion of why spatial models need every DOF meaningfully restrained); node_j is a roller (Uy,Uz fixed, axial and every rotation free) — the direct spatial generalization of the planar test's pin/roller pair.

ok
Cantilever elastic beam3 matches closed form under combined loadingcantilever_elastic_beam3_matches_closed_form_under_combined_loading

A 3D ElasticBeamColumn3 cantilever, run through the real Domain3/Analysis3 stack (not just the element-level checks in elastic_beam_column.rs), matching biaxial bending, axial, and torsion deflections against textbook closed forms simultaneously — the "axial, torsional, and both bending deflections of rotated cantilevers" acceptance criterion from the 3D design notes (member here is axis-aligned rather than rotated, since elastic_beam_column.rs's own tests already isolate the vec_xz transform on a skew member; this test's job is the Domain3 assembly path, not the transform).

ok
Loads on the same element accumulateloads_on_the_same_element_accumulate

Several loads on one element in one pattern sum rather than the last one winning (OpenSees semantics).

ok
Planar cantilever axial and transverse udl match closed formplanar_cantilever_axial_and_transverse_udl_match_closed_form

Axial + transverse UDL on a cantilever: tip ux = wx L² / 2EA, tip uy = wy L⁴ / 8EI, and the member end forces equal the statics result — the free end carries nothing, the fixed end carries the whole load.

ok
Simply supported beam udl matches closed form end rotationsimply_supported_beam_udl_matches_closed_form_end_rotation

A single ElasticBeamColumn spanning a simply-supported beam, under a uniform transverse element load, matches the classical closed-form end rotation theta = w*L^3 / (24*E*I).

More

A single 2-node beam element with consistent (virtual-work) equivalent nodal loads exactly reproduces the nodal DOFs of the continuous beam solution for element loads with no interior point loads — the same identity underlying the classical slope-deflection / moment-distribution fixed-end-moment method — so this is an exact check, not an approximation, despite using only one element.

ok
Spatial cantilever axial udl matches closed formspatial_cantilever_axial_udl_matches_closed_form

Spatial counterpart: a member along global x with vec_xz = [0,0,1] has local axes equal to global, so wx is plain axial.

ok
Zero uniform transverse load matches no load at allzero_uniform_transverse_load_matches_no_load_at_all

Zero load must still assemble (the wy == 0.0 && wz == 0.0 fast path in ElasticBeamColumn3::form_load_vector), and a pattern with no element load attached at all must behave identically to one whose load is exactly zero — the spatial counterpart of the implicit "no load" coverage every other element-load test gets for free by simply not calling add_element_load.

ok
Pdelta matches linear at zero axial force and softens under compressionpdelta_matches_linear_at_zero_axial_force_and_softens_under_compression

GeomTransf::PDelta sanity check: with zero axial force it must reduce exactly to Linear (the geometric-stiffness correction is a function of axial force, so it vanishes at N=0), and under a compressive axial force it must soften the bending stiffness relative to Linear (the intended P-Delta direction — see beam.rs's geometric_stiffness doc comment).

More

Each case runs as two equal LoadControl increments, not one: within a single Algorithm::Linear solve from a zero initial state, the axial force used for the geometric-stiffness correction is read from the pre-step displacement (always zero on a first step from rest), so a one-shot solve can never actually exercise it (see beam.rs's doc comment on why path-following P-Delta needs Newton iteration). Splitting into two increments lets the first establish a nonzero axial force that the second increment's tangent stiffness can then react to — a legitimate (if approximate) incremental-linear P-Delta technique.

ok
Pdelta3 matches linear3 at zero axial force and softens under compression in both planespdelta3_matches_linear3_at_zero_axial_force_and_softens_under_compression_in_both_planes

GeomTransf3::PDelta3 sanity check, the spatial analogue of elements/elastic_beam.rs's planar pdelta_matches_linear_at_zero_axial_force_and_ softens_under_compression (see its doc comment for why each case runs as two LoadControl increments rather than one): at zero axial force PDelta3 must reduce exactly to Linear3, and under compression it must soften the bending stiffness relative to Linear3.

More

Run in both bending planes (Uy, governed by Iz, and Uz, governed by Iy) — not just one — because ElasticBeamColumn3::geometric_ stiffness's y-plane block is a hand-derived sign flip of the z-plane one (ry = -dw/dx vs rz = +dv/dx), not copied from a reference; a bug there would only show up by actually exercising the Iy plane, not just Iz.

fiber_responses · 2
ok
Disp beam column fiber responses match hand computed strain and stressdisp_beam_column_fiber_responses_match_hand_computed_strain_and_stress

DispBeamColumn::fiber_responses after a converged pure-axial-load step must reproduce, fiber by fiber, the exact closed-form strain/stress a two-symmetric-elastic-fiber section gives — cross-checked against fiber_section.rs's own two_symmetric_elastic_fibers_reproduce_ea_ei_ exactly, here read back through the element rather than the bare FiberSection: a symmetric section under pure axial extension has zero curvature, so every fiber (regardless of its y) should show identical strain == eps0 == axial_disp/length and stress == e*eps0.

ok
Force beam column fiber responses are internally consistent with committed section stateforce_beam_column_fiber_responses_are_internally_consistent_with_committed_section_state

ForceBeamColumn::fiber_responses after a converged nonlinear step: summing stress*area back up across every fiber (the same definition FiberSection::trial's N uses) at each integration point must reproduce that point's true axial force (~0 here — pure transverse tip load, no net axial anywhere along a straight cantilever) — a self- consistency check that fiber_responses reads the same e_commit state local_force/commit already rely on, not stale or mismatched data.

force_beam · 6

ForceBeamColumn / ForceBeamColumn3: exact for prismatic elastic members, section coupling, and yielding with permanent set, in 2D and both 3D bending planes.

ok
Asymmetric biaxial section matches hand derived closed formasymmetric_biaxial_section_matches_hand_derived_closed_form

The two tests above both use doubly-symmetric fiber layouts with zero product of inertia (Iyz = 0), so neither exercises FiberSection3::trial's eiyz tangent term or the resulting cross-coupling between the two bending planes that a real, non-doubly-symmetric section has. This is the spatial analogue of elements/force_beam.rs's asymmetric_elastic_section_matches_hand_ derived_closed_form, generalized from a 2x2 to a 3x3 section flexibility.

More

Same technique as that planar test: for a single-element cantilever with only tip forces applied (no end moments), all five basic forces q = [q1..q5] are fixed by nodal equilibrium alone, independent of section properties (q1 = axial, q3 = q5 = 0 since there's no applied end moment in either plane, and q2/q4 follow from the tip shear balance in each plane via basic_deformation_matrix's A^T map, exactly as the planar test derives q2 = -tip_transverse * length). Then v = F*q gives the exact basic deformation from the section's elastic flexibility (constant along the member, since an elastic material's tangent doesn't depend on strain), with F = integral(b^T f b) dx evaluated in closed form from b_matrix's (xi-1)/xi shape functions (integral (xi-1)^2 = integral xi^2 = 1/3, integral (xi-1)*xi = -1/6, integral (xi-1) = -1/2, integral xi = 1/2 over xi in [0,1]) — the same integrals the planar test evaluates by hand, just applied to a 3x3 f instead of a 2x2 one. f = k^-1 (k FiberSection3::trial's tangent) is inverted here via the explicit 3x3 cofactor/determinant formula, independently of the nalgebra inversion the element itself uses. Only v1/v2/v4 are needed (not v3/v5, which fix the tip rotations Rz/Ry): with node_i fully fixed, basic_deformation_ matrix's v2/v4 rows depend only on Uy_j/Uz_j, giving the tip translations directly.

ok
Asymmetric elastic section matches hand derived closed formasymmetric_elastic_section_matches_hand_derived_closed_form

ForceBeamColumn's basic-force/section coupling (the element flexibility's off-diagonal terms) must be exercised too, not just the symmetric-section case above where axial/bending decouple.

More

This is checked against a hand-derived closed form, not DispBeamColumn: for a single-element cantilever, the three basic forces q are fully determined by nodal equilibrium alone (three free dof at the tip, three basic unknowns), independent of section properties, then v = F*q gives the exact basic deformation from the section's elastic flexibility. DispBeamColumn is not an oracle here — deliberately not used as one — because its independent linear- axial/cubic-Hermite shape functions are only exact for a section with no axial-bending coupling (EQ = 0); with this asymmetric section it's off by about 0.5% (verified separately), which is a real property of displacement-based elements, not a ForceBeamColumn bug.

ok
Four corner fiber section matches elastic beam column3 exactly in both planesfour_corner_fiber_section_matches_elastic_beam_column3_exactly_in_both_planes

For a prismatic elastic member, ForceBeamColumn3 is exact regardless of point count — same property elements/force_beam.rs checks planarly, now exercising both bending planes plus axial and (decoupled) torsion. This is the actual verification for ForceBeamColumn3's basic system (v4/v5's rotation-coefficient sign flip relative to v2/v3) — see that type's doc comment for why this test, not inspection, is the source of truth.

ok
Elastic pp section softens past yield and shows permanent set on unloadelastic_pp_section_softens_past_yield_and_shows_permanent_set_on_unload

ForceBeamColumn's state-determination Newton loop must actually engage material nonlinearity (not just reproduce the elastic case), and its q_commit/e_commit fields must correctly carry converged history across separate Analysis::step() calls, the same property materials/state.rs's permanent-set test checks for ZeroLength/ElasticPP.

More

Symmetric eight-fiber ElasticPP section (four layers each side, at y = ±5, ±10, ±15, ±20) rather than the minimal two- or four-fiber cases used elsewhere in this file: with too few layers, the whole section reaches its full plastic-moment capacity (every fiber past yield strain simultaneously) at only a modest load multiple, leaving the section tangent exactly singular — not the gradual, layer-by- layer softening a real fiber-discretized section gives (and which the state-determination Newton loop needs to stay well-posed through). At the load levels this test uses, the outer two layers yield while the inner two stay elastic, matching how an actual fiber-section model is built (many layers), not a worst-case coarse discretization.

My = fy*Iz/y_max (elastic section modulus at the outermost fiber) gives the tip load P_yield = My/L at which the fixed-end section (included exactly by BeamIntegration::Lobatto's endpoint) first yields.

ok
Two fiber elastic section matches elastic beam column exactlytwo_fiber_elastic_section_matches_elastic_beam_column_exactly

For a prismatic elastic member, a force-based element is exact regardless of point count — the textbook property that motivates the whole force-based formulation. Same structure as elements/disp_beam.rs's equivalent check: build the identical cantilever both ways and require the tip deflection to match ElasticBeamColumn's closed form to solver precision, not just approximately.

ok
Elastic pp section softens past yield in both planes and shows permanent set on unloadelastic_pp_section_softens_past_yield_in_both_planes_and_shows_permanent_set_on_unload

ForceBeamColumn3's state-determination Newton loop must engage material nonlinearity in both bending planes independently (not just reproduce the elastic case, and not just one plane) — the spatial analogue of elements/force_beam.rs's elastic_pp_section_softens_past_yield_and_shows_permanent_set_on_ unload. A doubly-symmetric eight-fiber section (four fibers on each of the +y/-y/+z/-z faces) yields independently under a y-only or z-only tip load, each past its own p_yield.

force_beam_loads · 4

ForceBeamColumn / ForceBeamColumn3 uniform element loads: elastic equivalence with the closed-form beams (displacements and member end forces), and yielding members benchmarked against OpenSeesPy.

ok
Planar force beam matches elastic beam under axial and transverse udlplanar_force_beam_matches_elastic_beam_under_axial_and_transverse_udl

A fiber force beam with two symmetric elastic fibers reproduces E·A/E·I exactly, so under the same uniform load it must match ElasticBeamColumn: same nodal displacements and same member end forces (the latter only if the load's fixed-end effect is subtracted identically).

ok
Spatial force beam matches elastic beam under triaxial udlspatial_force_beam_matches_elastic_beam_under_triaxial_udl

Spatial counterpart: four fibers at (±h, ±h) give Iy = Iz; wx, wy and wz all nonzero on a member along global x.

ok
Frozen udl then displacement controlled tip load matches openseesfrozen_udl_then_displacement_controlled_tip_load_matches_opensees

Gravity UDL ramped on a yielding cantilever, frozen with hold_pattern_constant, then a tip load pushed under displacement control — the two-phase shape pysees models take. The frozen element load must stay in the elements' section forces through the second phase's fresh Analysis (whose pseudo-time restarts at 0) and the committed state carried across. References: OpenSeesPy forceBeamColumn, loadConst, DisplacementControl.

ok
Yielding force members under udl match openseesyielding_force_members_under_udl_match_opensees

References are OpenSeesPy 3.8 forceBeamColumn (Lobatto, eleLoad -beamUniform) with the identical model.

local_force · 7
ok
Disp beam column local force matches cantilever equilibrium for horizontal memberdisp_beam_column_local_force_matches_cantilever_equilibrium_for_horizontal_member

DispBeamColumn (two-fiber elastic section) end forces for a horizontal cantilever with a tip load match the statically determinate result: shear equals the load and the fixed-end moment equals the load times the length.

ok
Elastic beam column local force decomposes purely axially for a load along a skew members own axiselastic_beam_column_local_force_decomposes_purely_axially_for_a_load_along_a_skew_members_own_axis

Isolates the local-axis rotation itself (as opposed to the tests above, which use a horizontal member where local coincides with global and so never actually exercise the rotation). A global load applied exactly along a skew member's own axis must decompose to pure axial local force — ElasticBeamColumn's local stiffness has zero coupling between its axial row/column and its bending rows/columns, so any nonzero local shear/moment here means the rotation itself (not the element formula) has a sign or axis error.

ok
Elastic beam column local force matches cantilever equilibrium for horizontal memberelastic_beam_column_local_force_matches_cantilever_equilibrium_for_horizontal_member

ElasticBeamColumn local end forces for a horizontal cantilever with a tip load match nodal equilibrium: shear equals the load and the fixed-end moment equals the load times the length.

ok
Force beam column3 local force matches cantilever equilibrium for horizontal memberforce_beam_column3_local_force_matches_cantilever_equilibrium_for_horizontal_member

Spatial counterpart, exercising ForceBeamColumn3::local_force's cached value (including its decoupled torsion term) against full 3D nodal equilibrium — same technique as the planar cantilever checks above, extended to all six components at each end. Reuses the load case from elements/force_beam.rs's exactness test (axial, biaxial shear, torque all simultaneously) so this also implicitly cross-checks that test's already-verified tip displacements are consistent with these reactions, not just internally self-consistent.

ok
Force beam column local force matches cantilever equilibrium for horizontal memberforce_beam_column_local_force_matches_cantilever_equilibrium_for_horizontal_member

The highest-risk case: ForceBeamColumn::local_force reads a value cached at commit time (to avoid re-running the Newton state determination), not recomputed fresh like every other element here — a caching bug (stale value, wrong sign in the cached expression) wouldn't be caught by anything that doesn't actually call commit.

ok
Truss local force matches axial nodal equilibriumtruss_local_force_matches_axial_nodal_equilibrium

Truss local forces for an axially loaded bar equal the applied load with opposite signs at the two nodes, [-N, 0, 0, N, 0, 0].

ok
Zero length local force matches spring nodal equilibriumzero_length_local_force_matches_spring_nodal_equilibrium

ZeroLength local forces for a spring loaded at one node equal the applied load with opposite signs at the two nodes, [-F, 0, 0, F, 0, 0].

multi_node · 3

The element interface is not tied to two-node elements or to a full per-node DOF layout: an element reports its own nodes and pushes its own contributions through the sinks. These tests drive a user-defined catalog with a three-node element (compact layout: ux only) and a four-node element (ux, uy) through the real Domain/Analysis stack and check tangent, resistance, loads, lumped mass, reactions and recorded forces against hand calculations.

ok
Elements of different sizes assemble into one systemelements_of_different_sizes_assemble_into_one_system

Elements of different sizes share nodes in one assembly.

ok
Four node element mass load and reactionfour_node_element_mass_load_and_reaction

A four-node element's lumped mass lands mass / 4 on ux and uy of every node, and its equivalent load is scaled by the pattern factor and seen by reaction only at the nodes it touches.

ok
Three node element with compact layout assembles and solvesthree_node_element_with_compact_layout_assembles_and_solves

Two springs in series, fixed at one end and loaded at the other: u1 = P/k, u2 = 2P/k. The element has three nodes and only a compact ux layout, and its recorded local force is its own three-entry vector.

truss · 2

Truss / Truss3 through the full Domain/Analysis stack: 2D closed-form axial displacement and the symmetric 3D tripod.

ok
Symmetric tripod matches analytical vertical stiffness via analysis3symmetric_tripod_matches_analytical_vertical_stiffness_via_analysis3

Spatial counterpart to elements/truss.rs's acceptance case, run through the real Domain3/Node3/Element3::Truss3/Analysis3 architecture (the spatial instantiation of the now-generic Domain/Analysis — see their doc comments) — a symmetric tripod of three truss members meeting at one free apex node, each dropping to a fixed base support spaced 120 degrees apart — the standard space-truss case with a clean closed form, and genuinely exercises all three translational DOFs at once (unlike a single truss, which only stiffens the direction along its own axis and leaves a singular system in the other two — that's not a valid spatial model on its own).

More

Base supports lie on a radius-3 circle at z=0; the apex sits at (0,0,4), so every bar is a 3-4-5 triangle (L=5). By the 3-fold symmetry, the assembled stiffness at the apex is diagonal (cross terms cancel), so a purely vertical load produces a purely vertical displacement: dz = P / (3 * A * E * H^2 / L^3).

ok
Truss matches closed form axial displacementtruss_matches_closed_form_axial_displacement

A 2-node truss (L=100, A=2, E=30000, P=50) gives the closed-form displacement P*L/(A*E) = 0.0833333333 through Domain/Analysis.

zero_length · 1
ok
Zero length ent matches hand calczero_length_ent_matches_hand_calc

A ZeroLength + Ent ("no tension") element under a compressive load. The whole step stays within Ent's single linear (engaged) regime, so Algorithm::Linear (no Newton iteration) is exact.

materials3 tests

Uniaxial material state handling (trial/commit semantics).

state · 2
ok
Failed step leaves domain unchangedfailed_step_leaves_domain_unchanged

Domain's snapshot-and-restore: a step that fails to converge must leave the domain exactly as it was before the attempt, not partway through a discarded Newton iteration. NormDispIncr with a single permitted iteration can never converge here (the one correction is the full elastic displacement, far above tol), forcing FailedToConverge deterministically — after a correction was applied to the domain — without needing a contrived numerically-hard system. (A zero iteration budget is rejected outright as AnalysisError::InvalidOption.)

ok
Multi step analysis shows real permanent set on partial unloadmulti_step_analysis_shows_real_permanent_set_on_partial_unload

Material's trial/commit split (see its doc comment) and Domain's snapshot-and-restore-on-failure, needed by fiber sections and the full material catalog, which depend on real path-dependent state.

More

Same Truss (k_t=50, elastic) + ZeroLength+ElasticPP (k_epp=100, fy=1.0) parallel system as analysis/newton.rs, but driven through two separate Analysis::step() calls — loading past yield, then partially unloading — rather than one. Algorithm::Newton's Newton loop only ever evaluates materials via Material::trial_stress_tangent (never mutates), and Domain::commit runs once after each step converges: step 1 leaves the EPP spring's plastic strain committed at ep = u1 - fy/e_epp, and step 2's unload must react to that committed history, not recompute from scratch.

Closed form: after step 1 (load 0->5, past yield), u1 = (5-1)/50 = 0.08, ep = 0.08 - 1.0/100 = 0.07. Step 2 unloads to load_factor=3 (increment -2.0); solving F = k_t*u + epp_force(u) with the EPP now elastic about ep=0.07 (since the trial stress e_epp*(u-ep) stays under fy for the unload direction): F = 50*u + 100*(u-0.07) = 150*u - 7 = 3 => u2 = 10/150 = 1/15. A model without real plasticity (recomputing from strain alone, ignoring ep) would instead stay on the plastic plateau and predict u2 = (3-1)/50 = 0.04 — a materially different, wrong answer.

committed_trial · 1

A trial at the strain a material was just committed at must report the same stress and tangent the committed trial did. OpenSees materials return their committed (stress, tangent) for a trial strain change under DBL_EPSILON; displacement-control predictors, element-state determination and arc-length seeds all evaluate there.

ok
Committed trial is idempotentcommitted_trial_is_idempotent

For each uniaxial material (except ElasticPP, see below), trial strain after a commit at the same strain must return the same stress and tangent as the committed trial did, along a long reversing strain path in both signs. OpenSees materials behave this way, and displacement control, element state determination and arc-length seeds all evaluate at the committed point.

shells11 tests
shell3 · 2

Shell3 (the DKT/Allman flat triangle) against OpenSees ShellDKGT, on three distorted triangles on the tilted plane z = 0.3 x + 0.1 y. Reference values are from OpenSeesPy (ShellDKGT + ElasticMembranePlateSection).

ok
Opensees doubles the triangle self weightopensees_doubles_the_triangle_self_weight

OpenSees' ShellDKGT integrates its self-weight with w * xsj instead of 0.5 * w * xsj (the mass and stiffness use the latter), applying twice the weight. Carapace applies the correct weight, so the OpenSees run with -selfWeight 0 0 -9.81 equals Carapace with twice the body acceleration (0, 0, 2 * 9.81).

ok
Triangle patch matches openseestriangle_patch_matches_opensees

Displacements, clamp reactions and the first triangle's four Gauss-point resultants.

shell4 · 9

Shell4 (the MITC4 flat shell) against OpenSees ShellMITC4 and closed-form plate theory.

The OpenSees references were produced with OpenSeesPy (ShellMITC4 + ElasticMembranePlateSection) on the exact models built here; the tolerance is 1e-8 relative to the largest value.

ok
Gauss point resultants match openseesgauss_point_resultants_match_opensees

The first element's committed strain and resultants at its four Gauss points (OpenSees' eleResponse(1, 'stresses') order and sign convention).

ok
Pressure resultant is p area along the normalpressure_resultant_is_p_area_along_the_normal

A pressure on a distorted quad is a consistent load: the clamp reactions sum to -p A e3, with e3 the normal given by the node order (here up the tilted plane).

ok
Shell and beam sharing a node match openseesshell_and_beam_sharing_a_node_match_opensees

A column framing into a shell corner: the shell stiffens all six DOFs, so the beam's rotations and the shell's share one node with no extra constraint.

ok
Self weight matches openseesself_weight_matches_opensees

Self-weight loads each node with rho h times its tributary area times the body vector. OpenSees' ShellMITC4 applies the opposite sign (-rho h b), so the reference run's -selfWeight 0 0 -9.81 is body(0, 0, 9.81) here.

ok
Tilted distorted patch matches openseestilted_distorted_patch_matches_opensees

Displacements and clamp reactions of two distorted, tilted quads under point loads.

ok
Warped quad matches openseeswarped_quad_matches_opensees

A quad with one node lifted out of the plane: the shell is treated as flat in its mean frame, exactly as OpenSees does, so the two agree.

ok
Scordelis lo roof matches openseesscordelis_lo_roof_matches_opensees

The Scordelis-Lo barrel vault roof (quarter model, 8x8 mesh) against OpenSees, and against the published vertical deflection of 0.3024 at the midpoint of the free edge: a mesh-converged value of about 0.30 (0.2965 at 12x12 in OpenSees).

ok
Simply supported square plate centre deflectionsimply_supported_square_plate_centre_deflection

A simply supported square plate under uniform pressure: the centre deflection is 0.004062 p a^4 / D (thin-plate Navier series).

ok
Simply supported square plate fundamental frequencysimply_supported_square_plate_fundamental_frequency

The fundamental frequency of a simply supported square plate is 2 pi^2 / a^2 sqrt(D / (rho h)). Carapace lumps the shell mass (OpenSees uses the consistent mass), so this is a closed-form check, not an OpenSees match.

wire78 tests

The CarapaceInputV1 wire format: decoding, validation and end-to-end runs.

arclength · 6

Arc-length continuation through the wire format and Session (docs/arclength.md §9 "WASM session / storage ordering"): both profiles decode arc settings, decreasing and repeated load factors keep monotonically increasing sample indices, the step budget doesn't change accepted samples, stop criteria end a stage early, and invalid combinations are rejected at decode.

ok
Runtime arc length errors reach the session as structured detailruntime_arc_length_errors_reach_the_session_as_structured_detail

Problems only detectable at run time reach the session as structured detail. With no reference load, decoding succeeds and the first advance finishes with ZeroLoadSensitivity in outcome.error.

ok
Invalid arc length options are rejected at decodeinvalid_arc_length_options_are_rejected_at_decode

Invalid arc-length settings in the wire format (radius bounds, scales, tolerances and so on) are rejected at decode time with InvalidAnalysisOption naming the offending field.

ok
Spatial profile decodes arc settings and lands on a load targetspatial_profile_decodes_arc_settings_and_lands_on_a_load_target

The 3D profile decodes arc-length settings too: a linear ZeroLength3 spring continued by arc-length stops exactly on the requested load-factor target.

ok
Snap back session orders samples by index and stops early on targetsnap_back_session_orders_samples_by_index_and_stops_early_on_target

A snap-back run through the wire format. Load factors rise and then fall (they are not monotonic), samples are stored in sample-index order regardless, and the displacement stop criterion ends the stage before its step cap.

ok
Stop detail and default combined convergence are reportedstop_detail_and_default_combined_convergence_are_reported

A stage that ends on a displacement target reports the stop reason and that it landed exactly. The default convergence test works, and an explicit combined convergence test decodes and runs as well.

ok
Advance budget does not change accepted samplesadvance_budget_does_not_change_accepted_samples

The step budget of advance is only a scheduling knob: running one step at a time produces exactly the same accepted samples as running the whole stage in one call.

constraints · 4

equalDofs and rigidDiaphragms tables through the wire format, in 2D and 3D.

ok
Decodes a rigid diaphragm3 and ties translation with no lever arm when untwisteddecodes_a_rigid_diaphragm3_and_ties_translation_with_no_lever_arm_when_untwisted

core::Domain3::rigid_diaphragm_about through the spatial path (RigidDiaphragmTable): reuses the skew-truss closed form from decodes_a_skew_truss3_and_matches_the_closed_form_axial_elongation to give the retained node a known, nonzero translation in both in-plane axes (x/z, perpendicular to normal = Y) while its rotation about normal stays fixed at zero — so the lever-arm term in Domain3::rigid_diaphragm_about's affine tie vanishes and the constrained node (which carries no element of its own) must reproduce the retained node's ux/uz exactly.

ok
Decodes a rigid diaphragm tying two nodes ux to the retained nodedecodes_a_rigid_diaphragm_tying_two_nodes_ux_to_the_retained_node

core::Domain::rigid_diaphragm (RigidDiaphragmTable) — same setup as the equal_dof test above, but tying two otherwise-elementless nodes' ux to the retained node in one call, proving the table's sparse per-row constrained list decodes correctly.

ok
Decodes an equal dof3 constraint tying one nodes ux to anotherdecodes_an_equal_dof3_constraint_tying_one_nodes_ux_to_another

core::Domain::equal_dof through the spatial path (EqualDofTable) — same shape as wire/elements.rs's own equal_dof test, just against Node3Ids: node 2 carries no element of its own and is free only in ux, tied to node 1's ux.

ok
Decodes an equal dof constraint tying one nodes ux to anotherdecodes_an_equal_dof_constraint_tying_one_nodes_ux_to_another

core::Domain::equal_dof (EqualDofTable, ConstraintHandler:: Transformation picked automatically by session::start_current_stage once Domain::has_mp_constraints() is true): node 2 carries no element of its own and is free only in ux, tied via equal_dof to node 1's ux — so its recorded displacement must exactly equal node 1's, which itself matches the plain closed-form truss elongation.

continuum · 7

the planeMaterials, triangles and quads tables, body and edge element loads, and Gauss-point recorders, decoded and run end to end against the same models built natively in carapace-core.

ok
A panel runs end to end identically to the same model built nativelya_panel_runs_end_to_end_identically_to_the_same_model_built_natively

A small panel (two Quad4 and a Tri3) decoded from the wire format and run through a session gives the same nodal displacements and Gauss-point strains and stresses as the same model built directly with carapace-core.

ok
Bad references and shapes are decode errorsbad_references_and_shapes_are_decode_errors

Malformed continuum tables are structured decode errors: unknown material or node indices, rows with the wrong number of nodes, and other shape problems, each reported with the table and row.

ok
Continuum tables and kinds are not in a 3d modelcontinuum_tables_and_kinds_are_not_in_a_3d_model

Continuum tables (quads, triangles, planeMaterials) and continuum recorders are rejected in a 3D input with TableNotInProfile, since those elements are 2D only.

ok
Gauss point recorders are bounds checked against the elementgauss_point_recorders_are_bounds_checked_against_the_element

Gauss-point recorders are checked against the element: Quad4 has 4 points and Tri3 has 1, and an out-of-range point or component is InvalidGaussPoint or an invalid-component error rather than a silent empty recording.

ok
Several body force patterns through the wire sum with their factorsseveral_body_force_patterns_through_the_wire_sum_with_their_factors

Wire-format version of the body-force summation: several body loads on one Quad4 across patterns with different series and scale factors sum to the same displacement as one pattern with the total load.

ok
The tables round trip through serdethe_tables_round_trip_through_serde

The continuum tables, loads and Gauss-point recorders survive a serialize/deserialize round trip unchanged, with the expected camelCase JSON shape (planeStress, edgePressure, gaussPoint), and still decode.

ok
Wrong kind loads and out of range edges are rejected without accumulatingwrong_kind_loads_and_out_of_range_edges_are_rejected_without_accumulating

Element loads of the wrong kind for their element (a beam load on a continuum element, continuum loads on a beam) and out-of-range edge indices are decode errors, and nothing is accumulated.

element_loads · 4

Uniform element loads through the wire format, and rejection on unsupported elements.

ok
Decodes a uniform element load and records loaded member end forcesdecodes_a_uniform_element_load_and_records_loaded_member_end_forces

A cantilever ElasticBeamColumn under a local-axis uniform load (wx axial + wy transverse) driven through the wire tables: tip displacements match the closed forms, and the element-force recorders report the member end forces (statics: the fixed end carries the whole load, the free end none), which only holds if recorders account for the element load.

ok
Decodes a uniform load on a disp beam columndecodes_a_uniform_load_on_a_disp_beam_column

elementKind: "dispBeamColumn" accepts a uniform element load: a two-fiber elastic section reproduces E·A/E·I, so the tip deflection is the closed form and the recorded member end forces are plain statics (fixed end carries the load, free end nothing).

ok
Decodes a uniform load on a force beam columndecodes_a_uniform_load_on_a_force_beam_column

elementKind: "forceBeamColumn" accepts a uniform element load: a two-fiber elastic section reproduces E·A/E·I, so the tip deflection is the closed form and the recorded member end forces are plain statics (fixed end carries the load, free end nothing).

ok
Rejects an element load on an unsupported element kindrejects_an_element_load_on_an_unsupported_element_kind

Element loads on a kind core doesn't apply them to are rejected up front rather than silently dropped.

elements · 11

Elements through the wire format: tables decode into the right elements and match closed forms (truss, zero-length, zero-length section, and uniaxial materials), in 2D and 3D.

ok
Decodes a single truss and matches the closed form displacementdecodes_a_single_truss_and_matches_the_closed_form_displacement

Smoke test for the whole node/material/element/pattern/sequence pipeline: the truss case (core/tests/elements/truss.rs's closed form), built entirely from wire-format tables instead of direct core calls.

ok
Decodes a skew truss3 and matches the closed form axial elongationdecodes_a_skew_truss3_and_matches_the_closed_form_axial_elongation

The closed-form axial-stiffness case (core/tests/m17's skew-truss analogue, core/tests/elements/truss.rs's straight-bar analogue), decoded entirely from 3D wire tables: a 3-4-5 skew truss should reproduce the same axial elongation a hand computation gives, exercising Truss3's direction-cosine geometry through the decoder rather than just core directly.

ok
Decodes a zero length3 with friction coupling on two shear axesdecodes_a_zero_length3_with_friction_coupling_on_two_shear_axes

ZeroLength3's friction coupling (ZeroLengthTable::friction), the spatial counterpart of wire/elements.rs's decodes_a_zero_length_with_friction_coupling: one normal DOF (ux) and two independent shear DOFs (uy, uz — Friction3::shear_dofs), each run through the Coulomb return map independently against the shared normal force (core/src/model/elements/zero_length.rs's friction3_shear_axes_slide_independently_against_shared_normal_force unit test covers the same physics directly against core).

ok
Decodes a zero length section3 and matches closed form axial stiffnessdecodes_a_zero_length_section3_and_matches_closed_form_axial_stiffness

ZeroLengthSection3 (core::ZeroLengthSection3, wired via ZeroLengthSectionTable): reproduces closed-form axial stiffness from a four-corner symmetric fiber section, the spatial counterpart of wire/elements.rs's own zero-length-section test.

ok
Decodes a zero length section and matches closed form axial stiffnessdecodes_a_zero_length_section_and_matches_closed_form_axial_stiffness

ZeroLengthSection (core::ZeroLengthSection, wired via ZeroLengthSectionTable): reproduces the same closed-form axial/moment stiffness core's own zero_length_section_reproduces_symmetric_ea_ei unit test checks directly against FiberSection, here decoded entirely from wire tables with a two-fiber symmetric section and no independent uy spring.

ok
Decodes a zero length with friction couplingdecodes_a_zero_length_with_friction_coupling

ZeroLength's friction coupling (ZeroLengthTable::friction), driven through the decoder rather than directly against core::Friction (see core/src/model/elements/zero_length.rs's own friction_sticks_below_yield_surface/friction_slides_past_yield_surface unit tests, which this exercises end to end from wire tables): a normal spring in compression plus a friction-coupled shear DOF, checked both below and past the Coulomb yield surface.

ok
Decodes an oriented zero length section3 and matches closed form axial stiffnessdecodes_an_oriented_zero_length_section3_and_matches_closed_form_axial_stiffness

ZeroLengthSectionTable::orient: local x = global Z (yp = global X), so the section's axial stiffness answers a uz load.

ok
Decodes an oriented zero length section and matches closed form axial stiffnessdecodes_an_oriented_zero_length_section_and_matches_closed_form_axial_stiffness

ZeroLengthSectionTable::orient: the section's axial direction turned onto global y (OpenSees -orient 0 1 0), so a uy load meets the same closed-form axial stiffness the unoriented test above gets in ux.

ok
Decodes hysteretic and pinching4 materials in the elastic rangedecodes_hysteretic_and_pinching4_materials_in_the_elastic_range

Material::Hysteretic/Material::Pinching4 (materials.rs's newly added arena entries): both decode without error and, for the symmetric-envelope well-within-elastic-range case checked here, behave like a plain elastic spring — a decode-plumbing smoke test, not a re-check of either material's own hysteresis logic (that lives in core/src/model/materials/{hysteretic,pinching4}.rs's unit tests).

ok
Rejects invalid zero length orientationsrejects_invalid_zero_length_orientations

Bad orient rows are decode errors, not silent global axes: a zero vector, a 2D vector leaving the xy plane (x3 != 0), and a row past the table.

ok
Truss with unfixed rotations solves and a load on an unused dof is a structured errortruss_with_unfixed_rotations_solves_and_a_load_on_an_unused_dof_is_a_structured_error

DOF activation through the wire: a model that leaves a truss's rotations unfixed (the compiler no longer has to fix them) solves, and a nodal load on a DOF nothing resists comes back as a structured error from advance, not a panic.

material_probe · 8

Material probe cases (unit zero-length spring, prescribed strain), as used by the PySees material preview.

ok
Composites resolve through the arenacomposites_resolve_through_the_arena

Composite materials refer to arena entries by index; the probed material may be any of them, and a Gap anywhere in the arena is still rejected.

ok
Concrete01 tension then compressionconcrete01_tension_then_compression

A Concrete01 probe has no tension capacity (zero stress at positive strain), goes into compression with softening past the peak, and unloads to zero stress at zero strain.

ok
Elasticelastic

An elastic probe follows a prescribed strain history through zero, positive and negative strains: stress equals E times strain.

ok
Elastic pp keeps committed plastic historyelastic_pp_keeps_committed_plastic_history

An elastic-perfectly-plastic probe keeps its committed plastic strain across targets: strains of +0.002, -0.002 and -0.0005 give stresses of +100, -100 and +50 (elastic unloading from the permanent set).

ok
Errors are structurederrors_are_structured

Probe errors are structured: an unsupported material (Gap) is UnsupportedMaterial, a NaN target is invalidTarget, and a rejected target leaves the committed state unchanged.

ok
Repeated targets do not advance staterepeated_targets_do_not_advance_state

Applying the same target strain repeatedly does not advance material state: the elastic-perfectly-plastic probe returns the same stress each time.

ok
Reset discards history and initial strain is appliedreset_discards_history_and_initial_strain_is_applied

reset discards the history, and the configured initial strain is applied: with an initial strain of 0.5, a target of 1.0 and later 0.5 give stresses 10 and 5 for E = 10.

ok
Steel02 and concrete02 probesteel02_and_concrete02_probe

Steel02 and Concrete02 are probe-able now.

modal · 5

Modal stages through the wire format: eigenvalues, shapes and participation, in 2D and 3D.

ok
A mode shape recorder past the computed mode count records nothinga_mode_shape_recorder_past_the_computed_mode_count_records_nothing

An out-of-range ModeShape.mode records nothing this call, rather than erroring — decode can't validate it upfront (a mode index isn't a table row; it's only meaningful once modal_analysis has actually run and produced however many modes it was asked for).

ok
Decodes a modal stage3 and matches the sdof truss closed formdecodes_a_modal_stage3_and_matches_the_sdof_truss_closed_form

The spatial SDOF-truss closed form (core/tests/dynamics/free_vibration.rs's spatial_truss_mass_matches_sdof_closed_form_frequency_via_modal_analysis3), decoded entirely from 3D wire tables — proves Modal/ModeShape work through the 3D decode path, not just the 2D one already checked in wire/elements.rs.

ok
Decodes a modal stage and matches the golden ratio closed formdecodes_a_modal_stage_and_matches_the_golden_ratio_closed_form

The golden-ratio closed form (core/tests/dynamics/modal.rs), decoded entirely from wire tables: a Modal stage's mode frequencies come back via ModeShape recorders, whose "time" component (StepOutcome:: load_factor's doc comment) is the mode's own frequency rather than a pseudo-time or load factor.

ok
Modal results report shapes and participation without recordersmodal_results_report_shapes_and_participation_without_recorders

Session::modal_results returns frequencies, full-node shapes and participation for a finished Modal stage — no recorders needed. For the 2-mass chain every mode's effective mass sums to the total x mass (all modes requested), shapes are M-normalized and sign-fixed.

ok
Modal shapes report a rigid diaphragm slaves motion through the constraintmodal_shapes_report_a_rigid_diaphragm_slaves_motion_through_the_constraint

A rigid-diaphragm slave has no equation of its own, so its mode-shape components must come from its constraint: reading them through equation_of (as the session used to) reported a slave that never moves. Mass and a rotational spring sit on the retained node; the translational springs sit on the slave, so every mode moves it.

profiles · 12

the unified CarapaceInputV1. Profile rejection, omitted tables, serde round trip, ndm validation, the rigidLinks/linearConstraints tables against the same models built natively in core, and recorder component bounds.

ok
A linear constraints table matches the native add constrainta_linear_constraints_table_matches_the_native_add_constraint

The general linearConstraints table, written as the three equations of a 2D rigid link, matches the same constraints added natively with add_constraint, and agrees with the rigid link to rounding.

ok
A rigid links table matches the native 2d rigid linka_rigid_links_table_matches_the_native_2d_rigid_link

The rigidLinks table decodes to the same model as a native 2D rigid_link: identical results from the same loads, and the slave node really moves with the master (including the rotation lever-arm term).

ok
A rigid links table matches the native 3d rigid linka_rigid_links_table_matches_the_native_3d_rigid_link

The rigidLinks table decodes to the same 3D model as a native rigid_link: six springs on the slave node and a general load on the master give identical results, including rotation-translation coupling through the offset.

ok
A table that belongs to the other profile is rejecteda_table_that_belongs_to_the_other_profile_is_rejected

A table that belongs to the other profile is rejected: 3D beam-column tables in a 2D input, and 2D beam-column tables in a 3D input, give TableNotInProfile.

ok
An element kind of the other profile is rejected in loads and recordersan_element_kind_of_the_other_profile_is_rejected_in_loads_and_recorders

Element loads and recorders that refer to an element kind of the other profile (a 3D beam in a 2D input, or the reverse) are rejected with ElementKindNotInProfile.

ok
An out of range recorder component is a decode erroran_out_of_range_recorder_component_is_a_decode_error

Element force and element load recorder components are bounded by the profile: the last valid component decodes, and one past it is InvalidRecorderComponent (2D forces have 6 components and 3D have 12).

ok
Omitted tables decode as emptyomitted_tables_decode_as_empty

Omitted tables decode as empty: a header-only input is a valid empty model in both profiles, and a truss model written with only the shared tables (every other table left out of the JSON) decodes and solves.

ok
Profile dependent row shapes are checkedprofile_dependent_row_shapes_are_checked

Rows whose shape depends on the profile are validated: friction takes one shear DOF in 2D and two in 3D, and orientation and other per-row data are checked, each failure reported as InvalidRow with the table and reason.

ok
Recorder dofs are bounded by the profilerecorder_dofs_are_bounded_by_the_profile

Recorder DOFs are bounded by the profile: DOF 3 does not exist in 2D (InvalidDof), while DOF 5 is valid in 3D.

ok
Rejects an unrecognized space before constructing a sessionrejects_an_unrecognized_space_before_constructing_a_session

An ndm other than 2 or 3 is rejected with UnsupportedNdm before any session is built.

ok
The unified input round trips through serdethe_unified_input_round_trips_through_serde

A populated 2D and 3D CarapaceInputV1 survives serialization to JSON and back unchanged, covering friction, orientation, constraints and the rest of the tables.

ok
Unknown ndm is rejectedunknown_ndm_is_rejected

ndm values 0, 1, 4 and 255 are each rejected with UnsupportedNdm.

recorders · 6

Recorders through the wire format: element force, reaction, fiber and element-load recorders.

ok
A fiber recorder on a non fiber element or out of range index records nothinga_fiber_recorder_on_a_non_fiber_element_or_out_of_range_index_records_nothing

An out-of-range Fiber.point/fiber records nothing this call, and a Fiber recorder aimed at a non-fiber element kind (Truss, here) records nothing either — both unresolved-by-runtime-state cases, same graceful-skip handling ModeShape's out-of-range mode gets, since neither can be checked before the analysis actually runs.

ok
An element load recorder follows the pattern factor then the frozen valuean_element_load_recorder_follows_the_pattern_factor_then_the_frozen_value

The ElementLoad recorder reports the load an element carries at each sample's pseudo-time: ramped by its pattern's factor during the stage that ramps it, then constant once the pattern is held — what a consumer needs to recover member internal-force diagrams from the recorded end forces.

ok
Decodes a fiber recorder and matches hand computed strain and stressdecodes_a_fiber_recorder_and_matches_hand_computed_strain_and_stress

The DispBeamColumn pure-axial closed form (core/tests/ elements/fiber_responses.rs's own disp_beam_column_fiber_responses_match_hand_ computed_strain_and_stress), decoded entirely from wire tables: a Fiber recorder on each of a symmetric section's two fibers must read identical strain/stress (pure axial extension, zero curvature).

ok
Decodes a reaction recorder and matches the closed form support reactiondecodes_a_reaction_recorder_and_matches_the_closed_form_support_reaction

The fixed-free truss closed form (core/tests/analysis/reaction.rs's own fixed_free_truss_reaction_exactly_opposes_the_applied_load), decoded entirely from wire tables: a Reaction recorder at the fixed support must read exactly the negative of the applied tip load.

ok
Decodes an elastic beam column3 element force recorderdecodes_an_elastic_beam_column3_element_force_recorder

A 3D ElasticBeamColumn3 cantilever with a tip load, decoded from the wire format, reports local end forces through an element force recorder that match nodal equilibrium (shear equals the load and the fixed-end moment equals the load times the length).

ok
Decodes an element force recorder alongside a node disp recorderdecodes_an_element_force_recorder_alongside_a_node_disp_recorder

The second recorder kind (results-storage-indexeddb.md's "several more types of recorders" plan): an ElementForce recorder alongside a NodeDisp one, on a horizontal cantilever ElasticBeamColumn — a statically determinate case, so the expected local force at both recorded components is plain nodal-equilibrium statics (the same technique core/tests/elements/local_force.rs uses), independent of the element's own stiffness formulation. Also proves node and element batches coexist correctly in one advance() call's recorder_batches, each keyed by its own recorder_index.

shells · 6

The shellSections and shell4s tables, shell pressure/self-weight element loads and shell Gauss-point recorders, decoded and run end to end. The reference values are from OpenSeesPy (ShellMITC4 + ElasticMembranePlateSection) on the same model; the native engine test (core/tests/shells) covers the element itself.

ok
Shell misuse is a decode errorshell_misuse_is_a_decode_error

Shells are 3D only; a shell load targets shells only; bad sections, indices and recorder components are structured errors.

ok
A shell patch runs end to end and matches openseesa_shell_patch_runs_end_to_end_and_matches_opensees

The tilted patch decoded from the wire reproduces OpenSees' displacements, with and without self-weight, and the first element's Gauss-point resultants.

ok
The shell tables round trip through serdethe_shell_tables_round_trip_through_serde

The tables serialize with the expected camelCase shapes and round-trip.

ok
Triangle misuse is a decode errortriangle_misuse_is_a_decode_error

A shell3s table in a 2D model is rejected, and its section index is checked.

ok
Shell pressure loads through the wireshell_pressure_loads_through_the_wire

A pressure through the wire is the same load as the native engine's: the clamp reactions of a fully clamped quad sum to p A along the normal.

ok
A triangle patch runs end to end and matches openseesa_triangle_patch_runs_end_to_end_and_matches_opensees

A triangle patch decoded from the wire format reproduces OpenSees' ShellDKGT displacements and the first triangle's resultants, and the table survives serde.

stages · 9

Analysis stages through the wire format: configurable algorithms, transient and pushover stages, record_initial, and reset_stage.

ok
Algorithm wire format uses tagged objectsalgorithm_wire_format_uses_tagged_objects

Algorithms use tagged objects; Newton settings default to the current tangent.

ok
Gravity then pushover matches native force beam column behaviorgravity_then_pushover_matches_native_force_beam_column_behavior

The first required case: a 2D fiber-section cantilever runs gravity (LoadControl), freezes it, then a displacement-controlled lateral pushover — built the same way core/tests/elements/force_beam.rs builds and checks a ForceBeamColumn cantilever, but assembled entirely from decoded wire tables. Elastic-range and post-yield behavior must agree with that native acceptance test, and gravity's frozen axial displacement must survive the pushover stage untouched (core/tests/analysis/load_patterns.rs's same check, here across a decoded multi-stage AnalysisSequence).

ok
Record initial adds a step zero sample per stagerecord_initial_adds_a_step_zero_sample_per_stage

header.recordInitial adds each static stage's initial conditions as its first sample: load factor 0 and the state the stage inherits (zero for the first, the first stage's end for the second). Without it a stage's samples are only its steps.

ok
Rejects invalid bounded paths before constructing the domainrejects_invalid_bounded_paths_before_constructing_the_domain

Malformed accelerograms are decode errors, never interpolation panics.

ok
Reset stage lets an unheld pattern ramp againreset_stage_lets_an_unheld_pattern_ramp_again

Without a hold, the same pattern ramps again from zero after a Reset.

ok
Reset stage reverts the state but keeps held patterns appliedreset_stage_reverts_the_state_but_keeps_held_patterns_applied

A Reset stage reverts displacements to the as-built state (a later stage starts from zero, not from where the previous one ended). Like OpenSees' reset, a pattern frozen by holdPatternsAfter stays frozen at its final factor: the next stage applies it in full from its first step.

ok
Configurable algorithms solve static and transient yieldingconfigurable_algorithms_solve_static_and_transient_yielding

Nonlinear equilibrium must be the same across each configurable solver.

ok
Modal damping anchors resolve at transient entrymodal_damping_anchors_resolve_at_transient_entry
ok
Decodes a transient stage and matches damped free vibration closed formdecodes_a_transient_stage_and_matches_damped_free_vibration_closed_form

Rayleigh-damped free vibration (core/tests/dynamics/free_vibration.rs's SDOF case), decoded from a Transient stage: matches the closed-form damped free-vibration displacement after a known number of Newmark steps.

Unit tests

carapace_core140 tests
analysis::bordered · 5

The (n+1)-unknown bordered system arc-length continuation solves for its tangent and its coupled corrections (docs/arclength.md §4–§5):

text [ K column ] [x_q ] [rhs_q ] [ row^T corner ] [x_lambda] = [rhs_lambda]

Block elimination by default. Only K is factored; the border is eliminated through its Schur complement corner - row^T K^-1 column. Factoring the bordered matrix directly lets partial pivoting pull the dense constraint row in early, and on a 40-story, 8-bay frame that fills L+U to ~10x what K alone needs and costs ~17x the factor time (measured by the ignored bordered_fill_and_factor_time_on_a_frame test), so the direct factorization is only the fallback: used when K's factorization breaks down (an exactly singular K at a smooth load limit, where the border keeps the system nonsingular) or when the Schur complement suffers cancellation. Every solve is then checked against the full bordered system's backward error either way.

Uses faer's low-level simplicial LU rather than SparseSolver's high-level one because the determinant sign — the bifurcation diagnostic in docs/arclength.md §7 — needs the U diagonal and both permutations, which the high-level type keeps private. With block elimination, det A = det K * schur. COLAMD orderings are cached per pattern (K's pattern only changes with the model; the border is stored with every structural entry, including zeros).

ignored
Bordered fill and factor time on a framebordered_fill_and_factor_time_on_a_frame

docs/arclength.md §5/§10 step 2: bordered fill and factor time on a representative frame, against the n-DOF supernodal LU the other integrators use. Not a pass/fail test — run with cargo test --release -p carapace-core bordered_fill -- --ignored --nocapture.

ok
Determinant sign matches dense value and unknown scales are inertdeterminant_sign_matches_dense_value_and_unknown_scales_are_inert
ok
Permutation paritypermutation_parity
ok
Singular k with nonsingular border solves exactlysingular_k_with_nonsingular_border_solves_exactly
ok
Truly singular bordered matrix is reportedtruly_singular_bordered_matrix_is_reported
analysis::convergence · 4
ok
Nonfinite state never convergesnonfinite_state_never_converges
ok
Combined uses moment tolerance on rotational equations onlycombined_uses_moment_tolerance_on_rotational_equations_only
ok
Small correction never hides large residual under combinedsmall_correction_never_hides_large_residual_under_combined
ok
Validation rejects bad combined optionsvalidation_rejects_bad_combined_options
analysis::solver · 3
ok
Changing values and pivots reuses symbolic but not numeric factorschanging_values_and_pivots_reuses_symbolic_but_not_numeric_factors
ok
Changed pattern with same size and nnz and changed dimensions rebuild cachechanged_pattern_with_same_size_and_nnz_and_changed_dimensions_rebuild_cache
ok
Singular pattern does not poison cache or invalidate owned factorssingular_pattern_does_not_poison_cache_or_invalidate_owned_factors
model::constraint · 4

General linear multi-point constraints: one slave DOF equals a linear combination of master DOFs on any nodes, u_slave = sum(coeff * u_master). equal_dof, the rigid diaphragms and rigid links are all generated as these. Constraints are homogeneous (no offset term), so an increment of a slave follows its masters' increments exactly.

ok
A coefficient other than one is not an alias and fixed masters vanisha_coefficient_other_than_one_is_not_an_alias_and_fixed_masters_vanish
ok
Chains substitute and unit single terms aliaschains_substitute_and_unit_single_terms_alias
ok
Cycles and duplicate slaves are reportedcycles_and_duplicate_slaves_are_reported
ok
Normalize merges repeated masters and drops vanishing termsnormalize_merges_repeated_masters_and_drops_vanishing_terms
model::continuum::jacobian · 3

The 2D isoparametric Jacobian: J = [dx/dxi dy/dxi; dx/deta dy/deta] (row a is the derivative with respect to natural coordinate a), its determinant and inverse, and physical shape-function derivatives.

ok
Affine maps have constant determinant equal to the area scaleaffine_maps_have_constant_determinant_equal_to_the_area_scale
ok
Inverse times matrix is identityinverse_times_matrix_is_identity
ok
Physical derivatives reproduce a linear field gradientphysical_derivatives_reproduce_a_linear_field_gradient
model::continuum::quadrature · 3

Gauss rules as (xi, eta, weight) triples ((t, weight) for the edge rule). Triangle weights sum to the reference area 1/2, quadrilateral weights to 4, edge weights to 2.

ok
Weights give the reference areasweights_give_the_reference_areas
ok
Quad and edge rules integrate cubics exactlyquad_and_edge_rules_integrate_cubics_exactly
ok
Triangle rules are exact to their degreetriangle_rules_are_exact_to_their_degree
model::continuum::shape · 3

Shape functions and reference derivatives of the 3-node triangle (natural coordinates xi, eta on the unit triangle) and the 4-node quadrilateral (xi, eta in [-1, 1]). Node order is counter-clockwise; quad node k sits at the reference corner QUAD4_CORNERS[k].

ok
Quad derivatives match central finite differencesquad_derivatives_match_central_finite_differences
ok
Shape functions are one at their own node and zero elsewhereshape_functions_are_one_at_their_own_node_and_zero_elsewhere
ok
Shape functions partition unity and derivatives sum to zeroshape_functions_partition_unity_and_derivatives_sum_to_zero
model::continuum::validate · 4

Geometry validation with a scale-aware tolerance: every threshold is relative to the element's characteristic length squared (its longest node-to-node distance squared), never an absolute number, so a unit element and a 1e6-scaled copy are judged identically.

ok
Convex trapezoid and parallelogram are acceptedconvex_trapezoid_and_parallelogram_are_accepted
ok
Repeated clockwise and degenerate inputs are rejectedrepeated_clockwise_and_degenerate_inputs_are_rejected
ok
The reflex corner quad is rejected though its gauss point determinants are positivethe_reflex_corner_quad_is_rejected_though_its_gauss_point_determinants_are_positive
ok
The tolerance is scale invariantthe_tolerance_is_scale_invariant
model::dof_table · 2
ok
Affine terms round trip and empty terms collapse to fixedaffine_terms_round_trip_and_empty_terms_collapse_to_fixed
ok
Unknown nodes and unset slots are fixedunknown_nodes_and_unset_slots_are_fixed
model::domain::numbering_tests · 2
ok
Identity ties alias equations and fixed retained dofs drop outidentity_ties_alias_equations_and_fixed_retained_dofs_drop_out

Equation numbers are assigned to free, unconstrained DOFs in node insertion order; an identity-tied DOF shares its retained DOF's equation; a fixed retained DOF leaves its tied DOF without one.

ok
Rigid diaphragm slave dofs resolve to affine termsrigid_diaphragm_slave_dofs_resolve_to_affine_terms

A 3D rigid diaphragm: the slave's in-plane translations are affine in the master's translation and rotation about the normal, with the lever-arm coefficients, and the slave has no equation of its own.

model::domain::domain3_tests · 1
ok
Cantilever truss3 matches analytical axial stiffnesscantilever_truss3_matches_analytical_axial_stiffness

A skew 3D truss cantilever: end load projected onto the bar axis should reproduce simple axial-bar elongation, exercised through the full Domain3 assembly/equation-numbering path (not just Truss3 in isolation, which truss.rs's own tests already cover).

model::elements::elastic_beam_column::spatial_tests · 2
ok
Axis aligned cantilever matches closed form axial biaxial bending and torsionaxis_aligned_cantilever_matches_closed_form_axial_biaxial_bending_and_torsion

A cantilever along global x with vec_xz = [0, 0, 1] (global z), which makes local [x, y, z] = global [x, y, z] exactly (R is the identity) — isolates the local stiffness formula itself from the GeomTransf3 rotation, which transform.rs's and truss.rs's own tests already cover for the transform-only case. With node_i fully fixed, the free-DOF system reduces to local_elastic_stiffness's [6..12, 6..12] block applied directly (no rotation needed), so tip displacements can be checked against textbook cantilever closed forms for axial, biaxial bending, and torsion simultaneously (all four modes are decoupled in a prismatic doubly-symmetric section).

ok
Skew cantilever transform round trips a known local solutionskew_cantilever_transform_round_trips_a_known_local_solution

The same cantilever driven through the full form_tangent_and_ resistance (transform included, not just the local block) with a skew member and a non-trivial orientation, checked by round-tripping: applying the closed-form local displacement (rotated into global coordinates) must reproduce the applied global load exactly, proving GeomTransf3/the local stiffness compose correctly rather than being separately-plausible but inconsistent.

model::elements::mask_conformance · 4
ok
Continuum elements declare exactly the translations they stiffencontinuum_elements_declare_exactly_the_translations_they_stiffen
ok
Planar elements declare the slots they stiffenplanar_elements_declare_the_slots_they_stiffen
ok
Spatial elements declare the slots they stiffenspatial_elements_declare_the_slots_they_stiffen
ok
Shell declares all six slots it stiffensshell_declares_all_six_slots_it_stiffens
model::elements::quad4 · 16

Quad4: the 4-node bilinear isoparametric quadrilateral, 2x2 Gauss integration, [ux, uy] per node (8 DOFs), plane stress or strain through its PlaneMaterial. Each Gauss point owns an independent copy of the material and commits it individually.

# Formulations

[Quad4Formulation::Full] is the plain element. It shear-locks in bending.

[Quad4Formulation::Enhanced] is the Wilson-Taylor incompatible-modes element (QM6/Q6). For each displacement component it adds the two shapes 1 - xi^2 and 1 - eta^2, four internal parameters alpha per element, discontinuous across edges and therefore condensed out locally. The enhanced strain is G~(xi, eta) alpha with G~ = (detJ0 / detJ(xi, eta)) * [J0^{-1} dM/dxi] expanded into strain rows; J0 is the Jacobian at the element center, and the detJ0 / detJ factor (Taylor-Beresford-Wilson) is what preserves the constant-strain patch test on distorted elements. The element stiffness is K_uu - K_ua K_aa^{-1} K_au, the resistance is that stiffness times the nodal displacements (the element is linear), and strains and stresses at the Gauss points are recovered as B u + G~ alpha and D times that, with alpha = -K_aa^{-1} K_au u.

Scope: Enhanced accepts only linear PlaneMaterials (for a nonlinear one alpha would be element internal state needing its own local Newton iteration). It is designed to cure shear (bending) locking. It is not a mixed or B-bar formulation and carries no volumetric-locking guarantee for nearly incompressible plane strain (nu -> 0.5); the plain element locks completely there, and in the tests (Cook's membrane, a pressurized thick cylinder at nu = 0.4999) the enhanced element was observed to do much better, but that is an observation about those problems, not a property of the formulation. Incompatible-mode elements also remain somewhat sensitive to trapezoidal distortion.

ok
A rigid motion is stress freea_rigid_motion_is_stress_free
ok
Body force totals b t a and edge loads total traction times lengthbody_force_totals_b_t_a_and_edge_loads_total_traction_times_length
ok
Condensation reproduces the resistance from the recovered stressescondensation_reproduces_the_resistance_from_the_recovered_stresses
ok
A nonlinear material is rejected by the enhanced element onlya_nonlinear_material_is_rejected_by_the_enhanced_element_only
ok
Cyclic relabeling of the nodes gives the same stiffnesscyclic_relabeling_of_the_nodes_gives_the_same_stiffness
ok
Each gauss point commits its own material historyeach_gauss_point_commits_its_own_material_history
ok
Invalid geometry is reported with the offending cornerinvalid_geometry_is_reported_with_the_offending_corner
ok
Gauss responses equal d b u for the plain elementgauss_responses_equal_d_b_u_for_the_plain_element
ok
Lumped masses are the row sums of the consistent masslumped_masses_are_the_row_sums_of_the_consistent_mass
ok
Mass sums to rho t a in each directionmass_sums_to_rho_t_a_in_each_direction
ok
Prepare is idempotent and the cache equals recomputationprepare_is_idempotent_and_the_cache_equals_recomputation
ok
Pressure acts along the inward normalpressure_acts_along_the_inward_normal
ok
Stiffness matches the closed form for the unit squarestiffness_matches_the_closed_form_for_the_unit_square
ok
Stiffness is symmetric with exactly three rigid body modesstiffness_is_symmetric_with_exactly_three_rigid_body_modes
ok
Recovered stress includes the internal modes and differs from d b urecovered_stress_includes_the_internal_modes_and_differs_from_d_b_u
ok
The enhanced stiffness is softer than the full one in bending onlythe_enhanced_stiffness_is_softer_than_the_full_one_in_bending_only
model::elements::tri3 · 8

Tri3: the 3-node constant-strain triangle. One Gauss point, 6 DOFs [ux, uy] per node, plane stress or strain through its PlaneMaterial.

ok
A rigid motion is stress freea_rigid_motion_is_stress_free
ok
Body force and edge loads total their resultantsbody_force_and_edge_loads_total_their_resultants
ok
Gauss response reports strain and the stress it impliesgauss_response_reports_strain_and_the_stress_it_implies
ok
Invalid geometry and section are reportedinvalid_geometry_and_section_are_reported
ok
Lumped mass sums to rho t a in each directionlumped_mass_sums_to_rho_t_a_in_each_direction
ok
Stiffness matches the closed form for the unit right trianglestiffness_matches_the_closed_form_for_the_unit_right_triangle
ok
Prepare is idempotent and the cache equals recomputationprepare_is_idempotent_and_the_cache_equals_recomputation
ok
Stiffness is symmetric with exactly three rigid body modesstiffness_is_symmetric_with_exactly_three_rigid_body_modes
model::elements::truss::spatial_tests · 2
ok
Mass is lumped over three translations onlymass_is_lumped_over_three_translations_only
ok
Skew truss matches direction cosine stiffness and resistanceskew_truss_matches_direction_cosine_stiffness_and_resistance
model::elements::zero_length · 21
ok
Friction drops capacity when normal dof is in tensionfriction_drops_capacity_when_normal_dof_is_in_tension

Out of compression (tension, per this codebase's sign convention), yield_force clamps to 0: any nonzero shear displacement slides immediately, with only the post-slip hardening term surviving.

ok
Friction3 shear axes slide independently against shared normal forcefriction3_shear_axes_slide_independently_against_shared_normal_force

ZeroLength3 with one normal DOF and two independent shear DOFs — each slides/sticks independently against the same normal force with no cross-talk between the two shear axes (the absence of coupling that distinguishes this from the deferred, circular- interaction-surface model).

ok
Friction remembers permanent slip after commitfriction_remembers_permanent_slip_after_commit

Mirrors elastic_pp_remembers_permanent_set_after_commit (elastic_like.rs): slide past the yield surface, commit, partially unload — the response must be elastic from the new committed slip, not from zero.

ok
Friction slides past yield surfacefriction_slides_past_yield_surface

Past the yield surface, V follows the bilinear-kinematic-hardening return map (Friction::evaluate's doc comment): V = b*trial + (1-b)*yield_force*sign(trial), tangent == b*k0.

ok
Friction sticks below yield surfacefriction_sticks_below_yield_surface

Below the yield surface, friction is a plain elastic spring: V == k0 * shear_rel, tangent == k0.

ok
Friction yield surface scales with normal forcefriction_yield_surface_scales_with_normal_force

The same shear displacement against two different normal-direction states: shear resistance depends on axial load level, not just a generic plasticity check — a lighter normal force slides at the same shear displacement a heavier one still sticks at.

ok
Oriented zero length3 rotates translations and rotations togetheroriented_zero_length3_rotates_translations_and_rotations_together

local x = global Z, local y = global X, local z = global Y: so local rx is global rz — rotations ride the same frame as translations.

ok
Oriented zero length at 45 degrees splits force and stays consistentoriented_zero_length_at_45_degrees_splits_force_and_stays_consistent

45 degrees: a unit global ux is cos 45 of local x, so the force splits equally between global x and y, and K stays consistent with the resistance (linear material: r = K u).

ok
Oriented zero length section3 axis follows local xoriented_zero_length_section3_axis_follows_local_x
ok
Oriented zero length section axis follows local xoriented_zero_length_section_axis_follows_local_x
ok
Oriented zero length spring follows the local x axisoriented_zero_length_spring_follows_the_local_x_axis

local x = global y (a 90 degree turn about z): a spring on dof 0 must resist relative uy, and nothing along global ux.

ok
Friction off diagonal tangent matches finite differencefriction_off_diagonal_tangent_matches_finite_difference

The most important test here, and the easiest place to get a sign or chain-rule factor wrong: perturb the normal DOF's relative displacement by a small h and confirm the analytical k[shear_dof, normal_dof] block matches dV/d(normal_rel) by finite difference while sliding — validates Friction::evaluate's d_force_d_normal (and the chain rule through the normal material's own tangent in form_tangent_and_resistance) directly, rather than trusting the derivation by inspection.

ok
Zero length dispatches across material regimeszero_length_dispatches_across_material_regimes

Proves the Material enum's dispatch generalizes to ZeroLength across every material variant and regime (elastic, plastic plateau, open gap, closed gap, tension, compression) by driving the element directly off manually-set node displacements — independent of Analysis/Algorithm, which is linear-only here (no Newton iteration) and so can't itself resolve equilibrium across a material's nonlinear regimes in one step.

ok
Zero length3 dispatches independently per global directionzero_length3_dispatches_independently_per_global_direction

ZeroLength3's spatial counterpart, exercising a rotational DOF (index 3, SpatialDof::Rx) in addition to a translational one — the planar ZeroLength test above only ever touches dof 0.

ok
Oriented zero length 2d rejects out of plane and degenerate framesoriented_zero_length_2d_rejects_out_of_plane_and_degenerate_frames
ok
Zero length section3 combines with an independent torsion springzero_length_section3_combines_with_an_independent_torsion_spring

ZeroLengthSection3 analogue: an rx torsion spring alongside the section's biaxial axial-moment response — the only way this element gets torsional stiffness at all (see the type's doc comment).

ok
Zero length section3 reproduces biaxial ea eiy eizzero_length_section3_reproduces_biaxial_ea_eiy_eiz

ZeroLengthSection3: four corner elastic fibers reproduce EA, EIy, EIz (see fiber_section.rs's four_corner_elastic_fibers_reproduce_ea_eiy_eiz_with_zero_cross_coupling) routed through the element's B-matrix mapping onto [ux, ry, rz]; uy/uz/rx — DOFs the section has no resultant for — stay zero.

ok
Zero length section combines with an independent shear springzero_length_section_combines_with_an_independent_shear_spring

A uy spring alongside the section is additive and uncoupled: it contributes independently to k/r on top of (not instead of) the section's coupled axial-moment response.

ok
Zero length section couples axial and moment for an asymmetric sectionzero_length_section_couples_axial_and_moment_for_an_asymmetric_section

An off-centroid single fiber couples axial and moment response (k_section[0][1] != 0) — something ZeroLength's independent per-DOF materials can never produce, since each of its directions is evaluated against its own material in isolation.

ok
Zero length section commit propagates fiber historyzero_length_section_commit_propagates_fiber_history

A section commit propagates into the element's subsequent trial response, the same commit/trial split Material and ZeroLength already rely on — proves ZeroLengthSection::commit actually drives FiberSection::commit (not just recomputing eps0/kappa and discarding them).

ok
Zero length section reproduces symmetric ea eizero_length_section_reproduces_symmetric_ea_ei

ZeroLengthSection: two symmetric elastic fibers reproduce the closed-form EA/EI (already validated directly against FiberSection::trial in fiber_section.rs's two_symmetric_elastic_fibers_reproduce_ea_ei_exactly), now routed through the element's B-matrix mapping onto [ux, rz].

model::fiber_section · 3
ok
Four corner elastic fibers reproduce ea eiy eiz with zero cross couplingfour_corner_elastic_fibers_reproduce_ea_eiy_eiz_with_zero_cross_coupling

FiberSection3 biaxial analogue of two_symmetric_elastic_fibers_ reproduce_ea_ei_exactly: four corner fibers at (±hy, ±hz) reproduce EA, EIy, EIz exactly and give zero product-of-inertia coupling (dMz/dky / dMy/dkz = 0) by symmetry — verifying the eps0 - y*kappa_z + z*kappa_y strain field and the N/Mz/My sums independently of any element that will later drive this section.

ok
Steel and concrete fiber section matches hand computed n m through yield and partial unloadsteel_and_concrete_fiber_section_matches_hand_computed_n_m_through_yield_and_partial_unload

Nonlinear composite check: a real reinforced-concrete-style section — one Steel01 rebar layer plus two Concrete01 layers — pushed past first yield/cracking and then partially unloaded. No closed form exists for a nonlinear composite section (unlike the elastic case above), so each state is checked against a hand computation: evaluate every fiber's Material directly at that fiber's own strain and sum N = sum(stress*area), M = -sum(stress*area*y) — the same definition FiberSection::trial uses, but computed independently, fiber by fiber, outside it.

ok
Two symmetric elastic fibers reproduce ea ei exactlytwo_symmetric_elastic_fibers_reproduce_ea_ei_exactly
model::integration · 2
ok
Lobatto includes the endpointslobatto_includes_the_endpoints
ok
Points are symmetric and weights sum to onepoints_are_symmetric_and_weights_sum_to_one
model::load_pattern::bounded_path_tests · 1
ok
Bounded accelerogram is zero outside its samplesbounded_accelerogram_is_zero_outside_its_samples

OpenSees-style accelerogram bounds, interpolation and useLast; exact arithmetic.

model::materials::composite · 6
ok
Min max fails permanently once strain exits boundsmin_max_fails_permanently_once_strain_exits_bounds
ok
Parallel history is independent per childparallel_history_is_independent_per_child
ok
Parallel of two elastics equals one elastic with summed stiffnessparallel_of_two_elastics_equals_one_elastic_with_summed_stiffness
ok
Series splits strain so each child sees equal stressseries_splits_strain_so_each_child_sees_equal_stress
ok
Series of two equal elastics halves the stiffnessseries_of_two_equal_elastics_halves_the_stiffness
ok
Min max passes through inner material before failuremin_max_passes_through_inner_material_before_failure
model::materials::concrete01 · 3
ok
Concrete01 carries no tensionconcrete01_carries_no_tension
ok
Concrete01 unloads with degrading stiffness below initialconcrete01_unloads_with_degrading_stiffness_below_initial
ok
Concrete01 matches parabolic envelope on first loadingconcrete01_matches_parabolic_envelope_on_first_loading
model::materials::concrete02 · 4
ok
Concrete02 matches compression envelope on first loadingconcrete02_matches_compression_envelope_on_first_loading
ok
Concrete02 softens in tension past ftconcrete02_softens_in_tension_past_ft
ok
Concrete02 is linear elastic in tension below ftconcrete02_is_linear_elastic_in_tension_below_ft
ok
Concrete02 unloads with degrading stiffness below initialconcrete02_unloads_with_degrading_stiffness_below_initial
model::materials::elastic_like · 4
ok
Elastic pp clamps symmetrically on first loadingelastic_pp_clamps_symmetrically_on_first_loading
ok
Elastic pp remembers permanent set after commitelastic_pp_remembers_permanent_set_after_commit
ok
Ent carries no tensionent_carries_no_tension
ok
Gap only engages past closuregap_only_engages_past_closure
model::materials::hysteretic · 4
ok
Hysteretic energy accumulates as half trapezoid of stress over strainhysteretic_energy_accumulates_as_half_trapezoid_of_stress_over_strain
ok
Hysteretic gives exact initial tangent at zero strainhysteretic_gives_exact_initial_tangent_at_zero_strain
ok
Hysteretic matches bilinear envelope on first loadinghysteretic_matches_bilinear_envelope_on_first_loading
ok
Hysteretic pinches on reload after a full reversalhysteretic_pinches_on_reload_after_a_full_reversal
model::materials::pinching4 · 5
ok
Pinching4 matches the six point envelope on first loadingpinching4_matches_the_six_point_envelope_on_first_loading

Test 2: state 1 rides the 6-point positive envelope. Checks every real breakpoint (1-4), a midpoint of every real segment, and the extrapolated point-5 segment beyond strain4p.

More

Note the point-0/point-1 segment: it runs from (1e-6, 1e-3) to (0.01, 10), i.e. slope exactly 1000 and therefore indistinguishable from a straight elastic line through the origin — which is the whole point of the synthesized point.

ok
Pinching4 falls back to a linear path when the reload segment is too stiffpinching4_falls_back_to_a_linear_path_when_the_reload_segment_is_too_stiff

Test 4: the same reversal, but with pinching parameters chosen to drive one of get_state3's degenerate-geometry fallbacks — the "linear unload-reload path expected" branch, taken when the constructed segment from point 1 to point 2 is stiffer than kmax (the greater of the unload stiffness and the damaged negative elastic stiffness, both 1000 here).

More

r_disp = 0.05 pulls point 1 in close to the origin while r_force = 0.9 keeps its stress near the full envelope value, so the 1-to-2 secant comes out at (−0.85+9)/(0.00415+0.0005) ≈ 1753, over kmax. The whole path is then discarded and redrawn as the straight 33%/67% split between points 0 and 3: (-0.01,-10) -> (-0.0001,-1.75) -> (0.0101, 6.75) -> (0.02, 15).

ok
Pinching4 gives exact initial tangent at zero strainpinching4_gives_exact_initial_tangent_at_zero_strain

Test 1: state 0 (NearOrigin) gives the synthesized near-origin point's slope exactly. SetEnvelope puts that point at u = 1e-4*strain1p with stress u*k, k = max(stress1p/strain1p, stress1n/strain1n) = 1000, so the state-0 tangent is exactly 1000 — the same slope as the first real backbone segment, but arrived at through an entirely different code path.

More

"Exactly" up to one ulp: the tangent is recovered as (u*k)/u rather than as k itself, which for u = 1e-6 lands on 1000.0000000000001. Reconstructing k directly would be a deviation from the source, so the test carries the tolerance instead.

ok
Pinching4 reverses from state three into state fourpinching4_reverses_from_state_three_into_state_four

The Trilinear3 -> Trilinear4 transition, i.e. reversing again mid-reload. Exercises the one getstate branch that both refreshes the positive damaged envelope and rebuilds the bracket from the committed point, and confirms the resulting state-4 path is anchored at that reversal point.

ok
Pinching4 reloads along a clean trilinear path after a reversalpinching4_reloads_along_a_clean_trilinear_path_after_a_reversal

Test 3: a full reversal off the positive envelope into state 3, checked against a hand-walked get_state3 trace that takes the clean trilinear path — no degenerate-geometry fallback fires.

More

r_disp <= r_force (0.2 vs. 0.4) is what keeps the reload-stiffness cap from engaging: the secant from point 1 to point 2 comes out at 1000*(1-r_force)/(1-r_disp) = 750, under the elastic 1000.

Hand trace, after committing 0.005 then 0.02 (so Cstrain = 0.02, Cstress = 15, max_strain_dmnd still at its initial 0.01): reversing to -0.005 sets max_dmnd = 0.02, and since -0.005 is not past u_min_damgd = -0.01, the new state is 3 with bracket low = (-0.01, negEnvlpStress(-0.01) = -10) and hgh = (0.02, 15). Then: point 1 = (-0.01*0.2, -10*0.4) = (-0.002, -4); point 2 = stress u_force*envlp_neg[3] = 0.05*-17 = -0.85, strain 0.02 - (15+0.85)/1000 = 0.00415. Both survive every check, so the path is (-0.01,-10) -> (-0.002,-4) -> (0.00415,-0.85) -> (0.02,15).

model::materials::plane · 9

PlaneMaterial: the n-D material seam for 2D continuum elements.

Same trial/commit design as the uniaxial Material (see its doc comment): a value holds only committed state, trial_stress_tangent is pure, and commit returns the new committed value. An element owns one independent PlaneMaterial per Gauss point, cloned from the prototype given to its constructor and committed individually, exactly as the fiber elements own one material per fiber. That is the seam where nonlinear plane materials (CSFM, damage, plasticity) plug in with per-point history.

Conventions, fixed now so nonlinear materials later do not change them: the strain vector is [eps_x, eps_y, gamma_xy] with engineering shear gamma_xy = 2 eps_xy; the stress vector is [sigma_x, sigma_y, tau_xy]; D maps one to the other.

ok
A stateful material keeps independent history per copya_stateful_material_keeps_independent_history_per_copy
ok
D is symmetric and positive definite for every variantd_is_symmetric_and_positive_definite_for_every_variant
ok
Elastic matrix packing matches the equivalent isotropic delastic_matrix_packing_matches_the_equivalent_isotropic_d
ok
Elastic variants are linear and commit changes nothingelastic_variants_are_linear_and_commit_changes_nothing
ok
Invalid constants are errors not panicsinvalid_constants_are_errors_not_panics
ok
Plane stress with effective constants equals plane strainplane_stress_with_effective_constants_equals_plane_strain
ok
Rotation by pi over four couples normal and shearrotation_by_pi_over_four_couples_normal_and_shear
ok
Orthotropic with equal moduli is isotropic for any angleorthotropic_with_equal_moduli_is_isotropic_for_any_angle
ok
Rotating by ninety degrees swaps the axesrotating_by_ninety_degrees_swaps_the_axes
model::materials::steel01 · 4
ok
Steel01 bilinear hardening past yieldsteel01_bilinear_hardening_past_yield
ok
Steel01 isotropic hardening shifts the bound on reversalsteel01_isotropic_hardening_shifts_the_bound_on_reversal
ok
Steel01 matches elastic on first loading within yieldsteel01_matches_elastic_on_first_loading_within_yield
ok
Steel01 trial at committed strain keeps committed tangentsteel01_trial_at_committed_strain_keeps_committed_tangent
model::materials::steel02 · 4
ok
Steel02 reversal starts out near elastic stiffnesssteel02_reversal_starts_out_near_elastic_stiffness
ok
Steel02 gives exact initial tangent at zero strainsteel02_gives_exact_initial_tangent_at_zero_strain
ok
Steel02 tangent approaches hardening slope far past yieldsteel02_tangent_approaches_hardening_slope_far_past_yield
ok
Steel02 tracks peak strain across a reversalsteel02_tracks_peak_strain_across_a_reversal
model::node · 1
ok
Planar and spatial aliases share one node implementationplanar_and_spatial_aliases_share_one_node_implementation
model::transform::geom_transf3_tests · 3
ok
Axis aligned member with z up gives identity rotationaxis_aligned_member_with_z_up_gives_identity_rotation
ok
Coincident endpoints panicscoincident_endpoints_panics
ok
Vec xz parallel to axis panicsvec_xz_parallel_to_axis_panics