feat(network): added half lifes, spin flip parity, better reaction acritecture

This commit is contained in:
2025-06-29 14:53:39 -04:00
parent 2a410dc3fd
commit 29af4c3bab
14 changed files with 2270 additions and 637 deletions

View File

@@ -1,10 +1,12 @@
#include "gridfire/engine/engine_culled.h"
#include "gridfire/engine/engine_adaptive.h"
#include <ranges>
#include <queue>
#include "gridfire/network.h"
#include "quill/LogMacros.h"
#include "quill/Logger.h"
namespace gridfire {
using fourdst::atomic::Species;
@@ -14,7 +16,8 @@ namespace gridfire {
m_baseEngine(baseEngine),
m_activeSpecies(baseEngine.getNetworkSpecies()),
m_activeReactions(baseEngine.getNetworkReactions()),
m_speciesIndexMap(constructSpeciesIndexMap())
m_speciesIndexMap(constructSpeciesIndexMap()),
m_reactionIndexMap(constructReactionIndexMap())
{
}
@@ -38,14 +41,47 @@ namespace gridfire {
speciesIndexMap.push_back(it->second);
} else {
LOG_ERROR(m_logger, "Species '{}' not found in full species map.", active_species.name());
m_logger -> flush_log();
throw std::runtime_error("Species not found in full species map: " + std::string(active_species.name()));
}
}
LOG_TRACE_L1(m_logger, "Successfully constructed species index map with {} entries.", speciesIndexMap.size());
LOG_TRACE_L1(m_logger, "Species index map constructed with {} entries.", speciesIndexMap.size());
return speciesIndexMap;
}
std::vector<size_t> AdaptiveEngineView::constructReactionIndexMap() const {
LOG_TRACE_L1(m_logger, "Constructing reaction index map for adaptive engine view...");
// --- Step 1: Create a reverse map using the reaction's unique ID as the key. ---
std::unordered_map<std::string_view, size_t> fullReactionReverseMap;
const auto& fullReactionSet = m_baseEngine.getNetworkReactions();
fullReactionReverseMap.reserve(fullReactionSet.size());
for (size_t i_full = 0; i_full < fullReactionSet.size(); ++i_full) {
fullReactionReverseMap[fullReactionSet[i_full].id()] = i_full;
}
// --- Step 2: Build the final index map using the active reaction set. ---
std::vector<size_t> reactionIndexMap;
reactionIndexMap.reserve(m_activeReactions.size());
for (const auto& active_reaction_ptr : m_activeReactions) {
auto it = fullReactionReverseMap.find(active_reaction_ptr.id());
if (it != fullReactionReverseMap.end()) {
reactionIndexMap.push_back(it->second);
} else {
LOG_ERROR(m_logger, "Active reaction '{}' not found in base engine during reaction index map construction.", active_reaction_ptr.id());
m_logger->flush_log();
throw std::runtime_error("Mismatch between active reactions and base engine.");
}
}
LOG_TRACE_L1(m_logger, "Reaction index map constructed with {} entries.", reactionIndexMap.size());
return reactionIndexMap;
}
void AdaptiveEngineView::update(const NetIn& netIn) {
LOG_TRACE_L1(m_logger, "Updating adaptive engine view...");
@@ -57,7 +93,7 @@ namespace gridfire {
if (netIn.composition.contains(species)) {
Y_full.push_back(netIn.composition.getMolarAbundance(std::string(species.name())));
} else {
LOG_DEBUG(m_logger, "Species '{}' not found in composition. Setting abundance to 0.0.", species.name());
LOG_TRACE_L2(m_logger, "Species '{}' not found in composition. Setting abundance to 0.0.", species.name());
Y_full.push_back(0.0);
}
}
@@ -65,25 +101,119 @@ namespace gridfire {
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
m_isStale = false;
std::vector<ReactionFlow> reactionFlows;
const auto& fullReactionSet = m_baseEngine.getNetworkReactions();
reactionFlows.reserve(fullReactionSet.size());
for (const auto& reactionPtr : fullReactionSet) {
const double flow = m_baseEngine.calculateMolarReactionFlow(*reactionPtr, Y_full, T9, rho);
reactionFlows.push_back({reactionPtr.get(), flow});
const double flow = m_baseEngine.calculateMolarReactionFlow(reactionPtr, Y_full, T9, rho);
reactionFlows.push_back({&reactionPtr, flow});
}
double max_flow = 0.0;
double min_flow = std::numeric_limits<double>::max();
double flowSum = 0.0;
for (const auto&[reactionPtr, flowRate] : reactionFlows) {
if (flowRate > max_flow) {
max_flow = flowRate;
} else if (flowRate < min_flow) {
min_flow = flowRate;
}
flowSum += flowRate;
LOG_TRACE_L2(m_logger, "Reaction '{}' has flow rate: {:0.3E} [mol/s]", reactionPtr->id(), flowRate);
}
flowSum /= reactionFlows.size();
LOG_DEBUG(m_logger, "Maximum reaction flow rate in adaptive engine view: {:0.3E} [mol/s]", max_flow);
LOG_DEBUG(m_logger, "Minimum reaction flow rate in adaptive engine view: {:0.3E} [mol/s]", min_flow);
LOG_DEBUG(m_logger, "Average reaction flow rate in adaptive engine view: {:0.3E} [mol/s]", flowSum);
const double relative_culling_threshold = m_config.get<double>("gridfire:AdaptiveEngineView:RelativeCullingThreshold", 1e-75);
double absoluteCullingThreshold = relative_culling_threshold * max_flow;
LOG_DEBUG(m_logger, "Relative culling threshold: {:0.3E} ({})", relative_culling_threshold, absoluteCullingThreshold);
// --- Reaction Culling ---
LOG_TRACE_L1(m_logger, "Culling reactions based on reaction flow rates...");
std::vector<const reaction::Reaction*> flowCulledReactions;
for (const auto&[reactionPtr, flowRate] : reactionFlows) {
if (flowRate > absoluteCullingThreshold) {
LOG_TRACE_L2(m_logger, "Maintaining reaction '{}' with relative (abs) flow rate: {:0.3E} ({:0.3E} [mol/s])", reactionPtr->id(), flowRate/max_flow, flowRate);
flowCulledReactions.push_back(reactionPtr);
}
}
LOG_DEBUG(m_logger, "Selected {} (total: {}, culled: {}) reactions based on flow rates.", flowCulledReactions.size(), fullReactionSet.size(), fullReactionSet.size() - flowCulledReactions.size());
// --- Connectivity Analysis ---
std::queue<Species> species_to_visit;
std::unordered_set<Species> reachable_species;
constexpr double ABUNDANCE_FLOOR = 1e-12; // Abundance floor for a species to be considered part of the initial fuel
for (const auto& species : fullSpeciesList) {
if (netIn.composition.contains(species) && netIn.composition.getMassFraction(std::string(species.name())) > ABUNDANCE_FLOOR) {
species_to_visit.push(species);
reachable_species.insert(species);
LOG_TRACE_L2(m_logger, "Species '{}' is part of the initial fuel.", species.name());
}
}
std::unordered_map<Species, std::vector<const reaction::Reaction*>> reactant_to_reactions_map;
for (const auto* reaction_ptr : flowCulledReactions) {
for (const auto& reactant : reaction_ptr->reactants()) {
reactant_to_reactions_map[reactant].push_back(reaction_ptr);
}
}
LOG_DEBUG(m_logger, "Maximum reaction flow rate in adaptive engine view: {:0.3E} [mol/s]", max_flow);
while (!species_to_visit.empty()) {
Species currentSpecies = species_to_visit.front();
species_to_visit.pop();
auto it = reactant_to_reactions_map.find(currentSpecies);
if (it == reactant_to_reactions_map.end()) {
continue; // The species does not initiate any further reactions
}
const auto& reactions = it->second;
for (const auto* reaction_ptr : reactions) {
for (const auto& product : reaction_ptr->products()) {
if (!reachable_species.contains(product)) {
reachable_species.insert(product);
species_to_visit.push(product);
LOG_TRACE_L2(m_logger, "Species '{}' is reachable via reaction '{}'.", product.name(), reaction_ptr->id());
}
}
}
}
LOG_DEBUG(m_logger, "Reachable species count: {}", reachable_species.size());
m_activeSpecies.assign(reachable_species.begin(), reachable_species.end());
std::ranges::sort(m_activeSpecies,
[](const Species &a, const Species &b) { return a.mass() < b.mass(); });
m_activeReactions.clear();
for (const auto* reaction_ptr : flowCulledReactions) {
bool all_reactants_present = true;
for (const auto& reactant : reaction_ptr->reactants()) {
if (!reachable_species.contains(reactant)) {
all_reactants_present = false;
break;
}
}
if (all_reactants_present) {
m_activeReactions.add_reaction(*reaction_ptr);
LOG_TRACE_L2(m_logger, "Maintaining reaction '{}' with all reactants present.", reaction_ptr->id());
} else {
LOG_TRACE_L1(m_logger, "Culling reaction '{}' due to missing reactants.", reaction_ptr->id());
}
}
LOG_DEBUG(m_logger, "Active reactions count: {} (total: {}, culled: {}, culled due to connectivity: {})", m_activeReactions.size(),
fullReactionSet.size(), fullReactionSet.size() - m_activeReactions.size(), flowCulledReactions.size() - m_activeReactions.size());
m_speciesIndexMap = constructSpeciesIndexMap();
m_reactionIndexMap = constructReactionIndexMap();
m_isStale = false;
}
const std::vector<Species> & AdaptiveEngineView::getNetworkSpecies() const {
@@ -91,65 +221,143 @@ namespace gridfire {
}
StepDerivatives<double> AdaptiveEngineView::calculateRHSAndEnergy(
const std::vector<double> &Y,
const std::vector<double> &Y_culled,
const double T9,
const double rho
) const {
return m_baseEngine.calculateRHSAndEnergy(Y, T9, rho);
validateState();
const auto Y_full = mapCulledToFull(Y_culled);
const auto [dydt, nuclearEnergyGenerationRate] = m_baseEngine.calculateRHSAndEnergy(Y_full, T9, rho);
StepDerivatives<double> culledResults;
culledResults.nuclearEnergyGenerationRate = nuclearEnergyGenerationRate;
culledResults.dydt = mapFullToCulled(dydt);
return culledResults;
}
void AdaptiveEngineView::generateJacobianMatrix(
const std::vector<double> &Y,
const std::vector<double> &Y_culled,
const double T9,
const double rho
) {
m_baseEngine.generateJacobianMatrix(Y, T9, rho);
validateState();
const auto Y_full = mapCulledToFull(Y_culled);
m_baseEngine.generateJacobianMatrix(Y_full, T9, rho);
}
double AdaptiveEngineView::getJacobianMatrixEntry(
const int i,
const int j
const int i_culled,
const int j_culled
) const {
return m_baseEngine.getJacobianMatrixEntry(i, j);
validateState();
const size_t i_full = mapCulledToFullSpeciesIndex(i_culled);
const size_t j_full = mapCulledToFullSpeciesIndex(j_culled);
return m_baseEngine.getJacobianMatrixEntry(i_full, j_full);
}
void AdaptiveEngineView::generateStoichiometryMatrix() {
validateState();
m_baseEngine.generateStoichiometryMatrix();
}
int AdaptiveEngineView::getStoichiometryMatrixEntry(
const int speciesIndex,
const int reactionIndex
const int speciesIndex_culled,
const int reactionIndex_culled
) const {
return m_baseEngine.getStoichiometryMatrixEntry(speciesIndex, reactionIndex);
validateState();
const size_t speciesIndex_full = mapCulledToFullSpeciesIndex(speciesIndex_culled);
const size_t reactionIndex_full = mapCulledToFullReactionIndex(reactionIndex_culled);
return m_baseEngine.getStoichiometryMatrixEntry(speciesIndex_full, reactionIndex_full);
}
double AdaptiveEngineView::calculateMolarReactionFlow(
const reaction::Reaction &reaction,
const std::vector<double> &Y,
const std::vector<double> &Y_culled,
const double T9,
const double rho
) const {
validateState();
if (!m_activeReactions.contains(reaction)) {
LOG_ERROR(m_logger, "Reaction '{}' is not part of the active reactions in the adaptive engine view.", reaction.id());
m_logger -> flush_log();
throw std::runtime_error("Reaction not found in active reactions: " + std::string(reaction.id()));
}
const auto Y = mapCulledToFull(Y_culled);
return m_baseEngine.calculateMolarReactionFlow(reaction, Y, T9, rho);
}
const reaction::REACLIBLogicalReactionSet & AdaptiveEngineView::getNetworkReactions() const {
const reaction::LogicalReactionSet & AdaptiveEngineView::getNetworkReactions() const {
return m_activeReactions;
}
std::unordered_map<fourdst::atomic::Species, double> AdaptiveEngineView::getSpeciesTimescales(
const std::vector<double> &Y,
std::unordered_map<Species, double> AdaptiveEngineView::getSpeciesTimescales(
const std::vector<double> &Y_culled,
const double T9,
const double rho
) const {
auto timescales = m_baseEngine.getSpeciesTimescales(Y, T9, rho);
for (const auto &species: timescales | std::views::keys) {
// remove species that are not in the active species list
if (std::ranges::find(m_activeSpecies, species) == m_activeSpecies.end()) {
timescales.erase(species);
validateState();
const auto Y_full = mapCulledToFull(Y_culled);
const auto fullTimescales = m_baseEngine.getSpeciesTimescales(Y_full, T9, rho);
std::unordered_map<Species, double> culledTimescales;
culledTimescales.reserve(m_activeSpecies.size());
for (const auto& active_species : m_activeSpecies) {
if (fullTimescales.contains(active_species)) {
culledTimescales[active_species] = fullTimescales.at(active_species);
}
}
return timescales;
return culledTimescales;
}
std::vector<double> AdaptiveEngineView::mapCulledToFull(const std::vector<double>& culled) const {
std::vector<double> full(m_baseEngine.getNetworkSpecies().size(), 0.0);
for (size_t i_culled = 0; i_culled < culled.size(); ++i_culled) {
const size_t i_full = m_speciesIndexMap[i_culled];
full[i_full] += culled[i_culled];
}
return full;
}
std::vector<double> AdaptiveEngineView::mapFullToCulled(const std::vector<double>& full) const {
std::vector<double> culled(m_activeSpecies.size(), 0.0);
for (size_t i_culled = 0; i_culled < m_activeSpecies.size(); ++i_culled) {
const size_t i_full = m_speciesIndexMap[i_culled];
culled[i_culled] = full[i_full];
}
return culled;
}
size_t AdaptiveEngineView::mapCulledToFullSpeciesIndex(size_t culledSpeciesIndex) const {
if (culledSpeciesIndex < 0 || culledSpeciesIndex >= static_cast<int>(m_speciesIndexMap.size())) {
LOG_ERROR(m_logger, "Culled index {} is out of bounds for species index map of size {}.", culledSpeciesIndex, m_speciesIndexMap.size());
m_logger->flush_log();
throw std::out_of_range("Culled index " + std::to_string(culledSpeciesIndex) + " is out of bounds for species index map of size " + std::to_string(m_speciesIndexMap.size()) + ".");
}
return m_speciesIndexMap[culledSpeciesIndex];
}
size_t AdaptiveEngineView::mapCulledToFullReactionIndex(size_t culledReactionIndex) const {
if (culledReactionIndex < 0 || culledReactionIndex >= static_cast<int>(m_reactionIndexMap.size())) {
LOG_ERROR(m_logger, "Culled index {} is out of bounds for reaction index map of size {}.", culledReactionIndex, m_reactionIndexMap.size());
m_logger->flush_log();
throw std::out_of_range("Culled index " + std::to_string(culledReactionIndex) + " is out of bounds for reaction index map of size " + std::to_string(m_reactionIndexMap.size()) + ".");
}
return m_reactionIndexMap[culledReactionIndex];
}
void AdaptiveEngineView::validateState() const {
if (m_isStale) {
LOG_ERROR(m_logger, "AdaptiveEngineView is stale. Please call update() before calculating RHS and energy.");
m_logger->flush_log();
throw std::runtime_error("AdaptiveEngineView is stale. Please call update() before calculating RHS and energy.");
}
}
}

View File

@@ -18,7 +18,6 @@
#include <vector>
#include <fstream>
#include <boost/numeric/ublas/vector.hpp>
#include <boost/numeric/odeint.hpp>
@@ -30,7 +29,7 @@ namespace gridfire {
syncInternalMaps();
}
GraphEngine::GraphEngine(reaction::REACLIBLogicalReactionSet reactions) :
GraphEngine::GraphEngine(reaction::LogicalReactionSet reactions) :
m_reactions(std::move(reactions)) {
syncInternalMaps();
}
@@ -61,21 +60,22 @@ namespace gridfire {
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());
}
}
for (const auto& name: uniqueSpeciesNames) {
auto it = fourdst::atomic::species.find(name);
auto it = fourdst::atomic::species.find(std::string(name));
if (it != fourdst::atomic::species.end()) {
m_networkSpecies.push_back(it->second);
m_networkSpeciesMap.insert({name, it->second});
} else {
LOG_ERROR(m_logger, "Species '{}' not found in global atomic species database.", name);
m_logger->flush_log();
throw std::runtime_error("Species not found in global atomic species database: " + std::string(name));
}
}
@@ -85,8 +85,8 @@ namespace gridfire {
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.emplace(reaction->id(), reaction.get());
for (auto& reaction: m_reactions) {
m_reactionIDMap.emplace(reaction.id(), &reaction);
}
LOG_TRACE_L1(m_logger, "Populated {} reactions in the reaction ID map.", m_reactionIDMap.size());
}
@@ -111,13 +111,13 @@ namespace gridfire {
// --- Basic Accessors and Queries ---
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());
LOG_TRACE_L3(m_logger, "Providing access to network species vector. Size: {}.", m_networkSpecies.size());
return m_networkSpecies;
}
const reaction::REACLIBLogicalReactionSet& GraphEngine::getNetworkReactions() const {
const reaction::LogicalReactionSet& 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());
LOG_TRACE_L3(m_logger, "Providing access to network reactions set. Size: {}.", m_reactions.size());
return m_reactions;
}
@@ -139,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();
@@ -148,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();
@@ -162,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;
}
}
@@ -170,12 +170,12 @@ 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;
}
}
@@ -187,7 +187,7 @@ namespace gridfire {
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 reaction::REACLIBLogicalReactionSet validationReactionSet = build_reaclib_nuclear_network(composition, false);
const reaction::LogicalReactionSet 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)
@@ -199,7 +199,7 @@ namespace gridfire {
// This allows for dynamic network modification while retaining caching for networks which are very similar.
if (validationReactionSet != m_reactions) {
LOG_DEBUG(m_logger, "Reaction set not cached. Rebuilding the reaction set for T9={} and culling={}.", T9, culling);
m_reactions = validationReactionSet;
m_reactions = std::move(validationReactionSet);
syncInternalMaps(); // Re-sync internal maps after updating reactions. Note this will also retrace the AD tape.
}
}
@@ -221,7 +221,7 @@ 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 = reaction->stoichiometry();
std::unordered_map<fourdst::atomic::Species, int> netStoichiometry = reaction.stoichiometry();
// Iterate through the species and their coefficients in the stoichiometry map
for (const auto& [species, coefficient] : netStoichiometry) {
@@ -234,7 +234,8 @@ 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());
m_logger -> flush_log();
throw std::runtime_error("Species not found in species to index map: " + std::string(species.name()));
}
}
@@ -255,8 +256,8 @@ namespace gridfire {
StepDerivatives<ADDouble> GraphEngine::calculateAllDerivatives(
const std::vector<ADDouble> &Y_in,
const ADDouble T9,
const ADDouble rho
const ADDouble &T9,
const ADDouble &rho
) const {
return calculateAllDerivatives<ADDouble>(Y_in, T9, rho);
}
@@ -300,7 +301,7 @@ namespace gridfire {
}
}
}
LOG_DEBUG(m_logger, "Jacobian matrix generated with dimensions: {} rows x {} columns.", m_jacobianMatrix.size1(), m_jacobianMatrix.size2());
LOG_TRACE_L1(m_logger, "Jacobian matrix generated with dimensions: {} rows x {} columns.", m_jacobianMatrix.size1(), m_jacobianMatrix.size2());
}
double GraphEngine::getJacobianMatrixEntry(const int i, const int j) const {
@@ -309,7 +310,7 @@ namespace gridfire {
std::unordered_map<fourdst::atomic::Species, int> GraphEngine::getNetReactionStoichiometry(
const reaction::Reaction &reaction
) const {
) {
return reaction.stoichiometry();
}
@@ -326,6 +327,7 @@ namespace gridfire {
std::ofstream dotFile(filename);
if (!dotFile.is_open()) {
LOG_ERROR(m_logger, "Failed to open file for writing: {}", filename);
m_logger->flush_log();
throw std::runtime_error("Failed to open file for writing: " + filename);
}
@@ -345,19 +347,19 @@ 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";
}
@@ -373,36 +375,32 @@ namespace gridfire {
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);
m_logger->flush_log();
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() << ";";
csvFile << reaction.id() << ";";
// Reactants
int count = 0;
for (const auto& reactant : reaction->reactants()) {
for (const auto& reactant : reaction.reactants()) {
csvFile << reactant.name();
if (++count < reaction->reactants().size()) {
if (++count < reaction.reactants().size()) {
csvFile << ",";
}
}
csvFile << ";";
count = 0;
for (const auto& product : reaction->products()) {
for (const auto& product : reaction.products()) {
csvFile << product.name();
if (++count < reaction->products().size()) {
if (++count < reaction.products().size()) {
csvFile << ",";
}
}
csvFile << ";" << reaction->qValue() << ";";
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();
auto sources = reaction.sources();
count = 0;
for (const auto& source : sources) {
csvFile << source;
@@ -413,9 +411,9 @@ namespace gridfire {
csvFile << ";";
// Reaction coefficients
count = 0;
for (const auto& rates : *reaclibReaction) {
for (const auto& rates : reaction) {
csvFile << rates;
if (++count < reaclibReaction->size()) {
if (++count < reaction.size()) {
csvFile << ",";
}
}
@@ -448,6 +446,7 @@ namespace gridfire {
const size_t numSpecies = m_networkSpecies.size();
if (numSpecies == 0) {
LOG_ERROR(m_logger, "Cannot record AD tape: No species in the network.");
m_logger->flush_log();
throw std::runtime_error("Cannot record AD tape: No species in the network.");
}
const size_t numADInputs = numSpecies + 2; // Note here that by not letting T9 and rho be independent variables, we are constraining the network to a constant temperature and density during each evaluation.

View File

@@ -19,15 +19,25 @@
//
// *********************************************************************** */
#include "gridfire/network.h"
#include "gridfire/reactions.h"
#include "../include/gridfire/reaction/reaction.h"
#include "gridfire/reaction/reaclib.h"
#include "gridfire/reaction/reaction.h"
#include <ranges>
#include <fstream>
#include "quill/LogMacros.h"
namespace gridfire {
std::vector<double> NetIn::MolarAbundance() const {
std::vector <double> y;
y.reserve(composition.getRegisteredSymbols().size());
const auto [fst, snd] = composition.getComposition();
for (const auto &name: fst | std::views::keys) {
y.push_back(composition.getMolarAbundance(name));
}
return y;
}
Network::Network(const NetworkFormat format) :
m_config(fourdst::config::Config::getInstance()),
m_logManager(fourdst::logging::LogManager::getInstance()),
@@ -36,6 +46,7 @@ namespace gridfire {
m_constants(fourdst::constant::Constants::getInstance()){
if (format == NetworkFormat::UNKNOWN) {
LOG_ERROR(m_logger, "nuclearNetwork::Network::Network() called with UNKNOWN format");
m_logger->flush_log();
throw std::runtime_error("nuclearNetwork::Network::Network() called with UNKNOWN format");
}
}
@@ -50,17 +61,12 @@ namespace gridfire {
return oldFormat;
}
reaction::REACLIBLogicalReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition, bool reverse) {
reaction::LogicalReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition, bool reverse) {
using namespace reaction;
std::vector<reaction::REACLIBReaction> reaclibReactions;
std::vector<Reaction> reaclibReactions;
auto logger = fourdst::logging::LogManager::getInstance().getLogger("log");
if (!reaclib::s_initialized) {
LOG_DEBUG(logger, "REACLIB reactions not initialized. Calling initializeAllReaclibReactions()...");
reaclib::initializeAllReaclibReactions();
}
for (const auto &reaction: reaclib::s_all_reaclib_reactions | std::views::values) {
for (const auto &reaction: reaclib::get_all_reactions()) {
if (reaction.is_reverse() != reverse) {
continue; // Skip reactions that do not match the requested direction
}
@@ -77,8 +83,8 @@ namespace gridfire {
reaclibReactions.push_back(reaction);
}
}
const REACLIBReactionSet reactionSet(reaclibReactions);
return REACLIBLogicalReactionSet(reactionSet);
const ReactionSet reactionSet(reaclibReactions);
return LogicalReactionSet(reactionSet);
}
// Trim whitespace from both ends of a string
@@ -97,61 +103,4 @@ namespace gridfire {
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.");
}
}
// 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

@@ -1,3 +1,146 @@
//
// Created by Emily Boudreaux on 6/28/25.
//
#include "fourdst/composition/atomicSpecies.h"
#include "fourdst/composition/species.h"
#include "gridfire/reaction/reaclib.h"
#include "gridfire/reaction/reactions_data.h"
#include "gridfire/network.h"
#include <stdexcept>
#include <sstream>
#include <vector>
#include <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());
}
namespace gridfire::reaclib {
static reaction::LogicalReactionSet* s_all_reaclib_reactions_ptr = nullptr;
#pragma pack(push, 1)
struct ReactionRecord {
int32_t chapter;
double qValue;
double coeffs[7];
bool reverse;
char label[8];
char rpName[64];
char reactants_str[128];
char products_str[128];
};
#pragma pack(pop)
std::ostream& operator<<(std::ostream& os, const ReactionRecord& r) {
os << "Chapter: " << r.chapter
<< ", Q-value: " << r.qValue
<< ", Coefficients: [" << r.coeffs[0] << ", " << r.coeffs[1] << ", "
<< r.coeffs[2] << ", " << r.coeffs[3] << ", " << r.coeffs[4] << ", "
<< r.coeffs[5] << ", " << r.coeffs[6] << "]"
<< ", Reverse: " << (r.reverse ? "true" : "false")
<< ", Label: '" << std::string(r.label, strnlen(r.label, sizeof(r.label))) << "'"
<< ", RP Name: '" << std::string(r.rpName, strnlen(r.rpName, sizeof(r.rpName))) << "'"
<< ", Reactants: '" << std::string(r.reactants_str, strnlen(r.reactants_str, sizeof(r.reactants_str))) << "'"
<< ", Products: '" << std::string(r.products_str, strnlen(r.products_str, sizeof(r.products_str))) << "'";
return os;
}
static std::vector<fourdst::atomic::Species> parseSpeciesString(const std::string_view str) {
std::vector<fourdst::atomic::Species> result;
std::stringstream ss{std::string(str)};
std::string name;
while (ss >> name) {
// Trim whitespace that might be left over from the fixed-width char arrays
const auto trimmed_name = trim_whitespace(name);
if (trimmed_name.empty()) continue;
auto it = fourdst::atomic::species.find(trimmed_name);
if (it != fourdst::atomic::species.end()) {
result.push_back(it->second);
} else {
// If a species is not found, it's a critical data error.
throw std::runtime_error("Unknown species in reaction data: " + std::string(trimmed_name));
}
}
return result;
}
static void initializeAllReaclibReactions() {
if (s_initialized) {
return;
}
// Cast the raw byte data to our structured record format.
const auto* records = reinterpret_cast<const ReactionRecord*>(raw_reactions_data);
const size_t num_reactions = raw_reactions_data_len / sizeof(ReactionRecord);
std::vector<reaction::Reaction> reaction_list;
reaction_list.reserve(num_reactions);
for (size_t i = 0; i < num_reactions; ++i) {
const auto& record = records[i];
// The char arrays from the binary are not guaranteed to be null-terminated
// if the string fills the entire buffer. We create null-terminated string_views.
const std::string_view label_sv(record.label, strnlen(record.label, sizeof(record.label)));
const std::string_view rpName_sv(record.rpName, strnlen(record.rpName, sizeof(record.rpName)));
const std::string_view reactants_sv(record.reactants_str, strnlen(record.reactants_str, sizeof(record.reactants_str)));
const std::string_view products_sv(record.products_str, strnlen(record.products_str, sizeof(record.products_str)));
auto reactants = parseSpeciesString(reactants_sv);
auto products = parseSpeciesString(products_sv);
const reaction::RateCoefficientSet rate_coeffs = {
record.coeffs[0], record.coeffs[1], record.coeffs[2],
record.coeffs[3], record.coeffs[4], record.coeffs[5],
record.coeffs[6]
};
// Construct the Reaction object. We use rpName for both the unique ID and the human-readable name.
reaction_list.emplace_back(
rpName_sv,
rpName_sv,
record.chapter,
reactants,
products,
record.qValue,
label_sv,
rate_coeffs,
record.reverse
);
}
// The ReactionSet takes the vector of all individual reactions.
reaction::ReactionSet reaction_set(std::move(reaction_list));
// The LogicalReactionSet groups reactions by their peName, which is what we want.
s_all_reaclib_reactions_ptr = new reaction::LogicalReactionSet(reaction_set);
s_initialized = true;
}
// --- Public Interface Implementation ---
const reaction::LogicalReactionSet& get_all_reactions() {
// This ensures that the initialization happens only on the first call.
if (!s_initialized) {
initializeAllReaclibReactions();
}
if (s_all_reaclib_reactions_ptr == nullptr) {
throw std::runtime_error("Reaclib reactions have not been initialized.");
}
return *s_all_reaclib_reactions_ptr;
}
} // namespace gridfire::reaclib

View File

@@ -3,9 +3,9 @@
#include<string_view>
#include<string>
#include<vector>
#include<memory>
#include<unordered_set>
#include<algorithm>
#include <ranges>
#include "quill/LogMacros.h"
@@ -18,15 +18,31 @@ namespace gridfire::reaction {
Reaction::Reaction(
const std::string_view id,
const double qValue,
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 RateCoefficientSet& sets,
const bool reverse) :
m_id(id),
m_qValue(qValue),
m_reactants(std::move(reactants)),
m_products(std::move(products)),
m_reverse(reverse) {}
m_id(id),
m_peName(peName),
m_chapter(chapter),
m_qValue(qValue),
m_reactants(reactants),
m_products(products),
m_sourceLabel(label),
m_rateCoefficients(sets),
m_reverse(reverse) {}
double Reaction::calculate_rate(const double T9) const {
return calculate_rate<double>(T9);
}
CppAD::AD<double> Reaction::calculate_rate(const CppAD::AD<double> T9) const {
return calculate_rate<CppAD::AD<double>>(T9);
}
bool Reaction::contains(const Species &species) const {
return contains_reactant(species) || contains_product(species);
@@ -122,27 +138,28 @@ namespace gridfire::reaction {
}
ReactionSet::ReactionSet(
std::vector<std::unique_ptr<Reaction>> reactions) :
m_reactions(std::move(reactions)) {
std::vector<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());
m_id += reaction.id();
m_reactionNameMap.emplace(reaction.id(), reaction);
}
}
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_reactions.push_back(reaction_ptr);
}
m_reactionNameMap.reserve(other.m_reactionNameMap.size());
for (const auto& reaction_ptr : m_reactions) {
m_reactionNameMap.emplace(reaction_ptr->id(), reaction_ptr.get());
m_reactionNameMap.emplace(reaction_ptr.id(), reaction_ptr);
}
}
@@ -155,28 +172,27 @@ namespace gridfire::reaction {
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::add_reaction(Reaction reaction) {
m_reactions.emplace_back(reaction);
m_id += m_reactions.back().id();
m_reactionNameMap.emplace(m_reactions.back().id(), m_reactions.back());
}
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());
void ReactionSet::remove_reaction(const Reaction& reaction) {
if (!m_reactionNameMap.contains(std::string(reaction.id()))) {
return;
}
m_reactionNameMap.erase(std::string(reaction->id()));
m_reactionNameMap.erase(std::string(reaction.id()));
std::erase_if(m_reactions, [&reaction](const std::unique_ptr<Reaction>& r) {
return *r == *reaction;
std::erase_if(m_reactions, [&reaction](const Reaction& r) {
return r == reaction;
});
}
bool ReactionSet::contains(const std::string_view& id) const {
for (const auto& reaction : m_reactions) {
if (reaction->id() == id) {
if (reaction.id() == id) {
return true;
}
}
@@ -185,23 +201,21 @@ namespace gridfire::reaction {
bool ReactionSet::contains(const Reaction& reaction) const {
for (const auto& r : m_reactions) {
if (*r == reaction) {
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);
});
void ReactionSet::clear() {
m_reactions.clear();
m_reactionNameMap.clear();
}
bool ReactionSet::contains_species(const Species& species) const {
for (const auto& reaction : m_reactions) {
if (reaction->contains(species)) {
if (reaction.contains(species)) {
return true;
}
}
@@ -210,7 +224,7 @@ namespace gridfire::reaction {
bool ReactionSet::contains_reactant(const Species& species) const {
for (const auto& r : m_reactions) {
if (r->contains_reactant(species)) {
if (r.contains_reactant(species)) {
return true;
}
}
@@ -219,7 +233,7 @@ namespace gridfire::reaction {
bool ReactionSet::contains_product(const Species& species) const {
for (const auto& r : m_reactions) {
if (r->contains_product(species)) {
if (r.contains_product(species)) {
return true;
}
}
@@ -228,15 +242,17 @@ namespace gridfire::reaction {
const Reaction& ReactionSet::operator[](const size_t index) const {
if (index >= m_reactions.size()) {
m_logger -> flush_log();
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];
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;
return it->second;
}
m_logger -> flush_log();
throw std::out_of_range("Species " + std::string(id) + " does not exist in ReactionSet.");
}
@@ -258,7 +274,7 @@ namespace gridfire::reaction {
std::vector<uint64_t> individualReactionHashes;
individualReactionHashes.reserve(m_reactions.size());
for (const auto& reaction : m_reactions) {
individualReactionHashes.push_back(reaction->hash(seed));
individualReactionHashes.push_back(reaction.hash(seed));
}
std::ranges::sort(individualReactionHashes);
@@ -268,146 +284,82 @@ namespace gridfire::reaction {
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) :
LogicalReaction::LogicalReaction(const std::vector<Reaction>& reactants) :
Reaction(reactants.front().peName(),
reactants.front().qValue(),
reactants.front().peName(),
reactants.front().chapter(),
reactants.front().reactants(),
reactants.front().products(),
reactants.front().is_reverse()),
m_chapter(reactants.front().chapter()) {
reactants.front().qValue(),
reactants.front().sourceLabel(),
reactants.front().rateCoefficients(),
reactants.front().is_reverse()) {
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()) + ".");
if (std::abs(std::abs(reaction.qValue()) - std::abs(m_qValue)) > 1e-6) {
LOG_ERROR(
m_logger,
"LogicalReaction constructed with reactions having different Q-values. Expected {} got {}.",
m_qValue,
reaction.qValue()
);
m_logger -> flush_log();
throw std::runtime_error("LogicalReaction constructed with reactions having different Q-values. Expected " + std::to_string(m_qValue) + " got " + std::to_string(reaction.qValue()) + " (difference : " + std::to_string(std::abs(reaction.qValue() - m_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) {
void LogicalReaction::add_reaction(const Reaction& 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()) + ".");
LOG_ERROR(m_logger, "Cannot add reaction with different peName to LogicalReaction. Expected {} got {}.", m_id, reaction.peName());
m_logger -> flush_log();
throw std::runtime_error("Cannot add reaction with different peName to LogicalReaction. 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.");
LOG_ERROR(m_logger, "Cannot add reaction with duplicate source label {} to LogicalReaction.", reaction.sourceLabel());
m_logger -> flush_log();
throw std::runtime_error("Cannot add reaction with duplicate source label " + std::string(reaction.sourceLabel()) + " to LogicalReaction.");
}
}
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()) + ".");
LOG_ERROR(m_logger, "LogicalReaction constructed with reactions having different Q-values. Expected {} got {}.", m_qValue, reaction.qValue());
m_logger -> flush_log();
throw std::runtime_error("LogicalReaction 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 {
double LogicalReaction::calculate_rate(const double T9) const {
return calculate_rate<double>(T9);
}
CppAD::AD<double> REACLIBLogicalReaction::calculate_rate(const CppAD::AD<double> T9) const {
CppAD::AD<double> LogicalReaction::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>>()) {
LogicalReactionSet::LogicalReactionSet(const ReactionSet &reactionSet) :
ReactionSet(std::vector<Reaction>()) {
std::unordered_map<std::string_view, std::vector<REACLIBReaction>> grouped_reactions;
std::unordered_map<std::string_view, std::vector<Reaction>> 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);
}
for (const auto& reaction : reactionSet) {
grouped_reactions[reaction.peName()].push_back(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());
for (const auto &reactions_for_peName: grouped_reactions | std::views::values) {
LogicalReaction logical_reaction(reactions_for_peName);
m_reactionNameMap.emplace(logical_reaction.id(), logical_reaction);
m_reactions.push_back(std::move(logical_reaction));
}
}
std::unordered_set<std::string> REACLIBLogicalReactionSet::peNames() const {
return m_peNames;
}
}
namespace std {

View File

@@ -21,6 +21,16 @@
namespace gridfire::solver {
NetOut QSENetworkSolver::evaluate(const NetIn &netIn) {
// --- Use the policy to decide whether to update the view ---
if (shouldUpdateView(netIn)) {
LOG_DEBUG(m_logger, "Solver update policy triggered, network view updating...");
m_engine.update(netIn);
LOG_DEBUG(m_logger, "Network view updated!");
m_lastSeenConditions = netIn;
m_isViewInitialized = true;
}
m_engine.generateJacobianMatrix(netIn.MolarAbundance(), netIn.temperature / 1e9, netIn.density);
using state_type = boost::numeric::ublas::vector<double>;
using namespace boost::numeric::odeint;
@@ -124,10 +134,19 @@ namespace gridfire::solver {
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 network_timescale = speciesTimescale.at(species);
const double abundance = Y[i];
if (std::isinf(timescale) || abundance < abundanceCutoff || timescale <= timescaleCutoff) {
double decay_timescale = std::numeric_limits<double>::infinity();
const double half_life = species.halfLife();
if (half_life > 0 && !std::isinf(half_life)) {
constexpr double LN2 = 0.69314718056;
decay_timescale = half_life / LN2;
}
const double final_timescale = std::min(network_timescale, decay_timescale);
if (std::isinf(final_timescale) || abundance < abundanceCutoff || final_timescale <= timescaleCutoff) {
QSESpeciesIndices.push_back(i);
} else {
dynamicSpeciesIndices.push_back(i);
@@ -171,59 +190,13 @@ namespace gridfire::solver {
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();
LOG_TRACE_L1(m_logger, "Calculating steady state abundances for QSE species...");
LOG_WARNING(m_logger, "QSE solver logic not yet implemented, assuming all QSE species have 0 abundance.");
// --- Prepare the QSE species vector ---
Eigen::VectorXd v_qse(indices.QSESpeciesIndices.size());
v_qse.setZero();
return v_qse.array();
}
NetOut QSENetworkSolver::initializeNetworkWithShortIgnition(const NetIn &netIn) const {
@@ -265,6 +238,48 @@ namespace gridfire::solver {
return postIgnition;
}
bool QSENetworkSolver::shouldUpdateView(const NetIn &conditions) const {
// Policy 1: If the view has never been initialized, we must update.
if (!m_isViewInitialized) {
return true;
}
// Policy 2: Check for significant relative change in temperature.
// Reaction rates are exponentially sensitive to temperature, so we use a tight threshold.
const double temp_threshold = m_config.get<double>("gridfire:solver:policy:temp_threshold", 0.05); // 5%
const double temp_relative_change = std::abs(conditions.temperature - m_lastSeenConditions.temperature) / m_lastSeenConditions.temperature;
if (temp_relative_change > temp_threshold) {
LOG_DEBUG(m_logger, "Temperature changed by {:.1f}%, triggering view update.", temp_relative_change * 100);
return true;
}
// Policy 3: Check for significant relative change in density.
const double rho_threshold = m_config.get<double>("gridfire:solver:policy:rho_threshold", 0.10); // 10%
const double rho_relative_change = std::abs(conditions.density - m_lastSeenConditions.density) / m_lastSeenConditions.density;
if (rho_relative_change > rho_threshold) {
LOG_DEBUG(m_logger, "Density changed by {:.1f}%, triggering view update.", rho_relative_change * 100);
return true;
}
// Policy 4: Check for fuel depletion.
// If a primary fuel source changes significantly, the network structure may change.
const double fuel_threshold = m_config.get<double>("gridfire:solver:policy:fuel_threshold", 0.15); // 15%
// Example: Check hydrogen abundance
const double h1_old = m_lastSeenConditions.composition.getMassFraction("H-1");
const double h1_new = conditions.composition.getMassFraction("H-1");
if (h1_old > 1e-12) { // Avoid division by zero
const double h1_relative_change = std::abs(h1_new - h1_old) / h1_old;
if (h1_relative_change > fuel_threshold) {
LOG_DEBUG(m_logger, "H-1 mass fraction changed by {:.1f}%, triggering view update.", h1_relative_change * 100);
return true;
}
}
// If none of the above conditions are met, the current view is still good enough.
return false;
}
void QSENetworkSolver::RHSFunctor::operator()(
const boost::numeric::ublas::vector<double> &YDynamic,
boost::numeric::ublas::vector<double> &dYdtDynamic,
@@ -295,6 +310,7 @@ namespace gridfire::solver {
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();
@@ -363,7 +379,7 @@ namespace gridfire::solver {
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());
std::ranges::copy(dydt, dYdt.begin());
dYdt(m_numSpecies) = eps;
}
@@ -373,10 +389,10 @@ namespace gridfire::solver {
double t,
boost::numeric::ublas::vector<double> &dfdt
) const {
J.resize(m_numSpecies + 1, m_numSpecies + 1);
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) {
for (int i = 0; i < m_numSpecies; ++i) {
for (int j = 0; j < m_numSpecies; ++j) {
J(i, j) = m_engine.getJacobianMatrixEntry(i, j);
}
}