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:
@@ -80,9 +80,11 @@ namespace gridfire {
|
||||
const double rho,
|
||||
const reaction::ReactionSet &activeReactions
|
||||
) const {
|
||||
LOG_TRACE_L2(m_logger, "Calculating RHS and Energy in GraphEngine at T9 = {}, rho = {}.", T9, rho);
|
||||
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) {
|
||||
LOG_TRACE_L2(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());
|
||||
@@ -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 ---
|
||||
return calculateAllDerivativesUsingPrecomputation(comp, bare_rates, bare_reverse_rates, T9, rho, activeReactions);
|
||||
} else {
|
||||
LOG_TRACE_L2(m_logger, "Not using precomputation for reaction rates in GraphEngine calculateRHSAndEnergy.");
|
||||
StepDerivatives<double> result = calculateAllDerivatives<double>(
|
||||
comp.getMolarAbundanceVector(),
|
||||
T9,
|
||||
@@ -598,6 +603,7 @@ namespace gridfire {
|
||||
const double rho,
|
||||
const reaction::ReactionSet &activeReactions
|
||||
) const {
|
||||
LOG_TRACE_L2(m_logger, "Computing screening factors for {} active reactions.", activeReactions.size());
|
||||
// --- Calculate screening factors ---
|
||||
const std::vector<double> screeningFactors = m_screeningModel->calculateScreeningFactors(
|
||||
activeReactions,
|
||||
@@ -672,6 +678,8 @@ namespace gridfire {
|
||||
reactionCounter++;
|
||||
}
|
||||
|
||||
LOG_TRACE_L2(m_logger, "Computed {} molar reaction flows for active reactions. Assembling these into RHS", molarReactionFlows.size());
|
||||
|
||||
// --- Assemble molar abundance derivatives ---
|
||||
StepDerivatives<double> result;
|
||||
for (const auto& species: m_networkSpecies) {
|
||||
|
||||
@@ -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 {
|
||||
if (!m_speciesToIndexMap.contains(row) || !m_speciesToIndexMap.contains(col)) {
|
||||
throw std::out_of_range("Species not found in NetworkJacobian operator().");
|
||||
@@ -37,6 +62,26 @@ namespace gridfire {
|
||||
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 {
|
||||
return {m_jacobianMatrix.rows(), m_jacobianMatrix.cols()};
|
||||
}
|
||||
@@ -56,5 +101,60 @@ namespace gridfire {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,11 +82,47 @@ namespace gridfire {
|
||||
const double T9,
|
||||
const double rho
|
||||
) const {
|
||||
LOG_TRACE_L2(m_logger, "Calculating RHS and Energy in AdaptiveEngineView at T9 = {}, rho = {}.", T9, rho);
|
||||
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);
|
||||
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);
|
||||
LOG_TRACE_L2(m_logger, "Base engine calculation of RHS and Energy complete.");
|
||||
|
||||
if (!result) {
|
||||
LOG_TRACE_L2(m_logger, "Base engine returned stale error during RHS and Energy calculation.");
|
||||
return std::unexpected{result.error()};
|
||||
}
|
||||
|
||||
|
||||
@@ -163,19 +163,51 @@ namespace gridfire {
|
||||
const double T9,
|
||||
const double rho
|
||||
) 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);
|
||||
LOG_TRACE_L2(m_logger, "Base engine calculation of RHS and Energy complete.");
|
||||
|
||||
if (!result) {
|
||||
LOG_TRACE_L2(m_logger, "Base engine returned stale error during RHS and Energy calculation.");
|
||||
return std::unexpected{result.error()};
|
||||
}
|
||||
|
||||
auto deriv = result.value();
|
||||
|
||||
LOG_TRACE_L2(m_logger, "Zeroing out algebraic species derivatives.");
|
||||
for (const auto& species : m_algebraic_species) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1157,13 +1189,35 @@ namespace gridfire {
|
||||
const double rho
|
||||
) const {
|
||||
LOG_TRACE_L1(m_logger, "Solving for QSE abundances...");
|
||||
LOG_TRACE_L2(m_logger, "Composition before QSE solving: {}", [&comp]() -> std::string {
|
||||
std::stringstream ss;
|
||||
size_t i = 0;
|
||||
for (const auto& [sp, y] : comp) {
|
||||
ss << std::format("{}: {}", sp.name(), y);
|
||||
if (i < comp.size() - 1) {
|
||||
ss << ", ";
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return ss.str();
|
||||
}());
|
||||
|
||||
fourdst::composition::Composition outputComposition(comp.getRegisteredSpecies());
|
||||
for (const auto& [sp, y]: comp) {
|
||||
outputComposition.setMolarAbundance(sp, y);
|
||||
}
|
||||
fourdst::composition::Composition outputComposition(comp);
|
||||
|
||||
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())) {
|
||||
continue;
|
||||
}
|
||||
@@ -1178,16 +1232,19 @@ namespace gridfire {
|
||||
const double Y = std::max(initial_abundance, abundance_floor);
|
||||
v_initial(i) = std::log(Y);
|
||||
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++;
|
||||
}
|
||||
|
||||
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);
|
||||
Eigen::LevenbergMarquardt lm(functor);
|
||||
lm.parameters.ftol = 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);
|
||||
LOG_TRACE_L2(m_logger, "Minimizing functor status: {}", lm_status_map.at(status));
|
||||
|
||||
if (status <= 0 || status >= 4) {
|
||||
std::stringstream msg;
|
||||
@@ -1459,13 +1516,14 @@ namespace gridfire {
|
||||
}
|
||||
LOG_TRACE_L2(
|
||||
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_rho,
|
||||
[&]() -> std::string {
|
||||
std::stringstream ss;
|
||||
for (long j = 0; j < y_qse.size(); ++j) {
|
||||
ss << y_qse(j);
|
||||
ss << "(" << v_qse(j) << ")";
|
||||
if (j < y_qse.size() - 1) {
|
||||
ss << ", ";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user