7#include "fourdst/composition/atomicSpecies.h"
8#include "fourdst/composition/composition.h"
9#include "fourdst/config/config.h"
12#include "unsupported/Eigen/NonLinearOptimization"
14#include <boost/numeric/odeint.hpp>
17#include <unordered_map>
22#include "quill/LogMacros.h"
29 LOG_DEBUG(
m_logger,
"Solver update policy triggered, network view updating...");
31 LOG_DEBUG(
m_logger,
"Network view updated!");
37 using state_type = boost::numeric::ublas::vector<double>;
38 using namespace boost::numeric::odeint;
42 constexpr double abundance_floor = 1.0e-30;
43 std::vector<double>Y_sanitized_initial;
44 Y_sanitized_initial.reserve(
m_engine.getNetworkSpecies().size());
46 LOG_DEBUG(
m_logger,
"Sanitizing initial abundances with a floor of {:0.3E}...", abundance_floor);
47 for (
const auto& species :
m_engine.getNetworkSpecies()) {
48 double molar_abundance = 0.0;
50 molar_abundance = postIgnition.
composition.getMolarAbundance(std::string(species.name()));
53 if (molar_abundance < abundance_floor) {
54 molar_abundance = abundance_floor;
56 Y_sanitized_initial.push_back(molar_abundance);
59 const double rho = netIn.
density;
62 Eigen::VectorXd Y_QSE;
67 ss << std::scientific << std::setprecision(5);
80 }
catch (
const std::runtime_error& e) {
81 LOG_ERROR(
m_logger,
"Failed to calculate steady state abundances. Aborting QSE evaluation.");
83 throw std::runtime_error(
"Failed to calculate steady state abundances: " + std::string(e.what()));
86 state_type YDynamic_ublas(indices.dynamicSpeciesIndices.size() + 1);
87 for (
size_t i = 0; i < indices.dynamicSpeciesIndices.size(); ++i) {
88 YDynamic_ublas(i) = Y_sanitized_initial[indices.dynamicSpeciesIndices[i]];
90 YDynamic_ublas(indices.dynamicSpeciesIndices.size()) = 0.0;
92 const RHSFunctor rhs_functor(
m_engine, indices.dynamicSpeciesIndices, indices.QSESpeciesIndices, Y_QSE, T9, rho);
93 const auto stepper = make_controlled<runge_kutta_dopri5<state_type>>(1.0e-8, 1.0e-8);
95 size_t stepCount = integrate_adaptive(
104 std::vector<double> YFinal = Y_sanitized_initial;
105 for (
size_t i = 0; i <indices.dynamicSpeciesIndices.size(); ++i) {
106 YFinal[indices.dynamicSpeciesIndices[i]] = YDynamic_ublas(i);
108 for (
size_t i = 0; i < indices.QSESpeciesIndices.size(); ++i) {
109 YFinal[indices.QSESpeciesIndices[i]] = Y_QSE(i);
112 const double finalSpecificEnergyRate = YDynamic_ublas(indices.dynamicSpeciesIndices.size());
115 std::vector<std::string> speciesNames(
m_engine.getNetworkSpecies().size());
116 std::vector<double> finalMassFractions(
m_engine.getNetworkSpecies().size());
117 double massFractionSum = 0.0;
119 for (
size_t i = 0; i < speciesNames.size(); ++i) {
120 const auto& species =
m_engine.getNetworkSpecies()[i];
121 speciesNames[i] = species.name();
122 finalMassFractions[i] = YFinal[i] * species.mass();
123 massFractionSum += finalMassFractions[i];
125 for (
auto& mf : finalMassFractions) {
126 mf /= massFractionSum;
129 fourdst::composition::Composition outputComposition(speciesNames, finalMassFractions);
132 netOut.
energy = finalSpecificEnergyRate;
138 const std::vector<double>& Y,
142 constexpr double timescaleCutoff = 1.0e-5;
143 constexpr double abundanceCutoff = 1.0e-15;
145 LOG_INFO(
m_logger,
"Partitioning species using T9={:0.2f} and ρ={:0.2e}", T9, rho);
146 LOG_INFO(
m_logger,
"Timescale Cutoff: {:.1e} s, Abundance Cutoff: {:.1e}", timescaleCutoff, abundanceCutoff);
148 std::vector<size_t>dynamicSpeciesIndices;
149 std::vector<size_t>QSESpeciesIndices;
151 std::unordered_map<fourdst::atomic::Species, double> speciesTimescale =
m_engine.getSpeciesTimescales(Y, T9, rho);
153 for (
size_t i = 0; i <
m_engine.getNetworkSpecies().size(); ++i) {
154 const auto& species =
m_engine.getNetworkSpecies()[i];
155 const double network_timescale = speciesTimescale.at(species);
156 const double abundance = Y[i];
158 double decay_timescale = std::numeric_limits<double>::infinity();
159 const double half_life = species.halfLife();
160 if (half_life > 0 && !std::isinf(half_life)) {
161 constexpr double LN2 = 0.69314718056;
162 decay_timescale = half_life / LN2;
165 const double final_timescale = std::min(network_timescale, decay_timescale);
167 if (std::isinf(final_timescale) || abundance < abundanceCutoff || final_timescale <= timescaleCutoff) {
168 QSESpeciesIndices.push_back(i);
170 dynamicSpeciesIndices.push_back(i);
173 LOG_INFO(
m_logger,
"Partitioning complete. Dynamical species: {}, QSE species: {}.", dynamicSpeciesIndices.size(), QSESpeciesIndices.size());
174 LOG_INFO(
m_logger,
"Dynamic species: {}", [dynamicSpeciesIndices](
const DynamicEngine& engine_wrapper) -> std::string {
177 for (
const auto& i : dynamicSpeciesIndices) {
179 if (count < dynamicSpeciesIndices.size() - 2) {
181 }
else if (count == dynamicSpeciesIndices.size() - 2) {
188 LOG_INFO(
m_logger,
"QSE species: {}", [QSESpeciesIndices](
const DynamicEngine& engine_wrapper) -> std::string {
191 for (
const auto& i : QSESpeciesIndices) {
193 if (count < QSESpeciesIndices.size() - 2) {
195 }
else if (count == QSESpeciesIndices.size() - 2) {
202 return {dynamicSpeciesIndices, QSESpeciesIndices};
206 const std::vector<double> &Y,
211 LOG_TRACE_L1(
m_logger,
"Calculating steady state abundances for QSE species...");
214 LOG_DEBUG(
m_logger,
"No QSE species to solve for.");
215 return Eigen::VectorXd(0);
229 v_qse_log_initial(i) = std::log(std::max(Y[indices.
QSESpeciesIndices[i]], 1e-99));
232 const static std::unordered_map<Eigen::LevenbergMarquardtSpace::Status, const char*> statusMessages = {
233 {Eigen::LevenbergMarquardtSpace::NotStarted,
"Not started"},
234 {Eigen::LevenbergMarquardtSpace::Running,
"Running"},
235 {Eigen::LevenbergMarquardtSpace::ImproperInputParameters,
"Improper input parameters"},
236 {Eigen::LevenbergMarquardtSpace::RelativeReductionTooSmall,
"Relative reduction too small"},
237 {Eigen::LevenbergMarquardtSpace::RelativeErrorTooSmall,
"Relative error too small"},
238 {Eigen::LevenbergMarquardtSpace::RelativeErrorAndReductionTooSmall,
"Relative error and reduction too small"},
239 {Eigen::LevenbergMarquardtSpace::CosinusTooSmall,
"Cosine too small"},
240 {Eigen::LevenbergMarquardtSpace::TooManyFunctionEvaluation,
"Too many function evaluations"},
241 {Eigen::LevenbergMarquardtSpace::FtolTooSmall,
"Function tolerance too small"},
242 {Eigen::LevenbergMarquardtSpace::XtolTooSmall,
"X tolerance too small"},
243 {Eigen::LevenbergMarquardtSpace::GtolTooSmall,
"Gradient tolerance too small"},
244 {Eigen::LevenbergMarquardtSpace::UserAsked,
"User asked to stop"}
247 Eigen::LevenbergMarquardt lm(functor);
248 const Eigen::LevenbergMarquardtSpace::Status info = lm.minimize(v_qse_log_initial);
250 if (info <= 0 || info >= 4) {
251 LOG_ERROR(
m_logger,
"QSE species minimization failed with status: {} ({})",
252 static_cast<int>(info), statusMessages.at(info));
253 throw std::runtime_error(
254 "QSE species minimization failed with status: " + std::to_string(
static_cast<int>(info)) +
255 " (" + std::string(statusMessages.at(info)) +
")"
258 LOG_DEBUG(
m_logger,
"QSE species minimization completed successfully with status: {} ({})",
259 static_cast<int>(info), statusMessages.at(info));
260 return v_qse_log_initial.array().exp();
265 const auto ignitionTemperature =
m_config.get<
double>(
266 "gridfire:solver:QSE:ignition:temperature",
269 const auto ignitionDensity =
m_config.get<
double>(
270 "gridfire:solver:QSE:ignition:density",
273 const auto ignitionTime =
m_config.get<
double>(
274 "gridfire:solver:QSE:ignition:tMax",
277 const auto ignitionStepSize =
m_config.get<
double>(
278 "gridfire:solver:QSE:ignition:dt0",
284 "Igniting network with T={:<5.3E}, ρ={:<5.3E}, tMax={:<5.3E}, dt0={:<5.3E}...",
291 NetIn preIgnition = netIn;
293 preIgnition.
density = ignitionDensity;
294 preIgnition.
tMax = ignitionTime;
295 preIgnition.
dt0 = ignitionStepSize;
297 const auto prevScreeningModel =
m_engine.getScreeningModel();
298 LOG_DEBUG(
m_logger,
"Setting screening model to BARE for high temperature and density ignition.");
302 LOG_INFO(
m_logger,
"Network ignition completed in {} steps.", postIgnition.
num_steps);
303 m_engine.setScreeningModel(prevScreeningModel);
304 LOG_DEBUG(
m_logger,
"Restoring previous screening model: {}",
static_cast<int>(prevScreeningModel));
316 const double temp_threshold =
m_config.get<
double>(
"gridfire:solver:policy:temp_threshold", 0.05);
318 if (temp_relative_change > temp_threshold) {
319 LOG_DEBUG(
m_logger,
"Temperature changed by {:.1f}%, triggering view update.", temp_relative_change * 100);
324 const double rho_threshold =
m_config.get<
double>(
"gridfire:solver:policy:rho_threshold", 0.10);
326 if (rho_relative_change > rho_threshold) {
327 LOG_DEBUG(
m_logger,
"Density changed by {:.1f}%, triggering view update.", rho_relative_change * 100);
333 const double fuel_threshold =
m_config.get<
double>(
"gridfire:solver:policy:fuel_threshold", 0.15);
337 const double h1_new = conditions.
composition.getMassFraction(
"H-1");
338 if (h1_old > 1e-12) {
339 const double h1_relative_change = std::abs(h1_new - h1_old) / h1_old;
340 if (h1_relative_change > fuel_threshold) {
341 LOG_DEBUG(
m_logger,
"H-1 mass fraction changed by {:.1f}%, triggering view update.", h1_relative_change * 100);
351 const boost::numeric::ublas::vector<double> &YDynamic,
352 boost::numeric::ublas::vector<double> &dYdtDynamic,
356 std::vector<double> YFull(
m_engine.getNetworkSpecies().size());
366 auto [full_dYdt, specificEnergyRate] =
m_engine.calculateRHSAndEnergy(YFull,
m_T9,
m_rho);
376 namespace ublas = boost::numeric::ublas;
377 namespace odeint = boost::numeric::odeint;
378 using fourdst::composition::Composition;
382 const unsigned long numSpecies =
m_engine.getNetworkSpecies().size();
384 const auto absTol =
m_config.get<
double>(
"gridfire:solver:DirectNetworkSolver:absTol", 1.0e-8);
385 const auto relTol =
m_config.get<
double>(
"gridfire:solver:DirectNetworkSolver:relTol", 1.0e-8);
387 size_t stepCount = 0;
392 ublas::vector<double> Y(numSpecies + 1);
394 for (
size_t i = 0; i < numSpecies; ++i) {
395 const auto& species =
m_engine.getNetworkSpecies()[i];
397 Y(i) = netIn.
composition.getMolarAbundance(std::string(species.name()));
398 }
catch (
const std::runtime_error) {
399 LOG_DEBUG(
m_logger,
"Species '{}' not found in composition. Setting abundance to 0.0.", species.name());
405 const auto stepper = odeint::make_controlled<odeint::rosenbrock4<double>>(absTol, relTol);
406 stepCount = odeint::integrate_adaptive(
408 std::make_pair(rhsFunctor, jacobianFunctor),
415 std::vector<double> finalMassFractions(numSpecies);
416 for (
size_t i = 0; i < numSpecies; ++i) {
417 const double molarMass =
m_engine.getNetworkSpecies()[i].mass();
418 finalMassFractions[i] = Y(i) * molarMass;
420 finalMassFractions[i] = 0.0;
424 std::vector<std::string> speciesNames;
425 speciesNames.reserve(numSpecies);
426 for (
const auto& species :
m_engine.getNetworkSpecies()) {
427 speciesNames.push_back(std::string(species.name()));
430 Composition outputComposition(speciesNames);
431 outputComposition.setMassFraction(speciesNames, finalMassFractions);
432 outputComposition.finalize(
true);
436 netOut.
energy = Y(numSpecies);
443 const boost::numeric::ublas::vector<double> &Y,
444 boost::numeric::ublas::vector<double> &dYdt,
447 const std::vector<double> y(Y.begin(),
m_numSpecies + Y.begin());
459 std::ranges::copy(dydt, dYdt.begin());
464 const boost::numeric::ublas::vector<double> &Y,
465 boost::numeric::ublas::matrix<double> &J,
467 boost::numeric::ublas::vector<double> &dfdt
473 J(i, j) =
m_engine.getJacobianMatrixEntry(i, j);
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.
quill::Logger * m_logger
Logger instance.
fourdst::config::Config & m_config
Configuration instance.
NetOut evaluate(const NetIn &netIn) override
Evaluates the network for a given timestep using direct integration.
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.
bool shouldUpdateView(const NetIn &conditions) const
Determines whether the adaptive engine view should be updated.
NetIn m_lastSeenConditions
The last seen input conditions.
quill::Logger * m_logger
Logger instance.
NetOut evaluate(const NetIn &netIn) override
Evaluates the network for a given timestep using the QSE approach.
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).
fourdst::config::Config & m_config
Configuration instance.
bool m_isViewInitialized
Flag indicating whether the adaptive engine view has been initialized.
NetOut initializeNetworkWithShortIgnition(const NetIn &netIn) const
Initializes the network with a short ignition phase.
@ BARE
No screening applied. The screening factor is always 1.0.
static constexpr double MIN_ABUNDANCE_THRESHOLD
Minimum abundance threshold below which species are ignored.
double density
Density in g/cm^3.
fourdst::composition::Composition composition
Composition of the network.
std::vector< double > MolarAbundance() const
double dt0
Initial time step.
double temperature
Temperature in Kelvin.
fourdst::composition::Composition composition
Composition of the network after evaluation.
double energy
Energy in ergs after evaluation.
int num_steps
Number of steps taken in the evaluation.
Functor for calculating the Jacobian matrix.
const size_t m_numSpecies
The number of species in the network.
DynamicEngine & m_engine
The engine used to evaluate the network.
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.
Functor for calculating the right-hand side of the ODEs.
DynamicEngine & m_engine
The engine used to evaluate the network.
const double m_T9
Temperature in units of 10^9 K.
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.
const double m_rho
Density in g/cm^3.
const size_t m_numSpecies
The number of species in the network.
Functor for calculating the residual and Jacobian for the QSE species using Eigen.
Functor for calculating the right-hand side of the ODEs for the dynamic species.
const Eigen::VectorXd & m_Y_QSE
Steady-state abundances of the QSE species.
DynamicEngine & m_engine
The engine used to evaluate the network.
const double m_T9
Temperature in units of 10^9 K.
const std::vector< size_t > & m_dynamicSpeciesIndices
Indices of the dynamic species.
const std::vector< size_t > & m_QSESpeciesIndices
Indices of the QSE species.
const double m_rho
Density in g/cm^3.
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.
Structure to hold indices of dynamic and QSE species.
std::vector< size_t > QSESpeciesIndices
Indices of fast species that are in QSE.
std::vector< size_t > dynamicSpeciesIndices
Indices of slow species that are not in QSE.