|
| 1 | +//------------------------------------------------------------------------------ |
| 2 | +// © 2025. Triad National Security, LLC. All rights reserved. This |
| 3 | +// program was produced under U.S. Government contract 89233218CNA000001 |
| 4 | +// for Los Alamos National Laboratory (LANL), which is operated by Triad |
| 5 | +// National Security, LLC for the U.S. Department of Energy/National |
| 6 | +// Nuclear Security Administration. All rights in the program are |
| 7 | +// reserved by Triad National Security, LLC, and the U.S. Department of |
| 8 | +// Energy/National Nuclear Security Administration. The Government is |
| 9 | +// granted for itself and others acting on its behalf a nonexclusive, |
| 10 | +// paid-up, irrevocable worldwide license in this material to reproduce, |
| 11 | +// prepare derivative works, distribute copies to the public, perform |
| 12 | +// publicly and display publicly, and to permit others to do so. |
| 13 | +//------------------------------------------------------------------------------ |
| 14 | + |
| 15 | +// This example takes two materials and walks through valid sets of |
| 16 | +// volume fractions alpha1, alpha2, such that the volume-averaged |
| 17 | +// densities rhobar1 = alpha1 rho1 and rhobar2 = alpha2 rho2 are |
| 18 | +// conserved and the volume fractions sum to 1. Here rho1 and rho2 are |
| 19 | +// the microphysical densities of each material. At each volume |
| 20 | +// fraction pair, it computes the temperature so that the total energy |
| 21 | +// sums to a user specified value. When the pressures of each material |
| 22 | +// agree at a given pair of volume fractions, pressure temperature |
| 23 | +// equilibrium has been achived. This is useful for building an |
| 24 | +// intuition for how pressure temperature equilibrium works and also |
| 25 | +// seeing what it looks like for a given pairs of materials. The |
| 26 | +// script also outputs the residual as well as the P/T of each |
| 27 | +// material at each point. This produces a "map" of the PT landscape |
| 28 | +// for the material pair. |
| 29 | + |
| 30 | +// C headers |
| 31 | +#include <cmath> |
| 32 | +#include <cstdio> |
| 33 | + |
| 34 | +// C++ headers |
| 35 | +#include <iostream> |
| 36 | +#include <memory> |
| 37 | +#include <string> |
| 38 | + |
| 39 | +// This library contains portable utilities |
| 40 | +#include <ports-of-call/portability.hpp> |
| 41 | +#include <ports-of-call/portable_errors.hpp> |
| 42 | + |
| 43 | +// This contains useful tools for preventing things like divide by zero |
| 44 | +#include <singularity-eos/base/robust_utils.hpp> |
| 45 | +// 1D root finding |
| 46 | +#include <singularity-eos/base/root-finding-1d/root_finding.hpp> |
| 47 | +// Needed to import the eos models |
| 48 | +#include <singularity-eos/eos/eos.hpp> |
| 49 | + |
| 50 | +// This library contains the spiner table object, which we will use to |
| 51 | +// store our output |
| 52 | +#include <spiner/databox.hpp> |
| 53 | +#include <spiner/interpolation.hpp> |
| 54 | + |
| 55 | +// These are the specializations of spiner we will use |
| 56 | +using DataBox = Spiner::DataBox<Real>; |
| 57 | +using RegularGrid1D = Spiner::RegularGrid1D<Real>; |
| 58 | +using Spiner::DBDeleter; |
| 59 | + |
| 60 | +// Number of equations of state to use, always 2 |
| 61 | +constexpr std::size_t NEOS = 2; |
| 62 | + |
| 63 | +// Set the EOS you want to use here. |
| 64 | +// Set the EOSs you want to use here. |
| 65 | +using EOS = singularity::Variant<singularity::SpinerEOSDependsRhoT>; |
| 66 | + |
| 67 | +int main(int argc, char *argv[]) { |
| 68 | + if (argc < 6) { |
| 69 | + std::cerr << "Usage: " << argv[0] |
| 70 | + << " residual_savename rhobar1 rhobar2 sietot nvfrac eosargs..." |
| 71 | + << std::endl; |
| 72 | + std::exit(1); |
| 73 | + } |
| 74 | + |
| 75 | + // This is needed for Kokkos |
| 76 | +#ifdef PORTABILITY_STRATEGY_KOKKOS |
| 77 | + Kokkos::initialize(); |
| 78 | +#endif |
| 79 | + { |
| 80 | + // File we'll save the residual map to, in ascii. P/T trajectories will be printed to |
| 81 | + // to stdout |
| 82 | + const std::string savename = argv[1]; |
| 83 | + // alpha rho for each material where here alpha is volume |
| 84 | + // fraction and rho is microphysical material density. |
| 85 | + const Real rhobar1 = atof(argv[2]); |
| 86 | + const Real rhobar2 = atof(argv[3]); |
| 87 | + // total specific internal energy in the problem. |
| 88 | + const Real sietot = atof(argv[4]); |
| 89 | + // number of vfrac points |
| 90 | + const std::size_t nvfrac = atoi(argv[5]); |
| 91 | + |
| 92 | + // Below are the arguments for an individual equation of |
| 93 | + // state. Here I'm assuming SpinerEOSDependsRhoT but you could |
| 94 | + // modify for your needs. |
| 95 | + const std::string materialsfile = argv[6]; |
| 96 | + const int matids[] = {atoi(argv[7]), atoi(argv[8])}; |
| 97 | + |
| 98 | + // Now let's load up the EOS's. |
| 99 | + EOS eos1 = singularity::SpinerEOSDependsRhoT(materialsfile, matids[0]); |
| 100 | + EOS eos2 = singularity::SpinerEOSDependsRhoT(materialsfile, matids[1]); |
| 101 | + |
| 102 | + // The total bulk density |
| 103 | + const Real rhotot = rhobar1 + rhobar2; |
| 104 | + const Real utot = rhotot * sietot; |
| 105 | + |
| 106 | + // We will be walking through volume fractions so let's grid that |
| 107 | + // up using a Spiner RegularGrid1D. This is the grid of alpha1s. alpha2 = 1 - alpha1. |
| 108 | + constexpr Real lvfrac_min = -5; |
| 109 | + constexpr Real lvfrac_max = -0.5; |
| 110 | + RegularGrid1D lvfracs(lvfrac_min, lvfrac_max, nvfrac); |
| 111 | + |
| 112 | + // We will be doing root finds in T space so we need to know what |
| 113 | + // our bounds are. There is often a minimum temperature for an EOS |
| 114 | + // but not a maximum. |
| 115 | + const Real Tmin = std::max(eos1.MinimumTemperature(), eos2.MinimumTemperature()); |
| 116 | + const Real Tmax = 1e10; // change this if needed |
| 117 | + |
| 118 | + // We also want to store quantities of interest as we walk the volume fractions |
| 119 | + // This is a way of managing databoxes so that memory is automatically managed |
| 120 | + // Temperature |
| 121 | + std::unique_ptr<DataBox, DBDeleter> pTs( |
| 122 | + new DataBox(Spiner::AllocationTarget::Device, nvfrac)); |
| 123 | + DataBox Ts = *pTs; // A shallow coppy of pTs. Unmanaged memory. |
| 124 | + // Pressure for materials 1 and 2 |
| 125 | + std::unique_ptr<DataBox, DBDeleter> pPs( |
| 126 | + new DataBox(Spiner::AllocationTarget::Device, nvfrac, NEOS)); |
| 127 | + DataBox Ps = *pPs; |
| 128 | + // vfrac, energy, and pressure residuals for the trajectory |
| 129 | + std::unique_ptr<DataBox, DBDeleter> pTrajRes( |
| 130 | + new DataBox(Spiner::AllocationTarget::Device, nvfrac, 3)); |
| 131 | + DataBox trajRes = *pTrajRes; |
| 132 | + |
| 133 | + // We also need to ensure whatever internal tabulated data for |
| 134 | + // each EOS is available on device. |
| 135 | + eos1 = eos1.GetOnDevice(); |
| 136 | + eos2 = eos2.GetOnDevice(); |
| 137 | + |
| 138 | + // Ok let's walk the grid. We will do so on device with a portableFor loop |
| 139 | + portableFor( |
| 140 | + "Compute P and T for each alpha", 0, nvfrac, PORTABLE_LAMBDA(const int i) { |
| 141 | + // volume fractions |
| 142 | + const Real lvfrac = lvfracs.x(i); |
| 143 | + const Real vfrac = std::pow(10., lvfrac); |
| 144 | + const Real alpha1 = vfrac; |
| 145 | + const Real alpha2 = 1. - vfrac; |
| 146 | + |
| 147 | + // microphysical densities |
| 148 | + Real rho1 = singularity::robust::ratio(rhobar1, alpha1); |
| 149 | + Real rho2 = singularity::robust::ratio(rhobar2, alpha2); |
| 150 | + const Real Tguess = 1500; // just an initial guess |
| 151 | + |
| 152 | + // solve for the temperature such that sum of internal |
| 153 | + // energies is the total energy given microphysical densities rho |
| 154 | + auto fu = [=](const Real T) { |
| 155 | + return rhobar1 * eos1.InternalEnergyFromDensityTemperature(rho1, T) + |
| 156 | + rhobar2 * eos2.InternalEnergyFromDensityTemperature(rho2, T); |
| 157 | + }; |
| 158 | + // sets the T variable by reference |
| 159 | + auto root_status = RootFinding1D::regula_falsi(fu, utot, Tguess, Tmin, Tmax, |
| 160 | + 1e-16, 1e-16, Ts(i)); |
| 161 | + if (root_status != RootFinding1D::Status::SUCCESS) { |
| 162 | + printf("# Root finder failed! " |
| 163 | + "alpha1 alpha2, rho1, rho2, sie1(rho1,Tguess), sie2(rho2,Tguess) " |
| 164 | + "sietot = " |
| 165 | + "%.14e %.14e %.14e %.14e %.14e %.14e %.14e\n", |
| 166 | + alpha1, alpha2, rho1, rho2, |
| 167 | + eos1.InternalEnergyFromDensityTemperature(rho1, Tguess), |
| 168 | + eos2.InternalEnergyFromDensityTemperature(rho2, Tguess), sietot); |
| 169 | + Ts(i) = Ps(i, 0) = Ps(i, 1) = -1; |
| 170 | + trajRes(i, 0) = trajRes(i, 1) = trajRes(i, 2) = -1; |
| 171 | + } else { |
| 172 | + // compute the pressures |
| 173 | + Ps(i, 0) = eos1.PressureFromDensityTemperature(rho1, Ts(i)); |
| 174 | + Ps(i, 1) = eos2.PressureFromDensityTemperature(rho2, Ts(i)); |
| 175 | + |
| 176 | + trajRes(i, 0) = alpha1 + alpha2 - 1.0; |
| 177 | + |
| 178 | + const Real u1 = |
| 179 | + rhobar1 * eos1.InternalEnergyFromDensityTemperature(rho1, Ts(i)); |
| 180 | + const Real u2 = |
| 181 | + rhobar2 * eos2.InternalEnergyFromDensityTemperature(rho2, Ts(i)); |
| 182 | + trajRes(i, 1) = singularity::robust::ratio(u1 + u2 - utot, utot); |
| 183 | + trajRes(i, 2) = singularity::robust::ratio(Ps(i, 1) - Ps(i, 0), utot); |
| 184 | + } |
| 185 | + }); |
| 186 | + // wait for completion |
| 187 | +#ifdef PORTABILITY_STRATEGY_KOKKOS |
| 188 | + Kokkos::fence(); |
| 189 | +#endif |
| 190 | + |
| 191 | + // Copy the Ts array to host |
| 192 | + std::unique_ptr<DataBox, DBDeleter> pTs_h( |
| 193 | + new DataBox(Spiner::AllocationTarget::Host, nvfrac)); |
| 194 | + DataBox Ts_h = *pTs_h; |
| 195 | + portableCopyToHost(Ts_h.data(), Ts.data(), Ts.sizeBytes()); |
| 196 | + |
| 197 | + // Get max temperature achieved. Note this could have been done in |
| 198 | + // a single step via a portableReduce, but we split it out for clarity. |
| 199 | + Real Tmax_walked = Ts_h.max(); |
| 200 | + |
| 201 | + // Let's make a T grid |
| 202 | + const Real nT = nvfrac + 1; |
| 203 | + |
| 204 | + RegularGrid1D gTs(Tmin, Tmax_walked, nT); |
| 205 | + |
| 206 | + // Now we can do one more loop where we compute the energy and |
| 207 | + // pressure residuals as a function of T and alpha |
| 208 | + std::unique_ptr<DataBox, DBDeleter> p_residuals( |
| 209 | + new DataBox(Spiner::AllocationTarget::Device, nT, nvfrac, NEOS)); |
| 210 | + DataBox residuals = *p_residuals; |
| 211 | + |
| 212 | + portableFor( |
| 213 | + "Compute residual", 0, nT, 0, nvfrac, PORTABLE_LAMBDA(const int j, const int i) { |
| 214 | + // Temperature |
| 215 | + const Real T = gTs.x(j); |
| 216 | + |
| 217 | + // volume fractions |
| 218 | + const Real lvfrac = lvfracs.x(i); |
| 219 | + const Real vfrac = std::pow(10., lvfrac); |
| 220 | + const Real alpha1 = vfrac; |
| 221 | + const Real alpha2 = 1. - vfrac; |
| 222 | + |
| 223 | + // microphysical densities |
| 224 | + Real rho1 = singularity::robust::ratio(rhobar1, alpha1); |
| 225 | + Real rho2 = singularity::robust::ratio(rhobar2, alpha2); |
| 226 | + |
| 227 | + const Real u1 = rhobar1 * eos1.InternalEnergyFromDensityTemperature(rho1, T); |
| 228 | + const Real u2 = rhobar2 * eos2.InternalEnergyFromDensityTemperature(rho2, T); |
| 229 | + residuals(j, i, 0) = u1 + u2 - utot; |
| 230 | + |
| 231 | + const Real P1 = eos1.PressureFromDensityTemperature(rho1, T); |
| 232 | + const Real P2 = eos2.PressureFromDensityTemperature(rho2, T); |
| 233 | + residuals(j, i, 1) = P1 - P2; |
| 234 | + }); |
| 235 | +#ifdef PORTABILITY_STRATEGY_KOKKOS |
| 236 | + Kokkos::fence(); |
| 237 | +#endif |
| 238 | + |
| 239 | + // Now we need to copy all of this data to host so we can output it |
| 240 | + std::unique_ptr<DataBox, DBDeleter> pPs_h( |
| 241 | + new DataBox(Spiner::AllocationTarget::Host, nvfrac, NEOS)); |
| 242 | + auto Ps_h = *pPs_h; |
| 243 | + portableCopyToHost(Ps_h.data(), Ps.data(), Ps.sizeBytes()); |
| 244 | + |
| 245 | + std::unique_ptr<DataBox, DBDeleter> pTrajRes_h( |
| 246 | + new DataBox(Spiner::AllocationTarget::Host, nvfrac, 3)); |
| 247 | + auto trajRes_h = *pTrajRes_h; |
| 248 | + portableCopyToHost(trajRes_h.data(), trajRes.data(), trajRes.sizeBytes()); |
| 249 | + |
| 250 | + std::unique_ptr<DataBox, DBDeleter> p_residuals_h( |
| 251 | + new DataBox(Spiner::AllocationTarget::Host, nT, nvfrac, NEOS)); |
| 252 | + auto residuals_h = *p_residuals_h; |
| 253 | + portableCopyToHost(residuals_h.data(), residuals.data(), residuals.sizeBytes()); |
| 254 | +#ifdef PORTABILITY_STRATEGY_KOKKOS |
| 255 | + Kokkos::fence(); |
| 256 | +#endif |
| 257 | + |
| 258 | + FILE *file = fopen(savename.c_str(), "w"); |
| 259 | + PORTABLE_REQUIRE(file != nullptr, "Unable to open file"); |
| 260 | + // Now lets save the residuals |
| 261 | + fprintf(file, "# j i T alpha_1 res_sie res_P\n"); |
| 262 | + for (std::size_t j = 0; j < nT; ++j) { |
| 263 | + for (std::size_t i = 0; i < nvfrac; ++i) { |
| 264 | + Real lvfrac = lvfracs.x(i); |
| 265 | + Real vfrac = std::pow(10., lvfrac); |
| 266 | + fprintf(file, "%ld %ld %.14e %.14e %.14e %.14e\n", j, i, gTs.x(j), vfrac, |
| 267 | + residuals_h(j, i, 0), residuals_h(j, i, 1)); |
| 268 | + } |
| 269 | + } |
| 270 | + fclose(file); |
| 271 | + |
| 272 | + printf("# sie and P residuals normalized by total energy\n"); |
| 273 | + printf("# i alpha_1 T P_1 P_2 res_alpha res_sie res_P\n"); |
| 274 | + for (std::size_t i = 0; i < nvfrac; ++i) { |
| 275 | + Real lvfrac = lvfracs.x(i); |
| 276 | + Real vfrac = std::pow(10., lvfrac); |
| 277 | + printf("%ld %.14e %.14e %.14e %.14e %.14e %.14e %.14e\n", i, vfrac, Ts_h(i), |
| 278 | + Ps_h(i, 0), Ps_h(i, 1), trajRes(i, 0), trajRes(i, 1), trajRes(i, 2)); |
| 279 | + } |
| 280 | + |
| 281 | + eos1.Finalize(); |
| 282 | + eos2.Finalize(); |
| 283 | + } |
| 284 | +#ifdef PORTABILITY_STRATEGY_KOKKOS |
| 285 | + Kokkos::finalize(); |
| 286 | +#endif |
| 287 | + return 0; |
| 288 | +} |
0 commit comments