feat(jacobian): Added regularization

There are times when the jacobian matrix has infinities or nans. If
these cases correspond to species (rows or columns) which have
effectivley zero abundance (i.e. if Y(Cl-32) ~ 1e-310 and
(dY(H-2)/dt)/dY(Cl-32) is inf) then it is safe to regularize these
entries to 0. If this is not done then the solver will end up finding
NaN values for the molar abundances on subsequent steps. This has been
implimented through a small regularization function in the
CVODE_solver_strategy file.
This commit is contained in:
2025-11-14 18:49:29 -05:00
parent 2ed629e0bf
commit b5d76e3728
7 changed files with 365 additions and 16 deletions

View File

@@ -9,6 +9,8 @@
#include <unordered_map> #include <unordered_map>
namespace gridfire { namespace gridfire {
using JacobianEntry = std::pair<std::pair<fourdst::atomic::Species, fourdst::atomic::Species>, double>;
class NetworkJacobian { class NetworkJacobian {
public: public:
explicit NetworkJacobian( explicit NetworkJacobian(
@@ -16,6 +18,10 @@ namespace gridfire {
const std::function<fourdst::atomic::Species(size_t)> &indexToSpeciesFunc const std::function<fourdst::atomic::Species(size_t)> &indexToSpeciesFunc
); );
NetworkJacobian(const NetworkJacobian& jacobian);
NetworkJacobian(NetworkJacobian&& jacobian) noexcept;
NetworkJacobian& operator=(NetworkJacobian&& jacobian) noexcept;
double operator()( double operator()(
const fourdst::atomic::Species& row, const fourdst::atomic::Species& row,
const fourdst::atomic::Species& col const fourdst::atomic::Species& col
@@ -26,12 +32,34 @@ namespace gridfire {
size_t j size_t j
) const; ) const;
void set(
const fourdst::atomic::Species& row,
const fourdst::atomic::Species& col,
double value
);
void set(
size_t i,
size_t j,
double value
);
void set(
const JacobianEntry &entry
);
std::tuple<size_t, size_t> shape() const; std::tuple<size_t, size_t> shape() const;
size_t rank() const; size_t rank() const;
size_t nnz() const; size_t nnz() const;
bool singular() const; bool singular() const;
[[nodiscard]] std::vector<JacobianEntry> infs() const;
[[nodiscard]] std::vector<JacobianEntry> nans() const;
[[nodiscard]] Eigen::SparseMatrix<double> data() const;
[[nodiscard]] const std::unordered_map<fourdst::atomic::Species, size_t>& mapping() const;
private: private:
Eigen::SparseMatrix<double> m_jacobianMatrix; Eigen::SparseMatrix<double> m_jacobianMatrix;
std::unordered_map<fourdst::atomic::Species, size_t> m_speciesToIndexMap; std::unordered_map<fourdst::atomic::Species, size_t> m_speciesToIndexMap;

View File

@@ -22,4 +22,8 @@ namespace gridfire::exceptions {
class SingularJacobianError final : public SolverError { class SingularJacobianError final : public SolverError {
using SolverError::SolverError; using SolverError::SolverError;
}; };
class IllConditionedJacobianError final : public SolverError {
using SolverError::SolverError;
};
} }

View File

@@ -80,9 +80,11 @@ namespace gridfire {
const double rho, const double rho,
const reaction::ReactionSet &activeReactions const reaction::ReactionSet &activeReactions
) const { ) const {
LOG_TRACE_L2(m_logger, "Calculating RHS and Energy in GraphEngine at T9 = {}, rho = {}.", T9, rho);
const double Ye = comp.getElectronAbundance(); const double Ye = comp.getElectronAbundance();
const double mue = 5.0e-3 * std::pow(rho * Ye, 1.0 / 3.0) + 0.5 * T9; const double mue = 0.0; // TODO: Remove
if (m_usePrecomputation) { if (m_usePrecomputation) {
LOG_TRACE_L2(m_logger, "Using precomputation for reaction rates in GraphEngine calculateRHSAndEnergy.");
std::vector<double> bare_rates; std::vector<double> bare_rates;
std::vector<double> bare_reverse_rates; std::vector<double> bare_reverse_rates;
bare_rates.reserve(activeReactions.size()); bare_rates.reserve(activeReactions.size());
@@ -96,9 +98,12 @@ namespace gridfire {
} }
} }
LOG_TRACE_L2(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 --- // --- 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); return calculateAllDerivativesUsingPrecomputation(comp, bare_rates, bare_reverse_rates, T9, rho, activeReactions);
} else { } else {
LOG_TRACE_L2(m_logger, "Not using precomputation for reaction rates in GraphEngine calculateRHSAndEnergy.");
StepDerivatives<double> result = calculateAllDerivatives<double>( StepDerivatives<double> result = calculateAllDerivatives<double>(
comp.getMolarAbundanceVector(), comp.getMolarAbundanceVector(),
T9, T9,
@@ -598,6 +603,7 @@ namespace gridfire {
const double rho, const double rho,
const reaction::ReactionSet &activeReactions const reaction::ReactionSet &activeReactions
) const { ) const {
LOG_TRACE_L2(m_logger, "Computing screening factors for {} active reactions.", activeReactions.size());
// --- Calculate screening factors --- // --- Calculate screening factors ---
const std::vector<double> screeningFactors = m_screeningModel->calculateScreeningFactors( const std::vector<double> screeningFactors = m_screeningModel->calculateScreeningFactors(
activeReactions, activeReactions,
@@ -672,6 +678,8 @@ namespace gridfire {
reactionCounter++; reactionCounter++;
} }
LOG_TRACE_L2(m_logger, "Computed {} molar reaction flows for active reactions. Assembling these into RHS", molarReactionFlows.size());
// --- Assemble molar abundance derivatives --- // --- Assemble molar abundance derivatives ---
StepDerivatives<double> result; StepDerivatives<double> result;
for (const auto& species: m_networkSpecies) { for (const auto& species: m_networkSpecies) {

View File

@@ -21,6 +21,31 @@ namespace gridfire {
} }
} }
NetworkJacobian::NetworkJacobian(
const NetworkJacobian &jacobian
) : m_jacobianMatrix(jacobian.m_jacobianMatrix),
m_speciesToIndexMap(jacobian.m_speciesToIndexMap),
m_rank(jacobian.m_rank)
{}
NetworkJacobian::NetworkJacobian(
NetworkJacobian &&jacobian
) noexcept : m_jacobianMatrix(std::move(jacobian.m_jacobianMatrix)),
m_speciesToIndexMap(std::move(jacobian.m_speciesToIndexMap)),
m_rank(jacobian.m_rank)
{}
NetworkJacobian & NetworkJacobian::operator=(
NetworkJacobian &&jacobian
) noexcept {
if (this != &jacobian) {
m_jacobianMatrix = std::move(jacobian.m_jacobianMatrix);
m_speciesToIndexMap = std::move(jacobian.m_speciesToIndexMap);
m_rank = jacobian.m_rank;
}
return *this;
}
double NetworkJacobian::operator()(const fourdst::atomic::Species &row, const fourdst::atomic::Species &col) const { double NetworkJacobian::operator()(const fourdst::atomic::Species &row, const fourdst::atomic::Species &col) const {
if (!m_speciesToIndexMap.contains(row) || !m_speciesToIndexMap.contains(col)) { if (!m_speciesToIndexMap.contains(row) || !m_speciesToIndexMap.contains(col)) {
throw std::out_of_range("Species not found in NetworkJacobian operator()."); throw std::out_of_range("Species not found in NetworkJacobian operator().");
@@ -37,6 +62,26 @@ namespace gridfire {
return m_jacobianMatrix.coeff(i, j); return m_jacobianMatrix.coeff(i, j);
} }
void NetworkJacobian::set(const fourdst::atomic::Species &row, const fourdst::atomic::Species &col, const double value) {
if (!m_speciesToIndexMap.contains(row) || !m_speciesToIndexMap.contains(col)) {
throw std::out_of_range("Species not found in NetworkJacobian set().");
}
const size_t i = m_speciesToIndexMap.at(row);
const size_t j = m_speciesToIndexMap.at(col);
set(i, j, value);
}
void NetworkJacobian::set(const size_t i, const size_t j, const double value) {
if (i >= m_jacobianMatrix.rows() || j >= m_jacobianMatrix.cols()) {
throw std::out_of_range("Index out of bounds in NetworkJacobian set().");
}
m_jacobianMatrix.coeffRef(i, j) = value;
}
void NetworkJacobian::set(const JacobianEntry &entry) {
set(entry.first.first, entry.first.second, entry.second);
}
std::tuple<size_t, size_t> NetworkJacobian::shape() const { std::tuple<size_t, size_t> NetworkJacobian::shape() const {
return {m_jacobianMatrix.rows(), m_jacobianMatrix.cols()}; return {m_jacobianMatrix.rows(), m_jacobianMatrix.cols()};
} }
@@ -56,5 +101,60 @@ namespace gridfire {
return m_rank < minDim; return m_rank < minDim;
} }
std::vector<JacobianEntry> NetworkJacobian::infs() const {
std::vector<JacobianEntry> infs;
for (int k=0; k<m_jacobianMatrix.outerSize(); ++k) {
for (Eigen::SparseMatrix<double>::InnerIterator it(m_jacobianMatrix,k); it; ++it) {
if (std::isinf(it.value())) {
fourdst::atomic::Species rowSpecies = std::ranges::find_if(
m_speciesToIndexMap,
[it](const auto& pair) {
return pair.second == static_cast<size_t>(it.row());
})->first;
fourdst::atomic::Species colSpecies = std::ranges::find_if(
m_speciesToIndexMap,
[it](const auto& pair) {
return pair.second == static_cast<size_t>(it.col());
})->first;
infs.emplace_back(std::make_pair(rowSpecies, colSpecies), it.value());
}
}
}
return infs;
}
std::vector<JacobianEntry> NetworkJacobian::nans() const {
std::vector<JacobianEntry> nans;
for (int k=0; k<m_jacobianMatrix.outerSize(); ++k) {
for (Eigen::SparseMatrix<double>::InnerIterator it(m_jacobianMatrix,k); it; ++it) {
if (std::isnan(it.value())) {
fourdst::atomic::Species rowSpecies = std::ranges::find_if(
m_speciesToIndexMap,
[it](const auto& pair) {
return pair.second == static_cast<size_t>(it.row());
})->first;
fourdst::atomic::Species colSpecies = std::ranges::find_if(
m_speciesToIndexMap,
[it](const auto& pair) {
return pair.second == static_cast<size_t>(it.col());
})->first;
nans.emplace_back(std::make_pair(rowSpecies, colSpecies), it.value());
}
}
}
return nans;
} }
Eigen::SparseMatrix<double> NetworkJacobian::data() const {
return m_jacobianMatrix;
}
const std::unordered_map<fourdst::atomic::Species, size_t> & NetworkJacobian::mapping() const {
return m_speciesToIndexMap;
}
}

View File

@@ -82,11 +82,47 @@ namespace gridfire {
const double T9, const double T9,
const double rho const double rho
) const { ) const {
LOG_TRACE_L2(m_logger, "Calculating RHS and Energy in AdaptiveEngineView at T9 = {}, rho = {}.", T9, rho);
validateState(); validateState();
LOG_TRACE_L2(
m_logger,
"Adaptive engine view state validated prior to composition collection. Input Composition: {}",
[&comp]() -> std::string {
std::stringstream ss;
size_t i = 0;
for (const auto& [species, abundance] : comp) {
ss << species.name() << ": " << abundance;
if (i < comp.size() - 1) {
ss << ", ";
}
i++;
}
return ss.str();
}());
fourdst::composition::Composition collectedComp = collectComposition(comp, T9, rho); fourdst::composition::Composition collectedComp = collectComposition(comp, T9, rho);
LOG_TRACE_L2(
m_logger,
"Composition Collected prior to passing to base engine. Collected Composition: {}",
[&comp, &collectedComp]() -> std::string {
std::stringstream ss;
size_t i = 0;
for (const auto& [species, abundance] : collectedComp) {
ss << species.name() << ": " << abundance;
if (comp.contains(species)) {
ss << " (input: " << comp.getMolarAbundance(species) << ")";
}
if (i < collectedComp.size() - 1) {
ss << ", ";
}
i++;
}
return ss.str();
}());
auto result = m_baseEngine.calculateRHSAndEnergy(collectedComp, T9, rho); auto result = m_baseEngine.calculateRHSAndEnergy(collectedComp, T9, rho);
LOG_TRACE_L2(m_logger, "Base engine calculation of RHS and Energy complete.");
if (!result) { if (!result) {
LOG_TRACE_L2(m_logger, "Base engine returned stale error during RHS and Energy calculation.");
return std::unexpected{result.error()}; return std::unexpected{result.error()};
} }

View File

@@ -163,19 +163,51 @@ namespace gridfire {
const double T9, const double T9,
const double rho const double rho
) const { ) const {
// const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho); LOG_TRACE_L2(m_logger, "Calculating RHS and Energy in MultiscalePartitioningEngineView at T9 = {}, rho = {}.", T9, rho);
LOG_TRACE_L2(m_logger, "Input composition is {}", [&comp]() -> std::string {
std::stringstream ss;
size_t i = 0;
for (const auto& [species, abundance] : comp) {
ss << species.name() << ": " << abundance;
if (i < comp.size() - 1) {
ss << ", ";
}
i++;
}
return ss.str();
}());
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho);
LOG_TRACE_L2(m_logger, "Equilibrated composition prior to calling base engine is {}", [&qseComposition, &comp]() -> std::string {
std::stringstream ss;
size_t i = 0;
for (const auto& [species, abundance] : qseComposition) {
ss << species.name() << ": " << abundance;
if (comp.contains(species)) {
ss << " (input: " << comp.getMolarAbundance(species) << ")";
}
if (i < qseComposition.size() - 1) {
ss << ", ";
}
i++;
}
return ss.str();
}());
const auto result = m_baseEngine.calculateRHSAndEnergy(comp, T9, rho); const auto result = m_baseEngine.calculateRHSAndEnergy(comp, T9, rho);
LOG_TRACE_L2(m_logger, "Base engine calculation of RHS and Energy complete.");
if (!result) { if (!result) {
LOG_TRACE_L2(m_logger, "Base engine returned stale error during RHS and Energy calculation.");
return std::unexpected{result.error()}; return std::unexpected{result.error()};
} }
auto deriv = result.value(); auto deriv = result.value();
LOG_TRACE_L2(m_logger, "Zeroing out algebraic species derivatives.");
for (const auto& species : m_algebraic_species) { for (const auto& species : m_algebraic_species) {
deriv.dydt[species] = 0.0; // Fix the algebraic species to the equilibrium abundances we calculate. deriv.dydt[species] = 0.0; // Fix the algebraic species to the equilibrium abundances we calculate.
} }
LOG_TRACE_L2(m_logger, "Done Zeroing out algebraic species derivatives.");
return deriv; return deriv;
} }
@@ -1157,13 +1189,35 @@ namespace gridfire {
const double rho const double rho
) const { ) const {
LOG_TRACE_L1(m_logger, "Solving for QSE abundances..."); LOG_TRACE_L1(m_logger, "Solving for QSE abundances...");
LOG_TRACE_L2(m_logger, "Composition before QSE solving: {}", [&comp]() -> std::string {
fourdst::composition::Composition outputComposition(comp.getRegisteredSpecies()); std::stringstream ss;
size_t i = 0;
for (const auto& [sp, y] : comp) { for (const auto& [sp, y] : comp) {
outputComposition.setMolarAbundance(sp, y); ss << std::format("{}: {}", sp.name(), y);
if (i < comp.size() - 1) {
ss << ", ";
} }
i++;
}
return ss.str();
}());
fourdst::composition::Composition outputComposition(comp);
for (const auto&[is_in_equilibrium, algebraic_species, seed_species, mean_timescale] : m_qse_groups) { for (const auto&[is_in_equilibrium, algebraic_species, seed_species, mean_timescale] : m_qse_groups) {
LOG_TRACE_L2(m_logger, "Working on QSE group with algebraic species: {}",
[&]() -> std::string {
std::stringstream ss;
size_t count = 0;
for (const auto& species: algebraic_species) {
ss << species.name();
if (count < algebraic_species.size() - 1) {
ss << ", ";
}
count++;
}
return ss.str();
}());
if (!is_in_equilibrium || (algebraic_species.empty() && seed_species.empty())) { if (!is_in_equilibrium || (algebraic_species.empty() && seed_species.empty())) {
continue; continue;
} }
@@ -1178,16 +1232,19 @@ namespace gridfire {
const double Y = std::max(initial_abundance, abundance_floor); const double Y = std::max(initial_abundance, abundance_floor);
v_initial(i) = std::log(Y); v_initial(i) = std::log(Y);
species_to_index_map.emplace(species, i); species_to_index_map.emplace(species, i);
LOG_TRACE_L2(m_logger, "For species {} initial molar abundance is {}, log scaled to {}. Species placed at index {}.", species.name(), Y, v_initial(i), i);
i++; i++;
} }
LOG_TRACE_L2(m_logger, "Setting up Eigen Levenberg-Marquardt solver for QSE group...");
EigenFunctor functor(*this, algebraic_species, comp, T9, rho, Y_scale, species_to_index_map); EigenFunctor functor(*this, algebraic_species, comp, T9, rho, Y_scale, species_to_index_map);
Eigen::LevenbergMarquardt lm(functor); Eigen::LevenbergMarquardt lm(functor);
lm.parameters.ftol = 1.0e-10; lm.parameters.ftol = 1.0e-10;
lm.parameters.xtol = 1.0e-10; lm.parameters.xtol = 1.0e-10;
LOG_TRACE_L1(m_logger, "Minimizing functor..."); LOG_TRACE_L2(m_logger, "Minimizing functor...");
Eigen::LevenbergMarquardtSpace::Status status = lm.minimize(v_initial); Eigen::LevenbergMarquardtSpace::Status status = lm.minimize(v_initial);
LOG_TRACE_L2(m_logger, "Minimizing functor status: {}", lm_status_map.at(status));
if (status <= 0 || status >= 4) { if (status <= 0 || status >= 4) {
std::stringstream msg; std::stringstream msg;
@@ -1459,13 +1516,14 @@ namespace gridfire {
} }
LOG_TRACE_L2( LOG_TRACE_L2(
m_view.m_logger, m_view.m_logger,
"Functor evaluation at T9 = {}, rho = {}, y_qse = <{}> complete. ||f|| = {}", "Functor evaluation at T9 = {}, rho = {}, y_qse (v_qse) = <{}> complete. ||f|| = {}",
m_T9, m_T9,
m_rho, m_rho,
[&]() -> std::string { [&]() -> std::string {
std::stringstream ss; std::stringstream ss;
for (long j = 0; j < y_qse.size(); ++j) { for (long j = 0; j < y_qse.size(); ++j) {
ss << y_qse(j); ss << y_qse(j);
ss << "(" << v_qse(j) << ")";
if (j < y_qse.size() - 1) { if (j < y_qse.size() - 1) {
ss << ", "; ss << ", ";
} }

View File

@@ -24,6 +24,8 @@
#include "gridfire/exceptions/error_solver.h" #include "gridfire/exceptions/error_solver.h"
namespace { namespace {
constexpr double MIN_ABUNDANCE_TO_CONTRIBUTE_TO_JACOBIAN = 1e-100;
std::unordered_map<int, std::string> cvode_ret_code_map { std::unordered_map<int, std::string> cvode_ret_code_map {
{0, "CV_SUCCESS: The solver succeeded."}, {0, "CV_SUCCESS: The solver succeeded."},
{1, "CV_TSTOP_RETURN: The solver reached the specified stopping time."}, {1, "CV_TSTOP_RETURN: The solver reached the specified stopping time."},
@@ -78,6 +80,34 @@ namespace {
check_cvode_flag(vec == nullptr ? -1 : 0, "N_VNew"); check_cvode_flag(vec == nullptr ? -1 : 0, "N_VNew");
return vec; return vec;
} }
gridfire::NetworkJacobian regularize_jacobian(const gridfire::NetworkJacobian& jacobian, const fourdst::composition::CompositionAbstract& comp, std::optional<quill::Logger*> logger = std::nullopt) {
const std::vector<gridfire::JacobianEntry> infs = jacobian.infs();
const std::vector<gridfire::JacobianEntry> nans = jacobian.nans();
if (infs.size() == 0 && nans.size() == 0) {
return jacobian;
}
gridfire::NetworkJacobian newJacobian = jacobian;
for (const auto& [iSp, dSp] : infs | std::views::keys) {
if (comp.getMolarAbundance(iSp) < MIN_ABUNDANCE_TO_CONTRIBUTE_TO_JACOBIAN || comp.getMolarAbundance(dSp) < MIN_ABUNDANCE_TO_CONTRIBUTE_TO_JACOBIAN) {
newJacobian.set(iSp, dSp, 0.0);
if (logger) {
LOG_TRACE_L1(logger.value(), "Regularized Jacobian entry ({}, {}) from inf to 0.0 due to low abundance.", iSp.name(), dSp.name());
}
}
}
for (const auto& [iSp, dSp] : nans | std::views::keys) {
if (comp.getMolarAbundance(iSp) < MIN_ABUNDANCE_TO_CONTRIBUTE_TO_JACOBIAN || comp.getMolarAbundance(dSp) < MIN_ABUNDANCE_TO_CONTRIBUTE_TO_JACOBIAN) {
newJacobian.set(iSp, dSp, 0.0);
if (logger) {
LOG_TRACE_L1(logger.value(), "Regularized Jacobian entry ({}, {}) from inf to 0.0 due to low abundance.", iSp.name(), dSp.name());
}
}
}
return newJacobian;
}
} }
namespace gridfire::solver { namespace gridfire::solver {
@@ -206,7 +236,9 @@ namespace gridfire::solver {
check_cvode_flag(CVodeSetUserData(m_cvode_mem, &user_data), "CVodeSetUserData"); check_cvode_flag(CVodeSetUserData(m_cvode_mem, &user_data), "CVodeSetUserData");
LOG_TRACE_L2(m_logger, "Taking one CVODE step...");
int flag = CVode(m_cvode_mem, netIn.tMax, m_Y, &current_time, CV_ONE_STEP); int flag = CVode(m_cvode_mem, netIn.tMax, m_Y, &current_time, CV_ONE_STEP);
LOG_TRACE_L2(m_logger, "CVODE step complete. Current time: {}, step status: {}", current_time, cvode_ret_code_map.at(flag));
if (user_data.captured_exception){ if (user_data.captured_exception){
std::rethrow_exception(std::make_exception_ptr(*user_data.captured_exception)); std::rethrow_exception(std::make_exception_ptr(*user_data.captured_exception));
@@ -539,13 +571,17 @@ namespace gridfire::solver {
const auto* instance = data->solver_instance; const auto* instance = data->solver_instance;
try { try {
LOG_TRACE_L2(instance->m_logger, "CVODE RHS wrapper called at time {}", t);
const CVODERHSOutputData out = instance->calculate_rhs(t, y, ydot, data); const CVODERHSOutputData out = instance->calculate_rhs(t, y, ydot, data);
data->reaction_contribution_map = out.reaction_contribution_map; data->reaction_contribution_map = out.reaction_contribution_map;
LOG_TRACE_L2(instance->m_logger, "CVODE RHS wrapper completed successfully at time {}", t);
return 0; return 0;
} catch (const exceptions::StaleEngineTrigger& e) { } 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); data->captured_exception = std::make_unique<exceptions::StaleEngineTrigger>(e);
return 1; // 1 Indicates a recoverable error, CVODE will retry the step return 1; // 1 Indicates a recoverable error, CVODE will retry the step
} catch (...) { } catch (...) {
LOG_CRITICAL(instance->m_logger, "Unrecoverable and Unknown exception caught in CVODE RHS wrapper at time {}", t);
return -1; // Some unrecoverable error return -1; // Some unrecoverable error
} }
} }
@@ -562,10 +598,13 @@ namespace gridfire::solver {
) { ) {
const auto* data = static_cast<CVODEUserData*>(user_data); const auto* data = static_cast<CVODEUserData*>(user_data);
const auto* engine = data->engine; const auto* engine = data->engine;
const auto* solver_instance = data->solver_instance;
LOG_TRACE_L2(solver_instance->m_logger, "CVODE Jacobian wrapper starting");
const size_t numSpecies = engine->getNetworkSpecies().size(); const size_t numSpecies = engine->getNetworkSpecies().size();
sunrealtype* y_data = N_VGetArrayPointer(y); sunrealtype* y_data = N_VGetArrayPointer(y);
// Solver constraints should keep these values very close to 0 but floating point noise can still result in very // Solver constraints should keep these values very close to 0 but floating point noise can still result in very
// small negative numbers which can result in NaN's and more immediate crashes in the composition // small negative numbers which can result in NaN's and more immediate crashes in the composition
// finalization stage // finalization stage
@@ -576,12 +615,75 @@ namespace gridfire::solver {
} }
std::vector<double> y_vec(y_data, y_data + numSpecies); std::vector<double> y_vec(y_data, y_data + numSpecies);
fourdst::composition::Composition composition(engine->getNetworkSpecies(), y_vec); fourdst::composition::Composition composition(engine->getNetworkSpecies(), y_vec);
LOG_TRACE_L2(solver_instance->m_logger, "Generating Jacobian matrix at time {} with {} species in composition (mean molecular mass: {})", t, composition.size(), composition.getMeanParticleMass());
LOG_TRACE_L2(solver_instance->m_logger, "Composition is {}", [&composition]() -> std::string {
std::stringstream ss;
size_t i = 0;
for (const auto& [species, abundance] : composition) {
ss << species.name() << ": " << abundance;
if (i < composition.size() - 1) {
ss << ", ";
}
i++;
}
return ss.str();
}());
LOG_TRACE_L2(solver_instance->m_logger, "Generating Jacobian matrix at time {}", t);
NetworkJacobian jac = engine->generateJacobianMatrix(composition, data->T9, data->rho); NetworkJacobian jac = engine->generateJacobianMatrix(composition, data->T9, data->rho);
LOG_TRACE_L2(solver_instance->m_logger, "Regularizing Jacobian matrix at time {}", t);
jac = regularize_jacobian(jac, composition, solver_instance->m_logger);
LOG_TRACE_L2(solver_instance->m_logger, "Done regularizing Jacobian matrix at time {}", t);
if (jac.infs().size() != 0 || jac.nans().size() != 0) {
auto infString = [&jac]() -> std::string {
std::stringstream ss;
size_t i = 0;
std::vector<JacobianEntry> entries = jac.infs();
for (const auto &[fst, snd]: entries | std::views::keys) {
ss << "J(" << fst.name() << ", " << snd.name() << ")";
if (i < entries.size() - 1) {
ss << ", ";
}
i++;
}
if (entries.size() == 0) {
ss << "None";
}
return ss.str();
};
auto nanString = [&jac]() -> std::string {
std::stringstream ss;
size_t i = 0;
std::vector<JacobianEntry> entries = jac.nans();
for (const auto &[fst, snd]: entries | std::views::keys) {
ss << "J(" << fst.name() << ", " << snd.name() << ")";
if (i < entries.size() - 1) {
ss << ", ";
}
i++;
}
if (entries.size() == 0) {
ss << "None";
}
return ss.str();
};
LOG_ERROR(
solver_instance->m_logger,
"Jacobian matrix generated at time {} contains {} infinite entries ({}) and {} NaN entries ({}). This will lead to a solver failure. GridFire will now halt.",
t,
jac.infs().size(),
infString(),
jac.nans().size(),
nanString()
);
throw exceptions::IllConditionedJacobianError(std::format("Jacobian matrix generated at time {} contains {} infinite entries ({}) and {} NaN entries ({}). This will lead to a solver failure. In order to ensure tractability GridFire will not proceed. Focus on improving conditioning of the Jacobian matrix. If you believe this is an error please contact the GridFire developers.", t, jac.infs().size(), infString(), jac.nans().size(), nanString()));
}
LOG_TRACE_L2(solver_instance->m_logger, "Jacobian matrix created at time {} of shape ({} x {}) and rank {}", t, std::get<0>(jac.shape()), std::get<1>(jac.shape()), jac.rank());
sunrealtype* J_data = SUNDenseMatrix_Data(J); sunrealtype* J_data = SUNDenseMatrix_Data(J);
const long int N = SUNDenseMatrix_Columns(J); const long int N = SUNDenseMatrix_Columns(J);
LOG_TRACE_L2(solver_instance->m_logger, "Transferring Jacobian matrix data to SUNDenseMatrix format at time {}", t);
for (size_t j = 0; j < numSpecies; ++j) { for (size_t j = 0; j < numSpecies; ++j) {
const fourdst::atomic::Species& species_j = engine->getNetworkSpecies()[j]; const fourdst::atomic::Species& species_j = engine->getNetworkSpecies()[j];
for (size_t i = 0; i < numSpecies; ++i) { for (size_t i = 0; i < numSpecies; ++i) {
@@ -589,16 +691,10 @@ namespace gridfire::solver {
// J(i,j) = d(f_i)/d(y_j) // J(i,j) = d(f_i)/d(y_j)
// Column-major order format for SUNDenseMatrix: J_data[j*N + i] indexes J(i,j) // Column-major order format for SUNDenseMatrix: J_data[j*N + i] indexes J(i,j)
const double dYi_dt = jac(species_i, species_j); const double dYi_dt = jac(species_i, species_j);
// if (i == j && dYi_dt == 0 && engine->getSpeciesStatus(species_i) == SpeciesStatus::ACTIVE) {
// std::cerr << "Warning: Jacobian matrix has a zero on the diagonal for species " << species_i.name() << ". This may lead to solver failure or pathological stiffness.\n";
// // throw exceptions::SingularJacobianError(
// // "Jacobian matrix has a zero on the diagonal for species " + std::string(species_i.name()) +
// // ". This will either lead to solver failure or pathological stiffness. In order to ensure tractability GridFire will not proceed. Focus on improving conditioning of the Jacobian matrix. If you believe this is an error please contact the GridFire developers."
// // );
// }
J_data[j * N + i] = dYi_dt; J_data[j * N + i] = dYi_dt;
} }
} }
LOG_TRACE_L2(solver_instance->m_logger, "Done transferring Jacobian matrix data to SUNDenseMatrix format at time {}", t);
// For now assume that the energy derivatives wrt. abundances are zero // For now assume that the energy derivatives wrt. abundances are zero
// TODO: Need a better way to build this part of the output jacobian so it properly pushes the solver // TODO: Need a better way to build this part of the output jacobian so it properly pushes the solver
@@ -631,13 +727,28 @@ namespace gridfire::solver {
std::vector<double> y_vec(y_data, y_data + numSpecies); std::vector<double> y_vec(y_data, y_data + numSpecies);
fourdst::composition::Composition composition(m_engine.getNetworkSpecies(), y_vec); fourdst::composition::Composition composition(m_engine.getNetworkSpecies(), y_vec);
LOG_TRACE_L2(m_logger, "Calculating RHS at time {} with {} species in composition (mean molecular mass: {})", t, composition.size(), composition.getMeanParticleMass());
const auto result = m_engine.calculateRHSAndEnergy(composition, data->T9, data->rho); const auto result = m_engine.calculateRHSAndEnergy(composition, data->T9, data->rho);
if (!result) { 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]}); throw exceptions::StaleEngineTrigger({data->T9, data->rho, y_vec, t, m_num_steps, y_data[numSpecies]});
} }
sunrealtype* ydot_data = N_VGetArrayPointer(ydot); sunrealtype* ydot_data = N_VGetArrayPointer(ydot);
const auto& [dydt, nuclearEnergyGenerationRate, reactionContributions] = result.value(); const auto& [dydt, nuclearEnergyGenerationRate, reactionContributions] = result.value();
LOG_TRACE_L2(m_logger, "Done calculating RHS at time {}, specific nuclear energy generation rate: {}", t, nuclearEnergyGenerationRate);
LOG_TRACE_L2(m_logger, "RHS at time {} is {}", t, [&dydt]() -> std::string {
std::stringstream ss;
size_t i = 0;
for (const auto& [species, rate] : dydt) {
ss << "dY(" << species.name() << ")/dt" << ": " << rate;
if (i < dydt.size() - 1) {
ss << ", ";
}
i++;
}
return ss.str();
}());
for (size_t i = 0; i < numSpecies; ++i) { for (size_t i = 0; i < numSpecies; ++i) {
fourdst::atomic::Species species = m_engine.getNetworkSpecies()[i]; fourdst::atomic::Species species = m_engine.getNetworkSpecies()[i];
@@ -657,6 +768,7 @@ namespace gridfire::solver {
const double relTol, const double relTol,
const double accumulatedEnergy const double accumulatedEnergy
) { ) {
LOG_TRACE_L2(m_logger, "Initializing CVODE integration resources with N: {}, current_time: {}, absTol: {}, relTol: {}", N, current_time, absTol, relTol);
cleanup_cvode_resources(false); // Cleanup any existing resources before initializing new ones cleanup_cvode_resources(false); // Cleanup any existing resources before initializing new ones
m_Y = init_sun_vector(N, m_sun_ctx); m_Y = init_sun_vector(N, m_sun_ctx);
@@ -706,9 +818,11 @@ namespace gridfire::solver {
check_cvode_flag(CVodeSetLinearSolver(m_cvode_mem, m_LS, m_J), "CVodeSetLinearSolver"); check_cvode_flag(CVodeSetLinearSolver(m_cvode_mem, m_LS, m_J), "CVodeSetLinearSolver");
check_cvode_flag(CVodeSetJacFn(m_cvode_mem, cvode_jac_wrapper), "CVodeSetJacFn"); check_cvode_flag(CVodeSetJacFn(m_cvode_mem, cvode_jac_wrapper), "CVodeSetJacFn");
LOG_TRACE_L2(m_logger, "CVODE solver initialized");
} }
void CVODESolverStrategy::cleanup_cvode_resources(const bool memFree) { void CVODESolverStrategy::cleanup_cvode_resources(const bool memFree) {
LOG_TRACE_L2(m_logger, "Cleaning up cvode resources");
if (m_LS) SUNLinSolFree(m_LS); if (m_LS) SUNLinSolFree(m_LS);
if (m_J) SUNMatDestroy(m_J); if (m_J) SUNMatDestroy(m_J);
if (m_Y) N_VDestroy(m_Y); if (m_Y) N_VDestroy(m_Y);
@@ -725,6 +839,7 @@ namespace gridfire::solver {
if (m_cvode_mem) CVodeFree(&m_cvode_mem); if (m_cvode_mem) CVodeFree(&m_cvode_mem);
m_cvode_mem = nullptr; m_cvode_mem = nullptr;
} }
LOG_TRACE_L2(m_logger, "Done Cleaning up cvode resources");
} }
void CVODESolverStrategy::log_step_diagnostics(const CVODEUserData &user_data, bool displayJacobianStiffness) const { void CVODESolverStrategy::log_step_diagnostics(const CVODEUserData &user_data, bool displayJacobianStiffness) const {