Skip to content

Numerical controls and input/output contracts

These controls extend the existing workflow. They do not silently change the physical problem or replace the declared numerical method.

Mesh and field inputs

mesh.from_arrays requires a 2D integer connectivity array, nonnegative global indices and finite real coordinates shaped (points, dimension). Integer coordinates become float64; explicit float32/float64 are retained. The supplied coordinate element is preserved, including high-order geometry. Input errors are exchanged across MPI ranks before collective mesh construction.

expressions.interpolate restores original field values if interpolation fails or produces non-finite output. Numerical interpolation failure on any rank rolls back values on every rank. Expression structure and parameters should be consistent across ranks. Known-field interpolation is distinct from symbolic unknowns used in a nonlinear residual.

Time updates and matrix reuse

The fixed-step transient field procedure snapshots fields before updating loads. A failed load update or solve restores the fields and re-applies loads at the last accepted time. Load-update callbacks should set values from time, rather than accumulate irreversible side effects. If load restoration fails, the original exception retains a note explaining that failure.

PreparedLinearProblem reuses its matrix for RHS-only or prescribed-value changes. After a bilinear coefficient changes, call refresh_matrix() before solving again. Mesh, space or constrained-dof changes require a new prepared problem. summary() reports matrix and RHS assembly counts. Refresh uses the existing solver configuration; no unconditional preconditioner-reuse flag is introduced.

The generalized-Maxwell adaptive path already uses step doubling and rollback. Its next accepted step is now limited by a bounded proportional error controller as well as the existing iteration controller. time.error_step_factor computes safety*(tolerance/error)**(1/(order+1)), bounded by the supplied factors. It proposes a step size; it does not itself accept a solution. This is a local error estimate, not a guarantee on accumulated global error or speedup for every case.

Transport and pressure

operators.intrinsic_time_scale accepts optional time_step, adding (2/dt)^2 to the inverse-squared advective/diffusive scale. Stagnation points avoid zero denominators; the SUPG contribution still depends on the streamline test term. This scale does not guarantee monotonicity, and the discrete transient residual must remain consistent with the chosen time scheme.

solvers.attach_nullspace(A, modes, rhs=b) accepts explicit physical null modes in the assembled matrix layout, orthonormalizes copies and tests them against A. For symmetric systems the optional RHS check detects incompatible load/flux. It does not pin pressure or silently project a physical imbalance away. The caller destroys the returned PETSc NullSpace. A constant pressure mode is only appropriate when boundary conditions leave that reference free; pressure or traction conditions may remove it. Nonsymmetric problems require separate left-nullspace treatment, not the optional symmetric-RHS check.

Nonlinear convergence and failure information

In standard finite-strain J2, the normalized path coordinate remains in [0, 1], while the resolved Step amplitude may reverse sign or exceed one. Ordinary strong reference values and natural loads use that amplitude. A fixed contact tool has no motion coordinate to update. Prescribed moving tools still enforce the admitted range of their motion definition; supporting a signed traction amplitude does not authorize extrapolating a tool schedule. PrescribedValuePath accepts finite signed scaling for constant/field values. When independent boundary histories are present, its input remains their normalized coordinate. Standard finite-J2 Steps continue to reject those independent histories, requiring one explicit Step amplitude.

Affine Newton backtracking now requires the stated sufficient residual decrease (or meeting the absolute residual tolerance). A merely smaller residual no longer bypasses that condition. Existing load cutback, material trial/commit and restart paths remain in use.

solvers.newton(line_search="basic") instead takes the full finite, material-admissible Newton update, even when the residual temporarily grows. line_search=None explicitly disables the search with the same meaning; it is not a request for PETSc's default backtracking. These choices now have the same meaning in serial/distributed affine reduction and the ordinary Newton backend. They do not waive final equilibrium, contact acceptance or constitutive validity, and are not a universal robustness or speed setting. Backtracking remains the default. An earlier affine implementation incorrectly treated basic Newton as a single Armijo trial; affected experimental basic/ no-search checkpoint identities now reject rather than silently changing the algorithm on continuation. Default backtracking identities are unchanged. The full-step interpretation follows PETSc's basic line-search implementation; it does not imply that full Newton is preferable on an arbitrary contact path.

Linear nonconvergence raises diagnostics.ComputationalFailure, retaining RuntimeError compatibility. exception.as_dict() includes a stable code, solver stage, PETSc reason, iterations, residual and suggested next checks. These are diagnostics, not a claim that one root cause has been proven.

solvers.newton(linear_solver=solvers.LinearSolverOptions(...)) preserves explicit inner rtol, atol and max_it on both SNES and affine-reduced routes. These control the linear correction, not the nonlinear equilibrium or contact tolerance. Unspecified SNES inner tolerances retain PETSc defaults; the existing affine relative-tolerance/iteration defaults are unchanged. Earlier adapters dropped these settings on SNES and dropped atol on the affine route. Explicitly configured policies now have a corrected transport identity, so an old affected checkpoint is not silently resumed under changed behavior. Unspecified default policies retain their existing identity.

Solver tolerances must be finite positive real values; iteration limits must be positive integers. Boolean values are not numerical controls. Invalid settings reject at policy construction rather than reaching the solver. The same early-validation principle applies to incrementation: steps.fixed requires a positive integer (not a truncated float or numeric string), automatic counts must be integers, and step sizes/growth/cutback controls must be finite real numbers in their declared ranges. max_cutbacks=0 explicitly disables retries and remains valid. Existing valid Python policy summaries retain their checkpoint identity; non-Python numeric scalars are normalized for JSON output. For supported inelastic Steps, maximum_inelastic_increment limits the accepted internal-variable increment and can trigger a cutback. It is not an estimate of force, displacement or accumulated path error. Small Newton residuals and an accepted plastic-increment bound do not replace mesh/path refinement at common physical stations.

The initial increment needs its own check when reporting accepted-path work. An increment can satisfy the plastic-state cap while crossing the onset of yielding; endpoint trapezoidal work then misses part of the load-path curvature. Reducing the Newton tolerance does not resolve that path integration error. Compare the first accepted work/energy change before refining every later increment. Use the existing initial control explicitly; do not infer one universal value from a particular material or contact example. Likewise, a lagged material predictor does not foresee the first contact event. MOOSE's material timestep limiter and separate function-change control illustrate the distinction between material-change and loading-path controls; they are references, not additional AgentFEM capabilities.

Accepted-material prediction (experimental, opt-in)

Serial displacement-only finite-strain J2 Steps with one built-in material may cap the next automatic increment using the last accepted plastic increment:

incrementation = steps.automatic(
    initial=0.02, minimum=1e-5, maximum=0.1, max_increments=1000,
    maximum_inelastic_increment=0.001,
    inelastic_prediction_safety=0.8,
)
step = model.step(target=displacement, incrementation=incrementation)

Both ordinary strong boundaries and supported affine-periodic constraints use the same controller. If h is the accepted increment, p its maximum plastic increment, L the declared limit and s the safety fraction, the proposal is max(minimum, min(Newton_proposal, h*s*L/p)). For p=0 it retains the ordinary Newton proposal. The cap never increases that proposal. The original hard acceptance check still applies to the next solve: prediction is lagged, does not foresee new contact or yielding, and cannot force acceptance at minimum. Loading knots, cutbacks, solver tolerances and the physical model are unchanged.

The default is disabled, preserving existing policy summaries and checkpoint identities. Enabling prediction adds its versioned algorithm and safety to identity; affine restart also binds the named acceptance measure. Resume restores the saved next increment. Invalid/missing evidence is an error, and failed proposal finalization restores the joint accepted boundary. No accepted field or energy history may include that failed attempt. Existing custom two-argument iteration policies remain callable when prediction is disabled.

Mixed displacement-pressure, regional materials, other material models and MPI are not admitted for this option yet. Capability inspection and execution reject unsupported requests; these restrictions do not apply to their existing non-predictive paths. Expert affine providers can explicitly name an inelastic_increment_measure in their acceptance_check record; its units and meaning must agree with the declared limit. That protocol is not automatic scientific validation of an external model.

This reduces avoidable rejected work; it is not a force-error estimator or an explicit-dynamics stability bound. Compare reaction, plastic state, contact and work histories at common physical stations and refine the limit. A larger or smaller number of accepted increments is not itself an accuracy criterion. The design is informed by MOOSE's lagged material timestep limit and its combination with iteration-based adaptation, not by a claim that another code's performance transfers to AgentFEM.

The reproducible bounded comparison is tools/benchmark_inelastic_prediction.py; it retains failed records, a finite fixed-path reference, common stations, repeated fields, ABBA timing and source identity. It does not establish continuum convergence or energy closure of the Gaussian-contact diagnostic.

A four-cell-edge cyclic P2/J2/plane-penalty/periodic-MPC comparison, with a maximum accepted plastic increment of 0.00075, records ordinary/prediction/ prediction/ordinary times of 186.897, 115.284, 114.090 and 185.903 s. The prediction reduces mean setup/solve/observe time by 38.47%, rejected attempts from 140 to 13 and total Newton corrections from 1300 to 806. Accepted increments change from 250 to 232: this is a different discretized path, not an equivalent-execution optimization. Across four common stations the largest reaction difference against the 512-increment reference is 0.204%, displacement vector relative difference 0.0235%, and reference-volume mean PEEQ difference 0.00448%. The reference is finite, not exact; this does not establish spatial convergence or guarantee those errors on another problem. Repeated fields pass rtol=2e-8, atol=2e-10. The source-frozen report is evidence/performance/2026-10-23-inelastic-increment-prediction.json. Do not add this percentage to the separate unchanged-path execution gain.

A prospective refinement at 33412d59 uses a 1024-increment reference and limits 0.00075 / 0.000375. Maximum common-station differences to that reference decrease from 0.202% to 0.104% in reaction, 0.0384% to 0.0119% in displacement vector, and 0.0291% to 0.00166% in volume-mean PEEQ. Accepted increments increase from 232 to 449; stricter resolution costs more work. The 512 -> 1024 fixed-reference final reaction itself changes by 0.405%, so neither reference should be labelled exact and the earlier smaller PEEQ difference to 512 steps is not an accuracy certificate. See evidence/performance/2026-10-23-inelastic-increment-refinement.json; this single refinement comparison makes no additional repeated-timing claim.

Solver and assembly policies

Custom NewtonSolverOptions.line_search_reduction and minimum_step_length apply to AgentFEM's native Newton loops. The SNES adapter now refuses custom backtracking values rather than silently dropping them. Default SNES behavior is unchanged: PETSc BT uses its own polynomial/backtracking search, not the native fixed-ratio rule. Selecting a common backtracking name does not promise identical trial sequences.

The serial affine frozen-equilibrium loop reuses its KSP object only while the constraint reduction is fixed. Each Newton solve still assembles and installs the current tangent. By default, no numerical preconditioner is deliberately frozen. The bounded normal-augmentation Procedure can retain this allocation across its outer iterations within one attempted increment; it never survives acceptance, cutback or restart. The object is released on success, failure or interruption. A supplied KSP remains caller-owned in solve_matrix_system; a one-shot KSP is always released by that helper, including exceptions during setup or solve.

For an implicit problem, a direct factorization package can be selected without changing the model or implementing a new solver:

solver_options = solvers.newton(
    linear_solver=solvers.direct_solver(package="mumps"),
)

This requires a PETSc build containing MUMPS. It is an explicit choice, not a portable speed guarantee or a new default. The default direct_solver() leaves the package selection to PETSc. Record the selected backend when comparing runtime: "LU" alone does not identify the factorization implementation. PETSc documents MUMPS as an optional direct backend. Iterative alternatives must also converge the total nonlinear problem at the declared accuracy; they do not justify loosening contact or material criteria. In particular, an ILU preconditioner is not generally symmetric, so a CG choice cannot be justified just by a symmetric-looking stiffness matrix; see PETSc PCILU.

Optional adaptive preconditioning

The serial affine Newton route also accepts an explicit managed linear policy:

solver_options = solvers.newton(
    linear_solver=solvers.LinearSolverOptions(
        ksp_type="gmres", pc_type="lu", factor_solver_type="mumps",
        rtol=1e-12, atol=1e-14, max_it=50,
        preconditioner_reuse="adaptive",
    ),
)

Every correction still uses the current total tangent, including contact and MPC reduction. Only the earlier factorization may be used as a right preconditioner; it is never substituted for the current matrix. The owner checks ||A x - b|| <= max(atol, rtol * ||b||) explicitly. An unsuccessful reused correction gets one fresh-preconditioner retry, after which ordinary nonconvergence/cutback rules apply. More than eight Krylov iterations causes the next correction to rebuild. These controls do not loosen nonlinear, contact or constitutive tolerances.

This first policy requires GMRES, LU or ILU, explicit positive linear tolerances and an iteration limit. MPI, raw KSP/SNES adapters and other nonlinear routes reject it rather than silently discard its verification contract. Defaults and existing direct-solver checkpoint identities are unchanged. The selected policy is recorded; factors themselves are never checkpointed. Increment checks report preconditioner uses/builds, rebuild retries and the maximum true linear residual-to-tolerance ratio.

A twelve-cell-edge, 32-increment cyclic finite-J2/plane-contact ABBA screen measured 13.39% less solve-and-observe time with this algorithm, retaining the same 355 Newton corrections and agreeing at four physical stations. Fresh 48-increment confirmation through the integrated public policy measures 13.73% less mean time (226.635, 195.011, 195.842, 226.402 s), with the same 533 Newton corrections and passing all station comparisons. These are bounded implicit results, not composite explicit-run or hardware-independent speed guarantees. See evidence/performance/2026-10-23-adaptive-preconditioner-screen.json and evidence/performance/2026-10-23-adaptive-preconditioner-confirmation.json. An additional combined ABBA comparison disables retained material trials and the outer linear workspace in the direct reference, then enables those owners with the managed policy. Its four times are 261.223, 196.909, 195.732 and 257.591 s: 24.32% less mean solve-and-observe time. All runs retain 48 accepted increments and 533 Newton corrections; U, committed state, first Piola stress, tangent, pressure and normal work pass the declared four-station comparison (rtol=1e-7, atol=1e-9). The two candidate paths each reuse 172 material responses and 435 preconditioners, with 98 preconditioner builds and no retry. This measures the combination, not a sum of separate percentage gains. The reference and candidate use the same material, load path, penalty and contact acceptance. See evidence/performance/2026-10-23-combined-affine-execution.json. The implementation follows PETSc's distinction between reusing a preconditioner and choosing its side.

Experimental constant-normal plane augmentation

Finite-strain J2 accepts an opt-in outer augmented-Lagrangian iteration through the ordinary Step. The default remains penalty contact:

step = model.step(
    target=displacement, material=material,
    incrementation=steps.fixed(4),
    normal_augmentation=steps.normal_augmentation(
        penetration_tolerance=1e-7,
        complementarity_tolerance=1e-6,
        length_unit="m", pressure_unit="Pa",
        maximum_augmentations=120,
    ),
)

Declare a named rigid_contact_pair with one fixed or translating analytical plane and a scalar penalty. The bounded implementation uses coalesced nodal reference-area weights on serial P1 tetrahedra and ordinary strong boundaries. A multiplier whose normal motion is already fixed by a strong boundary is rejected; the software does not arbitrarily divide reactions between redundant constraints. Rotating or curved tools, friction, general MPC, mixed fields, higher-order/hex cells, custom contact quadrature and changing contact partitions are not admitted here. This is not a mesh-independent mortar stability or general contact claim.

One narrow affine/MPC route also uses the same normal_augmentation option. It requires one exact periodic constraint and disjoint nonzero reduced normal row supports after u = T q + u_bar. The sparse admission rejects contact nodes whose normal motion is prescribed, as well as overlapping reduced supports; it does not delete rows or arbitrarily distribute contact pressure. Overlap may be mathematically solvable, but is outside this sufficient bounded admission. The condition proves discrete row independence, not mesh-uniform inf-sup stability. A plane imposed against immovable periodic corners therefore does not become admissible just because a smaller increment is used.

examples/finite_j2_periodic_augmented_plane.py gives an ordinary Step example with an interior contact patch, plastic loading/unloading/recontact, accepted displacement frames and restart. The same affine Procedure performs Newton, cutback, joint State acceptance and checkpointing. Its checks record outer updates and the largest inner Newton count separately. Signed relative normal work, its discrete absolute bound and open-gap contribution appear under <pair>_normal_* histories; they are not contact_potential, dissipation or automatic energy verification. Ordinary MPC/tool generalized work remains in the existing constraint histories.

The example's default point-pinned periodic corner class carries the net one-sided tool force. It is a discrete lifecycle demonstration, not a suitable continuum mesh-accuracy reference. The optional --affine-end-face-support declares finite-area x-motion guides on the two z end faces while leaving the interior contact patch free in its normal direction. This is a different physical fixture, not numerical stabilization or a repair of the contact law; its checkpoint identity differs. The associated sensitivity tool records which fixture was used and compares observables at common physical path stations.

A prescribed translation uses the existing prescribed_rigid_motion_schedule on the rigid body, with normalized endpoints 0 and 1 and no separate amplitude. For strong boundaries the ordinary Step amplitude owns the tool coordinate, including unloading and recontact. Multipliers stay on the same material-side reference nodes; every trial projects the current positions against the current tool pose. This does not require transferring history between tool facets, nor license such transfer. Motion along the plane is frictionless and contributes no work. The affine route instead uses its normalized macro-path coordinate for the tool schedule and the declared deformation-gradient path for the macro motion; it does not accept a second independent Step amplitude.

For gap g (negative in penetration), the inner solve uses fixed multiplier lambda and pressure p = max(0, lambda - penalty*g). Only after total Newton equilibrium may the outer iteration update lambda. Acceptance requires both penetration and complementarity: max(abs(lambda*g), abs(p*g)). The latter tolerance has pressure-times-length units (Pa m in the example), not force units. Unit strings declare the model's coherent units; they do not convert values. The penalty retains pressure-per-length units. A small penalty can require many outer iterations; a large one can worsen linear conditioning. Neither a faster solve nor universal penalty independence is promised. Do not confuse the load increment with Newton's minimum_step_length: the latter limits residual backtracking along one Newton correction. A very stiff contact onset can require a shorter correction even when the load step is adequate. The public NewtonSolverOptions exposes that floor explicitly; it is never reduced silently. A smaller floor permits more trial evaluations, not a weaker convergence or penetration tolerance. For automatic incrementation, Newton difficulty is the maximum inner Newton iteration count, not their sum over all augmentations. The latter remains in the total-work report. This prevents repeated easy inner solves from spuriously shrinking the load step. An exhausted outer budget still rejects the increment and follows the ordinary cutback policy.

Materials always start their trial update from the same accepted state during the entire outer loop. One accepted increment jointly commits displacement, plastic history, multiplier and prescribed tool pose. Newton/outer failures restore that boundary; an inner Newton success cannot be checkpointed. The ordinary serial save_checkpoint/load_checkpoint stores the multiplier basis, policy and accepted configuration and rejects incompatible restoration.

The October 19 candidate now uses the original mesh vertex coordinates for linear P1/Q1 contact traces, after identifying each vertex's displacement block. This removes coordinate-tabulation roundoff without moving the mesh, snapping to a tool or loosening an overlap limit. Experimental checkpoints created before this correction can have a different trace fingerprint and are intentionally rejected. Continue those runs with their original frozen candidate, or start a new run; there is no automatic multiplier transfer.

Result reports the actual contact force and augmentation counts. Reference-area pressure is not Cauchy pressure. The signed AL functional is not physical contact stored energy. The accepted ledger now reports signed numerical normal_contact_work separately, together with a non-cancelling normal_contact_work_bound and the positive-gap component normal_contact_open_gap_work. For matched constant-normal plane samples these are sum(w * (p_old+p_new)/2 * delta_gap) and, for the bound, the same sum with abs(delta_gap). For a fixed tool positive work enters the structure. For a translating tool this is relative-gap work: structural contact work equals tool work plus relative-gap work. Tool work is already part of external work, so it must not be added twice. Opening/closure can produce nonzero endpoint work even with exact complementarity at both ends. The bound applies only to this discrete work, not to the continuous-path integration error or general contact accuracy.

The original mechanical_energy_residual remains external work minus stored material energy and plastic dissipation. The additional normal_contact_corrected_energy_residual includes signed normal work; neither residual is forced to zero. There is no AL contact_potential history. Fixed-tool coordinate work is zero, which alone does not prove energy closure. Compare both residuals, the absolute-work bound, and increment refinement; cancellation can make an inaccurate coarse path look balanced. These histories do not automatically promote the result to verified.

Only jointly accepted endpoints enter this ledger. Failed attempts restore it with displacement, plastic state and multipliers. New checkpoints retain and check its complete accepted-path coverage; legacy checkpoints without prior work remain energy-unavailable after continuation. No missing history is reconstructed. Moving-plane checkpoints additionally validate the motion identity and resolved amplitude against the Step coordinate before publishing the restored state. Fixed-plane v1 snapshots retain their format. These semantics do not enable rotating/curved AL or MPI. results.export_accepted_contact_points(...) exports the same augmented pressure used in equilibrium, with its nodal reference-area location and enforcement recorded. It does not substitute the old penalty*penetration pressure. Increment records expose total Newton work, the maximum inner Newton count, outer augmentations and line-search residual evaluations. A rejected search reports its declared minimum step and trial count; it does not diagnose every such failure as a tangent error or automatically relax the search floor. In-memory rollback snapshots use immutable numerical arrays. JSON conversion belongs to the on-disk checkpoint boundary, not each attempted increment.

Run examples/finite_j2_augmented_plane.py from the source checkout for loading, output and restart. Tests compare a homogeneous elastic limit to an independent nominal-stress formula, a plastic limit to a prescribed-motion solution, and interrupted to uninterrupted loading. These are bounded numerical checks, not validation of arbitrary industrial forming.

examples/finite_j2_translating_augmented_plane.py adds prescribed compression, unloading and recontact through the same ordinary API. It exports accepted pressure, tool work, relative-gap work, raw/contact-aware residuals and a restart checkpoint. tests/test_translating_normal_augmentation.py compares this path with a scalar finite-J2 reference and 16/32/64/128 increments. Both pressure and plastic history are compared; balancing total force alone is insufficient. The independently checked moving-tool tangent freezes the old material state and multiplier, not the current geometry.

The nonuniform Poisson-coupled plastic reference additionally compares normal pressure with independently imposed displacement reactions on each mesh. A displacement tolerance is not a pressure-error tolerance: pressure accuracy must be checked separately against contact-tolerance and mesh refinement. See tests/test_translating_augmentation_plastic_reference.py; its tighter benchmark tolerances do not change the software defaults.

For this bounded plane route, steps.normal_augmentation(..., acceleration="aitken") optionally accelerates the outer multiplier updates. It uses reference-area-weighted dynamic relaxation, bounded to [0.1, 10], with resets on active-set changes and at every attempted increment. Degenerate or invalid secants revert to the ordinary update. The inner Newton method, penalty, loading and equilibrium/penetration/complementarity tolerances do not change. Failure still invokes the ordinary rollback/cutback path; there is no universal convergence or speed guarantee.

The default remains acceleration="none". Accelerated checkpoints carry a distinct numerical-policy identity and do not silently load into a different policy. Secant history is not physical State and never persists across accepted boundaries. Increment diagnostics record the actual relaxation factors and reset count. The public translating-plane example accepts --acceleration aitken; use the same option when resuming its checkpoint.

The partial-contact regression also exercises a tilted fixed plane under plastic loading, unloading and recontact. It compares every accepted displacement, material state, pressure, active set and energy record, restores a loaded checkpoint, and injects failure after pressure commit to check atomic rollback and retry. An independent equality-constrained elastic active-set reference checks both acceleration policies on two meshes. These tests cover a changing subset of fixed material-side nodes, not multiplier transfer to a new trace or curved-tool geometry.

tools/benchmark_multiplier_acceleration.py compares the two policies on the 32-increment cyclic example after warming both paths and alternating run order. The recorded serial Mac test reduced median solve time from 2.058 s to 1.404 s (31.8%), with outer updates 106 → 45 and Newton/linear solves 209 → 140. Accepted displacement history, final material/pressure and energy histories passed the declared comparison thresholds. This small implicit-plane result does not establish a speedup for explicit composite damage, general curved contact, MPI or other machines. The benchmark evidence is retained under evidence/contact/2026-10-20-multiplier-acceleration.json.

Accepted contact-point exports preserve the normalized Step coordinate as coordinate_value and record resolved_load_amplitude separately, with tool_motion_factor for each moving pair. For a cyclic path, coordinate 0.5 may mean tool factor 1 and coordinate 0.75 may mean factor 0. Comparing these different quantities would falsely reject valid intermediate frames. Stale or nonfinite tool factors still reject before output creation.

The split outer-multiplier/inner-equilibrium design follows the established segregated normal-contact formulation. This is methodological context, not an implementation equivalence claim.

The independent cyclic finite-J2 stop reference additionally checks stored energy, plastic dissipation and accepted work at 16/32/64/128 increments. See tests/test_normal_contact_energy.py. Numerical contact discontinuity work is also distinguished from physical stored energy in the Abaqus contact-energy example. This motivates separate reporting, not equivalence with Abaqus algorithms or its output variables.

tools/verify_normal_augmentation_spatial.py adds a nonuniform manufactured finite-Hencky elastic-limit solution on three refined tetrahedral meshes. It measures displacement and gradient errors, contact force and nodal pressure errors separately. The observed displacement/gradient orders are approximately two/one; the pressure is not exact and requires its own refinement evidence. This benchmark does not establish general multiplier-space stability or plastic-contact spatial accuracy.

tools/benchmark_normal_augmentation.py compares explicit penalty candidates on this example using warmups, shuffled repeats, the complete four-increment Step and all accepted material/field snapshots. Failed candidates remain in the report. It is a reproducible numerical-control experiment, not automatic tuning or a general acceleration benchmark. Both --penalty and --minimum-step-length are visible example options. Compare the complete accepted path, not only the final displacement, before adopting different controls for a history-dependent material.

This distinction is also made in the COMSOL contact settings: AL penalty affects outer convergence and can compromise robustness when too large. The software does not infer a universal value from one successful run.

The separation of nodal and integral multiplier spaces follows the caution in the GetFEM contact documentation: choosing a multiplier space requires stability analysis, not merely adding an outer loop.

Point coverage

sample = results.sample_points(field, points, missing="nan", return_info=True)
print(sample.summary())
values = sample.values

sample.located records mesh coverage; sample.finite records finite values. The summary separates missing points from non-finite values inside the mesh. Empty requests have undefined coverage (None), not a misleading 100% score. The default still returns the original NumPy array and raises on missing points. For discontinuous fields, the existing deterministic cell-owner convention is unchanged; sample either side of an interface for one-sided values.

Pressure reference selection:

Situation Treatment
Full velocity Dirichlet conditions leave constant pressure free Attach the constant-pressure null mode; check boundary-flux compatibility.
Reporting requires a specified mean pressure After a gauge-free solve, shift pressure by the difference between the target mean and its volume-weighted mean. This does not change velocity.
A physical pressure/traction condition already fixes the reference Do not add a constant-pressure nullspace or an extra pressure pin.
Using a single pressure dof as a numerical reference Record the dof/location and check velocity sensitivity; it must not replace missing physical boundary conditions.

A volume-weighted mean is the pressure integral divided by the domain measure, not the arithmetic mean of nodal coefficients. Nullspace treatment does not repair an unstable velocity/pressure element pair or inconsistent flux data.