feat(GridFire): major design changes

Switching to an Engine + solver design. Also brought xxHash and Eigen in. Working on QSE and Culling.
This commit is contained in:
2025-06-26 15:13:46 -04:00
parent dd03873bc9
commit cd191cff23
32 changed files with 2737 additions and 1441 deletions

View File

@@ -28,7 +28,7 @@
#include "fourdst/config/config.h"
#include "quill/LogMacros.h"
#include "gridfire/approx8.h"
#include "gridfire/engine/engine_approx8.h"
#include "gridfire/network.h"
/* Nuclear reaction network in cgs units based on Frank Timmes' "approx8".
@@ -131,14 +131,14 @@ namespace gridfire::approx8{
return rate_fit(T9,a);
}
// he3(he3,2p)he4
// he3(he3,2p)he4 ** (missing both coefficients but have a reaction)
double he3he4_rate(const vec7 &T9){
constexpr vec7 a1 = {1.560990e+01,0.000000e+00,-1.282710e+01,-3.082250e-02,-6.546850e-01,8.963310e-02,-6.666670e-01};
constexpr vec7 a2 = {1.770750e+01,0.000000e+00,-1.282710e+01,-3.812600e+00,9.422850e-02,-3.010180e-03,1.333330e+00};
return rate_fit(T9,a1) + rate_fit(T9,a2);
}
// he4 + he4 + he4 -> c12
// he4 + he4 + he4 -> c12 ** (missing middle coefficient but have other two)
double triple_alpha_rate(const vec7 &T9){
constexpr vec7 a1 = {-9.710520e-01,0.000000e+00,-3.706000e+01,2.934930e+01,-1.155070e+02,-1.000000e+01,-1.333330e+00};
constexpr vec7 a2 = {-1.178840e+01,-1.024460e+00,-2.357000e+01,2.048860e+01,-1.298820e+01,-2.000000e+01,-2.166670e+00};
@@ -153,7 +153,7 @@ namespace gridfire::approx8{
return rate_fit(T9,a1) + rate_fit(T9,a2);
}
// c12 + he4 -> o16
// c12 + he4 -> o16 ** (missing first coefficient but have the second)
double c12a_rate(const vec7 &T9){
constexpr vec7 a1={6.965260e+01,-1.392540e+00,5.891280e+01,-1.482730e+02,9.083240e+00,-5.410410e-01,7.035540e+01};
constexpr vec7 a2={2.546340e+02,-1.840970e+00,1.034110e+02,-4.205670e+02,6.408740e+01,-1.246240e+01,1.373030e+02};
@@ -508,27 +508,19 @@ namespace gridfire::approx8{
vector_type Approx8Network::convert_netIn(const NetIn &netIn) {
vector_type y(Approx8Net::nVar, 0.0);
y[Approx8Net::ih1] = netIn.composition.getMassFraction("H-1");
y[Approx8Net::ihe3] = netIn.composition.getMassFraction("He-3");
y[Approx8Net::ihe4] = netIn.composition.getMassFraction("He-4");
y[Approx8Net::ic12] = netIn.composition.getMassFraction("C-12");
y[Approx8Net::in14] = netIn.composition.getMassFraction("N-14");
y[Approx8Net::io16] = netIn.composition.getMassFraction("O-16");
y[Approx8Net::ine20] = netIn.composition.getMassFraction("Ne-20");
y[Approx8Net::img24] = netIn.composition.getMassFraction("Mg-24");
y[Approx8Net::ih1] = netIn.composition.getNumberFraction("H-1");
std::cout << "Approx8::convert_netIn -> H-1 fraction: " << y[Approx8Net::ih1] << std::endl;
y[Approx8Net::ihe3] = netIn.composition.getNumberFraction("He-3");
y[Approx8Net::ihe4] = netIn.composition.getNumberFraction("He-4");
y[Approx8Net::ic12] = netIn.composition.getNumberFraction("C-12");
y[Approx8Net::in14] = netIn.composition.getNumberFraction("N-14");
y[Approx8Net::io16] = netIn.composition.getNumberFraction("O-16");
y[Approx8Net::ine20] = netIn.composition.getNumberFraction("Ne-20");
y[Approx8Net::img24] = netIn.composition.getNumberFraction("Mg-24");
y[Approx8Net::iTemp] = netIn.temperature;
y[Approx8Net::iDensity] = netIn.density;
y[Approx8Net::iEnergy] = netIn.energy;
double ySum = 0.0;
for (int i = 0; i < Approx8Net::nIso; i++) {
y[i] /= Approx8Net::aIon[i];
ySum += y[i];
}
for (int i = 0; i < Approx8Net::nIso; i++) {
y[i] /= ySum;
}
return y;
}
};

View File

@@ -1,9 +1,9 @@
#include "gridfire/netgraph.h"
#include "fourdst/composition/atomicSpecies.h"
#include "fourdst/constants/const.h"
#include "gridfire/engine/engine_graph.h"
#include "gridfire/reaction/reaction.h"
#include "gridfire/network.h"
#include "gridfire/reaclib.h"
#include "fourdst/composition/species.h"
#include "fourdst/composition/atomicSpecies.h"
#include "quill/LogMacros.h"
@@ -14,6 +14,7 @@
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
#include <fstream>
@@ -22,31 +23,28 @@
namespace gridfire {
GraphNetwork::GraphNetwork(
GraphEngine::GraphEngine(
const fourdst::composition::Composition &composition
):
Network(REACLIB),
m_reactions(build_reaclib_nuclear_network(composition)) {
m_reactions(build_reaclib_nuclear_network(composition, false)) {
syncInternalMaps();
}
GraphNetwork::GraphNetwork(
const fourdst::composition::Composition &composition,
const double cullingThreshold,
const double T9
):
Network(REACLIB),
m_reactions(build_reaclib_nuclear_network(composition, cullingThreshold, T9)) {
syncInternalMaps();
}
GraphNetwork::GraphNetwork(const reaclib::REACLIBReactionSet &reactions) :
Network(REACLIB),
m_reactions(reactions) {
GraphEngine::GraphEngine(reaction::REACLIBLogicalReactionSet reactions) :
m_reactions(std::move(reactions)) {
syncInternalMaps();
}
void GraphNetwork::syncInternalMaps() {
StepDerivatives<double> GraphEngine::calculateRHSAndEnergy(
const std::vector<double> &Y,
const double T9,
const double rho
) const {
return calculateAllDerivatives<double>(Y, T9, rho);
}
void GraphEngine::syncInternalMaps() {
collectNetworkSpecies();
populateReactionIDMap();
populateSpeciesToIndexMap();
@@ -56,17 +54,17 @@ namespace gridfire {
}
// --- Network Graph Construction Methods ---
void GraphNetwork::collectNetworkSpecies() {
void GraphEngine::collectNetworkSpecies() {
m_networkSpecies.clear();
m_networkSpeciesMap.clear();
std::set<std::string_view> uniqueSpeciesNames;
for (const auto& reaction: m_reactions) {
for (const auto& reactant: reaction.reactants()) {
for (const auto& reactant: reaction->reactants()) {
uniqueSpeciesNames.insert(reactant.name());
}
for (const auto& product: reaction.products()) {
for (const auto& product: reaction->products()) {
uniqueSpeciesNames.insert(product.name());
}
}
@@ -84,84 +82,55 @@ namespace gridfire {
}
void GraphNetwork::populateReactionIDMap() {
LOG_INFO(m_logger, "Populating reaction ID map for REACLIB graph network (serif::network::GraphNetwork)...");
void GraphEngine::populateReactionIDMap() {
LOG_TRACE_L1(m_logger, "Populating reaction ID map for REACLIB graph network (serif::network::GraphNetwork)...");
m_reactionIDMap.clear();
for (const auto& reaction: m_reactions) {
m_reactionIDMap.insert({reaction.id(), reaction});
m_reactionIDMap.emplace(reaction->id(), reaction.get());
}
LOG_INFO(m_logger, "Populated {} reactions in the reaction ID map.", m_reactionIDMap.size());
LOG_TRACE_L1(m_logger, "Populated {} reactions in the reaction ID map.", m_reactionIDMap.size());
}
void GraphNetwork::populateSpeciesToIndexMap() {
void GraphEngine::populateSpeciesToIndexMap() {
m_speciesToIndexMap.clear();
for (size_t i = 0; i < m_networkSpecies.size(); ++i) {
m_speciesToIndexMap.insert({m_networkSpecies[i], i});
}
}
void GraphNetwork::reserveJacobianMatrix() {
void GraphEngine::reserveJacobianMatrix() {
// The implementation of this function (and others) constrains this nuclear network to a constant temperature and density during
// each evaluation.
size_t numSpecies = m_networkSpecies.size();
m_jacobianMatrix.clear();
m_jacobianMatrix.resize(numSpecies, numSpecies, false); // Sparse matrix, no initial values
LOG_INFO(m_logger, "Jacobian matrix resized to {} rows and {} columns.",
LOG_TRACE_L2(m_logger, "Jacobian matrix resized to {} rows and {} columns.",
m_jacobianMatrix.size1(), m_jacobianMatrix.size2());
}
// --- Basic Accessors and Queries ---
const std::vector<fourdst::atomic::Species>& GraphNetwork::getNetworkSpecies() const {
const std::vector<fourdst::atomic::Species>& GraphEngine::getNetworkSpecies() const {
// Returns a constant reference to the vector of unique species in the network.
LOG_DEBUG(m_logger, "Providing access to network species vector. Size: {}.", m_networkSpecies.size());
return m_networkSpecies;
}
const reaclib::REACLIBReactionSet& GraphNetwork::getNetworkReactions() const {
const reaction::REACLIBLogicalReactionSet& GraphEngine::getNetworkReactions() const {
// Returns a constant reference to the set of reactions in the network.
LOG_DEBUG(m_logger, "Providing access to network reactions set. Size: {}.", m_reactions.size());
return m_reactions;
}
bool GraphNetwork::involvesSpecies(const fourdst::atomic::Species& species) const {
bool GraphEngine::involvesSpecies(const fourdst::atomic::Species& species) const {
// Checks if a given species is present in the network's species map for efficient lookup.
const bool found = m_networkSpeciesMap.contains(species.name());
LOG_DEBUG(m_logger, "Checking if species '{}' is involved in the network: {}.", species.name(), found ? "Yes" : "No");
return found;
}
std::unordered_map<fourdst::atomic::Species, int> GraphNetwork::getNetReactionStoichiometry(const reaclib::REACLIBReaction& reaction) const {
// Calculates the net stoichiometric coefficients for species in a given reaction.
std::unordered_map<fourdst::atomic::Species, int> stoichiometry;
// Iterate through reactants, decrementing their counts
for (const auto& reactant : reaction.reactants()) {
auto it = m_networkSpeciesMap.find(reactant.name());
if (it != m_networkSpeciesMap.end()) {
stoichiometry[it->second]--; // Copy Species by value (PERF: Future performance improvements by using pointers or references (std::reference_wrapper<const ...>) or something like that)
} else {
LOG_WARNING(m_logger, "Reactant species '{}' in reaction '{}' not found in network species map during stoichiometry calculation.",
reactant.name(), reaction.id());
}
}
// Iterate through products, incrementing their counts
for (const auto& product : reaction.products()) {
auto it = m_networkSpeciesMap.find(product.name());
if (it != m_networkSpeciesMap.end()) {
stoichiometry[it->second]++; // Copy Species by value (PERF: Future performance improvements by using pointers or references (std::reference_wrapper<const ...>) or something like that)
} else {
LOG_WARNING(m_logger, "Product species '{}' in reaction '{}' not found in network species map during stoichiometry calculation.",
product.name(), reaction.id());
}
}
LOG_DEBUG(m_logger, "Calculated net stoichiometry for reaction '{}'. Total unique species in stoichiometry: {}.", reaction.id(), stoichiometry.size());
return stoichiometry;
}
// --- Validation Methods ---
bool GraphNetwork::validateConservation() const {
LOG_INFO(m_logger, "Validating mass (A) and charge (Z) conservation across all reactions in the network.");
bool GraphEngine::validateConservation() const {
LOG_TRACE_L1(m_logger, "Validating mass (A) and charge (Z) conservation across all reactions in the network.");
for (const auto& reaction : m_reactions) {
uint64_t totalReactantA = 0;
@@ -170,7 +139,7 @@ namespace gridfire {
uint64_t totalProductZ = 0;
// Calculate total A and Z for reactants
for (const auto& reactant : reaction.reactants()) {
for (const auto& reactant : reaction->reactants()) {
auto it = m_networkSpeciesMap.find(reactant.name());
if (it != m_networkSpeciesMap.end()) {
totalReactantA += it->second.a();
@@ -179,13 +148,13 @@ namespace gridfire {
// This scenario indicates a severe data integrity issue:
// a reactant is part of a reaction but not in the network's species map.
LOG_ERROR(m_logger, "CRITICAL ERROR: Reactant species '{}' in reaction '{}' not found in network species map during conservation validation.",
reactant.name(), reaction.id());
reactant.name(), reaction->id());
return false;
}
}
// Calculate total A and Z for products
for (const auto& product : reaction.products()) {
for (const auto& product : reaction->products()) {
auto it = m_networkSpeciesMap.find(product.name());
if (it != m_networkSpeciesMap.end()) {
totalProductA += it->second.a();
@@ -193,7 +162,7 @@ namespace gridfire {
} else {
// Similar critical error for product species
LOG_ERROR(m_logger, "CRITICAL ERROR: Product species '{}' in reaction '{}' not found in network species map during conservation validation.",
product.name(), reaction.id());
product.name(), reaction->id());
return false;
}
}
@@ -201,25 +170,24 @@ namespace gridfire {
// Compare totals for conservation
if (totalReactantA != totalProductA) {
LOG_ERROR(m_logger, "Mass number (A) not conserved for reaction '{}': Reactants A={} vs Products A={}.",
reaction.id(), totalReactantA, totalProductA);
reaction->id(), totalReactantA, totalProductA);
return false;
}
if (totalReactantZ != totalProductZ) {
LOG_ERROR(m_logger, "Atomic number (Z) not conserved for reaction '{}': Reactants Z={} vs Products Z={}.",
reaction.id(), totalReactantZ, totalProductZ);
reaction->id(), totalReactantZ, totalProductZ);
return false;
}
}
LOG_INFO(m_logger, "Mass (A) and charge (Z) conservation validated successfully for all reactions.");
LOG_TRACE_L1(m_logger, "Mass (A) and charge (Z) conservation validated successfully for all reactions.");
return true; // All reactions passed the conservation check
}
void GraphNetwork::validateComposition(const fourdst::composition::Composition &composition, double culling, double T9) {
void GraphEngine::validateComposition(const fourdst::composition::Composition &composition, double culling, double T9) {
// Check if the requested network has already been cached.
// PERF: Rebuilding this should be pretty fast but it may be a good point of optimization in the future.
const reaclib::REACLIBReactionSet validationReactionSet = build_reaclib_nuclear_network(composition, culling, T9);
const reaction::REACLIBLogicalReactionSet validationReactionSet = build_reaclib_nuclear_network(composition, false);
// TODO: need some more robust method here to
// A. Build the basic network from the composition's species with non zero mass fractions.
// B. rebuild a new composition from the reaction set's reactants + products (with the mass fractions from the things that are only products set to 0)
@@ -230,22 +198,22 @@ namespace gridfire {
// This allows for dynamic network modification while retaining caching for networks which are very similar.
if (validationReactionSet != m_reactions) {
LOG_INFO(m_logger, "Reaction set not cached. Rebuilding the reaction set for T9={} and culling={}.", T9, culling);
LOG_DEBUG(m_logger, "Reaction set not cached. Rebuilding the reaction set for T9={} and culling={}.", T9, culling);
m_reactions = validationReactionSet;
syncInternalMaps(); // Re-sync internal maps after updating reactions. Note this will also retrace the AD tape.
}
}
// --- Generate Stoichiometry Matrix ---
void GraphNetwork::generateStoichiometryMatrix() {
LOG_INFO(m_logger, "Generating stoichiometry matrix...");
void GraphEngine::generateStoichiometryMatrix() {
LOG_TRACE_L1(m_logger, "Generating stoichiometry matrix...");
// Task 1: Set dimensions and initialize the matrix
size_t numSpecies = m_networkSpecies.size();
size_t numReactions = m_reactions.size();
m_stoichiometryMatrix.resize(numSpecies, numReactions, false);
LOG_INFO(m_logger, "Stoichiometry matrix initialized with dimensions: {} rows (species) x {} columns (reactions).",
LOG_TRACE_L1(m_logger, "Stoichiometry matrix initialized with dimensions: {} rows (species) x {} columns (reactions).",
numSpecies, numReactions);
// Task 2: Populate the stoichiometry matrix
@@ -253,13 +221,10 @@ namespace gridfire {
size_t reactionColumnIndex = 0;
for (const auto& reaction : m_reactions) {
// Get the net stoichiometry for the current reaction
std::unordered_map<fourdst::atomic::Species, int> netStoichiometry = getNetReactionStoichiometry(reaction);
std::unordered_map<fourdst::atomic::Species, int> netStoichiometry = reaction->stoichiometry();
// Iterate through the species and their coefficients in the stoichiometry map
for (const auto& pair : netStoichiometry) {
const fourdst::atomic::Species& species = pair.first; // The Species object
const int coefficient = pair.second; // The stoichiometric coefficient
for (const auto& [species, coefficient] : netStoichiometry) {
// Find the row index for this species
auto it = m_speciesToIndexMap.find(species);
if (it != m_speciesToIndexMap.end()) {
@@ -269,21 +234,49 @@ namespace gridfire {
} else {
// This scenario should ideally not happen if m_networkSpeciesMap and m_speciesToIndexMap are correctly synced
LOG_ERROR(m_logger, "CRITICAL ERROR: Species '{}' from reaction '{}' stoichiometry not found in species to index map.",
species.name(), reaction.id());
species.name(), reaction->id());
throw std::runtime_error("Species not found in species to index map: " + std::string(species.name()));
}
}
reactionColumnIndex++; // Move to the next column for the next reaction
}
LOG_INFO(m_logger, "Stoichiometry matrix population complete. Number of non-zero elements: {}.",
LOG_TRACE_L1(m_logger, "Stoichiometry matrix population complete. Number of non-zero elements: {}.",
m_stoichiometryMatrix.nnz()); // Assuming nnz() exists for compressed_matrix
}
void GraphNetwork::generateJacobianMatrix(const std::vector<double> &Y, const double T9,
const double rho) {
StepDerivatives<double> GraphEngine::calculateAllDerivatives(
const std::vector<double> &Y_in,
const double T9,
const double rho
) const {
return calculateAllDerivatives<double>(Y_in, T9, rho);
}
LOG_INFO(m_logger, "Generating jacobian matrix for T9={}, rho={}..", T9, rho);
StepDerivatives<ADDouble> GraphEngine::calculateAllDerivatives(
const std::vector<ADDouble> &Y_in,
const ADDouble T9,
const ADDouble rho
) const {
return calculateAllDerivatives<ADDouble>(Y_in, T9, rho);
}
double GraphEngine::calculateMolarReactionFlow(
const reaction::Reaction &reaction,
const std::vector<double> &Y,
const double T9,
const double rho
) const {
return calculateMolarReactionFlow<double>(reaction, Y, T9, rho);
}
void GraphEngine::generateJacobianMatrix(
const std::vector<double> &Y,
const double T9,
const double rho
) {
LOG_TRACE_L1(m_logger, "Generating jacobian matrix for T9={}, rho={}..", T9, rho);
const size_t numSpecies = m_networkSpecies.size();
// 1. Pack the input variables into a vector for CppAD
@@ -307,51 +300,28 @@ namespace gridfire {
}
}
}
LOG_INFO(m_logger, "Jacobian matrix generated with dimensions: {} rows x {} columns.", m_jacobianMatrix.size1(), m_jacobianMatrix.size2());
LOG_DEBUG(m_logger, "Jacobian matrix generated with dimensions: {} rows x {} columns.", m_jacobianMatrix.size1(), m_jacobianMatrix.size2());
}
void GraphNetwork::detectStiff(const NetIn &netIn, const double T9, const unsigned long numSpecies, const boost::numeric::ublas::vector<double>& Y) {
// --- Heuristic for automatic stiffness detection ---
const std::vector<double> initial_y_stl(Y.begin(), Y.begin() + numSpecies);
const auto [dydt, specificEnergyRate] = calculateAllDerivatives<double>(initial_y_stl, T9, netIn.density);
const std::vector<double>& initial_dotY = dydt;
double min_destruction_timescale = std::numeric_limits<double>::max();
for (size_t i = 0; i < numSpecies; ++i) {
if (Y(i) > MIN_ABUNDANCE_THRESHOLD && initial_dotY[i] < 0.0) {
const double timescale = std::abs(Y(i) / initial_dotY[i]);
if (timescale < min_destruction_timescale) {
min_destruction_timescale = timescale;
}
}
}
// If no species are being destroyed, the system is not stiff.
if (min_destruction_timescale == std::numeric_limits<double>::max()) {
LOG_INFO(m_logger, "No species are undergoing net destruction. Network is considered non-stiff.");
m_stiff = false;
return;
}
constexpr double saftey_factor = 10;
const bool is_stiff = (netIn.dt0 > saftey_factor * min_destruction_timescale);
LOG_INFO(m_logger, "Fastest destruction timescale: {}. Initial dt0: {}. Stiffness detected: {}.",
min_destruction_timescale, netIn.dt0, is_stiff ? "Yes" : "No");
if (is_stiff) {
m_stiff = true;
LOG_INFO(m_logger, "Network is detected as stiff.");
} else {
m_stiff = false;
LOG_INFO(m_logger, "Network is detected as non-stiff.");
}
double GraphEngine::getJacobianMatrixEntry(const int i, const int j) const {
return m_jacobianMatrix(i, j);
}
void GraphNetwork::exportToDot(const std::string &filename) const {
LOG_INFO(m_logger, "Exporting network graph to DOT file: {}", filename);
std::unordered_map<fourdst::atomic::Species, int> GraphEngine::getNetReactionStoichiometry(
const reaction::Reaction &reaction
) const {
return reaction.stoichiometry();
}
int GraphEngine::getStoichiometryMatrixEntry(
const int speciesIndex,
const int reactionIndex
) const {
return m_stoichiometryMatrix(speciesIndex, reactionIndex);
}
void GraphEngine::exportToDot(const std::string &filename) const {
LOG_TRACE_L1(m_logger, "Exporting network graph to DOT file: {}", filename);
std::ofstream dotFile(filename);
if (!dotFile.is_open()) {
@@ -375,118 +345,104 @@ namespace gridfire {
dotFile << " // --- Reaction Edges ---\n";
for (const auto& reaction : m_reactions) {
// Create a unique ID for the reaction node
std::string reactionNodeId = "reaction_" + std::string(reaction.id());
std::string reactionNodeId = "reaction_" + std::string(reaction->id());
// Define the reaction node (small, black dot)
dotFile << " \"" << reactionNodeId << "\" [shape=point, fillcolor=black, width=0.1, height=0.1, label=\"\"];\n";
// Draw edges from reactants to the reaction node
for (const auto& reactant : reaction.reactants()) {
for (const auto& reactant : reaction->reactants()) {
dotFile << " \"" << reactant.name() << "\" -> \"" << reactionNodeId << "\";\n";
}
// Draw edges from the reaction node to products
for (const auto& product : reaction.products()) {
dotFile << " \"" << reactionNodeId << "\" -> \"" << product.name() << "\" [label=\"" << reaction.qValue() << " MeV\"];\n";
for (const auto& product : reaction->products()) {
dotFile << " \"" << reactionNodeId << "\" -> \"" << product.name() << "\" [label=\"" << reaction->qValue() << " MeV\"];\n";
}
dotFile << "\n";
}
dotFile << "}\n";
dotFile.close();
LOG_INFO(m_logger, "Successfully exported network to {}", filename);
LOG_TRACE_L1(m_logger, "Successfully exported network to {}", filename);
}
NetOut GraphNetwork::evaluate(const NetIn &netIn) {
namespace ublas = boost::numeric::ublas;
namespace odeint = boost::numeric::odeint;
void GraphEngine::exportToCSV(const std::string &filename) const {
LOG_TRACE_L1(m_logger, "Exporting network graph to CSV file: {}", filename);
const double T9 = netIn.temperature / 1e9; // Convert temperature from Kelvin to T9 (T9 = T / 1e9)
// validateComposition(netIn.composition, netIn.culling, T9);
const unsigned long numSpecies = m_networkSpecies.size();
constexpr double abs_tol = 1.0e-8;
constexpr double rel_tol = 1.0e-8;
size_t stepCount = 0;
// TODO: Pull these out into configuration options
ODETerm rhs_functor(*this, T9, netIn.density);
ublas::vector<double> Y(numSpecies + 1);
for (size_t i = 0; i < numSpecies; ++i) {
const auto& species = m_networkSpecies[i];
// Get the mass fraction for this specific species from the input object
try {
Y(i) = netIn.composition.getMassFraction(std::string(species.name()));
} catch (const std::runtime_error &e) {
LOG_INFO(m_logger, "Species {} not in base composition, adding...", species.name());
Y(i) = 0.0; // If the species is not in the composition, set its mass fraction to
std::ofstream csvFile(filename, std::ios::out | std::ios::trunc);
if (!csvFile.is_open()) {
LOG_ERROR(m_logger, "Failed to open file for writing: {}", filename);
throw std::runtime_error("Failed to open file for writing: " + filename);
}
csvFile << "Reaction;Reactants;Products;Q-value;sources;rates\n";
for (const auto& reaction : m_reactions) {
// Dynamic cast to REACLIBReaction to access specific properties
csvFile << reaction->id() << ";";
// Reactants
int count = 0;
for (const auto& reactant : reaction->reactants()) {
csvFile << reactant.name();
if (++count < reaction->reactants().size()) {
csvFile << ",";
}
}
csvFile << ";";
count = 0;
for (const auto& product : reaction->products()) {
csvFile << product.name();
if (++count < reaction->products().size()) {
csvFile << ",";
}
}
csvFile << ";" << reaction->qValue() << ";";
// Reaction coefficients
auto* reaclibReaction = dynamic_cast<const reaction::REACLIBLogicalReaction*>(reaction.get());
if (!reaclibReaction) {
LOG_ERROR(m_logger, "Failed to cast Reaction to REACLIBLogicalReaction in GraphNetwork::exportToCSV().");
throw std::runtime_error("Failed to cast Reaction to REACLIBLogicalReaction in GraphNetwork::exportToCSV(). This should not happen, please check your reaction setup.");
}
auto sources = reaclibReaction->sources();
count = 0;
for (const auto& source : sources) {
csvFile << source;
if (++count < sources.size()) {
csvFile << ",";
}
}
csvFile << ";";
// Reaction coefficients
count = 0;
for (const auto& rates : *reaclibReaction) {
csvFile << rates;
if (++count < reaclibReaction->size()) {
csvFile << ",";
}
}
csvFile << "\n";
}
Y(numSpecies) = 0; // initial specific energy rate, will be updated later
detectStiff(netIn, T9, numSpecies, Y);
m_stiff = false;
if (m_stiff) {
JacobianTerm jacobian_functor(*this, T9, netIn.density);
LOG_INFO(m_logger, "Making use of stiff ODE solver for network evaluation.");
auto stepper = odeint::make_controlled<odeint::rosenbrock4<double>>(abs_tol, rel_tol);
stepCount = odeint::integrate_adaptive(
stepper,
std::make_pair(rhs_functor, jacobian_functor),
Y,
0.0, // Start time
netIn.tMax,
netIn.dt0
);
} else {
LOG_INFO(m_logger, "Making use of ODE solver (non-stiff) for network evaluation.");
using state_type = ublas::vector<double>;
auto stepper = odeint::make_controlled<odeint::runge_kutta_dopri5<state_type>>(abs_tol, rel_tol);
stepCount = odeint::integrate_adaptive(
stepper,
rhs_functor,
Y,
0.0, // Start time
netIn.tMax,
netIn.dt0
);
}
double sumY = 0.0;
for (int i = 0; i < numSpecies; ++i) { sumY += Y(i); }
for (int i = 0; i < numSpecies; ++i) { Y(i) /= sumY; }
// --- Marshall output variables ---
// PERF: Im sure this step could be tuned to avoid so many copies, that is a job for another day
std::vector<std::string> speciesNames;
speciesNames.reserve(numSpecies);
for (const auto& species : m_networkSpecies) {
speciesNames.push_back(std::string(species.name()));
}
std::vector<double> finalAbundances(Y.begin(), Y.begin() + numSpecies);
fourdst::composition::Composition outputComposition(speciesNames, finalAbundances);
outputComposition.finalize(true);
NetOut netOut;
netOut.composition = outputComposition;
netOut.num_steps = stepCount;
netOut.energy = Y(numSpecies); // The last element in Y is the specific energy rate
return netOut;
csvFile.close();
LOG_TRACE_L1(m_logger, "Successfully exported network graph to {}", filename);
}
void GraphNetwork::recordADTape() {
LOG_INFO(m_logger, "Recording AD tape for the RHS calculation...");
std::unordered_map<fourdst::atomic::Species, double> GraphEngine::getSpeciesTimescales(const std::vector<double> &Y, const double T9,
const double rho) const {
auto [dydt, _] = calculateAllDerivatives<double>(Y, T9, rho);
std::unordered_map<fourdst::atomic::Species, double> speciesTimescales;
speciesTimescales.reserve(m_networkSpecies.size());
for (size_t i = 0; i < m_networkSpecies.size(); ++i) {
double timescale = std::numeric_limits<double>::infinity();
const auto species = m_networkSpecies[i];
if (std::abs(dydt[i]) > 0.0) {
timescale = std::abs(Y[i] / dydt[i]);
}
speciesTimescales.emplace(species, timescale);
}
return speciesTimescales;
}
void GraphEngine::recordADTape() {
LOG_TRACE_L1(m_logger, "Recording AD tape for the RHS calculation...");
// Task 1: Set dimensions and initialize the matrix
const size_t numSpecies = m_networkSpecies.size();
@@ -521,11 +477,11 @@ namespace gridfire {
// 5. Call the actual templated function
// We let T9 and rho be constant, so we pass them as fixed values.
auto derivatives = calculateAllDerivatives<CppAD::AD<double>>(adY, adT9, adRho);
auto [dydt, nuclearEnergyGenerationRate] = calculateAllDerivatives<CppAD::AD<double>>(adY, adT9, adRho);
m_rhsADFun.Dependent(adInput, derivatives.dydt);
m_rhsADFun.Dependent(adInput, dydt);
LOG_INFO(m_logger, "AD tape recorded successfully for the RHS calculation. Number of independent variables: {}.",
LOG_TRACE_L1(m_logger, "AD tape recorded successfully for the RHS calculation. Number of independent variables: {}.",
adInput.size());
}
}

View File

@@ -19,13 +19,13 @@
//
// *********************************************************************** */
#include "gridfire/network.h"
#include "gridfire/reactions.h"
#include "../include/gridfire/reaction/reaction.h"
#include <ranges>
#include <fstream>
#include "gridfire/approx8.h"
#include "quill/LogMacros.h"
#include "gridfire/reaclib.h"
#include "gridfire/reactions.h"
namespace gridfire {
Network::Network(const NetworkFormat format) :
@@ -50,231 +50,20 @@ namespace gridfire {
return oldFormat;
}
NetOut Network::evaluate(const NetIn &netIn) {
NetOut netOut;
switch (m_format) {
case APPROX8: {
approx8::Approx8Network network;
netOut = network.evaluate(netIn);
break;
}
case UNKNOWN: {
LOG_ERROR(m_logger, "Network format {} is not implemented.", FormatStringLookup.at(m_format));
throw std::runtime_error("Network format not implemented.");
}
default: {
LOG_ERROR(m_logger, "Unknown network format.");
throw std::runtime_error("Unknown network format.");
}
}
return netOut;
}
LogicalReaction::LogicalReaction(const std::vector<reaclib::REACLIBReaction> &reactions) {
if (reactions.empty()) {
LOG_ERROR(m_logger, "Empty reaction set provided to LogicalReaction constructor.");
throw std::runtime_error("Empty reaction set provided to LogicalReaction constructor.");
}
const auto& first_reaction = reactions.front();
m_peID = first_reaction.peName();
m_reactants = first_reaction.reactants();
m_products = first_reaction.products();
m_qValue = first_reaction.qValue();
m_reverse = first_reaction.is_reverse();
m_chapter = first_reaction.chapter();
m_rates.reserve(reactions.size());
for (const auto& reaction : reactions) {
m_rates.push_back(reaction.rateFits());
if (std::abs(reaction.qValue() - m_qValue) > 1e-6) {
LOG_ERROR(m_logger, "Inconsistent Q-values in reactions {}. All reactions must have the same Q-value.", m_peID);
throw std::runtime_error("Inconsistent Q-values in reactions. All reactions must have the same Q-value.");
}
}
}
LogicalReaction::LogicalReaction(const reaclib::REACLIBReaction &first_reaction) {
m_peID = first_reaction.peName();
m_reactants = first_reaction.reactants();
m_products = first_reaction.products();
m_qValue = first_reaction.qValue();
m_reverse = first_reaction.is_reverse();
m_chapter = first_reaction.chapter();
m_rates.reserve(1);
m_rates.push_back(first_reaction.rateFits());
}
void LogicalReaction::add_reaction(const reaclib::REACLIBReaction &reaction) {
if (std::abs(reaction.qValue() - m_qValue > 1e-6)) {
LOG_ERROR(m_logger, "Inconsistent Q-values in reactions {}. All reactions must have the same Q-value.", m_peID);
throw std::runtime_error("Inconsistent Q-values in reactions. All reactions must have the same Q-value.");
}
m_rates.push_back(reaction.rateFits());
}
auto LogicalReaction::begin() {
return m_rates.begin();
}
auto LogicalReaction::begin() const {
return m_rates.cbegin();
}
auto LogicalReaction::end() {
return m_rates.end();
}
auto LogicalReaction::end() const {
return m_rates.cend();
}
LogicalReactionSet::LogicalReactionSet(const std::vector<LogicalReaction> &reactions) {
if (reactions.empty()) {
LOG_ERROR(m_logger, "Empty reaction set provided to LogicalReactionSet constructor.");
throw std::runtime_error("Empty reaction set provided to LogicalReactionSet constructor.");
}
for (const auto& reaction : reactions) {
m_reactions.push_back(reaction);
}
m_reactionNameMap.reserve(m_reactions.size());
for (const auto& reaction : m_reactions) {
if (m_reactionNameMap.contains(reaction.id())) {
LOG_ERROR(m_logger, "Duplicate reaction ID '{}' found in LogicalReactionSet.", reaction.id());
throw std::runtime_error("Duplicate reaction ID found in LogicalReactionSet: " + std::string(reaction.id()));
}
m_reactionNameMap[reaction.id()] = reaction;
}
}
LogicalReactionSet::LogicalReactionSet(const std::vector<reaclib::REACLIBReaction> &reactions) {
std::vector<std::string_view> uniquePeNames;
for (const auto& reaction: reactions) {
if (uniquePeNames.empty()) {
uniquePeNames.emplace_back(reaction.peName());
} else if (std::ranges::find(uniquePeNames, reaction.peName()) == uniquePeNames.end()) {
uniquePeNames.emplace_back(reaction.peName());
}
}
for (const auto& peName : uniquePeNames) {
std::vector<reaclib::REACLIBReaction> reactionsForPe;
for (const auto& reaction : reactions) {
if (reaction.peName() == peName) {
reactionsForPe.push_back(reaction);
}
}
m_reactions.emplace_back(reactionsForPe);
}
m_reactionNameMap.reserve(m_reactions.size());
for (const auto& reaction : m_reactions) {
if (m_reactionNameMap.contains(reaction.id())) {
LOG_ERROR(m_logger, "Duplicate reaction ID '{}' found in LogicalReactionSet.", reaction.id());
throw std::runtime_error("Duplicate reaction ID found in LogicalReactionSet: " + std::string(reaction.id()));
}
m_reactionNameMap[reaction.id()] = reaction;
}
}
LogicalReactionSet::LogicalReactionSet(const reaclib::REACLIBReactionSet &reactionSet) {
LogicalReactionSet(reactionSet.get_reactions());
}
void LogicalReactionSet::add_reaction(const LogicalReaction& reaction) {
if (m_reactionNameMap.contains(reaction.id())) {
LOG_WARNING(m_logger, "Reaction {} already exists in the set. Not adding again.", reaction.id());
std::cerr << "Warning: Reaction " << reaction.id() << " already exists in the set. Not adding again." << std::endl;
return;
}
m_reactions.push_back(reaction);
m_reactionNameMap[reaction.id()] = reaction;
}
void LogicalReactionSet::add_reaction(const reaclib::REACLIBReaction& reaction) {
if (m_reactionNameMap.contains(reaction.id())) {
m_reactionNameMap[reaction.id()].add_reaction(reaction);
} else {
const LogicalReaction logicalReaction(reaction);
m_reactions.push_back(logicalReaction);
m_reactionNameMap[reaction.id()] = logicalReaction;
}
}
bool LogicalReactionSet::contains(const std::string_view &id) const {
for (const auto& reaction : m_reactions) {
if (reaction.id() == id) {
return true;
}
}
return false;
}
bool LogicalReactionSet::contains(const LogicalReaction &reactions) const {
for (const auto& reaction : m_reactions) {
if (reaction.id() == reactions.id()) {
return true;
}
}
return false;
}
bool LogicalReactionSet::contains_species(const fourdst::atomic::Species &species) const {
return contains_reactant(species) || contains_product(species);
}
bool LogicalReactionSet::contains_reactant(const fourdst::atomic::Species &species) const {
for (const auto& reaction : m_reactions) {
if (std::ranges::find(reaction.reactants(), species) != reaction.reactants().end()) {
return true;
}
}
return false;
}
bool LogicalReactionSet::contains_product(const fourdst::atomic::Species &species) const {
for (const auto& reaction : m_reactions) {
if (std::ranges::find(reaction.products(), species) != reaction.products().end()) {
return true;
}
}
return false;
}
const LogicalReaction & LogicalReactionSet::operator[](size_t index) const {
return m_reactions.at(index);
}
const LogicalReaction & LogicalReactionSet::operator[](const std::string_view &id) const {
return m_reactionNameMap.at(id);
}
auto LogicalReactionSet::begin() {
return m_reactions.begin();
}
auto LogicalReactionSet::begin() const {
return m_reactions.cbegin();
}
auto LogicalReactionSet::end() {
return m_reactions.end();
}
auto LogicalReactionSet::end() const {
return m_reactions.cend();
}
LogicalReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition) {
using namespace reaclib;
REACLIBReactionSet reactions;
reaction::REACLIBLogicalReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition, bool reverse) {
using namespace reaction;
std::vector<reaction::REACLIBReaction> reaclibReactions;
auto logger = fourdst::logging::LogManager::getInstance().getLogger("log");
if (!s_initialized) {
LOG_INFO(logger, "REACLIB reactions not initialized. Calling initializeAllReaclibReactions()...");
initializeAllReaclibReactions();
if (!reaclib::s_initialized) {
LOG_DEBUG(logger, "REACLIB reactions not initialized. Calling initializeAllReaclibReactions()...");
reaclib::initializeAllReaclibReactions();
}
for (const auto &reaction: s_all_reaclib_reactions | std::views::values) {
for (const auto &reaction: reaclib::s_all_reaclib_reactions | std::views::values) {
if (reaction.is_reverse() != reverse) {
continue; // Skip reactions that do not match the requested direction
}
bool gotReaction = true;
const auto& reactants = reaction.reactants();
for (const auto& reactant : reactants) {
@@ -284,24 +73,85 @@ namespace gridfire {
}
}
if (gotReaction) {
LOG_INFO(logger, "Adding reaction {} to REACLIB reaction set.", reaction.peName());
reactions.add_reaction(reaction);
LOG_TRACE_L3(logger, "Adding reaction {} to REACLIB reaction set.", reaction.peName());
reaclibReactions.push_back(reaction);
}
}
reactions.sort();
return LogicalReactionSet(reactions);
const REACLIBReactionSet reactionSet(reaclibReactions);
return REACLIBLogicalReactionSet(reactionSet);
}
LogicalReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition, const double culling, const double T9) {
using namespace reaclib;
LogicalReactionSet allReactions = build_reaclib_nuclear_network(composition);
LogicalReactionSet reactions;
for (const auto& reaction : allReactions) {
if (reaction.calculate_rate(T9) >= culling) {
reactions.add_reaction(reaction);
// Trim whitespace from both ends of a string
std::string trim_whitespace(const std::string& str) {
auto startIt = str.begin();
auto endIt = str.end();
while (startIt != endIt && std::isspace(static_cast<unsigned char>(*startIt))) {
++startIt;
}
if (startIt == endIt) {
return "";
}
auto ritr = std::find_if(str.rbegin(), std::string::const_reverse_iterator(startIt),
[](unsigned char ch){ return !std::isspace(ch); });
return std::string(startIt, ritr.base());
}
reaction::REACLIBLogicalReactionSet build_reaclib_nuclear_network_from_file(const std::string& filename, bool reverse) {
const auto logger = fourdst::logging::LogManager::getInstance().getLogger("log");
std::vector<std::string> reactionPENames;
std::ifstream infile(filename, std::ios::in);
if (!infile.is_open()) {
LOG_ERROR(logger, "Failed to open network definition file: {}", filename);
throw std::runtime_error("Failed to open network definition file: " + filename);
}
std::string line;
while (std::getline(infile, line)) {
std::string trimmedLine = trim_whitespace(line);
if (trimmedLine.empty() || trimmedLine.starts_with('#')) {
continue; // Skip empty lines and comments
}
reactionPENames.push_back(trimmedLine);
}
infile.close();
std::vector<reaction::REACLIBReaction> reaclibReactions;
if (reactionPENames.empty()) {
LOG_ERROR(logger, "No reactions found in the network definition file: {}", filename);
throw std::runtime_error("No reactions found in the network definition file: " + filename);
}
if (!reaclib::s_initialized) {
LOG_DEBUG(logger, "REACLIB reactions not initialized. Calling initializeAllReaclibReactions()...");
reaclib::initializeAllReaclibReactions();
} else {
LOG_DEBUG(logger, "REACLIB reactions already initialized.");
}
for (const auto& peName : reactionPENames) {
bool found = false;
for (const auto& reaction : reaclib::s_all_reaclib_reactions | std::views::values) {
if (reaction.peName() == peName && reaction.is_reverse() == reverse) {
reaclibReactions.push_back(reaction);
found = true;
LOG_TRACE_L3(logger, "Found reaction {} (version {}) in REACLIB database.", peName, reaction.sourceLabel());
}
}
if (!found) {
LOG_ERROR(logger, "Reaction {} not found in REACLIB database. Skipping...", peName);
throw std::runtime_error("Reaction not found in REACLIB database.");
}
}
return reactions;
// Log any reactions that were not found in the REACLIB database
for (const auto& peName : reactionPENames) {
if (std::ranges::find(reaclibReactions, peName, &reaction::REACLIBReaction::peName) == reaclibReactions.end()) {
LOG_WARNING(logger, "Reaction {} not found in REACLIB database.", peName);
}
}
const reaction::REACLIBReactionSet reactionSet(reaclibReactions);
return reaction::REACLIBLogicalReactionSet(reactionSet);
}
}

View File

@@ -0,0 +1,427 @@
#include "gridfire/reaction/reaction.h"
#include<string_view>
#include<string>
#include<vector>
#include<memory>
#include<unordered_set>
#include<algorithm>
#include "quill/LogMacros.h"
#include "fourdst/composition/atomicSpecies.h"
#include "xxhash64.h"
namespace gridfire::reaction {
using namespace fourdst::atomic;
Reaction::Reaction(
const std::string_view id,
const double qValue,
const std::vector<Species>& reactants,
const std::vector<Species>& products,
const bool reverse) :
m_id(id),
m_qValue(qValue),
m_reactants(std::move(reactants)),
m_products(std::move(products)),
m_reverse(reverse) {}
bool Reaction::contains(const Species &species) const {
return contains_reactant(species) || contains_product(species);
}
bool Reaction::contains_reactant(const Species& species) const {
for (const auto& reactant : m_reactants) {
if (reactant == species) {
return true;
}
}
return false;
}
bool Reaction::contains_product(const Species& species) const {
for (const auto& product : m_products) {
if (product == species) {
return true;
}
}
return false;
}
std::unordered_set<Species> Reaction::all_species() const {
auto rs = reactant_species();
auto ps = product_species();
rs.insert(ps.begin(), ps.end());
return rs;
}
std::unordered_set<Species> Reaction::reactant_species() const {
std::unordered_set<Species> reactantsSet;
for (const auto& reactant : m_reactants) {
reactantsSet.insert(reactant);
}
return reactantsSet;
}
std::unordered_set<Species> Reaction::product_species() const {
std::unordered_set<Species> productsSet;
for (const auto& product : m_products) {
productsSet.insert(product);
}
return productsSet;
}
int Reaction::stoichiometry(const Species& species) const {
int s = 0;
for (const auto& reactant : m_reactants) {
if (reactant == species) {
s--;
}
}
for (const auto& product : m_products) {
if (product == species) {
s++;
}
}
return s;
}
size_t Reaction::num_species() const {
return all_species().size();
}
std::unordered_map<Species, int> Reaction::stoichiometry() const {
std::unordered_map<Species, int> stoichiometryMap;
for (const auto& reactant : m_reactants) {
stoichiometryMap[reactant]--;
}
for (const auto& product : m_products) {
stoichiometryMap[product]++;
}
return stoichiometryMap;
}
double Reaction::excess_energy() const {
double reactantMass = 0.0;
double productMass = 0.0;
constexpr double AMU2MeV = 931.494893; // Conversion factor from atomic mass unit to MeV
for (const auto& reactant : m_reactants) {
reactantMass += reactant.mass();
}
for (const auto& product : m_products) {
productMass += product.mass();
}
return (reactantMass - productMass) * AMU2MeV;
}
uint64_t Reaction::hash(uint64_t seed) const {
return XXHash64::hash(m_id.data(), m_id.size(), seed);
}
ReactionSet::ReactionSet(
std::vector<std::unique_ptr<Reaction>> reactions) :
m_reactions(std::move(reactions)) {
if (m_reactions.empty()) {
return; // Case where the reactions will be added later.
}
m_reactionNameMap.reserve(reactions.size());
for (const auto& reaction : m_reactions) {
m_id += reaction->id();
m_reactionNameMap.emplace(reaction->id(), reaction.get());
}
}
ReactionSet::ReactionSet(const ReactionSet &other) {
m_reactions.reserve(other.m_reactions.size());
for (const auto& reaction_ptr: other.m_reactions) {
m_reactions.push_back(reaction_ptr->clone());
}
m_reactionNameMap.reserve(other.m_reactionNameMap.size());
for (const auto& reaction_ptr : m_reactions) {
m_reactionNameMap.emplace(reaction_ptr->id(), reaction_ptr.get());
}
}
ReactionSet & ReactionSet::operator=(const ReactionSet &other) {
if (this != &other) {
ReactionSet temp(other);
std::swap(m_reactions, temp.m_reactions);
std::swap(m_reactionNameMap, temp.m_reactionNameMap);
}
return *this;
}
void ReactionSet::add_reaction(std::unique_ptr<Reaction> reaction) {
m_reactions.emplace_back(std::move(reaction));
m_id += m_reactions.back()->id();
m_reactionNameMap.emplace(m_reactions.back()->id(), m_reactions.back().get());
}
void ReactionSet::remove_reaction(const std::unique_ptr<Reaction>& reaction) {
if (!m_reactionNameMap.contains(std::string(reaction->id()))) {
// LOG_INFO(m_logger, "Attempted to remove reaction {} that does not exist in ReactionSet. Skipping...", reaction->id());
return;
}
m_reactionNameMap.erase(std::string(reaction->id()));
std::erase_if(m_reactions, [&reaction](const std::unique_ptr<Reaction>& r) {
return *r == *reaction;
});
}
bool ReactionSet::contains(const std::string_view& id) const {
for (const auto& reaction : m_reactions) {
if (reaction->id() == id) {
return true;
}
}
return false;
}
bool ReactionSet::contains(const Reaction& reaction) const {
for (const auto& r : m_reactions) {
if (*r == reaction) {
return true;
}
}
return false;
}
void ReactionSet::sort(double T9) {
std::ranges::sort(m_reactions,
[&T9](const std::unique_ptr<Reaction>& r1, const std::unique_ptr<Reaction>& r2) {
return r1->calculate_rate(T9) < r2->calculate_rate(T9);
});
}
bool ReactionSet::contains_species(const Species& species) const {
for (const auto& reaction : m_reactions) {
if (reaction->contains(species)) {
return true;
}
}
return false;
}
bool ReactionSet::contains_reactant(const Species& species) const {
for (const auto& r : m_reactions) {
if (r->contains_reactant(species)) {
return true;
}
}
return false;
}
bool ReactionSet::contains_product(const Species& species) const {
for (const auto& r : m_reactions) {
if (r->contains_product(species)) {
return true;
}
}
return false;
}
const Reaction& ReactionSet::operator[](const size_t index) const {
if (index >= m_reactions.size()) {
throw std::out_of_range("Index" + std::to_string(index) + " out of range for ReactionSet of size " + std::to_string(m_reactions.size()) + ".");
}
return *m_reactions[index];
}
const Reaction& ReactionSet::operator[](const std::string_view& id) const {
if (auto it = m_reactionNameMap.find(std::string(id)); it != m_reactionNameMap.end()) {
return *it->second;
}
throw std::out_of_range("Species " + std::string(id) + " does not exist in ReactionSet.");
}
bool ReactionSet::operator==(const ReactionSet& other) const {
if (size() != other.size()) {
return false;
}
return hash() == other.hash();
}
bool ReactionSet::operator!=(const ReactionSet& other) const {
return !(*this == other);
}
uint64_t ReactionSet::hash(uint64_t seed) const {
if (m_reactions.empty()) {
return XXHash64::hash(nullptr, 0, seed);
}
std::vector<uint64_t> individualReactionHashes;
individualReactionHashes.reserve(m_reactions.size());
for (const auto& reaction : m_reactions) {
individualReactionHashes.push_back(reaction->hash(seed));
}
std::ranges::sort(individualReactionHashes);
const void* data = static_cast<const void*>(individualReactionHashes.data());
size_t sizeInBytes = individualReactionHashes.size() * sizeof(uint64_t);
return XXHash64::hash(data, sizeInBytes, seed);
}
REACLIBReaction::REACLIBReaction(
const std::string_view id,
const std::string_view peName,
const int chapter,
const std::vector<Species> &reactants,
const std::vector<Species> &products,
const double qValue,
const std::string_view label,
const REACLIBRateCoefficientSet &sets,
const bool reverse) :
Reaction(id, qValue, reactants, products, reverse),
m_peName(peName),
m_chapter(chapter),
m_sourceLabel(label),
m_rateCoefficients(sets) {}
std::unique_ptr<Reaction> REACLIBReaction::clone() const {
return std::make_unique<REACLIBReaction>(*this);
}
double REACLIBReaction::calculate_rate(const double T9) const {
return calculate_rate<double>(T9);
}
CppAD::AD<double> REACLIBReaction::calculate_rate(const CppAD::AD<double> T9) const {
return calculate_rate<CppAD::AD<double>>(T9);
}
REACLIBReactionSet::REACLIBReactionSet(std::vector<REACLIBReaction> reactions) :
ReactionSet(std::vector<std::unique_ptr<Reaction>>()) {
// Convert REACLIBReaction to unique_ptr<Reaction> and store in m_reactions
m_reactions.reserve(reactions.size());
m_reactionNameMap.reserve(reactions.size());
for (auto& reaction : reactions) {
m_reactions.emplace_back(std::make_unique<REACLIBReaction>(std::move(reaction)));
m_reactionNameMap.emplace(std::string(reaction.id()), m_reactions.back().get());
}
}
std::unordered_set<std::string> REACLIBReactionSet::peNames() const {
std::unordered_set<std::string> peNames;
for (const auto& reactionPtr: m_reactions) {
if (const auto* reaction = dynamic_cast<REACLIBReaction*>(reactionPtr.get())) {
peNames.insert(std::string(reaction->peName()));
} else {
// LOG_ERROR(m_logger, "Failed to cast Reaction to REACLIBReaction in REACLIBReactionSet::peNames().");
throw std::runtime_error("Failed to cast Reaction to REACLIBReaction in REACLIBReactionSet::peNames(). This should not happen, please check your reaction setup.");
}
}
return peNames;
}
REACLIBLogicalReaction::REACLIBLogicalReaction(const std::vector<REACLIBReaction>& reactants) :
Reaction(reactants.front().peName(),
reactants.front().qValue(),
reactants.front().reactants(),
reactants.front().products(),
reactants.front().is_reverse()),
m_chapter(reactants.front().chapter()) {
m_sources.reserve(reactants.size());
m_rates.reserve(reactants.size());
for (const auto& reaction : reactants) {
if (std::abs(reaction.qValue() - m_qValue) > 1e-6) {
LOG_ERROR(m_logger, "REACLIBLogicalReaction constructed with reactions having different Q-values. Expected {} got {}.", m_qValue, reaction.qValue());
throw std::runtime_error("REACLIBLogicalReaction constructed with reactions having different Q-values. Expected " + std::to_string(m_qValue) + " got " + std::to_string(reaction.qValue()) + ".");
}
m_sources.push_back(std::string(reaction.sourceLabel()));
m_rates.push_back(reaction.rateCoefficients());
}
}
REACLIBLogicalReaction::REACLIBLogicalReaction(const REACLIBReaction& reaction) :
Reaction(reaction.peName(),
reaction.qValue(),
reaction.reactants(),
reaction.products(),
reaction.is_reverse()),
m_chapter(reaction.chapter()) {
m_sources.reserve(1);
m_rates.reserve(1);
m_sources.push_back(std::string(reaction.sourceLabel()));
m_rates.push_back(reaction.rateCoefficients());
}
void REACLIBLogicalReaction::add_reaction(const REACLIBReaction& reaction) {
if (reaction.peName() != m_id) {
LOG_ERROR(m_logger, "Cannot add reaction with different peName to REACLIBLogicalReaction. Expected {} got {}.", m_id, reaction.peName());
throw std::runtime_error("Cannot add reaction with different peName to REACLIBLogicalReaction. Expected " + std::string(m_id) + " got " + std::string(reaction.peName()) + ".");
}
for (const auto& source : m_sources) {
if (source == reaction.sourceLabel()) {
LOG_ERROR(m_logger, "Cannot add reaction with duplicate source label {} to REACLIBLogicalReaction.", reaction.sourceLabel());
throw std::runtime_error("Cannot add reaction with duplicate source label " + std::string(reaction.sourceLabel()) + " to REACLIBLogicalReaction.");
}
}
if (std::abs(reaction.qValue() - m_qValue) > 1e-6) {
LOG_ERROR(m_logger, "REACLIBLogicalReaction constructed with reactions having different Q-values. Expected {} got {}.", m_qValue, reaction.qValue());
throw std::runtime_error("REACLIBLogicalReaction constructed with reactions having different Q-values. Expected " + std::to_string(m_qValue) + " got " + std::to_string(reaction.qValue()) + ".");
}
m_sources.push_back(std::string(reaction.sourceLabel()));
m_rates.push_back(reaction.rateCoefficients());
}
std::unique_ptr<Reaction> REACLIBLogicalReaction::clone() const {
return std::make_unique<REACLIBLogicalReaction>(*this);
}
double REACLIBLogicalReaction::calculate_rate(const double T9) const {
return calculate_rate<double>(T9);
}
CppAD::AD<double> REACLIBLogicalReaction::calculate_rate(const CppAD::AD<double> T9) const {
return calculate_rate<CppAD::AD<double>>(T9);
}
REACLIBLogicalReactionSet::REACLIBLogicalReactionSet(const REACLIBReactionSet &reactionSet) :
ReactionSet(std::vector<std::unique_ptr<Reaction>>()) {
std::unordered_map<std::string_view, std::vector<REACLIBReaction>> grouped_reactions;
for (const auto& reaction_ptr : reactionSet) {
if (const auto* reaclib_reaction = dynamic_cast<const REACLIBReaction*>(reaction_ptr.get())) {
grouped_reactions[reaclib_reaction->peName()].push_back(*reaclib_reaction);
}
}
m_reactions.reserve(grouped_reactions.size());
m_reactionNameMap.reserve(grouped_reactions.size());
for (const auto& [peName, reactions_for_peName] : grouped_reactions) {
m_peNames.insert(std::string(peName));
auto logical_reaction = std::make_unique<REACLIBLogicalReaction>(reactions_for_peName);
m_reactionNameMap.emplace(logical_reaction->id(), logical_reaction.get());
m_reactions.push_back(std::move(logical_reaction));
}
}
std::unordered_set<std::string> REACLIBLogicalReactionSet::peNames() const {
return m_peNames;
}
}
namespace std {
template<>
struct hash<gridfire::reaction::Reaction> {
size_t operator()(const gridfire::reaction::Reaction& r) const noexcept {
return r.hash(0);
}
};
template<>
struct hash<gridfire::reaction::ReactionSet> {
size_t operator()(const gridfire::reaction::ReactionSet& s) const noexcept {
return s.hash(0);
}
};
} // namespace std

View File

@@ -0,0 +1,384 @@
#include "gridfire/solver/solver.h"
#include "gridfire/engine/engine_graph.h"
#include "gridfire/network.h"
#include "fourdst/composition/atomicSpecies.h"
#include "fourdst/composition/composition.h"
#include "fourdst/config/config.h"
#include "Eigen/Dense"
#include "unsupported/Eigen/NonLinearOptimization"
#include <boost/numeric/odeint.hpp>
#include <vector>
#include <unordered_map>
#include <string>
#include <stdexcept>
#include "quill/LogMacros.h"
namespace gridfire::solver {
NetOut QSENetworkSolver::evaluate(const NetIn &netIn) {
using state_type = boost::numeric::ublas::vector<double>;
using namespace boost::numeric::odeint;
NetOut postIgnition = initializeNetworkWithShortIgnition(netIn);
constexpr double abundance_floor = 1.0e-30;
std::vector<double>Y_sanitized_initial;
Y_sanitized_initial.reserve(m_engine.getNetworkSpecies().size());
LOG_DEBUG(m_logger, "Sanitizing initial abundances with a floor of {:0.3E}...", abundance_floor);
for (const auto& species : m_engine.getNetworkSpecies()) {
double molar_abundance = 0.0;
if (postIgnition.composition.contains(species)) {
molar_abundance = postIgnition.composition.getMolarAbundance(std::string(species.name()));
}
if (molar_abundance < abundance_floor) {
molar_abundance = abundance_floor;
}
Y_sanitized_initial.push_back(molar_abundance);
}
const double T9 = netIn.temperature / 1e9; // Convert temperature from Kelvin to T9 (T9 = T / 1e9)
const double rho = netIn.density; // Density in g/cm^3
const auto indices = packSpeciesTypeIndexVectors(Y_sanitized_initial, T9, rho);
Eigen::VectorXd Y_QSE;
try {
Y_QSE = calculateSteadyStateAbundances(Y_sanitized_initial, T9, rho, indices);
} catch (const std::runtime_error& e) {
LOG_ERROR(m_logger, "Failed to calculate steady state abundances. Aborting QSE evaluation.");
m_logger->flush_log();
throw std::runtime_error("Failed to calculate steady state abundances: " + std::string(e.what()));
}
state_type YDynamic_ublas(indices.dynamicSpeciesIndices.size() + 1);
for (size_t i = 0; i < indices.dynamicSpeciesIndices.size(); ++i) {
YDynamic_ublas(i) = Y_sanitized_initial[indices.dynamicSpeciesIndices[i]];
}
YDynamic_ublas(indices.dynamicSpeciesIndices.size()) = 0.0; // Placeholder for specific energy rate
const RHSFunctor rhs_functor(m_engine, indices.dynamicSpeciesIndices, indices.QSESpeciesIndices, Y_QSE, T9, rho);
const auto stepper = make_controlled<runge_kutta_dopri5<state_type>>(1.0e-8, 1.0e-8);
size_t stepCount = integrate_adaptive(
stepper,
rhs_functor,
YDynamic_ublas,
0.0, // Start time
netIn.tMax,
netIn.dt0
);
std::vector<double> YFinal = Y_sanitized_initial;
for (size_t i = 0; i <indices.dynamicSpeciesIndices.size(); ++i) {
YFinal[indices.dynamicSpeciesIndices[i]] = YDynamic_ublas(i);
}
for (size_t i = 0; i < indices.QSESpeciesIndices.size(); ++i) {
YFinal[indices.QSESpeciesIndices[i]] = Y_QSE(i);
}
const double finalSpecificEnergyRate = YDynamic_ublas(indices.dynamicSpeciesIndices.size());
// --- Marshal output variables ---
std::vector<std::string> speciesNames(m_engine.getNetworkSpecies().size());
std::vector<double> finalMassFractions(m_engine.getNetworkSpecies().size());
double massFractionSum = 0.0;
for (size_t i = 0; i < speciesNames.size(); ++i) {
const auto& species = m_engine.getNetworkSpecies()[i];
speciesNames[i] = species.name();
finalMassFractions[i] = YFinal[i] * species.mass(); // Convert from molar abundance to mass fraction
massFractionSum += finalMassFractions[i];
}
for (auto& mf : finalMassFractions) {
mf /= massFractionSum; // Normalize to get mass fractions
}
fourdst::composition::Composition outputComposition(speciesNames, finalMassFractions);
NetOut netOut;
netOut.composition = outputComposition;
netOut.energy = finalSpecificEnergyRate; // Specific energy rate
netOut.num_steps = stepCount;
return netOut;
}
dynamicQSESpeciesIndices QSENetworkSolver::packSpeciesTypeIndexVectors(
const std::vector<double>& Y,
const double T9,
const double rho
) const {
constexpr double timescaleCutoff = 1.0e-5;
constexpr double abundanceCutoff = 1.0e-15;
LOG_INFO(m_logger, "Partitioning species using T9={:0.2f} and ρ={:0.2e}", T9, rho);
LOG_INFO(m_logger, "Timescale Cutoff: {:.1e} s, Abundance Cutoff: {:.1e}", timescaleCutoff, abundanceCutoff);
std::vector<size_t>dynamicSpeciesIndices; // Slow species that are not in QSE
std::vector<size_t>QSESpeciesIndices; // Fast species that are in QSE
std::unordered_map<fourdst::atomic::Species, double> speciesTimescale = m_engine.getSpeciesTimescales(Y, T9, rho);
for (size_t i = 0; i < m_engine.getNetworkSpecies().size(); ++i) {
const auto& species = m_engine.getNetworkSpecies()[i];
const double timescale = speciesTimescale[species];
const double abundance = Y[i];
if (std::isinf(timescale) || abundance < abundanceCutoff || timescale <= timescaleCutoff) {
QSESpeciesIndices.push_back(i);
} else {
dynamicSpeciesIndices.push_back(i);
}
}
LOG_INFO(m_logger, "Partitioning complete. Dynamical species: {}, QSE species: {}.", dynamicSpeciesIndices.size(), QSESpeciesIndices.size());
LOG_INFO(m_logger, "Dynamic species: {}", [dynamicSpeciesIndices](const DynamicEngine& engine_wrapper) -> std::string {
std::string result;
int count = 0;
for (const auto& i : dynamicSpeciesIndices) {
result += std::string(engine_wrapper.getNetworkSpecies()[i].name());
if (count < dynamicSpeciesIndices.size() - 2) {
result += ", ";
} else if (count == dynamicSpeciesIndices.size() - 2) {
result += " and ";
}
count++;
}
return result;
}(m_engine));
LOG_INFO(m_logger, "QSE species: {}", [QSESpeciesIndices](const DynamicEngine& engine_wrapper) -> std::string {
std::string result;
int count = 0;
for (const auto& i : QSESpeciesIndices) {
result += std::string(engine_wrapper.getNetworkSpecies()[i].name());
if (count < QSESpeciesIndices.size() - 2) {
result += ", ";
} else if (count == QSESpeciesIndices.size() - 2) {
result += " and ";
}
count++;
}
return result;
}(m_engine));
return {dynamicSpeciesIndices, QSESpeciesIndices};
}
Eigen::VectorXd QSENetworkSolver::calculateSteadyStateAbundances(
const std::vector<double> &Y,
const double T9,
const double rho,
const dynamicQSESpeciesIndices &indices
) const {
std::vector<double> Y_dyn;
Eigen::VectorXd Y_qse_initial(indices.QSESpeciesIndices.size());
for (const auto& i : indices.dynamicSpeciesIndices) { Y_dyn.push_back(Y[i]); }
for (size_t i = 0; i < indices.QSESpeciesIndices.size(); ++i) {
Y_qse_initial(i) = Y[indices.QSESpeciesIndices[i]];
if (Y_qse_initial(i) < 1.0e-99) { Y_qse_initial(i) = 1.0e-99; }
}
Eigen::VectorXd v_qse = Y_qse_initial.array().log();
EigenFunctor<double> qse_problem(m_engine, Y_dyn, indices.dynamicSpeciesIndices, indices.QSESpeciesIndices, T9, rho);
LOG_INFO(m_logger, "--- QSE Pre-Solve Diagnostics ---");
Eigen::VectorXd f_initial(indices.QSESpeciesIndices.size());
qse_problem(v_qse, f_initial);
LOG_INFO(m_logger, "Initial Guess ||f||: {:0.4e}", f_initial.norm());
Eigen::MatrixXd J_initial(indices.QSESpeciesIndices.size(), indices.QSESpeciesIndices.size());
qse_problem.df(v_qse, J_initial);
const Eigen::JacobiSVD<Eigen::MatrixXd> svd(J_initial);
double cond = svd.singularValues().maxCoeff() / svd.singularValues().minCoeff();
LOG_INFO(m_logger, "Initial Jacobian Condition Number: {:0.4e}", cond);
LOG_INFO(m_logger, "Starting QSE solve...");
Eigen::HybridNonLinearSolver<EigenFunctor<double>> solver(qse_problem);
solver.parameters.xtol = 1.0e-8; // Set tolerance
// 5. Run the solver. It will modify v_qse in place.
const int eigenStatus = solver.solve(v_qse);
// 6. Check for convergence and return the result
if(eigenStatus != Eigen::Success) {
LOG_WARNING(m_logger, "--- QSE SOLVER FAILED ---");
LOG_WARNING(m_logger, "Eigen status code: {}", eigenStatus);
LOG_WARNING(m_logger, "Iterations performed: {}", solver.iter);
// Log the final state that caused the failure
Eigen::VectorXd Y_qse_final_fail = v_qse.array().exp();
for(long i=0; i<v_qse.size(); ++i) {
LOG_WARNING(m_logger, "Final v_qse[{}]: {:0.4e} -> Y_qse[{}]: {:0.4e}", i, v_qse(i), i, Y_qse_final_fail(i));
}
// Log the residual at the final state
Eigen::VectorXd f_final(indices.QSESpeciesIndices.size());
qse_problem(v_qse, f_final);
LOG_WARNING(m_logger, "Final ||f||: {:0.4e}", f_final.norm());
throw std::runtime_error("Eigen QSE solver did not converge.");
}
LOG_INFO(m_logger, "Eigen QSE solver converged in {} iterations.", solver.iter);
return v_qse.array().exp();
}
NetOut QSENetworkSolver::initializeNetworkWithShortIgnition(const NetIn &netIn) const {
const auto ignitionTemperature = m_config.get<double>(
"gridfire:solver:QSE:ignition:temperature",
2e8
); // 0.2 GK
const auto ignitionDensity = m_config.get<double>(
"gridfire:solver:QSE:ignition:density",
1e6
); // 1e6 g/cm^3
const auto ignitionTime = m_config.get<double>(
"gridfire:solver:QSE:ignition:tMax",
1e-7
); // 0.1 μs
const auto ignitionStepSize = m_config.get<double>(
"gridfire:solver:QSE:ignition:dt0",
1e-15
); // 1e-15 seconds
LOG_INFO(
m_logger,
"Igniting network with T={:<5.3E}, ρ={:<5.3E}, tMax={:<5.3E}, dt0={:<5.3E}...",
ignitionTemperature,
ignitionDensity,
ignitionTime,
ignitionStepSize
);
NetIn preIgnition = netIn;
preIgnition.temperature = ignitionTemperature;
preIgnition.density = ignitionDensity;
preIgnition.tMax = ignitionTime;
preIgnition.dt0 = ignitionStepSize;
DirectNetworkSolver ignitionSolver(m_engine);
NetOut postIgnition = ignitionSolver.evaluate(preIgnition);
LOG_INFO(m_logger, "Network ignition completed in {} steps.", postIgnition.num_steps);
return postIgnition;
}
void QSENetworkSolver::RHSFunctor::operator()(
const boost::numeric::ublas::vector<double> &YDynamic,
boost::numeric::ublas::vector<double> &dYdtDynamic,
double t
) const {
// --- Populate the slow / dynamic species vector ---
std::vector<double> YFull(m_engine.getNetworkSpecies().size());
for (size_t i = 0; i < m_dynamicSpeciesIndices.size(); ++i) {
YFull[m_dynamicSpeciesIndices[i]] = YDynamic(i);
}
// --- Populate the QSE species vector ---
for (size_t i = 0; i < m_QSESpeciesIndices.size(); ++i) {
YFull[m_QSESpeciesIndices[i]] = m_Y_QSE(i);
}
auto [full_dYdt, specificEnergyRate] = m_engine.calculateRHSAndEnergy(YFull, m_T9, m_rho);
dYdtDynamic.resize(m_dynamicSpeciesIndices.size() + 1);
for (size_t i = 0; i < m_dynamicSpeciesIndices.size(); ++i) {
dYdtDynamic(i) = full_dYdt[m_dynamicSpeciesIndices[i]];
}
dYdtDynamic[m_dynamicSpeciesIndices.size()] = specificEnergyRate;
}
NetOut DirectNetworkSolver::evaluate(const NetIn &netIn) {
namespace ublas = boost::numeric::ublas;
namespace odeint = boost::numeric::odeint;
using fourdst::composition::Composition;
const double T9 = netIn.temperature / 1e9; // Convert temperature from Kelvin to T9 (T9 = T / 1e9)
const unsigned long numSpecies = m_engine.getNetworkSpecies().size();
const auto absTol = m_config.get<double>("gridfire:solver:DirectNetworkSolver:absTol", 1.0e-8);
const auto relTol = m_config.get<double>("gridfire:solver:DirectNetworkSolver:relTol", 1.0e-8);
size_t stepCount = 0;
RHSFunctor rhsFunctor(m_engine, T9, netIn.density);
JacobianFunctor jacobianFunctor(m_engine, T9, netIn.density);
ublas::vector<double> Y(numSpecies + 1);
for (size_t i = 0; i < numSpecies; ++i) {
const auto& species = m_engine.getNetworkSpecies()[i];
try {
Y(i) = netIn.composition.getMolarAbundance(std::string(species.name()));
} catch (const std::runtime_error) {
LOG_DEBUG(m_logger, "Species '{}' not found in composition. Setting abundance to 0.0.", species.name());
Y(i) = 0.0;
}
}
Y(numSpecies) = 0.0;
const auto stepper = odeint::make_controlled<odeint::rosenbrock4<double>>(absTol, relTol);
stepCount = odeint::integrate_adaptive(
stepper,
std::make_pair(rhsFunctor, jacobianFunctor),
Y,
0.0,
netIn.tMax,
netIn.dt0
);
std::vector<double> finalMassFractions(numSpecies);
for (size_t i = 0; i < numSpecies; ++i) {
const double molarMass = m_engine.getNetworkSpecies()[i].mass();
finalMassFractions[i] = Y(i) * molarMass; // Convert from molar abundance to mass fraction
if (finalMassFractions[i] < MIN_ABUNDANCE_THRESHOLD) {
finalMassFractions[i] = 0.0;
}
}
std::vector<std::string> speciesNames;
speciesNames.reserve(numSpecies);
for (const auto& species : m_engine.getNetworkSpecies()) {
speciesNames.push_back(std::string(species.name()));
}
Composition outputComposition(speciesNames, finalMassFractions);
outputComposition.finalize(true);
NetOut netOut;
netOut.composition = std::move(outputComposition);
netOut.energy = Y(numSpecies); // Specific energy rate
netOut.num_steps = stepCount;
return netOut;
}
void DirectNetworkSolver::RHSFunctor::operator()(
const boost::numeric::ublas::vector<double> &Y,
boost::numeric::ublas::vector<double> &dYdt,
double t
) const {
const std::vector<double> y(Y.begin(), m_numSpecies + Y.begin());
auto [dydt, eps] = m_engine.calculateRHSAndEnergy(y, m_T9, m_rho);
dYdt.resize(m_numSpecies + 1);
std::ranges::copy(dydt, dydt.begin());
dYdt(m_numSpecies) = eps;
}
void DirectNetworkSolver::JacobianFunctor::operator()(
const boost::numeric::ublas::vector<double> &Y,
boost::numeric::ublas::matrix<double> &J,
double t,
boost::numeric::ublas::vector<double> &dfdt
) const {
J.resize(m_numSpecies + 1, m_numSpecies + 1);
J.clear();
for (int i = 0; i < m_numSpecies + 1; ++i) {
for (int j = 0; j < m_numSpecies + 1; ++j) {
J(i, j) = m_engine.getJacobianMatrixEntry(i, j);
}
}
}
}