docs(ridfire)

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

View File

@@ -1,3 +1,31 @@
/**
* @file dynamic_engine_diagnostics.h
* @brief Diagnostics utilities for DynamicEngine instances.
*
* These free functions provide diagnostic tools for analyzing and reporting
* on the behavior of DynamicEngine instances in GridFire. They include
* functions for identifying limiting species, inspecting species balance,
* and evaluating Jacobian stiffness. Generally these functions are expensive to call
* and should be avoided in performance-critical code paths or during large simulations.
* These are primarily intended for debugging and analysis during development.
*
* @section Functions
* - report_limiting_species: Identifies species that are limiting the
* accuracy of the integration based on specified tolerances.
* - inspect_species_balance: Analyzes the production and destruction rates
* of a specified species to identify imbalances.
* - inspect_jacobian_stiffness: Evaluates the stiffness of the Jacobian
* matrix to identify potential numerical issues.
* - save_jacobian_to_file: Saves the Jacobian matrix to a file for
* external analysis.
*
* @note Each of these functions, aside from save_jacobian_to_file, may return results in JSON format for easy
* integration with other tools and workflows. If the `json` parameter is set
* then the output will be formatted as JSON; otherwise, it will be printed
* to standard output and `std::nullopt` will be returned.
*/
#pragma once
#include "gridfire/engine/engine_abstract.h"
@@ -5,40 +33,74 @@
#include <vector>
#include <string>
namespace gridfire::diagnostics {
void report_limiting_species(
const DynamicEngine& engine,
const std::vector<double>& Y_full,
const std::vector<double>& E_full,
#include "nlohmann/json.hpp"
namespace gridfire::engine::diagnostics {
/**
* @brief Report the species that are limiting the accuracy of the integration. This is useful for identifying
* species that may be limiting the timestepping due to their high relative errors compared to the specified tolerances.
* @param engine Constant reference to the DynamicEngine instance.
* @param Y_full Vector of the current species molar abundances sorted in the same order as the dynamic engine species list.
* @param E_full Vector of the current species molar abundance errors sorted in the same order as the dynamic engine species list.
* @param relTol Relative tolerance for the integration.
* @param absTol Absolute tolerance for the integration.
* @param top_n The number of top limiting species to report. Default is 10.
* @param json Flag indicating whether to return the results in JSON format. If false, results are printed to standard output.
* @return std::optional<nlohmann::json> JSON object containing the limiting species report if `json` is true; otherwise, std::nullopt.
*/
std::optional<nlohmann::json> report_limiting_species(
const DynamicEngine &engine,
const std::vector<double> &Y_full,
const std::vector<double> &E_full,
double relTol,
double absTol,
size_t top_n = 10
size_t top_n = 10,
bool json = false
);
void inspect_species_balance(
/**
* @brief Inspect the production and destruction balance of a specific species in the reaction network. This function analyzes the reactions that create and destroy the specified species,
* providing insights into potential imbalances that may affect the accuracy of the simulation.
* @param engine Constant reference to the DynamicEngine instance.
* @param species_name The name of the species to inspect.
* @param comp The current composition of the system as a fourdst::composition::Composition object.
* @param T9 The temperature in GK (10^9 K).
* @param rho The density in g/cm^3.
* @param json Flag indicating whether to return the results in JSON format. If false, results are printed to standard output.
* @return std::optional<nlohmann::json> JSON object containing the species balance report if `json` is true; otherwise, std::nullopt.
*/
std::optional<nlohmann::json> inspect_species_balance(
const DynamicEngine& engine,
const std::string& species_name,
const fourdst::composition::Composition &comp,
double T9,
double rho
double rho,
bool json
);
void inspect_jacobian_stiffness(
const DynamicEngine& engine,
const fourdst::composition::Composition &comp,
double T9,
double rho
);
void inspect_jacobian_stiffness(
const DynamicEngine& engine,
/**
* @brief Inspect the stiffness of the Jacobian matrix for the reaction network at the given temperature and density. This function evaluates the diagonal and off-diagonal elements of the Jacobian to identify potential numerical issues related to stiffness.
* @param engine Constant reference to the DynamicEngine instance.
* @param comp The current composition of the system as a fourdst::composition::Composition object.
* @param T9 The temperature in GK (10^9 K).
* @param rho The density in g/cm^3.
* @param json Flag indicating whether to return the results in JSON format. If false, results are printed to standard output.
* @return std::optional<nlohmann::json> JSON object containing the Jacobian stiffness report if `json` is true; otherwise, std::nullopt.
*/
std::optional<nlohmann::json> inspect_jacobian_stiffness(
const DynamicEngine &engine,
const fourdst::composition::Composition &comp,
double T9,
double rho,
bool save,
const std::optional<std::string>& filename
bool json
);
/**
* @brief Save the Jacobian matrix to a file for external analysis.
* @param jacobian Constant reference to the NetworkJacobian instance to be saved.
* @param filename The name of the file where the Jacobian will be saved.
*/
void save_jacobian_to_file(
const NetworkJacobian& jacobian,
const std::string& filename

View File

@@ -1,7 +1,7 @@
#pragma once
#include "gridfire/reaction/reaction.h"
#include "gridfire/network.h"
#include "gridfire/types/types.h"
#include "gridfire/screening/screening_abstract.h"
#include "gridfire/screening/screening_types.h"
@@ -9,8 +9,6 @@
#include "gridfire/engine/types/building.h"
#include "gridfire/engine/types/jacobian.h"
#include "gridfire/expectations/expected_engine.h"
#include "fourdst/composition/composition_abstract.h"
#include <vector>
@@ -31,16 +29,8 @@
* Emily M. Boudreaux
*/
namespace gridfire {
namespace gridfire::engine {
/**
* @brief Concept for types allowed in engine calculations.
*
* This concept restricts template parameters to either double or CppAD::AD<double>,
* enabling both standard and automatic differentiation types.
*/
template<typename T>
concept IsArithmeticOrAD = std::is_same_v<T, double> || std::is_same_v<T, CppAD::AD<double>>;
/**
* @brief Structure holding derivatives and energy generation for a network step.
@@ -68,8 +58,14 @@ namespace gridfire {
StepDerivatives() : dydt(), nuclearEnergyGenerationRate(T(0.0)) {}
};
using SparsityPattern = std::vector<std::pair<size_t, size_t>>;
using SparsityPattern = std::vector<std::pair<size_t, size_t>>; ///< Type alias for sparsity pattern representation.
/**
* @brief Structure holding derivatives of energy generation rate with respect to T and rho.
*
* This struct encapsulates the partial derivatives of the specific nuclear energy
* generation rate with respect to temperature and density.
*/
struct EnergyDerivatives {
double dEps_dT = 0.0; ///< Partial derivative of energy generation rate with respect to temperature.
double dEps_dRho = 0.0;///< Partial derivative of energy generation rate with respect to density.
@@ -80,6 +76,35 @@ namespace gridfire {
}
};
/**
* @brief Enumeration of possible engine statuses.
*
* This enum defines the various states an engine can be in after performing
* calculations, such as being up-to-date (OKAY), needing an update (STALE),
* or encountering an error (ERROR).
*/
enum class EngineStatus {
OKAY,
STALE,
ERROR,
COUNT
};
/**
* @brief Convert EngineStatus enum to string representation.
*
* @param status The EngineStatus value to convert.
* @return A string_view representing the name of the EngineStatus.
*/
constexpr std::string_view EngineStatus_to_string(const EngineStatus status) {
constexpr std::array<std::string_view, static_cast<size_t>(EngineStatus::COUNT)> names = {
"OKAY",
"STALE",
"ERROR"
};
return names[static_cast<size_t>(status)];
}
/**
* @brief Abstract base class for a reaction network engine.
*
@@ -123,7 +148,7 @@ namespace gridfire {
* time derivatives of all species and the specific nuclear energy generation
* rate for the current state.
*/
[[nodiscard]] virtual std::expected<StepDerivatives<double>, expectations::StaleEngineError> calculateRHSAndEnergy(
[[nodiscard]] virtual std::expected<StepDerivatives<double>, EngineStatus> calculateRHSAndEnergy(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
@@ -142,7 +167,7 @@ namespace gridfire {
*
* Intended usage: Derive from this class to implement engines that support
* advanced solver features such as implicit integration, sensitivity analysis,
* QSE (Quasi-Steady-State Equilibrium) handling, and more.
* QSE (Quasi-Steady-State Equilibrium) handling, and more. Generally this will be the main engine type
*/
class DynamicEngine : public Engine {
public:
@@ -162,6 +187,18 @@ namespace gridfire {
double rho
) const = 0;
/**
* @brief Generate the Jacobian matrix for the current state using a subset of active species.
*
* @param comp Composition object containing current abundances.
* @param T9 Temperature in units of 10^9 K.
* @param rho Density in g/cm^3.
* @param activeSpecies The set of species to include in the Jacobian calculation.
*
* This method must compute and store the Jacobian matrix (∂(dY/dt)_i/∂Y_j)
* for the current state, considering only the specified subset of active species.
* The matrix can then be accessed via getJacobianMatrixEntry().
*/
[[nodiscard]] virtual NetworkJacobian generateJacobianMatrix(
const fourdst::composition::CompositionAbstract &comp,
double T9,
@@ -169,6 +206,22 @@ namespace gridfire {
const std::vector<fourdst::atomic::Species>& activeSpecies
) const = 0;
/**
* @brief Generate the Jacobian matrix for the current state with a specified sparsity pattern.
*
* @param comp Composition object containing current abundances.
* @param T9 Temperature in units of 10^9 K.
* @param rho Density in g/cm^3.
* @param sparsityPattern The sparsity pattern to use for the Jacobian matrix.
*
* This method must compute and store the Jacobian matrix (∂(dY/dt)_i/∂Y_j)
* for the current state using automatic differentiation, taking into
* account the provided sparsity pattern. The matrix can then be accessed
* via `getJacobianMatrixEntry()`.
*
* @see getJacobianMatrixEntry()
*/
[[nodiscard]] virtual NetworkJacobian generateJacobianMatrix(
const fourdst::composition::CompositionAbstract &comp,
double T9,
@@ -242,6 +295,15 @@ namespace gridfire {
*/
[[nodiscard]] virtual const reaction::ReactionSet& getNetworkReactions() const = 0;
/**
* @brief Set the reactions for the network.
*
* @param reactions The set of reactions to use in the network.
*
* This method replaces the current set of reactions in the network
* with the provided set. It marks the engine as stale, requiring
* regeneration of matrices and recalculation of rates.
*/
virtual void setNetworkReactions(const reaction::ReactionSet& reactions) = 0;
/**
@@ -255,13 +317,24 @@ namespace gridfire {
* This method estimates the timescale for abundance change of each species,
* which can be used for timestep control, diagnostics, and reaction network culling.
*/
[[nodiscard]] virtual std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> getSpeciesTimescales(
[[nodiscard]] virtual std::expected<std::unordered_map<fourdst::atomic::Species, double>, EngineStatus> getSpeciesTimescales(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
) const = 0;
[[nodiscard]] virtual std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> getSpeciesDestructionTimescales(
/**
* @brief Compute destruction timescales for all species in the network.
*
* @param comp Composition object containing current abundances.
* @param T9 Temperature in units of 10^9 K.
* @param rho Density in g/cm^3.
* @return Map from Species to their destruction timescales (s).
*
* This method estimates the destruction timescale for each species,
* which can be useful for understanding reaction flows and equilibrium states.
*/
[[nodiscard]] virtual std::expected<std::unordered_map<fourdst::atomic::Species, double>, EngineStatus> getSpeciesDestructionTimescales(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
@@ -289,6 +362,26 @@ namespace gridfire {
*/
virtual fourdst::composition::Composition update(const NetIn &netIn) = 0;
/**
* @brief Check if the engine's internal state is stale.
*
* @param netIn A struct containing the current network input, such as
* temperature, density, and composition.
* @return True if the engine's state is stale and needs to be updated; false otherwise.
*
* This method allows derived classes to determine if their internal state
* is out-of-date with respect to the provided network conditions. If the engine
* is stale, it may require a call to `update()` before performing calculations.
*
* @par Usage Example:
* @code
* NetIn input = { ... };
* if (myEngine.isStale(input)) {
* // Update the engine before proceeding
* }
* @endcode
*/
[[nodiscard]]
virtual bool isStale(const NetIn& netIn) = 0;
/**
@@ -405,6 +498,15 @@ namespace gridfire {
double rho
) const = 0;
/**
* @brief Get the status of a species in the network.
*
* @param species The species to check.
* @return SpeciesStatus indicating whether the species is active, inactive, or culled.
*
* This method allows querying the current status of a specific species
* within the engine's network.
*/
[[nodiscard]] virtual SpeciesStatus getSpeciesStatus(const fourdst::atomic::Species& species) const = 0;
};

View File

@@ -5,7 +5,7 @@
#include "fourdst/logging/logging.h"
#include "fourdst/config/config.h"
#include "gridfire/network.h"
#include "gridfire/types/types.h"
#include "gridfire/reaction/reaction.h"
#include "gridfire/engine/engine_abstract.h"
#include "gridfire/screening/screening_abstract.h"
@@ -33,7 +33,7 @@
// this makes extra copies of the species, which is not ideal and could be optimized further.
// Even more relevant is the member m_reactionIDMap which makes copies of a REACLIBReaction for each reaction ID.
// REACLIBReactions are quite large data structures, so this could be a performance bottleneck.
namespace gridfire {
namespace gridfire::engine {
/**
* @brief Alias for CppAD AD type for double precision.
*
@@ -150,7 +150,7 @@ namespace gridfire {
*
* @see StepDerivatives
*/
[[nodiscard]] std::expected<StepDerivatives<double>, expectations::StaleEngineError> calculateRHSAndEnergy(
[[nodiscard]] std::expected<StepDerivatives<double>, engine::EngineStatus> calculateRHSAndEnergy(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
@@ -172,7 +172,7 @@ namespace gridfire {
*
* @see StepDerivatives
*/
[[nodiscard]] std::expected<StepDerivatives<double>, expectations::StaleEngineError> calculateRHSAndEnergy(
[[nodiscard]] std::expected<StepDerivatives<double>, EngineStatus> calculateRHSAndEnergy(
const fourdst::composition::CompositionAbstract& comp,
double T9,
double rho,
@@ -372,7 +372,8 @@ namespace gridfire {
* This method estimates the timescale for abundance change of each species,
* which can be used for timestep control or diagnostics.
*/
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> getSpeciesTimescales(
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, engine::EngineStatus>
getSpeciesTimescales(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
@@ -391,7 +392,7 @@ namespace gridfire {
* considering only the specified subset of reactions. This allows for flexible
* calculations with different reaction sets without modifying the engine's internal state.
*/
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> getSpeciesTimescales(
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, EngineStatus> getSpeciesTimescales(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho,
@@ -410,7 +411,8 @@ namespace gridfire {
* This method estimates the destruction timescale for each species,
* which can be useful for understanding reaction flows and equilibrium states.
*/
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> getSpeciesDestructionTimescales(
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, engine::EngineStatus>
getSpeciesDestructionTimescales(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
@@ -429,7 +431,7 @@ namespace gridfire {
* considering only the specified subset of reactions. This allows for flexible
* calculations with different reaction sets without modifying the engine's internal state.
*/
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> getSpeciesDestructionTimescales(
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, EngineStatus> getSpeciesDestructionTimescales(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho,
@@ -450,6 +452,7 @@ namespace gridfire {
const NetIn &netIn
) override;
/**
* @brief Checks if the engine view is stale and needs to be updated.
*
@@ -739,6 +742,16 @@ namespace gridfire {
*/
fourdst::composition::Composition collectComposition(const fourdst::composition::CompositionAbstract &comp, double T9, double rho) const override;
/**
* @brief Gets the status of a species in the network.
*
* @param species The species for which to get the status.
* @return SpeciesStatus indicating the status of the species.
*
* This method checks if the given species is part of the network and
* returns its status (e.g., Active, Inactive, NotFound).
*/
[[nodiscard]]
SpeciesStatus getSpeciesStatus(const fourdst::atomic::Species &species) const override;
@@ -854,6 +867,9 @@ namespace gridfire {
mutable CppAD::ADFun<double> m_rhsADFun; ///< CppAD function for the right-hand side of the ODE.
mutable CppAD::ADFun<double> m_epsADFun; ///< CppAD function for the energy generation rate.
mutable CppAD::sparse_jac_work m_jac_work; ///< Work object for sparse Jacobian calculations.
bool m_has_been_primed = false; ///< Flag indicating if the engine has been primed.
CppAD::sparse_rc<std::vector<size_t>> m_full_jacobian_sparsity_pattern; ///< Full sparsity pattern for the Jacobian matrix.
std::set<std::pair<size_t, size_t>> m_full_sparsity_set; ///< For quick lookups of the base sparsity pattern

View File

@@ -1,3 +1,7 @@
/**
* @file construction.h
* @brief Functions for constructing nuclear reaction networks.
*/
#pragma once
#include "gridfire/reaction/reaction.h"
@@ -9,9 +13,14 @@
#include "gridfire/reaction/weak/weak_interpolator.h"
namespace gridfire {
namespace gridfire::engine {
/**
* @brief Flags to specify which types of nuclear reactions to include when constructing a reaction network.
*
* These flags allow fine-grained control over the inclusion of strong and weak nuclear reactions
* (beta decay, electron/positron capture) from various sources (Reaclib, WRL) during network construction.
* They can be combined using bitwise operations to create custom reaction sets.
*/
enum class NetworkConstructionFlags : uint32_t {
NONE = 0,
@@ -34,22 +43,63 @@ namespace gridfire {
ALL = STRONG | WRL_WEAK
};
/** @brief Helper function to convert NetworkConstructionFlags to their underlying integer type.
*
* This function facilitates bitwise operations on NetworkConstructionFlags by converting them
* to their underlying integer representation.
*
* @param f The NetworkConstructionFlags value to convert.
* @return The underlying integer representation of the flag.
*/
constexpr auto to_underlying(NetworkConstructionFlags f) noexcept {
return static_cast<std::underlying_type_t<NetworkConstructionFlags>>(f);
}
/** @brief Bitwise OR operator for NetworkConstructionFlags.
*
* This operator allows combining two NetworkConstructionFlags values using the bitwise OR operation.
*
* @param lhs The left-hand side NetworkConstructionFlags value.
* @param rhs The right-hand side NetworkConstructionFlags value.
* @return A new NetworkConstructionFlags value representing the combination of the two inputs.
*/
inline NetworkConstructionFlags operator|(const NetworkConstructionFlags lhs, const NetworkConstructionFlags rhs) {
return static_cast<NetworkConstructionFlags>(to_underlying(lhs) | to_underlying(rhs));
}
/** @brief Bitwise AND operator for NetworkConstructionFlags.
*
* This operator allows checking for common flags between two NetworkConstructionFlags values
* using the bitwise AND operation.
*
* @param lhs The left-hand side NetworkConstructionFlags value.
* @param rhs The right-hand side NetworkConstructionFlags value.
* @return A new NetworkConstructionFlags value representing the intersection of the two inputs.
*/
inline NetworkConstructionFlags operator&(const NetworkConstructionFlags lhs, const NetworkConstructionFlags rhs) {
return static_cast<NetworkConstructionFlags>(to_underlying(lhs) & to_underlying(rhs));
}
/** @brief Checks if a specific flag is set within a NetworkConstructionFlags value.
*
* This function determines whether a particular flag is present in a given NetworkConstructionFlags value.
*
* @param flags The NetworkConstructionFlags value to check.
* @param flag_to_check The specific flag to look for.
* @return True if the flag is set; otherwise, false.
*/
inline bool has_flag(const NetworkConstructionFlags flags, const NetworkConstructionFlags flag_to_check) {
return (flags & flag_to_check) != NetworkConstructionFlags::NONE;
}
/** @brief Converts NetworkConstructionFlags to a human-readable string.
*
* This function generates a comma-separated string representation of the set flags
* within a NetworkConstructionFlags value. If no flags are set, it returns "No reactions".
*
* @param flags The NetworkConstructionFlags value to convert.
* @return A string listing the set flags or "No reactions" if none are set.
*/
inline std::string NetworkConstructionFlagsToString(NetworkConstructionFlags flags) {
std::stringstream ss;
constexpr std::array<NetworkConstructionFlags, 6> bases_flags_array = {

View File

@@ -2,12 +2,12 @@
#include "gridfire/engine/engine_abstract.h"
#include "gridfire/engine/engine_graph.h"
#include "gridfire/network.h"
#include "gridfire/types/types.h"
#include "fourdst/atomic/atomicSpecies.h"
namespace gridfire {
namespace gridfire::engine {
/**
* @brief Primes absent species in the network to their equilibrium abundances.

View File

@@ -1,8 +1,16 @@
/**
* @file building.h
* @brief Defines types related to building reaction networks in the GridFire engine.
*
* This file contains the enumeration and variant type used to specify the depth of reaction
* network construction within the GridFire simulation engine.
*
*/
#pragma once
#include <variant>
namespace gridfire {
namespace gridfire::engine {
/**
* @enum NetworkBuildDepth

View File

@@ -1,9 +1,18 @@
#pragma once
#include "gridfire/engine/types/building.h"
#include "gridfire/engine/types/reporting.h"
namespace gridfire {
namespace gridfire::engine {
/**
* @enum EngineTypes
* @brief Enumeration of different engine types available in GridFire.
*
* Values:
* - GRAPH_ENGINE: The standard graph-based engine.
* - ADAPTIVE_ENGINE_VIEW: An engine that adapts based on certain criteria.
* - MULTISCALE_PARTITIONING_ENGINE_VIEW: An engine that partitions the system at multiple scales.
* - PRIMING_ENGINE_VIEW: An engine that uses a priming strategy for simulations.
* - DEFINED_ENGINE_VIEW: An engine defined by user specifications.
* - FILE_DEFINED_ENGINE_VIEW: An engine defined through external files.
*/
enum class EngineTypes {
GRAPH_ENGINE,
ADAPTIVE_ENGINE_VIEW,
@@ -13,7 +22,13 @@ namespace gridfire {
FILE_DEFINED_ENGINE_VIEW
};
inline std::string engine_type_to_string(const EngineTypes type) {
/**
* @brief Converts an EngineTypes enum value to its corresponding string representation.
*
* @param type The EngineTypes enum value to convert.
* @return A string_view representing the name of the engine type.
*/
constexpr std::string_view engine_type_to_string(const EngineTypes type) {
switch (type) {
case EngineTypes::GRAPH_ENGINE:
return "GraphEngine";

View File

@@ -1,69 +1,175 @@
/**
* @file jacobian.h
* @brief Wrapper for handling the network Jacobian matrix in GridFire. Currently uses Eigen's SparseMatrix.
*/
#pragma once
#include "fourdst/atomic/atomicSpecies.h"
#include "fourdst/composition/composition_abstract.h"
#include "quill/Logger.h"
#include <Eigen/SparseCore>
#include <Eigen/SparseQR>
#include <tuple>
#include <functional>
#include <unordered_map>
#include <optional>
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>;
namespace gridfire::engine {
constexpr double MIN_ABUNDANCE_TO_CONTRIBUTE_TO_JACOBIAN = 1e-100; ///< Minimum abundance for a species to contribute to the Jacobian
using JacobianEntry = std::pair<std::pair<fourdst::atomic::Species, fourdst::atomic::Species>, double>; ///< Represents an entry in the Jacobian matrix
/**
* @class NetworkJacobian
* @brief A wrapper class for handling the network Jacobian matrix.
*
* This class encapsulates an Eigen::SparseMatrix to represent the Jacobian matrix of a reaction network.
* It provides methods for accessing and modifying matrix entries using species identifiers, as well as
* utility functions for analyzing the matrix (e.g., checking for singularity, counting non-zero entries).
*/
class NetworkJacobian {
public:
/**
* @brief Constructs a NetworkJacobian with the given sparse matrix and species index mapping function.
*
* @param jacobianMatrix The sparse matrix representing the Jacobian.
* @param indexToSpeciesFunc A function that maps matrix indices to species identifiers.
*/
explicit NetworkJacobian(
const Eigen::SparseMatrix<double>& jacobianMatrix,
const std::function<fourdst::atomic::Species(size_t)> &indexToSpeciesFunc
);
/**
* @brief Copy constructor for NetworkJacobian.
*
* @param jacobian The NetworkJacobian instance to copy from.
*/
NetworkJacobian(const NetworkJacobian& jacobian);
/**
* @brief Copy assignment operator for NetworkJacobian.
*
* @param jacobian The NetworkJacobian instance to copy from.
* @return Reference to the assigned NetworkJacobian instance.
*/
NetworkJacobian(NetworkJacobian&& jacobian) noexcept;
/**
* @brief Move constructor for NetworkJacobian.
*
* @param jacobian The NetworkJacobian instance to move from.
* @return Reference to the assigned NetworkJacobian instance.
*/
NetworkJacobian& operator=(NetworkJacobian&& jacobian) noexcept;
/**
* @brief Accesses the value at the specified row and column corresponding to the given species.
* @param row Species for the row
* @param col Species for the column
* @return value at the specified position
*/
double operator()(
const fourdst::atomic::Species& row,
const fourdst::atomic::Species& col
) const;
/**
* @brief Accesses the value at the specified row and column indices.
* @param i Row index
* @param j Column index
* @return value at the specified position
*/
double operator()(
size_t i,
size_t j
) const;
/**
* @brief Sets the value at the specified row and column corresponding to the given species.
* @param row Row species
* @param col Column species
* @param value Value to set
*/
void set(
const fourdst::atomic::Species& row,
const fourdst::atomic::Species& col,
double value
);
/**
* @brief Sets the value at the specified row and column indices.
* @param i Row index
* @param j Column index
* @param value Value to set
*/
void set(
size_t i,
size_t j,
double value
);
/**
* @brief Sets the value in the Jacobian matrix based on a JacobianEntry.
* @param entry The JacobianEntry containing row species, column species, and value.
*/
void set(
const JacobianEntry &entry
);
/**
* @brief Retrieves the shape of the Jacobian matrix as a tuple (rows, columns).
* @return A tuple containing the number of rows and columns.
*/
std::tuple<size_t, size_t> shape() const;
/**
* @brief Retrieves the rank of the Jacobian matrix.
* @return The rank of the matrix.
* @note Rank is computed using QR decomposition and cached for efficiency. The rank is computed the first time
* this method is called and stored for subsequent calls. If any set operation is performed on the matrix, the cached rank is invalidated.
*/
size_t rank() const;
/**
* @brief Get the number of non-zero entries in the Jacobian matrix.
* @return The number of non-zero entries.
*/
size_t nnz() const;
/**
* @brief Checks if the Jacobian matrix is singular.
* @return True if the matrix is singular, false otherwise.
*/
bool singular() const;
/**
* @brief Retrieves all entries in the Jacobian matrix that are infinite.
* @return A vector of JacobianEntry representing infinite entries.
*/
[[nodiscard]] std::vector<JacobianEntry> infs() const;
/**
* @brief Retrieves all entries in the Jacobian matrix that are NaN (Not a Number).
* @return A vector of JacobianEntry representing NaN entries.
*/
[[nodiscard]] std::vector<JacobianEntry> nans() const;
/**
* @brief Provides access to the underlying sparse matrix data.
* @return The Eigen::SparseMatrix representing the Jacobian.
*/
[[nodiscard]] Eigen::SparseMatrix<double> data() const;
/**
* @brief Provides access to the species-to-index mapping.
* @return A constant reference to the species-to-index mapping.
*/
[[nodiscard]] const std::unordered_map<fourdst::atomic::Species, size_t>& mapping() const;
/**
* @brief Exports the Jacobian matrix to a CSV file.
* @param filename The name of the CSV file to export to.
*/
void to_csv(const std::string& filename) const;
private:
@@ -73,6 +179,19 @@ namespace gridfire {
mutable std::optional<size_t> m_rank = std::nullopt;
};
/**
* @brief Regularizes the given Jacobian matrix based on the provided composition.
*
* This function applies regularization techniques to the Jacobian matrix to improve its numerical stability.
* Specifically any (row, column) entries corresponding to species who's abundance is below a threshold of
* MIN_ABUNDANCE_TO_CONTRIBUTE_TO_JACOBIAN in the provided composition will be set to zero if those entries
* were either infinite or NaN.
*
* @param jacobian The NetworkJacobian to be regularized.
* @param comp The composition used for regularization.
* @param logger Optional logger for logging regularization steps and information.
* @return A new NetworkJacobian instance representing the regularized Jacobian matrix.
*/
NetworkJacobian regularize_jacobian(
const NetworkJacobian& jacobian,
const fourdst::composition::CompositionAbstract& comp,

View File

@@ -2,14 +2,11 @@
#include <map>
#include <string>
#include <vector>
#include <utility>
#include <ostream>
#include <sstream>
#include "fourdst/composition/composition.h"
#include "fourdst/atomic/atomicSpecies.h"
namespace gridfire {
namespace gridfire::engine {
/**
* @enum PrimingReportStatus
@@ -28,6 +25,7 @@ namespace gridfire {
*/
enum class PrimingReportStatus {
SUCCESS,
ALREADY_PRIMED,
SOLVER_FAILURE,
};
@@ -40,6 +38,7 @@ namespace gridfire {
inline std::map<PrimingReportStatus, std::string> PrimingReportStatusStrings = {
{PrimingReportStatus::SUCCESS, "SUCCESS"},
{PrimingReportStatus::SOLVER_FAILURE, "SOLVER_FAILURE"},
{PrimingReportStatus::ALREADY_PRIMED, "ALREADY_PRIMED"},
};
/**
@@ -78,6 +77,16 @@ namespace gridfire {
}
};
/**
* @enum SpeciesStatus
* @brief Enumerates the status of a species in the simulation.
*
* These status codes indicate the current state of a species:
* - ACTIVE: The species is actively participating in reactions.
* - EQUILIBRIUM: The species is in equilibrium and not changing concentration.
* - INACTIVE_FLOW: The species is present but not currently flowing.
* - NOT_PRESENT: The species is not present in the system.
*/
enum class SpeciesStatus {
ACTIVE,
EQUILIBRIUM,
@@ -85,6 +94,12 @@ namespace gridfire {
NOT_PRESENT
};
/**
* @brief Convert a SpeciesStatus enum value to its string representation.
*
* @param status The SpeciesStatus value to convert.
* @return A string representing the SpeciesStatus.
*/
inline std::string SpeciesStatus_to_string(const SpeciesStatus status) {
switch (status) {
case SpeciesStatus::ACTIVE:

View File

@@ -3,7 +3,7 @@
#include "gridfire/engine/views/engine_view_abstract.h"
#include "gridfire/screening/screening_abstract.h"
#include "gridfire/screening/screening_types.h"
#include "gridfire/network.h"
#include "gridfire/types/types.h"
#include "fourdst/atomic/atomicSpecies.h"
#include "fourdst/config/config.h"
@@ -13,7 +13,7 @@
#include "quill/Logger.h"
namespace gridfire {
namespace gridfire::engine {
/**
* @class AdaptiveEngineView
* @brief An engine view that dynamically adapts the reaction network based on runtime conditions.
@@ -101,7 +101,7 @@ namespace gridfire {
* @throws std::runtime_error If the AdaptiveEngineView is stale (i.e., `update()` has not been called).
* @see AdaptiveEngineView::update()
*/
[[nodiscard]] std::expected<StepDerivatives<double>, expectations::StaleEngineError> calculateRHSAndEnergy(
[[nodiscard]] std::expected<StepDerivatives<double>, engine::EngineStatus> calculateRHSAndEnergy(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
@@ -140,6 +140,20 @@ namespace gridfire {
double rho
) const override;
/**
* @brief Generates the Jacobian matrix for some set of active species such that that set is a subset of the active species in the view.
*
* @param comp The current composition of the system.
* @param T9 The temperature in units of 10^9 K.
* @param rho The density in g/cm^3.
* @param activeSpecies The list of active species for which to generate the Jacobian.
*
* This method maps the culled abundances to the full network abundances and calls the base engine
* to generate the Jacobian matrix.
*
* @throws std::runtime_error If the AdaptiveEngineView is stale (i.e., `update()` has not been called).
* @see AdaptiveEngineView::update()
*/
[[nodiscard]] NetworkJacobian generateJacobianMatrix(
const fourdst::composition::CompositionAbstract &comp,
double T9,
@@ -147,6 +161,20 @@ namespace gridfire {
const std::vector<fourdst::atomic::Species> &activeSpecies
) const override;
/**
* @brief Generates the Jacobian matrix for the active species with a given sparsity pattern.
*
* @param comp The current composition of the system.
* @param T9 The temperature in units of 10^9 K.
* @param rho The density in g/cm^3.
* @param sparsityPattern The sparsity pattern to use for the Jacobian matrix.
*
* This method maps the culled abundances to the full network abundances and calls the base engine
* to generate the Jacobian matrix.
*
* @throws std::runtime_error If the AdaptiveEngineView is stale (i.e., `update()` has not been called).
* @see AdaptiveEngineView::update()
*/
[[nodiscard]] NetworkJacobian generateJacobianMatrix(
const fourdst::composition::CompositionAbstract &comp,
double T9,
@@ -213,6 +241,15 @@ namespace gridfire {
*/
[[nodiscard]] const reaction::ReactionSet& getNetworkReactions() const override;
/**
* @brief Sets the reaction set for the base engine.
*
* This method delegates the call to the base engine to set the reaction set.
*
* @param reactions The ReactionSet to set in the base engine.
*
* @post The reaction set of the base engine is updated.
*/
void setNetworkReactions(const reaction::ReactionSet& reactions) override;
/**
@@ -228,13 +265,26 @@ namespace gridfire {
*
* @throws std::runtime_error If the AdaptiveEngineView is stale (i.e., `update()` has not been called).
*/
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> getSpeciesTimescales(
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, EngineStatus> getSpeciesTimescales(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
) const override;
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> getSpeciesDestructionTimescales(
/**
* @brief Computes destruction timescales for all active species in the network.
*
* @param comp Composition object containing current abundances.
* @param T9 Temperature in units of 10^9 K.
* @param rho Density in g/cm^3.
* @return Map from Species to their destruction timescales (s).
*
* This method maps the culled abundances to the full network abundances and calls the base engine
* to compute the species destruction timescales.
*
* @throws std::runtime_error If the AdaptiveEngineView is stale (i.e., `update()` has not been called).
*/
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, EngineStatus> getSpeciesDestructionTimescales(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
@@ -278,14 +328,62 @@ namespace gridfire {
*/
[[nodiscard]] screening::ScreeningType getScreeningModel() const override;
/**
* @brief Gets the index of a species in the active species list.
*
* @param species The species for which to get the index.
* @return The index of the species in the active species list.
*
* @throws std::runtime_error If the AdaptiveEngineView is stale (i.e., `update()` has not been called).
* @throws std::out_of_range If the species is not part of the active species in the adaptive engine view.
*/
[[nodiscard]] size_t getSpeciesIndex(const fourdst::atomic::Species &species) const override;
/**
* @brief Maps the molar abundance vector from the active species to the full network species.
*
* @param netIn The current network input, containing temperature, density, and composition.
* @return A vector of molar abundances for all species in the full network.
*
* This method constructs a molar abundance vector for the full network by mapping the
* abundances from the active species in `netIn` to their corresponding indices in the
* full network. Species not present in `netIn` are assigned an abundance of zero.
*
* @throws std::runtime_error If the AdaptiveEngineView is stale (i.e., `update()` has not been called).
*/
[[nodiscard]] std::vector<double> mapNetInToMolarAbundanceVector(const NetIn &netIn) const override;
/**
* @brief Primes the engine with the given network input.
*
* @param netIn The current network input, containing temperature, density, and composition.
* @return A PrimingReport indicating the result of the priming operation.
*
* This method delegates the priming operation to the base engine.
*/
[[nodiscard]] PrimingReport primeEngine(const NetIn &netIn) override;
fourdst::composition::Composition collectComposition(const fourdst::composition::CompositionAbstract &comp, double T9, double rho) const override;
/**
* @brief Collect the composition of the base engine, ensure all active species are registered, and pass
* the composition back to the caller.
*
* @param comp The current composition of the system.
* @param T9 The temperature in units of 10^9 K.
* @param rho The density in g/cm^3.
*
* @note This function ensures that the state of both the base engine and the adaptive view are synchronized in the
* result back to the caller
*/
[[nodiscard]] fourdst::composition::Composition collectComposition(const fourdst::composition::CompositionAbstract &comp, double T9, double rho) const override;
/**
* @brief Gets the status of a species in the network.
*
* @param species The species for which to get the status.
* @return The SpeciesStatus indicating the status of the species.
*
* This method delegates the call to the base engine to get the species status. If the base engine says that the species is active but it is not in the active species list of this view, the status is returned as INACTIVE_FLOW.
*/
[[nodiscard]] SpeciesStatus getSpeciesStatus(const fourdst::atomic::Species &species) const override;
private:
using Config = fourdst::config::Config;

View File

@@ -4,7 +4,7 @@
#include "gridfire/engine/engine_abstract.h"
#include "gridfire/engine/engine_graph.h"
#include "gridfire/io/network_file.h"
#include "gridfire/network.h"
#include "gridfire/types/types.h"
#include "fourdst/config/config.h"
#include "fourdst/logging/logging.h"
@@ -13,7 +13,7 @@
#include <string>
namespace gridfire{
namespace gridfire::engine {
class DefinedEngineView : public DynamicEngine, public EngineView<DynamicEngine> {
public:
DefinedEngineView(const std::vector<std::string>& peNames, GraphEngine& baseEngine);
@@ -42,7 +42,7 @@ namespace gridfire{
*
* @throws std::runtime_error If the view is stale (i.e., `update()` has not been called after `setNetworkFile()`).
*/
[[nodiscard]] std::expected<StepDerivatives<double>, expectations::StaleEngineError> calculateRHSAndEnergy(
[[nodiscard]] std::expected<StepDerivatives<double>, engine::EngineStatus> calculateRHSAndEnergy(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
@@ -150,7 +150,15 @@ namespace gridfire{
*/
[[nodiscard]] const reaction::ReactionSet& getNetworkReactions() const override;
/**
* @brief Sets the active reactions in the network.
*
* @param reactions The ReactionSet containing the reactions to set as active.
*
* @post The view is marked as stale and will need to be updated.
*/
void setNetworkReactions(const reaction::ReactionSet& reactions) override;
/**
* @brief Computes timescales for all active species in the network.
*
@@ -161,13 +169,25 @@ namespace gridfire{
*
* @throws std::runtime_error If the view is stale.
*/
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> getSpeciesTimescales(
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, engine::EngineStatus>
getSpeciesTimescales(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
) const override;
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> getSpeciesDestructionTimescales(
/**
* @brief Computes destruction timescales for all active species in the network.
*
* @param comp A Composition object containing the current composition of the system
* @param T9 Temperature in units of 10^9 K.
* @param rho Density in g/cm^3.
* @return Map from Species to their destruction timescales (s).
*
* @throws std::runtime_error If the view is stale.
*/
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, engine::EngineStatus>
getSpeciesDestructionTimescales(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
@@ -186,6 +206,12 @@ namespace gridfire{
*/
fourdst::composition::Composition update(const NetIn &netIn) override;
/**
* @brief Checks if the engine view is stale.
*
* @param netIn The current network input (unused).
* @return True if the view is stale and needs to be updated; false otherwise.
*/
[[deprecated]] bool isStale(const NetIn& netIn) override;
/**
@@ -202,14 +228,45 @@ namespace gridfire{
*/
[[nodiscard]] screening::ScreeningType getScreeningModel() const override;
/** @brief Maps a species from the full network to its index in the defined active network.
*
* @param species The species to map.
* @return The index of the species in the active network.
*
* @throws std::runtime_error If the species is not in the active set.
*/
[[nodiscard]] size_t getSpeciesIndex(const fourdst::atomic::Species &species) const override;
/**
* @brief Map from a NetIn object to a vector of molar abundances for the active species.
* @param netIn The NetIn object containing the full network abundances.
* @return A vector of molar abundances for the active species.
*/
[[nodiscard]] std::vector<double> mapNetInToMolarAbundanceVector(const NetIn &netIn) const override;
/**
* @brief Prime the engine view for calculations. This will delegate to the base engine.
* @param netIn The current network input.
* @return The PrimingReport from the base engine.
*/
[[nodiscard]] PrimingReport primeEngine(const NetIn &netIn) override;
fourdst::composition::Composition collectComposition(const fourdst::composition::CompositionAbstract &comp, double T9, double rho) const override;
/**
* @brief Collects a Composition object from the base engine.
*
* @param comp The full Composition object.
* @param T9 The temperature in units of 10^9 K.
* @param rho The density in g/cm^3.
* @return A composition object representing the state of the engine stack and the current view.
*/
[[nodiscard]] fourdst::composition::Composition collectComposition(const fourdst::composition::CompositionAbstract &comp, double T9, double rho) const override;
/**
* @brief Gets the status of a species in the active network.
*
* @param species The species for which to get the status.
* @return The SpeciesStatus indicating if the species is active, inactive, or not present.
*/
[[nodiscard]] SpeciesStatus getSpeciesStatus(const fourdst::atomic::Species &species) const override;
protected:
bool m_isStale = true;

View File

@@ -3,14 +3,13 @@
#include "gridfire/engine/engine_abstract.h"
#include "gridfire/engine/views/engine_view_abstract.h"
#include "gridfire/engine/engine_graph.h"
#include "sundials/sundials_linearsolver.h"
#include "sundials/sundials_matrix.h"
#include "sundials/sundials_nvector.h"
#include "sundials/sundials_types.h"
#include "unsupported/Eigen/NonLinearOptimization"
namespace gridfire {
namespace gridfire::engine {
/**
* @class MultiscalePartitioningEngineView
* @brief An engine view that partitions the reaction network into multiple groups based on timescales.
@@ -119,12 +118,19 @@ namespace gridfire {
* @throws StaleEngineError If the QSE cache does not contain an entry for the given
* (T9, rho, Y_full). This indicates `update()` was not called recently enough.
*/
[[nodiscard]] std::expected<StepDerivatives<double>, expectations::StaleEngineError> calculateRHSAndEnergy(
[[nodiscard]] std::expected<StepDerivatives<double>, engine::EngineStatus> calculateRHSAndEnergy(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
) const override;
/**
* @brief Calculates the energy generation rate derivatives with respect to abundances.
* @param comp The current composition.
* @param T9 The temperature in units of 10^9 K.
* @param rho The density in g/cm^3.
* @return The energy generation rate derivatives (dEps/dT and dEps/drho).
*/
[[nodiscard]] EnergyDerivatives calculateEpsDerivatives(
const fourdst::composition::CompositionAbstract &comp,
double T9,
@@ -327,7 +333,8 @@ namespace gridfire {
* @pre The engine must have a valid QSE cache entry for the given state.
* @throws StaleEngineError If the QSE cache misses.
*/
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> getSpeciesTimescales(
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, engine::EngineStatus>
getSpeciesTimescales(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
@@ -353,7 +360,8 @@ namespace gridfire {
* @pre The engine must have a valid QSE cache entry for the given state.
* @throws StaleEngineError If the QSE cache misses.
*/
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, expectations::StaleEngineError> getSpeciesDestructionTimescales(
[[nodiscard]] std::expected<std::unordered_map<fourdst::atomic::Species, double>, engine::EngineStatus>
getSpeciesDestructionTimescales(
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
@@ -542,13 +550,54 @@ namespace gridfire {
*/
[[nodiscard]] const std::vector<fourdst::atomic::Species>& getDynamicSpecies() const;
/**
* @brief Checks if a species is involved in the partitioned network.
*
* @param species The species to check.
* @return `true` if the species is in either the dynamic or algebraic sets, `false` otherwise.
*
* @par Purpose
* To allow external queries about species involvement in the partitioned network.
*
* @par How
* It checks for membership in both `m_dynamic_species` and `m_algebraic_species`.
*
* @pre `partitionNetwork()` must have been called.
*/
bool involvesSpecies(const fourdst::atomic::Species &species) const;
/**
* @brief Check if a species is involved in the QSE (algebraic) set.
* @param species The species to check.
* @return Boolean indicating if the species is in the algebraic set.
*/
bool involvesSpeciesInQSE(const fourdst::atomic::Species &species) const;
/**
* @brief Check if a species is involved in the dynamic set.
* @param species The species to check.
* @return Boolean indicating if the species is in the dynamic set.
*/
bool involvesSpeciesInDynamic(const fourdst::atomic::Species &species) const;
/**
* @brief Gets a normalized composition with QSE species equilibrated.
*
* @param comp The input composition.
* @param T9 Temperature in units of 10^9 K.
* @param rho Density in g/cm^3.
* @return A new `Composition` object with algebraic species set to their equilibrium values.
*
* @par Purpose
* To provide a way to get the equilibrated composition without modifying the internal state.
*
* @par How
* It calls `solveQSEAbundances()` to compute the equilibrium abundances for the algebraic species,
* then constructs a new `Composition` object reflecting these values.
*
* @pre The engine must have a valid QSE partition for the given state.
* @throws StaleEngineError If the QSE cache misses.
*/
fourdst::composition::Composition getNormalizedEquilibratedComposition(const fourdst::composition::CompositionAbstract& comp, double T9, double rho) const;
/**
@@ -563,9 +612,23 @@ namespace gridfire {
*/
fourdst::composition::Composition collectComposition(const fourdst::composition::CompositionAbstract &comp, double T9, double rho) const override;
/**
* @brief Gets the status of a species in the network.
*
* @param species The species to query.
* @return The `SpeciesStatus` indicating if the species is dynamic, algebraic, or not involved.
*
* @par Purpose
* To allow external queries about the role of a species in the partitioned network.
*
* @par How
* It checks for membership in `m_dynamic_species` and `m_algebraic_species` to determine
* the appropriate status.
*
* @pre `partitionNetwork()` must have been called.
*/
SpeciesStatus getSpeciesStatus(const fourdst::atomic::Species &species) const override;
private:
/**
* @brief Struct representing a QSE group.
@@ -614,7 +677,16 @@ namespace gridfire {
*/
bool operator!=(const QSEGroup& other) const;
[[nodiscard]] [[maybe_unused]] std::string toString() const;
[[nodiscard]] [[maybe_unused]] std::string toString(bool verbose) const;
friend std::ostream& operator<<(std::ostream& os, const QSEGroup& group) {
os << group.toString(false);
return os;
}
bool contains(const fourdst::atomic::Species& species) const;
bool containsAlgebraic(const fourdst::atomic::Species &species) const;
bool containsSeed(const fourdst::atomic::Species &species) const;
};
class QSESolver {
@@ -642,7 +714,7 @@ namespace gridfire {
size_t solves() const;
void log_diagnostics() const;
void log_diagnostics(const QSEGroup &group, const fourdst::composition::Composition &comp) const;
private:
static int sys_func(
@@ -666,10 +738,15 @@ namespace gridfire {
fourdst::composition::Composition& comp;
const std::unordered_map<fourdst::atomic::Species, size_t>& qse_solve_species_index_map;
const std::vector<fourdst::atomic::Species>& qse_solve_species;
const QSESolver& instance;
};
private:
// Cache members
mutable size_t m_solves = 0;
mutable bool m_has_jacobian = false;
// Solver members
size_t m_N;
const DynamicEngine& m_engine;
std::vector<fourdst::atomic::Species> m_species;
@@ -692,6 +769,7 @@ namespace gridfire {
return LogManager::getInstance().getLogger("log");
}
};
struct FluxValidationResult {
@@ -743,7 +821,7 @@ namespace gridfire {
mutable std::unordered_map<uint64_t, fourdst::composition::Composition> m_composition_cache;
SUNContext m_sun_ctx = nullptr; // TODO: initialize and safely destroy this
SUNContext m_sun_ctx = nullptr;
private:
/**
@@ -770,6 +848,20 @@ namespace gridfire {
double rho
) const;
std::pair<bool, reaction::ReactionSet> group_is_a_qse_cluster(
const fourdst::composition::Composition &comp,
double T9,
double rho,
const QSEGroup &group
) const;
bool group_is_a_qse_pipeline(
const fourdst::composition::Composition &comp,
double T9,
double rho,
const QSEGroup &group
) const;
/**
* @brief Validates candidate QSE groups using flux analysis.
*

View File

@@ -10,7 +10,7 @@
#include <vector>
#include <string>
namespace gridfire {
namespace gridfire::engine {
/**
* @class NetworkPrimingEngineView

View File

@@ -25,7 +25,7 @@
* Emily M. Boudreaux
*/
namespace gridfire {
namespace gridfire::engine {
/**
* @brief Concept for types allowed as engine bases in EngineView.

View File

@@ -2,10 +2,25 @@
#include <source_location>
#include <string>
#include <format>
#include "gridfire/exceptions/error_gridfire.h"
namespace gridfire::exceptions {
/**
* @brief Exception class for debug-only errors.
*
* This exception is intended to be used for errors that should only
* occur during development and debugging. In release builds, attempts
* to throw this exception will result in a compilation error.
*
* Example usage:
* @code
* if (debug_condition_failed) {
* throw DebugException("Debug condition failed");
* }
* @endcode
*/
class DebugException final : public GridFireError{
public:
#ifdef NDEBUG

View File

@@ -3,46 +3,116 @@
#include "gridfire/exceptions/error_gridfire.h"
namespace gridfire::exceptions {
/**
* @brief Base class for engine-related exceptions.
*
* This class serves as the base for all exceptions specific to the
* reaction network engine in the GridFire library. It extends the
* GridFireError class and allows for custom error messages related
* to engine operations.
*/
class EngineError : public GridFireError {
using GridFireError::GridFireError;
};
/**
* @brief Exception for failures in partitioning the engine.
*
* This exception is thrown when the engine fails to partition
* the reaction network as required for certain calculations or
* optimizations.
*/
class FailedToPartitionEngineError final : public EngineError {
using EngineError::EngineError;
};
/**
* @brief Exception for errors during network resizing.
*
* This exception is thrown when the engine encounters an error
* while attempting to resize the reaction network, such as when
* adding or removing species or reactions.
*/
class NetworkResizedError final : public EngineError {
using EngineError::EngineError;
};
/**
* @brief Exception for failures in setting network reactions.
*
* This exception is thrown when the engine fails to properly
* set or initialize the reactions in the reaction network.
*/
class UnableToSetNetworkReactionsError final : public EngineError {
using EngineError::EngineError;
};
/**
* @brief Exception for invalid composition collection in the engine.
*
* This exception is thrown when the engine encounters an
* invalid state while trying to collect the composition from the entire engine stack
*/
class BadCollectionError final : public EngineError {
using EngineError::EngineError;
};
/**
* @brief Exception for invalid QSE solution in the engine.
*
* This exception is thrown when the engine computes an
* invalid solution while performing Quasi-Statistical Equilibrium (QSE) calculations.
*/
class InvalidQSESolutionError final : public EngineError {
using EngineError::EngineError;
};
/**
* @brief Exception for errors in calculating the right-hand side (RHS).
*
* This exception is thrown when the engine encounters an error
* while calculating the right-hand side of the reaction network equations.
*/
class BadRHSEngineError final : public EngineError {
using EngineError::EngineError;
};
/**
* @brief Base class for Jacobian-related exceptions.
*
* This class serves as the base for all exceptions specific to
* Jacobian matrix operations in the reaction network engine.
*/
class JacobianError : public EngineError {
using EngineError::EngineError;
};
/**
* @brief Exception for stale Jacobian matrix access.
*
* This exception is thrown when an attempt is made to access
* a Jacobian matrix that is stale and needs to be regenerated.
*/
class StaleJacobianError final : public JacobianError {
using JacobianError::JacobianError;
};
/**
* @brief Exception for uninitialized Jacobian matrix access.
*
* This exception is thrown when an attempt is made to access
* a Jacobian matrix that has not been initialized.
*/
class UninitializedJacobianError final: public JacobianError {
using JacobianError::JacobianError;
};
/**
* @brief Exception for unknown Jacobian matrix access.
*
* This exception is thrown when an attempt is made to access
* a Jacobian matrix that is unknown or not recognized by the engine.
*/
class UnknownJacobianError final : public JacobianError {
using JacobianError::JacobianError;
};

View File

@@ -4,6 +4,12 @@
#include <string>
namespace gridfire::exceptions {
/**
* @brief Base class for GridFire exceptions.
*
* This class serves as the base for all exceptions specific to the GridFire library.
* It extends the standard std::exception class and allows for custom error messages.
*/
class GridFireError : public std::exception {
public:
explicit GridFireError(std::string msg) : m_msg(std::move(msg)) {}

View File

@@ -6,6 +6,13 @@
#include "gridfire/exceptions/error_gridfire.h"
namespace gridfire::exceptions {
/**
* @class ReactionError
* @brief Base class for all exceptions related to reaction operations.
*
* This exception is the parent for more specific reaction-related errors. Catching this
* type will catch any exception originating from reaction handling.
*/
class ReactionError : public GridFireError {
private:
std::string m_message;

View File

@@ -3,26 +3,68 @@
#include "gridfire/exceptions/error_gridfire.h"
namespace gridfire::exceptions {
/**
* @class SolverError
* @brief Base class for all exceptions related to solver operations.
*
* This exception is the parent for more specific solver-related errors. Catching this
* type will catch any exception originating from the solver system.
*/
class SolverError : public GridFireError {
using GridFireError::GridFireError;
};
/**
* @class SingularJacobianError
* @brief Exception thrown when the Jacobian matrix is singular.
*
* This exception is thrown when the solver encounters a singular Jacobian matrix,
* which prevents it from proceeding with the solution of the system.
*/
class SingularJacobianError final : public SolverError {
using SolverError::SolverError;
};
/**
* @class IllConditionedJacobianError
* @brief Exception thrown when the Jacobian matrix is ill-conditioned.
*
* This exception is thrown when the solver detects that the Jacobian matrix
* is ill-conditioned, which may lead to inaccurate or unstable solutions.
*/
class IllConditionedJacobianError final : public SolverError {
using SolverError::SolverError;
};
/**
* @class SUNDIALSError
* @brief Base class for exceptions related to SUNDIALS solver operations.
*
* This class serves as the base for all exceptions specific to
* SUNDIALS solver errors in the GridFire library.
*/
class SUNDIALSError : public SolverError {
using SolverError::SolverError;
};
/**
* @class CVODESolverFailureError
* @brief Exception for failures in the CVODE solver.
*
* This exception is thrown when the CVODE solver from the SUNDIALS suite
* encounters a failure during its operation.
*/
class CVODESolverFailureError final : public SUNDIALSError {
using SUNDIALSError::SUNDIALSError;
};
/**
* @class KINSolSolverFailureError
* @brief Exception for failures in the KINSOL solver.
*
* This exception is thrown when the KINSOL solver from the SUNDIALS suite
* encounters a failure during its operation.
*/
class KINSolSolverFailureError final : public SUNDIALSError {
using SUNDIALSError::SUNDIALSError;
};

View File

@@ -3,10 +3,20 @@
#include "gridfire/exceptions/error_gridfire.h"
namespace gridfire::exceptions {
/**
* @brief Base class for utility-related errors in GridFire.
*
* This class serves as a base for all exceptions related to utility functions
*/
class UtilityError : public GridFireError {
using GridFireError::GridFireError;
};
/**
* @brief Exception class for hashing-related errors in GridFire.
*
* This class is used to represent errors that occur during hashing operations.
*/
class HashingError final : public UtilityError {
using UtilityError::UtilityError;
};

View File

@@ -8,3 +8,13 @@
#include "gridfire/exceptions/error_reaction.h"
#include "gridfire/exceptions/error_solver.h"
#include "gridfire/exceptions/error_utils.h"
/**
* @namespace gridfire::exceptions
* @brief Namespace for GridFire exception classes. All custom exceptions
* defined in GridFire inherit from std::exception and are organized
* within this namespace for clarity and modularity.
*
* All GridFire exception classes derive from std::exception and then from gridfire::exceptions::GridFireError;
*/
namespace gridfire::exceptions{}

View File

@@ -0,0 +1,14 @@
#pragma once
#include "gridfire/engine/engine.h"
#include "gridfire/exceptions/exceptions.h"
#include "gridfire/io/io.h"
#include "gridfire/partition/partition.h"
#include "gridfire/policy/policy.h"
#include "gridfire/reaction/reaction.h"
#include "gridfire/screening/screening.h"
#include "gridfire/solver/solver.h"
#include "gridfire/trigger/trigger.h"
#include "gridfire/utils/utils.h"
#include "types/types.h"

View File

@@ -0,0 +1,3 @@
#pragma once
#include "gridfire/io/generative/python.h"

View File

@@ -6,16 +6,59 @@
#include "gridfire/reaction/reaction.h"
#include "gridfire/engine/engine_abstract.h"
/**
* @brief Namespace for generative input/output functionalities.
*
* This namespace contains functions and structures related to exporting
* reactions and engines to Python code. It provides tools to convert
* internal representations of reactions and engines into Python functions
* and scripts that can be used for further analysis or integration with
* Python-based workflows.
*/
namespace gridfire::io::gen {
/**
* @brief Structure representing a Python function definition.
*
* This structure holds the name, code, and module requirements for
* a Python function that represents a reaction.
*/
struct PyFunctionDef {
std::string func_name;
std::string func_code;
std::vector<std::string> module_req;
};
/**
* @brief Exports a reaction to a Python function definition.
*
* @param reaction The reaction to export.
* @return A PyFunctionDef structure containing the Python function
* representation of the reaction.
*
* This function converts the given reaction into a Python function
* definition, including the necessary code and module requirements.
*/
PyFunctionDef exportReactionToPy(const reaction::Reaction& reaction);
std::string exportEngineToPy(const DynamicEngine& engine);
/**
* @brief Exports a dynamic engine to a Python script.
*
* @param engine The dynamic engine to export.
* @return A string containing the Python script representation of the engine.
*
* This function converts the given dynamic engine into a Python script
* that can be used to recreate the engine's functionality in Python.
*/
std::string exportEngineToPy(const engine::DynamicEngine& engine);
void exportEngineToPy(const DynamicEngine& engine, const std::string& fileName);
/**
* @brief Exports a dynamic engine to a Python file.
*
* @param engine The dynamic engine to export.
* @param fileName The name of the file to write the Python script to.
*
* This function writes the Python script representation of the given
* dynamic engine to the specified file.
*/
void exportEngineToPy(const engine::DynamicEngine& engine, const std::string& fileName);
}

View File

@@ -1,3 +1,4 @@
#pragma once
#include "gridfire/io/network_file.h"
#include "generative/generative.h"

View File

@@ -23,32 +23,117 @@
#include <memory>
namespace gridfire::policy {
/**
* @brief Base class for reaction chain policies that are active only within specific temperature ranges.
*
* Such chains may only operate effectively within certain temperature regimes, reflecting
* the physical conditions required for the reactions to proceed. This class allows defining
* such temperature-dependent behavior. This is one of the locations where domain specific knowledge is allowed
* within GridFire.
*/
class TemperatureDependentChainPolicy : public ReactionChainPolicy {
public:
/**
* @brief Construct a new Temperature Dependent Chain Policy object
* @param reactionIDs Vector of reaction IDs that comprise the chain
*
* @note The chain is considered active at all temperatures if no min/max T9 are provided.
*/
explicit TemperatureDependentChainPolicy(const std::vector<std::string>& reactionIDs);
/**
* @brief Construct a new Temperature Dependent Chain Policy object
* @param reactionIDs Vector of reaction IDs that comprise the chain
* @param minT9 Minimum temperature (in T9) for which the chain is active
*
* @note The chain is considered active at all temperatures above minT9.
*/
explicit TemperatureDependentChainPolicy(const std::vector<std::string>& reactionIDs, std::optional<double> minT9);
/**
* @brief Construct a new Temperature Dependent Chain Policy object
* @param reactionIDs Vector of reaction IDs that comprise the chain
* @param minT9 Minimum temperature (in T9) for which the chain is active
* @param maxT9 Maximum temperature (in T9) for which the chain is active
*/
explicit TemperatureDependentChainPolicy(const std::vector<std::string>& reactionIDs, std::optional<double> minT9, std::optional<double> maxT9);
/**
* @brief Virtual destructor
*/
~TemperatureDependentChainPolicy() override = default;
/**
* @brief Get the reactions that comprise the chain
* @return The set of reactions comprising the chain
*/
[[nodiscard]] const reaction::ReactionSet& get_reactions() const override;
/**
* @brief Check if a reaction or reaction ID is part of the chain
* @param id The reaction ID to check
* @return True if the reaction ID is part of the chain, false otherwise
*/
[[nodiscard]] bool contains(const std::string& id) const override;
/**
* @brief Check if a reaction is part of the chain
* @param reaction The reaction to check
* @return True if the reaction is part of the chain, false otherwise
*/
[[nodiscard]] bool contains(const reaction::Reaction& reaction) const override;
/**
* @brief Generate the hash for the reaction chain policy
* @param seed Seed value for the hash
* @return Unique hash representing the reaction chain policy. XXHash64 is used internally for speed and collision resistance.
*/
[[nodiscard]] uint64_t hash(uint64_t seed) const override;
/**
* @brief Equality operator
* @param other The other ReactionChainPolicy to compare against
* @return True if the two policies are equal, false otherwise
*/
[[nodiscard]] bool operator==(const ReactionChainPolicy& other) const override;
/**
* @brief Inequality operator
* @param other The other ReactionChainPolicy to compare against
* @return True if the two policies are not equal, false otherwise
*/
[[nodiscard]] bool operator!=(const ReactionChainPolicy& other) const override;
/**
* @brief Check if the reaction chain is active at a given temperature
* @param T9 Temperature in billions of Kelvin (T9)
* @return True if the chain is active at the given temperature, false otherwise
*/
[[nodiscard]] bool is_active(double T9) const;
protected:
/**
* @brief Struct to hold the active temperature range for the reaction chain
*/
struct ActiveTempRange {
std::optional<double> minT9;
std::optional<double> maxT9;
std::optional<double> minT9; ///< Minimum temperature (in T9) for which the chain is active
std::optional<double> maxT9; ///< Maximum temperature (in T9) for which the chain is active
};
ActiveTempRange m_tempRange;
std::vector<std::string> m_reactionIDs;
reaction::ReactionSet m_reactions;
ActiveTempRange m_tempRange; ///< Active temperature range for the reaction chain
std::vector<std::string> m_reactionIDs; ///< Vector of reaction IDs that comprise the chain
reaction::ReactionSet m_reactions; ///< Set of reactions that comprise the chain
};
/**
* @brief Proton-Proton I Chain Policy
*
* This class implements the Proton-Proton I chain of nuclear reactions. This chain's minimum temperature is
* set to T9=0.001 (or 1e6K). This chain includes reactions:
* - p(p,e+)d
* - d(p,g)he3
* - he3(he3,2p)he4
*/
class ProtonProtonIChainPolicy final: public TemperatureDependentChainPolicy {
public:
ProtonProtonIChainPolicy();
@@ -58,6 +143,17 @@ namespace gridfire::policy {
[[nodiscard]] std::string name() const override;
};
/**
* @brief Proton-Proton II Chain Policy
*
* This class implements the Proton-Proton II chain of nuclear reactions. This chain's minimum temperature is
* set to T9=0.001 (or 1e6K). This chain includes reactions:
* - p(p,e+)d
* - d(p,g)he3
* - he4(he3,g)be7
* - be7(e-,)li7
* - li7(p,a)he4
*/
class ProtonProtonIIChainPolicy final: public TemperatureDependentChainPolicy {
public:
ProtonProtonIIChainPolicy();
@@ -67,6 +163,17 @@ namespace gridfire::policy {
[[nodiscard]] std::string name() const override;
};
/**
* @brief Proton-Proton III Chain Policy
*
* This class implements the Proton-Proton III chain of nuclear reactions. This chain's minimum temperature is
* set to T9=0.001 (or 1e6K). This chain includes reactions:
* - p(p,e+)d
* - d(p,g)he3
* - he4(he3,g)be7
* - be7(p,g)b8
* - b8(,e+ a)he4
*/
class ProtonProtonIIIChainPolicy final: public TemperatureDependentChainPolicy {
public:
ProtonProtonIIIChainPolicy();
@@ -76,6 +183,17 @@ namespace gridfire::policy {
[[nodiscard]] std::string name() const override;
};
/**
* @brief Proton-Proton Chain Policy
*
* This class implements the overall Proton-Proton chain of nuclear reactions, combining the
* Proton-Proton I, II, and III chains. Enforcing this chain in the policy will ensure that all the
* Proton-Proton reactions are included in the network.
*
* @see ProtonProtonIChainPolicy
* @see ProtonProtonIIChainPolicy
* @see ProtonProtonIIIChainPolicy
*/
class ProtonProtonChainPolicy final : public MultiReactionChainPolicy {
public:
ProtonProtonChainPolicy();
@@ -84,6 +202,18 @@ namespace gridfire::policy {
std::vector<std::unique_ptr<ReactionChainPolicy>> m_chain_policies;
};
/**
* @brief CNO I Chain Policy
*
* This class implements the CNO I cycle of nuclear reactions. This chain's minimum temperature is
* set to T9=0.001 (or 1e6K). This chain includes reactions:
* - c12(p,g)n13
* - n13(,e+)c13
* - c13(p,g)n14
* - n14(p,g)o15
* - o15(,e+)n15
* - n15(p,a)c12
*/
class CNOIChainPolicy final : public TemperatureDependentChainPolicy {
public:
CNOIChainPolicy();
@@ -92,6 +222,18 @@ namespace gridfire::policy {
[[nodiscard]] std::string name() const override;
};
/**
* @brief CNO II Chain Policy
*
* This class implements the CNO II cycle of nuclear reactions. This chain's minimum temperature is
* set to T9=0.001 (or 1e6K). This chain includes reactions:
* - n15(p,g)o16
* - o16(p,g)f17
* - f17(p,g)ne18
* - ne18(,e+)f18
* - f18(p,a)o15
* - o15(,e+)n15
*/
class CNOIIChainPolicy final : public TemperatureDependentChainPolicy {
public:
CNOIIChainPolicy();
@@ -100,6 +242,18 @@ namespace gridfire::policy {
[[nodiscard]] std::string name() const override;
};
/**
* @brief CNO III Chain Policy
*
* This class implements the CNO III cycle of nuclear reactions. This chain's minimum temperature is
* set to T9=0.001 (or 1e6K). This chain includes reactions:
* - o17(p,g)f18
* - f18(,e+)o18
* - o18(p,a)n15
* - n15(p,g)o16
* - o16(p,g)f17
* - f17(,e+)o17
*/
class CNOIIIChainPolicy final : public TemperatureDependentChainPolicy {
public:
CNOIIIChainPolicy();
@@ -108,6 +262,18 @@ namespace gridfire::policy {
[[nodiscard]] std::string name() const override;
};
/**
* @brief CNO IV Chain Policy
*
* This class implements the CNO IV cycle of nuclear reactions. This chain's minimum temperature is
* set to T9=0.001 (or 1e6K). This chain includes reactions:
* - o18(p,g)f19
* - f19(p,a)o16
* - o16(p,g)f17
* - f17(,e+)o17
* - o17(p,g)f18
* - f18(,e+)o18
*/
class CNOIVChainPolicy final : public TemperatureDependentChainPolicy {
public:
CNOIVChainPolicy();
@@ -116,12 +282,36 @@ namespace gridfire::policy {
[[nodiscard]] std::string name() const override;
};
/**
* @brief CNO Chain Policy
*
* This class implements the overall CNO cycle of nuclear reactions, combining the
* CNO I, II, III, and IV chains. Enforcing this chain in the policy will ensure that all the
* CNO reactions are included in the network.
*
* @see CNOIChainPolicy
* @see CNOIIChainPolicy
* @see CNOIIIChainPolicy
* @see CNOIVChainPolicy
*/
class CNOChainPolicy final : public MultiReactionChainPolicy {
public:
CNOChainPolicy();
[[nodiscard]] std::string name() const override;
};
/**
* @brief Hot CNO I Chain Policy
*
* This class implements the Hot CNO I cycle of nuclear reactions. This chain's minimum temperature is
* set to T9=0.1 (or 1e8K). This chain includes reactions:
* - c12(p,g)n13
* - n13(p,g)o14
* - o14(,e+)n14
* - n14(p,g)o15
* - o15(,e+)n15
* - n15(p,a)c12
*/
class HotCNOIChainPolicy final : public TemperatureDependentChainPolicy {
public:
HotCNOIChainPolicy();
@@ -129,6 +319,18 @@ namespace gridfire::policy {
[[nodiscard]] std::string name() const override;
};
/**
* @brief Hot CNO II Chain Policy
*
* This class implements the Hot CNO II cycle of nuclear reactions. This chain's minimum temperature is
* set to T9=0.1 (or 1e8K). This chain includes reactions:
* - n15(p,g)o16
* - o16(p,g)f17
* - f17(p,g)ne18
* - ne18(,e+)f18
* - f18(p,a)o15
* - o15(,e+)n15
*/
class HotCNOIIChainPolicy final : public TemperatureDependentChainPolicy {
public:
HotCNOIIChainPolicy();
@@ -136,6 +338,18 @@ namespace gridfire::policy {
[[nodiscard]] std::string name() const override;
};
/**
* @brief Hot CNO III Chain Policy
*
* This class implements the Hot CNO III cycle of nuclear reactions. This chain's minimum temperature is
* set to T9=0.1 (or 1e8K). This chain includes reactions:
* - f18(p,g)ne19
* - ne19(,e+)f19
* - f19(p,a)o16
* - o16(p,g)f17
* - f17(p,g)ne18
* - ne18(,e+)f18
*/
class HotCNOIIIChainPolicy final : public TemperatureDependentChainPolicy {
public:
HotCNOIIIChainPolicy();
@@ -143,12 +357,31 @@ namespace gridfire::policy {
[[nodiscard]] std::string name() const override;
};
/**
* @brief Hot CNO Chain Policy
*
* This class implements the overall Hot CNO cycle of nuclear reactions, combining the
* Hot CNO I, II, and III chains. Enforcing this chain in the policy will ensure that all the
* Hot CNO reactions are included in the network.
*
* @see HotCNOIChainPolicy
* @see HotCNOIIChainPolicy
* @see HotCNOIIIChainPolicy
*/
class HotCNOChainPolicy final : public MultiReactionChainPolicy {
public:
HotCNOChainPolicy();
[[nodiscard]] std::string name() const override;
};
/**
* @brief Triple-Alpha Chain Policy
*
* This class implements the Triple-Alpha process of nuclear reactions. This chain's minimum temperature is
* set to T9=0.01 (or 1e7K). This chain includes reactions:
* - he4(he4,a)be8
* - be8(he4,g)c12
*/
class TripleAlphaChainPolicy final : public TemperatureDependentChainPolicy {
public:
TripleAlphaChainPolicy();
@@ -158,7 +391,16 @@ namespace gridfire::policy {
[[nodiscard]] std::string name() const override;
};
/**
* @brief Main Sequence Reaction Chain Policy
*
* This class implements the main sequence reaction chains, combining the
* Proton-Proton chain and the CNO cycle. Enforcing this chain in the policy will ensure that all the
* primary reactions for main sequence stars are included in the network.
*
* @see ProtonProtonChainPolicy
* @see CNOChainPolicy
*/
class MainSequenceReactionChainPolicy final : public MultiReactionChainPolicy {
public:
MainSequenceReactionChainPolicy();

View File

@@ -0,0 +1,6 @@
#pragma once
#include "gridfire/policy/policy_abstract.h"
#include "gridfire/policy/policy_logical.h"
#include "gridfire/policy/chains.h"
#include "gridfire/policy/stellar_policy.h"

View File

@@ -11,12 +11,6 @@
* example:
* - gridfire/policy/stellar_policy.h (concrete stellar network policy used in many examples)
* - gridfire/policy/chains.h (reaction-chain helper policies such as proton-proton, CNO)
*
* An example of using a concrete policy to construct and run an engine is available at:
* - tests/graphnet_sandbox/main.cpp
*
* @note Doxygen comments on public methods include @par Example usage blocks. Methods that may
* throw in concrete implementations include @throws tags.
*/
#pragma once
@@ -145,7 +139,7 @@ namespace gridfire::policy {
* NetOut out = solver.evaluate(netIn, true);
* @endcode
*/
[[nodiscard]] virtual DynamicEngine& construct() = 0;
[[nodiscard]] virtual engine::DynamicEngine& construct() = 0;
/**
* @brief Returns the current verification/construction status of the policy.
@@ -161,9 +155,9 @@ namespace gridfire::policy {
*/
[[nodiscard]] virtual NetworkPolicyStatus getStatus() const = 0;
[[nodiscard]] virtual const std::vector<std::unique_ptr<DynamicEngine>> &get_engine_stack() const = 0;
[[nodiscard]] virtual const std::vector<std::unique_ptr<engine::DynamicEngine>> &get_engine_stack() const = 0;
[[nodiscard]] virtual std::vector<EngineTypes> get_engine_types_stack() const = 0;
[[nodiscard]] virtual std::vector<engine::EngineTypes> get_engine_types_stack() const = 0;
[[nodiscard]] virtual const std::unique_ptr<partition::PartitionFunction>& get_partition_function() const = 0;
};

View File

@@ -22,30 +22,211 @@ namespace gridfire::policy {
*/
class MultiReactionChainPolicy : public ReactionChainPolicy {
public:
/**
* @brief Constructs a MultiReactionChainPolicy from a vector of ReactionChainPolicy instances.
*
* The provided chain policies are moved into the new MultiReactionChainPolicy instance.
*
* @param chain_policies vector of unique_ptr to ReactionChainPolicy instances.
*
* @par Example
* @code
* std::vector<std::unique_ptr<ReactionChainPolicy>> chains;
* chains.push_back(std::make_unique<ProtonProtonChainPolicy>());
* chains.push_back(std::make_unique<CNOChainPolicy>());
* MultiReactionChainPolicy multi(std::move(chains));
* @endcode
*/
explicit MultiReactionChainPolicy(std::vector<std::unique_ptr<ReactionChainPolicy>>&& chain_policies);
/**
* @brief Returns the vector of child ReactionChainPolicy instances.
*
* @return const std::vector<std::unique_ptr<ReactionChainPolicy>>& reference to the child chain policies.
*
* @par Example
* @code
* const auto &chains = multi.get_chain_policies();
* for (const auto &ch : chains) { std::cout << ch->get_reactions().size() << " reactions\n"; }
* @endcode
*/
[[nodiscard]] const std::vector<std::unique_ptr<ReactionChainPolicy>>& get_chain_policies() const;
/**
* @brief Returns the combined ReactionSet of all child chain policies.
*
* @return const reaction::ReactionSet& reference to the combined reactions.
*
* @par Example
* @code
* const auto &reactions = multi.get_reactions();
* std::cout << "Multi chain contains " << reactions.size() << " reactions\n";
* @endcode
*/
[[nodiscard]] const reaction::ReactionSet& get_reactions() const override;
/**
* @brief Checks if the MultiReactionChainPolicy contains a reaction by ID
*
* @param id the reaction ID to check for.
* @return true if the reaction ID is present in the combined ReactionSet, false otherwise.
*
* @par Example
* @code
* bool has_pp = multi.contains("p(p,e+)d");
* @endcode
*/
[[nodiscard]] bool contains(const std::string &id) const override;
/**
* @brief Checks if the MultiReactionChainPolicy contains a specific reaction.
*
* @param reaction the Reaction to check for.
* @return true if the reaction is present in the combined ReactionSet, false otherwise.
*
* @par Example
* @code
* reaction::Reaction r = ...; // obtain a Reaction instance
* bool has_reaction = multi.contains(r);
* @endcode
*/
[[nodiscard]] bool contains(const reaction::Reaction &reaction) const override;
/**
* @brief Creates a deep copy of the MultiReactionChainPolicy.
*
* @return std::unique_ptr<ReactionChainPolicy> unique pointer to the cloned instance.
*
* @par Example
* @code
* std::unique_ptr<ReactionChainPolicy> clone = multi.clone();
* @endcode
*/
[[nodiscard]] std::unique_ptr<ReactionChainPolicy> clone() const override;
/**
* @brief Returns the name of the MultiReactionChainPolicy.
*
* @return std::string the name of the policy.
*
* @par Example
* @code
* std::string n = multi.name();
* std::cout << "Using policy: " << n << std::endl;
* @endcode
*/
[[nodiscard]] std::string name() const override;
/**
* @brief Computes a hash value for the MultiReactionChainPolicy.
*
* @param seed the seed value for the hash computation.
* @return uint64_t the computed hash value.
*
* @par Example
* @code
* uint64_t h = multi.hash(0);
* std::cout << "Policy hash: " << h << std::endl;
* @endcode
*/
[[nodiscard]] uint64_t hash(uint64_t seed) const override;
/**
* @brief Equality comparison operator.
*
* @param other the other ReactionChainPolicy to compare against.
* @return true if the policies are equal, false otherwise.
*
* @par Example
* @code
* bool equal = (multi1 == multi2);
* @endcode
*/
[[nodiscard]] bool operator==(const ReactionChainPolicy& other) const override;
/**
* @brief Inequality comparison operator.
*
* @param other the other ReactionChainPolicy to compare against.
* @return true if the policies are not equal, false otherwise.
*
* @par Example
* @code
* bool not_equal = (multi1 != multi2);
* @endcode
*/
[[nodiscard]] bool operator!=(const ReactionChainPolicy& other) const override;
/**
* @brief Returns the number of child ReactionChainPolicy instances.
*
* @return size_t the number of child chain policies.
*
* @par Example
* @code
* size_t n = multi.size();
* std::cout << "Multi chain has " << n << " child policies\n";
* @endcode
*/
[[nodiscard]] size_t size() const;
/**
* @brief Returns iterator to the beginning of the child ReactionChainPolicy instances.
*
* @return iterator to the beginning.
*
* @par Example
* @code
* for (auto it = multi.begin(); it != multi.end(); ++it) {
* std::cout << (*it)->name() << std::endl;
* }
* @endcode
*/
auto begin() { return m_chain_policies.begin(); }
/**
* @brief Returns const iterator to the beginning of the child ReactionChainPolicy instances.
*
* @return const iterator to the beginning.
*
* @par Example
* @code
* for (auto it = multi.begin(); it != multi.end(); ++it) {
* std::cout << (*it)->name() << std::endl;
* }
* @endcode
*/
[[nodiscard]] auto begin() const { return m_chain_policies.cbegin(); }
/**
* @brief Returns iterator to the end of the child ReactionChainPolicy instances.
*
* @return iterator to the end.
*
* @par Example
* @code
* for (auto it = multi.begin(); it != multi.end(); ++it) {
* std::cout << (*it)->name() << std::endl;
* }
* @endcode
*/
auto end() { return m_chain_policies.end(); }
/**
* @brief Returns const iterator to the end of the child ReactionChainPolicy instances.
*
* @return const iterator to the end.
*
* @par Example
* @code
* for (auto it = multi.begin(); it != multi.end(); ++it) {
* std::cout << (*it)->name() << std::endl;
* }
* @endcode
*/
[[nodiscard]] auto end() const { return m_chain_policies.cend(); }
protected:
std::vector<std::unique_ptr<ReactionChainPolicy>> m_chain_policies{};
reaction::ReactionSet m_reactions;
std::vector<std::unique_ptr<ReactionChainPolicy>> m_chain_policies{}; ///< Child chain policies
reaction::ReactionSet m_reactions; ///< Combined reactions from all child policies
};
}

View File

@@ -135,7 +135,7 @@ namespace gridfire::policy {
* // ... run solver ...
* @endcode
*/
DynamicEngine& construct() override;
engine::DynamicEngine& construct() override;
/**
* @brief Gets the current status of the policy.
@@ -143,20 +143,20 @@ namespace gridfire::policy {
*/
[[nodiscard]] NetworkPolicyStatus getStatus() const override;
[[nodiscard]] const std::vector<std::unique_ptr<DynamicEngine>> &get_engine_stack() const override;
[[nodiscard]] const std::vector<std::unique_ptr<engine::DynamicEngine>> &get_engine_stack() const override;
[[nodiscard]] std::vector<EngineTypes> get_engine_types_stack() const override;
[[nodiscard]] std::vector<engine::EngineTypes> get_engine_types_stack() const override;
[[nodiscard]] const std::unique_ptr<partition::PartitionFunction>& get_partition_function() const override;
private:
std::set<fourdst::atomic::Species> m_seed_species;
std::set<fourdst::atomic::Species> m_seed_species; ///< The set of seed species required by this policy. These are H-1, He-3, He-4, C-12, N-14, O-16, Ne-20, Mg-24.
std::unique_ptr<ReactionChainPolicy> m_reaction_policy = std::make_unique<MainSequenceReactionChainPolicy>();
fourdst::composition::Composition m_initializing_composition;
std::unique_ptr<partition::PartitionFunction> m_partition_function;
std::vector<std::unique_ptr<DynamicEngine>> m_network_stack;
std::unique_ptr<ReactionChainPolicy> m_reaction_policy = std::make_unique<MainSequenceReactionChainPolicy>(); ///< The composed reaction chain policy (PP-chain + CNO-cycle).
fourdst::composition::Composition m_initializing_composition; ///< The initial composition used to build the network.
std::unique_ptr<partition::PartitionFunction> m_partition_function; ///< The partition function used in network construction.
std::vector<std::unique_ptr<engine::DynamicEngine>> m_network_stack; ///< The stack of dynamic engines constructed by the policy.
NetworkPolicyStatus m_status = NetworkPolicyStatus::UNINITIALIZED;
NetworkPolicyStatus m_status = NetworkPolicyStatus::UNINITIALIZED; ///< The current status of the policy.
private:
static std::unique_ptr<partition::PartitionFunction> build_partition_function();
[[nodiscard]] NetworkPolicyStatus check_status() const;

View File

@@ -569,7 +569,6 @@ namespace gridfire::rates::weak {
log_rhoYe
);
// 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();

View File

@@ -2,6 +2,7 @@
#include "gridfire/screening/screening_abstract.h"
#include "gridfire/reaction/reaction.h"
#include "gridfire/types/types.h"
#include "fourdst/logging/logging.h"
#include "quill/Logger.h"
@@ -96,7 +97,7 @@ namespace gridfire::screening {
* @param rho The density in g/cm^3.
* @return A vector of screening factors of type `T`.
*/
template <typename T>
template <IsArithmeticOrAD T>
[[nodiscard]] std::vector<T> calculateFactors_impl(
const reaction::ReactionSet& reactions,
const std::vector<fourdst::atomic::Species>& species,
@@ -137,7 +138,7 @@ namespace gridfire::screening {
* and unphysical screening factors (exp(2) ≈ 7.4).
* 6. **Final Factor**: The screening factor for the reaction is `exp(H_12)`.
*/
template <typename T>
template <IsArithmeticOrAD T>
std::vector<T> WeakScreeningModel::calculateFactors_impl(
const reaction::ReactionSet& reactions,
const std::vector<fourdst::atomic::Species>& species,

View File

@@ -1,99 +1,3 @@
#pragma once
#include "gridfire/engine/engine_abstract.h"
#include "gridfire/network.h"
#include <functional>
#include <any>
#include <vector>
#include <tuple>
#include <string>
namespace gridfire::solver {
/**
* @struct SolverContextBase
* @brief Base class for solver callback contexts.
*
* This struct serves as a base class for contexts that can be passed to solver callbacks, it enforces
* that derived classes implement a `describe` method that returns a vector of tuples describing
* the context that a callback will receive when called.
*/
class SolverContextBase {
public:
virtual ~SolverContextBase() = default;
/**
* @brief Describe the context for callback functions.
* @return A vector of tuples, each containing a string for the parameters name and a string for its type.
*
* This method should be overridden by derived classes to provide a description of the context
* that will be passed to the callback function. The intent of this method is that an end user can investigate
* the context that will be passed to the callback function, and use this information to craft their own
* callback function.
*/
[[nodiscard]] virtual std::vector<std::tuple<std::string, std::string>> describe() const = 0;
};
/**
* @class NetworkSolverStrategy
* @brief Abstract base class for network solver strategies.
*
* This class defines the interface for network solver strategies, which are responsible
* for integrating the reaction network ODEs over a given timestep. It is templated on the
* engine type to allow for different engine implementations to be used with the same solver.
*
* @tparam EngineT The type of engine to use with this solver strategy. Must inherit from Engine.
*/
template <typename EngineT>
class NetworkSolverStrategy {
public:
/**
* @brief Constructor for the NetworkSolverStrategy.
* @param engine The engine to use for evaluating the network.
*/
explicit NetworkSolverStrategy(EngineT& engine) : m_engine(engine) {};
/**
* @brief Virtual destructor.
*/
virtual ~NetworkSolverStrategy() = default;
/**
* @brief Evaluates the network for a given timestep.
* @param netIn The input conditions for the network.
* @return The output conditions after the timestep.
*/
virtual NetOut evaluate(const NetIn& netIn) = 0;
/**
* @brief set the callback function to be called at the end of each timestep.
*
* This function allows the user to set a callback function that will be called at the end of each timestep.
* The callback function will receive a gridfire::solver::<SOMESOLVER>::TimestepContext object. Note that
* depending on the solver, this context may contain different information. Further, the exact
* signature of the callback function is left up to each solver. Every solver should provide a type or type alias
* TimestepCallback that defines the signature of the callback function so that the user can easily
* get that type information.
*
* @param callback The callback function to be called at the end of each timestep.
*/
virtual void set_callback(const std::any& callback) = 0;
/**
* @brief Describe the context that will be passed to the callback function.
* @return A vector of tuples, each containing a string for the parameter's name and a string for its type.
*
* This method should be overridden by derived classes to provide a description of the context
* that will be passed to the callback function. The intent of this method is that an end user can investigate
* the context that will be passed to the callback function, and use this information to craft their own
* callback function.
*/
[[nodiscard]] virtual std::vector<std::tuple<std::string, std::string>> describe_callback_context() const = 0;
protected:
EngineT& m_engine; ///< The engine used by this solver strategy.
};
/**
* @brief Type alias for a network solver strategy that uses a DynamicEngine.
*/
using DynamicNetworkSolverStrategy = NetworkSolverStrategy<DynamicEngine>;
}
#include "gridfire/solver/strategies/strategies.h"

View File

@@ -1,8 +1,8 @@
#pragma once
#include "gridfire/solver/solver.h"
#include "gridfire/solver/strategies/strategy_abstract.h"
#include "gridfire/engine/engine_abstract.h"
#include "gridfire/network.h"
#include "gridfire/types/types.h"
#include "gridfire/exceptions/exceptions.h"
#include "fourdst/atomic/atomicSpecies.h"
@@ -85,7 +85,7 @@ namespace gridfire::solver {
* @param engine DynamicEngine used for RHS/Jacobian evaluation and network access.
* @throws std::runtime_error If SUNContext_Create fails.
*/
explicit CVODESolverStrategy(DynamicEngine& engine);
explicit CVODESolverStrategy(engine::DynamicEngine& engine);
/**
* @brief Destructor: cleans CVODE/SUNDIALS resources and frees SUNContext.
*/
@@ -152,6 +152,12 @@ namespace gridfire::solver {
*/
void set_stdout_logging_enabled(bool logging_enabled);
void set_absTol(double absTol);
void set_relTol(double relTol);
double get_absTol() const;
double get_relTol() const;
/**
* @brief Schema of fields exposed to the timestep callback context.
*/
@@ -173,7 +179,7 @@ namespace gridfire::solver {
const double T9; ///< Temperature in GK.
const double rho; ///< Density [g cm^-3].
const size_t num_steps; ///< Number of CVODE steps taken so far.
const DynamicEngine& engine; ///< Reference to the engine.
const engine::DynamicEngine& engine; ///< Reference to the engine.
const std::vector<fourdst::atomic::Species>& networkSpecies; ///< Species layout.
const size_t currentConvergenceFailures; ///< Total number of convergence failures
const size_t currentNonlinearIterations; ///< Total number of non-linear iterations
@@ -190,7 +196,7 @@ namespace gridfire::solver {
double t9,
double rho,
size_t num_steps,
const DynamicEngine& engine,
const engine::DynamicEngine& engine,
const std::vector<fourdst::atomic::Species>& networkSpecies,
size_t currentConvergenceFailure,
size_t currentNonlinearIterations,
@@ -219,12 +225,12 @@ namespace gridfire::solver {
*/
struct CVODEUserData {
CVODESolverStrategy* solver_instance{}; // Pointer back to the class instance
DynamicEngine* engine{};
engine::DynamicEngine* engine{};
double T9{};
double rho{};
double energy{};
const std::vector<fourdst::atomic::Species>* networkSpecies{};
std::unique_ptr<exceptions::StaleEngineTrigger> captured_exception = nullptr;
std::unique_ptr<exceptions::EngineError> captured_exception = nullptr;
std::optional<std::map<fourdst::atomic::Species, std::unordered_map<std::string, double>>> reaction_contribution_map;
};
@@ -281,6 +287,9 @@ namespace gridfire::solver {
*/
void cleanup_cvode_resources(bool memFree);
void set_detailed_step_logging(bool enabled);
/**
* @brief Compute and print per-component error ratios; run diagnostic helpers.
*
@@ -288,8 +297,8 @@ namespace gridfire::solver {
* sorted table of species with the highest error ratios; then invokes diagnostic routines to
* inspect Jacobian stiffness and species balance.
*/
void log_step_diagnostics(const CVODEUserData& user_data, bool displayJacobianStiffness, bool saveIntermediateJacobians, bool
displaySpeciesBalance) const;
void log_step_diagnostics(const CVODEUserData& user_data, bool displayJacobianStiffness, bool
displaySpeciesBalance, bool to_file, std::optional<std::string> filename) const;
private:
SUNContext m_sun_ctx = nullptr; ///< SUNDIALS context (lifetime of the solver).
void* m_cvode_mem = nullptr; ///< CVODE memory block.
@@ -305,5 +314,10 @@ namespace gridfire::solver {
bool m_stdout_logging_enabled = true; ///< If true, print per-step logs and use CV_ONE_STEP.
N_Vector m_constraints = nullptr; ///< CVODE constraints vector (>= 0 for species entries).
std::optional<double> m_absTol; ///< User-specified absolute tolerance.
std::optional<double> m_relTol; ///< User-specified relative tolerance.
bool m_detailed_step_logging = false; ///< If true, log detailed step diagnostics (error ratios, Jacobian, species balance).
};
}

View File

@@ -0,0 +1,5 @@
#pragma once
#include "gridfire/solver/strategies/triggers/triggers.h"
#include "gridfire/solver/strategies/strategy_abstract.h"
#include "gridfire/solver/strategies/CVODE_solver_strategy.h"

View File

@@ -0,0 +1,99 @@
#pragma once
#include "gridfire/engine/engine_abstract.h"
#include "gridfire/types/types.h"
#include <functional>
#include <any>
#include <vector>
#include <tuple>
#include <string>
namespace gridfire::solver {
/**
* @struct SolverContextBase
* @brief Base class for solver callback contexts.
*
* This struct serves as a base class for contexts that can be passed to solver callbacks, it enforces
* that derived classes implement a `describe` method that returns a vector of tuples describing
* the context that a callback will receive when called.
*/
class SolverContextBase {
public:
virtual ~SolverContextBase() = default;
/**
* @brief Describe the context for callback functions.
* @return A vector of tuples, each containing a string for the parameters name and a string for its type.
*
* This method should be overridden by derived classes to provide a description of the context
* that will be passed to the callback function. The intent of this method is that an end user can investigate
* the context that will be passed to the callback function, and use this information to craft their own
* callback function.
*/
[[nodiscard]] virtual std::vector<std::tuple<std::string, std::string>> describe() const = 0;
};
/**
* @class NetworkSolverStrategy
* @brief Abstract base class for network solver strategies.
*
* This class defines the interface for network solver strategies, which are responsible
* for integrating the reaction network ODEs over a given timestep. It is templated on the
* engine type to allow for different engine implementations to be used with the same solver.
*
* @tparam EngineT The type of engine to use with this solver strategy. Must inherit from Engine.
*/
template <typename EngineT>
class NetworkSolverStrategy {
public:
/**
* @brief Constructor for the NetworkSolverStrategy.
* @param engine The engine to use for evaluating the network.
*/
explicit NetworkSolverStrategy(EngineT& engine) : m_engine(engine) {};
/**
* @brief Virtual destructor.
*/
virtual ~NetworkSolverStrategy() = default;
/**
* @brief Evaluates the network for a given timestep.
* @param netIn The input conditions for the network.
* @return The output conditions after the timestep.
*/
virtual NetOut evaluate(const NetIn& netIn) = 0;
/**
* @brief set the callback function to be called at the end of each timestep.
*
* This function allows the user to set a callback function that will be called at the end of each timestep.
* The callback function will receive a gridfire::solver::<SOMESOLVER>::TimestepContext object. Note that
* depending on the solver, this context may contain different information. Further, the exact
* signature of the callback function is left up to each solver. Every solver should provide a type or type alias
* TimestepCallback that defines the signature of the callback function so that the user can easily
* get that type information.
*
* @param callback The callback function to be called at the end of each timestep.
*/
virtual void set_callback(const std::any& callback) = 0;
/**
* @brief Describe the context that will be passed to the callback function.
* @return A vector of tuples, each containing a string for the parameter's name and a string for its type.
*
* This method should be overridden by derived classes to provide a description of the context
* that will be passed to the callback function. The intent of this method is that an end user can investigate
* the context that will be passed to the callback function, and use this information to craft their own
* callback function.
*/
[[nodiscard]] virtual std::vector<std::tuple<std::string, std::string>> describe_callback_context() const = 0;
protected:
EngineT& m_engine; ///< The engine used by this solver strategy.
};
/**
* @brief Type alias for a network solver strategy that uses a DynamicEngine.
*/
using DynamicNetworkSolverStrategy = NetworkSolverStrategy<engine::DynamicEngine>;
}

View File

@@ -331,8 +331,8 @@ namespace gridfire::trigger::solver::CVODE {
*
* @param simulationTimeInterval Interval used by SimulationTimeTrigger (> 0).
* @param offDiagonalThreshold Off-diagonal Jacobian magnitude threshold (>= 0).
* @param relativeTimestepCollapseThreshold Threshold for timestep deviation (>= 0, and <= 1 when relative).
* @param timestepGrowthWindowSize Window size for timestep averaging (>= 1 recommended).
* @param timestepCollapseRatio Threshold for timestep deviation (>= 0, and <= 1 when relative).
* @param maxConvergenceFailures Window size for timestep averaging (>= 1 recommended).
* @return A unique_ptr to a composed Trigger<TimestepContext> implementing the policy above.
*
* @note The exact policy is subject to change; this function centralizes that decision.
@@ -340,7 +340,7 @@ namespace gridfire::trigger::solver::CVODE {
std::unique_ptr<Trigger<gridfire::solver::CVODESolverStrategy::TimestepContext>> makeEnginePartitioningTrigger(
const double simulationTimeInterval,
const double offDiagonalThreshold,
const double relativeTimestepCollapseThreshold,
const size_t timestepGrowthWindowSize
const double timestepCollapseRatio,
const size_t maxConvergenceFailures
);
}

View File

@@ -0,0 +1,3 @@
#pragma once
#include "gridfire/solver/strategies/triggers/engine_partitioning_trigger.h"

View File

@@ -0,0 +1,3 @@
#pragma once
#include "gridfire/trigger/procedures/trigger_pprint.h"

View File

@@ -0,0 +1,6 @@
#pragma once
#include "gridfire/trigger/trigger_abstract.h"
#include "gridfire/trigger/trigger_logical.h"
#include "gridfire/trigger/trigger_result.h"
#include "gridfire/trigger/procedures/trigger_procedures.h"

View File

@@ -20,12 +20,8 @@
// *********************************************************************** */
#pragma once
#include <vector>
#include "fourdst/composition/composition.h"
#include <unordered_map>
#include "cppad/cppad.hpp"
namespace gridfire {
struct NetIn {
@@ -51,4 +47,13 @@ namespace gridfire {
}
};
/**
* @brief Concept for types allowed in engine calculations.
*
* This concept restricts template parameters to either double or CppAD::AD<double>,
* enabling both standard and automatic differentiation types.
*/
template<typename T>
concept IsArithmeticOrAD = std::is_same_v<T, double> || std::is_same_v<T, CppAD::AD<double>>;
} // namespace nuclearNetwork

View File

@@ -0,0 +1,3 @@
#pragma once
#include "gridfire/utils/formatters/jacobian_format.h"

View File

@@ -0,0 +1,24 @@
#pragma once
#include <format>
#include "gridfire/engine/types/jacobian.h"
template <>
struct std::formatter<gridfire::engine::NetworkJacobian> {
static constexpr auto parse(const std::format_parse_context& ctx) {
return ctx.begin();
}
static auto format(const gridfire::engine::NetworkJacobian& obj, std::format_context& ctx) {
return std::format_to(ctx.out(), "NetworkJacobian(shape=({}, {}), nnz={}, rank={}, singular={})",
std::get<0>(obj.shape()),
std::get<1>(obj.shape()),
obj.nnz(),
obj.rank(),
obj.singular() ? "True" : "False"
);
}
};

View File

@@ -4,6 +4,7 @@
#include "fourdst/composition/composition.h"
#include <string>
#include <functional>
namespace gridfire::utils {
/**
@@ -57,11 +58,46 @@ namespace gridfire::utils {
* @endcode
*/
std::string formatNuclearTimescaleLogString(
const DynamicEngine& engine,
const engine::DynamicEngine& engine,
const fourdst::composition::Composition& composition,
double T9,
double rho
);
template <typename T>
concept Streamable = requires(std::ostream& os, const T& value) {
{ os << value } -> std::same_as<std::ostream&>;
};
template <
std::ranges::input_range Container,
typename Elem = std::ranges::range_reference_t<Container>,
typename Transform = std::identity,
typename Pred = bool(*)(const std::ranges::range_value_t<Container>&)
>
requires std::invocable<Transform, Elem> && Streamable<std::invoke_result_t<Transform, Elem>> && std::predicate<Pred, Elem>
static std::string iterable_to_delimited_string(
const Container& container,
const std::string_view delimiter = ", ",
Transform transform = {},
Pred pred = [](const auto&){ return true; }
) noexcept {
std::ostringstream oss;
bool first = true;
for (auto&& item : container) {
if (!std::invoke(pred, item)) {
continue;
}
if (!first) {
oss << delimiter;
}
oss << std::invoke(transform, item);
first = false;
}
return oss.str();
}
}

View File

@@ -6,7 +6,11 @@
#include "sundials/sundials_nvector.h"
namespace gridfire::utils {
inline std::unordered_map<int, std::string> cvode_ret_code_map {
enum class SUNDIALS_RET_CODE_TYPES {
CVODE,
KINSOL
};
static inline std::unordered_map<int, std::string> cvode_ret_code_map {
{0, "CV_SUCCESS: The solver succeeded."},
{1, "CV_TSTOP_RETURN: The solver reached the specified stopping time."},
{2, "CV_ROOT_RETURN: A root was found."},
@@ -40,6 +44,35 @@ namespace gridfire::utils {
{-30, "CV_PROJFUNC_FAIL: The projection function failed in an unrecoverable manner."},
{-31, "CV_REPTD_PROJFUNC_ERR: The projection function has repeated recoverable errors."}
};
static inline std::unordered_map<int, std::string> kinsol_ret_code_map {
{0, "KIN_SUCCESS: The solver succeeded."},
{1, "KIN_STEP_LT_STPTOL: The solver step size became less than the stopping tolerance."},
{2, "KIN_RES_REPTD_ERR: The residual function repeatedly failed recoverably."},
{-1, "KIN_MEM_NULL: The KINSOL memory structure is NULL."},
{-2, "KIN_ILL_INPUT: An illegal input was detected."},
{-3, "KIN_NO_MALLOC: The KINSOL memory structure has not been allocated."},
{-4, "KIN_MEM_FAIL: Memory allocation failed."},
{-5, "KIN_LINIT_FAIL: The linear solver's initialization function failed."},
{-6, "KIN_LSETUP_FAIL: The linear solver's setup function failed."},
{-7, "KIN_LSOLVE_FAIL: The linear solver's solve function failed."},
{-8, "KIN_RESFUNC_FAIL: The residual function failed in an unrecoverable manner."},
{-9, "KIN_CONSTR_FAIL: The inequality constraint was violated and the solver was unable to recover."},
{-10, "KIN_NLS_INIT_FAIL: The nonlinear solver's initialization function failed."},
{-11, "KIN_NLS_SETUP_FAIL: The nonlinear solver's setup function failed."},
{-12, "KIN_NLS_FAIL: The nonlinear solver's solve function failed."}
};
inline const std::unordered_map<int, std::string>& sundials_retcode_map(const SUNDIALS_RET_CODE_TYPES type) {
switch (type) {
case SUNDIALS_RET_CODE_TYPES::CVODE:
return cvode_ret_code_map;
case SUNDIALS_RET_CODE_TYPES::KINSOL:
return kinsol_ret_code_map;
default:
throw exceptions::CVODESolverFailureError("Unknown SUNDIALS return code type.");
}
}
inline void check_cvode_flag(const int flag, const std::string& func_name) {
if (flag < 0) {

View File

@@ -1,6 +1,5 @@
#pragma once
#include <iostream>
#include <utility>
#include <vector>
#include <string>
@@ -9,11 +8,10 @@
#include <algorithm>
#include <numeric>
#include <memory>
#include <format>
#include <print>
#include <cstdlib>
#include <cwchar>
#include <clocale>
#include "nlohmann/json.hpp"
@@ -23,15 +21,15 @@ namespace gridfire::utils {
// for mbtowc and wcwidth to function correctly with the system's locale.
size_t width = 0;
std::mbtowc(nullptr, 0, 0); // Reset multi-byte state
std::mbtowc(nullptr, nullptr, 0); // Reset multi-byte state
const char* p = s.c_str();
const char* end = s.c_str() + s.length();
while (p < end) {
wchar_t wc;
// Convert the next multi-byte char to a wide char
int byte_len = std::mbtowc(&wc, p, end - p);
// Convert the next multibyte char to a wide char
const int byte_len = std::mbtowc(&wc, p, end - p);
if (byte_len <= 0) {
// Invalid byte sequence or null char.
@@ -387,4 +385,33 @@ namespace gridfire::utils {
}
inline nlohmann::json to_json(const std::vector<std::unique_ptr<ColumnBase>>& columns) {
using json = nlohmann::json;
json j;
for (const auto& col : columns) {
std::vector<std::string> col_data;
const size_t row_count = col->getRowCount();
for (size_t i = 0; i < row_count; ++i) {
col_data.push_back(col->getCellData(i));
}
j[col->getHeader()] = col_data;
}
return j;
}
inline void to_json_file(const std::string& filename, const std::vector<std::vector<std::unique_ptr<ColumnBase>>> &tables, const std::vector<std::string>& tableNames) {
using json = nlohmann::json;
json j;
for (size_t t = 0; t < tables.size(); ++t) {
j[tableNames[t]] = to_json(tables[t]);
}
std::ofstream output(filename);
if (!output.is_open()) {
throw std::runtime_error("Failed to open file for writing: " + filename);
}
output << j.dump(4);
output.close();
}
}

View File

@@ -0,0 +1,7 @@
#pragma once
#include "gridfire/utils/formatters/formatters.h"
#include "gridfire/utils/hashing.h"
#include "gridfire/utils/logging.h"
#include "gridfire/utils/sundials.h"
#include "gridfire/utils/table_format.h"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
#include "gridfire/exceptions/error_engine.h"
#include "gridfire/engine/procedures/priming.h"
#include "gridfire/utils/sundials.h"
#include "gridfire/utils/logging.h"
#include <stdexcept>
#include <vector>
@@ -149,21 +150,6 @@ namespace {
return reactantSample != productSample;
}
const std::unordered_map<Eigen::LevenbergMarquardtSpace::Status, std::string> lm_status_map = {
{Eigen::LevenbergMarquardtSpace::Status::NotStarted, "NotStarted"},
{Eigen::LevenbergMarquardtSpace::Status::Running, "Running"},
{Eigen::LevenbergMarquardtSpace::Status::ImproperInputParameters, "ImproperInputParameters"},
{Eigen::LevenbergMarquardtSpace::Status::RelativeReductionTooSmall, "RelativeReductionTooSmall"},
{Eigen::LevenbergMarquardtSpace::Status::RelativeErrorTooSmall, "RelativeErrorTooSmall"},
{Eigen::LevenbergMarquardtSpace::Status::RelativeErrorAndReductionTooSmall, "RelativeErrorAndReductionTooSmall"},
{Eigen::LevenbergMarquardtSpace::Status::CosinusTooSmall, "CosinusTooSmall"},
{Eigen::LevenbergMarquardtSpace::Status::TooManyFunctionEvaluation, "TooManyFunctionEvaluation"},
{Eigen::LevenbergMarquardtSpace::Status::FtolTooSmall, "FtolTooSmall"},
{Eigen::LevenbergMarquardtSpace::Status::XtolTooSmall, "XtolTooSmall"},
{Eigen::LevenbergMarquardtSpace::Status::GtolTooSmall, "GtolTooSmall"},
{Eigen::LevenbergMarquardtSpace::Status::UserAsked, "UserAsked"}
};
void QuietErrorRouter(int line, const char *func, const char *file, const char *msg,
SUNErrCode err_code, void *err_user_data, SUNContext sunctx) {
@@ -177,7 +163,7 @@ namespace {
}
}
namespace gridfire {
namespace gridfire::engine {
using fourdst::atomic::Species;
MultiscalePartitioningEngineView::MultiscalePartitioningEngineView(
@@ -204,7 +190,7 @@ namespace gridfire {
return m_baseEngine.getNetworkSpecies();
}
std::expected<StepDerivatives<double>, expectations::StaleEngineError> MultiscalePartitioningEngineView::calculateRHSAndEnergy(
std::expected<StepDerivatives<double>, EngineStatus> MultiscalePartitioningEngineView::calculateRHSAndEnergy(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
@@ -355,7 +341,7 @@ namespace gridfire {
throw exceptions::UnableToSetNetworkReactionsError("setNetworkReactions is not supported in MultiscalePartitioningEngineView. Did you mean to call this on the base engine?");
}
std::expected<std::unordered_map<Species, double>, expectations::StaleEngineError> MultiscalePartitioningEngineView::getSpeciesTimescales(
std::expected<std::unordered_map<Species, double>, EngineStatus> MultiscalePartitioningEngineView::getSpeciesTimescales(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
@@ -372,8 +358,7 @@ namespace gridfire {
return speciesTimescales;
}
std::expected<std::unordered_map<Species, double>, expectations::StaleEngineError>
MultiscalePartitioningEngineView::getSpeciesDestructionTimescales(
std::expected<std::unordered_map<Species, double>, EngineStatus> MultiscalePartitioningEngineView::getSpeciesDestructionTimescales(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
@@ -396,6 +381,7 @@ namespace gridfire {
NetIn baseUpdatedNetIn = netIn;
baseUpdatedNetIn.composition = baseUpdatedComposition;
fourdst::composition::Composition equilibratedComposition = partitionNetwork(baseUpdatedNetIn);
m_composition_cache.clear();
return equilibratedComposition;
}
@@ -451,6 +437,11 @@ namespace gridfire {
std::unordered_map<Species, double> speciesTimescales = result.value();
std::vector<QSEGroup> newGroups;
for (const auto &[group, reactions] : std::views::zip(groups, groupReactions)) {
if (reactions.size() == 0) { // If a QSE group has gotten here it should have reactions associated with it. If it doesn't that is a serious error.
LOG_CRITICAL(m_logger, "No reactions specified for QSE group {} during pruning analysis.", group.toString(false));
throw std::runtime_error("No reactions specified for QSE group " + group.toString(false) + " during pruneValidatedGroups flux analysis.");
}
LOG_TRACE_L2(m_logger, "Attempting pruning of QSE Group {:40} (reactions: {}).", group.toString(false), utils::iterable_to_delimited_string(reactions, ", ", [](const auto& r) { return r->id(); }));
std::unordered_map<size_t, double> reactionFluxes;
std::unordered_map<size_t, const reaction::Reaction&> reactionLookup;
reactionFluxes.reserve(reactions.size());
@@ -501,9 +492,9 @@ namespace gridfire {
for (const auto& [hash, normalizedFlux] : reactionFluxes) {
if (normalizedFlux > -30) { // TODO: replace -30 with some more physically motivated value
pruned_reaction_fluxes.emplace(hash, normalizedFlux);
LOG_TRACE_L2(m_logger, "Retaining reaction {} with log(mean abundance normalized flux) {} during pruning.", reactionLookup.at(hash).id(), normalizedFlux);
LOG_TRACE_L2(m_logger, "Retaining reaction {:15} with log(mean abundance normalized flux) {:10.4E} during pruning.", reactionLookup.at(hash).id(), normalizedFlux);
} else {
LOG_TRACE_L2(m_logger, "Pruning reaction {} with log(mean abundance normalized flux) {} during pruning.", reactionLookup.at(hash).id(), normalizedFlux);
LOG_TRACE_L2(m_logger, "Pruning reaction {:15} with log(mean abundance normalized flux) {:10.4E} during pruning.", reactionLookup.at(hash).id(), normalizedFlux);
}
}
@@ -666,121 +657,23 @@ namespace gridfire {
// --- Step 5. Identify potential seed species for each candidate pool ---
LOG_TRACE_L1(m_logger, "Identifying potential seed species for candidate pools...");
const std::vector<QSEGroup> candidate_groups = constructCandidateGroups(connected_pools, comp, T9, rho);
LOG_TRACE_L1(m_logger, "Found {} candidate QSE groups for further analysis", candidate_groups.size());
LOG_TRACE_L2(
m_logger,
"{}",
[&]() -> std::string {
std::stringstream ss;
int j = 0;
for (const auto& group : candidate_groups) {
ss << "CandidateQSEGroup(Algebraic: {";
int i = 0;
for (const auto& species : group.algebraic_species) {
ss << species.name();
if (i < group.algebraic_species.size() - 1) {
ss << ", ";
}
}
ss << "}, Seed: {";
i = 0;
for (const auto& species : group.seed_species) {
ss << species.name();
if (i < group.seed_species.size() - 1) {
ss << ", ";
}
i++;
}
ss << "})";
if (j < candidate_groups.size() - 1) {
ss << ", ";
}
j++;
}
return ss.str();
}()
);
LOG_TRACE_L1(m_logger, "Found {} candidate QSE groups for further analysis ({})", candidate_groups.size(), utils::iterable_to_delimited_string(candidate_groups));
LOG_TRACE_L1(m_logger, "Validating candidate groups with flux analysis...");
const auto [validated_groups, invalidate_groups, validated_group_reactions] = validateGroupsWithFluxAnalysis(candidate_groups, comp, T9, rho);
LOG_TRACE_L1(
m_logger,
"Validated {} group(s) QSE groups. {}",
validated_groups.size(),
[&]() -> std::string {
std::stringstream ss;
int count = 0;
for (const auto& group : validated_groups) {
ss << "Group " << count + 1;
if (group.is_in_equilibrium) {
ss << " is in equilibrium";
} else {
ss << " is not in equilibrium";
}
if (count < validated_groups.size() - 1) {
ss << ", ";
}
count++;
}
return ss.str();
}()
);
LOG_TRACE_L1(m_logger, "Validated {} group(s) QSE groups. {}", validated_groups.size(), utils::iterable_to_delimited_string(validated_groups));
LOG_TRACE_L1(m_logger, "Pruning groups based on log abundance-normalized flux analysis...");
const std::vector<QSEGroup> prunedGroups = pruneValidatedGroups(validated_groups, validated_group_reactions, comp, T9, rho);
LOG_TRACE_L1(m_logger, "After Pruning remaining groups are: {}", [&]() -> std::string {
std::stringstream ss;
int j = 0;
for (const auto& group : prunedGroups) {
ss << "PrunedQSEGroup(Algebraic: {";
int i = 0;
for (const auto& species : group.algebraic_species) {
ss << species.name();
if (i < group.algebraic_species.size() - 1) {
ss << ", ";
}
i++;
}
ss << "}, Seed: {";
i = 0;
for (const auto& species : group.seed_species) {
ss << species.name();
if (i < group.seed_species.size() - 1) {
ss << ", ";
}
i++;
}
ss << "})";
if (j < prunedGroups.size() - 1) {
ss << ", ";
}
j++;
}
return ss.str();
}());
LOG_TRACE_L1(m_logger, "After Pruning remaining groups are: {}", utils::iterable_to_delimited_string(prunedGroups));
LOG_TRACE_L1(m_logger, "Re-validating pruned groups with flux analysis...");
auto [pruned_validated_groups, _, __] = validateGroupsWithFluxAnalysis(prunedGroups, comp, T9, rho);
LOG_TRACE_L1(
m_logger,
"After re-validation, {} QSE groups remain. ({})",
pruned_validated_groups.size(),
[&pruned_validated_groups]()->std::string {
std::stringstream ss;
size_t count = 0;
for (const auto& group : pruned_validated_groups) {
ss << group.toString();
if (pruned_validated_groups.size() > 1 && count < pruned_validated_groups.size() - 1) {
ss << ", ";
}
count++;
}
return ss.str();
}()
);
LOG_TRACE_L1(m_logger, "After re-validation, {} QSE groups remain. ({})",pruned_validated_groups.size(), utils::iterable_to_delimited_string(pruned_validated_groups));
m_qse_groups = pruned_validated_groups;
LOG_TRACE_L1(m_logger, "Identified {} QSE groups.", m_qse_groups.size());
LOG_TRACE_L1(m_logger, "Pushing all identified algebraic species into algebraic set...");
for (const auto& group : m_qse_groups) {
// Add algebraic species to the algebraic set
for (const auto& species : group.algebraic_species) {
@@ -789,41 +682,34 @@ namespace gridfire {
}
}
}
LOG_TRACE_L1(m_logger, "Algebraic species identified: {}", utils::iterable_to_delimited_string(m_algebraic_species));
LOG_INFO(
m_logger,
"Partitioning complete. Found {} dynamic species, {} algebraic (QSE) species ({}) spread over {} QSE group{}.",
m_dynamic_species.size(),
m_algebraic_species.size(),
[&]() -> std::string {
std::stringstream ss;
size_t count = 0;
for (const auto& species : m_algebraic_species) {
ss << species.name();
if (m_algebraic_species.size() > 1 && count < m_algebraic_species.size() - 1) {
ss << ", ";
}
count++;
}
return ss.str();
}(),
utils::iterable_to_delimited_string(m_algebraic_species),
m_qse_groups.size(),
m_qse_groups.size() == 1 ? "" : "s"
);
// Sort the QSE groups by mean timescale so that fastest groups get equilibrated first (as these may feed slower groups)
LOG_TRACE_L1(m_logger, "Sorting algebraic set by mean timescale...");
std::ranges::sort(m_qse_groups, [](const QSEGroup& a, const QSEGroup& b) {
return a.mean_timescale < b.mean_timescale;
});
LOG_TRACE_L1(m_logger, "Finalizing dynamic species list...");
for (const auto& species : m_baseEngine.getNetworkSpecies()) {
const bool involvesAlgebraic = involvesSpeciesInQSE(species);
if (std::ranges::find(m_dynamic_species, species) == m_dynamic_species.end() && !involvesAlgebraic) {
// Species is classed as neither dynamic nor algebraic at end of partitioning → add to dynamic set. This ensures that all species are classified.
m_dynamic_species.push_back(species);
}
}
LOG_TRACE_L1(m_logger, "Final dynamic species set: {}", utils::iterable_to_delimited_string(m_dynamic_species));
LOG_TRACE_L1(m_logger, "Creating QSE solvers for each identified QSE group...");
for (const auto& group : m_qse_groups) {
std::vector<Species> groupAlgebraicSpecies;
for (const auto& species : group.algebraic_species) {
@@ -831,8 +717,11 @@ namespace gridfire {
}
m_qse_solvers.push_back(std::make_unique<QSESolver>(groupAlgebraicSpecies, m_baseEngine, m_sun_ctx));
}
LOG_TRACE_L1(m_logger, "{} QSE solvers created.", m_qse_solvers.size());
LOG_TRACE_L1(m_logger, "Calculating final equilibrated composition...");
fourdst::composition::Composition result = getNormalizedEquilibratedComposition(comp, T9, rho);
LOG_TRACE_L1(m_logger, "Final equilibrated composition calculated...");
return result;
}
@@ -1163,15 +1052,10 @@ namespace gridfire {
const auto destructionTimescale= m_baseEngine.getSpeciesDestructionTimescales(comp, T9, rho);
const auto netTimescale = m_baseEngine.getSpeciesTimescales(comp, T9, rho);
if (!destructionTimescale) {
LOG_ERROR(m_logger, "Failed to get species destruction timescales due to stale engine state");
m_logger->flush_log();
throw exceptions::StaleEngineError("Failed to get species destruction timescales due to stale engine state");
}
if (!netTimescale) {
LOG_ERROR(m_logger, "Failed to get net species timescales due to stale engine state");
m_logger->flush_log();
throw exceptions::StaleEngineError("Failed to get net species timescales due to stale engine state");
if (!destructionTimescale || !netTimescale) {
LOG_CRITICAL(m_logger, "Failed to compute species timescales for partitioning due to base engine error.");
m_logger -> flush_log();
throw exceptions::EngineError("Failed to compute species timescales for partitioning in MultiscalePartitioningEngineView due to base engine error.");
}
const std::unordered_map<Species, double>& destruction_timescales = destructionTimescale.value();
[[maybe_unused]] const std::unordered_map<Species, double>& net_timescales = netTimescale.value();
@@ -1182,7 +1066,7 @@ namespace gridfire {
[&]() -> std::string {
std::stringstream ss;
for (const auto& [species, destruction_timescale] : destruction_timescales) {
ss << std::format("For {} destruction timescale is {}s\n", species.name(), destruction_timescale);
ss << std::format("For {:6} destruction timescale is {:10.4E}s\n", species.name(), destruction_timescale);
}
return ss.str();
}()
@@ -1194,10 +1078,10 @@ namespace gridfire {
for (const auto & species : all_species) {
double destruction_timescale = destruction_timescales.at(species);
if (std::isfinite(destruction_timescale) && destruction_timescale > 0) {
LOG_TRACE_L2(m_logger, "Species {} has finite destruction timescale: destruction: {} s, net: {} s", species.name(), destruction_timescale, net_timescales.at(species));
LOG_TRACE_L2(m_logger, "Species {:6} has finite destruction timescale: destruction: {:10.4E} s, net: {:10.4E} s", species.name(), destruction_timescale, net_timescales.at(species));
sorted_destruction_timescales.emplace_back(destruction_timescale, species);
} else {
LOG_TRACE_L2(m_logger, "Species {} has infinite or negative destruction timescale: destruction: {} s, net: {} s", species.name(), destruction_timescale, net_timescales.at(species));
LOG_TRACE_L2(m_logger, "Species {:6} has infinite or negative destruction timescale: destruction: {:10.4E} s, net: {:10.4E} s", species.name(), destruction_timescale, net_timescales.at(species));
}
}
@@ -1219,50 +1103,44 @@ namespace gridfire {
constexpr double MAX_MOLAR_ABUNDANCE_THRESHOLD = 1e-10; // Maximum molar abundance which a fast species is allowed to have (anything more abundant is always considered dynamic)
constexpr double MIN_MOLAR_ABUNDANCE_THRESHOLD = 1e-50; // Minimum molar abundance to consider a species at all (anything less abundance will be classed as dynamic but with the intent that some latter view will deal with it)
LOG_TRACE_L2(m_logger, "Found {} species with finite timescales.", sorted_destruction_timescales.size());
LOG_TRACE_L2(m_logger, "Absolute QSE timescale threshold: {} seconds ({} years).",
LOG_TRACE_L2(m_logger, "Found {:5} species with finite timescales.", sorted_destruction_timescales.size());
LOG_TRACE_L2(m_logger, "Absolute QSE timescale threshold: {:7.3E} seconds ({:7.2E} years).",
ABSOLUTE_QSE_TIMESCALE_THRESHOLD, ABSOLUTE_QSE_TIMESCALE_THRESHOLD / 3.156e7);
LOG_TRACE_L2(m_logger, "Minimum gap threshold: {} orders of magnitude.", MIN_GAP_THRESHOLD);
LOG_TRACE_L2(m_logger, "Maximum molar abundance threshold for fast species consideration : {}.", MAX_MOLAR_ABUNDANCE_THRESHOLD);
LOG_TRACE_L2(m_logger, "Minimum molar abundance threshold for species consideration : {}.", MIN_MOLAR_ABUNDANCE_THRESHOLD);
LOG_TRACE_L2(m_logger, "Minimum gap threshold: {:3} orders of magnitude.", MIN_GAP_THRESHOLD);
LOG_TRACE_L2(m_logger, "Maximum molar abundance threshold for fast species consideration : {:3}.", MAX_MOLAR_ABUNDANCE_THRESHOLD);
LOG_TRACE_L2(m_logger, "Minimum molar abundance threshold for species consideration : {:3}.", MIN_MOLAR_ABUNDANCE_THRESHOLD);
std::vector<Species> dynamic_pool_species;
std::vector<std::pair<double, Species>> fast_candidates;
// 1. First Pass: Absolute Timescale Cutoff
for (const auto& [destruction_timescale, species] : sorted_destruction_timescales) {
if (species == n_1) {
LOG_TRACE_L2(m_logger, "Skipping neutron (n) from timescale analysis. Neutrons are always considered dynamic due to their extremely high connectivity.");
dynamic_pool_species.push_back(species);
continue;
}
if (destruction_timescale > ABSOLUTE_QSE_TIMESCALE_THRESHOLD) {
LOG_TRACE_L2(m_logger, "Species {} with timescale {} is considered dynamic (slower than qse timescale threshold).",
LOG_TRACE_L2(m_logger, "Species {:5} with timescale {:10.4E} is considered dynamic (slower than qse timescale threshold).",
species.name(), destruction_timescale);
dynamic_pool_species.push_back(species);
} else {
const double Yi = comp.getMolarAbundance(species);
if (Yi > MAX_MOLAR_ABUNDANCE_THRESHOLD) {
LOG_TRACE_L2(m_logger, "Species {} with abundance {} is considered dynamic (above minimum abundance threshold of {}).",
LOG_TRACE_L2(m_logger, "Species {:5} with abundance {:10.4E} is considered dynamic (above minimum abundance threshold of {}).",
species.name(), Yi, MAX_MOLAR_ABUNDANCE_THRESHOLD);
dynamic_pool_species.push_back(species);
continue;
}
if (Yi < MIN_MOLAR_ABUNDANCE_THRESHOLD) {
LOG_TRACE_L2(m_logger, "Species {} with abundance {} is considered dynamic (below minimum abundance threshold of {}). Likely another network view (such as adaptive engine view) will be needed to deal with this species",
LOG_TRACE_L2(m_logger, "Species {:5} with abundance {:10.4E} is considered dynamic (below minimum abundance threshold of {:10.4E}). Likely another network view (such as adaptive engine view) will be needed to deal with this species",
species.name(), Yi, MIN_MOLAR_ABUNDANCE_THRESHOLD);
dynamic_pool_species.push_back(species);
continue;
}
LOG_TRACE_L2(m_logger, "Species {} with timescale {} and molar abundance {} is a candidate fast species (faster than qse timescale threshold and less than the molar abundance threshold).",
LOG_TRACE_L2(m_logger, "Species {:5} with timescale {:10.4E} and molar abundance {:10.4E} is a candidate fast species (faster than qse timescale threshold and less than the molar abundance threshold).",
species.name(), destruction_timescale, Yi);
fast_candidates.emplace_back(destruction_timescale, species);
}
}
if (!dynamic_pool_species.empty()) {
LOG_TRACE_L2(m_logger, "Found {} dynamic species (slower than QSE timescale threshold).", dynamic_pool_species.size());
LOG_TRACE_L2(m_logger, "Found {:5} dynamic species (slower than QSE timescale threshold).", dynamic_pool_species.size());
final_pools.push_back(dynamic_pool_species);
}
@@ -1277,7 +1155,7 @@ namespace gridfire {
const double t1 = fast_candidates[i].first;
const double t2 = fast_candidates[i+1].first;
if (std::log10(t1) - std::log10(t2) > MIN_GAP_THRESHOLD) {
LOG_TRACE_L2(m_logger, "Detected gap between species {} (timescale {:0.2E}) and {} (timescale {:0.2E}).",
LOG_TRACE_L2(m_logger, "Detected gap between species {:5} (timescale {:8.2E}) and {:5} (timescale {:10.2E}).",
fast_candidates[i].second.name(), t1,
fast_candidates[i+1].second.name(), t2);
split_points.push_back(i + 1);
@@ -1349,18 +1227,12 @@ namespace gridfire {
}
MultiscalePartitioningEngineView::FluxValidationResult MultiscalePartitioningEngineView::validateGroupsWithFluxAnalysis(
const std::vector<QSEGroup> &candidate_groups,
std::pair<bool, reaction::ReactionSet> MultiscalePartitioningEngineView::group_is_a_qse_cluster(
const fourdst::composition::Composition &comp,
const double T9, const double rho
const double T9,
const double rho,
const QSEGroup &group
) const {
std::vector<QSEGroup> validated_groups;
std::vector<reaction::ReactionSet> group_reactions;
std::vector<QSEGroup> invalidated_groups;
validated_groups.reserve(candidate_groups.size());
group_reactions.reserve(candidate_groups.size());
for (auto& group : candidate_groups) {
reaction::ReactionSet group_reaction_set;
constexpr double FLUX_RATIO_THRESHOLD = 5;
const std::unordered_set<Species> algebraic_group_members(
@@ -1373,7 +1245,7 @@ namespace gridfire {
group.seed_species.end()
);
// Values for measuring the flux coupling vs leakage
reaction::ReactionSet group_reaction_set;
double coupling_flux = 0.0;
double leakage_flux = 0.0;
@@ -1404,50 +1276,18 @@ namespace gridfire {
}
if (!has_internal_algebraic_product && !has_internal_algebraic_reactant) {
LOG_TRACE_L3(m_logger, "{}: Skipping reaction {} as it has no internal algebraic species in reactants or products.", group.toString(), reaction->id());
LOG_TRACE_L3(m_logger, "{}: Skipping reaction {} as it has no internal algebraic species in reactants or products.", group.toString(false), reaction->id());
continue;
}
group_reaction_set.add_reaction(reaction->clone());
LOG_TRACE_L2(
LOG_TRACE_L3(
m_logger,
"Reaction {} (Coupling {}) has flow {} mol g^-1 s^-1 contributing to QSEGroup {}",
"Reaction {:20} has flow {:15.4E} mol g^-1 s^-1 contributing to QSEGroup {:40}",
reaction->id(),
[&group, &reaction]() -> std::string {
std::ostringstream ss;
if (group.algebraic_species.empty()) {
ss << "N/A (no algebraic species)";
} else {
// Make a string of all the group species coupled from the reaction in the form of
// "A, B -> C, D"
int count = 0;
for (const auto& species : group.algebraic_species) {
if (std::ranges::find(reaction->reactants(), species) != reaction->reactants().end()) {
ss << species.name();
if (count < group.algebraic_species.size() - 1) {
ss << ", ";
}
count++;
}
}
ss << " -> ";
count = 0;
for (const auto& species : group.algebraic_species) {
if (std::ranges::find(reaction->products(), species) != reaction->products().end()) {
ss << species.name();
if (count < group.algebraic_species.size() - 1) {
ss << ", ";
}
count++;
}
}
}
return ss.str();
}(),
flow,
group.toString()
group.toString(false)
);
double algebraic_participants = 0;
@@ -1460,13 +1300,13 @@ namespace gridfire {
for (const auto& species : participants) {
if (algebraic_group_members.contains(species)) {
LOG_TRACE_L3(m_logger, "{}: Species {} is an algebraic participant in reaction {}.", group.toString(), species.name(), reaction->id());
LOG_TRACE_L3(m_logger, "{}: Species {} is an algebraic participant in reaction {}.", group.toString(true), species.name(), reaction->id());
algebraic_participants++;
} else if (seed_group_members.contains(species)) {
LOG_TRACE_L3(m_logger, "{}: Species {} is a seed participant in reaction {}.", group.toString(), species.name(), reaction->id());
LOG_TRACE_L3(m_logger, "{}: Species {} is a seed participant in reaction {}.", group.toString(true), species.name(), reaction->id());
seed_participants++;
} else {
LOG_TRACE_L3(m_logger, "{}: Species {} is an external participant in reaction {}.", group.toString(), species.name(), reaction->id());
LOG_TRACE_L3(m_logger, "{}: Species {} is an external participant in reaction {}.", group.toString(true), species.name(), reaction->id());
external_participants++;
}
}
@@ -1484,54 +1324,105 @@ namespace gridfire {
leakage_flux += flow * leakage_fraction;
coupling_flux += flow * coupling_fraction;
}
if (coupling_flux / leakage_flux > FLUX_RATIO_THRESHOLD) {
LOG_TRACE_L1(
m_logger,
"Group containing {} is in equilibrium due to high coupling flux and balanced creation and destruction rate: <coupling: leakage flux = {}, coupling flux = {}, ratio = {} (Threshold: {})>",
[&]() -> std::string {
std::stringstream ss;
int count = 0;
for (const auto& species: group.algebraic_species) {
ss << species.name();
if (count < group.algebraic_species.size() - 1) {
ss << ", ";
}
count++;
}
return ss.str();
}(),
leakage_flux,
coupling_flux,
coupling_flux / leakage_flux,
FLUX_RATIO_THRESHOLD
LOG_TRACE_L2(m_logger, "Reaction {:20} contributes coupling flux {:15.4E} and leakage flux {:15.4E} to QSEGroup {}.",
reaction->id(),
flow * coupling_fraction,
flow * leakage_fraction,
group.toString(false)
);
}
bool leakage_coupled = (coupling_flux / leakage_flux > FLUX_RATIO_THRESHOLD);
return std::make_pair(leakage_coupled, group_reaction_set);
}
bool MultiscalePartitioningEngineView::group_is_a_qse_pipeline(
const fourdst::composition::Composition &comp,
const double T9,
const double rho,
const QSEGroup &group
) const {
// Total fluxes (Standard check)
double total_prod = 0.0;
double total_dest = 0.0;
// Charged-particle only fluxes (Heuristic for fast-neutron regimes)
double charged_prod = 0.0;
double charged_dest = 0.0;
for (const auto& reaction : m_baseEngine.getNetworkReactions()) {
const double flow = m_baseEngine.calculateMolarReactionFlow(*reaction, comp, T9, rho);
if (std::abs(flow) < 1.0e-99) continue;
int groupNetStoichiometry = 0;
for (const auto& species : group.algebraic_species) {
groupNetStoichiometry += reaction->stoichiometry(species);
}
if (groupNetStoichiometry == 0) continue;
const double flux = flow * groupNetStoichiometry;
const bool is_neutron_reaction = reaction->contains(n_1);
if (flux > 0.0) {
total_prod += flux;
if (!is_neutron_reaction) charged_prod += flux;
} else {
total_dest += -flux;
if (!is_neutron_reaction) charged_dest += -flux;
}
}
// Check 1: Total Balance
const double mean_total = (total_prod + total_dest) / 2.0;
const double diff_total = std::abs(total_prod - total_dest);
bool total_balanced = (mean_total > 0) && ((diff_total / mean_total) < 0.05);
// Check 2: Charged-Particle Balance (The "Neutron-Exclusion" Check)
// Only valid if there IS charged flow (avoid 0/0 success)
const double mean_charged = (charged_prod + charged_dest) / 2.0;
const double diff_charged = std::abs(charged_prod - charged_dest);
bool charged_balanced = (mean_charged > 0) && ((diff_charged / mean_charged) < 0.05);
LOG_TRACE_L2(m_logger, "{} Pipeline Check | Total Pass: {} | Charged Pass: {}",
group.toString(false), total_balanced, charged_balanced);
return total_balanced || charged_balanced;
}
MultiscalePartitioningEngineView::FluxValidationResult MultiscalePartitioningEngineView::validateGroupsWithFluxAnalysis(
const std::vector<QSEGroup> &candidate_groups,
const fourdst::composition::Composition &comp,
const double T9, const double rho
) const {
std::vector<QSEGroup> validated_groups;
std::vector<reaction::ReactionSet> group_reactions;
std::vector<QSEGroup> invalidated_groups;
validated_groups.reserve(candidate_groups.size());
group_reactions.reserve(candidate_groups.size());
for (auto& group : candidate_groups) {
// Values for measuring the flux coupling vs leakage
auto [leakage_coupled, group_reaction_set] = group_is_a_qse_cluster(comp, T9, rho, group);
bool is_flow_balanced = group_is_a_qse_pipeline(comp, T9, rho, group);
if (leakage_coupled) {
LOG_TRACE_L1(m_logger, "{} is in equilibrium due to high coupling flux", group.toString(false));
validated_groups.emplace_back(group);
validated_groups.back().is_in_equilibrium = true;
group_reactions.emplace_back(group_reaction_set);
} else {
LOG_TRACE_L1(
m_logger,
"Group containing {} is NOT in equilibrium: <coupling: leakage flux = {}, coupling flux = {}, ratio = {} (Threshold: {})>",
[&]() -> std::string {
std::stringstream ss;
int count = 0;
for (const auto& species : group.algebraic_species) {
ss << species.name();
if (count < group.algebraic_species.size() - 1) {
ss << ", ";
}
count++;
else if (is_flow_balanced) {
LOG_TRACE_L1(m_logger, "{} is in equilibrium due to balanced production and destruction fluxes.", group.toString(false));
validated_groups.emplace_back(group);
validated_groups.back().is_in_equilibrium = true;
group_reactions.emplace_back(group_reaction_set);
}
return ss.str();
}(),
leakage_flux,
coupling_flux,
coupling_flux / leakage_flux,
FLUX_RATIO_THRESHOLD
);
else {
LOG_TRACE_L1(m_logger, "{} is NOT in equilibrium due to high leakage flux and lack of pipeline detection.", group.toString(false));
invalidated_groups.emplace_back(group);
invalidated_groups.back().is_in_equilibrium = false;
}
@@ -1553,9 +1444,15 @@ namespace gridfire {
for (const auto& [group, solver]: std::views::zip(m_qse_groups, m_qse_solvers)) {
const fourdst::composition::Composition groupResult = solver->solve(outputComposition, T9, rho);
for (const auto& [sp, y] : groupResult) {
if (!std::isfinite(y)) {
LOG_CRITICAL(m_logger, "Non-finite abundance {} computed for species {} in QSE group solve at T9 = {}, rho = {}.",
y, sp.name(), T9, rho);
m_logger->flush_log();
throw exceptions::EngineError("Non-finite abundance computed for species " + std::string(sp.name()) + " in QSE group solve.");
}
outputComposition.setMolarAbundance(sp, y);
}
solver->log_diagnostics();
solver->log_diagnostics(group, outputComposition);
}
LOG_TRACE_L2(m_logger, "Done solving for QSE abundances!");
return outputComposition;
@@ -1569,9 +1466,9 @@ namespace gridfire {
) const {
const auto& result = m_baseEngine.getSpeciesDestructionTimescales(comp, T9, rho);
if (!result) {
LOG_ERROR(m_logger, "Failed to get species timescales due to stale engine state");
LOG_CRITICAL(m_logger, "Failed to get species destruction timescales due base engine failure");
m_logger->flush_log();
throw exceptions::StaleEngineError("Failed to get species timescales due to stale engine state");
throw exceptions::EngineError("Failed to get species destruction timescales due base engine failure");
}
const std::unordered_map<Species, double> all_timescales = result.value();
@@ -1674,9 +1571,9 @@ namespace gridfire {
const auto& all_reactions = m_baseEngine.getNetworkReactions();
const auto& result = m_baseEngine.getSpeciesDestructionTimescales(comp, T9, rho);
if (!result) {
LOG_ERROR(m_logger, "Failed to get species timescales due to stale engine state");
LOG_ERROR(m_logger, "Failed to get species destruction timescales due base engine failure");
m_logger->flush_log();
throw exceptions::StaleEngineError("Failed to get species timescales due to stale engine state");
throw exceptions::EngineError("Failed to get species destruction timescales due base engine failure");
}
const std::unordered_map<Species, double> destruction_timescales = result.value();
@@ -1764,6 +1661,18 @@ namespace gridfire {
//////////////////////////////////
bool MultiscalePartitioningEngineView::QSEGroup::contains(const fourdst::atomic::Species &species) const {
return algebraic_species.contains(species) || seed_species.contains(species);
}
bool MultiscalePartitioningEngineView::QSEGroup::containsAlgebraic(const Species &species) const {
return algebraic_species.contains(species);
}
bool MultiscalePartitioningEngineView::QSEGroup::containsSeed(const Species &species) const {
return seed_species.contains(species);
}
MultiscalePartitioningEngineView::QSESolver::QSESolver(
const std::vector<fourdst::atomic::Species>& species,
const DynamicEngine& engine,
@@ -1773,6 +1682,20 @@ namespace gridfire {
m_engine(engine),
m_species(species),
m_sun_ctx(sun_ctx) {
LOG_TRACE_L1(getLogger(), "Initializing QSE Solver with {} species ({})", m_N,
[&]() -> std::string {
std::stringstream ss;
int count = 0;
for (const auto& sp : species) {
ss << sp.name();
if (count < species.size() - 1) {
ss << ", ";
}
count++;
}
return ss.str();
}()
);
m_Y = utils::init_sun_vector(m_N, m_sun_ctx);
m_scale = N_VClone(m_Y);
m_f_scale = N_VClone(m_Y);
@@ -1810,8 +1733,8 @@ namespace gridfire {
utils::check_cvode_flag(KINSetNumMaxIters(m_kinsol_mem, 200), "KINSetNumMaxIters");
// We want to effectively disable this since enormous changes in order of magnitude are realistic for this problem.
utils::check_cvode_flag(KINSetMaxNewtonStep(m_kinsol_mem, 200), "KINSetMaxNewtonStep");
utils::check_cvode_flag(KINSetMaxNewtonStep(m_kinsol_mem, std::numeric_limits<double>::infinity()), "KINSetMaxNewtonStep");
LOG_TRACE_L1(getLogger(), "Finished initializing QSE Solver.");
}
MultiscalePartitioningEngineView::QSESolver::~QSESolver() {
@@ -1862,7 +1785,8 @@ namespace gridfire {
rho,
result,
m_speciesMap,
m_species
m_species,
*this
};
utils::check_cvode_flag(KINSetUserData(m_kinsol_mem, &data), "KINSetUserData");
@@ -1878,7 +1802,7 @@ namespace gridfire {
constexpr double abundance_floor = 1.0e-100;
Y = std::max(abundance_floor, Y);
y_data[i] = Y;
scale_data[i] = 1.0;
scale_data[i] = 1.0 / Y;
}
auto initial_rhs = m_engine.calculateRHSAndEnergy(result, T9, rho);
@@ -1893,17 +1817,66 @@ namespace gridfire {
f_scale_data[i] = 1.0 / (dydt + 1e-15);
}
if (m_solves > 0) {
if (m_solves > 0 && m_has_jacobian) {
// After the initial solution we want to allow kinsol to reuse its state
utils::check_cvode_flag(KINSetNoInitSetup(m_kinsol_mem, SUNTRUE), "KINSetNoInitSetup");
} else {
utils::check_cvode_flag(KINSetNoInitSetup(m_kinsol_mem, SUNFALSE), "KINSetNoInitSetup");
}
LOG_TRACE_L2(
getLogger(),
"Starting KINSol QSE solver with initial state: {}",
[&comp, &initial_rhs, &data]() -> std::string {
std::ostringstream oss;
oss << "Solve species: <";
size_t count = 0;
for (const auto& species : data.qse_solve_species) {
oss << species.name();
if (count < data.qse_solve_species.size() - 1) {
oss << ", ";
}
count++;
}
oss << "> | Initial abundances and rates: ";
count = 0;
for (const auto& [species, abundance] : comp) {
oss << species.name() << ": Y = " << abundance << ", dY/dt = " << initial_rhs.value().dydt.at(species);
if (count < comp.size() - 1) {
oss << ", ";
}
count++;
}
return oss.str();
}()
);
LOG_TRACE_L2(
getLogger(),
"Jacobian Prior to KINSol is: {}",
[&]() -> std::string {
std::ostringstream oss;
sunrealtype* J_data = SUNDenseMatrix_Data(m_J);
const sunindextype N = SUNDenseMatrix_Columns(m_J);
oss << "[";
for (size_t i = 0; i < m_N; ++i) {
oss << "[";
for (size_t j = 0; j < m_N; ++j) {
oss << J_data[i * N + j];
if (j < m_N - 1) {
oss << ", ";
}
}
oss << "]";
}
oss << "]";
return oss.str();
}()
);
const int flag = KINSol(m_kinsol_mem, m_Y, KIN_LINESEARCH, m_scale, m_f_scale);
if (flag < 0) {
LOG_WARNING(getLogger(), "KINSol failed to converge while solving QSE abundances with flag {}.", utils::cvode_ret_code_map.at(flag));
return comp;
throw exceptions::InvalidQSESolutionError("KINSol failed to converge while solving QSE abundances. Check the log file for more details regarding the specific failure mode.");
}
for (size_t i = 0; i < m_N; ++i) {
@@ -1919,18 +1892,56 @@ namespace gridfire {
return m_solves;
}
void MultiscalePartitioningEngineView::QSESolver::log_diagnostics() const {
void MultiscalePartitioningEngineView::QSESolver::log_diagnostics(const QSEGroup &group, const fourdst::composition::Composition &comp) const {
long int nni, nfe, nje;
int flag = KINGetNumNonlinSolvIters(m_kinsol_mem, &nni);
utils::check_cvode_flag(KINGetNumNonlinSolvIters(m_kinsol_mem, &nni), "KINGetNumNonlinSolvIters");
flag = KINGetNumFuncEvals(m_kinsol_mem, &nfe);
utils::check_cvode_flag(KINGetNumFuncEvals(m_kinsol_mem, &nfe), "KINGetNumFuncEvals");
flag = KINGetNumJacEvals(m_kinsol_mem, &nje);
utils::check_cvode_flag(KINGetNumJacEvals(m_kinsol_mem, &nje), "KINGetNumJacEvals");
LOG_INFO(getLogger(),
"QSE Stats | Iters: {} | RHS Evals: {} | Jac Evals: {} | Ratio (J/I): {:.2f}",
nni, nfe, nje, static_cast<double>(nje) / static_cast<double>(nni)
LOG_TRACE_L1(getLogger(),
"QSE Stats | Iters: {} | RHS Evals: {} | Jac Evals: {} | Ratio (J/I): {:.2f} | Algebraic Species: {}",
nni,
nfe,
nje,
static_cast<double>(nje) / static_cast<double>(nni),
[&group, &comp]() -> std::string {
std::ostringstream oss;
size_t count = 0;
oss << "[";
for (const auto& species : group.algebraic_species) {
oss << species.name() << "(Y = " << comp.getMolarAbundance(species) << ")";
if (count < group.algebraic_species.size() - 1) {
oss << ", ";
}
count++;
}
oss << "]";
return oss.str();
}()
);
LOG_TRACE_L2(getLogger(),
"Jacobian After KINSol is: {}",
[&]() -> std::string {
std::ostringstream oss;
sunrealtype* J_data = SUNDenseMatrix_Data(m_J);
const sunindextype N = SUNDenseMatrix_Columns(m_J);
oss << "[";
for (size_t i = 0; i < m_N; ++i) {
oss << "[";
for (size_t j = 0; j < m_N; ++j) {
oss << J_data[i * N + j];
if (j < m_N - 1) {
oss << ", ";
}
}
oss << "]";
}
oss << "]";
return oss.str();
}()
);
getLogger()->flush_log(true);
}
@@ -1949,6 +1960,12 @@ namespace gridfire {
const auto& map = data->qse_solve_species_index_map;
for (size_t index = 0; index < data->qse_solve_species.size(); ++index) {
const auto& species = data->qse_solve_species[index];
if (!std::isfinite(y_data[index])) {
std::string msg = std::format("Non-finite abundance {} encountered for species {} in QSE solver sys_func. Attempting recovery...",
y_data[index], species.name());
LOG_ERROR(getLogger(), "{}", msg);
return 1; // Potentially recoverable error
}
data->comp.setMolarAbundance(species, y_data[index]);
}
@@ -1959,6 +1976,36 @@ namespace gridfire {
}
const auto& dydt = result.value().dydt;
#ifndef NDEBUG
// In debug mode check that all dydt values are finite and not NaN
for (const auto &species: map | std::views::keys) {
const double value = dydt.at(species);
if (!std::isfinite(value)) {
std::vector<std::pair<Species, double>> invalid_species;
for (const auto &s: map | std::views::keys) {
const double v = dydt.at(s);
if (!std::isfinite(v)) {
invalid_species.push_back(std::make_pair(s, v));
}
}
std::string msg = std::format("Non-finite dydt values encountered for species: {}",
[&invalid_species]() -> std::string {
std::ostringstream ss;
size_t count = 0;
for (const auto& [sp, val] : invalid_species) {
ss << sp.name() << ": " << val;
if (count < invalid_species.size() - 1) {
ss << ", ";
}
count++;
}
return ss.str();
}());
LOG_CRITICAL(getLogger(), "{}", msg);
throw exceptions::InvalidQSESolutionError(msg);
}
}
#endif
for (const auto& [species, index] : map) {
f_data[index] = dydt.at(species);
@@ -2000,6 +2047,8 @@ namespace gridfire {
}
}
data->instance.m_has_jacobian = true;
return 0;
}
@@ -2024,7 +2073,7 @@ namespace gridfire {
void MultiscalePartitioningEngineView::QSEGroup::addSpeciesToAlgebraic(const Species &species) {
if (seed_species.contains(species)) {
const std::string msg = std::format("Cannot add species {} to algebraic set as it is already in the seed set.", species.name());
const std::string msg = std::format("Cannot add species {:8} to algebraic set as it is already in the seed set.", species.name());
throw std::invalid_argument(msg);
}
if (!algebraic_species.contains(species)) {
@@ -2034,7 +2083,7 @@ namespace gridfire {
void MultiscalePartitioningEngineView::QSEGroup::addSpeciesToSeed(const Species &species) {
if (algebraic_species.contains(species)) {
const std::string msg = std::format("Cannot add species {} to seed set as it is already in the algebraic set.", species.name());
const std::string msg = std::format("Cannot add species {:8} to seed set as it is already in the algebraic set.", species.name());
throw std::invalid_argument(msg);
}
if (!seed_species.contains(species)) {
@@ -2054,28 +2103,17 @@ namespace gridfire {
return !(*this == other);
}
std::string MultiscalePartitioningEngineView::QSEGroup::toString() const {
std::stringstream ss;
ss << "QSEGroup(Algebraic: [";
size_t count = 0;
for (const auto& species : algebraic_species) {
ss << species.name();
if (count < algebraic_species.size() - 1) {
ss << ", ";
std::string MultiscalePartitioningEngineView::QSEGroup::toString(const bool verbose) const {
if (verbose) {
return std::format(
"QSEGroup(Algebraic: [{:40}], Seed [{:40}], Mean Timescale: {:10.4E}, Is in Equilibrium: {:6}",
utils::iterable_to_delimited_string(algebraic_species),
utils::iterable_to_delimited_string(seed_species),
mean_timescale,
is_in_equilibrium ? "True" : "False"
);
}
count++;
return std::format("QSEGroup({})", utils::iterable_to_delimited_string(algebraic_species));
}
ss << "], Seed: [";
count = 0;
for (const auto& species : seed_species) {
ss << species.name();
if (count < seed_species.size() - 1) {
ss << ", ";
}
count++;
}
ss << "], Mean Timescale: " << mean_timescale << ", Is In Equilibrium: " << (is_in_equilibrium ? "True" : "False") << ")";
return ss.str();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
# Define the library
gridfire_sources = files(
'lib/network.cpp',
'lib/engine/engine_graph.cpp',
'lib/engine/views/engine_adaptive.cpp',
'lib/engine/views/engine_defined.cpp',
@@ -42,6 +41,7 @@ gridfire_build_dependencies = [
eigen_dep,
plugin_dep,
sundials_dep,
json_dep,
]
# Define the libnetwork library so it can be linked against by other parts of the build system

View File

@@ -1,35 +1,21 @@
#include <iostream>
#include <fstream>
#include "gridfire/engine/engine_graph.h"
#include "gridfire/engine/views/engine_adaptive.h"
#include "gridfire/solver/strategies/CVODE_solver_strategy.h"
#include "gridfire/policy/stellar_policy.h"
#include "gridfire/utils/table_format.h"
#include "gridfire/network.h"
#include "gridfire/gridfire.h"
#include "fourdst/composition/composition.h"
#include "fourdst/plugin/bundle/bundle.h"
#include "fourdst/logging/logging.h"
#include "fourdst/atomic/species.h"
#include "fourdst/composition/utils.h"
#include "quill/Logger.h"
#include "quill/LogMacros.h"
#include "quill/Backend.h"
#include <clocale>
#include <functional>
#include "fourdst/atomic/species.h"
#include "fourdst/composition/utils.h"
#include "gridfire/exceptions/error_solver.h"
#include <boost/json/src.hpp>
#include "gridfire/reaction/reaclib.h"
#include "gridfire/solver/strategies/CVODE_solver_strategy.h"
static std::terminate_handler g_previousHandler = nullptr;
boost::json::object g_reaction_contribution_history;
@@ -38,16 +24,7 @@ static bool s_wrote_abundance_history = false;
static bool s_wrote_reaction_history = false;
void quill_terminate_handler();
inline std::unique_ptr<gridfire::partition::PartitionFunction> build_partition_function() {
using gridfire::partition::BasePartitionType;
const auto partitionFunction = gridfire::partition::CompositePartitionFunction({
BasePartitionType::RauscherThielemann,
BasePartitionType::GroundState
});
return std::make_unique<gridfire::partition::CompositePartitionFunction>(partitionFunction);
}
gridfire::NetIn init(const double temp) {
gridfire::NetIn init(const double temp, const double rho, const double tMax) {
std::setlocale(LC_ALL, "");
g_previousHandler = std::set_terminate(quill_terminate_handler);
quill::Logger* logger = fourdst::logging::LogManager::getInstance().getLogger("log");
@@ -63,10 +40,10 @@ gridfire::NetIn init(const double temp) {
NetIn netIn;
netIn.composition = composition;
netIn.temperature = temp;
netIn.density = 1.5e2;
netIn.density = rho;
netIn.energy = 0;
netIn.tMax = 1e17;
netIn.tMax = tMax;
netIn.dt0 = 1e-12;
return netIn;
@@ -277,17 +254,31 @@ void callback_main(const gridfire::solver::CVODESolverStrategy::TimestepContext&
int main() {
using namespace gridfire;
constexpr double temp = 1.5e7; // 15 MK
const NetIn netIn = init(temp);
constexpr double temp = 1.5e7;
constexpr double rho = 1.5e2;
constexpr double tMax = 3e17;
NetIn netIn = init(temp, rho, tMax);
policy::MainSequencePolicy stellarPolicy(netIn.composition);
stellarPolicy.construct();
DynamicEngine& engine = stellarPolicy.construct();
engine::DynamicEngine& engine = stellarPolicy.construct();
solver::CVODESolverStrategy solver(engine);
solver.set_callback(solver::CVODESolverStrategy::TimestepCallback(callback_main));
const NetOut netOut = solver.evaluate(netIn, true);
log_results(netOut, netIn);
log_callback_data(temp);
// LOG_TRACE_L1(LogManager::getInstance().getLogger("log"), "Performing final engine update for verification...");
//
//
// const std::vector<double> Y = {0.2131, 1.991e-6, 0.1917, 6.310e-7, 3.296e-4, 9.62e-3, 1.62e-3, 5.16e-4};
// const std::vector<std::string> symbols = {"H-1", "He-3", "He-4", "C-12", "N-14", "O-16", "Ne-20", "Mg-24"};
//
// fourdst::composition::Composition manualLateComposition(symbols, Y);
// const fourdst::composition::Composition comp = engine.update(netIn);
// std::cout << comp.getMolarAbundance("H-2") << std::endl;
}