Engineering Loads, Steps, and Resultants¶
AgentFEM treats common CAE operations as reusable scientific assets rather than case-local UFL fragments.
Inspectable scientific formulas¶
Configuration files and AI agents often need to describe a source, boundary value, or initial field as mathematics rather than executable Python:
source = expressions.as_ufl(
"2*pi**2*sin(pi*x)*sin(pi*y)",
domain,
)
expressions.interpolate(
temperature,
"exp(-t)*sin(pi*x)*sin(pi*y)",
parameters={"t": 0.25},
)
ScientificExpression is a deliberately small language. It accepts
x/y/z/t, declared parameters, arithmetic, and reviewed functions such as
sin, cos, exp, and sqrt. Arbitrary calls, attributes, indexing, and
Python statements are rejected before UFL compilation. Its summary retains
the original formula and language version, so an agent-readable input remains
inspectable instead of becoming an opaque callable.
The same validated formula has two deliberate execution routes. as_ufl
lowers symbolic physics into a variational form; interpolate evaluates known
loads, coefficients, initial values, and boundary data directly on NumPy
coordinate arrays without eval or a per-form C++ JIT. This keeps scientific
meaning identical while allowing many generated cases to reuse one compiled
finite-element operator.
This interface is for trusted mathematical structure, not for guessing a missing equation. A formula that is inconsistent with its stated PDE remains a model or dataset error.
Sequential thermal--mechanical field handoff¶
Solve the thermal problem first on the same runtime mesh. Keep its accepted temperature separate from the structural input field, then use the existing engineering configuration and ordinary structural Step:
thermal_result = thermal_model.step(target=T).solve_result()
T_solid = fields.temperature(domain, value=300.0)
solid_model.eigenstrain(eigenstrains.thermal(T_solid))
stage = solid_model.stage("thermal-to-solid")
stage.predefine(T_solid, T, method="interpolate")
with stage.field_transaction(displacement=u):
result = solid_model.step(target=u, configuration=stage).solve_result()
The models, material, boundary conditions and u must be defined normally;
this fragment is a handoff, not another solver. copy (the default) requires
the identical function-space object. interpolate explicitly permits different
spaces on the same mesh with identical value shapes. Neither mode transfers
across meshes or silently smooths/project fields. Interpolation is not an
L2-conservative projection. Imported/reordered meshes need a separately
verified transfer method even when coefficient-array lengths match.
For a transient source, pass both source_time=t and target_time=t to
predefine. These are explicit caller-supplied physical times, checked for
finite equality; they do not infer a field's history or convert time units.
Omitted times are recorded as unspecified, appropriate for static snapshots.
Temperature values and reference temperatures must already use a consistent
unit convention. No unit conversion is guessed.
Assignments are staged before any target is changed and reject non-finite
values collectively. model.step(configuration=...) also restores the fields
if lowering fails. To cover downstream solve/result failure, the explicit
field_transaction above protects predefined targets and named unknowns; the
accepted thermal source is untouched. This is an in-memory nodal-field
transaction, not rollback of files, solver caches, or plastic material history.
All ranks must participate with the same field names, mesh, and declaration.
Backend failures within unfinished collectives need backend error handling.
Each new ordinary static structural Step assembles its operator for the supplied
temperature, including temperature-dependent material properties. Do not reuse
a manually prepared stiffness after changing temperature-dependent stiffness.
For existing transient Procedures, use the explicit time-input contract: an
eigenstrain-only update changes the RHS; material stiffness changes invalidate
the operator. predefine itself is a snapshot, not a live callback binding.
The result retains the construction-time configuration under
metadata.engineering_step.field_transfers, with source/target elements,
method, time declaration and mesh relation. This record is not a scientific
validation claim or a portable mesh fingerprint. Thermal restart uses the
thermal Procedure's existing checkpoint; after restoring, transfer the accepted
temperature at its accepted time and recompute the structural stage. No
combined multiphysics checkpoint or two-way coupled iteration is claimed.
Regression coverage: tests/test_sequential_field_transfer.py solves uniform
and linear steady temperature fields, checks free expansion and restrained
plane-strain stress, rejects mesh/component/time mismatch, verifies collective
rollback, and transfers a restored transient heat solution. The same tests run
with two MPI ranks. Multiple-material constitutive/eigenstrain semantics retain
their independent regression in tests/test_thermoelastic_semantics.py.
Two successive temperature-dependent-stiffness stages also check the stress
against the current elastic modulus, rather than only checking successful solve.
Retry mechanics without repeating heat¶
The existing examples/thermal_stress_wall_2d.py now exposes a small restart
workflow using the same public Step API:
python examples/thermal_stress_wall_2d.py --smoke --output outputs/wall
python examples/thermal_stress_wall_2d.py --smoke --output outputs/wall --resume-heat
The first command saves the accepted thermal checkpoint and heat result. The
second restores that completed stage and solves elasticity again; it performs
zero thermal increments. Omit --smoke for the normal mesh/time horizon, but
do not mix smoke and normal checkpoints. Existing accepted heat is never
silently overwritten by a fresh run. The example checks its source-file hash,
AgentFEM version and smoke setting before restoration; a changed recipe needs
a new directory. This deliberately conservative example guard is not a general
semantic fingerprint for arbitrary scientific Python or external assets.
The structural result records the upstream checkpoint manifest SHA-256, accepted time, recovery policy and thermal solve count. The checkpoint loader validates its payload; the digest alone is not a scientific validation claim. Existing portable checkpoint support is reused, not duplicated into a new coupling archive. Both stages still need their own scientific acceptance.
If a structural trial is rejected inside stage.field_transaction, both the
nodal fields and the stage's prior transfer evidence are restored. Correct the
explicit cause and build a fresh structural Step; the accepted thermal source
remains unchanged. Do not reuse or publish a result object from the rejected
attempt. Files already written are not rolled back: use an attempt-specific
output path when preserving failed-attempt artifacts matters. This recovery
recipe applies to elasticity; history-dependent mechanics additionally needs
its own committed material checkpoint and is not covered by nodal rollback.
The regression now includes two thermal conductivities, two regional temperature-dependent moduli and expansion coefficients, checking the exact DG0 cell average of quadratic thermal stress. It also rejects a completed structural trial on one MPI rank, retries without additional thermal solves, and checks that accepted result metadata does not change with later transfers.
The one-way dependency is the same distinction used in the Abaqus analysis overview: sequential thermal stress is appropriate when the thermal solution does not require feedback from the mechanical response. See the bounded two-way design before introducing feedback.
Continuum loads¶
model.elastic_foundation(on=base, stiffness=2.0e8, mode="normal")
model.centrifugal((0.0, 0.0, 120.0), center=(0.0, 0.0, 0.0))
model.hydrostatic_pressure(
density=1000.0, gravity=(0.0, 0.0, -9.81),
reference_point=(0.0, 0.0, 0.0), on=wetted_surface,
)
model.distributing_coupling(
(0.0, 0.0, -50_000.0), moment=(2_000.0, 0.0, 0.0),
reference_point=load_application_point, on=loaded_surface,
)
For ordinary self-weight, density belongs to the material and acceleration belongs to the load:
steel = model.material(
elasticity.isotropic_elastic(
young=210.0e9,
poisson=0.30,
density=7850.0,
)
)
model.gravity((0.0, 0.0, -9.81))
model.gravity(...) creates the reference volume force density
rho * acceleration. With multiple registered materials it creates one load
per explicitly assigned material region, preserving each density. A raw force
density remains available as model.body_force(...). Both routes enter the
same external-force operator, amplitude, Step activation, reaction, and work
contracts used by the selected procedure. Abaqus *DLOAD, GRAV migration can
lower one or more explicitly assigned three-dimensional solid-section regions;
parent or overlapping ELSET membership is never guessed.
The foundation contributes a boundary stiffness matrix. Centrifugal loading
uses registered material densities and regions. Hydrostatic pressure follows
p = p_ref + rho g dot (x - x_ref). Distributing coupling constructs a
traction whose integrated force and moment equal the reference-point
resultants, avoiding a mesh-sensitive single solid-node force.
Local coordinates and named reference points¶
local = coordinates.cartesian(
origin=(0.0, 0.0, 0.0),
x=(0.0, 1.0, 0.0), y=(-1.0, 0.0, 0.0), z=(0.0, 0.0, 1.0),
name="fixture",
)
rp = coordinates.reference_point((100.0, 20.0, 0.0), name="RP-1")
model.remote_force(
(0.0, -50_000.0, 0.0), moment=(2_000.0, 0.0, 0.0),
reference_point=rp, system=local, on=loaded_surface,
)
model.remote_displacement(
U, reference_point=rp, translation=(0.0, 1.0, 0.0),
rotation=(0.0, 0.0, 0.01), system=local, on=driven_surface,
)
Coordinate axes are validated as a right-handed orthonormal basis. Vector and
tensor transforms are public and inspectable. remote_force uses the existing
continuum distribution and preserves force and moment about the named point;
remote_displacement prescribes the corresponding rigid boundary motion and
participates in nonlinear load-factor ramping. It does not claim an unknown
reference-point degree of freedom or a general kinematic MPC.
Step inheritance¶
preload = model.stage("preload")
preload.activate_load(gravity)
service = model.stage("service", previous=preload)
service.activate_load(pressure)
service.deactivate_load("temporary_fixture_force")
service.deactivate_constraint("temporary_fixture")
service.predefine(temperature, initial_temperature)
step = model.step(target=U, configuration=service)
An EngineeringStep says which named loads and constraints are active. It is
separate from steps.automatic(...), which controls increments, and from
solvers.newton(...), which controls algebraic convergence. Only explicit
changes are recorded; other assets inherit from the preceding Step.
Lowering uses a shallow configured Model view. The source Model's load and constraint registries are never temporarily replaced, while the executable Step retains the exact active view used to produce its result. This makes exception handling, concurrent campaign preparation, and provenance inspection deterministic. Applying an explicitly declared predefined field remains an intentional field-state operation.
Engineering resultants¶
section = results.section_resultant(S, on=cut, about=reference_point)
free_body = results.free_body_resultant(
boundary_tractions=((traction, outer_boundary),),
body_forces=((rho_g, volume),), about=reference_point,
)
path = results.sample_path(S, start=a, end=b)
Section force/moment, free-body force/moment, and path sampling are MPI-global scientific quantities for verification, histories, campaigns, and learning datasets. A fully kinematic reference-point MPC remains separate from the implemented load-distribution contract.
Repeated linear systems¶
For a transient or parameter loop whose left-hand side and constrained degree set stay fixed, prepare the algebraic problem once:
with solvers.prepare_linear_problem(a, L, T, bcs=bcs, options=options) as solve:
for time_value in accepted_times:
t.value = time_value
solve.solve() # refreshes the right-hand side; reuses A and the KSP
This is the lower-level counterpart of AgentFEM's managed transient Steps. It keeps the mathematical forms public while avoiding repeated matrix assembly and factorization in external protocol adapters.