docs(ridfire)

Added more documentation, also moved all engine code into
gridfire::engine namespace to be more in line with other parts of teh
code base
This commit is contained in:
2025-11-24 09:07:49 -05:00
parent 15ed7f70b1
commit 9fab4fbfae
64 changed files with 2506 additions and 848 deletions

View File

@@ -5,17 +5,17 @@
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>
namespace gridfire::diagnostics {
void report_limiting_species(
const DynamicEngine& engine,
const std::vector<double>& Y_full,
const std::vector<double>& E_full,
namespace gridfire::engine::diagnostics {
std::optional<nlohmann::json> report_limiting_species(
const DynamicEngine &engine,
const std::vector<double> &Y_full,
const std::vector<double> &E_full,
const double relTol,
const double absTol,
const size_t top_n
const size_t top_n,
bool json
) {
struct SpeciesError {
std::string name;
@@ -66,15 +66,21 @@ namespace gridfire::diagnostics {
columns.push_back(std::make_unique<utils::Column<double>>("Abundance", sorted_abundances));
columns.push_back(std::make_unique<utils::Column<double>>("Error", sorted_errors));
std::cout << utils::format_table("Timestep Limiting Species", columns) << std::endl;
if (json) {
return utils::to_json(columns);
}
utils::print_table("Timestep Limiting Species", columns);
return std::nullopt;
}
void inspect_species_balance(
std::optional<nlohmann::json> inspect_species_balance(
const DynamicEngine& engine,
const std::string& species_name,
const fourdst::composition::Composition &comp,
const double T9,
const double rho
const double rho,
bool json
) {
const auto& species_obj = fourdst::atomic::species.at(species_name);
@@ -103,12 +109,18 @@ namespace gridfire::diagnostics {
}
}
nlohmann::json j;
{
std::vector<std::unique_ptr<utils::ColumnBase>> columns;
columns.push_back(std::make_unique<utils::Column<std::string>>("Reaction ID", creation_ids));
columns.push_back(std::make_unique<utils::Column<int>>("Stoichiometry", creation_stoichiometry));
columns.push_back(std::make_unique<utils::Column<double>>("Molar Flow", creation_flows));
std::cout << utils::format_table("Creation Reactions for " + species_name, columns) << std::endl;
if (json) {
j["Creation_Reactions_" + species_name] = utils::to_json(columns);
}
else {
utils::print_table("Creation Reactions for " + species_name, columns);
}
}
{
@@ -116,43 +128,44 @@ namespace gridfire::diagnostics {
columns.push_back(std::make_unique<utils::Column<std::string>>("Reaction ID", destruction_ids));
columns.push_back(std::make_unique<utils::Column<int>>("Stoichiometry", destruction_stoichiometry));
columns.push_back(std::make_unique<utils::Column<double>>("Molar Flow", destruction_flows));
std::cout << utils::format_table("Destruction Reactions for " + species_name, columns) << std::endl;
if (json) {
j["Destruction_Reactions_" + species_name] = utils::to_json(columns);
} else {
utils::print_table("Destruction Reactions for " + species_name, columns);
}
}
std::cout << "--- Balance Summary for " << species_name << " ---" << std::endl;
std::cout << " Total Creation Rate: " << std::scientific << total_creation_flow << " [mol/g/s]" << std::endl;
std::cout << " Total Destruction Rate: " << std::scientific << total_destruction_flow << " [mol/g/s]" << std::endl;
std::cout << " Net dY/dt: " << std::scientific << (total_creation_flow - total_destruction_flow) << std::endl;
std::cout << "-----------------------------------" << std::endl;
std::vector<std::unique_ptr<utils::ColumnBase>> summary_columns;
summary_columns.push_back(std::make_unique<utils::Column<std::string>>("Metric", std::vector<std::string>{
"Total Creation Rate [mol/g/s]",
"Total Destruction Rate [mol/g/s]",
"Net dY/dt [mol/g/s]"
}));
summary_columns.push_back(std::make_unique<utils::Column<double>>("Value", std::vector<double>{
total_creation_flow,
total_destruction_flow,
total_creation_flow - total_destruction_flow
}));
if (json) {
j["Species_Balance_Summary_" + species_name] = utils::to_json(summary_columns);
return j;
}
utils::print_table("Species Balance Summary for " + species_name, summary_columns);
return std::nullopt;
}
void inspect_jacobian_stiffness(
std::optional<nlohmann::json> inspect_jacobian_stiffness(
const DynamicEngine &engine,
const fourdst::composition::Composition &comp,
const double T9,
const double rho
) {
inspect_jacobian_stiffness(engine, comp, T9, rho, false, std::nullopt);
}
void inspect_jacobian_stiffness(
const DynamicEngine& engine,
const fourdst::composition::Composition &comp,
const double T9,
const double rho,
const bool save,
const std::optional<std::string> &filename
const bool json
) {
NetworkJacobian jac = engine.generateJacobianMatrix(comp, T9, rho);
jac = regularize_jacobian(jac, comp);
if (save) {
if (!filename.has_value()) {
throw std::invalid_argument("Filename must be provided when save is true.");
}
jac.to_csv(filename.value());
}
const auto& species_list = engine.getNetworkSpecies();
@@ -172,16 +185,28 @@ namespace gridfire::diagnostics {
}
}
std::cout << "\n--- Jacobian Stiffness Report ---" << std::endl;
if (max_diag_species.has_value()) {
std::cout << " Largest Diagonal Element (d(dYi/dt)/dYi): " << std::scientific << max_diag
<< " for species " << max_diag_species->name() << std::endl;
std::vector<std::unique_ptr<utils::ColumnBase>> jacobian_columns;
jacobian_columns.push_back(std::make_unique<utils::Column<std::string>>("Metric", std::vector<std::string>{
"Largest Diagonal Element (d(dYi/dt)/dYi)",
"Largest Off-Diagonal Element (d(dYi/dt)/dYj)"
}));
jacobian_columns.push_back(std::make_unique<utils::Column<double>>("Value", std::vector<double>{
max_diag,
max_off_diag
}));
jacobian_columns.push_back(std::make_unique<utils::Column<std::string>>("Species", std::vector<std::string>{
max_diag_species.has_value() ? std::string(max_diag_species->name()) : "N/A",
max_off_diag_species.has_value() ?
("d(" + std::string(max_off_diag_species->first.name()) + ")/d(" + std::string(max_off_diag_species->second.name()) + ")")
: "N/A"
}));
if (json) {
nlohmann::json j;
j["Jacobian_Stiffness"] = utils::to_json(jacobian_columns);
return j;
}
if (max_off_diag_species.has_value()) {
std::cout << " Largest Off-Diagonal Element (d(dYi/dt)/dYj): " << std::scientific << max_off_diag
<< " for d(" << max_off_diag_species->first.name()
<< ")/d(" << max_off_diag_species->second.name() << ")" << std::endl;
}
std::cout << "---------------------------------" << std::endl;
utils::print_table("Jacobian Stiffness Diagnostics", jacobian_columns);
return std::nullopt;
}
}

View File

@@ -1,6 +1,6 @@
#include "gridfire/engine/engine_graph.h"
#include "gridfire/reaction/reaction.h"
#include "gridfire/network.h"
#include "gridfire/types/types.h"
#include "gridfire/screening/screening_types.h"
#include "gridfire/engine/procedures/priming.h"
#include "gridfire/partition/partition_ground.h"
@@ -32,7 +32,7 @@
#include "cppad/utility/sparse_rcv.hpp"
namespace gridfire {
namespace gridfire::engine {
GraphEngine::GraphEngine(
const fourdst::composition::Composition &composition,
const BuildDepthType buildDepth
@@ -66,7 +66,7 @@ namespace gridfire {
syncInternalMaps();
}
std::expected<StepDerivatives<double>, expectations::StaleEngineError> GraphEngine::calculateRHSAndEnergy(
std::expected<StepDerivatives<double>, EngineStatus> GraphEngine::calculateRHSAndEnergy(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
@@ -74,17 +74,17 @@ namespace gridfire {
return calculateRHSAndEnergy(comp, T9, rho, m_reactions);
}
std::expected<StepDerivatives<double>, expectations::StaleEngineError> GraphEngine::calculateRHSAndEnergy(
std::expected<StepDerivatives<double>, EngineStatus> GraphEngine::calculateRHSAndEnergy(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho,
const reaction::ReactionSet &activeReactions
) const {
LOG_TRACE_L2(m_logger, "Calculating RHS and Energy in GraphEngine at T9 = {}, rho = {}.", T9, rho);
LOG_TRACE_L3(m_logger, "Calculating RHS and Energy in GraphEngine at T9 = {}, rho = {}.", T9, rho);
const double Ye = comp.getElectronAbundance();
const double mue = 0.0; // TODO: Remove
if (m_usePrecomputation) {
LOG_TRACE_L2(m_logger, "Using precomputation for reaction rates in GraphEngine calculateRHSAndEnergy.");
LOG_TRACE_L3(m_logger, "Using precomputation for reaction rates in GraphEngine calculateRHSAndEnergy.");
std::vector<double> bare_rates;
std::vector<double> bare_reverse_rates;
bare_rates.reserve(activeReactions.size());
@@ -98,7 +98,7 @@ namespace gridfire {
}
}
LOG_TRACE_L2(m_logger, "Precomputed {} forward and {} reverse reaction rates for active reactions.", bare_rates.size(), bare_reverse_rates.size());
LOG_TRACE_L3(m_logger, "Precomputed {} forward and {} reverse reaction rates for active reactions.", bare_rates.size(), bare_reverse_rates.size());
// --- The public facing interface can always use the precomputed version since taping is done internally ---
return calculateAllDerivativesUsingPrecomputation(comp, bare_rates, bare_reverse_rates, T9, rho, activeReactions);
@@ -543,6 +543,15 @@ namespace gridfire {
fullNetIn.temperature = netIn.temperature;
fullNetIn.density = netIn.density;
// Short circuit path if already primed
// if (m_has_been_primed) {
// PrimingReport report;
// report.primedComposition = composition;
// report.success = true;
// report.status = PrimingReportStatus::ALREADY_PRIMED;
// return report;
// }
std::optional<std::vector<reaction::ReactionType>> reactionTypesToIgnore = std::nullopt;
if (!m_useReverseReactions) {
reactionTypesToIgnore = {reaction::ReactionType::WEAK};
@@ -550,6 +559,7 @@ namespace gridfire {
auto primingReport = primeNetwork(fullNetIn, *this, reactionTypesToIgnore);
m_has_been_primed = true;
return primingReport;
}
@@ -603,7 +613,7 @@ namespace gridfire {
const double rho,
const reaction::ReactionSet &activeReactions
) const {
LOG_TRACE_L2(m_logger, "Computing screening factors for {} active reactions.", activeReactions.size());
LOG_TRACE_L3(m_logger, "Computing screening factors for {} active reactions.", activeReactions.size());
// --- Calculate screening factors ---
const std::vector<double> screeningFactors = m_screeningModel->calculateScreeningFactors(
activeReactions,
@@ -636,6 +646,11 @@ namespace gridfire {
forwardAbundanceProduct = 0.0;
break; // No need to continue if one of the reactants has zero abundance
}
double factor = std::pow(comp.getMolarAbundance(reactant), power);
if (!std::isfinite(factor)) {
LOG_CRITICAL(m_logger, "Non-finite factor encountered in forward abundance product for reaction '{}'. Check input abundances for validity.", reaction->id());
throw exceptions::BadRHSEngineError("Non-finite factor encountered in forward abundance product.");
}
forwardAbundanceProduct *= std::pow(comp.getMolarAbundance(reactant), power);
}
@@ -652,7 +667,10 @@ namespace gridfire {
precomputedReaction.symmetry_factor *
forwardAbundanceProduct *
std::pow(rho, numReactants > 1 ? static_cast<double>(numReactants) - 1 : 0.0);
if (!std::isfinite(forwardMolarReactionFlow)) {
LOG_CRITICAL(m_logger, "Non-finite forward molar reaction flow computed for reaction '{}'. Check input abundances and rates for validity.", reaction->id());
throw exceptions::BadRHSEngineError("Non-finite forward molar reaction flow computed.");
}
// --- Reverse reaction flow ---
// Only do this is the reaction has a non-zero reverse symmetry factor (i.e. is reversible)
@@ -678,7 +696,7 @@ namespace gridfire {
reactionCounter++;
}
LOG_TRACE_L2(m_logger, "Computed {} molar reaction flows for active reactions. Assembling these into RHS", molarReactionFlows.size());
LOG_TRACE_L3(m_logger, "Computed {} molar reaction flows for active reactions. Assembling these into RHS", molarReactionFlows.size());
// --- Assemble molar abundance derivatives ---
StepDerivatives<double> result;
@@ -860,10 +878,6 @@ namespace gridfire {
for (size_t j = 0; j < numSpecies; ++j) {
double value = dotY[i * (numSpecies + 2) + j];
if (std::abs(value) > MIN_JACOBIAN_THRESHOLD || i == j) { // Always keep diagonal elements to avoid pathological stiffness
if (i == j && value == 0) {
LOG_WARNING(m_logger, "While generating the Jacobian matrix, a zero diagonal element was encountered at index ({}, {}) (species: {}, abundance: {}). This may lead to numerical instability. Setting to -1 to avoid singularity", i, j, m_networkSpecies[i].name(), adInput[i]);
// value = -1.0;
}
triplets.emplace_back(i, j, value);
}
}
@@ -966,7 +980,7 @@ namespace gridfire {
CppAD::sparse_rcv<std::vector<size_t>, std::vector<double>> jac_subset(CppAD_sparsity_pattern);
// PERF: one of *the* most pressing things that needs to be done is remove the nead for this call every
// PERF: one of *the* most pressing things that needs to be done is remove the need for this call every
// time the jacobian is needed since coloring is expensive and we are throwing away the caching
// power of CppAD by clearing the work vector each time. We do this since we make a new subset every
// time. However, a better solution would be to make the subset stateful so it only changes if the requested
@@ -1100,7 +1114,7 @@ namespace gridfire {
LOG_TRACE_L1(m_logger, "Successfully exported network graph to {}", filename);
}
std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> GraphEngine::getSpeciesTimescales(
std::expected<std::unordered_map<fourdst::atomic::Species, double>, EngineStatus> GraphEngine::getSpeciesTimescales(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
@@ -1108,7 +1122,7 @@ namespace gridfire {
return getSpeciesTimescales(comp, T9, rho, m_reactions);
}
std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> GraphEngine::getSpeciesTimescales(
std::expected<std::unordered_map<fourdst::atomic::Species, double>, EngineStatus> GraphEngine::getSpeciesTimescales(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho,
@@ -1144,7 +1158,7 @@ namespace gridfire {
return speciesTimescales;
}
std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> GraphEngine::getSpeciesDestructionTimescales(
std::expected<std::unordered_map<fourdst::atomic::Species, double>, EngineStatus> GraphEngine::getSpeciesDestructionTimescales(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
@@ -1152,7 +1166,7 @@ namespace gridfire {
return getSpeciesDestructionTimescales(comp, T9, rho, m_reactions);
}
std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> GraphEngine::getSpeciesDestructionTimescales(
std::expected<std::unordered_map<fourdst::atomic::Species, double>, EngineStatus> GraphEngine::getSpeciesDestructionTimescales(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho,

View File

@@ -19,6 +19,11 @@
#include "quill/LogMacros.h"
namespace {
// Simple heuristic to check if a reaclib reaction is a strong or weak reaction
/* A weak reaction is defined here as one where:
- The number of reactants is equal to the number of products
- There is only one reactant and one product
- The mass number (A) of the reactant is equal to the mass number (A) of the product
*/
bool reaclib_reaction_is_weak(const gridfire::reaction::Reaction& reaction) {
const std::vector<fourdst::atomic::Species>& reactants = reaction.reactants();
const std::vector<fourdst::atomic::Species>& products = reaction.products();
@@ -41,10 +46,10 @@ namespace {
gridfire::reaction::ReactionSet register_weak_reactions(
const gridfire::rates::weak::WeakRateInterpolator &weakInterpolator,
const gridfire::NetworkConstructionFlags reactionTypes
const gridfire::engine::NetworkConstructionFlags reactionTypes
) {
gridfire::reaction::ReactionSet weak_reaction_pool;
if (!has_flag(reactionTypes, gridfire::NetworkConstructionFlags::WRL_WEAK)) {
if (!has_flag(reactionTypes, gridfire::engine::NetworkConstructionFlags::WRL_WEAK)) {
return weak_reaction_pool;
}
@@ -58,7 +63,7 @@ namespace {
parent_species.z() - 1
);
if (downProduct.has_value()) { // Only add the reaction if the Species map contains the product
if (has_flag(reactionTypes, gridfire::NetworkConstructionFlags::BETA_PLUS)) {
if (has_flag(reactionTypes, gridfire::engine::NetworkConstructionFlags::BETA_PLUS)) {
weak_reaction_pool.add_reaction(
std::make_unique<gridfire::rates::weak::WeakReaction>(
parent_species,
@@ -67,7 +72,7 @@ namespace {
)
);
}
if (has_flag(reactionTypes, gridfire::NetworkConstructionFlags::ELECTRON_CAPTURE)) {
if (has_flag(reactionTypes, gridfire::engine::NetworkConstructionFlags::ELECTRON_CAPTURE)) {
weak_reaction_pool.add_reaction(
std::make_unique<gridfire::rates::weak::WeakReaction>(
parent_species,
@@ -78,7 +83,7 @@ namespace {
}
}
if (upProduct.has_value()) { // Only add the reaction if the Species map contains the product
if (has_flag(reactionTypes, gridfire::NetworkConstructionFlags::BETA_MINUS)) {
if (has_flag(reactionTypes, gridfire::engine::NetworkConstructionFlags::BETA_MINUS)) {
weak_reaction_pool.add_reaction(
std::make_unique<gridfire::rates::weak::WeakReaction>(
parent_species,
@@ -87,7 +92,7 @@ namespace {
)
);
}
if (has_flag(reactionTypes, gridfire::NetworkConstructionFlags::POSITRON_CAPTURE)) {
if (has_flag(reactionTypes, gridfire::engine::NetworkConstructionFlags::POSITRON_CAPTURE)) {
weak_reaction_pool.add_reaction(
std::make_unique<gridfire::rates::weak::WeakReaction>(
parent_species,
@@ -103,14 +108,14 @@ namespace {
}
gridfire::reaction::ReactionSet register_strong_reactions(
const gridfire::NetworkConstructionFlags reaction_types
const gridfire::engine::NetworkConstructionFlags reaction_types
) {
gridfire::reaction::ReactionSet strong_reaction_pool;
if (has_flag(reaction_types, gridfire::NetworkConstructionFlags::STRONG)) {
if (has_flag(reaction_types, gridfire::engine::NetworkConstructionFlags::STRONG)) {
const auto& allReaclibReactions = gridfire::reaclib::get_all_reaclib_reactions();
for (const auto& reaction : allReaclibReactions) {
const bool isWeakReaction = reaclib_reaction_is_weak(*reaction);
const bool okayToUseReaclibWeakReaction = has_flag(reaction_types, gridfire::NetworkConstructionFlags::REACLIB_WEAK);
const bool okayToUseReaclibWeakReaction = has_flag(reaction_types, gridfire::engine::NetworkConstructionFlags::REACLIB_WEAK);
const bool reaclibWeakOkay = !isWeakReaction || okayToUseReaclibWeakReaction;
if (!reaction->is_reverse() && reaclibWeakOkay) {
@@ -121,17 +126,17 @@ namespace {
return strong_reaction_pool;
}
bool validate_unique_weak_set(gridfire::NetworkConstructionFlags flag) {
bool validate_unique_weak_set(gridfire::engine::NetworkConstructionFlags flag) {
// This method ensures that weak reactions will only be fetched from either reaclib or the weak reaction library (WRL)
// but not both
std::array<gridfire::NetworkConstructionFlags, 4> WRL_Flags = {
gridfire::NetworkConstructionFlags::BETA_PLUS,
gridfire::NetworkConstructionFlags::ELECTRON_CAPTURE,
gridfire::NetworkConstructionFlags::POSITRON_CAPTURE,
gridfire::NetworkConstructionFlags::BETA_MINUS
std::array<gridfire::engine::NetworkConstructionFlags, 4> WRL_Flags = {
gridfire::engine::NetworkConstructionFlags::BETA_PLUS,
gridfire::engine::NetworkConstructionFlags::ELECTRON_CAPTURE,
gridfire::engine::NetworkConstructionFlags::POSITRON_CAPTURE,
gridfire::engine::NetworkConstructionFlags::BETA_MINUS
};
if (!has_flag(flag, gridfire::NetworkConstructionFlags::REACLIB_WEAK)) {
if (!has_flag(flag, gridfire::engine::NetworkConstructionFlags::REACLIB_WEAK)) {
return true;
}
for (const auto& WRLReactionType : WRL_Flags) {
@@ -143,7 +148,7 @@ namespace {
}
}
namespace gridfire {
namespace gridfire::engine {
using reaction::ReactionSet;
using reaction::Reaction;
using fourdst::atomic::Species;

View File

@@ -6,7 +6,7 @@
#include "gridfire/solver/solver.h"
#include "gridfire/engine/engine_abstract.h"
#include "gridfire/network.h"
#include "gridfire/types/types.h"
#include "gridfire/exceptions/error_solver.h"
#include "fourdst/logging/logging.h"
@@ -15,7 +15,7 @@
#include "quill/LogMacros.h"
namespace gridfire {
namespace gridfire::engine {
using fourdst::composition::Composition;
using fourdst::atomic::Species;
@@ -26,6 +26,11 @@ namespace gridfire {
) {
const auto logger = LogManager::getInstance().getLogger("log");
solver::CVODESolverStrategy integrator(engine);
// Do not need high precision for priming
integrator.set_absTol(1e-3);
integrator.set_relTol(1e-3);
integrator.set_stdout_logging_enabled(false);
NetIn solverInput(netIn);

View File

@@ -10,7 +10,7 @@
#include "quill/LogMacros.h"
namespace gridfire {
namespace gridfire::engine {
NetworkJacobian::NetworkJacobian(
const Eigen::SparseMatrix<double>& jacobianMatrix,
const std::function<fourdst::atomic::Species(size_t)> &indexToSpeciesFunc

View File

@@ -5,13 +5,13 @@
#include <algorithm>
#include "gridfire/network.h"
#include "gridfire/types/types.h"
#include "gridfire/exceptions/error_engine.h"
#include "quill/LogMacros.h"
#include "quill/Logger.h"
namespace gridfire {
namespace gridfire::engine {
using fourdst::atomic::Species;
AdaptiveEngineView::AdaptiveEngineView(
DynamicEngine &baseEngine
@@ -77,7 +77,7 @@ namespace gridfire {
return m_activeSpecies;
}
std::expected<StepDerivatives<double>, expectations::StaleEngineError> AdaptiveEngineView::calculateRHSAndEnergy(
std::expected<StepDerivatives<double>, EngineStatus> AdaptiveEngineView::calculateRHSAndEnergy(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
@@ -205,7 +205,7 @@ namespace gridfire {
throw exceptions::UnableToSetNetworkReactionsError("AdaptiveEngineView does not support setting network reactions directly. Use update() with NetIn instead. Perhaps you meant to call this on the base engine?");
}
std::expected<std::unordered_map<Species, double>, expectations::StaleEngineError> AdaptiveEngineView::getSpeciesTimescales(
std::expected<std::unordered_map<Species, double>, EngineStatus> AdaptiveEngineView::getSpeciesTimescales(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
@@ -231,8 +231,7 @@ namespace gridfire {
}
std::expected<std::unordered_map<Species, double>, expectations::StaleEngineError>
AdaptiveEngineView::getSpeciesDestructionTimescales(
std::expected<std::unordered_map<Species, double>, EngineStatus> AdaptiveEngineView::getSpeciesDestructionTimescales(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
@@ -357,7 +356,7 @@ namespace gridfire {
if (!reachable.contains(species)) {
to_vist.push(species);
reachable.insert(species);
LOG_TRACE_L2(m_logger, "Network Connectivity Analysis: Species '{}' is part of the initial fuel.", species.name());
LOG_TRACE_L2(m_logger, "Network Connectivity Analysis: Species {:5} is part of the initial fuel", species.name());
}
}
}
@@ -378,7 +377,7 @@ namespace gridfire {
if (!reachable.contains(product)) {
reachable.insert(product);
new_species_found_in_pass = true;
LOG_TRACE_L2(m_logger, "Network Connectivity Analysis: Species '{}' is reachable via reaction '{}'.", product.name(), reaction->id());
LOG_TRACE_L2(m_logger, "Network Connectivity Analysis: Species {:5} is reachable via reaction {:20}", product.name(), reaction->id());
}
}
}
@@ -397,19 +396,19 @@ namespace gridfire {
LOG_TRACE_L1(m_logger, "Culling reactions based on flow rates...");
const auto relative_culling_threshold = m_config.get<double>("gridfire:AdaptiveEngineView:RelativeCullingThreshold", 1e-75);
double absoluteCullingThreshold = relative_culling_threshold * maxFlow;
LOG_DEBUG(m_logger, "Relative culling threshold: {:0.3E} ({})", relative_culling_threshold, absoluteCullingThreshold);
LOG_DEBUG(m_logger, "Relative culling threshold: {:7.3E} ({:7.3E})", relative_culling_threshold, absoluteCullingThreshold);
std::vector<const reaction::Reaction*> culledReactions;
for (const auto& [reactionPtr, flowRate]: allFlows) {
bool keepReaction = false;
if (flowRate > absoluteCullingThreshold) {
LOG_TRACE_L2(m_logger, "Maintaining reaction '{}' with relative (abs) flow rate: {:0.3E} ({:0.3E} [mol/s])", reactionPtr->id(), flowRate/maxFlow, flowRate);
LOG_TRACE_L2(m_logger, "Maintaining reaction '{:20}' with relative (abs) flow rate: {:7.3E} ({:7.3E} [mol/s])", reactionPtr->id(), flowRate/maxFlow, flowRate);
keepReaction = true;
} else {
bool zero_flow_due_to_reachable_reactants = false;
if (flowRate < 1e-99 && flowRate > 0.0) {
for (const auto& reactant: reactionPtr->reactants()) {
if (comp.getMolarAbundance(reactant) < 1e-99 && reachableSpecies.contains(reactant)) {
LOG_TRACE_L1(m_logger, "Maintaining reaction '{}' with low flow ({:0.3E} [mol/s/g]) due to reachable reactant '{}'.", reactionPtr->id(), flowRate, reactant.name());
LOG_TRACE_L1(m_logger, "Maintaining reaction {:20} with low flow ({:7.3E} [mol/s/g]) due to reachable reactant '{:6}'.", reactionPtr->id(), flowRate, reactant.name());
zero_flow_due_to_reachable_reactants = true;
break;
}
@@ -422,10 +421,10 @@ namespace gridfire {
if (keepReaction) {
culledReactions.push_back(reactionPtr);
} else {
LOG_TRACE_L1(m_logger, "Culling reaction '{}' due to low flow rate or lack of connectivity.", reactionPtr->id());
LOG_TRACE_L1(m_logger, "Culling reaction '{:20}' due to low flow rate or lack of connectivity.", reactionPtr->id());
}
}
LOG_DEBUG(m_logger, "Selected {} (total: {}, culled: {}) reactions based on flow rates.", culledReactions.size(), allFlows.size(), allFlows.size() - culledReactions.size());
LOG_DEBUG(m_logger, "Selected {:5} (total: {:5}, culled: {:5}) reactions based on flow rates.", culledReactions.size(), allFlows.size(), allFlows.size() - culledReactions.size());
return culledReactions;
}
@@ -438,8 +437,9 @@ namespace gridfire {
) const {
const auto result = m_baseEngine.getSpeciesTimescales(comp, T9, rho);
if (!result) {
LOG_ERROR(m_logger, "Failed to get species timescales due to stale engine state.");
throw exceptions::StaleEngineError("Failed to get species timescales");
LOG_CRITICAL(m_logger, "Failed to get species timescales due to base engine failure");
m_logger->flush_log();
throw exceptions::EngineError("Failed to get species timescales due base engine failure");
}
std::unordered_map<Species, double> timescales = result.value();
std::set<Species> onlyProducedSpecies;

View File

@@ -17,7 +17,7 @@
#include "fourdst/composition/exceptions/exceptions_composition.h"
namespace gridfire {
namespace gridfire::engine {
using fourdst::atomic::Species;
DefinedEngineView::DefinedEngineView(
@@ -40,7 +40,7 @@ namespace gridfire {
return m_activeSpeciesVectorCache.value();
}
std::expected<StepDerivatives<double>, expectations::StaleEngineError> DefinedEngineView::calculateRHSAndEnergy(
std::expected<StepDerivatives<double>, EngineStatus> DefinedEngineView::calculateRHSAndEnergy(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
@@ -170,7 +170,7 @@ namespace gridfire {
m_activeSpeciesVectorCache = std::nullopt; // Invalidate species vector cache
}
std::expected<std::unordered_map<Species, double>, expectations::StaleEngineError> DefinedEngineView::getSpeciesTimescales(
std::expected<std::unordered_map<Species, double>, EngineStatus> DefinedEngineView::getSpeciesTimescales(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
@@ -193,7 +193,7 @@ namespace gridfire {
return definedTimescales;
}
std::expected<std::unordered_map<Species, double>, expectations::StaleEngineError> DefinedEngineView::getSpeciesDestructionTimescales(
std::expected<std::unordered_map<Species, double>, EngineStatus> DefinedEngineView::getSpeciesDestructionTimescales(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@
#include <unordered_map>
namespace gridfire {
namespace gridfire::engine {
using fourdst::atomic::species;
NetworkPrimingEngineView::NetworkPrimingEngineView(

View File

@@ -137,7 +137,7 @@ namespace gridfire::io::gen {
}
std::string exportEngineToPy(const gridfire::DynamicEngine& engine) {
std::string exportEngineToPy(const engine::DynamicEngine& engine) {
auto reactions = engine.getNetworkReactions();
std::vector<std::string> functions;
functions.emplace_back(R"(import numpy as np
@@ -150,7 +150,7 @@ from typing import Dict, List, Tuple, Callable)");
return join<std::string>(functions, "\n\n");
}
void exportEngineToPy(const DynamicEngine &engine, const std::string &fileName) {
void exportEngineToPy(const engine::DynamicEngine &engine, const std::string &fileName) {
const std::string funcCode = exportEngineToPy(engine);
std::ofstream outFile(fileName);
outFile << funcCode;

View File

@@ -47,22 +47,22 @@ namespace gridfire::policy {
m_partition_function = build_partition_function();
}
DynamicEngine& MainSequencePolicy::construct() {
engine::DynamicEngine& MainSequencePolicy::construct() {
m_network_stack.clear();
m_network_stack.emplace_back(
std::make_unique<GraphEngine>(m_initializing_composition, *m_partition_function, NetworkBuildDepth::ThirdOrder, NetworkConstructionFlags::DEFAULT)
std::make_unique<engine::GraphEngine>(m_initializing_composition, *m_partition_function, engine::NetworkBuildDepth::ThirdOrder, engine::NetworkConstructionFlags::DEFAULT)
);
auto& graphRepr = dynamic_cast<GraphEngine&>(*m_network_stack.back().get());
auto& graphRepr = dynamic_cast<engine::GraphEngine&>(*m_network_stack.back().get());
graphRepr.setUseReverseReactions(false);
m_network_stack.emplace_back(
std::make_unique<MultiscalePartitioningEngineView>(*m_network_stack.back().get())
std::make_unique<engine::MultiscalePartitioningEngineView>(*m_network_stack.back().get())
);
m_network_stack.emplace_back(
std::make_unique<AdaptiveEngineView>(*m_network_stack.back().get())
std::make_unique<engine::AdaptiveEngineView>(*m_network_stack.back().get())
);
m_status = NetworkPolicyStatus::INITIALIZED_UNVERIFIED;
@@ -96,18 +96,18 @@ namespace gridfire::policy {
return m_status;
}
const std::vector<std::unique_ptr<DynamicEngine>> &MainSequencePolicy::get_engine_stack() const {
const std::vector<std::unique_ptr<engine::DynamicEngine>> &MainSequencePolicy::get_engine_stack() const {
if (m_status != NetworkPolicyStatus::INITIALIZED_VERIFIED) {
throw exceptions::PolicyError("Cannot get engine stack from MainSequencePolicy: Policy is not initialized and verified. Call construct() first.");
}
return m_network_stack;
}
std::vector<EngineTypes> MainSequencePolicy::get_engine_types_stack() const {
std::vector<engine::EngineTypes> MainSequencePolicy::get_engine_types_stack() const {
return {
EngineTypes::GRAPH_ENGINE,
EngineTypes::MULTISCALE_PARTITIONING_ENGINE_VIEW,
EngineTypes::ADAPTIVE_ENGINE_VIEW
engine::EngineTypes::GRAPH_ENGINE,
engine::EngineTypes::MULTISCALE_PARTITIONING_ENGINE_VIEW,
engine::EngineTypes::ADAPTIVE_ENGINE_VIEW
};
}

View File

@@ -3,7 +3,7 @@
#include "gridfire/reaction/reaclib.h"
#include "gridfire/reaction/reactions_data.h"
#include "gridfire/network.h"
#include "gridfire/types/types.h"
#include "gridfire/exceptions/error_reaction.h"
#include <stdexcept>
@@ -11,7 +11,6 @@
#include <vector>
#include <string>
#include <format>
#include <print>
#include <expected>
namespace {
@@ -38,7 +37,7 @@ namespace {
BadFormat // Generic error (e.g., from an out_of_range exception)
};
std::string error_to_string(ReactionParseError err) {
std::string error_to_string(const ReactionParseError err) {
switch (err) {
case ReactionParseError::MissingOpenParenthesis:
return "Missing '('";

View File

@@ -1,6 +1,6 @@
#include "gridfire/solver/strategies/CVODE_solver_strategy.h"
#include "gridfire/network.h"
#include "gridfire/types/types.h"
#include "gridfire/utils/table_format.h"
#include "gridfire/engine/diagnostics/dynamic_engine_diagnostics.h"
@@ -19,6 +19,7 @@
#include "fourdst/atomic/species.h"
#include "fourdst/composition/exceptions/exceptions_composition.h"
#include "gridfire/engine/engine_graph.h"
#include "gridfire/engine/types/engine_types.h"
#include "gridfire/solver/strategies/triggers/engine_partitioning_trigger.h"
#include "gridfire/trigger/procedures/trigger_pprint.h"
#include "gridfire/exceptions/error_solver.h"
@@ -26,6 +27,7 @@
namespace gridfire::solver {
using namespace gridfire::engine;
CVODESolverStrategy::TimestepContext::TimestepContext(
const double t,
@@ -99,14 +101,26 @@ namespace gridfire::solver {
) {
LOG_TRACE_L1(m_logger, "Starting solver evaluation with T9: {} and rho: {}", netIn.temperature/1e9, netIn.density);
LOG_TRACE_L1(m_logger, "Building engine update trigger....");
auto trigger = trigger::solver::CVODE::makeEnginePartitioningTrigger(1e12, 1e10, 1e-6, 10);
auto trigger = trigger::solver::CVODE::makeEnginePartitioningTrigger(1e12, 1e10, 0.5, 2);
LOG_TRACE_L1(m_logger, "Engine update trigger built!");
const double T9 = netIn.temperature / 1e9; // Convert temperature from Kelvin to T9 (T9 = T / 1e9)
const auto absTol = m_config.get<double>("gridfire:solver:CVODESolverStrategy:absTol", 1.0e-8);
const auto relTol = m_config.get<double>("gridfire:solver:CVODESolverStrategy:relTol", 1.0e-5);
// The tolerance selection algorithm is that
// 1. Default tolerances are taken from the config file
// 2. If the user has set tolerances in code, those override the config
// 3. If the user has not set tolerances in code and the config does not have them, use hardcoded defaults
auto absTol = m_config.get<double>("gridfire:solver:CVODESolverStrategy:absTol", 1.0e-8);
auto relTol = m_config.get<double>("gridfire:solver:CVODESolverStrategy:relTol", 1.0e-5);
if (m_absTol) {
absTol = *m_absTol;
}
if (m_relTol) {
relTol = *m_relTol;
}
LOG_TRACE_L1(m_logger, "Starting engine update chain...");
fourdst::composition::Composition equilibratedComposition = m_engine.update(netIn);
@@ -143,6 +157,7 @@ namespace gridfire::solver {
size_t total_steps = 0;
LOG_TRACE_L1(m_logger, "Starting CVODE iteration");
fourdst::composition::Composition postStep = equilibratedComposition;
while (current_time < netIn.tMax) {
user_data.T9 = T9;
user_data.rho = netIn.density;
@@ -156,13 +171,10 @@ namespace gridfire::solver {
LOG_TRACE_L2(m_logger, "CVODE step complete. Current time: {}, step status: {}", current_time, utils::cvode_ret_code_map.at(flag));
if (user_data.captured_exception){
LOG_CRITICAL(m_logger, "An exception was captured during RHS evaluation ({}). Rethrowing...", user_data.captured_exception->what());
std::rethrow_exception(std::make_exception_ptr(*user_data.captured_exception));
}
// TODO: Come up with some way to save these to a file rather than spamming stdout. JSON maybe? OPAT?
// log_step_diagnostics(user_data, true, false, false);
// exit(0);
utils::check_cvode_flag(flag, "CVode");
long int n_steps;
@@ -178,21 +190,41 @@ namespace gridfire::solver {
size_t iter_diff = (total_nonlinear_iterations + nliters) - prev_nonlinear_iterations;
size_t convFail_diff = (total_convergence_failures + nlcfails) - prev_convergence_failures;
if (m_stdout_logging_enabled) {
std::cout << std::scientific << std::setprecision(3)
<< "Step: " << std::setw(6) << total_steps + n_steps
<< " | Updates: " << std::setw(3) << total_update_stages_triggered
<< " | Epoch Steps: " << std::setw(4) << n_steps
<< " | t: " << current_time << " [s]"
<< " | dt: " << last_step_size << " [s]"
// << " | Molar Abundance (min a): " << y_data[0] << " [mol/g]"
// << " | Accumulated Energy: " << current_energy << " [erg/g]"
<< " | Iterations: " << std::setw(6) << total_nonlinear_iterations + nliters
<< " (+" << std::setw(2) << iter_diff << ")"
<< " | Total Convergence Failures: " << std::setw(2) << total_convergence_failures + nlcfails
<< " (+" << std::setw(2) << convFail_diff << ")"
<< "\n";
std::println(
"Step: {:6} | Updates: {:3} | Epoch Steps: {:4} | t: {:.3e} [s] | dt: {:15.6E} [s] | Iterations: {:6} (+{:2}) | Total Convergence Failures: {:2} (+{:2})",
total_steps + n_steps,
total_update_stages_triggered,
n_steps,
current_time,
last_step_size,
total_nonlinear_iterations + nliters,
iter_diff,
total_convergence_failures + nlcfails,
convFail_diff
);
}
LOG_INFO(m_logger, "Completed {} steps to time {} [s] (dt = {} [s]). Current specific energy: {} [erg/g]", n_steps, current_time, last_step_size, current_energy);
for (size_t i = 0; i < numSpecies; ++i) {
const auto& species = m_engine.getNetworkSpecies()[i];
if (y_data[i] > 0.0) {
postStep.setMolarAbundance(species, y_data[i]);
}
}
fourdst::composition::Composition collectedComposition = m_engine.collectComposition(postStep, netIn.temperature/1e9, netIn.density);
for (size_t i = 0; i < numSpecies; ++i) {
y_data[i] = collectedComposition.getMolarAbundance(m_engine.getNetworkSpecies()[i]);
}
LOG_INFO(m_logger, "Completed {:5} steps to time {:10.4E} [s] (dt = {:15.6E} [s]). Current specific energy: {:15.6E} [erg/g]", total_steps + n_steps, current_time, last_step_size, current_energy);
LOG_DEBUG(m_logger, "Current composition (molar abundance): {}", [&]() -> std::string {
std::stringstream ss;
for (size_t i = 0; i < numSpecies; ++i) {
const auto& species = m_engine.getNetworkSpecies()[i];
ss << species.name() << ": (y_data = " << y_data[i] << ", collected = " << collectedComposition.getMolarAbundance(species) << ")";
if (i < numSpecies - 1) {
ss << ", ";
}
}
return ss.str();
}());
static const std::map<fourdst::atomic::Species,
std::unordered_map<std::string,double>> kEmptyMap{};
@@ -224,8 +256,9 @@ namespace gridfire::solver {
}
trigger->step(ctx);
// log_step_diagnostics(user_data, true, true, false);
// exit(0);
if (m_detailed_step_logging) {
log_step_diagnostics(user_data, true, true, true, "step_" + std::to_string(total_steps + n_steps) + ".json");
}
if (trigger->check(ctx)) {
if (m_stdout_logging_enabled && displayTrigger) {
@@ -389,6 +422,7 @@ namespace gridfire::solver {
numSpecies = m_engine.getNetworkSpecies().size();
N = numSpecies + 1;
LOG_INFO(m_logger, "Starting CVODE reinitialization after engine update...");
cleanup_cvode_resources(true);
m_cvode_mem = CVodeCreate(CV_BDF, m_sun_ctx);
@@ -397,6 +431,8 @@ namespace gridfire::solver {
initialize_cvode_integration_resources(N, numSpecies, current_time, currentComposition, absTol, relTol, accumulated_energy);
utils::check_cvode_flag(CVodeReInit(m_cvode_mem, current_time, m_Y), "CVodeReInit");
// throw exceptions::DebugException("Debug");
LOG_INFO(m_logger, "Done reinitializing CVODE after engine update. The next log messages will be from the first step after reinitialization...");
}
}
@@ -405,7 +441,7 @@ namespace gridfire::solver {
std::cout << std::flush;
}
LOG_TRACE_L2(m_logger, "CVODE iteration complete");
LOG_INFO(m_logger, "CVODE iteration complete");
sunrealtype* y_data = N_VGetArrayPointer(m_Y);
accumulated_energy += y_data[numSpecies];
@@ -417,16 +453,41 @@ namespace gridfire::solver {
}
}
LOG_TRACE_L2(m_logger, "Constructing final composition= with {} species", numSpecies);
LOG_INFO(m_logger, "Constructing final composition= with {} species", numSpecies);
fourdst::composition::Composition topLevelComposition(m_engine.getNetworkSpecies(), y_vec);
LOG_INFO(m_logger, "Final composition constructed from solver state successfully! ({})", [&topLevelComposition]() -> std::string {
std::ostringstream ss;
size_t i = 0;
for (const auto& [species, abundance] : topLevelComposition) {
ss << species.name() << ": " << abundance;
if (i < topLevelComposition.size() - 1) {
ss << ", ";
}
++i;
}
return ss.str();
}());
LOG_INFO(m_logger, "Collecting final composition...");
fourdst::composition::Composition outputComposition = m_engine.collectComposition(topLevelComposition, netIn.temperature/1e9, netIn.density);
assert(outputComposition.getRegisteredSymbols().size() == equilibratedComposition.getRegisteredSymbols().size());
LOG_TRACE_L2(m_logger, "Final composition constructed successfully!");
LOG_INFO(m_logger, "Final composition constructed successfully! ({})", [&outputComposition]() -> std::string {
std::ostringstream ss;
size_t i = 0;
for (const auto& [species, abundance] : outputComposition) {
ss << species.name() << ": " << abundance;
if (i < outputComposition.size() - 1) {
ss << ", ";
}
++i;
}
return ss.str();
}());
LOG_TRACE_L2(m_logger, "Constructing output data...");
LOG_INFO(m_logger, "Constructing output data...");
NetOut netOut;
netOut.composition = outputComposition;
netOut.energy = accumulated_energy;
@@ -461,6 +522,30 @@ namespace gridfire::solver {
m_stdout_logging_enabled = logging_enabled;
}
void CVODESolverStrategy::set_absTol(double absTol) {
m_absTol = absTol;
}
void CVODESolverStrategy::set_relTol(double relTol) {
m_relTol = relTol;
}
double CVODESolverStrategy::get_absTol() const {
if (m_absTol.has_value()) {
return m_absTol.value();
} else {
return -1.0;
}
}
double CVODESolverStrategy::get_relTol() const {
if (m_relTol.has_value()) {
return m_relTol.value();
} else {
return -1.0;
}
}
std::vector<std::tuple<std::string, std::string>> CVODESolverStrategy::describe_callback_context() const {
return {};
}
@@ -476,13 +561,13 @@ namespace gridfire::solver {
try {
LOG_TRACE_L2(instance->m_logger, "CVODE RHS wrapper called at time {}", t);
const CVODERHSOutputData out = instance->calculate_rhs(t, y, ydot, data);
data->reaction_contribution_map = out.reaction_contribution_map;
const auto [reaction_contribution_map] = instance->calculate_rhs(t, y, ydot, data);
data->reaction_contribution_map = reaction_contribution_map;
LOG_TRACE_L2(instance->m_logger, "CVODE RHS wrapper completed successfully at time {}", t);
return 0;
} catch (const exceptions::StaleEngineTrigger& e) {
LOG_ERROR(instance->m_logger, "StaleEngineTrigger caught in CVODE RHS wrapper at time {}: {}", t, e.what());
data->captured_exception = std::make_unique<exceptions::StaleEngineTrigger>(e);
} catch (const exceptions::EngineError& e) {
LOG_ERROR(instance->m_logger, "EngineError caught in CVODE RHS wrapper at time {}: {}", t, e.what());
data->captured_exception = std::make_unique<exceptions::EngineError>(e);
return 1; // 1 Indicates a recoverable error, CVODE will retry the step
} catch (...) {
LOG_CRITICAL(instance->m_logger, "Unrecoverable and Unknown exception caught in CVODE RHS wrapper at time {}", t);
@@ -634,8 +719,8 @@ namespace gridfire::solver {
LOG_TRACE_L2(m_logger, "Calculating RHS at time {} with {} species in composition", t, composition.size());
const auto result = m_engine.calculateRHSAndEnergy(composition, data->T9, data->rho);
if (!result) {
LOG_WARNING(m_logger, "StaleEngineTrigger thrown during RHS calculation at time {}", t);
throw exceptions::StaleEngineTrigger({data->T9, data->rho, y_vec, t, m_num_steps, y_data[numSpecies]});
LOG_CRITICAL(m_logger, "Failed to calculate RHS at time {}: {}", t, EngineStatus_to_string(result.error()));
throw exceptions::BadRHSEngineError(std::format("Failed to calculate RHS at time {}: {}", t, EngineStatus_to_string(result.error())));
}
sunrealtype* ydot_data = N_VGetArrayPointer(ydot);
@@ -746,12 +831,21 @@ namespace gridfire::solver {
LOG_TRACE_L2(m_logger, "Done Cleaning up cvode resources");
}
void CVODESolverStrategy::set_detailed_step_logging(const bool enabled) {
m_detailed_step_logging = enabled;
}
void CVODESolverStrategy::log_step_diagnostics(
const CVODEUserData &user_data,
bool displayJacobianStiffness,
bool saveIntermediateJacobians,
bool displaySpeciesBalance
bool displaySpeciesBalance,
bool to_file,
std::optional<std::string> filename
) const {
if (to_file && !filename.has_value()) {
LOG_ERROR(m_logger, "Filename must be provided when logging diagnostics to file.");
throw exceptions::UtilityError("Filename must be provided when logging diagnostics to file.");
}
// --- 1. Get CVODE Step Statistics ---
sunrealtype hlast, hcur, tcur;
@@ -762,6 +856,7 @@ namespace gridfire::solver {
utils::check_cvode_flag(CVodeGetLastOrder(m_cvode_mem, &qlast), "CVodeGetLastOrder");
utils::check_cvode_flag(CVodeGetCurrentTime(m_cvode_mem, &tcur), "CVodeGetCurrentTime");
nlohmann::json j;
{
std::vector<std::string> labels = {"Current Time (tcur)", "Last Step (hlast)", "Current Step (hcur)", "Last Order (qlast)"};
std::vector<double> values = {static_cast<double>(tcur), static_cast<double>(hlast), static_cast<double>(hcur), static_cast<double>(qlast)};
@@ -770,7 +865,11 @@ namespace gridfire::solver {
columns.push_back(std::make_unique<utils::Column<std::string>>("Statistic", labels));
columns.push_back(std::make_unique<utils::Column<double>>("Value", values));
std::cout << utils::format_table("CVODE Step Stats", columns) << std::endl;
if (to_file) {
j["CVODE_Step_Stats"] = utils::to_json(columns);
} else {
utils::print_table("CVODE Step Stats", columns);
}
}
// --- 2. Get CVODE Cumulative Solver Statistics ---
@@ -803,7 +902,11 @@ namespace gridfire::solver {
columns.push_back(std::make_unique<utils::Column<std::string>>("Counter", labels));
columns.push_back(std::make_unique<utils::Column<long int>>("Count", values));
std::cout << utils::format_table("CVODE Cumulative Stats", columns) << std::endl;
if (to_file) {
j["CVODE_Cumulative_Stats"] = utils::to_json(columns);
} else {
utils::print_table("CVODE Cumulative Stats", columns);
}
}
// --- 3. Get Estimated Local Errors (Your Original Logic) ---
@@ -822,7 +925,10 @@ namespace gridfire::solver {
std::vector<double> Y_full(y_data, y_data + num_components - 1);
std::vector<double> E_full(y_err_data, y_err_data + num_components - 1);
diagnostics::report_limiting_species(*user_data.engine, Y_full, E_full, relTol, absTol);
auto result = diagnostics::report_limiting_species(*user_data.engine, Y_full, E_full, relTol, absTol, 10, to_file);
if (to_file && result.has_value()) {
j["Limiting_Species"] = result.value();
}
std::ranges::replace_if(
Y_full,
@@ -880,7 +986,11 @@ namespace gridfire::solver {
table.push_back(std::make_unique<utils::Column<double>>(destructionTimescaleColumn));
table.push_back(std::make_unique<utils::Column<std::string>>(speciesStatusColumn));
utils::print_table("Species Timescales", table);
if (to_file) {
j["Species_Timescales"] = utils::to_json(table);
} else {
utils::print_table("Species Timescales", table);
}
}
@@ -917,29 +1027,41 @@ namespace gridfire::solver {
columns.push_back(std::make_unique<utils::Column<fourdst::atomic::Species>>("Species", sorted_species));
columns.push_back(std::make_unique<utils::Column<double>>("Error Ratio", sorted_err_ratios));
std::cout << utils::format_table("Species Error Ratios (Log)", columns) << std::endl;
if (to_file) {
j["Species_Error_Ratios_Linear"] = utils::to_json(columns);
} else {
utils::print_table("Species Error Ratios (Linear)", columns);
}
}
// --- 4. Call Your Jacobian and Balance Diagnostics ---
if (displayJacobianStiffness) {
std::cout << "--- Starting Jacobian Diagnostics ---" << std::endl;
std::optional<std::string> filename;
if (saveIntermediateJacobians) {
filename = std::format("jacobian_s{}.csv", nsteps);
auto jStiff = diagnostics::inspect_jacobian_stiffness(*user_data.engine, composition, user_data.T9, user_data.rho, to_file);
if (to_file && jStiff.has_value()) {
j["Jacobian_Stiffness_Diagnostics"] = jStiff.value();
}
diagnostics::inspect_jacobian_stiffness(*user_data.engine, composition, user_data.T9, user_data.rho, saveIntermediateJacobians, filename);
std::cout << "--- Finished Jacobian Diagnostics ---" << std::endl;
}
if (displaySpeciesBalance) {
std::cout << "--- Starting Species Balance Diagnostics ---" << std::endl;
// Limit this to the top N species to avoid spamming
const size_t num_species_to_inspect = std::min(sorted_species.size(), static_cast<size_t>(5));
std::cout << " > Inspecting balance for top " << num_species_to_inspect << " species with highest error ratio:" << std::endl;
for (size_t i = 0; i < num_species_to_inspect; ++i) {
const auto& species = sorted_species[i];
diagnostics::inspect_species_balance(*user_data.engine, std::string(species.name()), composition, user_data.T9, user_data.rho);
auto sbr = diagnostics::inspect_species_balance(*user_data.engine, std::string(species.name()), composition, user_data.T9, user_data.rho, to_file);
if (to_file && sbr.has_value()) {
j[std::string("Species_Balance_Diagnostics_") + species.name().data()] = sbr.value();
}
}
std::cout << "--- Finished Species Balance Diagnostics ---" << std::endl;
}
if (to_file) {
std::ofstream ofs(filename.value());
if (!ofs.is_open()) {
LOG_ERROR(m_logger, "Failed to open file {} for writing diagnostics.", filename.value());
throw exceptions::UtilityError(std::format("Failed to open file {} for writing diagnostics.", filename.value()));
}
ofs << j.dump(4);
ofs.close();
LOG_TRACE_L2(m_logger, "Diagnostics written to file {}", filename.value());
}
}

View File

@@ -203,6 +203,7 @@ namespace gridfire::trigger::solver::CVODE {
void TimestepCollapseTrigger::update(const gridfire::solver::CVODESolverStrategy::TimestepContext &ctx) {
m_updates++;
m_timestep_window.clear();
}
void TimestepCollapseTrigger::step(
@@ -279,7 +280,7 @@ namespace gridfire::trigger::solver::CVODE {
void ConvergenceFailureTrigger::update(
const gridfire::solver::CVODESolverStrategy::TimestepContext &ctx
) {
// --- ConvergenceFailureTrigger::update does nothing and is intentionally left blank --- //
m_window.clear();
}
void ConvergenceFailureTrigger::step(
@@ -371,27 +372,38 @@ namespace gridfire::trigger::solver::CVODE {
std::unique_ptr<Trigger<gridfire::solver::CVODESolverStrategy::TimestepContext>> makeEnginePartitioningTrigger(
const double simulationTimeInterval,
const double offDiagonalThreshold,
const double relativeTimestepCollapseThreshold,
const size_t timestepGrowthWindowSize
const double timestepCollapseRatio,
const size_t maxConvergenceFailures
) {
using ctx_t = gridfire::solver::CVODESolverStrategy::TimestepContext;
// Create the individual conditions that can trigger a repartitioning
// The current trigger logic is as follows
// 1. Trigger every 1000th time that the simulation time exceeds the simulationTimeInterval
// 2. OR if any off-diagonal Jacobian entry exceeds the offDiagonalThreshold
// 3. OR every 10th time that the timestep growth exceeds the timestepGrowthThreshold (relative or absolute)
// 4. OR if the number of convergence failures begins to grow
// 1. INSTABILITY TRIGGERS (High Priority)
auto convergenceFailureTrigger = std::make_unique<ConvergenceFailureTrigger>(
maxConvergenceFailures,
1.0f,
10
);
// TODO: This logic likely needs to be revisited; however, for now it is easy enough to change and test and it works reasonably well
auto simulationTimeTrigger = std::make_unique<EveryNthTrigger<ctx_t>>(std::make_unique<SimulationTimeTrigger>(simulationTimeInterval), 1000);
auto timestepCollapseTrigger = std::make_unique<TimestepCollapseTrigger>(
timestepCollapseRatio,
true, // relative
5
);
auto instabilityGroup = std::make_unique<OrTrigger<ctx_t>>(
std::move(convergenceFailureTrigger),
std::move(timestepCollapseTrigger)
);
// 2. MAINTENANCE TRIGGERS
auto offDiagTrigger = std::make_unique<OffDiagonalTrigger>(offDiagonalThreshold);
auto timestepGrowthTrigger = std::make_unique<EveryNthTrigger<ctx_t>>(std::make_unique<TimestepCollapseTrigger>(relativeTimestepCollapseThreshold, true, timestepGrowthWindowSize), 10);
auto convergenceFailureTrigger = std::make_unique<ConvergenceFailureTrigger>(5, 1.0f, 10);
auto convergenceOrTimestepTrigger = std::make_unique<OrTrigger<ctx_t>>(std::move(timestepGrowthTrigger), std::move(convergenceFailureTrigger));
return convergenceOrTimestepTrigger;
// Combine: (Instability) OR (Structure Change)
return std::make_unique<OrTrigger<ctx_t>>(
std::move(instabilityGroup),
std::move(offDiagTrigger)
);
}
}

View File

@@ -9,7 +9,7 @@
#include <string>
std::string gridfire::utils::formatNuclearTimescaleLogString(
const DynamicEngine& engine,
const engine::DynamicEngine& engine,
const fourdst::composition::Composition& composition,
const double T9,
const double rho
@@ -17,7 +17,7 @@ std::string gridfire::utils::formatNuclearTimescaleLogString(
auto const& result = engine.getSpeciesTimescales(composition, T9, rho);
if (!result) {
std::ostringstream ss;
ss << "Failed to get species timescales: " << result.error();
ss << "Failed to get species timescales: " << engine::EngineStatus_to_string(result.error());
return ss.str();
}
const std::unordered_map<fourdst::atomic::Species, double>& timescales = result.value();