Skip to content

Stable Steps and Compact Field Output

This note records two architectural decisions made while turning the Abaqus C3D10H periodic-cell workflow into reusable AgentFEM infrastructure.

Stable public step, extensible lowering

The public language is:

from agentfem import steps

step = model.step(
    target=u,
    material=material,
    constraints=constraints,
    incrementation=steps.automatic(
        initial=0.1,
        minimum=1.0e-5,
        maximum=0.25,
        max_increments=100,
    ),
)

It should not become model.neo_hookean_step, model.j2_step, model.creep_step, and one more method for every future constitutive law. Those names mix two distinct decisions:

  1. the Study selects the analysis family and physics;
  2. a step provider decides whether one implemented material protocol can be lowered to an executable problem.

step_providers.py now owns that second decision. A provider declares:

  • accepted analysis names;
  • a read-only compatibility predicate;
  • a lowering function;
  • priority and a human-readable description.

The default registry currently contains linear-static, implicit-Euler heat, finite-strain hyperelastic, 3D stateful J2 and creep, Newmark/generalized-alpha implicit dynamics, and central-difference explicit providers. The registry is public and inspectable through models.step_providers().

Each built-in provider also owns a StepOptionContract. This keeps the single model.step(...) entry point extensible without turning its **options boundary into an unchecked dictionary. Misspelled or procedure-inappropriate keywords fail before form assembly with a repair suggestion; required physical inputs such as dt, steps, or duration are checked at lowering. The same contract is emitted by agentfem capabilities --json, so command-line agents, IDEs, and future GUIs inspect exactly what runtime execution enforces.

The contract distinguishes three relationships: accepted names define the provider vocabulary, required names must all be present, and an exactly_one_of group represents equivalent scientific coordinates for which one—and only one—must be selected. For example, harmonic analysis accepts either frequency in Hz or angular_frequency in rad/s, never both. A capability report consequently separates supported (a provider exists and the supplied options fit its vocabulary) from ready (all required scientific inputs are present). This lets discovery stay useful before a model is fully specified without weakening execution-time validation. model.validate() may receive a partial option mapping while builder compatibility checks; complete required-input enforcement belongs to the immutable request consumed by model.step(...) lowering.

model.step(...) normalizes this call into an immutable StepRequest. The request carries the resolved SolutionProcedure, target, material reference, and a read-only option mapping through both step_capability(...) and provider lowering. An explicit procedure= may override the Study preference, but a conflicting method= or incompatible equation order fails before assembly. This is the first typed boundary behind the readable Python language; it does not add a second user-facing configuration system.

The request also exposes an immutable StepExecutionPolicy containing the declared solver, output, transient-history, progress, and checkpoint controls. Individual keywords remain the public language; the policy is their common inspection and provenance form. A Step execution context retains it, and a completed result records the JSON-safe summary under metadata["execution_context"]["declared_policies"]. The historical "policies" key remains an equivalent 0.2 compatibility view. None means the selected provider used its own default; resolved time-step, integration, and solver details are recorded in metadata["step"] by the executable Step.

The provider registry now calls internal scientific builders directly for linear static/steady conduction, finite-strain and mixed hyperelasticity, J2 plasticity, implicit creep, explicit dynamics, finite-strain explicit dynamics, and implicit structural dynamics. Their historical Model methods are compatibility delegates; they no longer contain parallel scientific implementations:

model.step(...) -> StepRequest -> provider -> scientific builder -> problem
                                  |                         |
                                  + execution policy ------+-> result lifecycle

This is an extension seam, not a promise that arbitrary registered code is scientifically valid. A new stateful material provider is admissible only after its constitutive state, trial/commit/rollback policy, tangent, increment control, output, and benchmarks exist. Until then, the material-point law stays truthfully below the FEM-integrated maturity level.

Step, increment, iteration, and frame

These words are not interchangeable:

  • a Step is one analysis procedure or loading stage;
  • an Increment advances load or time within a Step;
  • an Iteration is one Newton equilibrium correction inside an Increment;
  • an Attempt is one try at an Increment, including retries after cutback;
  • a Frame is a saved result state, not a unit of nonlinear solution.

Automatic incrementation is the default for the affine finite-strain path. max_increments=10 means “use no more than ten accepted increments”; it does not request exactly ten. Exact fixed subdivision remains explicit:

step = model.step(..., incrementation=steps.fixed(10))

The automatic controller increases the next increment after fast convergence, keeps or reduces it after harder convergence, and rolls the solution back before retrying a failed attempt with a cutback. minimum, maximum, max_increments, and max_cutbacks are termination safeguards. Constitutive models with internal variables must implement trial/commit/rollback before using the same controller globally.

Solver convergence remains a separate, visible choice:

step = model.step(
    ...,
    incrementation=steps.automatic(max_increments=100),
    solver_options=solvers.newton(
        maximum_iterations=25,
        linear_solver=solvers.direct_solver(),
    ),
)

Thus max_increments limits accepted load/time advances, whereas max_it limits Newton iterations in one attempt.

solvers.newton(...) is independent of the constraint implementation. AgentFEM translates the same public policy to PETSc SNES for ordinary boundary conditions or to exact affine-reduction algebra for periodic equations. Backend-specific classes remain available for expert tuning but are not the standard model language.

Output requests do not control the nonlinear algorithm

Saving every accepted increment follows the Abaqus/Standard convention:

output = results.field_output("U", "S", "E", every="increment")

Uniform output marks use output intervals:

output = results.field_output("U", "S", "E", intervals=6)
step = model.step(..., output=output)

With intervals=6, the initial state and six interval endpoints form seven result frames. The nonlinear controller lands on those requested marks and may insert additional internal increments after cutbacks. frames is reserved for the states read back from a result dataset; it is not a solve-control argument.

Amplitude coordinates follow the same separation. A natural load without an amplitude receives the nonlinear Step's proportional load factor. A load with an amplitude evaluates that history at normalized step time and is not scaled again. Linear static evaluates model histories at step end, while heat and dynamics evaluate them at physical time.

The output lifecycle owns cadence, files and result registration; the selected provider owns constitutive field recovery. Continuum finite-strain materials use the standard stress/strain recovery, while surfaces such as a woven fabric register their own catalogued generalized strains, resultants and directions. This keeps one OutputPlan without pretending that every formulation has a Cauchy-stress field.

Standard run feedback

Nonlinear steps report a concise rank-zero lifecycle without requiring routine CLI flags. The default stream keeps step boundaries, cutbacks, failures and a 30-second wall-clock heartbeat; set AGENTFEM_VERBOSITY=1 for every accepted increment or AGENTFEM_VERBOSITY=2 for every Newton iteration:

[STEP 1] periodic_neo_hookean | automatic_incrementation
  [INC 1 | ATT 1] 0 -> 0.25 (d=0.25)
    ITER 01 | residual=...
  [INC 1] CONVERGED | iterations=...

Passing status_file="job.sta" adds a flushed, line-oriented status record. progress=False remains available for managed campaigns, but silence is not the interactive default.

Transient and nonlinear reporters also provide a wall-clock heartbeat when a nominal output interval or one accepted load increment takes unusually long. The default is deliberately coarse (30 seconds). Transient heartbeats report accepted increment/time, completion, rate, approximate ETA, stability information when available, and the latest energy error. Nonlinear heartbeats report the active increment, load factor and latest completed Newton residual. Neither mode emits one line per internal operation.

The event recorder is bounded rather than proportional to an arbitrarily long run. It retains up to 4096 execution events, preferentially preserving visible increments, cutbacks, failures, and completion events; hidden repetitive increments are discarded after that capacity. Scientific time histories are controlled separately by history_every, visualization by save_every, and restart durability by the checkpoint policy. Thus lowering console traffic does not change the requested scientific histories, while progress telemetry cannot exhaust memory during a long explicit analysis. Heat, explicit dynamics, implicit dynamics, and J2 use this same evidence contract.

One logical field dataset

The default finite-strain result consists of:

results.xdmf   small temporal/field description
results.h5     compressed numerical payload

The HDF5 payload stores topology once, retains reference coordinates, and stores each frame's deformed coordinates plus all requested point and cell attributes. The XDMF presents those frames as one temporal collection. ParaView can therefore:

  • discover time/load values;
  • play the true x+u geometry directly;
  • switch U, stress, strain, J, energy, and other fields on the same grid;
  • avoid one VTU file per frame and avoid a manual Warp filter.

XDMF and HDF5 are one logical dataset. Putting the heavy arrays inline in XML would create a much larger, slower text file without improving scientific meaning. PVD/VTU remains an optional compatibility backend, not the default.

Completed static elasticity, J2, and creep results use the same results.write_result_fields(...) implementation. J2 and creep retain raw integration-point S/PE/CE state in SimulationResult; the common writer records that those fields were omitted from ordinary visualization attributes and writes the explicitly recovered *_CELL fields instead. No hidden nodal extrapolation or smoothing is introduced by asking for output=.

The same single-grid XDMF/HDF5 layout is now used incrementally by serial heat, implicit-dynamics, and explicit-dynamics steps; transient output no longer creates one repeated Grid per Function. Under MPI, AgentFEM retains two explicit products. DOLFINx collective XDMF/HDF5 is the compact scientific route, while a registered fields_paraview PVD artifact is the presentation route. Each saved time is one parallel unstructured-grid dataset carrying continuous fields as PointData and DG0 fields as CellData. ParaView can apply one Warp By Vector filter without duplicate unwarped Blocks or manual Extract Block. PVD/PVTU/VTU uses multiple piece files and reference geometry, so it does not replace checkpoint or compact scientific storage. SimulationResult.metadata["field_output"] makes the choice executable: recommended_visualization_artifact is the file users and agents should open, while scientific_artifact retains the compact numerical record and visualization_requires_extract_block states whether a block-selection step is necessary. The ordinary serial and MPI result paths both report False.

Arbitrary chained Abaqus *EQUATION constraints have a distributed dolfinx_mpc backend. It resolves the source equation graph before mapping labels to global dofs, explicitly supplies relations for owned and ghost slaves, assembles constrained operators collectively, and back-substitutes Newton corrections. The dependency is optional, compiled, version-matched to DOLFINx, and never silently emulated when unavailable.

Result roles remain distinct

  • A field series answers “where is a quantity distributed?”
  • A history answers “how did a selected value evolve?”
  • A QoI answers “what value enters a report, decision, campaign, or dataset?”

The unified XDMF does not replace exact homogenized histories or training tables. Visualization fields may be projected to cell centers, whereas the periodic-cell NPZ/CSV response is integrated from the governing UFL expressions. Consumers should not reconstruct authoritative macro response by re-averaging a visualization field.

The complete declaration can collect these distinct roles:

output = results.output_plan(
    output_directory,
    field=results.field_output("U", "S", "E", "J", every="increment"),
    requests=(
        results.solver_history(),
        results.periodic_cell_history(periodicity),
        results.source_node_history(nodes, RIGHT=7, TOP=9),
        results.finite_strain_checks(constraint=periodicity),
    ),
    presentation=results.presentation(animation="gif"),
)

The plan is passed into model.step(...) so exact output marks constrain only where the nonlinear path must land. Requests that require every accepted state bind a lightweight online observer before the solve; field snapshots retain their independent cadence. After the solve, finalize(...) writes fields, materializes histories and diagnostics, attaches artifacts, and writes the model record and result manifest. A case-specific response plot remains in the case directory rather than becoming a misleading general result function.

Implemented adjacent foundations

  • automatic finite-strain increments for ordinary strong-boundary and affine providers, with convergence-based growth, rollback, cutback, accepted/attempt histories, and .sta reporting;
  • axis-aligned solid symmetry/roller constraints;
  • reference dead pressure and current follower pressure using Nanson pullback;
  • exact piecewise-constant material-point creep histories;
  • auditable fatigue assessment from a named SimulationResult history.

These additions are intentionally reusable. They do not turn material-point creep into a global creep solver or scalar S-N fatigue into a general multiaxial damage model.

Accepted-increment checkpoint policy

All three transient routes—central difference, Newmark/generalized-alpha, and implicit-Euler heat transfer—consume the same policy object:

from agentfem import checkpointing

step = model.step(
    target=field,
    dt=dt,
    steps=1000,
    checkpoint=checkpointing.every(
        100,
        directory="checkpoints",
        final=True,
        keep_last=3,
    ),
)

checkpointing.every(..., portable=True) or step.save_checkpoint(..., portable=True) additionally writes a global, physical-node-keyed nodal state. It can be read with a different MPI partition or rank count; two-rank heat and split-interface Explicit states are automatically tested by continuation on one rank. The default remains the smaller rank-sharded format. Portable state uses bounds-scaled integer coordinate keys to absorb partition-dependent mesh-construction roundoff while retaining a global connectivity identity. Coincident but independent interface nodes are disambiguated by durable source input-node identity. The current NPZ implementation is suitable for laboratory-scale restart; a collective HDF5 implementation is the later scale path. Stateful J2, creep, and finite-strain J2 already use stable portable cell-and-point identity across the tested rank-count changes; larger collective archives remain the scale path.

A checkpoint is written only after an increment has been accepted, its scientific state has been committed, and the corresponding convergence event has joined the execution trace. The final accepted boundary is retained even when the total increment count is not divisible by the cadence. keep_last bounds scheduled-checkpoint storage; AgentFEM publishes the new manifest and all rank shards before deleting older generations, removes only files named by their manifests, and preserves an explicit restart-source record. Omitting keep_last retains every scheduled checkpoint. Restart restores the accepted scientific state, continuation coordinates, and execution trace before appending a new resumed segment; it does not splice visualization frames from an earlier run into the new in-memory time series. Keep the earlier result artifacts when one continuous presentation record is required. Larger collective archives, rather than cross-partition identity itself, remain the scale path.

Next gates

  1. Extend collective MPI output from reference-configuration scientific fields to an ownership-safe directly deformed geometry product.
  2. Extend the implemented MPI-global strong-BC resultant and proportional linear-static energy closure into named region histories; define affine/weak reactions and non-zero prescribed-displacement work separately.
  3. Extend the implemented portable MPI J2/creep/finite-strain-J2 cell identity and multi-region state to larger repartitioned scheduled campaigns and collective archives.
  4. Extend the implemented analytical and external J2 paths, physical cutback, cyclic amplitudes, quadrature fields and strong-displacement work/energy histories with natural/affine work definitions, mesh convergence, and complete external-deck reproduction.
  5. Deepen the implemented global implicit creep route with external component histories, accepted-time observables and execution-efficiency evidence without weakening its Newton and time-integration gates.
  6. Let fatigue continue to consume verified extracted histories; add a fatigue field only after hotspot/element mapping and provenance are defined.
  7. Add load-controlled finite-strain examples for dead and follower pressure, including tangent and sign checks.
  8. Build on the implemented shared transient energy/checkpoint lifecycle, accepted-increment automatic cadence, and heat-balance history with broader mechanical work terms, retention policy, and cross-partition MPI portability.