feat(NetworkJacobian): Rank caching, regularization, and CSV export

Comptuting the rank of a large matrix with QR factorization can be
expensive and the rank is often never needed. We have implimented a
caching system so that the rank is only computed when asked for, and
then only once. Further, the regularization method which was previously
in an anonomous namespace inside of a single translation unit has been
moved to the jacobian header and implimentation file. This allows all
parts of GridFire to access the same regularization method. Finally a
small CSV output method has been added which is useful for debugging
This commit is contained in:
2025-11-15 09:05:41 -05:00
parent 4a404f6e12
commit b65626ca20
2 changed files with 106 additions and 11 deletions

View File

@@ -1,14 +1,18 @@
#pragma once #pragma once
#include "fourdst/atomic/atomicSpecies.h" #include "fourdst/atomic/atomicSpecies.h"
#include "fourdst/composition/composition_abstract.h"
#include "quill/Logger.h"
#include <Eigen/SparseCore> #include <Eigen/SparseCore>
#include <Eigen/SparseQR> #include <Eigen/SparseQR>
#include <tuple> #include <tuple>
#include <functional> #include <functional>
#include <unordered_map> #include <unordered_map>
#include <optional>
namespace gridfire { namespace gridfire {
constexpr double MIN_ABUNDANCE_TO_CONTRIBUTE_TO_JACOBIAN = 1e-100;
using JacobianEntry = std::pair<std::pair<fourdst::atomic::Species, fourdst::atomic::Species>, double>; using JacobianEntry = std::pair<std::pair<fourdst::atomic::Species, fourdst::atomic::Species>, double>;
class NetworkJacobian { class NetworkJacobian {
@@ -60,9 +64,18 @@ namespace gridfire {
[[nodiscard]] Eigen::SparseMatrix<double> data() const; [[nodiscard]] Eigen::SparseMatrix<double> data() const;
[[nodiscard]] const std::unordered_map<fourdst::atomic::Species, size_t>& mapping() const; [[nodiscard]] const std::unordered_map<fourdst::atomic::Species, size_t>& mapping() const;
void to_csv(const std::string& filename) 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;
size_t m_rank;
mutable std::optional<size_t> m_rank = std::nullopt;
}; };
NetworkJacobian regularize_jacobian(
const NetworkJacobian& jacobian,
const fourdst::composition::CompositionAbstract& comp,
std::optional<quill::Logger*> logger = std::nullopt
);
} }

View File

@@ -1,7 +1,15 @@
#include "gridfire/engine/types/jacobian.h" #include "gridfire/engine/types/jacobian.h"
#include <format>
#include <fstream>
#include <print>
#include <chrono>
#include <ranges>
#include <Eigen/SparseCore> #include <Eigen/SparseCore>
#include <Eigen/SparseQR> #include <Eigen/SparseQR>
#include "quill/LogMacros.h"
namespace gridfire { namespace gridfire {
NetworkJacobian::NetworkJacobian( NetworkJacobian::NetworkJacobian(
const Eigen::SparseMatrix<double>& jacobianMatrix, const Eigen::SparseMatrix<double>& jacobianMatrix,
@@ -12,13 +20,6 @@ namespace gridfire {
m_speciesToIndexMap[species] = i; m_speciesToIndexMap[species] = i;
} }
if (m_jacobianMatrix.rows() == 0 || m_jacobianMatrix.cols() == 0) {
m_rank = 0;
} else {
Eigen::SparseQR<Eigen::SparseMatrix<double>, Eigen::COLAMDOrdering<int>> solver;
solver.compute(m_jacobianMatrix);
m_rank = solver.rank();
}
} }
NetworkJacobian::NetworkJacobian( NetworkJacobian::NetworkJacobian(
@@ -57,7 +58,7 @@ namespace gridfire {
double NetworkJacobian::operator()(const size_t i, const size_t j) const { double NetworkJacobian::operator()(const size_t i, const size_t j) const {
if (i >= m_jacobianMatrix.rows() || j >= m_jacobianMatrix.cols()) { if (i >= m_jacobianMatrix.rows() || j >= m_jacobianMatrix.cols()) {
throw std::out_of_range("Index out of bounds in NetworkJacobian operator()."); throw std::out_of_range(std::format("Index ({}, {}) out of bounds in NetworkJacobian operator() for jacobian of shape ({}, {}).", i, j, m_jacobianMatrix.rows(), m_jacobianMatrix.cols()));
} }
return m_jacobianMatrix.coeff(i, j); return m_jacobianMatrix.coeff(i, j);
} }
@@ -73,7 +74,7 @@ namespace gridfire {
void NetworkJacobian::set(const size_t i, const size_t j, const double value) { void NetworkJacobian::set(const size_t i, const size_t j, const double value) {
if (i >= m_jacobianMatrix.rows() || j >= m_jacobianMatrix.cols()) { if (i >= m_jacobianMatrix.rows() || j >= m_jacobianMatrix.cols()) {
throw std::out_of_range("Index out of bounds in NetworkJacobian set()."); throw std::out_of_range(std::format("Index ({}, {}) out of bounds in NetworkJacobian set() for jacobian of shape ({}, {}).", i, j, m_jacobianMatrix.rows(), m_jacobianMatrix.cols()));
} }
m_jacobianMatrix.coeffRef(i, j) = value; m_jacobianMatrix.coeffRef(i, j) = value;
} }
@@ -91,7 +92,17 @@ namespace gridfire {
} }
size_t NetworkJacobian::rank() const { size_t NetworkJacobian::rank() const {
return m_rank; if (m_rank) {
return m_rank.value();
}
if (m_jacobianMatrix.rows() == 0 || m_jacobianMatrix.cols() == 0) {
m_rank = 0;
} else {
Eigen::SparseQR<Eigen::SparseMatrix<double>, Eigen::COLAMDOrdering<int>> solver;
solver.compute(m_jacobianMatrix);
m_rank = solver.rank();
}
return m_rank.value();
} }
bool NetworkJacobian::singular() const { bool NetworkJacobian::singular() const {
@@ -157,4 +168,75 @@ namespace gridfire {
const std::unordered_map<fourdst::atomic::Species, size_t> & NetworkJacobian::mapping() const { const std::unordered_map<fourdst::atomic::Species, size_t> & NetworkJacobian::mapping() const {
return m_speciesToIndexMap; return m_speciesToIndexMap;
} }
void NetworkJacobian::to_csv(const std::string &filename) const {
std::ofstream file(filename);
if (file.is_open()) {
const size_t numSpecies = m_jacobianMatrix.rows();
size_t i = 0;
file << "# Species: ";
for (size_t k = 0; k < numSpecies; ++k) {
fourdst::atomic::Species species = std::ranges::find_if(
m_speciesToIndexMap,
[k](const auto& pair) {
return pair.second == k;
})->first;
file << species.name();
if (i < numSpecies - 1) {
file << ",";
}
i++;
}
const std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
const std::time_t now_c = std::chrono::system_clock::to_time_t(now);
file << "\n";
file << "# Rows: " << numSpecies << ", Columns: " << numSpecies << "\n";
file << "# Generated on " << std::ctime(&now_c) << "\n";
file << "# Generated by GridFire NetworkJacobian::to_csv\n";
for (size_t row = 0; row < numSpecies; ++row) {
for (size_t col = 0; col < numSpecies; ++col) {
file << this->operator()(row, col);
if (col < numSpecies - 1) {
file << ",";
}
}
file << "\n";
}
file.close();
}
}
NetworkJacobian regularize_jacobian(
const NetworkJacobian& jacobian,
const fourdst::composition::CompositionAbstract& comp,
const std::optional<quill::Logger*> logger
) {
const std::vector<JacobianEntry> infs = jacobian.infs();
const std::vector<JacobianEntry> nans = jacobian.nans();
if (infs.size() == 0 && nans.size() == 0) {
return jacobian;
}
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;
}
} }