fix(weakRates): major progress in resolving bugs

bigs were introduced by the interface change from accepting raw molar abundance vectors to using the composition vector. This commit resolves many of these, including preformant ways to report that a species is not present in the composition and unified index lookups using composition object tooling.

BREAKING CHANGE:
This commit is contained in:
2025-10-10 09:12:40 -04:00
parent 13e2ea9ffa
commit 2f1077c02d
21 changed files with 17953 additions and 375 deletions

View File

@@ -18,6 +18,7 @@
#include <vector> #include <vector>
#include <memory> #include <memory>
#include <ranges> #include <ranges>
#include <functional>
#include <boost/numeric/ublas/matrix_sparse.hpp> #include <boost/numeric/ublas/matrix_sparse.hpp>
@@ -707,6 +708,7 @@ namespace gridfire {
* @param rho Density in g/cm^3. * @param rho Density in g/cm^3.
* @param Ye * @param Ye
* @param mue * @param mue
* @param speciesIDLookup
* @return Molar flow rate for the reaction (e.g., mol/g/s). * @return Molar flow rate for the reaction (e.g., mol/g/s).
* *
* This method computes the net rate at which the given reaction proceeds * This method computes the net rate at which the given reaction proceeds
@@ -716,14 +718,17 @@ namespace gridfire {
T calculateMolarReactionFlow( T calculateMolarReactionFlow(
const reaction::Reaction &reaction, const reaction::Reaction &reaction,
const std::vector<T>& Y, const std::vector<T>& Y,
const T T9, T T9,
const T rho, T Ye, T mue T rho,
T Ye,
T mue,
const std::function<std::optional<size_t>(const fourdst::atomic::Species &)>&speciesIDLookup
) const; ) const;
template<IsArithmeticOrAD T> template<IsArithmeticOrAD T>
T calculateReverseMolarReactionFlow( T calculateReverseMolarReactionFlow(
const T T9, T T9,
const T rho, T rho,
std::vector<T> screeningFactors, std::vector<T> screeningFactors,
const std::vector<T>& Y, const std::vector<T>& Y,
size_t reactionIndex, size_t reactionIndex,
@@ -739,6 +744,7 @@ namespace gridfire {
* @param rho Density in g/cm^3. * @param rho Density in g/cm^3.
* @param Ye * @param Ye
* @param mue * @param mue
* @param speciesLookup
* @return StepDerivatives<T> containing dY/dt and energy generation rate. * @return StepDerivatives<T> containing dY/dt and energy generation rate.
* *
* This method calculates the time derivatives of all species and the * This method calculates the time derivatives of all species and the
@@ -748,7 +754,10 @@ namespace gridfire {
[[nodiscard]] StepDerivatives<T> calculateAllDerivatives( [[nodiscard]] StepDerivatives<T> calculateAllDerivatives(
const std::vector<T>& Y_in, const std::vector<T>& Y_in,
T T9, T T9,
T rho, T Ye, T mue T rho,
T Ye,
T mue,
std::function<std::optional<size_t>(const fourdst::atomic::Species &)> speciesLookup
) const; ) const;
// /** // /**
@@ -869,7 +878,8 @@ namespace gridfire {
const T T9, const T T9,
const T rho, const T rho,
const T Ye, const T Ye,
const T mue const T mue,
const std::function<std::optional<size_t>(const fourdst::atomic::Species &)> speciesLookup
) const { ) const {
std::vector<T> screeningFactors = m_screeningModel->calculateScreeningFactors( std::vector<T> screeningFactors = m_screeningModel->calculateScreeningFactors(
m_reactions, m_reactions,
@@ -913,16 +923,21 @@ namespace gridfire {
const T N_A = static_cast<T>(m_constants.Na); // Avogadro's number in mol^-1 const T N_A = static_cast<T>(m_constants.Na); // Avogadro's number in mol^-1
const T c = static_cast<T>(m_constants.c); // Speed of light in cm/s const T c = static_cast<T>(m_constants.c); // Speed of light in cm/s
// TODO: It may be prudent to introduce assertions here which validate units but will be removed in release builds (to ensure that unit inconsistencies do not creep in during future development)
// libconstants already has units built in so this should be straightforward.
// --- SINGLE LOOP OVER ALL REACTIONS --- // --- SINGLE LOOP OVER ALL REACTIONS ---
for (size_t reactionIndex = 0; reactionIndex < m_reactions.size(); ++reactionIndex) { for (size_t reactionIndex = 0; reactionIndex < m_reactions.size(); ++reactionIndex) {
const auto& reaction = m_reactions[reactionIndex]; const auto& reaction = m_reactions[reactionIndex];
// 1. Calculate forward reaction rate // 1. Calculate forward reaction rate
const T forwardMolarReactionFlow = screeningFactors[reactionIndex] * const T forwardMolarReactionFlow = screeningFactors[reactionIndex] *
calculateMolarReactionFlow<T>(reaction, Y, T9, rho, Ye, mue); calculateMolarReactionFlow<T>(
reaction,
Y,
T9,
rho,
Ye,
mue,
speciesLookup
);
// 2. Calculate reverse reaction rate // 2. Calculate reverse reaction rate
T reverseMolarFlow = static_cast<T>(0.0); T reverseMolarFlow = static_cast<T>(0.0);
@@ -965,7 +980,8 @@ namespace gridfire {
const T T9, const T T9,
const T rho, const T rho,
const T Ye, const T Ye,
const T mue const T mue,
const std::function<std::optional<size_t>(const fourdst::atomic::Species &)>& speciesIDLookup
) const { ) const {
// --- Pre-setup (flags to control conditionals in an AD safe / branch aware manner) --- // --- Pre-setup (flags to control conditionals in an AD safe / branch aware manner) ---
@@ -989,10 +1005,12 @@ namespace gridfire {
// --- Loop through each unique reactant species and calculate the molar concentration for that species then multiply that into the accumulator --- // --- Loop through each unique reactant species and calculate the molar concentration for that species then multiply that into the accumulator ---
for (const auto& [species_name, count] : reactant_counts) { for (const auto& [species_name, count] : reactant_counts) {
// --- Resolve species to molar abundance --- // --- Resolve species to molar abundance ---
// PERF: Could probably optimize out this lookup // TODO: We need some way to handle the case when a species in the reaction is not part of the composition being tracked
const auto species_it = m_speciesToIndexMap.find(m_networkSpeciesMap.at(species_name)); const std::optional<size_t> species_index = speciesIDLookup(m_networkSpeciesMap.at(species_name));
const size_t species_index = species_it->second; if (!species_index.has_value()) {
const T Yi = Y[species_index]; return static_cast<T>(0.0); // If any reactant is not present, the reaction cannot proceed
}
const T Yi = Y[species_index.value()];
// --- If count is > 1 , we need to raise the molar concentration to the power of count since there are really count bodies in that reaction --- // --- If count is > 1 , we need to raise the molar concentration to the power of count since there are really count bodies in that reaction ---
molar_concentration_product *= CppAD::pow(Yi, static_cast<T>(count)); // ni^count molar_concentration_product *= CppAD::pow(Yi, static_cast<T>(count)); // ni^count

View File

@@ -2,6 +2,7 @@
#include <exception> #include <exception>
#include <string> #include <string>
#include <utility>
#include <vector> #include <vector>
namespace gridfire::exceptions { namespace gridfire::exceptions {
@@ -17,41 +18,41 @@ namespace gridfire::exceptions {
int m_total_steps; int m_total_steps;
double m_eps_nuc; double m_eps_nuc;
}; };
explicit StaleEngineTrigger(const state &s) explicit StaleEngineTrigger(state s)
: m_state(s) {} : m_state(std::move(s)) {}
const char* what() const noexcept override{ [[nodiscard]] const char* what() const noexcept override{
return "Engine reports stale state. This means that the caller should trigger a update of the engine state before continuing with the integration. If you as an end user are seeing this error, it is likely a bug in the code that should be reported. Please provide the input parameters and the context in which this error occurred. Thank you for your help!"; return "Engine reports stale state. This means that the caller should trigger a update of the engine state before continuing with the integration. If you as an end user are seeing this error, it is likely a bug in the code that should be reported. Please provide the input parameters and the context in which this error occurred. Thank you for your help!";
} }
state getState() const { [[nodiscard]] state getState() const {
return m_state; return m_state;
} }
size_t numSpecies() const { [[nodiscard]] size_t numSpecies() const {
return m_state.m_Y.size(); return m_state.m_Y.size();
} }
size_t totalSteps() const { [[nodiscard]] size_t totalSteps() const {
return m_state.m_total_steps; return m_state.m_total_steps;
} }
double energy() const { [[nodiscard]] double energy() const {
return m_state.m_eps_nuc; return m_state.m_eps_nuc;
} }
double getMolarAbundance(const size_t index) const { [[nodiscard]] double getMolarAbundance(const size_t index) const {
if (index > m_state.m_Y.size() - 1) { if (index > m_state.m_Y.size() - 1) {
throw std::out_of_range("Index out of bounds for molar abundance vector."); throw std::out_of_range("Index out of bounds for molar abundance vector.");
} }
return m_state.m_Y[index]; return m_state.m_Y[index];
} }
double temperature() const { [[nodiscard]] double temperature() const {
return m_state.m_T9 * 1e9; // Convert T9 back to Kelvin return m_state.m_T9 * 1e9; // Convert T9 back to Kelvin
} }
double density() const { [[nodiscard]] double density() const {
return m_state.m_rho; return m_state.m_rho;
} }
private: private:
@@ -61,10 +62,10 @@ namespace gridfire::exceptions {
class StaleEngineError final : public EngineError { class StaleEngineError final : public EngineError {
public: public:
explicit StaleEngineError(const std::string& message) explicit StaleEngineError(std::string message)
: m_message(message) {} : m_message(std::move(message)) {}
const char* what() const noexcept override { [[nodiscard]] const char* what() const noexcept override {
return m_message.c_str(); return m_message.c_str();
} }
@@ -74,10 +75,10 @@ namespace gridfire::exceptions {
class FailedToPartitionEngineError final : public EngineError { class FailedToPartitionEngineError final : public EngineError {
public: public:
explicit FailedToPartitionEngineError(const std::string& message) explicit FailedToPartitionEngineError(std::string message)
: m_message(message) {} : m_message(std::move(message)) {}
const char* what() const noexcept override { [[nodiscard]] const char* what() const noexcept override {
return m_message.c_str(); return m_message.c_str();
} }
private: private:
@@ -86,10 +87,10 @@ namespace gridfire::exceptions {
class NetworkResizedError final : public EngineError { class NetworkResizedError final : public EngineError {
public: public:
explicit NetworkResizedError(const std::string& message) explicit NetworkResizedError(std::string message)
: m_message(message) {} : m_message(std::move(message)) {}
const char* what() const noexcept override { [[nodiscard]] const char* what() const noexcept override {
return m_message.c_str(); return m_message.c_str();
} }
private: private:
@@ -98,10 +99,10 @@ namespace gridfire::exceptions {
class UnableToSetNetworkReactionsError final : public EngineError { class UnableToSetNetworkReactionsError final : public EngineError {
public: public:
explicit UnableToSetNetworkReactionsError(const std::string& message) explicit UnableToSetNetworkReactionsError(std::string message)
: m_message(message) {} : m_message(std::move(message)) {}
const char* what() const noexcept override { [[nodiscard]] const char* what() const noexcept override {
return m_message.c_str(); return m_message.c_str();
} }

View File

@@ -42,7 +42,7 @@ namespace gridfire::expectations {
} }
}; };
struct StaleEngineError : EngineError { struct StaleEngineError final : EngineError {
StaleEngineErrorTypes staleType; StaleEngineErrorTypes staleType;
explicit StaleEngineError(const StaleEngineErrorTypes sType) explicit StaleEngineError(const StaleEngineErrorTypes sType)

View File

@@ -6,7 +6,6 @@
#include "fourdst/logging/logging.h" #include "fourdst/logging/logging.h"
#include <string> #include <string>
#include <unordered_map>
#include <vector> #include <vector>
#include <memory> #include <memory>
@@ -99,6 +98,6 @@ namespace gridfire::partition {
* @return Unique pointer to a new PartitionFunction instance of the given type. * @return Unique pointer to a new PartitionFunction instance of the given type.
* @throws std::runtime_error If the given type is not recognized. * @throws std::runtime_error If the given type is not recognized.
+ */ + */
[[nodiscard]] std::unique_ptr<PartitionFunction> selectPartitionFunction(const BasePartitionType type) const; [[nodiscard]] std::unique_ptr<PartitionFunction> selectPartitionFunction(BasePartitionType type) const;
}; };
} }

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#define GRIDFIRE_WEAK_REACTION_LIB_SENTINEL -60.0 #define GRIDFIRE_WEAK_REACTION_LIB_SENTINEL (-60.0)
#include "gridfire/reaction/reaction.h" #include "gridfire/reaction/reaction.h"
#include "gridfire/reaction/weak/weak_types.h" #include "gridfire/reaction/weak/weak_types.h"
@@ -541,22 +541,75 @@ namespace gridfire::rates::weak {
m_atomic(ax, ay); m_atomic(ax, ay);
rateConstant = static_cast<T>(ay[0]); rateConstant = static_cast<T>(ay[0]);
} else { // The case where T is of type double } else { // The case where T is of type double
const std::expected<WeakRatePayload, InterpolationError> result = m_interpolator.get_rates( std::expected<WeakRatePayload, InterpolationError> result = m_interpolator.get_rates(
static_cast<uint16_t>(m_reactant_a), static_cast<uint16_t>(m_reactant_a),
static_cast<uint8_t>(m_reactant_z), static_cast<uint8_t>(m_reactant_z),
T9, T9,
log_rhoYe, log_rhoYe
mue
); );
// TODO: Clean this up. When a bit of code needs this many comments to make it clear it is bad code
if (!result.has_value()) {
bool okayToClamp = true;
const auto&[errorType, boundsErrorInfo] = result.error();
// The logic here is
// 1. If there is any bounds error in T9 then we do not allow clamping. T9 should be a large enough grid
// that the user should not be asking for values outside the grid.
// 2. If there is no bounds error in T9, but there is a bounds error in log_rhoYe, then we only allow
// clamping if the query value is below the minimum of the grid. If it is above the maximum
// of the grid, then we do not allow clamping. The reason for this is that at high density,
// screening and other effects can make a significant difference to the rates, and
// the user should be aware that they are asking for a value outside the grid.
// There are a couple of safety asserts in here that are only active in debug builds. These are to
// ensure that our assumptions about the error information are correct. These should really never
// be triggered, but if they are, they will help us to identify any issues.
if (errorType == InterpolationErrorType::BOUNDS_ERROR) {
assert(boundsErrorInfo.has_value()); // must be true if type is BOUNDS_ERROR, removed in release builds
if (boundsErrorInfo->contains(TableAxes::T9)) {
okayToClamp = false;
} else {
assert(boundsErrorInfo->contains(TableAxes::LOG_RHOYE)); // must be true if T9 is not, removed in release builds
const BoundsErrorInfo& boundsError = boundsErrorInfo->at(TableAxes::LOG_RHOYE);
if (boundsError.queryValue > boundsError.axisMaxValue) {
okayToClamp = false;
}
assert(boundsError.queryValue < boundsError.axisMinValue); // Given the above logic, this must be true, removed in release builds
}
}
if (!okayToClamp) {
const InterpolationErrorType type = result.error().type;
const std::string msg = std::format(
"Failed to interpolate weak rate for {} (A={}, Z={}) at T9={}, log10(rho*Ye)={}, with error: {}. Clamping disallowed due to either query value being out of bounds in T9 or being above the maximum in log10(rho*Ye).",
m_reactant.name(), m_reactant_a, m_reactant_z, T9, log_rhoYe, InterpolationErrorTypeMap.at(type)
);
throw std::runtime_error(msg);
}
// In the case we get here the error was a bounds error in log_rhoYe and the query value was below the minimum of the grid
// so the solution is to clamp the query value to the minimum of the grid and try again.
result = m_interpolator.get_rates(
static_cast<uint16_t>(m_reactant_a),
static_cast<uint8_t>(m_reactant_z),
T9,
boundsErrorInfo->at(TableAxes::LOG_RHOYE).axisMinValue
);
// Check the result again. If it fails this time then we have a real problem and we throw.
if (!result.has_value()) { if (!result.has_value()) {
const InterpolationErrorType type = result.error().type; const InterpolationErrorType type = result.error().type;
const std::string msg = std::format( const std::string msg = std::format(
"Failed to interpolate weak rate for (A={}, Z={}) at T9={}, log10(rho*Ye)={}, mu_e={} with error: {}", "After clamping, failed to interpolate weak rate for {} (A={}, Z={}) at T9={}, log10(rho*Ye)={}, with error: {}",
m_reactant.name(), m_reactant_a, m_reactant_z, T9, log_rhoYe, mue, InterpolationErrorTypeMap.at(type) m_reactant.name(), m_reactant_a, m_reactant_z, T9, log_rhoYe, InterpolationErrorTypeMap.at(type)
); );
throw std::runtime_error(msg); throw std::runtime_error(msg);
} }
}
const WeakRatePayload payload = result.value(); const WeakRatePayload payload = result.value();
const double logRate = get_log_rate_from_payload(payload); const double logRate = get_log_rate_from_payload(payload);
if (logRate <= GRIDFIRE_WEAK_REACTION_LIB_SENTINEL) { if (logRate <= GRIDFIRE_WEAK_REACTION_LIB_SENTINEL) {

View File

@@ -2,6 +2,7 @@
#include "gridfire/reaction/weak/weak_types.h" #include "gridfire/reaction/weak/weak_types.h"
#include "fourdst/composition/atomicSpecies.h" #include "fourdst/composition/atomicSpecies.h"
#include "fourdst/logging/logging.h"
#include <unordered_map> #include <unordered_map>
#include <cstdint> #include <cstdint>
@@ -66,7 +67,6 @@ namespace gridfire::rates::weak {
* @param Z Proton number of the isotope. * @param Z Proton number of the isotope.
* @param t9 Temperature in GK (10^9 K). * @param t9 Temperature in GK (10^9 K).
* @param log_rhoYe Log10 of rho*Ye (cgs density times electron fraction). * @param log_rhoYe Log10 of rho*Ye (cgs density times electron fraction).
* @param mu_e Electron chemical potential (MeV).
* @return expected<WeakRatePayload, InterpolationError>: payload on success; * @return expected<WeakRatePayload, InterpolationError>: payload on success;
* InterpolationError::UNKNOWN_SPECIES_ERROR if (A,Z) not present; or * InterpolationError::UNKNOWN_SPECIES_ERROR if (A,Z) not present; or
* InterpolationError::BOUNDS_ERROR if any coordinate is outside the table * InterpolationError::BOUNDS_ERROR if any coordinate is outside the table
@@ -84,8 +84,7 @@ namespace gridfire::rates::weak {
uint16_t A, uint16_t A,
uint8_t Z, uint8_t Z,
double t9, double t9,
double log_rhoYe, double log_rhoYe
double mu_e
) const; ) const;
/** /**
@@ -100,7 +99,6 @@ namespace gridfire::rates::weak {
* @param Z Proton number of the isotope. * @param Z Proton number of the isotope.
* @param t9 Temperature in GK (10^9 K). * @param t9 Temperature in GK (10^9 K).
* @param log_rhoYe Log10 of rho*Ye (cgs density times electron fraction). * @param log_rhoYe Log10 of rho*Ye (cgs density times electron fraction).
* @param mu_e Electron chemical potential (MeV).
* @return expected<WeakRateDerivatives, InterpolationError>: derivative payload on success; * @return expected<WeakRateDerivatives, InterpolationError>: derivative payload on success;
* otherwise an InterpolationError as described above. * otherwise an InterpolationError as described above.
* @par Example * @par Example
@@ -114,10 +112,10 @@ namespace gridfire::rates::weak {
uint16_t A, uint16_t A,
uint8_t Z, uint8_t Z,
double t9, double t9,
double log_rhoYe, double log_rhoYe
double mu_e
) const; ) const;
private: private:
quill::Logger* m_logger = fourdst::logging::LogManager::getInstance().getLogger("log");
/** /**
* @brief Pack (A,Z) into a 32-bit key used for the internal map. * @brief Pack (A,Z) into a 32-bit key used for the internal map.
* *

View File

@@ -93,18 +93,18 @@ namespace gridfire::rates::weak {
}; };
/** /**
* @brief Partial derivatives of the log10() fields w.r.t. (T9, log10(rho*Ye), mu_e). * @brief Partial derivatives of the log10() fields w.r.t. (T9, log10(rho*Ye)).
* *
* Array ordering is [d/dT9, d/dlogRhoYe, d/dMuE] for each corresponding field. * Array ordering is [d/dT9, d/dlogRhoYe] for each corresponding field.
*/ */
struct WeakRateDerivatives { struct WeakRateDerivatives {
// Each array holds [d/dT9, d/dlogRhoYe, d/dMuE] // Each array holds [d/dT9, d/dlogRhoYe, d/dMuE]
std::array<double, 3> d_log_beta_plus; std::array<double, 2> d_log_beta_plus;
std::array<double, 3> d_log_electron_capture; std::array<double, 2> d_log_electron_capture;
std::array<double, 3> d_log_neutrino_loss_ec; std::array<double, 2> d_log_neutrino_loss_ec;
std::array<double, 3> d_log_beta_minus; std::array<double, 2> d_log_beta_minus;
std::array<double, 3> d_log_positron_capture; std::array<double, 2> d_log_positron_capture;
std::array<double, 3> d_log_antineutrino_loss_bd; std::array<double, 2> d_log_antineutrino_loss_bd;
}; };
/** /**
@@ -133,6 +133,19 @@ namespace gridfire::rates::weak {
LOG_RHOYE, ///< log10(rho*Ye). LOG_RHOYE, ///< log10(rho*Ye).
MUE ///< Electron chemical potential (MeV). MUE ///< Electron chemical potential (MeV).
}; };
}
// This need to be here to avoid compiler issues related to the order of specialization
namespace std {
template <>
struct hash<gridfire::rates::weak::TableAxes> {
std::size_t operator()(gridfire::rates::weak::TableAxes t) const noexcept {
return std::hash<int>()(static_cast<int>(t));
}
};
}
namespace gridfire::rates::weak {
/** /**
* @brief Detailed bounds information for a BOUNDS_ERROR. * @brief Detailed bounds information for a BOUNDS_ERROR.
@@ -154,20 +167,20 @@ namespace gridfire::rates::weak {
std::optional<std::unordered_map<TableAxes, BoundsErrorInfo>> boundsErrorInfo = std::nullopt; std::optional<std::unordered_map<TableAxes, BoundsErrorInfo>> boundsErrorInfo = std::nullopt;
}; };
/** /**
* @brief Regular 3D grid and payloads for a single isotope (A,Z). * @brief Regular 2D grid and payloads for a single isotope (A,Z).
* *
* Axes are monotonically increasing per dimension. Data vector is laid out in * Axes are monotonically increasing per dimension. Data vector is laid out in
* row-major order with index computed as: * row-major order with index computed as:
* index = ((i_t9 * rhoYe_axis.size() + j_rhoYe) * mue_axis.size()) + k_mue *
* index = i_t9 * N_rhoYe + j_rhoYe
*
*/ */
struct IsotopeGrid { struct IsotopeGrid {
std::vector<double> t9_axis; ///< Unique sorted T9 grid. std::vector<double> t9_axis; ///< Unique sorted T9 grid.
std::vector<double> rhoYe_axis;///< Unique sorted log10(rho*Ye) grid. std::vector<double> rhoYe_axis; ///< Unique sorted log10(rho*Ye) grid.
std::vector<double> mue_axis; ///< Unique sorted mu_e grid. std::vector<WeakRatePayload> data; ///< MuE axis for each (T9, log_rhoYe) pair (the table is ragged in mu_e). This is also where the payloads are stored.
// index = ((i_t9 * rhoYe_axis.size() + j_rhoYe) * mue_axis.size()) + k_mue
std::vector<WeakRatePayload> data; ///< Payloads at each grid node.
}; };
/** /**
@@ -217,3 +230,4 @@ namespace gridfire::rates::weak {
} }
}; };
} }

View File

@@ -21,11 +21,15 @@
// Include headers for linear solvers and N_Vectors // Include headers for linear solvers and N_Vectors
// We will use preprocessor directives to select the correct ones // We will use preprocessor directives to select the correct ones
#include <cvode/cvode.h> // For CVDls (serial dense linear solver)
#include <sundials/sundials_context.h> #include <sundials/sundials_context.h>
#include <sunmatrix/sunmatrix_dense.h> #include <sunmatrix/sunmatrix_dense.h>
#include <sunlinsol/sunlinsol_dense.h> #include <sunlinsol/sunlinsol_dense.h>
// These are the possible N_Vector implementations. We use the compiler defines to select the most appropriate one for the build.
// If none are defined, we default to serial.
// For precompiled binaries we will need to ensure that we have versions built for all three types (ideally with some runtime
// checks that will fail gracefully if the user tries to use an unsupported type).
#ifdef SUNDIALS_HAVE_OPENMP #ifdef SUNDIALS_HAVE_OPENMP
#include <nvector/nvector_openmp.h> #include <nvector/nvector_openmp.h>
#endif #endif
@@ -103,7 +107,7 @@ namespace gridfire::solver {
* if present after a step, it is rethrown for upstream handling. * if present after a step, it is rethrown for upstream handling.
* - Prints/collects diagnostics per step (step size, energy, solver iterations). * - Prints/collects diagnostics per step (step size, energy, solver iterations).
* - On trigger activation, rebuilds CVODE resources to reflect a changed network and * - On trigger activation, rebuilds CVODE resources to reflect a changed network and
* reinitializes the state using the latest engine composition, preserving energy. * reinitialized the state using the latest engine composition, preserving energy.
* - At the end, converts molar abundances to mass fractions and assembles NetOut, * - At the end, converts molar abundances to mass fractions and assembles NetOut,
* including derivatives of energy w.r.t. T and rho from the engine. * including derivatives of energy w.r.t. T and rho from the engine.
* *
@@ -250,7 +254,7 @@ namespace gridfire::solver {
* @brief Compute and print per-component error ratios; run diagnostic helpers. * @brief Compute and print per-component error ratios; run diagnostic helpers.
* *
* Gathers CVODE's estimated local errors, converts the state to a Composition, and prints a * Gathers CVODE's estimated local errors, converts the state to a Composition, and prints a
* sorted table of species with highest error ratios; then invokes diagnostic routines to * sorted table of species with the highest error ratios; then invokes diagnostic routines to
* inspect Jacobian stiffness and species balance. * inspect Jacobian stiffness and species balance.
*/ */
void log_step_diagnostics(const CVODEUserData& user_data) const; void log_step_diagnostics(const CVODEUserData& user_data) const;

View File

@@ -1,10 +1,9 @@
#pragma once #pragma once
#include "gridfire/engine/engine_abstract.h" #include "gridfire/engine/engine_abstract.h"
#include "fourdst/composition/composition.h"
#include <string> #include <string>
#include <vector>
namespace gridfire::utils { namespace gridfire::utils {
/** /**
@@ -17,7 +16,7 @@ namespace gridfire::utils {
* *
* @param engine A constant reference to a `DynamicEngine` object, used to * @param engine A constant reference to a `DynamicEngine` object, used to
* calculate the species timescales. * calculate the species timescales.
* @param Y A vector of the molar abundances (mol/g) for each species. * @param composition The current composition of the plasma
* @param T9 The temperature in units of 10^9 K. * @param T9 The temperature in units of 10^9 K.
* @param rho The plasma density in g/cm^3. * @param rho The plasma density in g/cm^3.
* @return A std::string containing the formatted table of species and their * @return A std::string containing the formatted table of species and their

View File

@@ -79,7 +79,19 @@ namespace gridfire {
// --- 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); return calculateAllDerivativesUsingPrecomputation(comp, bare_rates, bare_reverse_rates, T9, rho);
} else { } else {
return calculateAllDerivatives<double>(comp.getMolarAbundanceVector(), T9, rho, Ye, mue); return calculateAllDerivatives<double>(
comp.getMolarAbundanceVector(),
T9,
rho,
Ye,
mue,
[&comp](const fourdst::atomic::Species& species) -> std::optional<size_t> {
if (comp.contains(species)) {
return comp.getSpeciesIndex(species); // Return the index of the species in the composition
}
return std::nullopt; // Species not found in the composition
}
);
} }
} }
@@ -305,11 +317,18 @@ namespace gridfire {
return 0.0; // If reverse reactions are not used, return 0.0 return 0.0; // If reverse reactions are not used, return 0.0
} }
const double temp = T9 * 1e9; // Convert T9 to Kelvin const double temp = T9 * 1e9; // Convert T9 to Kelvin
const double Ye = comp.getElectronAbundance();
// TODO: This is a dummy value for the electron chemical potential. We eventually need to replace this with an EOS call. // Reverse reactions are only relevant for strong reactions (at least during the vast majority of stellar evolution)
const double mue = 5.0e-3 * std::pow(rho * Ye, 1.0 / 3.0) + 0.5 * T9; // So here we just let these be dummy values since we know
// 1. The reaction should always be strong
// 2. The strong reaction rate is independent of Ye and mue
//
// In development builds the assert below will confirm this
constexpr double Ye = 0.0;
constexpr double mue = 0.0;
// It is a logic error to call this function on a weak reaction
assert(reaction.type() != gridfire::reaction::ReactionType::WEAK);
// In debug builds we check the units on kB to ensure it is in erg/K. This is removed in release builds to avoid overhead. (Note assert is a no-op in release builds) // In debug builds we check the units on kB to ensure it is in erg/K. This is removed in release builds to avoid overhead. (Note assert is a no-op in release builds)
assert(Constants::getInstance().get("kB").unit == "erg / K"); assert(Constants::getInstance().get("kB").unit == "erg / K");
@@ -317,7 +336,8 @@ namespace gridfire {
const double kBMeV = m_constants.kB * 624151; // Convert kB to MeV/K NOTE: This relies on the fact that m_constants.kB is in erg/K! const double kBMeV = m_constants.kB * 624151; // Convert kB to MeV/K NOTE: This relies on the fact that m_constants.kB is in erg/K!
const double expFactor = std::exp(-reaction.qValue() / (kBMeV * temp)); const double expFactor = std::exp(-reaction.qValue() / (kBMeV * temp));
double reverseRate = 0.0; double reverseRate = 0.0;
const double forwardRate = reaction.calculate_rate(T9, rho, Ye, mue, comp.getMolarAbundanceVector(), m_indexToSpeciesMap); // We also let Y be an empy vector since the strong reaction rate is independent of Y
const double forwardRate = reaction.calculate_rate(T9, rho, Ye, mue, {}, m_indexToSpeciesMap);
if (reaction.reactants().size() == 2 && reaction.products().size() == 2) { if (reaction.reactants().size() == 2 && reaction.products().size() == 2) {
reverseRate = calculateReverseRateTwoBody(reaction, T9, forwardRate, expFactor); reverseRate = calculateReverseRateTwoBody(reaction, T9, forwardRate, expFactor);
@@ -696,10 +716,20 @@ namespace gridfire {
const double Ye = comp.getElectronAbundance(); const double Ye = comp.getElectronAbundance();
// TODO: This is a dummy placeholder which must be replaced with an EOS call return calculateMolarReactionFlow<double>(
const double mue = 5.0e-3 * std::pow(rho * Ye, 1.0 / 3.0) + 0.5 * T9; reaction,
comp.getMolarAbundanceVector(),
return calculateMolarReactionFlow<double>(reaction, comp.getMolarAbundanceVector(), T9, rho, Ye, mue); T9,
rho,
Ye,
0.0,
[&comp](const fourdst::atomic::Species& species) -> std::optional<size_t> {
if (comp.contains(species)) { // Species present in the composition
return comp.getSpeciesIndex(species);
}
return std::nullopt; // Species not present
}
);
} }
void GraphEngine::generateJacobianMatrix( void GraphEngine::generateJacobianMatrix(
@@ -908,10 +938,20 @@ namespace gridfire {
const double rho const double rho
) const { ) const {
const double Ye = comp.getElectronAbundance(); const double Ye = comp.getElectronAbundance();
// TODO: This is a dummy placeholder which must be replaced with an EOS call
const double mue = 5.0e-3 * std::pow(rho * Ye, 1.0 / 3.0) + 0.5 * T9;
auto [dydt, _] = calculateAllDerivatives<double>(comp.getMolarAbundanceVector(), T9, rho, Ye, mue); auto [dydt, _] = calculateAllDerivatives<double>(
comp.getMolarAbundanceVector(),
T9,
rho,
Ye,
0.0,
[&comp](const fourdst::atomic::Species& species) -> std::optional<size_t> {
if (comp.contains(species)) { // Species present in the composition
return comp.getSpeciesIndex(species);
}
return std::nullopt; // Species not present
}
);
std::unordered_map<fourdst::atomic::Species, double> speciesTimescales; std::unordered_map<fourdst::atomic::Species, double> speciesTimescales;
speciesTimescales.reserve(m_networkSpecies.size()); speciesTimescales.reserve(m_networkSpecies.size());
for (const auto& species : m_networkSpecies) { for (const auto& species : m_networkSpecies) {
@@ -930,17 +970,39 @@ namespace gridfire {
const double rho const double rho
) const { ) const {
const double Ye = comp.getElectronAbundance(); const double Ye = comp.getElectronAbundance();
// TODO: This is a dummy placeholder which must be replaced with an EOS call const std::vector<double>& Y = comp.getMolarAbundanceVector();
const double mue = 5.0e-3 * std::pow(rho * Ye, 1.0 / 3.0) + 0.5 * T9;
auto speciesLookup = [&comp](const fourdst::atomic::Species& species) -> std::optional<size_t> {
if (comp.contains(species)) { // Species present in the composition
return comp.getSpeciesIndex(species);
}
return std::nullopt; // Species not present
};
auto [dydt, _] = calculateAllDerivatives<double>(
Y,
T9,
rho,
Ye,
0.0,
speciesLookup
);
auto [dydt, _] = calculateAllDerivatives<double>(comp.getMolarAbundanceVector(), T9, rho, Ye, mue);
std::unordered_map<fourdst::atomic::Species, double> speciesDestructionTimescales; std::unordered_map<fourdst::atomic::Species, double> speciesDestructionTimescales;
speciesDestructionTimescales.reserve(m_networkSpecies.size()); speciesDestructionTimescales.reserve(m_networkSpecies.size());
for (const auto& species : m_networkSpecies) { for (const auto& species : m_networkSpecies) {
double netDestructionFlow = 0.0; double netDestructionFlow = 0.0;
for (const auto& reaction : m_reactions) { for (const auto& reaction : m_reactions) {
if (reaction->stoichiometry(species) < 0) { if (reaction->stoichiometry(species) < 0) {
const auto flow = calculateMolarReactionFlow<double>(*reaction, comp.getMolarAbundanceVector(), T9, rho, Ye, mue); const auto flow = calculateMolarReactionFlow<double>(
*reaction,
Y,
T9,
rho,
Ye,
0.0,
speciesLookup
);
netDestructionFlow += flow; netDestructionFlow += flow;
} }
} }
@@ -979,7 +1041,7 @@ namespace gridfire {
m_logger->flush_log(); m_logger->flush_log();
throw std::runtime_error("Cannot record AD tape: No species in the network."); throw std::runtime_error("Cannot record AD tape: No species in the network.");
} }
const size_t numADInputs = numSpecies + 2; // Note here that by not letting T9 and rho be independent variables, we are constraining the network to a constant temperature and density during each evaluation. const size_t numADInputs = numSpecies + 2; // Y + T9 + rho
// --- CppAD Tape Recording --- // --- CppAD Tape Recording ---
// 1. Declare independent variable (adY) // 1. Declare independent variable (adY)
@@ -1004,13 +1066,22 @@ namespace gridfire {
const CppAD::AD<double> adRho = adInput[numSpecies + 1]; const CppAD::AD<double> adRho = adInput[numSpecies + 1];
// Dummy values for Ye and mue to let taping happen // Dummy values for Ye and mue to let taping happen
const CppAD::AD<double> adYe = 1.0; const CppAD::AD<double> adYe = 1e6;
const CppAD::AD<double> adMue = 1.0; const CppAD::AD<double> adMue = 10.0;
// 5. Call the actual templated function // 5. Call the actual templated function
// We let T9 and rho be constant, so we pass them as fixed values. // We let T9 and rho be constant, so we pass them as fixed values.
auto [dydt, nuclearEnergyGenerationRate] = calculateAllDerivatives<CppAD::AD<double>>(adY, adT9, adRho, adYe, adMue); auto [dydt, nuclearEnergyGenerationRate] = calculateAllDerivatives<CppAD::AD<double>>(
adY,
adT9,
adRho,
adYe,
adMue,
[&](const fourdst::atomic::Species& querySpecies) -> size_t {
return m_speciesToIndexMap.at(querySpecies); // TODO: This is bad, needs to be fixed
}
);
// Extract the raw vector from the associative map // Extract the raw vector from the associative map
std::vector<CppAD::AD<double>> dydt_vec; std::vector<CppAD::AD<double>> dydt_vec;
@@ -1049,10 +1120,19 @@ namespace gridfire {
const CppAD::AD<double> adRho = adInput[numSpecies + 1]; const CppAD::AD<double> adRho = adInput[numSpecies + 1];
// Dummy values for Ye and mue to let taping happen // Dummy values for Ye and mue to let taping happen
const CppAD::AD<double> adYe = 1.0; const CppAD::AD<double> adYe = 1e6;
const CppAD::AD<double> adMue = 1.0; const CppAD::AD<double> adMue = 1.0;
auto [dydt, nuclearEnergyGenerationRate] = calculateAllDerivatives<CppAD::AD<double>>(adY, adT9, adRho, adYe, adMue); auto [dydt, nuclearEnergyGenerationRate] = calculateAllDerivatives<CppAD::AD<double>>(
adY,
adT9,
adRho,
adYe,
adMue,
[&](const fourdst::atomic::Species& querySpecies) -> size_t {
return m_speciesToIndexMap.at(querySpecies); // TODO: This is bad, needs to be fixed
}
);
std::vector<CppAD::AD<double>> adOutput(1); std::vector<CppAD::AD<double>> adOutput(1);
adOutput[0] = nuclearEnergyGenerationRate; adOutput[0] = nuclearEnergyGenerationRate;
@@ -1157,8 +1237,10 @@ namespace gridfire {
if ( p != 0) { return false; } if ( p != 0) { return false; }
const double T9 = tx[0]; const double T9 = tx[0];
// TODO: Handle rho and Y // This is an interesting problem because the reverse rate should only ever be computed for strong reactions
const double reverseRate = m_engine.calculateReverseRate(m_reaction, T9, 0, {}); // Which do not depend on rho or Y. However, the signature requires them...
// For now, we just pass dummy values for rho and Y
const double reverseRate = m_engine.calculateReverseRate(m_reaction, T9, 0.0, {});
// std::cout << m_reaction.peName() << " reverseRate: " << reverseRate << " at T9: " << T9 << "\n"; // std::cout << m_reaction.peName() << " reverseRate: " << reverseRate << " at T9: " << T9 << "\n";
ty[0] = reverseRate; // Store the reverse rate in the output vector ty[0] = reverseRate; // Store the reverse rate in the output vector
@@ -1178,7 +1260,9 @@ namespace gridfire {
const double T9 = tx[0]; const double T9 = tx[0];
const double reverseRate = ty[0]; const double reverseRate = ty[0];
// TODO: Handle rho and Y // This is an interesting problem because the reverse rate should only ever be computed for strong reactions
// Which do not depend on rho or Y. However, the signature requires them...
// For now, we just pass dummy values for rho and Y
const double derivative = m_engine.calculateReverseRateTwoBodyDerivative(m_reaction, T9, 0, {}, reverseRate); const double derivative = m_engine.calculateReverseRateTwoBodyDerivative(m_reaction, T9, 0, {}, reverseRate);
px[0] = py[0] * derivative; // Return the derivative of the reverse rate with respect to T9 px[0] = py[0] * derivative; // Return the derivative of the reverse rate with respect to T9

View File

@@ -26,9 +26,10 @@ namespace gridfire {
ReactionSet build_nuclear_network( ReactionSet build_nuclear_network(
const Composition& composition, const Composition& composition,
const rates::weak::WeakRateInterpolator& weak_interpolator, const rates::weak::WeakRateInterpolator& weakInterpolator,
BuildDepthType maxLayers, BuildDepthType maxLayers,
bool reverse_reaclib) { bool reverse
) {
int depth; int depth;
if (std::holds_alternative<NetworkBuildDepth>(maxLayers)) { if (std::holds_alternative<NetworkBuildDepth>(maxLayers)) {
depth = static_cast<int>(std::get<NetworkBuildDepth>(maxLayers)); depth = static_cast<int>(std::get<NetworkBuildDepth>(maxLayers));
@@ -47,41 +48,53 @@ namespace gridfire {
// Clone all relevant REACLIB reactions into the master pool // Clone all relevant REACLIB reactions into the master pool
const auto& allReaclibReactions = reaclib::get_all_reaclib_reactions(); const auto& allReaclibReactions = reaclib::get_all_reaclib_reactions();
for (const auto& reaction : allReaclibReactions) { for (const auto& reaction : allReaclibReactions) {
if (reaction->is_reverse() == reverse_reaclib) { if (reaction->is_reverse() == reverse) {
master_reaction_pool.add_reaction(reaction->clone()); master_reaction_pool.add_reaction(reaction->clone());
} }
} }
for (const auto& parent_species: weak_interpolator.available_isotopes()) { for (const auto& parent_species: weakInterpolator.available_isotopes()) {
std::expected<Species, fourdst::atomic::SpeciesErrorType> upProduct = fourdst::atomic::az_to_species(
parent_species.a(),
parent_species.z() + 1
);
std::expected<Species, fourdst::atomic::SpeciesErrorType> downProduct = fourdst::atomic::az_to_species(
parent_species.a(),
parent_species.z() - 1
);
if (downProduct.has_value()) { // Only add the reaction if the Species map contains the product
master_reaction_pool.add_reaction( master_reaction_pool.add_reaction(
std::make_unique<rates::weak::WeakReaction>( std::make_unique<rates::weak::WeakReaction>(
parent_species, parent_species,
rates::weak::WeakReactionType::BETA_PLUS_DECAY, rates::weak::WeakReactionType::BETA_PLUS_DECAY,
weak_interpolator weakInterpolator
)
);
master_reaction_pool.add_reaction(
std::make_unique<rates::weak::WeakReaction>(
parent_species,
rates::weak::WeakReactionType::BETA_MINUS_DECAY,
weak_interpolator
) )
); );
master_reaction_pool.add_reaction( master_reaction_pool.add_reaction(
std::make_unique<rates::weak::WeakReaction>( std::make_unique<rates::weak::WeakReaction>(
parent_species, parent_species,
rates::weak::WeakReactionType::ELECTRON_CAPTURE, rates::weak::WeakReactionType::ELECTRON_CAPTURE,
weak_interpolator weakInterpolator
)
);
}
if (upProduct.has_value()) { // Only add the reaction if the Species map contains the product
master_reaction_pool.add_reaction(
std::make_unique<rates::weak::WeakReaction>(
parent_species,
rates::weak::WeakReactionType::BETA_MINUS_DECAY,
weakInterpolator
) )
); );
master_reaction_pool.add_reaction( master_reaction_pool.add_reaction(
std::make_unique<rates::weak::WeakReaction>( std::make_unique<rates::weak::WeakReaction>(
parent_species, parent_species,
rates::weak::WeakReactionType::POSITRON_CAPTURE, rates::weak::WeakReactionType::POSITRON_CAPTURE,
weak_interpolator weakInterpolator
) )
); );
} }
}
// --- Step 2: Use non-owning raw pointers for the fast build algorithm --- // --- Step 2: Use non-owning raw pointers for the fast build algorithm ---
std::vector<Reaction*> remainingReactions; std::vector<Reaction*> remainingReactions;
@@ -140,7 +153,13 @@ namespace gridfire {
break; break;
} }
LOG_TRACE_L1(logger, "Layer {}: Collected {} new reactions. New products this layer: {}", collectedReactionPtrs.size() - collectedReactionPtrs.size(), newProductsThisLayer.size()); LOG_TRACE_L1(
logger,
"Layer {}: Collected {} new reactions. New products this layer: {}",
layer,
collectedReactionPtrs.size() - collectedReactionPtrs.size(),
newProductsThisLayer.size()
);
availableSpecies.insert(newProductsThisLayer.begin(), newProductsThisLayer.end()); availableSpecies.insert(newProductsThisLayer.begin(), newProductsThisLayer.end());
remainingReactions = std::move(reactionsForNextPass); remainingReactions = std::move(reactionsForNextPass);
} }

View File

@@ -17,7 +17,7 @@ namespace gridfire {
const reaction::Reaction* findDominantCreationChannel ( const reaction::Reaction* findDominantCreationChannel (
const DynamicEngine& engine, const DynamicEngine& engine,
const Species& species, const Species& species,
const fourdst::composition::Composition &comp, const Composition &comp,
const double T9, const double T9,
const double rho const double rho
) { ) {

View File

@@ -17,7 +17,13 @@
namespace { namespace {
using namespace fourdst::atomic; using namespace fourdst::atomic;
std::vector<double> packCompositionToVector(const fourdst::composition::Composition& composition, const gridfire::GraphEngine& engine) { //TODO: Replace all calls to this function with composition.getMolarAbundanceVector() so that
// we don't have to keep this function around. (Cant do this right now because there is not a
// guarantee that this function will return the same ordering as the canonical vector representation)
std::vector<double> packCompositionToVector(
const fourdst::composition::Composition& composition,
const gridfire::GraphEngine& engine
) {
std::vector<double> Y(engine.getNetworkSpecies().size(), 0.0); std::vector<double> Y(engine.getNetworkSpecies().size(), 0.0);
const auto& allSpecies = engine.getNetworkSpecies(); const auto& allSpecies = engine.getNetworkSpecies();
for (size_t i = 0; i < allSpecies.size(); ++i) { for (size_t i = 0; i < allSpecies.size(); ++i) {
@@ -780,6 +786,7 @@ namespace gridfire {
LOG_TRACE_L1(m_logger, "Partitioning by timescale..."); LOG_TRACE_L1(m_logger, "Partitioning by timescale...");
const auto result= m_baseEngine.getSpeciesDestructionTimescales(comp, T9, rho); const auto result= m_baseEngine.getSpeciesDestructionTimescales(comp, T9, rho);
const auto netTimescale = m_baseEngine.getSpeciesTimescales(comp, T9, rho); const auto netTimescale = m_baseEngine.getSpeciesTimescales(comp, T9, rho);
if (!result) { if (!result) {
LOG_ERROR(m_logger, "Failed to get species destruction timescales due to stale engine state"); LOG_ERROR(m_logger, "Failed to get species destruction timescales due to stale engine state");
m_logger->flush_log(); m_logger->flush_log();
@@ -792,6 +799,14 @@ namespace gridfire {
} }
const std::unordered_map<Species, double>& all_timescales = result.value(); const std::unordered_map<Species, double>& all_timescales = result.value();
const std::unordered_map<Species, double>& net_timescales = netTimescale.value(); const std::unordered_map<Species, double>& net_timescales = netTimescale.value();
for (const auto& [species, destructionTimescale] : all_timescales) {
std::cout << "Species: " << species.name()
<< ", Destruction Timescale: " << (std::isfinite(destructionTimescale) ? std::to_string(destructionTimescale) : "inf")
<< " s, Net Timescale: " << (std::isfinite(net_timescales.at(species)) ? std::to_string(net_timescales.at(species)) : "inf")
<< " s\n";
}
const auto& all_species = m_baseEngine.getNetworkSpecies(); const auto& all_species = m_baseEngine.getNetworkSpecies();
std::vector<std::pair<double, Species>> sorted_timescales; std::vector<std::pair<double, Species>> sorted_timescales;
@@ -1075,6 +1090,11 @@ namespace gridfire {
normalized_composition.setMassFraction(species, 0.0); normalized_composition.setMassFraction(species, 0.0);
} }
} }
bool normCompFinalizedOkay = normalized_composition.finalize(true);
if (!normCompFinalizedOkay) {
LOG_ERROR(m_logger, "Failed to finalize composition before QSE solve.");
throw std::runtime_error("Failed to finalize composition before QSE solve.");
}
Eigen::VectorXd Y_scale(algebraic_species.size()); Eigen::VectorXd Y_scale(algebraic_species.size());
Eigen::VectorXd v_initial(algebraic_species.size()); Eigen::VectorXd v_initial(algebraic_species.size());
@@ -1330,10 +1350,18 @@ namespace gridfire {
Eigen::VectorXd y_qse = m_Y_scale.array() * v_qse.array().sinh(); // Convert to physical abundances using asinh scaling Eigen::VectorXd y_qse = m_Y_scale.array() * v_qse.array().sinh(); // Convert to physical abundances using asinh scaling
for (const auto& species: m_qse_solve_species) { for (const auto& species: m_qse_solve_species) {
if (!comp_trial.contains(species)) { if (!comp_trial.hasSymbol(std::string(species.name()))) {
comp_trial.registerSpecies(species); comp_trial.registerSpecies(species);
} }
comp_trial.setMassFraction(species, y_qse[static_cast<long>(m_qse_solve_species_index_map.at(species))]); const double molarAbundance = y_qse[static_cast<long>(m_qse_solve_species_index_map.at(species))];
const double massFraction = molarAbundance * species.mass();
comp_trial.setMassFraction(species, massFraction);
}
const bool didFinalize = comp_trial.finalize(false);
if (!didFinalize) {
std::string msg = std::format("Failed to finalize composition (comp_trial) in {} at line {}", __FILE__, __LINE__);
throw std::runtime_error(msg);
} }
const auto result = m_view->getBaseEngine().calculateRHSAndEnergy(comp_trial, m_T9, m_rho); const auto result = m_view->getBaseEngine().calculateRHSAndEnergy(comp_trial, m_T9, m_rho);
@@ -1357,10 +1385,18 @@ namespace gridfire {
Eigen::VectorXd y_qse = m_Y_scale.array() * v_qse.array().sinh(); // Convert to physical abundances using asinh scaling Eigen::VectorXd y_qse = m_Y_scale.array() * v_qse.array().sinh(); // Convert to physical abundances using asinh scaling
for (const auto& species: m_qse_solve_species) { for (const auto& species: m_qse_solve_species) {
if (!comp_trial.contains(species)) { if (!comp_trial.hasSymbol(std::string(species.name()))) {
comp_trial.registerSpecies(species); comp_trial.registerSpecies(species);
} }
comp_trial.setMassFraction(species, y_qse[static_cast<long>(m_qse_solve_species_index_map.at(species))]); const double molarAbundance = y_qse[static_cast<long>(m_qse_solve_species_index_map.at(species))];
const double massFraction = molarAbundance * species.mass();
comp_trial.setMassFraction(species, massFraction);
}
const bool didFinalize = comp_trial.finalize(false);
if (!didFinalize) {
std::string msg = std::format("Failed to finalize composition (comp_trial) in {} at line {}", __FILE__, __LINE__);
throw std::runtime_error(msg);
} }
m_view->getBaseEngine().generateJacobianMatrix(comp_trial, m_T9, m_rho); m_view->getBaseEngine().generateJacobianMatrix(comp_trial, m_T9, m_rho);
@@ -1368,14 +1404,16 @@ namespace gridfire {
const long N = static_cast<long>(m_qse_solve_species.size()); const long N = static_cast<long>(m_qse_solve_species.size());
J_qse.resize(N, N); J_qse.resize(N, N);
long rowID = 0; long rowID = 0;
long colID = 0;
for (const auto& rowSpecies : m_qse_solve_species) { for (const auto& rowSpecies : m_qse_solve_species) {
long colID = 0;
for (const auto& colSpecies: m_qse_solve_species) { for (const auto& colSpecies: m_qse_solve_species) {
J_qse(rowID++, colID++) = m_view->getBaseEngine().getJacobianMatrixEntry( J_qse(rowID, colID) = m_view->getBaseEngine().getJacobianMatrixEntry(
rowSpecies, rowSpecies,
colSpecies colSpecies
); );
colID += 1;
} }
rowID += 1;
} }
// Chain rule for asinh scaling: // Chain rule for asinh scaling:

View File

@@ -19,7 +19,7 @@ namespace gridfire::partition {
const int a, const int a,
const double T9 const double T9
) const { ) const {
LOG_TRACE_L2(m_logger, "Evaluating ground state partition function for Z={} A={} T9={}", z, a, T9); LOG_TRACE_L3(m_logger, "Evaluating ground state partition function for Z={} A={} T9={}", z, a, T9);
const int key = make_key(z, a); const int key = make_key(z, a);
const double spin = m_ground_state_spin.at(key); const double spin = m_ground_state_spin.at(key);
return (2.0 * spin) + 1.0; return (2.0 * spin) + 1.0;
@@ -30,7 +30,7 @@ namespace gridfire::partition {
const int a, const int a,
const double T9 const double T9
) const { ) const {
LOG_TRACE_L2(m_logger, "Evaluating derivative of ground state partition function for Z={} A={} T9={}", z, a, T9); LOG_TRACE_L3(m_logger, "Evaluating derivative of ground state partition function for Z={} A={} T9={}", z, a, T9);
return 0.0; return 0.0;
} }

View File

@@ -44,21 +44,21 @@ namespace gridfire::partition {
const int a, const int a,
const double T9 const double T9
) const { ) const {
LOG_TRACE_L2(m_logger, "Evaluating Rauscher-Thielemann partition function for Z={} A={} T9={}", z, a, T9); LOG_TRACE_L3(m_logger, "Evaluating Rauscher-Thielemann partition function for Z={} A={} T9={}", z, a, T9);
const auto [bound, data, upperIndex, lowerIndex] = find(z, a, T9); const auto [bound, data, upperIndex, lowerIndex] = find(z, a, T9);
switch (bound) { switch (bound) {
case FRONT: { case FRONT: {
LOG_TRACE_L2(m_logger, "Using FRONT bound for Z={} A={} T9={}", z, a, T9); LOG_TRACE_L3(m_logger, "Using FRONT bound for Z={} A={} T9={}", z, a, T9);
return data.normalized_g_values.front() * (2.0 * data.ground_state_spin + 1.0); return data.normalized_g_values.front() * (2.0 * data.ground_state_spin + 1.0);
} }
case BACK: { case BACK: {
LOG_TRACE_L2(m_logger, "Using BACK bound for Z={} A={} T9={}", z, a, T9); LOG_TRACE_L3(m_logger, "Using BACK bound for Z={} A={} T9={}", z, a, T9);
return data.normalized_g_values.back() * (2.0 * data.ground_state_spin + 1.0); return data.normalized_g_values.back() * (2.0 * data.ground_state_spin + 1.0);
} }
case MIDDLE: { case MIDDLE: {
LOG_TRACE_L2(m_logger, "Using MIDDLE bound for Z={} A={} T9={}", z, a, T9); LOG_TRACE_L3(m_logger, "Using MIDDLE bound for Z={} A={} T9={}", z, a, T9);
} }
} }
@@ -79,10 +79,10 @@ namespace gridfire::partition {
const int a, const int a,
const double T9 const double T9
) const { ) const {
LOG_TRACE_L2(m_logger, "Evaluating derivative of Rauscher-Thielemann partition function for Z={} A={} T9={}", z, a, T9); LOG_TRACE_L3(m_logger, "Evaluating derivative of Rauscher-Thielemann partition function for Z={} A={} T9={}", z, a, T9);
const auto [bound, data, upperIndex, lowerIndex] = find(z, a, T9); const auto [bound, data, upperIndex, lowerIndex] = find(z, a, T9);
if (bound == FRONT || bound == BACK) { if (bound == FRONT || bound == BACK) {
LOG_TRACE_L2(m_logger, "Derivative is zero for Z={} A={} T9={} (bound: {})", z, a, T9, bound == FRONT ? "FRONT" : "BACK"); LOG_TRACE_L3(m_logger, "Derivative is zero for Z={} A={} T9={} (bound: {})", z, a, T9, bound == FRONT ? "FRONT" : "BACK");
return 0.0; // Derivative is zero at the boundaries return 0.0; // Derivative is zero at the boundaries
} }
const auto [T9_high, G_norm_high, T9_low, G_norm_low] = get_interpolation_points( const auto [T9_high, G_norm_high, T9_low, G_norm_low] = get_interpolation_points(

View File

@@ -16,6 +16,11 @@
namespace { namespace {
std::unordered_map<fourdst::atomic::SpeciesErrorType, std::string> SpeciesErrorTypeMap = {
{fourdst::atomic::SpeciesErrorType::ELEMENT_SYMBOL_NOT_FOUND, "Element symbol not found (Z out of range)"},
{fourdst::atomic::SpeciesErrorType::SPECIES_SYMBOL_NOT_FOUND, "Species symbol not found ((A,Z) out of range)"}
};
fourdst::atomic::Species resolve_weak_product( fourdst::atomic::Species resolve_weak_product(
const gridfire::rates::weak::WeakReactionType type, const gridfire::rates::weak::WeakReactionType type,
const fourdst::atomic::Species& reactant const fourdst::atomic::Species& reactant
@@ -23,26 +28,37 @@ namespace {
using namespace fourdst::atomic; using namespace fourdst::atomic;
using namespace gridfire::rates::weak; using namespace gridfire::rates::weak;
std::optional<Species> product; // Use optional so that we can start in a valid "null" state int zMod = 0;
switch (type) { switch (type) {
case WeakReactionType::BETA_MINUS_DECAY: case WeakReactionType::BETA_MINUS_DECAY:
product = az_to_species(reactant.a(), reactant.z() + 1); zMod = 1;
return product.value(); break;
case WeakReactionType::BETA_PLUS_DECAY: case WeakReactionType::BETA_PLUS_DECAY:
product = az_to_species(reactant.a(), reactant.z() - 1); zMod = -1;
return product.value(); break;
case WeakReactionType::ELECTRON_CAPTURE:
product = az_to_species(reactant.a(), reactant.z() - 1);
return product.value();
case WeakReactionType::POSITRON_CAPTURE: case WeakReactionType::POSITRON_CAPTURE:
product = az_to_species(reactant.a(), reactant.z() + 1); zMod = 1;
break;
case WeakReactionType::ELECTRON_CAPTURE:
zMod = -1;
break; break;
} }
if (!product.has_value()) { std::expected<Species, SpeciesErrorType> product = az_to_species(reactant.a(), reactant.z() + zMod);
throw std::runtime_error("Failed to resolve weak reaction product for reactant: " + std::string(reactant.name()));
} if (product.has_value()) {
return product.value(); return product.value();
} }
const std::string msg = std::format(
"Failed to resolve weak reaction product (A: {}, Z: {}) for reactant {} (looked up A: {}, Z: {}): {}",
reactant.a(),
reactant.z(),
reactant.name(),
reactant.a(),
reactant.z() + zMod,
SpeciesErrorTypeMap.at(product.error())
);
throw std::runtime_error(msg);
}
std::string resolve_weak_id( std::string resolve_weak_id(
const gridfire::rates::weak::WeakReactionType type, const gridfire::rates::weak::WeakReactionType type,
@@ -77,7 +93,16 @@ namespace gridfire::rates::weak {
// ReSharper disable once CppUseStructuredBinding // ReSharper disable once CppUseStructuredBinding
for (const auto& weak_reaction_record : UNIFIED_WEAK_DATA) { for (const auto& weak_reaction_record : UNIFIED_WEAK_DATA) {
Species species = az_to_species(weak_reaction_record.A, weak_reaction_record.Z); std::expected<Species, SpeciesErrorType> species_result = az_to_species(weak_reaction_record.A, weak_reaction_record.Z);
if (!species_result.has_value()) {
const SpeciesErrorType type = species_result.error();
const std::string msg = std::format(
"Failed to load weak reaction data for (A={}, Z={}) with error: {}",
weak_reaction_record.A, weak_reaction_record.Z, SpeciesErrorTypeMap.at(type)
);
throw std::runtime_error(msg);
}
const Species& species = species_result.value();
if (weak_reaction_record.log_beta_minus > GRIDFIRE_WEAK_REACTION_LIB_SENTINEL) { if (weak_reaction_record.log_beta_minus > GRIDFIRE_WEAK_REACTION_LIB_SENTINEL) {
m_weak_network[species].push_back( m_weak_network[species].push_back(
@@ -148,7 +173,7 @@ namespace gridfire::rates::weak {
std::expected<std::vector<WeakReactionEntry>, WeakMapError> WeakReactionMap::get_species_reactions( std::expected<std::vector<WeakReactionEntry>, WeakMapError> WeakReactionMap::get_species_reactions(
const std::string &species_name) const { const std::string &species_name) const {
const fourdst::atomic::Species species = fourdst::atomic::species.at(species_name); const fourdst::atomic::Species& species = fourdst::atomic::species.at(species_name);
if (m_weak_network.contains(species)) { if (m_weak_network.contains(species)) {
return m_weak_network.at(species); return m_weak_network.at(species);
} }
@@ -284,9 +309,7 @@ namespace gridfire::rates::weak {
case WeakReactionType::POSITRON_CAPTURE: case WeakReactionType::POSITRON_CAPTURE:
Q_MeV = nuclearMassDiff_MeV + 2.0 * electronMass_MeV; Q_MeV = nuclearMassDiff_MeV + 2.0 * electronMass_MeV;
break; break;
case WeakReactionType::BETA_MINUS_DECAY: case WeakReactionType::BETA_MINUS_DECAY: // Same as electron capture so we can simply fall through
Q_MeV = nuclearMassDiff_MeV;
break;
case WeakReactionType::ELECTRON_CAPTURE: case WeakReactionType::ELECTRON_CAPTURE:
Q_MeV = nuclearMassDiff_MeV; Q_MeV = nuclearMassDiff_MeV;
break; break;
@@ -306,8 +329,7 @@ namespace gridfire::rates::weak {
static_cast<uint16_t>(m_reactant_a), static_cast<uint16_t>(m_reactant_a),
static_cast<uint8_t>(m_reactant_z), static_cast<uint8_t>(m_reactant_z),
T9, T9,
std::log10(rho * Ye), std::log10(rho * Ye)
mue
); );
if (!rates.has_value()) { if (!rates.has_value()) {
@@ -374,8 +396,7 @@ namespace gridfire::rates::weak {
static_cast<uint16_t>(m_reactant_a), static_cast<uint16_t>(m_reactant_a),
static_cast<uint8_t>(m_reactant_z), static_cast<uint8_t>(m_reactant_z),
T9, T9,
log_rhoYe, log_rhoYe
mue
); );
if (!rates.has_value()) { if (!rates.has_value()) {
const InterpolationErrorType type = rates.error().type; const InterpolationErrorType type = rates.error().type;
@@ -386,9 +407,22 @@ namespace gridfire::rates::weak {
throw std::runtime_error(msg); throw std::runtime_error(msg);
} }
// TODO: Finish implementing this (just need a switch statement) double logRate = 0.0;
return 0.0; switch (m_type) {
case WeakReactionType::BETA_MINUS_DECAY:
logRate = rates->d_log_beta_minus[0];
break;
case WeakReactionType::BETA_PLUS_DECAY:
logRate = rates->d_log_beta_plus[0];
break;
case WeakReactionType::ELECTRON_CAPTURE:
logRate = rates->d_log_electron_capture[0];
break;
case WeakReactionType::POSITRON_CAPTURE:
logRate = rates->d_log_positron_capture[0];
break;
}
return logRate;
} }
reaction::ReactionType WeakReaction::type() const { reaction::ReactionType WeakReaction::type() const {
@@ -437,9 +471,7 @@ namespace gridfire::rates::weak {
case WeakReactionType::BETA_MINUS_DECAY: case WeakReactionType::BETA_MINUS_DECAY:
logNeutrinoLoss = payload.log_antineutrino_loss_bd; logNeutrinoLoss = payload.log_antineutrino_loss_bd;
break; break;
case WeakReactionType::BETA_PLUS_DECAY: case WeakReactionType::BETA_PLUS_DECAY: // Same as electron capture so we can simply fall through
logNeutrinoLoss = payload.log_neutrino_loss_ec;
break;
case WeakReactionType::ELECTRON_CAPTURE: case WeakReactionType::ELECTRON_CAPTURE:
logNeutrinoLoss = payload.log_neutrino_loss_ec; logNeutrinoLoss = payload.log_neutrino_loss_ec;
break; break;
@@ -450,6 +482,7 @@ namespace gridfire::rates::weak {
return logNeutrinoLoss; return logNeutrinoLoss;
} }
// Note that the input vector tx is of size 3: [T9, log10(rho*Ye), mu_e]
bool WeakReaction::AtomicWeakRate::forward ( bool WeakReaction::AtomicWeakRate::forward (
const size_t p, const size_t p,
const size_t q, const size_t q,
@@ -464,20 +497,18 @@ namespace gridfire::rates::weak {
} }
const double T9 = tx[0]; const double T9 = tx[0];
const double log10_rhoye = tx[1]; const double log10_rhoye = tx[1];
const double mu_e = tx[2];
const std::expected<WeakRatePayload, InterpolationError> result = m_interpolator.get_rates( const std::expected<WeakRatePayload, InterpolationError> result = m_interpolator.get_rates(
static_cast<uint16_t>(m_a), static_cast<uint16_t>(m_a),
static_cast<uint8_t>(m_z), static_cast<uint8_t>(m_z),
T9, T9,
log10_rhoye, log10_rhoye
mu_e
); );
if (!result.has_value()) { if (!result.has_value()) {
const InterpolationErrorType type = result.error().type; const InterpolationErrorType type = result.error().type;
const std::string msg = std::format( const std::string msg = std::format(
"Failed to interpolate weak rate for (A={}, Z={}) at T9={}, log10(rho*Ye)={}, mu_e={} with error: {}", "Failed to interpolate weak rate for (A={}, Z={}) at T9={}, log10(ρ Ye)={}, with error: {}",
m_a, m_z, T9, log10_rhoye, mu_e, InterpolationErrorTypeMap.at(type) m_a, m_z, T9, log10_rhoye, InterpolationErrorTypeMap.at(type)
); );
throw std::runtime_error(msg); throw std::runtime_error(msg);
} }
@@ -501,7 +532,7 @@ namespace gridfire::rates::weak {
} }
if (vx.size() > 0) { // Set up the sparsity pattern. This is saying that all input variables affect the output variable. if (vx.size() > 0) { // Set up the sparsity pattern. This is saying that all input variables affect the output variable.
const bool any_input_varies = vx[0] || vx[1] || vx[2]; const bool any_input_varies = vx[0] || vx[1];
vy[0] = any_input_varies; vy[0] = any_input_varies;
vy[1] = any_input_varies; vy[1] = any_input_varies;
} }
@@ -517,7 +548,6 @@ namespace gridfire::rates::weak {
) { ) {
const double T9 = tx[0]; const double T9 = tx[0];
const double log10_rhoye = tx[1]; const double log10_rhoye = tx[1];
const double mu_e = tx[2];
const double forwardPassRate = ty[0]; // This is the rate from the forward pass. const double forwardPassRate = ty[0]; // This is the rate from the forward pass.
const double forwardPassNeutrinoLossRate = ty[1]; // This is the neutrino loss rate from the forward pass. const double forwardPassNeutrinoLossRate = ty[1]; // This is the neutrino loss rate from the forward pass.
@@ -526,55 +556,47 @@ namespace gridfire::rates::weak {
static_cast<uint16_t>(m_a), static_cast<uint16_t>(m_a),
static_cast<uint8_t>(m_z), static_cast<uint8_t>(m_z),
T9, T9,
log10_rhoye, log10_rhoye
mu_e
); );
if (!result.has_value()) { if (!result.has_value()) {
const InterpolationErrorType type = result.error().type; const InterpolationErrorType type = result.error().type;
const std::string msg = std::format( const std::string msg = std::format(
"Failed to interpolate weak rate derivatives for (A={}, Z={}) at T9={}, log10(rho*Ye)={}, mu_e={} with error: {}", "Failed to interpolate weak rate derivatives for (A={}, Z={}) at T9={}, log10(ρ Ye)={}, with error: {}",
m_a, m_z, T9, log10_rhoye, mu_e, InterpolationErrorTypeMap.at(type) m_a, m_z, T9, log10_rhoye, InterpolationErrorTypeMap.at(type)
); );
throw std::runtime_error(msg); throw std::runtime_error(msg);
} }
// ReSharper disable once CppUseStructuredBinding
const WeakRateDerivatives derivatives = result.value(); const WeakRateDerivatives derivatives = result.value();
std::array<double, 3> dLogRate; // d(rate)/dT9, d(rate)/dlogRhoYe, d(rate)/dMuE std::array<double, 2> dLogRate{}; // d(rate)/dT9, d(rate)/dlogRhoYe
std::array<double, 3> dLogNuLoss; // d(nu loss)/dT9, d(nu loss)/dlogRhoYe, d(nu loss)/dMuE std::array<double, 2> dLogNuLoss{}; // d(nu loss)/dT9, d(nu loss)/dlogRhoYe
switch (m_type) { switch (m_type) {
case WeakReactionType::BETA_MINUS_DECAY: case WeakReactionType::BETA_MINUS_DECAY:
dLogRate[0] = derivatives.d_log_beta_minus[0]; dLogRate[0] = derivatives.d_log_beta_minus[0];
dLogRate[1] = derivatives.d_log_beta_minus[1]; dLogRate[1] = derivatives.d_log_beta_minus[1];
dLogRate[2] = derivatives.d_log_beta_minus[2];
dLogNuLoss[0] = derivatives.d_log_antineutrino_loss_bd[0]; dLogNuLoss[0] = derivatives.d_log_antineutrino_loss_bd[0];
dLogNuLoss[1] = derivatives.d_log_antineutrino_loss_bd[1]; dLogNuLoss[1] = derivatives.d_log_antineutrino_loss_bd[1];
dLogNuLoss[2] = derivatives.d_log_antineutrino_loss_bd[2];
break; break;
case WeakReactionType::BETA_PLUS_DECAY: case WeakReactionType::BETA_PLUS_DECAY:
dLogRate[0] = derivatives.d_log_beta_plus[0]; dLogRate[0] = derivatives.d_log_beta_plus[0];
dLogRate[1] = derivatives.d_log_beta_plus[1]; dLogRate[1] = derivatives.d_log_beta_plus[1];
dLogRate[2] = derivatives.d_log_beta_plus[2];
dLogNuLoss[0] = derivatives.d_log_neutrino_loss_ec[0]; dLogNuLoss[0] = derivatives.d_log_neutrino_loss_ec[0];
dLogNuLoss[1] = derivatives.d_log_neutrino_loss_ec[1]; dLogNuLoss[1] = derivatives.d_log_neutrino_loss_ec[1];
dLogNuLoss[2] = derivatives.d_log_neutrino_loss_ec[2];
break; break;
case WeakReactionType::ELECTRON_CAPTURE: case WeakReactionType::ELECTRON_CAPTURE:
dLogRate[0] = derivatives.d_log_electron_capture[0]; dLogRate[0] = derivatives.d_log_electron_capture[0];
dLogRate[1] = derivatives.d_log_electron_capture[1]; dLogRate[1] = derivatives.d_log_electron_capture[1];
dLogRate[2] = derivatives.d_log_electron_capture[2];
dLogNuLoss[0] = derivatives.d_log_neutrino_loss_ec[0]; dLogNuLoss[0] = derivatives.d_log_neutrino_loss_ec[0];
dLogNuLoss[1] = derivatives.d_log_neutrino_loss_ec[1]; dLogNuLoss[1] = derivatives.d_log_neutrino_loss_ec[1];
dLogNuLoss[2] = derivatives.d_log_neutrino_loss_ec[2];
break; break;
case WeakReactionType::POSITRON_CAPTURE: case WeakReactionType::POSITRON_CAPTURE:
dLogRate[0] = derivatives.d_log_positron_capture[0]; dLogRate[0] = derivatives.d_log_positron_capture[0];
dLogRate[1] = derivatives.d_log_positron_capture[1]; dLogRate[1] = derivatives.d_log_positron_capture[1];
dLogRate[2] = derivatives.d_log_positron_capture[2];
dLogNuLoss[0] = derivatives.d_log_antineutrino_loss_bd[0]; dLogNuLoss[0] = derivatives.d_log_antineutrino_loss_bd[0];
dLogNuLoss[1] = derivatives.d_log_antineutrino_loss_bd[1]; dLogNuLoss[1] = derivatives.d_log_antineutrino_loss_bd[1];
dLogNuLoss[2] = derivatives.d_log_antineutrino_loss_bd[2];
break; break;
} }
@@ -583,12 +605,10 @@ namespace gridfire::rates::weak {
// Contributions from the reaction rate (output 0) // Contributions from the reaction rate (output 0)
px[0] = py[0] * forwardPassRate * ln10 * dLogRate[0]; px[0] = py[0] * forwardPassRate * ln10 * dLogRate[0];
px[1] = py[0] * forwardPassRate * ln10 * dLogRate[1]; px[1] = py[0] * forwardPassRate * ln10 * dLogRate[1];
px[2] = py[0] * forwardPassRate * ln10 * dLogRate[2];
// Contributions from the neutrino loss rate (output 1) // Contributions from the neutrino loss rate (output 1)
px[0] += py[1] * forwardPassNeutrinoLossRate * ln10 * dLogNuLoss[0]; px[0] += py[1] * forwardPassNeutrinoLossRate * ln10 * dLogNuLoss[0];
px[1] += py[1] * forwardPassNeutrinoLossRate * ln10 * dLogNuLoss[1]; px[1] += py[1] * forwardPassNeutrinoLossRate * ln10 * dLogNuLoss[1];
px[2] += py[1] * forwardPassNeutrinoLossRate * ln10 * dLogNuLoss[2];
return true; return true;
@@ -602,7 +622,6 @@ namespace gridfire::rates::weak {
std::set<size_t> all_input_deps; std::set<size_t> all_input_deps;
all_input_deps.insert(r[0].begin(), r[0].end()); all_input_deps.insert(r[0].begin(), r[0].end());
all_input_deps.insert(r[1].begin(), r[1].end()); all_input_deps.insert(r[1].begin(), r[1].end());
all_input_deps.insert(r[2].begin(), r[2].end());
// What this is saying is that both output variables depend on all input variables. // What this is saying is that both output variables depend on all input variables.
s[0] = all_input_deps; s[0] = all_input_deps;
@@ -623,7 +642,6 @@ namespace gridfire::rates::weak {
st[0] = all_output_deps; st[0] = all_output_deps;
st[1] = all_output_deps; st[1] = all_output_deps;
st[2] = all_output_deps;
return true; return true;
} }

View File

@@ -13,54 +13,61 @@
#include "fourdst/composition/species.h" #include "fourdst/composition/species.h"
#include "quill/LogMacros.h"
namespace gridfire::rates::weak { namespace gridfire::rates::weak {
WeakRateInterpolator::WeakRateInterpolator(const RowDataTable &raw_data) { WeakRateInterpolator::WeakRateInterpolator(const RowDataTable &raw_data) {
// Group all raw data rows by their isotope ID.
std::map<uint32_t, std::vector<const RateDataRow*>> grouped_rows; std::map<uint32_t, std::vector<const RateDataRow*>> grouped_rows;
for (const auto& row : raw_data) { for (const auto& row : raw_data) {
grouped_rows[pack_isotope_id(row.A, row.Z)].push_back(&row); grouped_rows[pack_isotope_id(row.A, row.Z)].push_back(&row);
} }
// Process each isotope's data to build a simple 2D grid.
for (auto const& [isotope_id, rows] : grouped_rows) { for (auto const& [isotope_id, rows] : grouped_rows) {
IsotopeGrid grid; IsotopeGrid grid;
std::set<float> unique_t9, unique_rhoYe, unique_mue; // Establish the T9 and log(rho*Ye) axes
std::set<float> unique_t9, unique_rhoYe;
for (const auto* row : rows) { for (const auto* row : rows) {
unique_t9.emplace(row->t9); unique_t9.emplace(row->t9);
unique_rhoYe.emplace(row->log_rhoye); unique_rhoYe.emplace(row->log_rhoye);
unique_mue.emplace(row->mu_e);
} }
grid.t9_axis.reserve(unique_t9.size()); grid.t9_axis.assign(unique_t9.begin(), unique_t9.end());
grid.rhoYe_axis.reserve(unique_rhoYe.size()); grid.rhoYe_axis.assign(unique_rhoYe.begin(), unique_rhoYe.end());
grid.mue_axis.reserve(unique_mue.size());
grid.t9_axis.insert(grid.t9_axis.begin(), unique_t9.begin(), unique_t9.end());
grid.rhoYe_axis.insert(grid.rhoYe_axis.begin(), unique_rhoYe.begin(), unique_rhoYe.end());
grid.mue_axis.insert(grid.mue_axis.begin(), unique_mue.begin(), unique_mue.end());
std::ranges::sort(grid.t9_axis);
std::ranges::sort(grid.rhoYe_axis);
std::ranges::sort(grid.mue_axis);
const size_t nt9 = grid.t9_axis.size(); const size_t nt9 = grid.t9_axis.size();
const size_t nrhoYe = grid.rhoYe_axis.size(); const size_t nrhoYe = grid.rhoYe_axis.size();
const size_t nmue = grid.mue_axis.size();
grid.data.resize(nt9 * nrhoYe * nmue); grid.data.resize(nt9 * nrhoYe);
// Reverse map for quick index lookup // Create reverse maps for efficient index lookups.
std::unordered_map<float, size_t> t9_map, rhoYe_map, mue_map; std::unordered_map<double, size_t> t9_map, rhoYe_map;
for (size_t i = 0; i < nt9; i++) { t9_map[grid.t9_axis[i]] = i; } for (size_t i = 0; i < nt9; i++) { t9_map[grid.t9_axis[i]] = i; }
for (size_t j = 0; j < nrhoYe; j++) { rhoYe_map[grid.rhoYe_axis[j]] = j; } for (size_t j = 0; j < nrhoYe; j++) { rhoYe_map[grid.rhoYe_axis[j]] = j; }
for (size_t k = 0; k < nmue; k++) { mue_map[grid.mue_axis[k]] = k; }
// Use a set to detect duplicate (T9, rhoYe) pairs, which would be a data error.
std::set<std::pair<float, float>> seen_coords;
// Populate the 2D grid.
for (const auto* row: rows) { for (const auto* row: rows) {
if (auto [it, inserted] = seen_coords.insert({row->t9, row->log_rhoye}); !inserted) {
auto A = static_cast<uint16_t>(isotope_id >> 8);
auto Z = static_cast<uint8_t>(isotope_id & 0xFF);
std::string msg = std::format(
"Duplicate data point for isotope (A={}, Z={}) at (T9={}, log10(rho*Ye)={}) in weak rate table. This indicates corrupted or malformed input data and should be taken as an unrecoverable error.",
A, Z, row->t9, row->log_rhoye
);
LOG_ERROR(m_logger, "{}", msg);
throw std::runtime_error(msg);
}
size_t i_t9 = t9_map.at(row->t9); size_t i_t9 = t9_map.at(row->t9);
size_t j_rhoYe = rhoYe_map.at(row->log_rhoye); size_t j_rhoYe = rhoYe_map.at(row->log_rhoye);
size_t k_mue = mue_map.at(row->mu_e);
size_t index = (i_t9 * nrhoYe + j_rhoYe) * nmue + k_mue; size_t index = i_t9 * nrhoYe + j_rhoYe;
grid.data[index] = WeakRatePayload{ grid.data[index] = WeakRatePayload{
row->log_beta_plus, row->log_beta_plus,
row->log_electron_capture, row->log_electron_capture,
@@ -74,16 +81,21 @@ namespace gridfire::rates::weak {
} }
} }
std::vector<fourdst::atomic::Species> WeakRateInterpolator::available_isotopes() const { std::vector<fourdst::atomic::Species> WeakRateInterpolator::available_isotopes() const {
std::vector<fourdst::atomic::Species> isotopes; using namespace fourdst::atomic;
std::vector<Species> isotopes;
for (const auto &packed_id: m_rate_table | std::views::keys) { for (const auto &packed_id: m_rate_table | std::views::keys) {
const uint16_t A = static_cast<uint16_t>(packed_id >> 8); const auto A = static_cast<uint16_t>(packed_id >> 8);
const uint8_t Z = static_cast<uint8_t>(packed_id & 0xFF); const auto Z = static_cast<uint8_t>(packed_id & 0xFF);
try { std::expected<Species, SpeciesErrorType> result = az_to_species(A, Z);
fourdst::atomic::Species species = fourdst::atomic::az_to_species(A, Z); if (!result.has_value()) {
isotopes.push_back(species); std::string msg = "Could not convert A=" + std::to_string(A) + ", Z=" + std::to_string(Z) + " to Species: ";
} catch (const std::exception& e) { msg += (result.error() == SpeciesErrorType::ELEMENT_SYMBOL_NOT_FOUND) ? "Unknown element (Z out of range)." : "Invalid isotope (A < Z or A out of range).";
throw std::runtime_error("Error converting A=" + std::to_string(A) + ", Z=" + std::to_string(Z) + " to Species: " + e.what()); LOG_TRACE_L3(m_logger, "{}", msg);
} else {
isotopes.emplace_back(result.value());
} }
} }
return isotopes; return isotopes;
@@ -93,16 +105,17 @@ namespace gridfire::rates::weak {
const uint16_t A, const uint16_t A,
const uint8_t Z, const uint8_t Z,
const double t9, const double t9,
const double log_rhoYe, const double log_rhoYe
const double mu_e
) const { ) const {
const auto it = m_rate_table.find(pack_isotope_id(A, Z)); const auto it = m_rate_table.find(pack_isotope_id(A, Z));
if (it == m_rate_table.end()) { if (it == m_rate_table.end()) {
return std::unexpected(InterpolationError{InterpolationErrorType::UNKNOWN_SPECIES_ERROR}); return std::unexpected(InterpolationError{InterpolationErrorType::UNKNOWN_SPECIES_ERROR});
} }
const auto&[t9_axis, rhoYe_axis, mue_axis, data] = it->second; const auto& grid = it->second;
const auto& t9_axis = grid.t9_axis;
const auto& rhoYe_axis = grid.rhoYe_axis;
// Now find the bracketing indices for t9, log_rhoYe, and mu_e // Find bracketing indices for the 2D (t9, rhoYe) grid
auto find_lower_index = [](const std::vector<double>& axis, const double value) -> std::optional<size_t> { auto find_lower_index = [](const std::vector<double>& axis, const double value) -> std::optional<size_t> {
const auto upperBoundIterator = std::ranges::upper_bound(axis, value); const auto upperBoundIterator = std::ranges::upper_bound(axis, value);
if (upperBoundIterator == axis.begin() || upperBoundIterator == axis.end()) { if (upperBoundIterator == axis.begin() || upperBoundIterator == axis.end()) {
@@ -113,161 +126,79 @@ namespace gridfire::rates::weak {
const auto i_t9_opt = find_lower_index(t9_axis, t9); const auto i_t9_opt = find_lower_index(t9_axis, t9);
const auto j_rhoYe_opt = find_lower_index(rhoYe_axis, log_rhoYe); const auto j_rhoYe_opt = find_lower_index(rhoYe_axis, log_rhoYe);
const auto k_mue_opt = find_lower_index(mue_axis, mu_e);
if (!i_t9_opt || !j_rhoYe_opt || !k_mue_opt) { // Handle bounds errors for the 2D grid
if (!i_t9_opt || !j_rhoYe_opt) {
std::unordered_map<TableAxes, BoundsErrorInfo> boundsInfo; std::unordered_map<TableAxes, BoundsErrorInfo> boundsInfo;
if (!i_t9_opt) { if (!i_t9_opt.has_value()) {
boundsInfo[TableAxes::T9] = BoundsErrorInfo{ boundsInfo[TableAxes::T9] = BoundsErrorInfo{TableAxes::T9, t9_axis.front(), t9_axis.back(), t9};
TableAxes::T9,
t9_axis.front(),
t9_axis.back(),
t9
};
} }
if (!j_rhoYe_opt) { if (!j_rhoYe_opt.has_value()) {
boundsInfo[TableAxes::LOG_RHOYE] = BoundsErrorInfo{ boundsInfo[TableAxes::LOG_RHOYE] = BoundsErrorInfo{TableAxes::LOG_RHOYE, rhoYe_axis.front(), rhoYe_axis.back(), log_rhoYe};
TableAxes::LOG_RHOYE,
rhoYe_axis.front(),
rhoYe_axis.back(),
log_rhoYe
};
} }
if (!k_mue_opt) { return std::unexpected(InterpolationError{InterpolationErrorType::BOUNDS_ERROR, boundsInfo});
boundsInfo[TableAxes::MUE] = BoundsErrorInfo{
TableAxes::MUE,
mue_axis.front(),
mue_axis.back(),
mu_e
};
}
return std::unexpected(
InterpolationError{
InterpolationErrorType::BOUNDS_ERROR,
boundsInfo
}
);
} }
const size_t i = i_t9_opt.value(); const size_t i = i_t9_opt.value();
const size_t j = j_rhoYe_opt.value(); const size_t j = j_rhoYe_opt.value();
const size_t k = k_mue_opt.value();
// Coordinates of the bounding cube
const double t1 = t9_axis[i];
const double t2 = t9_axis[i + 1];
const double r1 = rhoYe_axis[j];
const double r2 = rhoYe_axis[j + 1];
const double m1 = mue_axis[k];
const double m2 = mue_axis[k + 1];
const double td = (t9 - t1) / (t2 - t1);
const double rd = (log_rhoYe - r1) / (r2 - r1);
const double md = (mu_e - m1) / (m2 - m1);
auto lerp = [](const double v0, const double v1, const double t) {
return v0 * (1 - t) + v1 * t;
};
auto interpolationField = [&](auto field_accessor) {
const size_t nrhoYe = rhoYe_axis.size(); const size_t nrhoYe = rhoYe_axis.size();
const size_t nmue = mue_axis.size();
auto get_val = [&](const size_t i_t, const size_t j_r, const size_t k_m) { // Get the four corner payloads for the bilinear interpolation
return field_accessor(data[(i_t * nrhoYe + j_r) * nmue + k_m]); const auto& p00 = grid.data[(i * nrhoYe) + j];
const auto& p01 = grid.data[(i * nrhoYe) + j + 1];
const auto& p10 = grid.data[((i + 1) * nrhoYe) + j];
const auto& p11 = grid.data[((i + 1) * nrhoYe) + j + 1];
// Fractional distances for the 2D bilinear interpolation
const double td = (t9 - t9_axis[i]) / (t9_axis[i + 1] - t9_axis[i]);
const double rd = (log_rhoYe - rhoYe_axis[j]) / (rhoYe_axis[j + 1] - rhoYe_axis[j]);
// Helper lambda to linearly interpolate between two full payloads
auto lerp_payload = [](const WeakRatePayload& p0, const WeakRatePayload& p1, double t) {
return WeakRatePayload{
.log_beta_plus = std::lerp(p0.log_beta_plus, p1.log_beta_plus, t),
.log_electron_capture = std::lerp(p0.log_electron_capture, p1.log_electron_capture, t),
.log_neutrino_loss_ec = std::lerp(p0.log_neutrino_loss_ec, p1.log_neutrino_loss_ec, t),
.log_beta_minus = std::lerp(p0.log_beta_minus, p1.log_beta_minus, t),
.log_positron_capture = std::lerp(p0.log_positron_capture, p1.log_positron_capture, t),
.log_antineutrino_loss_bd = std::lerp(p0.log_antineutrino_loss_bd, p1.log_antineutrino_loss_bd, t),
};
}; };
const double c000 = get_val(i, j, k); // Perform the bilinear interpolation
const double c001 = get_val(i, j, k + 1); const WeakRatePayload p0 = lerp_payload(p00, p01, rd);
const double c010 = get_val(i, j + 1, k); const WeakRatePayload p1 = lerp_payload(p10, p11, rd);
const double c011 = get_val(i, j + 1, k + 1);
const double c100 = get_val(i + 1, j, k);
const double c101 = get_val(i + 1, j, k + 1);
const double c110 = get_val(i + 1, j + 1, k);
const double c111 = get_val(i + 1, j + 1, k + 1);
const double c00 = lerp(c000, c001, md); return lerp_payload(p0, p1, td);
const double c01 = lerp(c010, c011, md);
const double c10 = lerp(c100, c101, md);
const double c11 = lerp(c110, c111, md);
const double c0 = lerp(c00, c01, rd);
const double c1 = lerp(c10, c11, rd);
return lerp(c0, c1, td);
};
WeakRatePayload result;
result.log_beta_plus = interpolationField([](const WeakRatePayload& p) { return p.log_beta_plus; });
result.log_electron_capture = interpolationField([](const WeakRatePayload& p) { return p.log_electron_capture; });
result.log_neutrino_loss_ec = interpolationField([](const WeakRatePayload& p) { return p.log_neutrino_loss_ec; });
result.log_beta_minus = interpolationField([](const WeakRatePayload& p) { return p.log_beta_minus; });
result.log_positron_capture = interpolationField([](const WeakRatePayload& p) { return p.log_positron_capture; });
result.log_antineutrino_loss_bd = interpolationField([](const WeakRatePayload& p) { return p.log_antineutrino_loss_bd; });
return result;
} }
std::expected<WeakRateDerivatives, InterpolationError> WeakRateInterpolator::get_rate_derivatives( std::expected<WeakRateDerivatives, InterpolationError> WeakRateInterpolator::get_rate_derivatives(
uint16_t A, uint16_t A,
uint8_t Z, uint8_t Z,
double t9, double t9,
double log_rhoYe, double log_rhoYe
double mu_e
) const { ) const {
WeakRateDerivatives result; WeakRateDerivatives result{};
//TODO: Make this perturbation scale aware
constexpr double eps = 1e-6; // Small perturbation for finite difference constexpr double eps = 1e-6; // Small perturbation for finite difference
// Perturbations for finite difference // Perturbations for finite difference
const double h_t9 = (t9 > 1e-9) ? t9 * eps : eps; const double h_t9 = (t9 > 1e-9) ? t9 * eps : eps;
const auto payload_plus_t9 = get_rates(A, Z, t9 + h_t9, log_rhoYe, mu_e); const auto payload_plus_t9 = get_rates(A, Z, t9 + h_t9, log_rhoYe);
const auto payload_minus_t9 = get_rates(A, Z, t9 - h_t9, log_rhoYe, mu_e); const auto payload_minus_t9 = get_rates(A, Z, t9 - h_t9, log_rhoYe);
const double h_rhoYe = (std::abs(log_rhoYe) > 1e-9) ? std::abs(log_rhoYe) * eps : eps; const double h_rhoYe = (std::abs(log_rhoYe) > 1e-9) ? std::abs(log_rhoYe) * eps : eps;
const auto payload_plus_rhoYe = get_rates(A, Z, t9, log_rhoYe + h_rhoYe, mu_e); const auto payload_plus_rhoYe = get_rates(A, Z, t9, log_rhoYe + h_rhoYe);
const auto payload_minus_rhoYe = get_rates(A, Z, t9, log_rhoYe - h_rhoYe, mu_e); const auto payload_minus_rhoYe = get_rates(A, Z, t9, log_rhoYe - h_rhoYe);
const double h_mue = (std::abs(mu_e) > 1e-9) ? std::abs(mu_e) * eps : eps;
const auto payload_plus_mue = get_rates(A, Z, t9, log_rhoYe, mu_e + h_mue);
const auto payload_minus_mue = get_rates(A, Z, t9, log_rhoYe, mu_e - h_mue);
if (!payload_plus_t9 || !payload_minus_t9 || !payload_plus_rhoYe || !payload_minus_rhoYe || !payload_plus_mue || !payload_minus_mue) { if (!payload_plus_t9 || !payload_minus_t9 || !payload_plus_rhoYe || !payload_minus_rhoYe) {
const auto it = m_rate_table.find(pack_isotope_id(A, Z)); // Determine which perturbation failed and return a consolidated error
if (it == m_rate_table.end()) { auto first_error = !payload_plus_t9 ? payload_plus_t9.error() :
return std::unexpected(InterpolationError{InterpolationErrorType::UNKNOWN_SPECIES_ERROR}); !payload_minus_t9 ? payload_minus_t9.error() :
} !payload_plus_rhoYe ? payload_plus_rhoYe.error() :
payload_minus_rhoYe.error();
const IsotopeGrid& grid = it->second; return std::unexpected(first_error);
InterpolationError error;
std::unordered_map<TableAxes, BoundsErrorInfo> boundsInfo;
if (!payload_minus_t9 || !payload_plus_t9) {
boundsInfo[TableAxes::T9] = BoundsErrorInfo{
TableAxes::T9,
grid.t9_axis.front(),
grid.t9_axis.back(),
t9
};
}
if (!payload_minus_rhoYe || !payload_plus_rhoYe) {
boundsInfo[TableAxes::LOG_RHOYE] = BoundsErrorInfo{
TableAxes::LOG_RHOYE,
grid.rhoYe_axis.front(),
grid.rhoYe_axis.back(),
log_rhoYe
};
}
if (!payload_minus_mue || !payload_plus_mue) {
boundsInfo[TableAxes::MUE] = BoundsErrorInfo{
TableAxes::MUE,
grid.mue_axis.front(),
grid.mue_axis.back(),
mu_e
};
}
error.type = InterpolationErrorType::BOUNDS_ERROR;
error.boundsErrorInfo = boundsInfo;
return std::unexpected(error);
} }
// Derivatives wrt. T9 // Derivatives wrt. T9
@@ -288,15 +219,6 @@ namespace gridfire::rates::weak {
result.d_log_positron_capture[1] = (payload_plus_rhoYe->log_positron_capture - payload_minus_rhoYe->log_positron_capture) / rhoYe_denominator; result.d_log_positron_capture[1] = (payload_plus_rhoYe->log_positron_capture - payload_minus_rhoYe->log_positron_capture) / rhoYe_denominator;
result.d_log_antineutrino_loss_bd[1] = (payload_plus_rhoYe->log_antineutrino_loss_bd - payload_minus_rhoYe->log_antineutrino_loss_bd) / rhoYe_denominator; result.d_log_antineutrino_loss_bd[1] = (payload_plus_rhoYe->log_antineutrino_loss_bd - payload_minus_rhoYe->log_antineutrino_loss_bd) / rhoYe_denominator;
// Derivatives wrt. MuE
const double mue_denominator = 2 * h_mue;
result.d_log_beta_plus[2] = (payload_plus_mue->log_beta_plus - payload_minus_mue->log_beta_plus) / mue_denominator;
result.d_log_beta_minus[2] = (payload_plus_mue->log_beta_minus - payload_minus_mue->log_beta_minus) / mue_denominator;
result.d_log_electron_capture[2] = (payload_plus_mue->log_electron_capture - payload_minus_mue->log_electron_capture) / mue_denominator;
result.d_log_neutrino_loss_ec[2] = (payload_plus_mue->log_neutrino_loss_ec - payload_minus_mue->log_neutrino_loss_ec) / mue_denominator;
result.d_log_positron_capture[2] = (payload_plus_mue->log_positron_capture - payload_minus_mue->log_positron_capture) / mue_denominator;
result.d_log_antineutrino_loss_bd[2] = (payload_plus_mue->log_antineutrino_loss_bd - payload_minus_mue->log_antineutrino_loss_bd) / mue_denominator;
return result; return result;
} }

View File

@@ -393,8 +393,8 @@ namespace gridfire::solver {
void CVODESolverStrategy::calculate_rhs( void CVODESolverStrategy::calculate_rhs(
const sunrealtype t, const sunrealtype t,
const N_Vector y, N_Vector y,
const N_Vector ydot, N_Vector ydot,
const CVODEUserData *data const CVODEUserData *data
) const { ) const {
const size_t numSpecies = m_engine.getNetworkSpecies().size(); const size_t numSpecies = m_engine.getNetworkSpecies().size();

View File

@@ -1,4 +1,4 @@
[wrap-git] [wrap-git]
url = https://github.com/4D-STAR/fourdst url = https://github.com/4D-STAR/fourdst
revision = v0.8.1 revision = v0.8.2
depth = 1 depth = 1

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -81,7 +81,7 @@ int main(int argc, char* argv[]){
g_previousHandler = std::set_terminate(quill_terminate_handler); g_previousHandler = std::set_terminate(quill_terminate_handler);
quill::Logger* logger = fourdst::logging::LogManager::getInstance().getLogger("log"); quill::Logger* logger = fourdst::logging::LogManager::getInstance().getLogger("log");
logger->set_log_level(quill::LogLevel::TraceL3); logger->set_log_level(quill::LogLevel::TraceL2);
LOG_INFO(logger, "Starting Adaptive Engine View Example..."); LOG_INFO(logger, "Starting Adaptive Engine View Example...");
using namespace gridfire; using namespace gridfire;