LCOV - code coverage report
Current view: top level - src/criticality - BoratedWater.C (source / functions) Hit Total Coverage
Test: neams-th-coe/cardinal: 93e9c4 Lines: 82 87 94.3 %
Date: 2026-07-16 12:06:10 Functions: 4 4 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : /********************************************************************/
       2             : /*                  SOFTWARE COPYRIGHT NOTIFICATION                 */
       3             : /*                             Cardinal                             */
       4             : /*                                                                  */
       5             : /*                  (c) 2021 UChicago Argonne, LLC                  */
       6             : /*                        ALL RIGHTS RESERVED                       */
       7             : /*                                                                  */
       8             : /*                 Prepared by UChicago Argonne, LLC                */
       9             : /*               Under Contract No. DE-AC02-06CH11357               */
      10             : /*                With the U. S. Department of Energy               */
      11             : /*                                                                  */
      12             : /*             Prepared by Battelle Energy Alliance, LLC            */
      13             : /*               Under Contract No. DE-AC07-05ID14517               */
      14             : /*                With the U. S. Department of Energy               */
      15             : /*                                                                  */
      16             : /*                 See LICENSE for full restrictions                */
      17             : /********************************************************************/
      18             : 
      19             : #ifdef ENABLE_OPENMC_COUPLING
      20             : 
      21             : #define NUCLEAR_DATA_IMPLEMENTATION
      22             : 
      23             : #include "BoratedWater.h"
      24             : #include "UserErrorChecking.h"
      25             : #include "NuclearData.h"
      26             : #include "openmc/capi.h"
      27             : #include "openmc/constants.h"
      28             : #include "openmc/nuclide.h"
      29             : 
      30             : registerMooseObject("CardinalApp", BoratedWater);
      31             : 
      32             : InputParameters
      33          26 : BoratedWater::validParams()
      34             : {
      35          26 :   auto params = OpenMCMaterialSearch::validParams();
      36          52 :   params.addParam<std::vector<std::string>>(
      37             :       "absent_nuclides",
      38             :       "Natural nuclides of hydrogen, oxygen, or boron which are missing from your cross section "
      39             :       "library; some cross section libraries do not have entries for O17 and O18. If your library "
      40             :       "does not have these nuclides you will get an error from this object trying to add them. For "
      41             :       "these missing nuclides, specify them here and their abundance will be applied to the main "
      42             :       "isotope of each element. Currently, only O18 is supported.");
      43          26 :   params.addClassDescription(
      44             :       "Searches for criticality using natural boron ppm in water in units of weight ppm");
      45          26 :   return params;
      46           0 : }
      47             : 
      48          18 : BoratedWater::BoratedWater(const InputParameters & parameters) : OpenMCMaterialSearch(parameters)
      49             : {
      50          18 :   if (_openmc_problem->runRandomRay())
      51           2 :     mooseError(
      52             :         "The BoratedWater criticality search is not supported when running the random ray solver!");
      53             : 
      54             :   // apply additional checks on the minimum and maximum
      55          16 :   if (_minimum < 0)
      56           4 :     paramError("minimum",
      57           2 :                "The 'minimum' boron ppm (" + std::to_string(_minimum) + ") must be positive!");
      58          14 :   if (_maximum > 50000)
      59           4 :     paramError("maximum",
      60             :                "The borated water composition is computed using a dilute species approximation. "
      61             :                "Results will not be accurate if the boron species is no longer dilute, which we "
      62           2 :                "take as 5\% weight concentration or higher. Please decrease 'maximum' (" +
      63           0 :                    std::to_string(_maximum) + ") to an upper limit which is in the dilute regime.");
      64             : 
      65             :   // We take the provided material, retaining its density, but then overwriting
      66             :   // any nuclides there to be H2O + boron weight ppm and add the S(a,b) tables
      67             :   // for hydrogen. Therefore, we perform a check here if any nuclides in the
      68             :   // material are *not* those we are going to add back when we wipe the material,
      69             :   // to notify the user they probably provided the wrong material ID or they need
      70             :   // to use a more general material modification object that allows full control
      71             :   // over additional nuclides (e.g., if their water includes corrosion products
      72             :   // that they do want to be there).
      73             :   // TODO: add the S(a,b) tables
      74             : 
      75             :   const int * nuclides;
      76             :   const double * densities;
      77             :   int n;
      78          12 :   int err = openmc_material_get_densities(_material_index, &nuclides, &densities, &n);
      79          12 :   catchOpenMCError(err, "get nuclide densities from material " + std::to_string(_material_id));
      80             : 
      81             :   // get all the natural isotopes of H, O, B
      82          12 :   _hydrogen_natural = NuclearData::Nuclide::getAbundances("H");
      83          12 :   _boron_natural = NuclearData::Nuclide::getAbundances("B");
      84          24 :   _oxygen_natural = NuclearData::Nuclide::getAbundances("O");
      85             : 
      86             :   std::vector<std::string> allowable;
      87          36 :   for (const auto & i : _hydrogen_natural)
      88          24 :     allowable.push_back(i.first);
      89          36 :   for (const auto & i : _boron_natural)
      90          24 :     allowable.push_back(i.first);
      91          48 :   for (const auto & i : _oxygen_natural)
      92          36 :     allowable.push_back(i.first);
      93             : 
      94          12 :   applyAbsentNuclides(allowable);
      95             : 
      96             :   // check if any nuclides already defined on the material do not intersect with
      97             :   // natural isotopes of H, O, B
      98          10 :   std::string full_names = "";
      99          66 :   for (int i = 0; i < n; ++i)
     100             :   {
     101          56 :     auto idx = nuclides[i];
     102          56 :     std::string name = openmc::data::nuclides[idx]->name_;
     103          56 :     if (std::find(allowable.begin(), allowable.end(), name) == allowable.end())
     104          16 :       full_names += name + " ";
     105             :   }
     106             : 
     107          10 :   if (!full_names.empty())
     108             :   {
     109           2 :     std::ostringstream msg;
     110             :     msg << "The criticality search will clear out all nuclides in material "
     111           2 :         << std::to_string(_material_id)
     112             :         << " and replace them with the naturally-abundant nuclides in hydrogen, oxygen, and boron. "
     113           4 :            "Any other nuclides which existed in the material will be deleted."
     114             :         << std::endl;
     115             :     msg << "\nThe material you provided contains nuclides which are not the natural isotopes of H, "
     116             :            "B, and O: "
     117           2 :         << full_names.substr(0, full_names.length() - 1) << ". These will be deleted from material "
     118           8 :         << std::to_string(_material_id) << " when the boron concentration is changed." << std::endl;
     119             :     msg << "\nFor general criticality searches based on material composition, please contact the "
     120           2 :            "Cardinal developer team.";
     121           2 :     mooseWarning(msg.str());
     122           0 :   }
     123             : 
     124             :   // Compute the molar mass of pure water
     125           8 :   _M_H2O = 0.0;
     126          24 :   for (const auto & h : _hydrogen_natural)
     127          16 :     _M_H2O += 2.0 * h.second * NuclearData::Nuclide::getAtomicMass(h.first);
     128          24 :   for (const auto & o : _oxygen_natural)
     129          16 :     _M_H2O += 1.0 * o.second * NuclearData::Nuclide::getAtomicMass(o.first);
     130             : 
     131             :   // Compute the molar mass of pure boron
     132           8 :   _M_B = 0.0;
     133          24 :   for (const auto & b : _boron_natural)
     134          16 :     _M_B += 1.0 * b.second * NuclearData::Nuclide::getAtomicMass(b.first);
     135           8 : }
     136             : 
     137             : void
     138          96 : BoratedWater::updateOpenMCModel(const Real & ppm)
     139             : {
     140          96 :   _console << " OpenMC will run with next guess for boron = " << ppm << " [ppm] ..." << std::endl;
     141             : 
     142             :   // A coupled thermal-fluid app may set the material density just before we enter
     143             :   // this routine; we will preserve that density which may be set and we interpret
     144             :   // it to be the SOLUTION density to be fully consistent with energy conservation.
     145             : 
     146             :   // Compute the number fractions of each element
     147          96 :   Real frac_H2O = (1 - ppm * 1e-6) / _M_H2O;
     148          96 :   Real frac_H = 2 * frac_H2O;
     149             :   Real frac_O = frac_H2O;
     150          96 :   Real frac_B = ppm * 1e-6 / _M_B;
     151             : 
     152             :   double rho;
     153          96 :   int err = openmc_material_get_density(_material_index, &rho);
     154         192 :   catchOpenMCError(err, "get density for material " + std::to_string(_material_id));
     155             : 
     156             :   std::vector<std::string> names;
     157             :   std::vector<double> densities;
     158         288 :   for (const auto & h : _hydrogen_natural)
     159             :   {
     160         192 :     names.push_back(h.first);
     161         192 :     densities.push_back(h.second * frac_H * rho * openmc::N_AVOGADRO);
     162             :   }
     163         288 :   for (const auto & o : _oxygen_natural)
     164             :   {
     165         192 :     names.push_back(o.first);
     166         192 :     densities.push_back(o.second * frac_O * rho * openmc::N_AVOGADRO);
     167             :   }
     168         288 :   for (const auto & b : _boron_natural)
     169             :   {
     170         192 :     names.push_back(b.first);
     171         192 :     densities.push_back(b.second * frac_B * rho * openmc::N_AVOGADRO);
     172             :   }
     173             : 
     174          96 :   openmc::model::materials[_material_index]->set_densities(names, densities /* atom/b-cm */);
     175          96 : }
     176             : 
     177             : void
     178          12 : BoratedWater::applyAbsentNuclides(const std::vector<std::string> & allowable)
     179             : {
     180          24 :   if (isParamValid("absent_nuclides"))
     181             :   {
     182          24 :     const auto & absent = getParam<std::vector<std::string>>("absent_nuclides");
     183             : 
     184          22 :     for (const auto & a : absent)
     185             :     {
     186             :       // only missing nuclides for H, O, B are meaningful
     187          12 :       if (std::find(allowable.begin(), allowable.end(), a) == allowable.end())
     188           2 :         paramWarning("absent_nuclides",
     189             :                      "Only absent isotopes of hydrogen, oxygen, or boron will be used to adjust "
     190           2 :                      "natural abundances. The entry '" +
     191           0 :                          a + "' will be unused.");
     192             : 
     193             :       // adjust the natural abundances if nuclides are missing from the cross section library
     194             :       // TODO: implement in a general fashion, this only works for O18 which is also the only
     195             :       // absent nuclide we expect in practice
     196          10 :       if (a == "O18")
     197             :       {
     198             :         // find which entry in _oxygen_natural has O16 and O18; this allows these isotopes
     199             :         // to appear in any order
     200             :         unsigned int idx16;
     201             :         unsigned int idx18;
     202             :         unsigned int idx = 0;
     203          40 :         for (const auto & o : _oxygen_natural)
     204             :         {
     205          30 :           if (o.first == "O16")
     206             :             idx16 = idx;
     207          30 :           if (o.first == "O18")
     208             :             idx18 = idx;
     209          30 :           idx++;
     210             :         }
     211             : 
     212          10 :         _oxygen_natural[idx16].second += _oxygen_natural[idx18].second;
     213          10 :         _oxygen_natural.erase(_oxygen_natural.begin() + idx18);
     214             :       }
     215             :       else
     216           0 :         paramError(
     217             :             "absent_nuclides",
     218             :             "Cardinal currently only assumes that O18 may be missing from your cross section "
     219             :             "library; please contact the Cardinal developer team to generalize this capability");
     220             :     }
     221             :   }
     222          10 : }
     223             : #endif

Generated by: LCOV version 1.14