Declaring inputs, outputs, and parameters¶
This tutorial walks through how to declare the inputs, outputs, and parameters of a custom model. The next tutorial in the series (The forward operator) shows how to fill in the actual computation.
The running example¶
A projectile of velocity \(\boldsymbol{v}\) in a viscous medium feels gravity plus linear drag:
So the model needs:
an input — the velocity \(\boldsymbol{v}\) (a
Vec),an output — the acceleration \(\boldsymbol{a}\) (also a
Vec),a parameter — the drag coefficient \(\mu\) (a
Scalarwe may want to fit to data later, so it has to be calibratable), anda buffer — the gravitational acceleration \(\boldsymbol{g}\) (a known constant of the environment).
The schema¶
A model declares its inputs, outputs, parameters, and buffers in one
class-level hit = HitSchema(...) block. Each line is a single call
to a helper from neml2.schema:
from __future__ import annotations
from neml2.factory import register_neml2_object
from neml2.models.model import Model
from neml2.schema import HitSchema, buffer, input, output, parameter
from neml2.types import Scalar, Vec
class ProjectileAcceleration(Model):
"""Projectile acceleration under gravity and linear drag: a = g - mu * v."""
hit = HitSchema(
input("velocity", Vec, "Projectile velocity"),
output("acceleration", Vec, "Projectile acceleration"),
parameter("dynamic_viscosity", Scalar, "Drag coefficient mu", attr="mu"),
buffer(
"gravity",
Vec,
"Gravitational acceleration vector",
attr="g",
default=Vec.fill(0.0, -9.81, 0.0),
),
)
def forward(self, velocity, *promoted_params, v=None):
raise NotImplementedError
A few notes on the schema:
The string in each declaration (
"velocity","dynamic_viscosity", …) is the name the input file will use.The third argument is the field’s docstring — it shows up in the auto-generated syntax catalog.
attr="mu"on the parameter names its storage slot. Insideforwardread it throughself._get_param("mu", promoted_params, Scalar)— readingself.mudirectly there is rejected, because it bypasses parameter promotion (see the note below). Outsideforward(for inspection)self.muis fine.A
bufferis a constant that travels with the model (e.g., goes to GPU when the model does) but isn’t trainable.
Variable declaration¶
input("velocity", Vec, "Velocity of the projectile")
input(name, type_cls, doc) declares one input variable. type_cls
is one of the typed wrappers from neml2.types — Scalar, Vec,
R2, SR2, MRP, etc. — and tells the framework what shape and
algebra to expect. output(...) works the same way; it declares an
output instead.
The name string serves two purposes: it’s both the HIT option name
the user writes in the input file, AND the default variable name when
no rename is supplied. So a fresh instance reports
input_spec = {"velocity": Vec}:
m = ProjectileAcceleration(dynamic_viscosity=0.001)
m.input_spec
{'velocity': neml2.types.vec.Vec}
m.output_spec
{'acceleration': neml2.types.vec.Vec}
If the user passes a rename, the new name shows up in the spec dict instead:
m_renamed = ProjectileAcceleration(
velocity="v",
acceleration="a",
dynamic_viscosity=0.001,
)
m_renamed.input_spec, m_renamed.output_spec
({'v': neml2.types.vec.Vec}, {'a': neml2.types.vec.Vec})
That’s what lets the same class slot into many different input files — each file picks the names it wants.
Note
The order of input(...) calls matters: it determines the order of
positional arguments to forward(). Same for output(...) and the
return tuple. The next tutorial,
The forward operator, shows how that works.
Parameter declaration¶
parameter(
"dynamic_viscosity",
Scalar,
"Dynamic viscosity of the medium",
attr="mu",
)
A parameter declaration takes a name, a type, a docstring, and the
attribute it should be exposed under (mu here). Other useful options:
default=...makes the parameter optional in the input file — if the user omits it, the schema default is used.allow_promotion=Truelets the parameter be promoted to a runtime input — useful when we want to drive the parameter from another model’s output. See Composing with existing models for the details.
attr="mu" names the storage slot — handy when the math uses
Greek-letter conventions and we’d rather not spell out
dynamic_viscosity everywhere. The underlying storage is a
torch.nn.Parameter (visible in named_parameters()).
Note
Inside forward, read a parameter through
self._get_param("mu", promoted_params, Scalar) — not self.mu. A bare
self.mu inside forward is rejected by a runtime guard: it bypasses
parameter promotion, so it would break the moment the parameter is
promoted to a runtime input (neml2-compile -p), when the static
nn.Parameter no longer exists. _get_param works for both static
and promoted parameters, which is why forward’s signature carries the
*promoted_params pack (def forward(self, velocity, *promoted_params, v=None)):
promoted parameters arrive there. Buffers are not affected — a buffer
like self.g is read directly. The next tutorial,
The forward operator, shows this in a real forward body.
Outside forward — for inspection, calibration setup, etc. —
attribute access via self.mu is fine and returns a typed Scalar
wrapper carrying the same data:
dict(m.named_parameters())
{'mu': Parameter containing:
tensor(0.0010, dtype=torch.float64, requires_grad=True)}
m.mu
Scalar(data=Parameter containing:
tensor(0.0010, dtype=torch.float64, requires_grad=True), sub_batch_ndim=0, sub_batch_state=(), sub_batch_meta=(), k_ndim=0, k_state=(), k_pairing=())
The model is a torch.nn.Module, so the usual PyTorch idioms work
unchanged — model.to(device), model.state_dict(),
torch.optim.Adam(model.parameters(), ...), etc.
Inspecting the declared surface¶
Once the declarations are in place, the surface is available as attributes:
print("input_spec :", m.input_spec)
print("output_spec:", m.output_spec)
print("parameters :", [name for name, _ in m.named_parameters()])
print("buffers :", [name for name, _ in m.named_buffers()])
input_spec : {'velocity': <class 'neml2.types.vec.Vec'>}
output_spec: {'acceleration': <class 'neml2.types.vec.Vec'>}
parameters : ['mu']
buffers : ['g']
That’s everything we need to declare the shape of a model. The next two tutorials cover the rest:
Connecting to input files — how this class shows up in a HIT input file so users construct it without touching Python.
The forward operator — the
forward()body that turns these declarations into actual computation.