GridFire 0.0.1a
General Purpose Nuclear Network
Loading...
Searching...
No Matches
solver.cpp
Go to the documentation of this file.
3#include "gridfire/network.h"
4
5#include "fourdst/composition/atomicSpecies.h"
6#include "fourdst/composition/composition.h"
7#include "fourdst/config/config.h"
8
9#include "Eigen/Dense"
10#include "unsupported/Eigen/NonLinearOptimization"
11
12#include <boost/numeric/odeint.hpp>
13
14#include <vector>
15#include <unordered_map>
16#include <string>
17#include <stdexcept>
18
19#include "quill/LogMacros.h"
20
21namespace gridfire::solver {
22
24 // --- Use the policy to decide whether to update the view ---
25 if (shouldUpdateView(netIn)) {
26 LOG_DEBUG(m_logger, "Solver update policy triggered, network view updating...");
27 m_engine.update(netIn);
28 LOG_DEBUG(m_logger, "Network view updated!");
29
32 }
33 m_engine.generateJacobianMatrix(netIn.MolarAbundance(), netIn.temperature / 1e9, netIn.density);
34 using state_type = boost::numeric::ublas::vector<double>;
35 using namespace boost::numeric::odeint;
36
37 NetOut postIgnition = initializeNetworkWithShortIgnition(netIn);
38
39 constexpr double abundance_floor = 1.0e-30;
40 std::vector<double>Y_sanitized_initial;
41 Y_sanitized_initial.reserve(m_engine.getNetworkSpecies().size());
42
43 LOG_DEBUG(m_logger, "Sanitizing initial abundances with a floor of {:0.3E}...", abundance_floor);
44 for (const auto& species : m_engine.getNetworkSpecies()) {
45 double molar_abundance = 0.0;
46 if (postIgnition.composition.contains(species)) {
47 molar_abundance = postIgnition.composition.getMolarAbundance(std::string(species.name()));
48 }
49
50 if (molar_abundance < abundance_floor) {
51 molar_abundance = abundance_floor;
52 }
53 Y_sanitized_initial.push_back(molar_abundance);
54 }
55 const double T9 = netIn.temperature / 1e9; // Convert temperature from Kelvin to T9 (T9 = T / 1e9)
56 const double rho = netIn.density; // Density in g/cm^3
57
58 const auto indices = packSpeciesTypeIndexVectors(Y_sanitized_initial, T9, rho);
59 Eigen::VectorXd Y_QSE;
60 try {
61 Y_QSE = calculateSteadyStateAbundances(Y_sanitized_initial, T9, rho, indices);
62 } catch (const std::runtime_error& e) {
63 LOG_ERROR(m_logger, "Failed to calculate steady state abundances. Aborting QSE evaluation.");
64 m_logger->flush_log();
65 throw std::runtime_error("Failed to calculate steady state abundances: " + std::string(e.what()));
66 }
67
68 state_type YDynamic_ublas(indices.dynamicSpeciesIndices.size() + 1);
69 for (size_t i = 0; i < indices.dynamicSpeciesIndices.size(); ++i) {
70 YDynamic_ublas(i) = Y_sanitized_initial[indices.dynamicSpeciesIndices[i]];
71 }
72 YDynamic_ublas(indices.dynamicSpeciesIndices.size()) = 0.0; // Placeholder for specific energy rate
73
74 const RHSFunctor rhs_functor(m_engine, indices.dynamicSpeciesIndices, indices.QSESpeciesIndices, Y_QSE, T9, rho);
75 const auto stepper = make_controlled<runge_kutta_dopri5<state_type>>(1.0e-8, 1.0e-8);
76
77 size_t stepCount = integrate_adaptive(
78 stepper,
79 rhs_functor,
80 YDynamic_ublas,
81 0.0, // Start time
82 netIn.tMax,
83 netIn.dt0
84 );
85
86 std::vector<double> YFinal = Y_sanitized_initial;
87 for (size_t i = 0; i <indices.dynamicSpeciesIndices.size(); ++i) {
88 YFinal[indices.dynamicSpeciesIndices[i]] = YDynamic_ublas(i);
89 }
90 for (size_t i = 0; i < indices.QSESpeciesIndices.size(); ++i) {
91 YFinal[indices.QSESpeciesIndices[i]] = Y_QSE(i);
92 }
93
94 const double finalSpecificEnergyRate = YDynamic_ublas(indices.dynamicSpeciesIndices.size());
95
96 // --- Marshal output variables ---
97 std::vector<std::string> speciesNames(m_engine.getNetworkSpecies().size());
98 std::vector<double> finalMassFractions(m_engine.getNetworkSpecies().size());
99 double massFractionSum = 0.0;
100
101 for (size_t i = 0; i < speciesNames.size(); ++i) {
102 const auto& species = m_engine.getNetworkSpecies()[i];
103 speciesNames[i] = species.name();
104 finalMassFractions[i] = YFinal[i] * species.mass(); // Convert from molar abundance to mass fraction
105 massFractionSum += finalMassFractions[i];
106 }
107 for (auto& mf : finalMassFractions) {
108 mf /= massFractionSum; // Normalize to get mass fractions
109 }
110
111 fourdst::composition::Composition outputComposition(speciesNames, finalMassFractions);
112 NetOut netOut;
113 netOut.composition = outputComposition;
114 netOut.energy = finalSpecificEnergyRate; // Specific energy rate
115 netOut.num_steps = stepCount;
116 return netOut;
117 }
118
120 const std::vector<double>& Y,
121 const double T9,
122 const double rho
123 ) const {
124 constexpr double timescaleCutoff = 1.0e-5;
125 constexpr double abundanceCutoff = 1.0e-15;
126
127 LOG_INFO(m_logger, "Partitioning species using T9={:0.2f} and ρ={:0.2e}", T9, rho);
128 LOG_INFO(m_logger, "Timescale Cutoff: {:.1e} s, Abundance Cutoff: {:.1e}", timescaleCutoff, abundanceCutoff);
129
130 std::vector<size_t>dynamicSpeciesIndices; // Slow species that are not in QSE
131 std::vector<size_t>QSESpeciesIndices; // Fast species that are in QSE
132
133 std::unordered_map<fourdst::atomic::Species, double> speciesTimescale = m_engine.getSpeciesTimescales(Y, T9, rho);
134
135 for (size_t i = 0; i < m_engine.getNetworkSpecies().size(); ++i) {
136 const auto& species = m_engine.getNetworkSpecies()[i];
137 const double network_timescale = speciesTimescale.at(species);
138 const double abundance = Y[i];
139
140 double decay_timescale = std::numeric_limits<double>::infinity();
141 const double half_life = species.halfLife();
142 if (half_life > 0 && !std::isinf(half_life)) {
143 constexpr double LN2 = 0.69314718056;
144 decay_timescale = half_life / LN2;
145 }
146
147 const double final_timescale = std::min(network_timescale, decay_timescale);
148
149 if (std::isinf(final_timescale) || abundance < abundanceCutoff || final_timescale <= timescaleCutoff) {
150 QSESpeciesIndices.push_back(i);
151 } else {
152 dynamicSpeciesIndices.push_back(i);
153 }
154 }
155 LOG_INFO(m_logger, "Partitioning complete. Dynamical species: {}, QSE species: {}.", dynamicSpeciesIndices.size(), QSESpeciesIndices.size());
156 LOG_INFO(m_logger, "Dynamic species: {}", [dynamicSpeciesIndices](const DynamicEngine& engine_wrapper) -> std::string {
157 std::string result;
158 int count = 0;
159 for (const auto& i : dynamicSpeciesIndices) {
160 result += std::string(engine_wrapper.getNetworkSpecies()[i].name());
161 if (count < dynamicSpeciesIndices.size() - 2) {
162 result += ", ";
163 } else if (count == dynamicSpeciesIndices.size() - 2) {
164 result += " and ";
165 }
166 count++;
167 }
168 return result;
169 }(m_engine));
170 LOG_INFO(m_logger, "QSE species: {}", [QSESpeciesIndices](const DynamicEngine& engine_wrapper) -> std::string {
171 std::string result;
172 int count = 0;
173 for (const auto& i : QSESpeciesIndices) {
174 result += std::string(engine_wrapper.getNetworkSpecies()[i].name());
175 if (count < QSESpeciesIndices.size() - 2) {
176 result += ", ";
177 } else if (count == QSESpeciesIndices.size() - 2) {
178 result += " and ";
179 }
180 count++;
181 }
182 return result;
183 }(m_engine));
184 return {dynamicSpeciesIndices, QSESpeciesIndices};
185 }
186
188 const std::vector<double> &Y,
189 const double T9,
190 const double rho,
191 const dynamicQSESpeciesIndices &indices
192 ) const {
193 LOG_TRACE_L1(m_logger, "Calculating steady state abundances for QSE species...");
194 LOG_WARNING(m_logger, "QSE solver logic not yet implemented, assuming all QSE species have 0 abundance.");
195
196 // --- Prepare the QSE species vector ---
197 Eigen::VectorXd v_qse(indices.QSESpeciesIndices.size());
198 v_qse.setZero();
199 return v_qse.array();
200 }
201
203 const auto ignitionTemperature = m_config.get<double>(
204 "gridfire:solver:QSE:ignition:temperature",
205 2e8
206 ); // 0.2 GK
207 const auto ignitionDensity = m_config.get<double>(
208 "gridfire:solver:QSE:ignition:density",
209 1e6
210 ); // 1e6 g/cm^3
211 const auto ignitionTime = m_config.get<double>(
212 "gridfire:solver:QSE:ignition:tMax",
213 1e-7
214 ); // 0.1 μs
215 const auto ignitionStepSize = m_config.get<double>(
216 "gridfire:solver:QSE:ignition:dt0",
217 1e-15
218 ); // 1e-15 seconds
219
220 LOG_INFO(
221 m_logger,
222 "Igniting network with T={:<5.3E}, ρ={:<5.3E}, tMax={:<5.3E}, dt0={:<5.3E}...",
223 ignitionTemperature,
224 ignitionDensity,
225 ignitionTime,
226 ignitionStepSize
227 );
228
229 NetIn preIgnition = netIn;
230 preIgnition.temperature = ignitionTemperature;
231 preIgnition.density = ignitionDensity;
232 preIgnition.tMax = ignitionTime;
233 preIgnition.dt0 = ignitionStepSize;
234
235 DirectNetworkSolver ignitionSolver(m_engine);
236 NetOut postIgnition = ignitionSolver.evaluate(preIgnition);
237 LOG_INFO(m_logger, "Network ignition completed in {} steps.", postIgnition.num_steps);
238 return postIgnition;
239 }
240
241 bool QSENetworkSolver::shouldUpdateView(const NetIn &conditions) const {
242 // Policy 1: If the view has never been initialized, we must update.
243 if (!m_isViewInitialized) {
244 return true;
245 }
246
247 // Policy 2: Check for significant relative change in temperature.
248 // Reaction rates are exponentially sensitive to temperature, so we use a tight threshold.
249 const double temp_threshold = m_config.get<double>("gridfire:solver:policy:temp_threshold", 0.05); // 5%
250 const double temp_relative_change = std::abs(conditions.temperature - m_lastSeenConditions.temperature) / m_lastSeenConditions.temperature;
251 if (temp_relative_change > temp_threshold) {
252 LOG_DEBUG(m_logger, "Temperature changed by {:.1f}%, triggering view update.", temp_relative_change * 100);
253 return true;
254 }
255
256 // Policy 3: Check for significant relative change in density.
257 const double rho_threshold = m_config.get<double>("gridfire:solver:policy:rho_threshold", 0.10); // 10%
258 const double rho_relative_change = std::abs(conditions.density - m_lastSeenConditions.density) / m_lastSeenConditions.density;
259 if (rho_relative_change > rho_threshold) {
260 LOG_DEBUG(m_logger, "Density changed by {:.1f}%, triggering view update.", rho_relative_change * 100);
261 return true;
262 }
263
264 // Policy 4: Check for fuel depletion.
265 // If a primary fuel source changes significantly, the network structure may change.
266 const double fuel_threshold = m_config.get<double>("gridfire:solver:policy:fuel_threshold", 0.15); // 15%
267
268 // Example: Check hydrogen abundance
269 const double h1_old = m_lastSeenConditions.composition.getMassFraction("H-1");
270 const double h1_new = conditions.composition.getMassFraction("H-1");
271 if (h1_old > 1e-12) { // Avoid division by zero
272 const double h1_relative_change = std::abs(h1_new - h1_old) / h1_old;
273 if (h1_relative_change > fuel_threshold) {
274 LOG_DEBUG(m_logger, "H-1 mass fraction changed by {:.1f}%, triggering view update.", h1_relative_change * 100);
275 return true;
276 }
277 }
278
279 // If none of the above conditions are met, the current view is still good enough.
280 return false;
281 }
282
284 const boost::numeric::ublas::vector<double> &YDynamic,
285 boost::numeric::ublas::vector<double> &dYdtDynamic,
286 double t
287 ) const {
288 // --- Populate the slow / dynamic species vector ---
289 std::vector<double> YFull(m_engine.getNetworkSpecies().size());
290 for (size_t i = 0; i < m_dynamicSpeciesIndices.size(); ++i) {
291 YFull[m_dynamicSpeciesIndices[i]] = YDynamic(i);
292 }
293
294 // --- Populate the QSE species vector ---
295 for (size_t i = 0; i < m_QSESpeciesIndices.size(); ++i) {
296 YFull[m_QSESpeciesIndices[i]] = m_Y_QSE(i);
297 }
298
299 auto [full_dYdt, specificEnergyRate] = m_engine.calculateRHSAndEnergy(YFull, m_T9, m_rho);
300
301 dYdtDynamic.resize(m_dynamicSpeciesIndices.size() + 1);
302 for (size_t i = 0; i < m_dynamicSpeciesIndices.size(); ++i) {
303 dYdtDynamic(i) = full_dYdt[m_dynamicSpeciesIndices[i]];
304 }
305 dYdtDynamic[m_dynamicSpeciesIndices.size()] = specificEnergyRate;
306 }
307
309 namespace ublas = boost::numeric::ublas;
310 namespace odeint = boost::numeric::odeint;
311 using fourdst::composition::Composition;
312
313
314 const double T9 = netIn.temperature / 1e9; // Convert temperature from Kelvin to T9 (T9 = T / 1e9)
315 const unsigned long numSpecies = m_engine.getNetworkSpecies().size();
316
317 const auto absTol = m_config.get<double>("gridfire:solver:DirectNetworkSolver:absTol", 1.0e-8);
318 const auto relTol = m_config.get<double>("gridfire:solver:DirectNetworkSolver:relTol", 1.0e-8);
319
320 size_t stepCount = 0;
321
322 RHSFunctor rhsFunctor(m_engine, T9, netIn.density);
323 JacobianFunctor jacobianFunctor(m_engine, T9, netIn.density);
324
325 ublas::vector<double> Y(numSpecies + 1);
326
327 for (size_t i = 0; i < numSpecies; ++i) {
328 const auto& species = m_engine.getNetworkSpecies()[i];
329 try {
330 Y(i) = netIn.composition.getMolarAbundance(std::string(species.name()));
331 } catch (const std::runtime_error) {
332 LOG_DEBUG(m_logger, "Species '{}' not found in composition. Setting abundance to 0.0.", species.name());
333 Y(i) = 0.0;
334 }
335 }
336 Y(numSpecies) = 0.0;
337
338 const auto stepper = odeint::make_controlled<odeint::rosenbrock4<double>>(absTol, relTol);
339 stepCount = odeint::integrate_adaptive(
340 stepper,
341 std::make_pair(rhsFunctor, jacobianFunctor),
342 Y,
343 0.0,
344 netIn.tMax,
345 netIn.dt0
346 );
347
348 std::vector<double> finalMassFractions(numSpecies);
349 for (size_t i = 0; i < numSpecies; ++i) {
350 const double molarMass = m_engine.getNetworkSpecies()[i].mass();
351 finalMassFractions[i] = Y(i) * molarMass; // Convert from molar abundance to mass fraction
352 if (finalMassFractions[i] < MIN_ABUNDANCE_THRESHOLD) {
353 finalMassFractions[i] = 0.0;
354 }
355 }
356
357 std::vector<std::string> speciesNames;
358 speciesNames.reserve(numSpecies);
359 for (const auto& species : m_engine.getNetworkSpecies()) {
360 speciesNames.push_back(std::string(species.name()));
361 }
362
363 Composition outputComposition(speciesNames, finalMassFractions);
364 outputComposition.finalize(true);
365
366 NetOut netOut;
367 netOut.composition = std::move(outputComposition);
368 netOut.energy = Y(numSpecies); // Specific energy rate
369 netOut.num_steps = stepCount;
370
371 return netOut;
372 }
373
375 const boost::numeric::ublas::vector<double> &Y,
376 boost::numeric::ublas::vector<double> &dYdt,
377 double t
378 ) const {
379 const std::vector<double> y(Y.begin(), m_numSpecies + Y.begin());
380 auto [dydt, eps] = m_engine.calculateRHSAndEnergy(y, m_T9, m_rho);
381 dYdt.resize(m_numSpecies + 1);
382 std::ranges::copy(dydt, dYdt.begin());
383 dYdt(m_numSpecies) = eps;
384 }
385
387 const boost::numeric::ublas::vector<double> &Y,
388 boost::numeric::ublas::matrix<double> &J,
389 double t,
390 boost::numeric::ublas::vector<double> &dfdt
391 ) const {
392 J.resize(m_numSpecies+1, m_numSpecies+1);
393 J.clear();
394 for (int i = 0; i < m_numSpecies; ++i) {
395 for (int j = 0; j < m_numSpecies; ++j) {
396 J(i, j) = m_engine.getJacobianMatrixEntry(i, j);
397 }
398 }
399 }
400}
Abstract class for engines supporting Jacobian and stoichiometry operations.
virtual const std::vector< fourdst::atomic::Species > & getNetworkSpecies() const =0
Get the list of species in the network.
A network solver that directly integrates the reaction network ODEs.
Definition solver.h:379
quill::Logger * m_logger
Logger instance.
Definition solver.h:483
fourdst::config::Config & m_config
Configuration instance.
Definition solver.h:484
NetOut evaluate(const NetIn &netIn) override
Evaluates the network for a given timestep using direct integration.
Definition solver.cpp:308
Eigen::VectorXd calculateSteadyStateAbundances(const std::vector< double > &Y, const double T9, const double rho, const dynamicQSESpeciesIndices &indices) const
Calculates the steady-state abundances of the QSE species.
Definition solver.cpp:187
bool shouldUpdateView(const NetIn &conditions) const
Determines whether the adaptive engine view should be updated.
Definition solver.cpp:241
NetIn m_lastSeenConditions
The last seen input conditions.
Definition solver.h:366
quill::Logger * m_logger
Logger instance.
Definition solver.h:362
NetOut evaluate(const NetIn &netIn) override
Evaluates the network for a given timestep using the QSE approach.
Definition solver.cpp:23
dynamicQSESpeciesIndices packSpeciesTypeIndexVectors(const std::vector< double > &Y, const double T9, const double rho) const
Packs the species indices into vectors based on their type (dynamic or QSE).
Definition solver.cpp:119
fourdst::config::Config & m_config
Configuration instance.
Definition solver.h:363
bool m_isViewInitialized
Flag indicating whether the adaptive engine view has been initialized.
Definition solver.h:365
NetOut initializeNetworkWithShortIgnition(const NetIn &netIn) const
Initializes the network with a short ignition phase.
Definition solver.cpp:202
static constexpr double MIN_ABUNDANCE_THRESHOLD
Minimum abundance threshold below which species are ignored.
double density
Density in g/cm^3.
Definition network.h:58
double tMax
Maximum time.
Definition network.h:55
fourdst::composition::Composition composition
Composition of the network.
Definition network.h:54
std::vector< double > MolarAbundance() const
Definition network.cpp:30
double dt0
Initial time step.
Definition network.h:56
double temperature
Temperature in Kelvin.
Definition network.h:57
fourdst::composition::Composition composition
Composition of the network after evaluation.
Definition network.h:66
double energy
Energy in ergs after evaluation.
Definition network.h:68
int num_steps
Number of steps taken in the evaluation.
Definition network.h:67
Functor for calculating the Jacobian matrix.
Definition solver.h:444
const size_t m_numSpecies
The number of species in the network.
Definition solver.h:448
DynamicEngine & m_engine
The engine used to evaluate the network.
Definition solver.h:445
void operator()(const boost::numeric::ublas::vector< double > &Y, boost::numeric::ublas::matrix< double > &J, double t, boost::numeric::ublas::vector< double > &dfdt) const
Calculates the Jacobian matrix.
Definition solver.cpp:386
Functor for calculating the right-hand side of the ODEs.
Definition solver.h:402
DynamicEngine & m_engine
The engine used to evaluate the network.
Definition solver.h:403
const double m_T9
Temperature in units of 10^9 K.
Definition solver.h:404
void operator()(const boost::numeric::ublas::vector< double > &Y, boost::numeric::ublas::vector< double > &dYdt, double t) const
Calculates the time derivatives of the species abundances.
Definition solver.cpp:374
const double m_rho
Density in g/cm^3.
Definition solver.h:405
const size_t m_numSpecies
The number of species in the network.
Definition solver.h:406
Functor for calculating the right-hand side of the ODEs for the dynamic species.
Definition solver.h:201
const Eigen::VectorXd & m_Y_QSE
Steady-state abundances of the QSE species.
Definition solver.h:205
DynamicEngine & m_engine
The engine used to evaluate the network.
Definition solver.h:202
const double m_T9
Temperature in units of 10^9 K.
Definition solver.h:206
const std::vector< size_t > & m_dynamicSpeciesIndices
Indices of the dynamic species.
Definition solver.h:203
const std::vector< size_t > & m_QSESpeciesIndices
Indices of the QSE species.
Definition solver.h:204
const double m_rho
Density in g/cm^3.
Definition solver.h:207
void operator()(const boost::numeric::ublas::vector< double > &YDynamic, boost::numeric::ublas::vector< double > &dYdtDynamic, double t) const
Calculates the time derivatives of the dynamic species.
Definition solver.cpp:283
Structure to hold indices of dynamic and QSE species.
Definition solver.h:27
std::vector< size_t > QSESpeciesIndices
Indices of fast species that are in QSE.
Definition solver.h:29