Local history materials in a structural model¶
For a Cauchy-stress/small-strain provider, the bounded companion route is now
model.step(target=u, duration=..., time_unit="s", ...). It accepts serial,
homogeneous 3D models with a declared stress-free zero-history initial state.
See the runnable relaxation project
and its time/restart contract.
This does not reinterpret a small-strain tangent as a finite-strain dP/dF.
Without duration, the existing small-strain route still requires
rate_independent=True and keeps its portable checkpoint format.
The experimental finite-strain history route lets a material author supply a
local update, not another global solver. An ordinary case still uses
Study → Model → material/boundaries → model.step → SimulationResult.
The current admission is serial, one homogeneous material, strong prescribed
displacements and reference dead loads, in 3D or explicitly declared 2D plane
strain/limited plane stress. It is not a hydrogel-specific solver or automatic multiaxial inference
from a uniaxial calibration.
Displacement must be a standalone H1-conforming vector field with one component
per spatial dimension. DG fields require interface terms, and a mixed-field
subspace cannot silently discard its pressure equation. Both are rejected.
Start with the complete example¶
examples/history_material
contains four files: case.py, material.py, agentfem.toml, and README.md.
With an installation exposing mechanics.FiniteStrainMaterialStep(checkpoint=..., output_every=...) and
constitutive.PlaneStressReduction (checked by the project manifest), run:
agentfem check
agentfem run --name viscous -- --material viscoelastic
agentfem run --name damage -- --material damage
agentfem run --name plane-stress -- --material viscoelastic --assumption plane_stress
Only the local material changes. Both use the same plane-strain structure, hold/reversal schedule, quadrature, Newton procedure and output path. The example material constants are synthetic; neither example is a calibrated hydrogel law, robust rubber formulation or mesh-objective fracture model.
The model-level entry is:
model.material(material)
step = model.step(
target=u,
duration=2.0,
time_unit="s",
amplitude=cycle,
incrementation=steps.fixed(16),
output=run.artifact("results.xdmf"),
output_every=4,
checkpoint=checkpointing.every(
4, directory=run.artifact("checkpoints"), keep_last=2
),
)
result = step.solve_result()
run.publish(result)
time_unit records the chosen consistent unit; it does not convert material
parameters. duration is required and positive. The amplitude argument is a
normalized coordinate, not physical seconds. At coordinate a, material time
is duration*a; an increment from a0 to a1 has physical
dt=duration*(a1-a0). Tabulated reversal/hold knots are visited exactly, even
when a requested fixed or automatic increment would cross a knot.
Choose field output independently of the solver¶
output_every=4 writes fields every four accepted increments. The default
is 1; None writes only the requested segment endpoint and any accepted
continuation boundary. Positive integers are required otherwise. Failed attempts
and Newton iterations do not count. The count includes restored accepted history.
The requested pause/completion endpoint is always included, even off cadence;
continuation publishes its actual starting state before advancing, whether it is
restored from disk or continued in memory. No fictitious material response is
generated at time zero.
Sampling does not insert time knots, change the accepted time partition, discard
scalar histories, or alter checkpoint frequency. Unsampled increments skip field
recovery and field writes, but still perform the complete constitutive and
equilibrium update. The final SimulationResult still includes its full fields.
field_output metadata reports the cadence, saved physical times and frame count.
Output cadence is not part of scientific checkpoint identity: a continuation can
choose another cadence. Unsaved past fields cannot be reconstructed without
recomputation. Save a fresh path per continuation and join compatible segments.
This bounded option does not admit the general OutputPlan: exact prescribed
output times may require new integration stations, which would be a different
time discretization for history-dependent laws. There is no claim of a solver
speedup or a fixed frame count when adaptive cutback adds accepted increments.
The material contract¶
For selected raw material histories, add
material_points=(results.material_point("site", cell=0, point=0),) to the
same Step. It records accepted site.S/P/F/E_GL, state and supplied temperature
independently of field cadence, with complete same-layout restart. See
co-located histories for measures and units.
This is not a nodal probe or a history of spatial maxima.
Reuse constitutive.UserMaterial, MaterialStateSchema, MaterialPointInput
and MaterialPointOutput; there is no second neural-material interface.
| Author supplies | Meaning |
|---|---|
name, state_schema |
Named scalar/tensor internal variables, initial values, optional units and output aliases |
tangent_convention |
Exactly MaterialTangentConvention.first_piola_deformation_gradient() for this structural route |
summary() |
Stable JSON identity including implementation version, material parameters and scientific assumptions; not mutable counters |
update(point) |
Local stress, algorithmic tangent, trial state, optional stored energy and suggested time scale |
Optional update_batch(request) |
Same ordered point responses; no hidden commit or shared mutable history |
Inputs contain committed F_n, trial F, committed state, endpoint physical
time, positive dt and the Step's optional material-property vector. The local
contract also has optional thermal/external fields. This structural route now
supplies the bounded prescribed temperature input below; arbitrary external
fields and spatial temperature histories remain separate capabilities.
For an expensive local Newton solve, compute shared rate/Jacobian quantities once when both callbacks request the identical candidate. Keep this reuse inside one update with fixed committed state, time and parameters; copy the candidate key because numerical solvers may reuse their input buffer. Do not reuse a previous increment's material response. The optional batch interface can reduce call overhead separately. Verify both changes on evolving accepted paths, not only repeated evaluation of one frozen material point. The P92 evaluation audit gives a bounded example without changing the framework's material or state contracts.
Prescribed temperature, without a second solver¶
Pass a constant kelvin value as temperature=300.0, or reuse a scalar history:
temperature = histories.temperature(300.0)
for time, kelvin in [(0.0, 300.0), (0.6, 360.0), (1.4, 340.0), (2.0, 400.0)]:
temperature.record(time, kelvin)
step = model.step(target=u, duration=2.0, time_unit="s", temperature=temperature)
The material receives endpoint temperature and temperature_increment
relative to the attempted increment's accepted start. Temperature times are
physical, unlike the normalized mechanical amplitude. The Step visits both
histories' knots and recomputes the temperature increment after cutback. The
material's dP/dF holds prescribed temperature fixed.
The Step freezes the scalar, piecewise-linear input at construction. Editing
the source history afterward does not change this Step; construct a new Step
for a changed problem. Kelvin values must be positive, the coordinate must be
time, and the recorded interval must cover the full duration. Time numbers
use the Step's time_unit; no conversion is inferred. The live source is not
modified. Spatial histories and previous-value/discontinuous interpolation
are not admitted by this first consumer.
Accepted output includes TEMP, recovered TEMP_CELL and a compact
temperature history. Restart binds the frozen thermal schedule and checks
the saved temperature against accepted time. The material must start from a
reference state consistent with its initial temperature; initialization does
not invent an equilibrium or material time increment.
This is one-way prescribed thermal input, not heat conduction or two-way coupling. Temperature dependence and thermal expansion belong to the local material update; the framework does not add thermal strain again. Stored energy output alone is not a thermomechanical first-law audit.
The existing neutral output contract returns Cauchy stress and an explicitly
declared 9×9 algorithmic dP/dF, in row-major iJ,kL order. The quadrature
driver converts stress to first Piola with P=det(F)*sigma*F^(-T). The tangent
must differentiate the complete discrete material update while holding old
state fixed, not just its elastic predictor. A spatial UMAT Jacobian is not
this tangent. A material returning P in the Cauchy slot is incorrect.
The Procedure assembles ∫ P:grad(v) dV - external_work and the matching
reference tangent. Plane strain embeds the in-plane gradient in a 3×3 F
with F33=1 and zero out-of-plane shear. It neither solves plane stress nor
guesses missing transverse constitutive behavior. Initial displacement is zero;
the material author must choose a compatible initial reference state.
Explicit plane-stress reduction¶
For a compatible three-dimensional law, use a 2D plane_stress Study and
add this option to the same model.step(...):
The declaration asserts that the material and its accepted history preserve
reflection symmetry about the model plane. It is not inferred from one stress
sample or from a uniaxial calibration. The current bounded reduction admits
block-diagonal F with positive F33; at every quadrature point it solves
P33=0 using a safeguarded local Newton iteration on log(F33). Every candidate
reuses exactly the same old state, old full F, endpoint time and physical dt.
Transverse shear stress and tangent coupling are checked. A positive transverse
algorithmic stiffness is required; failure rejects the global attempt and the
usual increment policy controls cutback.
For in-plane components a=(11,12,21,22), the derivative supplied to structural
assembly is A_aa - A_a33 * A_33a / A_3333, using the full discrete update's
dP/dF. The original material output is not modified: Cauchy stress is converted
using the actual 3D determinant and effective F, not an F33=1 surrogate.
constitutive.reduce_plane_stress(material, point, reduction=policy) exposes
the same uncommitted material-point calculation, returning the original 3D
response and a separate 4×4 reduced tangent. Scalar local iterations currently
call update; they do not imply a vectorized material-batch speedup.
Use constitutive.check_material_tangent(material, point) for the complete
3D law, then constitutive.check_plane_stress_tangent(material, point,
reduction=policy) for the reduced discrete derivative. The latter independently
resolves transverse equilibrium for each positive/negative in-plane
perturbation and returns explicit 4×4 component-order evidence. It does not
replace missing tangents, commit history, switch to one-sided differences or
claim experimental validation. For nonsmooth laws, check each smooth branch
separately; choose perturbations resolvable above the local solve tolerance.
Effective 3D F, including accepted thickness stretch, belongs to the shared
trial/commit/rollback and checkpoint lifecycle. The result includes raw F
and recovered F_CELL; its (2,2) component is the thickness stretch. Default
visualization remains a 2D reference surface, not an extruded thickness mesh.
2D forces/energies are per unit reference thickness. Local relative tolerance
uses a physical stress scale (not an arbitrary unit-sized floor); optional
absolute tolerance has the material's stress unit and is part of restart identity.
This is not general anisotropic plane stress, finite-rotation shells, membrane directors, mixed incompressibility, or automatic multiaxial physics. Out-of-plane rotation/shear and singular or negative transverse stiffness are rejected. A compatible initial reference state is still the author's responsibility. A 1D calibrated law still needs explicit transverse/shear/ volume assumptions before it can be used here.
Who owns history and failure¶
- The material computes a candidate response from the input old state. It must not mutate an independent, hidden global history.
- The framework evaluates every Newton/line-search candidate from the same
committed state and
F_n. A successful global increment commits once. - Local failure, failed equilibrium or material-requested cutback restores displacement, material state, stress, tangent and loading values together.
- Automatic cutback subdivides physical time as well as the normalized path. It does not imply a material-specific error estimator or automatic local substepping. A provider implementing substeps must return the derivative of the whole substepped update.
- Initialization and restart do not integrate a fictitious tiny time step to reconstruct stress. Accepted response arrays are part of restart state.
step.solve(until=0.5) pauses at normalized coordinate 0.5.
step.solve_result(until=0.5, output="first.xdmf") also publishes that accepted
segment through the standard result interface. A paused result has
status="partial", equilibrium_converged=True and path_complete=False in
its solve metadata. The existing converged flag remains a full-path flag.
The trust report identifies a computed accepted segment, not uncomputed
data, complete-path convergence or independent scientific verification.
The physical time and requested/accepted coordinates are explicit. At an
already accepted coordinate, this method can publish current state without
reintegration; it cannot synthesize past fields. Values before the accepted
coordinate, zero, nonfinite values and booleans are rejected before output is
opened. Continuation writes to a new path; preserve or join both segments.
Choose an existing path station to preserve the original increment partition;
an extra pause station is a different time discretization for history laws.
step.save_checkpoint(path) requires an accepted state; load_checkpoint(path)
checks mesh/space, schema, parameters, loading, duration, response arrays and
the accepted/attempted time ledger before changing live state. It also checks
displacement/gradient consistency and the prescribed boundary values at the
saved time. Restore failure is atomic. This first checkpoint format is serial
and same-layout only.
Changing rank count or remeshing is not supported by this Step.
Automatic accepted-state checkpoints¶
Use the existing checkpointing.every(...) policy in model.step(checkpoint=...).
Cadence counts globally accepted increments, including those restored from
an earlier checkpoint, not Newton iterations or failed attempts. final=True
also saves the endpoint of a requested solve segment, including a planned pause;
final=False saves only the cadence stations. An arbitrary interruption may
therefore require recomputing at most the unsaved accepted interval. It never
implies every completed increment was durable.
Saving follows numerical acceptance and precedes optional field output and
progress callbacks. The payload is atomically published through the shared
archive writer; a small discovery sidecar follows. If writing fails, the solve
raises without numerical cutback, preserving live accepted state, earlier
archives and already published frames. A complete payload is independently
restorable even if its discovery sidecar could not be written. Only after
successful payload and sidecar publication does keep_last remove old scheduled
checkpoints. Manual saves, restart sources and files left by earlier processes
are not adopted or deleted. Thus retention applies to the current Step's
scheduled records, not a blanket disk quota across restarts.
Use a new run-owned checkpoint directory for an independent simulation. A
scheduled filename collision is an error, never permission to overwrite old
evidence. Cadence and destination are operational policy, not material identity:
a fresh compatible Step can restore using a different directory or cadence.
every/keep_last require positive integers, flags require booleans and
prefix is a filename prefix, not a path. This prevents accidental truncation
or escaping the declared output directory.
The educational case keeps the latest two scheduled archives (every four
accepted increments) and a convenient accepted.npz/final.npz end-of-segment
archive. No observer or checkpoint-writing loop belongs in the case. There is
no automatic save of a failed trial state and no new cross-rank portability.
Accepted fields are observations, not editable initial conditions. Do not modify displacement, committed history, stress/tangent, physical-time position or live loading constants through their array views. The Step detects such changes at continuation, reaction/output and checkpoint boundaries, including after an accepted-state observer. Prescribed targets and the increment policy must also remain consistent with the Step's construction identity. Reference coordinates and geometry connectivity are fixed after construction, including rigid translations of the reference mesh; this bounded route does not support ALE or remeshing. Path endpoints must be finite real scalars, not booleans, complex values or single-element arrays. To run a different problem, construct another Step; do not reuse an old checkpoint identity. If external code has already corrupted live state, restore a valid checkpoint into a fresh compatible Step. Detection does not reconstruct overwritten arrays or authenticate arbitrary archive edits. Ordinary numerical failure still uses the existing atomic trial rollback, without this user intervention.
Results that can be viewed directly¶
solve_result(output="results.xdmf") writes accepted frames through the shared
io.ResultWriter. Each time has one reference mesh with three-component
visualization displacement and compatible cell fields. In ParaView, use
Warp By Vector, then color by S_CELL or a recovered state. No hand-edited
XML, Extract Block operation, node merge or connectivity code belongs in the
case.
The small-strain user-material result preserves declared state units on both
integration-point fields and their _CELL averages. Stress and energy-density
units come from the model unit system, as does the reaction-force unit; an
unspecified model unit system remains unspecified, not silently assumed SI.
State aliases must not replace solver fields or another state's _CELL field.
Conflicts are rejected when constructing the Step, before material evaluation.
This route uses immutable closed frame files under results-data/ and updates
the small results.xdmf index atomically. The index is readable during the run;
refresh the reader to see new frames. Reference geometry and topology are
shared, not repeated in each frame. Preserve the index and its data directory.
The writer's artifacts() inventories only files referenced by its current
published view, including a partial view after interruption.
Raw S, P and state remain integration-point fields in SimulationResult.
*_CELL is a reference-geometry-weighted DG0 average, not nodal extrapolation,
interelement smoothing or a raw quadrature plot. Stress measure, recovery and
state units are recorded. RF is the full nodal residual; select constrained
DOFs when extracting reactions. Optional SENER is included in the final
result only when defined at every point there; the live visualization schema
does not fabricate zero energy for earlier points where a provider omitted it.
Stored energy is not total internal work or proof of energy balance.
State aliases and energy-component names must not collide with displacement,
stress, reaction or recovered *_CELL names. Collisions fail during Step
construction rather than silently replacing fields during publication.
A restored Step writes a new visualization segment and records its restart
source. It first publishes the restored accepted boundary, without integrating
the material again. This covers a process exit after checkpoint publication but
before field publication. If both segments contain that boundary, the joined
view checks every field and retains one identical frame. It cannot invent
fields that were not saved in the old run. Compatible
saved segments can be joined with the existing zero-copy continuation view.
If the restored checkpoint is older than the last saved field frame, the
recomputed interval overlaps old output. Opt in to
results.join_xdmf_segments(..., overlap="identical") to compare all overlapping
stations and every saved array exactly before deduplication. Different time
partitions, mesh/schema changes or conflicting arrays are rejected, not hidden
by a "latest wins" rule. The default still admits only one identical boundary.
This is a visualization consistency check, not a material/restart certification.
Numerical acceptance precedes output: an I/O failure preserves the last
accepted restartable state and the already published frames.
Progress-reporting failures are not material rejections and do not trigger
numerical cutback; a secondary reporting error never hides the original failure.
Completed-result and reaction publication also check accepted displacement and
material identity. Changing a solved field or material parameter externally
cannot silently turn stale stresses into a new accepted result. Recursive
result publication during an active solve is rejected before opening a writer.
Evidence and limits¶
tests/test_finite_strain_material_step.py checks fixed-old-state material
tangents, superposed rotations, degree-one/two 2D/3D patches against material points,
nonuniform traction equilibrium and assembled tangent directions, physical-time
sensitivity, cutback, restart equivalence, corrupt restore rejection and real
VTK reading/warping. tests/test_live_xdmf.py also checks process interruption
and concurrent reading of closed published frames. These verify the bounded
workflow, not arbitrary user constitutive physics.
tests/test_history_plane_stress.py additionally checks independent scalar
force-controlled history solutions, a separate Neo-Hookean thickness solution,
the condensed local and assembled structural tangents on both smooth history
branches, degree-one/two patches, unit rescaling, in-plane rotation, cutback,
thickness-aware restart, readable interrupted/continued frames, nonuniform
damage spatial refinement, viscous time refinement, independent uniaxial J2
local reduction and real VTK/Warp reading of stress and F_CELL.
None of these tests calibrates a user material or promotes arbitrary laws.
Unsupported combinations fail explicitly: MPI, contact/weak boundaries, MPC,
regional materials, mixed pressure, undeclared/general plane stress, spatial temperature histories,
undeclared/spatial tangents, arbitrary UMAT conversion and declarative
OutputPlan. The required Newton policy raises on exhausted increments;
non-raising failure mode is not admitted yet. Nearly incompressible materials
and softening require appropriate formulation/regularization and mesh/time
studies; this displacement route does not remove locking or localization.
Material authors should first run constitutive.check_material_tangent and
independent loading, unloading, hold, cycle, stress-measure and rotation checks.
For a nonsmooth constitutive switch, test each smooth branch and the switch
separately; a centered difference across the switch is not its one-sided
algorithmic derivative. Solver completion remains distinct from verification
and experimental validation.
The full 3D tangent checker reports one_sided_components if a negative
perturbation would invert F; such evidence is labeled mixed difference, not
central difference. The reduced checker instead rejects inadmissible
perturbations. Both normalize error arithmetic before forming norms, so stress
unit scaling does not turn a wrong tangent into a passing zero-error check.
Perturbations too small to change the stored gradient are rejected explicitly.
Both checkers isolate every provider request, including the baseline and local
plane-stress iterations. If update() changes an input array (F_old, F_new,
old state, properties or external fields), verification rejects it with
AFM-MATERIAL-INPUT-MUTATION and leaves the caller's request unchanged. Return
trial history in state_new; do not edit committed input history. This audit
runs in verification only, not every production integration-point update. It
does not prove the absence of hidden mutable state in an arbitrary provider.
The small-strain checker follows the same isolation and unit-robust error rules,
while keeping its own stress/strain and shear convention. Scalar and batch
Cauchy-stress symmetry checks normalize each point independently; a large
neighboring stress cannot hide a smaller point's nonsymmetric response.
These material contracts are real-valued. Complex gradients, states, stresses
or tangents are rejected before conversion instead of losing their imaginary
parts; a complex-valued frequency-domain constitutive contract is a separate
capability. The same rule applies before restoring this Step's state arrays.
Rotation evidence without guessed history frames¶
constitutive.check_material_rotation(material, point, Q, state_frames=...)
compares a discrete update with its observer-rotated counterpart. It checks
Cauchy stress, first Piola stress, the full declared dP/dF and each named
history variable separately. old_rotation=Qn can differ from Q; omitting it
tests a constant observer rotation. All rotations must be proper orthogonal
3×3 matrices. The test does not rotate material axes in the reference body and
therefore does not assume isotropy.
Every state name needs an explicit transformation declaration:
invariant: scalar, reference-frame or corotated history unchanged by the selected spatial observer transformation;spatial_vector:Q @ v, with declared shape(3,);spatial_tensor:Q @ T @ Q.T, with declared shape(3, 3).
The old history uses Qn, returned history uses Q. A mixed tensor with only
one spatial index is not a spatial second-order tensor: use a separately
verified adapter rather than selecting a convenient frame. Nonempty external
field arrays are rejected because their frames have not been declared. Material
properties must be scalar/reference parameters; scalar temperatures are unchanged.
Errors use independent channel norms. Optional stress_scale and named
state_scales are positive characteristic values in the corresponding physical
units and are recorded in the evidence. Supply meaningful scales when testing
near-zero responses; there is no hidden unit-size absolute tolerance. A large
age variable cannot conceal a small strain-history error. A passing covariance
check is not a passing derivative check: run check_material_tangent too, over
loading, unloading and hold states. Neither one-point test proves path-wide
objectivity, thermodynamics, stability, or agreement with experiments.
Extension authors¶
Use a Step provider only when introducing a genuinely different global
procedure. A local material in the admitted scope does not need one. The
upgrade checker recognizes an explicit module-level
StepProvider(lower=function_name, ...) and does not recommend replacing that
lowerer's necessary add_step by recursive model.step. Custom output inside
an extension is directed to io.ResultWriter, not its enclosing
solve_result. There is no blanket exemption for a directory named providers;
indirect/dynamic registration still needs manual review.
Design references¶
The separation follows established external-material practice, not a new constitutive algorithm: MGIS/FEniCS state update and revert and DOLFINx external constitutive operators keep local integration separate from global residual/tangent assembly. AgentFEM reuses its own existing material and state contracts; it does not add an MGIS or JAX runtime dependency for this workflow.
The transverse solve and tangent condensation follow the established local plane-stress strategy discussed in Ahmadi et al., plane stress in finite deformation hyperelasticity. Here it is applied to the declared algorithmic tangent of a fixed-old-state history update; the paper's hyperelastic evidence alone does not validate history materials. The need to differentiate the actual discrete material update, rather than substituting a continuum modulus, is also established in Simo and Taylor's consistent tangent work and their plane-stress return mapping. Those papers motivate the consistency requirement; they do not certify this generic finite-strain adapter or arbitrary user laws.
Prescribed thermal input follows the existing old/new material-state separation
also used by MGIS.
tests/test_history_material_temperature.py checks independent thermal patches,
fixed-temperature tangents, scalar/batch/plane-stress transport, physical-time
knots, cutback, atomic restart and unified output. These are numerical contract
checks with synthetic coefficients, not a high-temperature alloy validation.
The observer transformations follow the
Sierra/SM frame-indifference theory:
F*=Q F and sigma*=Q sigma Q.T. Differentiating the first-Piola relation
P*=Q P at fixed old state transforms the two spatial indices of dP/dF.
MFront's generic interface
also keeps stress measures, tangent measures and rotation operations explicit;
its material-frame rotation helpers are not a substitute for this observer test.