https://mooseframework.inl.gov
ThermochimicaData.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 "ThermochimicaData.h"
12 #include "ThermochimicaUtils.h"
13 #include "ActionWarehouse.h"
14 #include "libmesh/int_range.h"
15 
16 #include <sys/mman.h> // for mmap
17 #include <unistd.h> // for fork
18 #include <sys/socket.h> // for socketpair
19 #include <csignal> // for kill
20 
21 #ifdef THERMOCHIMICA_ENABLED
22 #include "Thermochimica-cxx.h"
23 #include "checkUnits.h"
24 #endif
25 
26 registerMooseObject("ChemicalReactionsApp", ThermochimicaNodalData);
27 registerMooseObject("ChemicalReactionsApp", ThermochimicaElementData);
28 
29 template <bool is_nodal>
32 {
34 
35  if constexpr (is_nodal)
37  params, "Provides access to Thermochimica-calculated data at nodes.");
38  else
40  params, "Provides access to Thermochimica-calculated data at elements.");
41 
42  params.addRequiredCoupledVar("elements", "Amounts of elements");
43  params.addRequiredCoupledVar("temperature", "Coupled temperature");
44  params.addCoupledVar("pressure", 1.0, "Pressure");
45 
46  MooseEnum reinit_type("none time nodal", "nodal");
47  params.addParam<MooseEnum>(
48  "reinit_type", reinit_type, "Reinitialization scheme to use with Thermochimica");
49 
50  params.addCoupledVar("output_element_potentials", "Chemical potentials of elements");
51  params.addCoupledVar("output_phases", "Amounts of phases to be output");
52  params.addCoupledVar("output_species", "Amounts of species to be output");
53  params.addCoupledVar("output_vapor_pressures", "Vapour pressures of species to be output");
54  params.addCoupledVar("output_element_phases",
55  "Elements whose molar amounts in specific phases are requested");
56 
57  MooseEnum mUnit_op("mole_fraction moles", "moles");
58  params.addParam<MooseEnum>(
59  "output_species_unit", mUnit_op, "Mass unit for output species: mole_fractions or moles");
60 
61  if constexpr (is_nodal)
62  params.set<bool>("unique_node_execute") = true;
63 
64  params.addPrivateParam<ChemicalCompositionAction *>("_chemical_composition_action");
65  params.addParam<FileName>("thermofile",
66  "Thermodynamic file to be used for Thermochimica calculations");
67  return params;
68 }
69 
70 void
72 {
73  exit(0);
74 }
75 
76 template <bool is_nodal>
78  : ThermochimicaDataBaseParent<is_nodal>(parameters),
79  _pressure(coupledValue("pressure")),
80  _temperature(coupledValue("temperature")),
81  _n_phases(coupledComponents("output_phases")),
82  _n_species(coupledComponents("output_species")),
83  _n_elements(coupledComponents("elements")),
84  _n_vapor_species(coupledComponents("output_vapor_pressures")),
85  _n_phase_elements(coupledComponents("output_element_phases")),
86  _n_potentials(coupledComponents("output_element_potentials")),
87  _el(_n_elements),
88  _action(*parameters.getCheckedPointerParam<ChemicalCompositionAction *>(
89  "_chemical_composition_action")),
90  _el_ids(_action.elementIDs()),
91  _reinit(parameters.get<MooseEnum>("reinit_type").getEnum<ReinitializationType>()),
92  _ph_names(_action.phases()),
93  _element_potentials(_action.elementPotentials()),
94  _species_phase_pairs(_action.speciesPhasePairs()),
95  _vapor_phase_pairs(_action.vaporPhasePairs()),
96  _phase_element_pairs(_action.phaseElementPairs()),
97  _output_element_potentials(isCoupled("output_element_potentials")),
98  _output_vapor_pressures(isCoupled("output_vapor_pressures")),
99  _output_element_phases(isCoupled("output_element_phases")),
100  _ph(_n_phases),
101  _sp(_n_species),
102  _vp(_n_vapor_species),
103  _el_pot(_n_potentials),
104  _el_ph(_n_phase_elements),
105  _output_mass_unit(parameters.get<MooseEnum>("output_species_unit").getEnum<OutputMassUnit>())
106 {
108 
109  if (_el_ids.size() != _n_elements)
110  mooseError("Element IDs size does not match number of elements.");
111  for (const auto i : make_range(_n_elements))
112  _el[i] = &coupledValue("elements", i);
113 
114  if (isParamValid("output_phases"))
115  {
116  if (_ph_names.size() != _n_phases)
117  mooseError("Phase names vector size does not match number of phases.");
118 
119  for (const auto i : make_range(_n_phases))
120  _ph[i] = &writableVariable("output_phases", i);
121  }
122 
123  if (isParamValid("output_species"))
124  {
125  if (_species_phase_pairs.size() != _n_species)
126  mooseError("Species name vector size does not match number of output species.");
127 
128  for (const auto i : make_range(_n_species))
129  _sp[i] = &writableVariable("output_species", i);
130  }
131 
132  if (isParamValid("output_vapor_pressures"))
133  {
134  if (_vapor_phase_pairs.size() != _n_vapor_species)
135  mooseError("Vapor species name vector size does not match number of output vapor species.");
136 
137  for (const auto i : make_range(_n_vapor_species))
138  _vp[i] = &writableVariable("output_vapor_pressures", i);
139  }
140 
141  if (isParamValid("output_element_phases"))
142  {
144  mooseError("Element phase vector size does not match number of output elements in phases");
145 
146  for (const auto i : make_range(_n_phase_elements))
147  _el_ph[i] = &writableVariable("output_element_phases", i);
148  }
149 
150  if (isParamValid("output_element_potentials"))
151  {
152  if (_element_potentials.size() != _n_potentials)
153  mooseError("Element potentials vector size does not match number of element potentials "
154  "specified for output.");
155 
156  for (const auto i : make_range(_n_potentials))
157  _el_pot[i] = &writableVariable("output_element_potentials", i);
158  }
159  // buffer size
160  const auto dofid_size = std::max(/* send */ 1, /* receive */ 0);
161  const auto real_size =
162  std::max(/* send */ 2 + _n_elements,
163  /* receive */ _n_phases + _n_species + _element_potentials.size() +
165 
166  // set up shared memory for communication with child process
167  auto shared_mem =
168  static_cast<std::byte *>(mmap(nullptr,
169  dofid_size * sizeof(dof_id_type) + real_size * sizeof(Real),
170  PROT_READ | PROT_WRITE,
171  MAP_ANONYMOUS | MAP_SHARED,
172  -1 /* fd */,
173  0 /* offset */));
174  if (shared_mem == MAP_FAILED)
175  mooseError("Failed to allocate shared memory for thermochimica IPC.");
176 
177  // set up buffer partitions
178  _shared_dofid_mem = reinterpret_cast<dof_id_type *>(shared_mem);
179  _shared_real_mem = reinterpret_cast<Real *>(shared_mem + dofid_size * sizeof(dof_id_type));
180 
181  // set up a bidirectional communication socket
182  int sockets[2];
183  if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) < 0)
184  mooseError("Failed to create socketpair for thermochimica IPC.");
185 
186  // fork child process that will manage thermochimica calls
187  _pid = fork();
188  if (_pid < 0)
189  mooseError("Fork failed for thermochimica library.");
190  if (_pid == 0)
191  {
192  // here we are in the child process
193  _socket = sockets[0];
194  // clean exit upon SIGTERM (mainly for Civet code coverage)
195  signal(SIGTERM, ThermochimicaDataBase_handler);
196 
197 #ifdef THERMOCHIMICA_ENABLED
198  // Initialize database in Thermochimica
199  if (isParamValid("thermofile"))
200  {
201  const auto thermo_file = this->template getParam<FileName>("thermofile");
202 
203  if (thermo_file.length() > 1024)
204  this->paramError("thermofile",
205  "Path exceeds Thermochimica's maximal permissible length of 1024 with ",
206  thermo_file.length(),
207  " characters: ",
208  thermo_file);
209 
210  Thermochimica::setThermoFilename(thermo_file);
211 
212  // Read in thermodynamics model, only once
213  Thermochimica::parseThermoFile();
214 
215  const auto idbg = Thermochimica::checkInfoThermo();
216  if (idbg != 0)
217  this->paramError("thermofile", "Thermochimica data file cannot be parsed. ", idbg);
218  }
219 #endif
220 
221  while (true)
222  server();
223  }
224 
225  // parent process continues here
226  _socket = sockets[1];
227 }
228 
229 template <bool is_nodal>
231 {
232  if (_pid)
233  kill(_pid, SIGTERM);
234 }
235 
236 template <bool is_nodal>
237 void
239 {
240 }
241 
242 template <bool is_nodal>
243 template <typename T>
244 void
246 {
247  T msg;
248  while (read(_socket, &msg, sizeof(T)) == 0)
249  {
250  if (errno == EAGAIN)
251  continue;
252  mooseError("Read error waiting for '", expect_msg, "' ", errno, ' ', strerror(errno));
253  }
254  if (msg != expect_msg)
255  mooseError("Expected '", expect_msg, "' but received '", msg, "'");
256 }
257 
258 template <bool is_nodal>
259 template <typename T>
260 void
262 {
263  if (write(_socket, &send_msg, sizeof(T)) != sizeof(T))
264  mooseError("Failed to notify thermochimica library child process.");
265 }
266 
267 template <bool is_nodal>
268 void
270 {
271  // either one DOF at a node or (currently) one DOF for constant monomial FV!
272  // This is enforced automatically by the ChemicalComposition action, which creates the correct
273  // variables.
274  const unsigned int qp = 0;
275 
276  // store current dofID
277  if constexpr (is_nodal)
278  _shared_dofid_mem[0] = this->_current_node->id();
279  else
280  _shared_dofid_mem[0] = this->_current_elem->id();
281 
282  // store all required data in shared memory
283  _shared_real_mem[0] = _temperature[qp];
284  _shared_real_mem[1] = _pressure[qp];
285  for (const auto i : make_range(_n_elements))
286  _shared_real_mem[2 + i] = (*_el[i])[qp];
287 
288  // message child process to trigger calculation
289  notify('A');
290 
291  // and wait for the child process to signal end of calculation
292  expect('B');
293 
294  // unpack data from shared memory
295  std::size_t idx = 0;
296 
297  for (const auto i : make_range(_n_phases))
298  _ph[i]->setDofValue(_shared_real_mem[idx++], qp);
299 
300  for (const auto i : make_range(_n_species))
301  _sp[i]->setDofValue(_shared_real_mem[idx++], qp);
302 
303  if (_output_element_potentials)
304  for (const auto i : index_range(_element_potentials))
305  _el_pot[i]->setDofValue(_shared_real_mem[idx++], qp);
306 
307  if (_output_vapor_pressures)
308  for (const auto i : make_range(_n_vapor_species))
309  _vp[i]->setDofValue(_shared_real_mem[idx++], qp);
310 
311  if (_output_element_phases)
312  for (const auto i : make_range(_n_phase_elements))
313  _el_ph[i]->setDofValue(_shared_real_mem[idx++], qp);
314 }
315 
316 template <bool is_nodal>
317 void
319 {
320  // wait for message from parent process
321  expect('A');
322 
323 #ifdef THERMOCHIMICA_ENABLED
324  // fetch data from shared memory
325  _current_id = _shared_dofid_mem[0];
326 
327  auto temperature = _shared_real_mem[0];
328  auto pressure = _shared_real_mem[1];
329 
330  // Set temperature and pressure for thermochemistry solver
331  Thermochimica::setTemperaturePressure(temperature, pressure);
332 
333  // Reset all element masses to 0
334  Thermochimica::setElementMass(0, 0.0);
335 
336  // Set element masses
337  for (const auto i : make_range(_n_elements))
338  Thermochimica::setElementMass(_el_ids[i], _shared_real_mem[2 + i]);
339 
340  // Optionally ask for a re-initialization (if reinit_requested == true)
341  reinitDataMooseToTc();
342 
343  // Calculate thermochemical equilibrium
344  Thermochimica::thermochimica();
345 
346  // Check for error status
347  auto idbg = Thermochimica::checkInfoThermo();
348  if (idbg != 0)
349  // error out for now, but we should send a code to the parent process
350  mooseError("Thermochimica error ", idbg);
351 
352  // Save data for future reinits
353  reinitDataMooseFromTc();
354 
355  // Get requested phase indices if phase concentration output was requested
356  // i.e. if output_phases is coupled
357  auto moles_phase = Thermochimica::getMolesPhase();
358 
359  std::size_t idx = 0;
360 
361  for (const auto i : make_range(_n_phases))
362  {
363  // Is this maybe constant? No it isn't for now
364  auto [index, idbg] = Thermochimica::getPhaseIndex(_ph_names[i]);
365  if (idbg != 0)
366  mooseError("Failed to get index of phase '", _ph_names[i], "'");
367  // Convert from 1-based (fortran) to 0-based (c++) indexing
368  if (index - 1 < 0)
369  _shared_real_mem[idx] = 0.0;
370  else
371  _shared_real_mem[idx] = moles_phase[index - 1];
372  idx++;
373  }
374 
375  auto db_phases = Thermochimica::getPhaseNamesSystem();
376  auto getSpeciesMoles =
377  [this, moles_phase, db_phases](const std::string phase,
378  const std::string species) -> std::pair<double, int>
379  {
380  Real value = 0.0;
381  int code = 0;
382 
383  auto [index, idbg] = Thermochimica::getPhaseIndex(phase);
384 
385  if (Thermochimica::isPhaseMQM(
386  std::distance(db_phases.begin(), std::find(db_phases.begin(), db_phases.end(), phase))))
387  {
388  auto [fraction, idbg] = Thermochimica::getMqmqaPairMolFraction(phase, species);
389 
390  switch (_output_mass_unit)
391  {
392  case OutputMassUnit::FRACTION:
393  {
394  value = fraction;
395  code = idbg;
396  break;
397  }
398  case OutputMassUnit::MOLES:
399  {
400  auto [molesPair, idbgPair] = Thermochimica::getMqmqaMolesPairs(phase);
401  value = molesPair * fraction;
402  code = idbg + idbgPair;
403  break;
404  }
405  default:
406  break;
407  }
408  }
409  else
410  {
411  auto [fraction, idbg] = Thermochimica::getOutputMolSpeciesPhase(phase, species);
412  switch (_output_mass_unit)
413  {
414  case OutputMassUnit::FRACTION:
415  {
416  value = fraction;
417  code = idbg;
418  break;
419  }
420  case OutputMassUnit::MOLES:
421  {
422  value = index >= 1 ? moles_phase[index - 1] * fraction : 0.0;
423  code = idbg;
424  break;
425  }
426  default:
427  break;
428  }
429  }
430  return {value, code};
431  };
432 
433  for (const auto i : make_range(_n_species))
434  {
435  auto [fraction, idbg] = getSpeciesMoles(
436  _species_phase_pairs[i].first,
437  _species_phase_pairs[i].second); // can we somehow use IDs instead of strings here?
438 
439  if (idbg == 0)
440  _shared_real_mem[idx] = fraction;
441  else if (idbg == 1)
442  _shared_real_mem[idx] = 0.0;
443 #ifndef NDEBUG
444  else
445  mooseError("Failed to get phase speciation for phase '",
446  _species_phase_pairs[i].first,
447  "' and species '",
448  _species_phase_pairs[i].second,
449  "'. Thermochimica returned ",
450  idbg);
451 #endif
452  idx++;
453  }
454 
455  if (_output_element_potentials)
456  for (const auto i : index_range(_element_potentials))
457  {
458  auto [potential, idbg] = Thermochimica::getOutputChemPot(_element_potentials[i]);
459  if (idbg == 0)
460  _shared_real_mem[idx] = potential;
461  else if (idbg == 1)
462  // element not present, just leave this at 0 for now
463  _shared_real_mem[idx] = 0.0;
464 #ifndef NDEBUG
465  else if (idbg == -1)
466  mooseError("Failed to get element potential for element '",
467  _element_potentials[i],
468  "'. Thermochimica returned ",
469  idbg);
470 #endif
471  idx++;
472  }
473 
474  if (_output_vapor_pressures)
475  for (const auto i : make_range(_n_vapor_species))
476  {
477  auto [fraction, moles, idbg] =
478  Thermochimica::getOutputMolSpecies(_vapor_phase_pairs[i].second);
479  libmesh_ignore(moles);
480 
481  if (idbg == 0)
482  _shared_real_mem[idx] = fraction * pressure;
483  else if (idbg == 1)
484  _shared_real_mem[idx] = 0.0;
485 #ifndef NDEBUG
486  else
487  mooseError("Failed to get vapor pressure for phase '",
488  _vapor_phase_pairs[i].first,
489  "' and species '",
490  _vapor_phase_pairs[i].second,
491  "'. Thermochimica returned ",
492  idbg);
493 #endif
494  idx++;
495  }
496 
497  if (_output_element_phases)
498  for (const auto i : make_range(_n_phase_elements))
499  {
500  auto [moles, idbg] = Thermochimica::getElementMolesInPhase(_phase_element_pairs[i].second,
501  _phase_element_pairs[i].first);
502 
503  if (idbg == 0)
504  _shared_real_mem[idx] = moles;
505  else if (idbg == 1)
506  _shared_real_mem[idx] = 0.0;
507 #ifndef NDEBUG
508  else
509  mooseError("Failed to get moles of element '",
510  _phase_element_pairs[i].second,
511  "' in phase '",
512  _phase_element_pairs[i].first,
513  "'. Thermochimica returned ",
514  idbg);
515 #endif
516  idx++;
517  }
518 #endif
519  // Send message back to parent process
520  notify('B');
521 }
522 
523 template <bool is_nodal>
524 void
526 {
527 #ifdef THERMOCHIMICA_ENABLED
528  auto & d = _data[_current_id];
529 
530  if (_reinit != ReinitializationType::NONE)
531  {
532  Thermochimica::saveReinitData();
533  auto data = Thermochimica::getReinitData();
534 
535  if (_reinit == ReinitializationType::TIME)
536  {
537  d._assemblage = std::move(data.assemblage);
538  d._moles_phase = std::move(data.molesPhase);
539  d._element_potential = std::move(data.elementPotential);
540  d._chemical_potential = std::move(data.chemicalPotential);
541  d._mol_fraction = std::move(data.moleFraction);
542  d._elements_used = std::move(data.elementsUsed);
543  d._reinit_available = data.reinitAvailable;
544  }
545  }
546 #endif
547 }
548 
549 template <bool is_nodal>
550 void
552 {
553 #ifdef THERMOCHIMICA_ENABLED
554  // Tell Thermochimica whether a re-initialization is requested for this calculation
555  switch (_reinit)
556  {
557  case ReinitializationType::NONE:
558  Thermochimica::setReinitRequested(false);
559  break;
560  default:
561  Thermochimica::setReinitRequested(true);
562  }
563  // If we have re-initialization data and want a re-initialization, then
564  // load data into Thermochimica
565  auto it = _data.find(_current_id);
566 
567  if (it != _data.end() &&
568  _reinit == ReinitializationType::TIME) // If doing previous timestep reinit
569  {
570  auto & d = it->second;
571  if (d._reinit_available)
572  {
573  Thermochimica::resetReinit();
574  Thermochimica::ReinitializationData data;
575  data.assemblage = d._assemblage;
576  data.molesPhase = d._moles_phase;
577  data.elementPotential = d._element_potential;
578  data.chemicalPotential = d._chemical_potential;
579  data.moleFraction = d._mol_fraction;
580  data.elementsUsed = d._elements_used;
581  Thermochimica::setReinitData(data);
582  }
583  }
584 #endif
585 }
586 
587 template <bool is_nodal>
590 {
591  if constexpr (!is_nodal)
592  mooseError("Requesting nodal data from an element object.");
593  return this->getData(node_id);
594 }
595 
596 template <bool is_nodal>
599 {
600  if constexpr (is_nodal)
601  mooseError("Requesting per element data from a nodal object.");
602  return this->getData(element_id);
603 }
604 
605 template <bool is_nodal>
608 {
609  const auto it = _data.find(id);
610  if (it == _data.end())
611  {
612  if constexpr (is_nodal)
613  mooseError("Unable to look up data for node ", id);
614  else
615  mooseError("Unable to look up data for element ", id);
616  }
617  return it->second;
618 }
619 
620 template class ThermochimicaDataBase<true>;
621 template class ThermochimicaDataBase<false>;
OutputMassUnit
Mass unit for output species.
static InputParameters validParams()
const std::vector< std::pair< std::string, std::string > > & _species_phase_pairs
const Data & getElementData(dof_id_type id) const
const std::size_t _n_elements
const Data & getNodalData(dof_id_type id) const
void addParam(const std::string &name, const std::initializer_list< typename T::value_type > &value, const std::string &doc_string)
void addPrivateParam(const std::string &name, const T &value)
void mooseError(Args &&... args)
void reinitDataMooseFromTc()
Function to get re-initialization data from Thermochimica and save it in member variables of this Use...
typename std::conditional< is_nodal, NodalUserObject, ElementUserObject >::type ThermochimicaDataBaseParent
User object that performs a Gibbs energy minimization at each node by calling the Thermochimica code...
T & set(const std::string &name, bool quiet_mode=false)
ThermochimicaDataBase(const InputParameters &parameters)
std::vector< MooseWritableVariable * > _el_pot
Writable chemical potential variables for each element.
void reinitDataMooseToTc()
Function to load re-initialization data saved in this UserObject back into Thermochimica.
virtual void initialize() override
static const std::string temperature
Definition: NS.h:59
int _socket
communication socket
const std::vector< std::string > & _ph_names
std::vector< unsigned int > _el_ids
std::vector< MooseWritableVariable * > _vp
Writable vapour pressures for each element.
const std::vector< std::pair< std::string, std::string > > & _phase_element_pairs
const std::vector< std::string > & _element_potentials
void libmesh_ignore(const Args &...)
Real value(unsigned n, unsigned alpha, unsigned beta, Real x)
const std::size_t _n_phases
InputParameters validParams()
const std::size_t _n_vapor_species
registerMooseObject("ChemicalReactionsApp", ThermochimicaNodalData)
std::vector< const VariableValue * > _el
void ThermochimicaDataBase_handler(int)
const Data & getData(dof_id_type id) const
const std::size_t _n_potentials
const std::size_t _n_species
void checkLibraryAvailability(MooseObject &self)
Check if thermochimica is available and throw an error if it is not.
dof_id_type * _shared_dofid_mem
shared memory pointer for dof_id_type values
void addCoupledVar(const std::string &name, const std::string &doc_string)
void addRequiredCoupledVar(const std::string &name, const std::string &doc_string)
void notify(T send_msg)
Real * _shared_real_mem
shared memory pointer for Real values
virtual void execute() override
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
std::vector< MooseWritableVariable * > _sp
Writable species amount variables.
const std::size_t _n_phase_elements
static const std::string pressure
Definition: NS.h:56
IntRange< T > make_range(T beg, T end)
std::vector< MooseWritableVariable * > _el_ph
Writable variable for molar amounts of each element in specified phase.
void expect(T expect_msg)
The ChemicalCompositionAction sets up user objects, aux kernels, and aux variables for a thermochemis...
void addClassDescription(InputParameters &params, const std::string &desc)
Add the supplied class description if thermochimica is available, otherwise add a warning message...
const std::vector< std::pair< std::string, std::string > > & _vapor_phase_pairs
auto index_range(const T &sizable)
const Elem & get(const ElemType type_in)
std::vector< MooseWritableVariable * > _ph
Writable phase amount variables.
unsigned int idx(const ElemType type, const unsigned int nx, const unsigned int i, const unsigned int j)
uint8_t dof_id_type