https://mooseframework.inl.gov
NEML2ModelExecutor.C
Go to the documentation of this file.
1 //* This file is part of the MOOSE framework
2 //* https://mooseframework.inl.gov
3 //*
4 //* All rights reserved, see COPYRIGHT for full restrictions
5 //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
6 //*
7 //* Licensed under LGPL 2.1, please see LICENSE for details
8 //* https://www.gnu.org/licenses/lgpl-2.1.html
9 
10 #include "NEML2ModelExecutor.h"
11 #include "MOOSEToNEML2.h"
12 #include "NEML2Utils.h"
13 #include <string>
14 
15 #ifdef NEML2_ENABLED
16 #include "libmesh/id_types.h"
17 #include "neml2/tensors/functions/jacrev.h"
18 #include "neml2/dispatchers/ValueMapLoader.h"
19 #endif
20 
22 
25 {
26  auto params = emptyInputParameters();
27  // allow user to explicit skip required input variables
28  params.addParam<std::vector<std::string>>(
29  "skip_inputs",
30  {},
32  "List of NEML2 variables to skip error checking when setting up the model input. If an "
33  "input variable is skipped, its value will stay zero. If a required input variable is "
34  "not skipped, an error will be raised."));
35  return params;
36 }
37 
40 {
43  params.addClassDescription(NEML2Utils::docstring("Execute the specified NEML2 model"));
44 
45  params.addRequiredParam<UserObjectName>(
46  "batch_index_generator",
47  "The NEML2BatchIndexGenerator used to generate the element-to-batch-index map.");
48  params.addParam<std::vector<UserObjectName>>(
49  "gatherers",
50  {},
52  "List of MOOSE*ToNEML2 user objects gathering MOOSE data as NEML2 input variables"));
53  params.addParam<std::vector<UserObjectName>>(
54  "param_gatherers",
55  {},
57  "List of MOOSE*ToNEML2 user objects gathering MOOSE data as NEML2 model parameters"));
58 
59  // Since we use the NEML2 model to evaluate the residual AND the Jacobian at the same time, we
60  // want to execute this user object only at execute_on = LINEAR (i.e. during residual evaluation).
61  // The NONLINEAR exec flag below is for computing Jacobian during automatic scaling.
63  execute_options = {EXEC_INITIAL, EXEC_LINEAR, EXEC_NONLINEAR};
64  params.set<ExecFlagEnum>("execute_on") = execute_options;
65 
66  return params;
67 }
68 
71 #ifdef NEML2_ENABLED
72  ,
73  _batch_index_generator(getUserObject<NEML2BatchIndexGenerator>("batch_index_generator")),
74  _output_ready(false),
75  _error_message("")
76 #endif
77 {
78 #ifdef NEML2_ENABLED
79  validateModel();
80 
81  // add user object dependencies by name (the UOs do not need to exist yet for this)
82  for (const auto & gatherer_name : getParam<std::vector<UserObjectName>>("gatherers"))
83  _depend_uo.insert(gatherer_name);
84  for (const auto & gatherer_name : getParam<std::vector<UserObjectName>>("param_gatherers"))
85  _depend_uo.insert(gatherer_name);
86 
87  // variables to skip error checking (converting vector to set to prevent duplicate checks)
88  for (const auto & var_name : getParam<std::vector<std::string>>("skip_inputs"))
89  _skip_vars.insert(NEML2Utils::parseVariableName(var_name));
90 #endif
91 }
92 
93 #ifdef NEML2_ENABLED
94 void
96 {
97  // deal with user object provided inputs
98  for (const auto & gatherer_name : getParam<std::vector<UserObjectName>>("gatherers"))
99  {
100  // gather coupled user objects late to ensure they are constructed. Do not add them as
101  // dependencies (that's already done in the constructor).
102  const auto & uo = getUserObjectByName<MOOSEToNEML2>(gatherer_name, /*is_dependency=*/false);
103 
104  // the target neml2 variable must exist on the input axis
105  if (!model().input_axis().has_variable(NEML2Utils::parseVariableName(uo.NEML2Name())))
106  mooseError("The MOOSEToNEML2 gatherer named '",
107  gatherer_name,
108  "' is gathering MOOSE data for a non-existent NEML2 input variable named '",
109  uo.NEML2Name(),
110  "'.");
111 
112  // tell the gatherer to gather for a model input variable
113  const auto varname = NEML2Utils::parseVariableName(uo.NEML2Name());
114  if (varname.is_old_force() || varname.is_old_state())
116  else
117  uo.setMode(MOOSEToNEML2::Mode::VARIABLE);
118 
119  addGatheredVariable(gatherer_name, uo.NEML2VariableName());
120  _gatherers.push_back(&uo);
121  }
122 
123  // deal with user object provided model parameters
124  for (const auto & gatherer_name : getParam<std::vector<UserObjectName>>("param_gatherers"))
125  {
126  // gather coupled user objects late to ensure they are constructed. Do not add them as
127  // dependencies (that's already done in the constructor).
128  const auto & uo = getUserObjectByName<MOOSEToNEML2>(gatherer_name, /*is_dependency=*/false);
129 
130  // introspect the NEML2 model to figure out if the gatherer UO is gathering for a NEML2 input
131  // variable or for a NEML2 model parameter
132  if (model().named_parameters().count(uo.NEML2Name()) != 1)
133  mooseError("The MOOSEToNEML2 gatherer named '",
134  gatherer_name,
135  "' is gathering MOOSE data for a non-existent NEML2 model parameter named '",
136  uo.NEML2Name(),
137  "'.");
138 
139  // tell the gatherer to gather for a model parameter
140  uo.setMode(MOOSEToNEML2::Mode::PARAMETER);
141 
142  addGatheredParameter(gatherer_name, uo.NEML2ParameterName());
143  _gatherers.push_back(&uo);
144  }
145 
146  // iterate over set of required inputs and error out if we find one that is not provided
147  std::vector<neml2::VariableName> required_inputs = model().input_axis().variable_names();
148  for (const auto & input : required_inputs)
149  {
150  if (_skip_vars.count(input))
151  continue;
152  // skip input state variables because they are "initial guesses" to the nonlinear system
153  if (input.is_state())
154  continue;
155  if (!_gathered_variable_names.count(input))
156  paramError("gatherers", "The required model input `", input, "` is not gathered");
157  }
158 
159  // If a variable is stateful, then it'd better been retrieved by someone! In theory that's not
160  // sufficient for stateful data management, but that's the best we can do here without being
161  // overly restrictive.
162  for (const auto & input : required_inputs)
163  if (input.is_old_state() && !_retrieved_outputs.count(input.current()))
164  mooseError(
165  "The NEML2 model requires a stateful input variable `",
166  input,
167  "`, but its state counterpart on the output axis has not been retrieved by any object. "
168  "Therefore, there is no way to properly propagate the corresponding stateful data in "
169  "time. The common solution to this problem is to add a NEML2ToMOOSE retriever such as "
170  "those called `NEML2To*MOOSEMaterialProperty`.");
171 }
172 
173 std::size_t
175 {
176  return _batch_index_generator.getBatchIndex(elem_id);
177 }
178 
179 void
180 NEML2ModelExecutor::addGatheredVariable(const UserObjectName & gatherer_name,
181  const neml2::VariableName & var)
182 {
183  if (_gathered_variable_names.count(var))
184  paramError("gatherers",
185  "The NEML2 input variable `",
186  var,
187  "` gathered by UO '",
188  gatherer_name,
189  "' is already gathered by another gatherer.");
190  _gathered_variable_names.insert(var);
191 }
192 
193 void
194 NEML2ModelExecutor::addGatheredParameter(const UserObjectName & gatherer_name,
195  const std::string & param)
196 {
197  if (_gathered_parameter_names.count(param))
198  paramError("gatherers",
199  "The NEML2 model parameter `",
200  param,
201  "` gathered by UO '",
202  gatherer_name,
203  "' is already gathered by another gatherer.");
204  _gathered_parameter_names.insert(param);
205 }
206 
207 void
209 {
211  return;
212 
213  _output_ready = false;
214  _error = false;
215  _error_message.clear();
216 }
217 
218 void
220 {
222  return;
223 
224  _output_ready = false;
225 }
226 
227 void
229 {
231  return;
232 
233  fillInputs();
234 
235  if (_t_step > 0)
236  {
237  applyPredictor();
238  auto success = solve();
239  if (success)
240  extractOutputs();
241  }
242 }
243 
244 void
246 {
247  try
248  {
249  for (const auto & uo : _gatherers)
250  uo->insertInto(_in, _model_params);
251 
252  // Send input variables and parameters to device
253  for (auto & [var, val] : _in)
254  val = val.to(device());
255  for (auto & [param, pval] : _model_params)
256  pval = pval.to(device());
257 
258  // Update model parameters
259  model().set_parameters(_model_params);
260  _model_params.clear();
261 
262  // Request gradient for the model parameters that we request AD for
263  for (const auto & [y, dy] : _retrieved_parameter_derivatives)
264  for (const auto & [p, tensor] : dy)
265  model().get_parameter(p).requires_grad_(true);
266  }
267  catch (std::exception & e)
268  {
269  mooseError("An error occurred while filling inputs for the NEML2 model. Error message:\n",
270  e.what(),
272  }
273 }
274 
275 void
277 {
278  try
279  {
280  if (!model().input_axis().has_state())
281  return;
282  if (!model().input_axis().has_old_state())
283  return;
284 
285  // Set trial state variables (i.e., initial guesses).
286  // Right now we hard-code to use the old state as the trial state.
287  // TODO: implement other predictors
288  const auto & input_state = model().input_axis().subaxis(neml2::STATE);
289  const auto & input_old_state = model().input_axis().subaxis(neml2::OLD_STATE);
290  for (const auto & var : input_state.variable_names())
291  if (input_old_state.has_variable(var))
292  _in[var.prepend(neml2::STATE)] = _in[var.prepend(neml2::OLD_STATE)];
293  }
294  catch (std::exception & e)
295  {
296  mooseError("An error occurred while applying predictor for the NEML2 model. Error message:\n",
297  e.what(),
299  }
300 }
301 
302 void
304 {
305  // Figure out what our batch size is
306  std::vector<neml2::Tensor> defined;
307  for (const auto & [key, value] : _in)
308  defined.push_back(value);
309  const auto batch_shape = neml2::utils::broadcast_batch_sizes(defined);
310 
311  // Make all inputs conformal
312  for (auto & [key, value] : _in)
313  if (value.batch_sizes() != batch_shape)
314  _in[key] = value.batch_unsqueeze(0).batch_expand(batch_shape);
315 }
316 
317 bool
319 {
320  try
321  {
322  // Evaluate the NEML2 material model
323  TIME_SECTION("NEML2 solve", 3, "Solving NEML2 material model");
324 
325  // NEML2 requires double precision
326  auto prev_dtype = neml2::get_default_dtype();
327  neml2::set_default_dtype(neml2::kFloat64);
328 
329  if (scheduler())
330  {
331  // We only need consistent batch sizes if we are using the dispatcher
332  expandInputs();
333  neml2::ValueMapLoader loader(_in, 0);
334  std::tie(_out, _dout_din) = dispatcher()->run(loader);
335  }
336  else
337  std::tie(_out, _dout_din) = model().value_and_dvalue(_in);
338  _in.clear();
339 
340  // Restore the default dtype
341  neml2::set_default_dtype(prev_dtype);
342  }
343  catch (std::exception & e)
344  {
345  _error_message = e.what();
346  _error = true;
347  }
348 
349  return !_error;
350 }
351 
352 void
354 {
355  try
356  {
357  const auto N = _batch_index_generator.getBatchIndex();
358 
359  // retrieve outputs
360  for (auto & [y, target] : _retrieved_outputs)
361  target = _out[y].to(torch::kCPU);
362 
363  // retrieve parameter derivatives
364  for (auto & [y, dy] : _retrieved_parameter_derivatives)
365  for (auto & [p, target] : dy)
366  target = neml2::jacrev(_out[y],
367  model().get_parameter(p),
368  /*retain_graph=*/true,
369  /*create_graph=*/false,
370  /*allow_unused=*/false)
371  .to(torch::kCPU);
372 
373  // clear output
374  _out.clear();
375 
376  // retrieve derivatives
377  for (auto & [y, dy] : _retrieved_derivatives)
378  for (auto & [x, target] : dy)
379  {
380  const auto & source = _dout_din[y][x];
381  if (source.defined())
382  target = source.to(torch::kCPU).batch_expand({neml2::Size(N)});
383  }
384 
385  // clear derivatives
386  _dout_din.clear();
387  }
388  catch (std::exception & e)
389  {
390  mooseError("An error occurred while retrieving outputs from the NEML2 model. Error message:\n",
391  e.what(),
393  }
394 }
395 
396 void
398 {
400  return;
401 
402  // See if any rank failed
403  processor_id_type pid;
405 
406  // Fail the next nonlinear convergence check if any rank failed
407  if (_error)
408  {
410  if (_communicator.rank() == 0)
411  {
412  std::string msg = "NEML2 model execution failed on at least one processor with ID " +
413  std::to_string(pid) + ". Error message:\n";
414  msg += _error_message;
415  if (_fe_problem.isTransient())
416  msg += "\nTo recover, the solution will fail and then be re-attempted with a reduced time "
417  "step.";
418  _console << COLOR_YELLOW << msg << COLOR_DEFAULT << std::endl;
419  }
421  }
422  else if (_t_step > 0)
423  _output_ready = true;
424 }
425 
426 void
428 {
430  mooseError("NEML2 output variables and derivatives must be retrieved during object "
431  "construction. This is a code problem.");
432 }
433 
434 const neml2::Tensor &
435 NEML2ModelExecutor::getOutput(const neml2::VariableName & output_name) const
436 {
438 
439  if (!model().output_axis().has_variable(output_name))
440  mooseError("Trying to retrieve a non-existent NEML2 output variable '", output_name, "'.");
441 
442  return _retrieved_outputs[output_name];
443 }
444 
445 const neml2::Tensor &
446 NEML2ModelExecutor::getOutputDerivative(const neml2::VariableName & output_name,
447  const neml2::VariableName & input_name) const
448 {
450 
451  if (!model().output_axis().has_variable(output_name))
452  mooseError("Trying to retrieve the derivative of NEML2 output variable '",
453  output_name,
454  "' with respect to NEML2 input variable '",
455  input_name,
456  "', but the NEML2 output variable does not exist.");
457 
458  if (!model().input_axis().has_variable(input_name))
459  mooseError("Trying to retrieve the derivative of NEML2 output variable '",
460  output_name,
461  "' with respect to NEML2 input variable '",
462  input_name,
463  "', but the NEML2 input variable does not exist.");
464 
465  return _retrieved_derivatives[output_name][input_name];
466 }
467 
468 const neml2::Tensor &
469 NEML2ModelExecutor::getOutputParameterDerivative(const neml2::VariableName & output_name,
470  const std::string & parameter_name) const
471 {
473 
474  if (!model().output_axis().has_variable(output_name))
475  mooseError("Trying to retrieve the derivative of NEML2 output variable '",
476  output_name,
477  "' with respect to NEML2 model parameter '",
478  parameter_name,
479  "', but the NEML2 output variable does not exist.");
480 
481  if (model().named_parameters().count(parameter_name) != 1)
482  mooseError("Trying to retrieve the derivative of NEML2 output variable '",
483  output_name,
484  "' with respect to NEML2 model parameter '",
485  parameter_name,
486  "', but the NEML2 model parameter does not exist.");
487 
488  return _retrieved_parameter_derivatives[output_name][parameter_name];
489 }
490 
491 #endif
virtual void addGatheredVariable(const UserObjectName &, const neml2::VariableName &)
Register a NEML2 input variable gathered by a gatherer.
neml2::WorkScheduler * scheduler()
Get the work scheduler.
std::vector< const MOOSEToNEML2 * > _gatherers
MOOSE data gathering user objects.
A MultiMooseEnum object to hold "execute_on" flags.
Definition: ExecFlagEnum.h:21
static InputParameters validParams()
void execute() override
Execute method.
neml2::DerivMap _retrieved_derivatives
set of derivatives that were retrieved (by other objects)
bool shouldCompute(const SubProblem &)
Determine whether the NEML2 material model should be evaluated.
Definition: NEML2Utils.C:59
Interface class to provide common input parameters, members, and methods for MOOSEObjects that use NE...
virtual bool solve()
Perform the material update.
neml2::Model & model() const
Get the NEML2 model.
bool _error
Whether an error was encountered.
const neml2::Device & device() const
Get the target compute device.
processor_id_type rank() const
The main MOOSE class responsible for handling user-defined parameters in almost every MOOSE system...
std::set< neml2::VariableName > _gathered_variable_names
neml2::ValueMap _out
The output variables of the material model.
static std::string NEML2_help_message
Definition: NEML2Utils.h:152
const Parallel::Communicator & _communicator
const std::unique_ptr< DispatcherType > & dispatcher() const
Get the work dispatcher.
virtual void extractOutputs()
Extract output derivatives with respect to input variables and model parameters.
registerMooseObject("MooseApp", NEML2ModelExecutor)
void setFailNextNonlinearConvergenceCheck()
Skip further residual evaluations and fail the next nonlinear convergence check(s) ...
InputParameters emptyInputParameters()
ExecFlagEnum getDefaultExecFlagEnum()
Return the default ExecFlagEnum for MOOSE.
Definition: MooseUtils.C:1067
std::set< std::string > _gathered_parameter_names
void initialSetup() override
Gets called at the beginning of the simulation before this object is asked to do its job...
const NEML2BatchIndexGenerator & _batch_index_generator
The NEML2BatchIndexGenerator used to generate the element-to-batch-index map.
const neml2::Tensor & getOutputDerivative(const neml2::VariableName &output_name, const neml2::VariableName &input_name) const
Get a reference(!) to the requested output derivative view.
std::string _error_message
Error message.
uint8_t processor_id_type
int & _t_step
The number of the time step.
std::set< neml2::VariableName > _skip_vars
Real value(unsigned n, unsigned alpha, unsigned beta, Real x)
std::map< neml2::VariableName, std::map< std::string, neml2::Tensor > > _retrieved_parameter_derivatives
set of parameter derivatives that were retrieved (by other objects)
virtual void fillInputs()
Fill input variables and model parameters using the gatherers.
std::string docstring(const std::string &desc)
Augment docstring if NEML2 is not enabled.
Definition: NEML2Utils.C:77
const T & getParam(const std::string &name) const
Retrieve a parameter for the object.
void initialize() override
Called before execute() is ever called so that data can be cleared.
void paramError(const std::string &param, Args... args) const
Emits an error prefixed with the file and line number of the given param (from the input file) along ...
void maxloc(T &r, unsigned int &max_id) const
const ExecFlagType EXEC_LINEAR
Definition: Moose.C:29
virtual void checkExecutionStage() const final
Prevent output and derivative retrieval after construction.
virtual void applyPredictor()
Apply the predictor to set current trial state.
void broadcast(T &data, const unsigned int root_id=0, const bool identical_sizes=false) const
static InputParameters actionParams()
Parameters that can be specified under the NEML2Action common area.
const ExecFlagType EXEC_NONLINEAR
Definition: Moose.C:31
std::set< std::string > _depend_uo
Depend UserObjects that to be used both for determining user object sorting and by AuxKernel for find...
Definition: UserObject.h:228
const neml2::Tensor & getOutput(const neml2::VariableName &output_name) const
Get a reference(!) to the requested output view.
std::map< std::string, neml2::Tensor > _model_params
The model parameters to update (gathered from MOOSE)
NEML2ModelExecutor executes a NEML2 model.
bool _output_ready
flag that indicates if output data has been fully computed
FEProblemBase & _fe_problem
Reference to the FEProblemBase for this user object.
Definition: UserObject.h:211
neml2::DerivMap _dout_din
The derivative of the output variables w.r.t. the input variables.
virtual void addGatheredParameter(const UserObjectName &, const std::string &)
Register a NEML2 model parameter gathered by a gatherer.
void meshChanged() override
Called on this object when the mesh changes.
void mooseError(Args &&... args) const
Emits an error prefixed with object name and type.
static InputParameters validParams()
const neml2::Tensor & getOutputParameterDerivative(const neml2::VariableName &output_name, const std::string &parameter_name) const
Get a reference(!) to the requested output parameter derivative view.
void finalize() override
Finalize.
const ConsoleStream _console
An instance of helper class to write streams to the Console objects.
virtual void validateModel() const
Validate the NEML2 material model.
virtual bool isTransient() const override
virtual void expandInputs()
Expand tensor shapes if necessary to conformal sizes.
std::size_t getBatchIndex() const
Get the current batch index (in almost all cases this is the total batch size)
class infix_ostream_iterator if defined(__GNUC__) &&!defined(__clang__) &&(__GNUC__<
GCC9 currently hits a "no type named &#39;value_type&#39;" error during build if this is removed and iterator...
neml2::VariableName parseVariableName(const std::string &)
Parse a raw string into NEML2 variable name.
Definition: NEML2Utils.C:49
std::size_t getBatchIndex(dof_id_type elem_id) const
Get the batch index for the given element ID.
neml2::ValueMap _in
The input variables of the material model.
neml2::ValueMap _retrieved_outputs
set of output variables that were retrieved (by other objects)
NEML2ModelExecutor(const InputParameters &params)
uint8_t dof_id_type
virtual bool startedInitialSetup()
Returns true if we are in or beyond the initialSetup stage.
NEML2BatchIndexGenerator iterates over the mesh and generates a map from element ID to batch index wh...
const ExecFlagType EXEC_INITIAL
Definition: Moose.C:28