NEML2 2.0.0
All Classes Namespaces Functions Variables Typedefs Enumerations Enumerator Friends Modules Pages
Model composition

Problem definition

Recall that the complete model for simulating the projectile trajectory is

(1)x˙=v,(2)v˙=a=gμv,(3)r=={xxn(ttn)x˙vvn(ttn)v˙},(4){xv}=rootx,v(r),

subject to appropriate initial conditions x0 and v0

Among these equations:

This tutorial demonstrates that our custom model ProjectileAcceleration can be composed with other existing, predefined NEML2 models.

Input file

The following input file composes the constitutive model for a single-step update for 5 projectiles each with a different dynamic viscosity, i.e., the shape of "mu" is (5;).

[Tensors]
[g]
type = Vec
values = '0 -9.81 0'
[]
[mu]
type = Scalar
values = '0.01 0.05 0.1 0.5 1'
batch_shape = (5)
[]
[]
[Models]
[eq2]
type = ProjectileAcceleration
velocity = 'state/v'
acceleration = 'state/a'
gravitational_acceleration = 'g'
dynamic_viscosity = 'mu'
[]
[eq3a]
type = VecBackwardEulerTimeIntegration
variable = 'state/x'
rate = 'state/v'
[]
[eq3b]
type = VecBackwardEulerTimeIntegration
variable = 'state/v'
rate = 'state/a'
[]
[eq3]
type = ComposedModel
models = 'eq3a eq3b'
[]
[system]
type = ComposedModel
models = 'eq2 eq3'
[]
[eq4]
type = ImplicitUpdate
implicit_model = 'system'
solver = 'newton'
[]
[]
[Solvers]
[newton]
type = Newton
rel_tol = 1e-08
abs_tol = 1e-10
max_its = 50
[]
[]

Lauching the projectiles

To obtain the entire trajectory, the constitutive model need to be recursively integrated. As discussed in a previous tutorial, a transient driver should be used to perform the recursive constitutive update. Conveniently, in this example, since we are dealing with an autonomous system (i.e., no external "forces" are needed to drive the constitutive update), the vanilla TransientDriver can be used:

[Drivers]
[driver]
type = TransientDriver
model = 'eq4'
prescribed_time = 'times'
ic_Vec_names = 'state/x state/v'
ic_Vec_values = 'x0 v0'
save_as = 'result.pt'
show_input_axis = true
show_output_axis = true
[]
[]
[Tensors]
[end_time]
type = Scalar
values = '1'
batch_shape = (1,1)
[]
[times]
type = LinspaceScalar
start = 0
end = 'end_time'
nstep = 100
[]
[x0]
type = Vec
values = '0 0 0'
[]
[v0]
type = Vec
values = "10 5 0
8 6 0"
batch_shape = (2,1)
[]
[]

The five projectiles are launched from the same position (the origin) but with two different lauching velocities. Note how broadcasting is used to simultaneously simulate the trajectories of all 10 combinations.

#include "neml2/drivers/Driver.h"
int
main()
{
using namespace neml2;
load_input("input.i");
auto & driver = get_driver("driver");
driver.run();
}
Definition DiagnosticsInterface.cxx:30
constexpr auto kFloat64
Definition types.h:53
void set_default_dtype(Dtype dtype)
Definition defaults.cxx:32
Driver & get_driver(const std::string &dname)
A convenient function to manufacture a neml2::Driver.
Definition Driver.cxx:34
void load_input(const std::filesystem::path &path, const std::string &additional_input)
A convenient function to parse all options from an input file.
Definition Factory.cxx:35

Output

eq4's input axis:
0: forces/t [(0, 1)]
1: old_forces/t [(1, 2)]
2: old_state/v [(2, 5)]
3: old_state/x [(5, 8)]
4: state/v [(8, 11)]
5: state/x [(11, 14)]
eq4's output axis:
0: state/v [(0, 3)]
1: state/x [(3, 6)]

The following Python script plots the trajectories loaded from "result.pt" written by the driver.

import torch
from matplotlib import pyplot as plt
# Load the result
res = torch.jit.load("result.pt")
O = dict(res.output.named_buffers())
# Plot
fig, ax = plt.subplots(1, 2, figsize=(10, 5))
nstep = 100
nvel = 2 # two launching velocities
nproj = 5 # five projectiles (with different dynamic viscosity)
for i in range(nvel):
for j in range(nproj):
x = [O["{}.state/x".format(n)][i, j, 0].item() for n in range(1, nstep)]
y = [O["{}.state/x".format(n)][i, j, 1].item() for n in range(1, nstep)]
ax[i].plot(x, y, "--")
ax[i].set_xlabel("x")
ax[i].set_ylabel("y")
ax[i].grid()
fig.tight_layout()
fig.savefig("trajectories.svg")
Projectile trajectories