Results, Batch Simulation, and Learning Data¶
One Result Contract¶
results.SimulationResult is the scientific result of one analysis. It can
hold:
- scalar or fixed-shape quantities of interest;
- live finite-element fields and field-artifact links;
- time/load/iteration histories;
- solver and model metadata;
- visualization, checkpoint, and report artifacts.
- a scientific verification report and explicit trust level.
XDMF, CSV, and NumPy files are artifacts, not the result abstraction itself.
This distinction lets one solve feed visualization, a campaign, a report, or a
training dataset without each consumer reverse-engineering output files.
write_manifest(...) records artifacts inside the manifest directory with
relative paths by default, so moving one complete result directory does not
break its links. It also adds a deterministic provenance seal over the result
record and every registered artifact. Run agentfem verify result.json to
detect a partial copy, replaced HDF5 payload, or edited manifest. This integrity
check is deliberately separate from scientific verification; see
docs/provenance_seal.md.
Linear problems, engineering AnalysisStep objects, and nonlinear problems
provide solve_result() while preserving the older solve() field-return
contract.
Linear solve_result() records PETSc KSP reason, iteration count, and residual
norm. It no longer infers convergence merely because the Python call returned.
The result can consume that evidence through a low-ceremony policy:
solved = step.solve_result()
solved.add_quantity("maximum_displacement", max_u, unit="m")
solved.verify(
"engineering",
required_quantities=("maximum_displacement",),
).require()
One execution evidence stream¶
Standard nonlinear, explicit, implicit-dynamic, first-order transient, and J2
steps emit the same JSON-safe SolveEvent contract. The terminal, optional
.sta status file, result histories, and complete manifest trace are views of
that one stream. print_every changes only human display cadence: every
accepted time increment remains in result.metadata["execution"], while
cutbacks and failed attempts remain auditable rather than being filtered out.
This is important for both reproducibility and AI operation. An agent does not
infer convergence by scraping prose, and a human does not receive a simplified
story that differs from the machine record. Stateful J2 owns its quadrature
state, while heat, Explicit dynamics, and Standard dynamics share one transient
checkpoint envelope. CheckpointRecord makes every schema and portability
boundary visible without pretending a partition-bound checkpoint is portable.
Transient heat, Explicit dynamics, and Standard dynamics also share one-call field output:
The returned result owns the accepted-time evidence and references both the
XDMF index and HDF5 heavy data. Heat writes temperature by default; structural
dynamics writes displacement, velocity, and acceleration. Supplying
fields=(...) replaces that default catalog. Request output on the first solve
or when resuming an incomplete checkpoint. A resumed output is marked
continuation_segment with its physical start time. AgentFEM refuses to
fabricate missing intermediate frames after a step has already completed.
The three transient procedures can also pause and resume with the same public contract:
step.run(until_step=50)
checkpoint = step.save_checkpoint("restart/step-50")
resumed = build_the_same_step()
resumed.load_checkpoint(checkpoint)
result = resumed.solve_result(output="restart/continuation.xdmf")
The checkpoint records current fields, accepted time, algorithm identity, execution events, diagnostic histories, mesh/function layout, and one state shard per MPI rank. Version 2 uses atomic publication, generation-specific shards, and per-shard integrity checks. It intentionally requires the same mesh partition and MPI size. This is useful restart today and a precise boundary for the future global-cell identity needed by cross-partition restart.
Dynamics results carry kinetic energy and, when the step exposes a linear
stiffness operator, recoverable strain and total mechanical energy. Transient
heat carries thermal_content = 1^T C T. These are sampled from the same
engineering operators consumed by the solution procedure and survive restart.
Field output versus history output¶
AgentFEM follows the established finite-element distinction:
- field output is a spatial field over a mesh at selected frames;
- history output is a frequently sampled scalar/vector/tensor at a small region, probe, control node, or global integral;
- quantities of interest are named values selected for reports, campaigns, verification, or learning datasets.
SimulationResult.add_quantity and add_history construct an already computed
scientific result. They are not output-request syntax. Batch forms
add_quantities(mapping) and add_histories(axis, mapping) avoid repetitive
calls. A field-output request is declared before export:
request = results.field_output(
"U", "S", "E", "EVOL",
every=1,
configuration="deformed",
backend="xdmf",
)
artifacts = request.write_finite_strain(
"output",
domain=domain,
snapshots=step.snapshots,
material=material,
)
The field catalog owns aliases and physical meaning. In a finite-strain
context, E resolves to LE; GREEN requests Green--Lagrange strain
explicitly. This keeps short engineering vocabulary without allowing one name
to silently change tensor meaning.
One compact scientific and presentation series¶
AgentFEM's serial static-solid and finite-strain result paths write one temporal
.xdmf index and one compressed .h5 heavy-data store. Topology is stored
once. Each frame
contains:
- its load/time value;
- coordinates
x + scale*u; - nodal
UandUMAG; - every requested cell field on that same grid.
ParaView therefore opens one source, finds the time axis, animates the actual
deformed geometry, and switches among all fields without a Warp filter or
multi-block selection. The HDF5 store also retains the reference coordinates,
so the directly viewable result does not discard finite-element provenance.
deformation_scale=1 is the physical configuration; another value is
explicitly a presentation scale.
Ordinary static output retains reference coordinates and exposes U as point
data, so Warp By Vector produces one deformed geometry with every cell and
point attribute attached. Finite-strain presentation output can instead store
x + scale*u directly when requested. SimulationResult.metadata.field_output
records the backend, layout, and geometry convention.
The low-level io.XDMFTimeSeries mirrors DOLFINx and may expose one XDMF Grid
per Function. Prefer step.solve_result(output=...) for a serial multi-field
ParaView product. The collective MPI fallback remains DOLFINx XDMF/HDF5 and can
appear as multiple blocks; use Extract Block, Append Attributes, and
Warp By Vector when a combined presentation is required.
XDMF is the small XML description and HDF5 is the compact numerical payload.
Inlining millions of values into XML would produce a much larger and slower
file, so the pair should be treated as one logical result dataset. The optional
backend="pvd" and "both" modes remain available for tools that specifically
require PVD/VTU, but they are no longer the default.
Quantities of Interest¶
results.integral, results.average, and results.l2_norm assemble physical
expressions with MPI reduction. SimulationResult.add_dof_statistics(...)
captures coefficient extrema and counts for quick inspection; those values are
explicitly not physical domain integrals.
solved = step.solve_result()
solved.add_dof_statistics(u, prefix="u", unit="m")
solved.add_quantity(
"mean_temperature",
results.average(T, measure=dx),
unit="K",
)
solved.add_quantities({"mean_temperature": mean_T, "heat_flux": total_flux})
solved.write_manifest("result.json")
Point and path probes¶
Field values at physical coordinates use the same MPI-safe interface in serial and distributed runs:
tip_u = results.probe(U, at=(length, height / 2))
temperatures = results.sample_points(
T,
((0.0, 0.0), (0.5, 0.0), (1.0, 0.0)),
)
centerline = results.sample_path(
T,
start=(0.0, height / 2),
end=(length, height / 2),
count=101,
)
centerline.add_to(
solved,
name="centerline_temperature",
unit="K",
distance_unit="m",
)
AgentFEM locates owned cells, chooses one deterministic MPI owner for each
point, and returns the same ordered values on every rank. All ranks must request
identical coordinates. Missing points raise by default; missing="nan" is an
explicit alternative for exploratory sampling. For a discontinuous field, a
point exactly on an interelement boundary has a side ambiguity, so place the
probe inside the intended cell when a one-sided value is required.
Standard stress and strain fields¶
Model-generated static elasticity now provides its standard fields in one call:
result = step.solve_result(output="solid.xdmf")
# result.fields contains Displacement, S, E, and MISES
# Opt in to a diagnostic field set when the analysis needs it.
energy_result = step.solve_result(
output="solid_with_energy.xdmf",
field_variables=("S", "E", "MISES", "SENER"),
)
Small-strain elasticity uses one reusable projection path. Application code normally requests it through the step so field creation and single-grid output remain one transaction:
solid_result = step.solve_result(
output="solid.xdmf",
field_variables=("S", "E", "MISES", "SENER"),
)
The available standard names are S, E, MISES, and SENER. The
engineering default is U/S/E/MISES: U is the primary unknown, S/E are
standard mechanics fields, and MISES is materialized for immediate plotting
even though it is derived from S. SENER is opt-in because a per-cell energy
density is primarily diagnostic; total strain energy and energy closure belong
in compact histories or verification quantities.
The default derived-field representation is a global L2 projection onto
discontinuous DG0, hence a cell average rather than an arbitrary centroid
value. It is not an integration-point dump, nodal extrapolation, or smoothed
nodal field. No values are averaged across neighboring cells or material
interfaces. Each FieldResult.processing record preserves this distinction.
In plane strain, Mises stress includes the constitutively implied out-of-plane
stress. The lower-level results.project(...) remains available for reviewed
UFL expressions and higher-order discontinuous output spaces.
For regional materials, results.small_strain_partition_fields(...) assembles
one global projection from (material, region) contributions. It preserves a
single field over the original mesh rather than writing one incomplete field
per material.
results.reaction_resultant(problem, on=support, component=0) reports the
MPI-global residual resultant on one named strong-Dirichlet boundary. Omitting
on retains the whole-field sum. Affine MPC, weak, and contact reactions
deliberately require separate definitions. For proportional linear
loading, diagnostics.linear_static_energy(...) reports strain energy,
external work, and their closure. results.static_work_balance(...) extends
that contract to non-zero strong prescribed motion by integrating the
conjugate reaction along a proportional path. Ordinary model-generated static
solid results record natural-load work, prescribed-motion work, total external
work, strain energy, and their closure automatically.
Affine MPC reactions must be defined from the dual of the reduced constraint or from a declared macro-motion mode; summing eliminated slave residuals is not invariant to the chosen elimination graph. Weak-constraint reactions are consistent boundary tractions/fluxes from the weak form. Until those dual consumers are implemented, AgentFEM marks their work balance unavailable instead of applying the strong-Dirichlet formula.
A model-generated linear static solid also records force equilibrium without extra application code:
result = model.step(target=displacement).solve_result()
result.quantities["external_force_resultant"]
result.quantities["reaction_force_resultant"]
result.quantities["force_balance_residual"]
result.quantities["relative_force_balance_error"]
The external resultant is assembled from the same complete right-hand side
used by the solver; it therefore includes all body and boundary contributions
in F. The reaction is the unconstrained residual at strong Dirichlet dofs.
The metadata states this scope explicitly rather than implying support for MPC,
contact, weak-constraint, or multiplier reactions.
Structured observation grids¶
A common physical grid separates a learned field representation from the FEM mesh used by each case:
grid = surrogates.regular_grid(
bounds=((0.0, length), (0.0, height)),
shape=(128, 64),
)
sample = datasets.fem_observation_sample(
U,
grid,
name="displacement",
unit="m",
components=("U1", "U2"),
outside="mask",
)
sample.write("displacement_grid.npz")
The export records physical axes, array order, component layout, units, mesh-independence policy, and an optional inside-geometry mask. It is MPI-safe and uses the same coordinates for every case. This is a real data-preparation path for structured-grid neural operators; model architecture and training remain external responsibilities.
Campaign to Dataset¶
A campaign evaluator may return:
- a mapping of declared outputs;
campaigns.CaseOutcomewith provenance/artifacts;- or a
SimulationResult.
In the last case AgentFEM extracts only the declared QoIs into the scientific dataset. Live DOLFINx fields are not serialized into table columns; their artifacts remain linked.
Campaign configuration can be written as safe JSON for parameter definitions, sampling, output contracts, and execution policy. Python still supplies the trusted model builder/evaluator. The JSON file never imports or evaluates arbitrary code.
The current runner is serial across cases and MPI-aware within each FEM case. Use deterministic plan shards for schedulers or separate MPI jobs rather than Python threads.
Before training, require reviewed campaign evidence:
dataset = report.require_dataset(
minimum_samples=4,
quality="engineering",
)
training = surrogates.train(
dataset,
estimator=surrogates.RidgeSurrogate(),
validation_fraction=0.2,
seed=2026,
)
guarded = training.guard(fallback=run_high_fidelity_case)
require_dataset() rejects a partial campaign by default. A quality preset
requires each sample to carry an accepted result assessment and records the
dataset admission decision. The lower-level minimum_trust_level remains
available when a project deliberately supplies its own evidence policy. Failed simulations
are not silently discarded before learning; the caller must review them and
set allow_partial=True deliberately. That acceptance and the failed case IDs
are retained in the returned dataset metadata. surrogates.train(...) keeps the
reproducible split, trained estimator, independent validation, and
applicability guard together. The same protocol accepts the built-in
transparent baselines or the optional PyTorch MLP adapter.
For direct PyTorch training without an AgentFEM trainer:
AgentFEM owns names, units, shapes, provenance, splitting evidence, validation, and applicability. PyTorch owns tensors, modules, automatic differentiation, optimizers, and training loops.
Next Result Priorities¶
- affine-MPC, weak, and contact reaction force/resultant definitions beyond the current named strong-boundary and J2 residual fields;
- nodal smoothing and higher-order stress recovery beyond implemented DG projection;
- natural-load, weak-constraint, affine-MPC, and broader transient work/energy histories beyond the current strong-displacement J2 history;
- cross-partition MPI restart identity and checkpoint retention policies;
- graph and basis-coefficient field encodings for unstructured neural operators;
- scheduler executors that preserve identical case records.