Physics-Informed Motion (Neo-Hookean)
A MeshGraphNet trained on displacement alone has no opinion about whether the motion it predicts is motion tissue could undergo. An element may inflate, thin past what myocardium allows, or invert outright, and an L2 loss notices only to the extent that the vertices land in the wrong place. This module adds a neo-Hookean strain energy to that loss, which prices exactly those deformations, and exposes the Cauchy stress the same constitutive law implies.
The energy is
with \(I_1 = \operatorname{tr}(F^T F)\) and \(J = \det F\), evaluated from the deformation gradient of each tetrahedron. Spatial derivatives come from PhysicsNeMo Sym’s least-squares gradient reconstruction, its method for unstructured meshes.
The two appearances of \(J\) are not the same quantity in code. An inverted element makes \(\det F\) non-positive, and \(\ln J\) would then poison the whole loss with a NaN, so the logarithmic terms use \(\max(J, 10^{-6})\): an inversion costs a large finite penalty and stays trainable rather than ending the run. The incompressibility penalty \((J - 1)^2\) uses the raw determinant, which is signed and therefore already prices an inversion correctly.
That clamp is also why the inversion count matters. It keeps an inverted element
finite, which is exactly what would let one pass unnoticed, so
PhysicsInformedMotion.inverted_element_count reports the unclamped
determinant’s non-positive entries and is the only signal that the predicted
motion turned tissue inside out.
Requirements
The shape model must be volumetric: a strain energy needs volume elements, and the template’s own cells are what supply them. A surface model has no interior and no deformation gradient can be formed on it. Tutorial 16 builds such a model; see Tutorials.
physicsnemo.sym supplies PhysicsInformer and ships inside
nvidia-physicsnemo, so no separate install is needed. It is imported lazily.
Training method
- class physiotwin4d.TrainPhysicsNeMoPhysicsInformedMotion(log_level=20)[source]
Bases:
TrainPhysicsNeMoMGNTrain a MeshGraphNet whose loss also prices the tissue’s strain energy.
Identical to
physiotwin4d.TrainPhysicsNeMoMGNapart from the loss, which becomesdata + lambda_physics * (energy + incompressibility). The data term is scored on normalized targets, as before, while the physics term is scored in millimeters and kilopascals, so predictions are returned to physical units before the residual sees them.Call
set_mechanics(),set_elements()andset_reference_meshes()before training, unlesslambda_physicsis zero – which reproduces the data-only MeshGraphNet exactly and is the ablation a physics-informed run is measured against.- model_tag = 'physics_informed_motion'
- set_mechanics(residual, lambda_physics=0.1)[source]
Set the constitutive residual and how heavily it is weighted.
- Parameters:
residual (
Optional[PhysicsInformedMotion]) – ConfiguredPhysicsInformedMotion, orNoneto train on displacement alone.lambda_physics (
float) – Weight of the physics term.0.0skips it entirely, reproducing the data-only MeshGraphNet, which is the ablation a physics-informed run is measured against.
- Raises:
ValueError – If lambda_physics is negative, or is positive without a residual to evaluate.
- Return type:
- property inverted_element_count: int
Elements whose Jacobian went non-positive during training.
Non-zero means the network predicted motion that turns tissue inside out somewhere, which the clamp keeps trainable but does not make physical.
- set_reference_meshes(reference_meshes)[source]
Set each subject’s fitted reference mesh, keyed by subject id.
These are the undeformed configurations the strain energy is measured against – the same meshes the manifests name as
fitted_reference_mesh, whose points the targets are defined at.- Return type:
- set_elements(tets)[source]
Set the
(n_tet, 4)template elements the residual is summed over.- Return type:
Constitutive law and geometry
Physics-informed motion training for PhysicsNeMo mesh-stage models.
physiotwin4d.TrainPhysicsNeMoMGN scores predicted motion against
measured displacement alone, so nothing in its loss rules out motion no
myocardium could undergo: locally inverted elements, non-physical dilation, a
wall that thins past what tissue allows. This module adds a neo-Hookean strain
energy to that loss, which prices those deformations, and exposes the Cauchy
stress the same constitutive law implies so predicted motion can be rendered as
stress.
The energy needs volume elements, so the shape model must be a tetrahedral one: the template’s own cells become the elements, which is what makes one set of element node ids valid for every subject and phase at once.
Reference configuration. The residual is measured against each subject’s
fitted reference geometry, never the shared template. The stored targets are
phase.points - fitted_reference.points, so the fitted reference is the
undeformed state; measuring against the population mean instead would charge
every subject a strain energy for merely being shaped unlike the mean, which
confuses variation between subjects with deformation within one.
Two formulations of the same energy live here on purpose. The symbolic one
(neo_hookean_pde()) is what PhysicsNeMo Sym differentiates and evaluates
during training; the tensor one (NeoHookeanResidual) is what computes
the Cauchy stress for export, which the symbolic path does not hand back. The
tests cross-check them against each other.
PhysicsNeMo Sym is an optional dependency imported lazily, so import
physiotwin4d works without it. It ships inside nvidia-physicsnemo.
- physiotwin4d.train_physicsnemo_physics_informed_motion.tet_volumes(points, tets)[source]
Return each tetrahedron’s volume and the volume lumped onto each node.
The nodal volumes are the quadrature weights the residual is averaged with, so a region contributes in proportion to the tissue it holds rather than to how finely it happens to be meshed.
- Parameters:
- Return type:
- Returns:
(element_volumes, nodal_volumes), shaped(n_tet,)and(n_points,).- Raises:
ValueError – If any element is inverted or degenerate. Templates come from
physiotwin4d.ContourTools.trim_tetrahedra_to_surface(), which holds every cell above a scaled Jacobian of 0.1, so a violation here means the template is broken rather than merely tight.
- physiotwin4d.train_physicsnemo_physics_informed_motion.tet_edges(tets)[source]
Return the unique undirected
(n_edge, 2)edges of a tetrahedral mesh.This is the stencil the least-squares gradient reconstruction fits over.
- Return type:
- physiotwin4d.train_physicsnemo_physics_informed_motion.edge_matrix(points, tets)[source]
Return the
(n_tet, 3, 3)matrix whose columns are an element’s edges.- Return type:
- physiotwin4d.train_physicsnemo_physics_informed_motion.compute_deformation_gradient(reference_points, displacement, tets, reference_inverse=None)[source]
Return the per-element deformation gradient
F.F = Ds @ Dm^-1, with the columns ofDmthe reference edge vectors of an element and the columns ofDsits deformed ones – exact for the linear shape functions a tetrahedron carries.- Parameters:
reference_points (
Tensor) –(n_points, 3)undeformed node positions.displacement (
Tensor) –(n_points, 3)predicted displacement, same units.tets (
Tensor) –(n_tet, 4)node ids.reference_inverse (
Optional[Tensor]) – Cached(n_tet, 3, 3)inverse ofDm. It depends only on the reference configuration, so passing it back in avoids re-inverting it once per phase of the same subject.
- Return type:
- Returns:
(n_tet, 3, 3)deformation gradients.
- class physiotwin4d.train_physicsnemo_physics_informed_motion.NeoHookeanResidual(mu_kpa=10.0, lambda_lame_kpa=100.0, log_level=20)[source]
Compressible neo-Hookean constitutive law, evaluated on tensors.
The strain energy density is
W = (mu / 2) (I1 - 3) - mu ln(J) + (lambda / 2) ln(J)^2with
I1 = tr(F^T F)andJ = det(F). Its Cauchy stress issigma = (mu / J)(B - I) + (lambda ln(J) / J) IforB = F F^T.- Parameters:
- __init__(mu_kpa=10.0, lambda_lame_kpa=100.0, log_level=20)[source]
Initialize the base class with logging configuration.
- Parameters:
class_name – Name for the class (used in log messages). If None, uses the class name. Default: None
log_level (
int|str) – Logging level. Can be an integer (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL) or a string (‘DEBUG’, ‘INFO’, ‘WARNING’, ‘ERROR’, ‘CRITICAL’). Default: logging.INFOlog_to_file – Optional file path to write logs to in addition to console output. Default: None
- property inverted_element_count: int
Elements whose Jacobian went non-positive since this was constructed.
Non-zero means the deformation turns tissue inside out somewhere. The clamp in
jacobian()keeps the energy finite so training can continue, which is also what would let an inversion pass unnoticed.
- jacobian(deformation_gradient)[source]
Return
det(F)clamped away from zero, counting any inversion.- Return type:
- strain_energy(deformation_gradient)[source]
Return the per-element strain energy density, in kilopascals.
- Return type:
- physiotwin4d.train_physicsnemo_physics_informed_motion.neo_hookean_pde(mu_kpa, lambda_lame_kpa)[source]
Return the neo-Hookean energy as a PhysicsNeMo Sym
PDE.The symbolic form is what
PhysicsInformedMotiondifferentiates: it is written in terms of the displacement fieldsu,vandw, so PhysicsNeMo Sym supplies their spatial derivatives and assemblesF = I + grad(u)itself.
- class physiotwin4d.train_physicsnemo_physics_informed_motion.PhysicsInformedMotion(tets, n_points, mu_kpa=10.0, lambda_lame_kpa=100.0, device=None, log_level=20)[source]
Evaluate the neo-Hookean residual of a predicted displacement field.
Spatial derivatives come from PhysicsNeMo Sym’s least-squares gradient reconstruction, the method built for unstructured meshes: it fits a gradient at every node over that node’s edge neighborhood. The neighborhood is fixed by the template’s topology, so the connectivity is built once here rather than per batch.
- Parameters:
tets (
ndarray) –(n_tet, 4)template element node ids.n_points (
int) – Node count of the template.mu_kpa (
float) – Shear modulus, in kilopascals.lambda_lame_kpa (
float) – First Lame parameter, in kilopascals.device (
Optional[device]) – Device the residual is evaluated on. Defaults to the GPU when there is one, since this is the most expensive part of the loss and has to sit where the predictions are. Pass it explicitly when training somewhere other than the default device.log_level (
int|str) – Logging level. Default:logging.INFO.
- __init__(tets, n_points, mu_kpa=10.0, lambda_lame_kpa=100.0, device=None, log_level=20)[source]
Initialize the base class with logging configuration.
- Parameters:
class_name – Name for the class (used in log messages). If None, uses the class name. Default: None
log_level (
int|str) – Logging level. Can be an integer (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL) or a string (‘DEBUG’, ‘INFO’, ‘WARNING’, ‘ERROR’, ‘CRITICAL’). Default: logging.INFOlog_to_file – Optional file path to write logs to in addition to console output. Default: None
- property device: torch.device
Device this residual’s connectivity and symbolic graph were built on.
Fixed at construction:
PhysicsInformeris given the device when its graph is compiled, so the residual cannot be moved afterwards. The trainer checks this against the device it predicts on.Read from a tensor the residual actually owns, so the CUDA index is concrete even when the caller passed an index-less
torch.device("cuda").
- property inverted_element_count: int
Nodes whose Jacobian went non-positive since this was built.
Non-zero means the network predicted motion that turns tissue inside out somewhere. The energy clamps
Jto stay finite and trainable, so without this count an inversion would leave no trace.
- __call__(reference_points, displacement_mm, nodal_volumes)[source]
Return the volume-weighted
(strain energy, incompressibility).- Parameters:
reference_points (
Tensor) –(n_points, 3)undeformed positions of this subject, in millimeters.displacement_mm (
Tensor) –(n_points, 3)predicted displacement, in millimeters rather than in the normalized units the data loss is scored on.nodal_volumes (
Tensor) –(n_points,)quadrature weights fromtet_volumes().
- Return type:
- Returns:
Two scalars, each a nodal-volume-weighted mean over the mesh.
- class physiotwin4d.train_physicsnemo_physics_informed_motion.TrainPhysicsNeMoPhysicsInformedMotion(log_level=20)[source]
Train a MeshGraphNet whose loss also prices the tissue’s strain energy.
Identical to
physiotwin4d.TrainPhysicsNeMoMGNapart from the loss, which becomesdata + lambda_physics * (energy + incompressibility). The data term is scored on normalized targets, as before, while the physics term is scored in millimeters and kilopascals, so predictions are returned to physical units before the residual sees them.Call
set_mechanics(),set_elements()andset_reference_meshes()before training, unlesslambda_physicsis zero – which reproduces the data-only MeshGraphNet exactly and is the ablation a physics-informed run is measured against.- set_mechanics(residual, lambda_physics=0.1)[source]
Set the constitutive residual and how heavily it is weighted.
- Parameters:
residual (
Optional[PhysicsInformedMotion]) – ConfiguredPhysicsInformedMotion, orNoneto train on displacement alone.lambda_physics (
float) – Weight of the physics term.0.0skips it entirely, reproducing the data-only MeshGraphNet, which is the ablation a physics-informed run is measured against.
- Raises:
ValueError – If lambda_physics is negative, or is positive without a residual to evaluate.
- Return type:
- property inverted_element_count: int
Elements whose Jacobian went non-positive during training.
Non-zero means the network predicted motion that turns tissue inside out somewhere, which the clamp keeps trainable but does not make physical.
- set_reference_meshes(reference_meshes)[source]
Set each subject’s fitted reference mesh, keyed by subject id.
These are the undeformed configurations the strain energy is measured against – the same meshes the manifests name as
fitted_reference_mesh, whose points the targets are defined at.- Return type:
- set_elements(tets)[source]
Set the
(n_tet, 4)template elements the residual is summed over.- Return type:
Notes
The reference configuration is the subject’s own fit, not the mean. The
stored targets are phase.points - fitted_reference.points, so the fitted
reference is the undeformed state. Measuring the residual against the
population mean instead would charge every subject a strain energy for merely
being shaped unlike the mean, confusing variation between subjects with
deformation within one.
Two formulations of one law. The symbolic energy
(neo_hookean_pde())
is what PhysicsNeMo Sym differentiates during training; the tensor one
(NeoHookeanResidual)
computes the Cauchy stress for export, which the symbolic path does not hand
back. tests/test_physics_informed_motion.py cross-checks them against each
other on the same field.
Loss scale. The data term is scored on normalized displacement and the
physics term in millimeters and kilopascals, so lambda_physics is a value to
sweep rather than one to trust. The two components are accumulated separately so
they can be reported apart.