Model parameters¶
You’ll pick up the LinearIsotropicElasticity model from
Running your first model and read, change, and
differentiate through its moduli from Python.
The input file¶
Same input file as the previous tutorial — re-used here so we can focus on the parameters:
# Same linear-isotropic-elasticity model as the previous tutorial.
# Re-used here so the focus stays on parameter access / mutation rather
# than on a new physical setup.
[Models]
[elasticity]
type = LinearIsotropicElasticity
coefficients = '200e3 0.3'
coefficient_types = 'YOUNGS_MODULUS POISSONS_RATIO'
[]
[]
Listing the parameters¶
A NEML2 model is a torch.nn.Module, so named_parameters() works
the same way it does for any PyTorch model:
import neml2
model = neml2.load_model("input.i", "elasticity")
for name, param in model.named_parameters():
print(f"{name}: {param.data.item()}")
E: 200000.0
nu: 0.3
The two parameters E and nu correspond to the two coefficients
we passed in the input file.
Reading a parameter by name¶
Each parameter is also exposed as a Python attribute on the model.
Reading model.E returns a Scalar — one of NEML2’s typed tensor
wrappers (see Tensor types for the full list):
print("E =", model.E)
print("nu =", model.nu)
E = Scalar(data=Parameter containing:
tensor(200000., dtype=torch.float64, requires_grad=True), sub_batch_ndim=0, sub_batch_state=(), sub_batch_meta=(), k_ndim=0, k_state=(), k_pairing=())
nu = Scalar(data=Parameter containing:
tensor(0.3000, dtype=torch.float64, requires_grad=True), sub_batch_ndim=0, sub_batch_state=(), sub_batch_meta=(), k_ndim=0, k_state=(), k_pairing=())
The underlying torch.nn.Parameter lives at .data. Use .data
when you need the raw torch.Tensor — for example to pull out a
numerical value with .item(). (This is user-facing; library code
stays on the typed wrappers.)
print("E =", model.E.data.item())
print("nu =", model.nu.data.item())
E = 200000.0
nu = 0.3
Changing a parameter¶
There are two ways to change a parameter at runtime. Pick based on
whether you want to keep the existing nn.Parameter (and any
optimizer state attached to it) or replace it outright.
In place, on .data¶
In-place mutation keeps the same nn.Parameter object. Leaf tensors
that require grad can’t be mutated in place directly — autograd would
lose the history. Wrap the mutation in torch.no_grad() to tell
PyTorch you’re intentionally side-stepping the graph for this
assignment:
import torch
with torch.no_grad():
model.E.data.fill_(150e3)
print("E =", model.E.data.item())
E = 150000.0
Use this inside an optimization loop — the optimizer holds a
reference to the underlying nn.Parameter (the one
named_parameters() returns), and in-place mutation through .data
writes back to that same Parameter.
Rebinding the attribute¶
Assigning a fresh torch.nn.Parameter swaps the slot wholesale:
model.nu = torch.nn.Parameter(torch.tensor(0.25, dtype=torch.float64))
print("nu =", model.nu.data.item())
nu = 0.25
Reach for this when the new parameter has different properties — a
different dtype, a different requires_grad, or a batched shape
(see Model parameters revisited for batched parameter
values, or Vectorization for batched inputs).
Rebinding invalidates any optimizer that was tracking the old
nn.Parameter, so re-create the optimizer afterwards if there was
one.
Freezing a parameter¶
Toggle requires_grad off and the parameter is excluded from
autodiff — its gradient stays None through a backward pass, and any
optimizer tracking it will leave it alone:
model.E.data.requires_grad_(False)
print("E.requires_grad =", model.E.data.requires_grad)
E.requires_grad = False
Use this to hold one modulus fixed while calibrating the other — see Parameter calibration for the full workflow.
Parameters flow into outputs¶
Changing a parameter changes the next evaluation’s output. Reload the
model so the state is clean, then evaluate it once, halve E, and
evaluate again:
from neml2.types import SR2
model = neml2.load_model("input.i", "elasticity")
strain = SR2.fill(0.01, 0.0, 0.0, 0.0, 0.0, 0.0)
print("E = 200e3 ->", model(strain).data)
model.E = torch.nn.Parameter(torch.tensor(100e3, dtype=torch.float64))
print("E = 100e3 ->", model(strain).data)
E = 200e3 -> tensor([2692.3076, 1153.8462, 1153.8462, 0.0000, 0.0000, 0.0000],
grad_fn=<AddBackward0>)
E = 100e3 -> tensor([1346.1538, 576.9231, 576.9231, 0.0000, 0.0000, 0.0000],
grad_fn=<AddBackward0>)
For linear elasticity the stress scales linearly with \(E\), so the second row is exactly half the first. Any parameter change shows up on the next forward call — no need to re-load the input file.
Differentiating through parameters¶
Autodiff flows through NEML2 parameters just like it does through any
other nn.Module weight. Backward from a scalar reduction of the
output gives gradients on each parameter:
model = neml2.load_model("input.i", "elasticity")
strain = SR2.fill(0.01, 0.0, 0.0, 0.0, 0.0, 0.0)
stress = model(strain)
stress.data.sum().backward()
print("dL/dE =", model.E.data.grad.item())
print("dL/dnu =", model.nu.data.grad.item())
dL/dE = 0.02499999979940744
dL/dnu = 24999.999386098603
This is what calibration workflows build on — embed the model in a
larger PyTorch graph, define a loss against experimental data, and
optimize the moduli with any torch.optim optimizer.
Automatic differentiation and
Parameter calibration walk the full pipeline; for
calibration through a time-stepped transient, see
Recurrent calibration with pyzag.
Where to go next¶
A parameter doesn’t have to be a literal — it can also be supplied by another model, promoted to an input on the host, or shared across siblings in a
ComposedModel. See Model parameters revisited.Real material models compose several pieces (elasticity, hardening, flow rule, …), each contributing its own parameters. The mechanism is the subject of Cross-referencing and Model composition.
Batched evaluation — both over the inputs and over the parameters themselves — is covered in Vectorization.
Once you’re comfortable reading and mutating parameters, the Parameter calibration tutorials cover calibrating them against experimental data via PyTorch autograd.