feat(GridFire): major design changes

Switching to an Engine + solver design. Also brought xxHash and Eigen in. Working on QSE and Culling.
This commit is contained in:
2025-06-26 15:13:46 -04:00
parent dd03873bc9
commit cd191cff23
32 changed files with 2737 additions and 1441 deletions

View File

@@ -1,254 +0,0 @@
#pragma once
#include <algorithm>
#include <utility>
#include <vector>
#include <string>
#include <iostream>
#include <unordered_map>
#include "fourdst/composition/atomicSpecies.h"
#include "cppad/cppad.hpp"
namespace gridfire::reaclib {
/**
* @struct RateFitSet
* @brief Holds the seven fitting parameters for a single REACLIB rate set.
* @details The thermonuclear reaction rate for a single set is calculated as:
* rate = exp(a0 + a1/T9 + a2/T9^(-1/3) + a3*T9^(1/3) + a4*T9 + a5*T9^(5/3) + a6*ln(T9))
* where T9 is the temperature in billions of Kelvin. The total rate for a
* reaction is the sum of the rates from all its sets.
*/
struct RateFitSet {
double a0, a1, a2, a3, a4, a5, a6;
};
/**
* @struct REACLIBReaction
* @brief Represents a single nuclear reaction from the JINA REACLIB database.
* @details This struct is designed to be constructed at compile time (constexpr) from
* the data parsed by the Python generation script. It stores all necessary
* information to identify a reaction and calculate its rate.
*/
class REACLIBReaction {
private:
std::string m_id; ///< Unique identifier for the reaction, generated by the Python script.
std::string_view m_peName; ///< Name of the reaction in (projectile, ejectile) notation (e.g. p(p, g)d)
int m_chapter; ///< Chapter number from the REACLIB database, defining the reaction structure.
std::vector<fourdst::atomic::Species> m_reactantNames; ///< Names of the reactant species involved in the reaction.
std::vector<fourdst::atomic::Species> m_productNames; ///< Names of the product species produced by the reaction.
double m_qValue; ///< Q-value of the reaction in MeV, representing the energy released or absorbed.
std::string m_sourceLabel; ///< Source label for the rate data, indicating the origin of the rate coefficients (e.g., "wc12w", "st08").
RateFitSet m_rateSets; ///< Fitting coefficients for the reaction rate, containing the seven parameters used in the rate calculation.
bool m_reverse = false; ///< indicates if the reaction is reversed
friend bool operator==(const REACLIBReaction& lhs, const REACLIBReaction& rhs);
friend bool operator!=(const REACLIBReaction& lhs, const REACLIBReaction& rhs);
public:
/**
* @brief Constructs a REACLIBReaction object at compile time.
* @param id A unique string identifier generated by the Python script.
* @param chapter The REACLIB chapter number, defining the reaction structure.
* @param reactants A vector of strings with the names of the reactant species.
* @param products A vector of strings with the names of the product species.
* @param qValue The Q-value of the reaction in MeV.
* @param label The source label for the rate data (e.g., "wc12w", "st08").
* @param sets A vector of RateFitSet, containing the fitting coefficients for the rate.
* @param reverse A boolean indicating if the reaction is reversed (default is false).
*/
REACLIBReaction(
std::string id,
std::string_view peName,
int chapter,
std::vector<fourdst::atomic::Species> reactants,
std::vector<fourdst::atomic::Species> products,
double qValue,
std::string label,
RateFitSet sets,
bool reverse = false)
: m_id(std::move(id)),
m_peName(peName),
m_chapter(chapter),
m_reactantNames(std::move(reactants)),
m_productNames(std::move(products)),
m_qValue(qValue),
m_sourceLabel(std::move(label)),
m_rateSets(sets),
m_reverse(reverse) {}
template <typename GeneralScalarType>
[[nodiscard]] GeneralScalarType calculate_rate(const GeneralScalarType T9) const {
const GeneralScalarType T913 = CppAD::pow(T9, 1.0/3.0);
const GeneralScalarType rateExponent = m_rateSets.a0 +
m_rateSets.a1 / T9 +
m_rateSets.a2 / T913 +
m_rateSets.a3 * T913 +
m_rateSets.a4 * T9 +
m_rateSets.a5 * CppAD::pow(T9, 5.0/3.0) +
m_rateSets.a6 * CppAD::log(T9);
return CppAD::exp(rateExponent);
}
[[nodiscard]] const std::string& id() const { return m_id; }
[[nodiscard]] std::string_view peName() const { return m_peName; }
[[nodiscard]] int chapter() const { return m_chapter; }
[[nodiscard]] const std::vector<fourdst::atomic::Species>& reactants() const { return m_reactantNames; }
[[nodiscard]] const std::vector<fourdst::atomic::Species>& products() const { return m_productNames; }
[[nodiscard]] double qValue() const { return m_qValue; }
[[nodiscard]] std::string sourceLabel() const { return m_sourceLabel; }
[[nodiscard]] bool is_reverse() const { return m_reverse; }
[[nodiscard]] RateFitSet rateFits() const { return m_rateSets; }
friend std::ostream& operator<<(std::ostream& os, const REACLIBReaction& reaction) {
os << "REACLIBReaction(" << reaction.m_peName << ", "
<< "Chapter: " << reaction.m_chapter << ")";
return os;
}
friend bool operator==(const REACLIBReaction& lhs, const REACLIBReaction& rhs);
friend bool operator!=(const REACLIBReaction& lhs, const REACLIBReaction& rhs);
};
class REACLIBReactionSet {
private:
std::vector<REACLIBReaction> m_reactions;
public:
REACLIBReactionSet() = default;
explicit REACLIBReactionSet(std::vector<REACLIBReaction> r) : m_reactions(std::move(r)) {}
void add_reaction(const REACLIBReaction& reaction) {
if (not contains(reaction.id())) {
m_reactions.push_back(reaction);
}
}
void remove_reaction(const REACLIBReaction& reaction) {
if (contains(reaction.id())) {
m_reactions.erase(std::remove_if(m_reactions.begin(), m_reactions.end(),
[&reaction](const REACLIBReaction& r) { return r.id() == reaction.id(); }), m_reactions.end());
}
}
[[nodiscard]] bool contains(const std::string& id) const {
for (const auto& reaction : m_reactions) {
if (reaction.id() == id) {
return true;
}
}
return false;
}
[[nodiscard]] bool contains(const REACLIBReaction& reaction) const {
for (const auto& r : m_reactions) {
if (r == reaction) {
return true;
}
}
return false;
}
[[nodiscard]] size_t size() const {
return m_reactions.size();
}
friend std::ostream& operator<<(std::ostream& os, const REACLIBReactionSet& set) {
os << "REACLIBReactionSet(";
int reactionNo = 0;
for (const auto& reaction : set.m_reactions) {
reactionNo++;
os << reaction.id();
if (reactionNo < set.m_reactions.size()) {
os << ", ";
}
}
os << ")";
return os;
}
void sort(double T9=1.0) {
// Sort based on the evaluation of each reaction's rate function
std::sort(m_reactions.begin(), m_reactions.end(),
[T9](const REACLIBReaction& a, const REACLIBReaction& b) {
return a.calculate_rate(T9) > b.calculate_rate(T9);
});
}
auto begin() {
return m_reactions.begin();
}
auto end() {
return m_reactions.end();
}
bool containsSpecies(const fourdst::atomic::Species &species) const {
for (const auto& reaction : m_reactions) {
if (std::ranges::find(reaction.reactants(), species) != reaction.reactants().end() ||
std::ranges::find(reaction.products(), species) != reaction.products().end()) {
return true;
}
}
return false;
}
[[nodiscard]] const REACLIBReaction& operator[](size_t index) const {
if (index >= m_reactions.size()) {
throw std::out_of_range("Index out of range in REACLIBReactionSet.");
}
return m_reactions[index];
}
[[nodiscard]] std::vector<REACLIBReaction> get_reactions() const {
return m_reactions;
}
[[nodiscard]] auto begin() const {
return m_reactions.cbegin();
}
[[nodiscard]] auto end() const {
return m_reactions.cend();
}
};
static std::unordered_map<std::string, REACLIBReaction> s_all_reaclib_reactions;
static bool s_initialized = false;
inline bool operator==(const REACLIBReaction& lhs, const REACLIBReaction& rhs) {
return lhs.m_id == rhs.m_id;
}
inline bool operator!=(const REACLIBReaction& lhs, const REACLIBReaction& rhs) {
return !(lhs == rhs);
}
inline bool operator==(const REACLIBReactionSet& lhs, const REACLIBReactionSet& rhs) {
if (lhs.size() != rhs.size()) return false;
for (const auto& reaction : lhs) {
if (!rhs.contains(reaction)) return false;
}
return true;
}
inline bool operator!=(const REACLIBReactionSet& lhs, const REACLIBReactionSet& rhs) {
return !(lhs == rhs);
}
}
namespace std {
template<>
struct hash<gridfire::reaclib::REACLIBReaction> {
size_t operator()(const gridfire::reaclib::REACLIBReaction& r) const noexcept {
return std::hash<std::string>()(r.id());
}
};
} // namespace std

View File

@@ -9,12 +9,15 @@
#pragma once
#include "fourdst/composition/atomicSpecies.h"
#include "fourdst/composition/species.h"
#include "reaclib.h"
#include "gridfire/reaction/reaction.h"
namespace gridfire::reaclib {
static std::unordered_map<std::string, reaction::REACLIBReaction> s_all_reaclib_reactions;
static bool s_initialized = false;
inline void initializeAllReaclibReactions() {
using namespace reaction;
if (s_initialized) return; // already initialized
s_initialized = true;
s_all_reaclib_reactions.clear();

View File

@@ -1,5 +1,4 @@
required_headers = [
'gridfire/reaclib.h',
'gridfire/reactions.h',
]
@@ -14,7 +13,6 @@ reaclib_reactions_dep = declare_dependency(
message('✅ GridFire reaclib_reactions dependency declared')
to_install_headers = [
'include/gridfire/reaclib.h',
'include/gridfire/reactions.h',
]
install_headers(to_install_headers, subdir: 'gridfire/gridfire')

View File

@@ -0,0 +1,5 @@
eigen_dep = dependency(
'eigen3',
version : '>=3.3',
fallback : ['eigen', 'eigen_dep']
)

View File

@@ -3,5 +3,7 @@ cmake = import('cmake')
subdir('fourdst')
subdir('boost')
subdir('cppad')
subdir('xxHash')
subdir('eigen')

View File

@@ -0,0 +1,33 @@
xxHash - Extremely Fast Hash algorithm
Header File
Copyright (C) 2012-2021 Yann Collet
BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php)
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
You can contact the author at:
- xxHash homepage: https://www.xxhash.com
- xxHash source repository: https://github.com/Cyan4973/xxHash

View File

@@ -0,0 +1,355 @@
/*
BSD 2-Clause License
constexpr-xxh3 - C++20 constexpr implementation of the XXH3 64-bit variant of xxHash
Copyright (c) 2021-2023, chys <admin@chys.info> <chys87@github>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
This file uses code from Yann Collet's xxHash implementation.
Original xxHash copyright notice:
xxHash - Extremely Fast Hash algorithm
Header File
Copyright (C) 2012-2020 Yann Collet
*/
#pragma once
#include <cstddef>
#include <cstdint>
#include <iterator> // for std::data, std::size
#include <type_traits>
#include <utility>
namespace constexpr_xxh3 {
template <typename T>
concept ByteType = (std::is_integral_v<T> && sizeof(T) == 1)
#if defined __cpp_lib_byte && __cpp_lib_byte >= 201603
|| std::is_same_v<T, std::byte>
#endif
;
template <typename T>
concept BytePtrType = requires (T ptr) {
requires std::is_pointer_v<T>;
requires ByteType<std::remove_cvref_t<decltype(*ptr)>>;
};
template <typename T>
concept BytesType = requires (const T& bytes) {
{ std::data(bytes) };
requires BytePtrType<decltype(std::data(bytes))>;
// -> std::convertible_to is not supported widely enough
{ static_cast<size_t>(std::size(bytes)) };
};
inline constexpr uint32_t swap32(uint32_t x) noexcept {
return ((x << 24) & 0xff000000) | ((x << 8) & 0x00ff0000) |
((x >> 8) & 0x0000ff00) | ((x >> 24) & 0x000000ff);
}
template <typename T>
inline constexpr uint32_t readLE32(const T* ptr) noexcept {
return uint8_t(ptr[0]) | uint32_t(uint8_t(ptr[1])) << 8 |
uint32_t(uint8_t(ptr[2])) << 16 | uint32_t(uint8_t(ptr[3])) << 24;
}
inline constexpr uint64_t swap64(uint64_t x) noexcept {
return ((x << 56) & 0xff00000000000000ULL) |
((x << 40) & 0x00ff000000000000ULL) |
((x << 24) & 0x0000ff0000000000ULL) |
((x << 8) & 0x000000ff00000000ULL) |
((x >> 8) & 0x00000000ff000000ULL) |
((x >> 24) & 0x0000000000ff0000ULL) |
((x >> 40) & 0x000000000000ff00ULL) |
((x >> 56) & 0x00000000000000ffULL);
}
template <typename T>
inline constexpr uint64_t readLE64(const T* ptr) noexcept {
return readLE32(ptr) | uint64_t(readLE32(ptr + 4)) << 32;
}
inline constexpr void writeLE64(uint8_t* dst, uint64_t v) noexcept {
for (int i = 0; i < 8; ++i) dst[i] = uint8_t(v >> (i * 8));
}
inline constexpr uint32_t PRIME32_1 = 0x9E3779B1U;
inline constexpr uint32_t PRIME32_2 = 0x85EBCA77U;
inline constexpr uint32_t PRIME32_3 = 0xC2B2AE3DU;
inline constexpr uint64_t PRIME64_1 = 0x9E3779B185EBCA87ULL;
inline constexpr uint64_t PRIME64_2 = 0xC2B2AE3D27D4EB4FULL;
inline constexpr uint64_t PRIME64_3 = 0x165667B19E3779F9ULL;
inline constexpr uint64_t PRIME64_4 = 0x85EBCA77C2B2AE63ULL;
inline constexpr uint64_t PRIME64_5 = 0x27D4EB2F165667C5ULL;
inline constexpr size_t SECRET_DEFAULT_SIZE = 192;
inline constexpr size_t SECRET_SIZE_MIN = 136;
inline constexpr uint8_t kSecret[SECRET_DEFAULT_SIZE]{
0xb8, 0xfe, 0x6c, 0x39, 0x23, 0xa4, 0x4b, 0xbe, 0x7c, 0x01, 0x81, 0x2c,
0xf7, 0x21, 0xad, 0x1c, 0xde, 0xd4, 0x6d, 0xe9, 0x83, 0x90, 0x97, 0xdb,
0x72, 0x40, 0xa4, 0xa4, 0xb7, 0xb3, 0x67, 0x1f, 0xcb, 0x79, 0xe6, 0x4e,
0xcc, 0xc0, 0xe5, 0x78, 0x82, 0x5a, 0xd0, 0x7d, 0xcc, 0xff, 0x72, 0x21,
0xb8, 0x08, 0x46, 0x74, 0xf7, 0x43, 0x24, 0x8e, 0xe0, 0x35, 0x90, 0xe6,
0x81, 0x3a, 0x26, 0x4c, 0x3c, 0x28, 0x52, 0xbb, 0x91, 0xc3, 0x00, 0xcb,
0x88, 0xd0, 0x65, 0x8b, 0x1b, 0x53, 0x2e, 0xa3, 0x71, 0x64, 0x48, 0x97,
0xa2, 0x0d, 0xf9, 0x4e, 0x38, 0x19, 0xef, 0x46, 0xa9, 0xde, 0xac, 0xd8,
0xa8, 0xfa, 0x76, 0x3f, 0xe3, 0x9c, 0x34, 0x3f, 0xf9, 0xdc, 0xbb, 0xc7,
0xc7, 0x0b, 0x4f, 0x1d, 0x8a, 0x51, 0xe0, 0x4b, 0xcd, 0xb4, 0x59, 0x31,
0xc8, 0x9f, 0x7e, 0xc9, 0xd9, 0x78, 0x73, 0x64, 0xea, 0xc5, 0xac, 0x83,
0x34, 0xd3, 0xeb, 0xc3, 0xc5, 0x81, 0xa0, 0xff, 0xfa, 0x13, 0x63, 0xeb,
0x17, 0x0d, 0xdd, 0x51, 0xb7, 0xf0, 0xda, 0x49, 0xd3, 0x16, 0x55, 0x26,
0x29, 0xd4, 0x68, 0x9e, 0x2b, 0x16, 0xbe, 0x58, 0x7d, 0x47, 0xa1, 0xfc,
0x8f, 0xf8, 0xb8, 0xd1, 0x7a, 0xd0, 0x31, 0xce, 0x45, 0xcb, 0x3a, 0x8f,
0x95, 0x16, 0x04, 0x28, 0xaf, 0xd7, 0xfb, 0xca, 0xbb, 0x4b, 0x40, 0x7e,
};
inline constexpr std::pair<uint64_t, uint64_t> mult64to128(
uint64_t lhs, uint64_t rhs) noexcept {
uint64_t lo_lo = uint64_t(uint32_t(lhs)) * uint32_t(rhs);
uint64_t hi_lo = (lhs >> 32) * uint32_t(rhs);
uint64_t lo_hi = uint32_t(lhs) * (rhs >> 32);
uint64_t hi_hi = (lhs >> 32) * (rhs >> 32);
uint64_t cross = (lo_lo >> 32) + uint32_t(hi_lo) + lo_hi;
uint64_t upper = (hi_lo >> 32) + (cross >> 32) + hi_hi;
uint64_t lower = (cross << 32) | uint32_t(lo_lo);
return {lower, upper};
}
inline constexpr uint64_t mul128_fold64(uint64_t lhs, uint64_t rhs) noexcept {
#if defined __GNUC__ && __WORDSIZE >= 64
// It appears both GCC and Clang support evaluating __int128 as constexpr
auto product = static_cast<unsigned __int128>(lhs) * rhs;
return uint64_t(product >> 64) ^ uint64_t(product);
#else
auto product = mult64to128(lhs, rhs);
return product.first ^ product.second;
#endif
}
inline constexpr uint64_t XXH64_avalanche(uint64_t h) noexcept {
h = (h ^ (h >> 33)) * PRIME64_2;
h = (h ^ (h >> 29)) * PRIME64_3;
return h ^ (h >> 32);
}
inline constexpr uint64_t XXH3_avalanche(uint64_t h) noexcept {
h = (h ^ (h >> 37)) * 0x165667919E3779F9ULL;
return h ^ (h >> 32);
}
inline constexpr uint64_t rrmxmx(uint64_t h, uint64_t len) noexcept {
h ^= ((h << 49) | (h >> 15)) ^ ((h << 24) | (h >> 40));
h *= 0x9FB21C651E98DF25ULL;
h ^= (h >> 35) + len;
h *= 0x9FB21C651E98DF25ULL;
return h ^ (h >> 28);
}
template <typename T, typename S>
constexpr uint64_t mix16B(const T* input, const S* secret,
uint64_t seed) noexcept {
return mul128_fold64(readLE64(input) ^ (readLE64(secret) + seed),
readLE64(input + 8) ^ (readLE64(secret + 8) - seed));
}
inline constexpr size_t STRIPE_LEN = 64;
inline constexpr size_t SECRET_CONSUME_RATE = 8;
inline constexpr size_t ACC_NB = STRIPE_LEN / sizeof(uint64_t);
template <typename T, typename S>
constexpr void accumulate_512(uint64_t* acc, const T* input,
const S* secret) noexcept {
for (size_t i = 0; i < ACC_NB; i++) {
uint64_t data_val = readLE64(input + 8 * i);
uint64_t data_key = data_val ^ readLE64(secret + i * 8);
acc[i ^ 1] += data_val;
acc[i] += uint32_t(data_key) * (data_key >> 32);
}
}
template <typename T, typename S>
constexpr uint64_t hashLong_64b_internal(const T* input, size_t len,
const S* secret,
size_t secretSize) noexcept {
uint64_t acc[ACC_NB]{PRIME32_3, PRIME64_1, PRIME64_2, PRIME64_3,
PRIME64_4, PRIME32_2, PRIME64_5, PRIME32_1};
size_t nbStripesPerBlock = (secretSize - STRIPE_LEN) / SECRET_CONSUME_RATE;
size_t block_len = STRIPE_LEN * nbStripesPerBlock;
size_t nb_blocks = (len - 1) / block_len;
for (size_t n = 0; n < nb_blocks; n++) {
for (size_t i = 0; i < nbStripesPerBlock; i++)
accumulate_512(acc, input + n * block_len + i * STRIPE_LEN,
secret + i * SECRET_CONSUME_RATE);
for (size_t i = 0; i < ACC_NB; i++)
acc[i] = (acc[i] ^ (acc[i] >> 47) ^
readLE64(secret + secretSize - STRIPE_LEN + 8 * i)) *
PRIME32_1;
}
size_t nbStripes = ((len - 1) - (block_len * nb_blocks)) / STRIPE_LEN;
for (size_t i = 0; i < nbStripes; i++)
accumulate_512(acc, input + nb_blocks * block_len + i * STRIPE_LEN,
secret + i * SECRET_CONSUME_RATE);
accumulate_512(acc, input + len - STRIPE_LEN,
secret + secretSize - STRIPE_LEN - 7);
uint64_t result = len * PRIME64_1;
for (size_t i = 0; i < 4; i++)
result +=
mul128_fold64(acc[2 * i] ^ readLE64(secret + 11 + 16 * i),
acc[2 * i + 1] ^ readLE64(secret + 11 + 16 * i + 8));
return XXH3_avalanche(result);
}
template <typename T, typename S, typename HashLong>
constexpr uint64_t XXH3_64bits_internal(const T* input, size_t len,
uint64_t seed, const S* secret,
size_t secretLen,
HashLong f_hashLong) noexcept {
if (len == 0) {
return XXH64_avalanche(seed ^
(readLE64(secret + 56) ^ readLE64(secret + 64)));
} else if (len < 4) {
uint64_t keyed = ((uint32_t(uint8_t(input[0])) << 16) |
(uint32_t(uint8_t(input[len >> 1])) << 24) |
uint8_t(input[len - 1]) | (uint32_t(len) << 8)) ^
((readLE32(secret) ^ readLE32(secret + 4)) + seed);
return XXH64_avalanche(keyed);
} else if (len <= 8) {
uint64_t keyed =
(readLE32(input + len - 4) + (uint64_t(readLE32(input)) << 32)) ^
((readLE64(secret + 8) ^ readLE64(secret + 16)) -
(seed ^ (uint64_t(swap32(uint32_t(seed))) << 32)));
return rrmxmx(keyed, len);
} else if (len <= 16) {
uint64_t input_lo =
readLE64(input) ^
((readLE64(secret + 24) ^ readLE64(secret + 32)) + seed);
uint64_t input_hi =
readLE64(input + len - 8) ^
((readLE64(secret + 40) ^ readLE64(secret + 48)) - seed);
uint64_t acc =
len + swap64(input_lo) + input_hi + mul128_fold64(input_lo, input_hi);
return XXH3_avalanche(acc);
} else if (len <= 128) {
uint64_t acc = len * PRIME64_1;
size_t secret_off = 0;
for (size_t i = 0, j = len; j > i; i += 16, j -= 16) {
acc += mix16B(input + i, secret + secret_off, seed);
acc += mix16B(input + j - 16, secret + secret_off + 16, seed);
secret_off += 32;
}
return XXH3_avalanche(acc);
} else if (len <= 240) {
uint64_t acc = len * PRIME64_1;
for (size_t i = 0; i < 128; i += 16)
acc += mix16B(input + i, secret + i, seed);
acc = XXH3_avalanche(acc);
for (size_t i = 128; i < len / 16 * 16; i += 16)
acc += mix16B(input + i, secret + (i - 128) + 3, seed);
acc += mix16B(input + len - 16, secret + SECRET_SIZE_MIN - 17, seed);
return XXH3_avalanche(acc);
} else {
return f_hashLong(input, len, seed, secret, secretLen);
}
}
template <BytesType Bytes>
constexpr size_t bytes_size(const Bytes& bytes) noexcept {
return std::size(bytes);
}
template <ByteType T, size_t N>
constexpr size_t bytes_size(T (&)[N]) noexcept {
return (N ? N - 1 : 0);
}
/// Basic interfaces
template <ByteType T>
consteval uint64_t XXH3_64bits_const(const T* input, size_t len) noexcept {
return XXH3_64bits_internal(
input, len, 0, kSecret, sizeof(kSecret),
[](const T* input, size_t len, uint64_t, const void*,
size_t) constexpr noexcept {
return hashLong_64b_internal(input, len, kSecret, sizeof(kSecret));
});
}
template <ByteType T, ByteType S>
consteval uint64_t XXH3_64bits_withSecret_const(const T* input, size_t len,
const S* secret,
size_t secretSize) noexcept {
return XXH3_64bits_internal(
input, len, 0, secret, secretSize,
[](const T* input, size_t len, uint64_t, const S* secret,
size_t secretLen) constexpr noexcept {
return hashLong_64b_internal(input, len, secret, secretLen);
});
}
template <ByteType T>
consteval uint64_t XXH3_64bits_withSeed_const(const T* input, size_t len,
uint64_t seed) noexcept {
if (seed == 0) return XXH3_64bits_const(input, len);
return XXH3_64bits_internal(
input, len, seed, kSecret, sizeof(kSecret),
[](const T* input, size_t len, uint64_t seed, const void*,
size_t) constexpr noexcept {
uint8_t secret[SECRET_DEFAULT_SIZE];
for (size_t i = 0; i < SECRET_DEFAULT_SIZE; i += 16) {
writeLE64(secret + i, readLE64(kSecret + i) + seed);
writeLE64(secret + i + 8, readLE64(kSecret + i + 8) - seed);
}
return hashLong_64b_internal(input, len, secret, sizeof(secret));
});
}
/// Convenient interfaces
template <BytesType Bytes>
consteval uint64_t XXH3_64bits_const(const Bytes& input) noexcept {
return XXH3_64bits_const(std::data(input), bytes_size(input));
}
template <BytesType Bytes, BytesType Secret>
consteval uint64_t XXH3_64bits_withSecret_const(const Bytes& input,
const Secret& secret) noexcept {
return XXH3_64bits_withSecret_const(std::data(input), bytes_size(input),
std::data(secret), bytes_size(secret));
}
template <BytesType Bytes>
consteval uint64_t XXH3_64bits_withSeed_const(const Bytes& input,
uint64_t seed) noexcept {
return XXH3_64bits_withSeed_const(std::data(input), bytes_size(input), seed);
}
} // namespace constexpr_xxh3

View File

@@ -0,0 +1,202 @@
// //////////////////////////////////////////////////////////
// xxhash64.h
// Copyright (c) 2016 Stephan Brumme. All rights reserved.
// see http://create.stephan-brumme.com/disclaimer.html
//
#pragma once
#include <stdint.h> // for uint32_t and uint64_t
/// XXHash (64 bit), based on Yann Collet's descriptions, see http://cyan4973.github.io/xxHash/
/** How to use:
uint64_t myseed = 0;
XXHash64 myhash(myseed);
myhash.add(pointerToSomeBytes, numberOfBytes);
myhash.add(pointerToSomeMoreBytes, numberOfMoreBytes); // call add() as often as you like to ...
// and compute hash:
uint64_t result = myhash.hash();
// or all of the above in one single line:
uint64_t result2 = XXHash64::hash(mypointer, numBytes, myseed);
Note: my code is NOT endian-aware !
**/
class XXHash64
{
public:
/// create new XXHash (64 bit)
/** @param seed your seed value, even zero is a valid seed **/
explicit XXHash64(uint64_t seed)
{
state[0] = seed + Prime1 + Prime2;
state[1] = seed + Prime2;
state[2] = seed;
state[3] = seed - Prime1;
bufferSize = 0;
totalLength = 0;
}
/// add a chunk of bytes
/** @param input pointer to a continuous block of data
@param length number of bytes
@return false if parameters are invalid / zero **/
bool add(const void* input, uint64_t length)
{
// no data ?
if (!input || length == 0)
return false;
totalLength += length;
// byte-wise access
const unsigned char* data = (const unsigned char*)input;
// unprocessed old data plus new data still fit in temporary buffer ?
if (bufferSize + length < MaxBufferSize)
{
// just add new data
while (length-- > 0)
buffer[bufferSize++] = *data++;
return true;
}
// point beyond last byte
const unsigned char* stop = data + length;
const unsigned char* stopBlock = stop - MaxBufferSize;
// some data left from previous update ?
if (bufferSize > 0)
{
// make sure temporary buffer is full (16 bytes)
while (bufferSize < MaxBufferSize)
buffer[bufferSize++] = *data++;
// process these 32 bytes (4x8)
process(buffer, state[0], state[1], state[2], state[3]);
}
// copying state to local variables helps optimizer A LOT
uint64_t s0 = state[0], s1 = state[1], s2 = state[2], s3 = state[3];
// 32 bytes at once
while (data <= stopBlock)
{
// local variables s0..s3 instead of state[0]..state[3] are much faster
process(data, s0, s1, s2, s3);
data += 32;
}
// copy back
state[0] = s0; state[1] = s1; state[2] = s2; state[3] = s3;
// copy remainder to temporary buffer
bufferSize = stop - data;
for (uint64_t i = 0; i < bufferSize; i++)
buffer[i] = data[i];
// done
return true;
}
/// get current hash
/** @return 64 bit XXHash **/
uint64_t hash() const
{
// fold 256 bit state into one single 64 bit value
uint64_t result;
if (totalLength >= MaxBufferSize)
{
result = rotateLeft(state[0], 1) +
rotateLeft(state[1], 7) +
rotateLeft(state[2], 12) +
rotateLeft(state[3], 18);
result = (result ^ processSingle(0, state[0])) * Prime1 + Prime4;
result = (result ^ processSingle(0, state[1])) * Prime1 + Prime4;
result = (result ^ processSingle(0, state[2])) * Prime1 + Prime4;
result = (result ^ processSingle(0, state[3])) * Prime1 + Prime4;
}
else
{
// internal state wasn't set in add(), therefore original seed is still stored in state2
result = state[2] + Prime5;
}
result += totalLength;
// process remaining bytes in temporary buffer
const unsigned char* data = buffer;
// point beyond last byte
const unsigned char* stop = data + bufferSize;
// at least 8 bytes left ? => eat 8 bytes per step
for (; data + 8 <= stop; data += 8)
result = rotateLeft(result ^ processSingle(0, *(uint64_t*)data), 27) * Prime1 + Prime4;
// 4 bytes left ? => eat those
if (data + 4 <= stop)
{
result = rotateLeft(result ^ (*(uint32_t*)data) * Prime1, 23) * Prime2 + Prime3;
data += 4;
}
// take care of remaining 0..3 bytes, eat 1 byte per step
while (data != stop)
result = rotateLeft(result ^ (*data++) * Prime5, 11) * Prime1;
// mix bits
result ^= result >> 33;
result *= Prime2;
result ^= result >> 29;
result *= Prime3;
result ^= result >> 32;
return result;
}
/// combine constructor, add() and hash() in one static function (C style)
/** @param input pointer to a continuous block of data
@param length number of bytes
@param seed your seed value, e.g. zero is a valid seed
@return 64 bit XXHash **/
static uint64_t hash(const void* input, uint64_t length, uint64_t seed)
{
XXHash64 hasher(seed);
hasher.add(input, length);
return hasher.hash();
}
private:
/// magic constants :-)
static const uint64_t Prime1 = 11400714785074694791ULL;
static const uint64_t Prime2 = 14029467366897019727ULL;
static const uint64_t Prime3 = 1609587929392839161ULL;
static const uint64_t Prime4 = 9650029242287828579ULL;
static const uint64_t Prime5 = 2870177450012600261ULL;
/// temporarily store up to 31 bytes between multiple add() calls
static const uint64_t MaxBufferSize = 31+1;
uint64_t state[4];
unsigned char buffer[MaxBufferSize];
uint64_t bufferSize;
uint64_t totalLength;
/// rotate bits, should compile to a single CPU instruction (ROL)
static inline uint64_t rotateLeft(uint64_t x, unsigned char bits)
{
return (x << bits) | (x >> (64 - bits));
}
/// process a single 64 bit value
static inline uint64_t processSingle(uint64_t previous, uint64_t input)
{
return rotateLeft(previous + input * Prime2, 31) * Prime1;
}
/// process a block of 4x4 bytes, this is the main part of the XXHash32 algorithm
static inline void process(const void* data, uint64_t& state0, uint64_t& state1, uint64_t& state2, uint64_t& state3)
{
const uint64_t* block = (const uint64_t*) data;
state0 = processSingle(state0, block[0]);
state1 = processSingle(state1, block[1]);
state2 = processSingle(state2, block[2]);
state3 = processSingle(state3, block[3]);
}
};

View File

@@ -0,0 +1,4 @@
xxhash_dep = declare_dependency(
include_directories: include_directories('include')
)

View File

@@ -24,6 +24,31 @@ project('GridFire', 'cpp', version: '0.0.1a', default_options: ['cpp_std=c++23']
add_project_arguments('-fvisibility=default', language: 'cpp')
# We disable these because of CppAD
add_project_arguments('-Wno-bitwise-instead-of-logical', language: 'cpp')
llevel = get_option('log_level')
logbase='QUILL_COMPILE_ACTIVE_LOG_LEVEL_'
if (llevel == 'traceL3')
log_argument = logbase + 'TRACE_L3'
elif (llevel == 'traceL2')
log_argument = logbase + 'TRACE_L2'
elif (llevel == 'traceL1')
log_argument = logbase + 'TRACE_L1'
elif (llevel == 'debug')
log_argument = logbase + 'DEBUG'
elif (llevel == 'info')
log_argument = logbase + 'INFO'
elif (llevel == 'warning')
log_argument = logbase + 'WARNING'
elif (llevel == 'error')
log_argument = logbase + 'ERROR'
elif (llevel == 'critical')
log_argument = logbase + 'CRITICAL'
endif
log_argument = '-DQUILL_COMPILE_ACTIVE_LOG_LEVEL=' + log_argument
add_project_arguments(log_argument, language: 'cpp')
cpp = meson.get_compiler('cpp')
subdir('build-config')

View File

@@ -1,20 +1 @@
# Build options for the project
# Usage:
# -Doption_name=value
# -Dbuild_tests=true (default) build tests
# -Duser_mode=false (default) enable user mode (set mode = 0) If user mode is enabled then the optimization level is set to 3 and the build type is set to release
# -Dbuild_python=true (default) build Python bindings
option('build_tests', type: 'boolean', value: true, description: 'Build tests')
option('user_mode', type: 'boolean', value: false, description: 'Enable user mode (set mode = 0)')
option('build_python', type: 'boolean', value: true, description: 'Build Python bindings')
option(
'config_error_handling',
type: 'combo',
choices: [ 'none', 'warn', 'harsh' ],
value: 'none',
description: 'What to do if a config file fails to load: silent (none), warning (warn), or error (harsh)'
)
option('build_post_run_utils', type: 'boolean', value: true, description: 'Build Helper Utilities')
option('build_debug_utils', type: 'boolean', value: true, description: 'Build Debug Utilities')
option('log_level', type: 'combo', choices: ['traceL3', 'traceL2', 'traceL1', 'debug', 'info', 'warning', 'error', 'critial'], value: 'info', description: 'Set the log level for the GridFire library')

View File

@@ -0,0 +1,66 @@
#pragma once
#include "gridfire/network.h" // For NetIn, NetOut
#include "../reaction/reaction.h"
#include "fourdst/composition/composition.h"
#include "fourdst/config/config.h"
#include "fourdst/logging/logging.h"
#include <vector>
#include <unordered_map>
namespace gridfire {
template<typename T>
concept IsArithmeticOrAD = std::is_same_v<T, double> || std::is_same_v<T, CppAD::AD<double>>;
template <IsArithmeticOrAD T>
struct StepDerivatives {
std::vector<T> dydt; ///< Derivatives of abundances.
T nuclearEnergyGenerationRate = T(0.0); ///< Specific energy generation rate.
};
class Engine {
public:
virtual ~Engine() = default;
virtual const std::vector<fourdst::atomic::Species>& getNetworkSpecies() const = 0;
virtual StepDerivatives<double> calculateRHSAndEnergy(
const std::vector<double>& Y,
double T9,
double rho
) const = 0;
};
class DynamicEngine : public Engine {
public:
virtual void generateJacobianMatrix(
const std::vector<double>& Y,
double T9, double rho
) = 0;
virtual double getJacobianMatrixEntry(
int i,
int j
) const = 0;
virtual void generateStoichiometryMatrix() = 0;
virtual int getStoichiometryMatrixEntry(
int speciesIndex,
int reactionIndex
) const = 0;
virtual double calculateMolarReactionFlow(
const reaction::Reaction& reaction,
const std::vector<double>& Y,
double T9,
double rho
) const = 0;
virtual const reaction::REACLIBLogicalReactionSet& getNetworkReactions() const = 0;
virtual std::unordered_map<fourdst::atomic::Species, double> getSpeciesTimescales(
const std::vector<double>& Y,
double T9,
double rho
) const = 0;
};
}

View File

@@ -0,0 +1,55 @@
#pragma once
#include "gridfire/engine/engine_abstract.h"
#include "fourdst/composition/atomicSpecies.h"
namespace gridfire {
class CulledEngine final : public DynamicEngine {
public:
explicit CulledEngine(DynamicEngine& baseEngine);
const std::vector<fourdst::atomic::Species>& getNetworkSpecies() const override;
StepDerivatives<double> calculateRHSAndEnergy(
const std::vector<double> &Y,
double T9,
double rho
) const override;
void generateJacobianMatrix(
const std::vector<double> &Y,
double T9,
double rho
) override;
double getJacobianMatrixEntry(
int i,
int j
) const override;
void generateStoichiometryMatrix() override;
int getStoichiometryMatrixEntry(
int speciesIndex,
int reactionIndex
) const override;
double calculateMolarReactionFlow(
const reaction::Reaction &reaction,
const std::vector<double> &Y,
double T9,
double rho
) const override;
const reaction::REACLIBLogicalReactionSet& getNetworkReactions() const override;
std::unordered_map<fourdst::atomic::Species, double> getSpeciesTimescales(
const std::vector<double> &Y,
double T9,
double rho
) const override;
private:
DynamicEngine& m_baseEngine;
std::unordered_map<fourdst::atomic::Species, size_t> m_fullSpeciesToIndexMap;
std::vector<fourdst::atomic::Species> m_culledSpecies;
private:
std::unordered_map<fourdst::atomic::Species, size_t> populatedSpeciesToIndexMap;
std::vector<fourdst::atomic::Species> determineCullableSpecies;
};
}

View File

@@ -0,0 +1,277 @@
#pragma once
#include "fourdst/composition/atomicSpecies.h"
#include "fourdst/composition/composition.h"
#include "fourdst/logging/logging.h"
#include "fourdst/config/config.h"
#include "gridfire/network.h"
#include "gridfire/reaction/reaction.h"
#include "gridfire/engine/engine_abstract.h"
#include <string>
#include <unordered_map>
#include <vector>
#include <boost/numeric/ublas/matrix_sparse.hpp>
#include "cppad/cppad.hpp"
// PERF: The function getNetReactionStoichiometry returns a map of species to their stoichiometric coefficients for a given reaction.
// 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 {
typedef CppAD::AD<double> ADDouble; ///< Alias for CppAD AD type for double precision.
using fourdst::config::Config;
using fourdst::logging::LogManager;
using fourdst::constant::Constants;
static constexpr double MIN_DENSITY_THRESHOLD = 1e-18;
static constexpr double MIN_ABUNDANCE_THRESHOLD = 1e-18;
static constexpr double MIN_JACOBIAN_THRESHOLD = 1e-24;
class GraphEngine final : public DynamicEngine{
public:
explicit GraphEngine(const fourdst::composition::Composition &composition);
explicit GraphEngine(reaction::REACLIBLogicalReactionSet reactions);
StepDerivatives<double> calculateRHSAndEnergy(
const std::vector<double>& Y,
const double T9,
const double rho
) const override;
void generateJacobianMatrix(
const std::vector<double>& Y,
const double T9,
const double rho
) override;
void generateStoichiometryMatrix() override;
double calculateMolarReactionFlow(
const reaction::Reaction& reaction,
const std::vector<double>&Y,
const double T9,
const double rho
) const override;
[[nodiscard]] const std::vector<fourdst::atomic::Species>& getNetworkSpecies() const override;
[[nodiscard]] const reaction::REACLIBLogicalReactionSet& getNetworkReactions() const override;
[[nodiscard]] double getJacobianMatrixEntry(
const int i,
const int j
) const override;
[[nodiscard]] std::unordered_map<fourdst::atomic::Species, int> getNetReactionStoichiometry(
const reaction::Reaction& reaction
) const;
[[nodiscard]] int getStoichiometryMatrixEntry(
const int speciesIndex,
const int reactionIndex
) const override;
[[nodiscard]] std::unordered_map<fourdst::atomic::Species, double> getSpeciesTimescales(
const std::vector<double>& Y,
double T9,
double rho
) const override;
[[nodiscard]] bool involvesSpecies(
const fourdst::atomic::Species& species
) const;
void exportToDot(
const std::string& filename
) const;
void exportToCSV(
const std::string& filename
) const;
private:
reaction::REACLIBLogicalReactionSet m_reactions; ///< Set of REACLIB reactions in the network.
std::unordered_map<std::string_view, reaction::Reaction*> m_reactionIDMap; ///< Map from reaction ID to REACLIBReaction. //PERF: This makes copies of REACLIBReaction and could be a performance bottleneck.
std::vector<fourdst::atomic::Species> m_networkSpecies; ///< Vector of unique species in the network.
std::unordered_map<std::string_view, fourdst::atomic::Species> m_networkSpeciesMap; ///< Map from species name to Species object.
std::unordered_map<fourdst::atomic::Species, size_t> m_speciesToIndexMap; ///< Map from species to their index in the stoichiometry matrix.
boost::numeric::ublas::compressed_matrix<int> m_stoichiometryMatrix; ///< Stoichiometry matrix (species x reactions).
boost::numeric::ublas::compressed_matrix<double> m_jacobianMatrix; ///< Jacobian matrix (species x species).
CppAD::ADFun<double> m_rhsADFun; ///< CppAD function for the right-hand side of the ODE.
Config& m_config = Config::getInstance();
Constants& m_constants = Constants::getInstance(); ///< Access to physical constants.
quill::Logger* m_logger = LogManager::getInstance().getLogger("log");
private:
void syncInternalMaps();
void collectNetworkSpecies();
void populateReactionIDMap();
void populateSpeciesToIndexMap();
void reserveJacobianMatrix();
void recordADTape();
[[nodiscard]] bool validateConservation() const;
void validateComposition(
const fourdst::composition::Composition &composition,
double culling,
double T9
);
template <IsArithmeticOrAD T>
T calculateMolarReactionFlow(
const reaction::Reaction &reaction,
const std::vector<T> &Y,
const T T9,
const T rho
) const;
template<IsArithmeticOrAD T>
StepDerivatives<T> calculateAllDerivatives(
const std::vector<T> &Y_in,
T T9,
T rho
) const;
StepDerivatives<double> calculateAllDerivatives(
const std::vector<double>& Y_in,
const double T9,
const double rho
) const;
StepDerivatives<ADDouble> calculateAllDerivatives(
const std::vector<ADDouble>& Y_in,
const ADDouble T9,
const ADDouble rho
) const;
};
template<IsArithmeticOrAD T>
StepDerivatives<T> GraphEngine::calculateAllDerivatives(
const std::vector<T> &Y_in, T T9, T rho) const {
// --- Setup output derivatives structure ---
StepDerivatives<T> result;
result.dydt.resize(m_networkSpecies.size(), static_cast<T>(0.0));
// --- AD Pre-setup (flags to control conditionals in an AD safe / branch aware manner) ---
// ----- Constants for AD safe calculations ---
const T zero = static_cast<T>(0.0);
const T one = static_cast<T>(1.0);
// ----- Initialize variables for molar concentration product and thresholds ---
// Note: the logic here is that we use CppAD::CondExprLt to test thresholds and if they are less we set the flag
// to zero so that the final returned reaction flow is 0. This is as opposed to standard if statements
// which create branches that break the AD tape.
const T rho_threshold = static_cast<T>(MIN_DENSITY_THRESHOLD);
// --- Check if the density is below the threshold where we ignore reactions ---
T threshold_flag = CppAD::CondExpLt(rho, rho_threshold, zero, one); // If rho < threshold, set flag to 0
std::vector<T> Y = Y_in;
for (size_t i = 0; i < m_networkSpecies.size(); ++i) {
// We use CppAD::CondExpLt to handle AD taping and prevent branching
// Note that while this is syntactically more complex this is equivalent to
// if (Y[i] < 0) {Y[i] = 0;}
// The issue is that this would introduce a branch which would require the auto diff tape to be re-recorded
// each timestep, which is very inefficient.
Y[i] = CppAD::CondExpLt(Y[i], zero, zero, Y[i]); // Ensure no negative abundances
}
const T u = static_cast<T>(m_constants.get("u").value); // Atomic mass unit in grams
const T N_A = static_cast<T>(m_constants.get("N_a").value); // Avogadro's number in mol^-1
const T c = static_cast<T>(m_constants.get("c").value); // Speed of light in cm/s
// --- SINGLE LOOP OVER ALL REACTIONS ---
for (size_t reactionIndex = 0; reactionIndex < m_reactions.size(); ++reactionIndex) {
const auto& reaction = m_reactions[reactionIndex];
// 1. Calculate reaction rate
const T molarReactionFlow = calculateMolarReactionFlow<T>(reaction, Y, T9, rho);
// 2. Use the rate to update all relevant species derivatives (dY/dt)
for (size_t speciesIndex = 0; speciesIndex < m_networkSpecies.size(); ++speciesIndex) {
const T nu_ij = static_cast<T>(m_stoichiometryMatrix(speciesIndex, reactionIndex));
result.dydt[speciesIndex] += threshold_flag * nu_ij * molarReactionFlow / rho;
}
}
T massProductionRate = static_cast<T>(0.0); // [mol][s^-1]
for (const auto& [species, index] : m_speciesToIndexMap) {
massProductionRate += result.dydt[index] * species.mass() * u;
}
result.nuclearEnergyGenerationRate = -massProductionRate * N_A * c * c; // [cm^2][s^-3] = [erg][s^-1][g^-1]
return result;
}
template <IsArithmeticOrAD T>
T GraphEngine::calculateMolarReactionFlow(
const reaction::Reaction &reaction,
const std::vector<T> &Y,
const T T9,
const T rho
) const {
// --- Pre-setup (flags to control conditionals in an AD safe / branch aware manner) ---
// ----- Constants for AD safe calculations ---
const T zero = static_cast<T>(0.0);
const T one = static_cast<T>(1.0);
// ----- Initialize variables for molar concentration product and thresholds ---
// Note: the logic here is that we use CppAD::CondExprLt to test thresholds and if they are less we set the flag
// to zero so that the final returned reaction flow is 0. This is as opposed to standard if statements
// which create branches that break the AD tape.
const T Y_threshold = static_cast<T>(MIN_ABUNDANCE_THRESHOLD);
T threshold_flag = one;
// --- Calculate the molar reaction rate (in units of [s^-1][cm^3(N-1)][mol^(1-N)] for N reactants) ---
const T k_reaction = reaction.calculate_rate(T9);
// --- Cound the number of each reactant species to account for species multiplicity ---
std::unordered_map<std::string, int> reactant_counts;
reactant_counts.reserve(reaction.reactants().size());
for (const auto& reactant : reaction.reactants()) {
reactant_counts[std::string(reactant.name())]++;
}
// --- Accumulator for the molar concentration ---
auto molar_concentration_product = static_cast<T>(1.0);
// --- Loop through each unique reactant species and calculate the molar concentration for that species then multiply that into the accumulator ---
for (const auto& [species_name, count] : reactant_counts) {
// --- Resolve species to molar abundance ---
// PERF: Could probably optimize out this lookup
const auto species_it = m_speciesToIndexMap.find(m_networkSpeciesMap.at(species_name));
const size_t species_index = species_it->second;
const T Yi = Y[species_index];
// --- Check if the species abundance is below the threshold where we ignore reactions ---
threshold_flag *= CppAD::CondExpLt(Yi, Y_threshold, zero, one);
// --- Convert from molar abundance to molar concentration ---
T molar_concentration = Yi * rho;
// --- If count is > 1 , we need to raise the molar concentration to the power of count since there are really count bodies in that reaction ---
molar_concentration_product *= CppAD::pow(molar_concentration, static_cast<T>(count)); // ni^count
// --- Apply factorial correction for identical reactions ---
if (count > 1) {
molar_concentration_product /= static_cast<T>(std::tgamma(static_cast<double>(count + 1))); // Gamma function for factorial
}
}
// --- Final reaction flow calculation [mol][s^-1][cm^-3] ---
// Note: If the threshold flag ever gets set to zero this will return zero.
// This will result basically in multiple branches being written to the AD tape, which will make
// the tape more expensive to record, but it will also mean that we only need to record it once for
// the entire network.
return molar_concentration_product * k_reaction * threshold_flag;
}
};

View File

@@ -1,571 +0,0 @@
#pragma once
#include "fourdst/composition/atomicSpecies.h"
#include "fourdst/composition/composition.h"
#include "gridfire/network.h"
#include "gridfire/reaclib.h"
#include <string>
#include <unordered_map>
#include <vector>
#include <boost/numeric/ublas/matrix_sparse.hpp>
#include "cppad/cppad.hpp"
#include "quill/LogMacros.h"
// PERF: The function getNetReactionStoichiometry returns a map of species to their stoichiometric coefficients for a given reaction.
// 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.
/**
* @file netgraph.h
* @brief Defines the GraphNetwork class for representing and evolving nuclear reaction networks as a heavily connected graph.
*
* This file provides the GraphNetwork class, which implements a graph-based nuclear reaction network
* using REACLIB reactions and supports both stiff and non-stiff ODE integration. The network is constructed
* from a composition and can be queried for species, reactions, and stoichiometry.
*
* This is a general nuclear reaction network; however, it does not currently manage reverse reactions, weak reactions,
* or other reactions which become much more relevant for more extreme astrophysical sources.
*
* @see gridfire::GraphNetwork
*/
namespace gridfire {
/**
* @brief Concept to check if a type is either double or CppAD::AD<double>.
*
* Used to enable templated functions for both arithmetic and automatic differentiation types.
*/
template<typename T>
concept IsArithmeticOrAD = std::is_same_v<T, double> || std::is_same_v<T, CppAD::AD<double>>;
/// Minimum density threshold (g/cm^3) below which reactions are ignored.
static constexpr double MIN_DENSITY_THRESHOLD = 1e-18;
/// Minimum abundance threshold below which reactions are ignored.
static constexpr double MIN_ABUNDANCE_THRESHOLD = 1e-18;
/// Minimum Jacobian entry threshold for sparsity.
static constexpr double MIN_JACOBIAN_THRESHOLD = 1e-24;
/**
* @brief Graph-based nuclear reaction network using REACLIB reactions.
*
* GraphNetwork constructs a reaction network from a given composition, builds the associated
* stoichiometry and Jacobian matrices, and provides methods for evaluating the network's evolution
* using ODE solvers. It supports both stiff and non-stiff integration and can be queried for
* network species, reactions, and stoichiometry.
*
* @note Currently does not handle reverse reactions, weak reactions, or other complex reaction types. These will be added in future versions.
*
* Example usage:
* @code
* serif::composition::Composition comp = ...;
* serif::network::GraphNetwork net(comp);
* serif::network::NetIn input;
* input.composition = comp;
* input.temperature = 1.5e9;
* input.density = 1e6;
* input.tMax = 1.0;
* input.dt0 = 1e-3;
* serif::network::NetOut result = net.evaluate(input);
* @endcode
*/
class GraphNetwork final : public Network {
public:
/**
* @brief Construct a GraphNetwork from a composition.
* @param composition The composition specifying the initial isotopic abundances.
*/
explicit GraphNetwork(const fourdst::composition::Composition &composition);
/**
* @brief Construct a GraphNetwork from a composition with culling and temperature.
* @param composition The composition specifying the initial isotopic abundances.
* @param cullingThreshold specific reaction rate threshold to cull reactions below.
* @param T9 Temperature in units of 10^9 K where culling is evaluated at.
*
* @see serif::network::build_reaclib_nuclear_network
*/
explicit GraphNetwork(const fourdst::composition::Composition &composition,
const double cullingThreshold, const double T9);
explicit GraphNetwork(const reaclib::REACLIBReactionSet& reactions);
/**
* @brief Evolve the network for the given input conditions.
*
* This is the primary entry point for users of GridFire. This function integrates the network ODEs using either a stiff or non-stiff solver,
* depending on the detected stiffness of the system.
*
* @param netIn The input structure containing composition, temperature, density, and integration parameters.
* @return NetOut The output structure containing the final composition, number of steps, and energy.
*
* Example:
* @code
* serif::network::NetIn input;
* // ... set up input ...
* serif::network::NetOut result = net.evaluate(input);
* @endcode
*/
NetOut evaluate(const NetIn &netIn) override;
/**
* @brief Get the vector of unique species in the network.
* @return Reference to the vector of species.
*
* Example:
* @code
* const auto& species = net.getNetworkSpecies();
* for (const auto& sp : species) {
* std::cout << sp.name() << std::endl;
* }
* @endcode
*/
[[nodiscard]] const std::vector<fourdst::atomic::Species>& getNetworkSpecies() const;
/**
* @brief Get the set of REACLIB reactions in the network.
* @return Reference to the set of reactions.
*/
[[nodiscard]] const reaclib::REACLIBReactionSet& getNetworkReactions() const;
/**
* @brief Get the net stoichiometric coefficients for all species in a reaction.
*
* Returns a map from species to their net stoichiometric coefficient (products minus reactants).
*
* @param reaction The REACLIB reaction to analyze.
* @return Map from species to stoichiometric coefficient.
*
* @throws std::runtime_error If a species in the reaction is not found in the network.
*
* Example:
* @code
* for (const auto& reaction : net.getNetworkReactions()) {
* auto stoichiometry = net.getNetReactionStoichiometry(reaction);
* // ...
* }
* @endcode
*/
[[nodiscard]] std::unordered_map<fourdst::atomic::Species, int> getNetReactionStoichiometry(
const reaclib::REACLIBReaction &reaction) const;
/**
* @brief Check if a species is present in the network.
* @param species The species to check.
* @return True if the species is present, false otherwise.
*
* Example:
* @code
* if (net.involvesSpecies(mySpecies)) { ... }
* @endcode
*/
[[nodiscard]] bool involvesSpecies(const fourdst::atomic::Species& species) const;
/**
* @brief Detect cycles in the reaction network (not implemented).
* @return Vector of cycles, each represented as a vector of reaction IDs.
*
* @note This is not yet implemented; however, it will allow for easy detection of things like the CNO cycle.
* @deprecated Not implemented.
*/
[[deprecated("not implimented")]] [[nodiscard]] std::vector<std::vector<std::string>> detectCycles() const = delete;
/**
* @brief Perform a topological sort of the reactions (not implemented).
* @return Vector of reaction IDs in topologically sorted order.
* @deprecated Not implemented.
*/
[[deprecated("not implimented")]] [[nodiscard]] std::vector<std::string> topologicalSortReactions() const = delete;
/**
* @brief Export the network to a DOT file for visualization.
* @param filename The name of the output DOT file.
*/
void exportToDot(const std::string& filename) const;
private:
reaclib::REACLIBReactionSet m_reactions; ///< Set of REACLIB reactions in the network.
std::unordered_map<std::string_view, const reaclib::REACLIBReaction> m_reactionIDMap; ///< Map from reaction ID to REACLIBReaction. //PERF: This makes copies of REACLIBReaction and could be a performance bottleneck.
std::vector<fourdst::atomic::Species> m_networkSpecies; ///< Vector of unique species in the network.
std::unordered_map<std::string_view, fourdst::atomic::Species> m_networkSpeciesMap; ///< Map from species name to Species object.
std::unordered_map<fourdst::atomic::Species, size_t> m_speciesToIndexMap; ///< Map from species to their index in the stoichiometry matrix.
boost::numeric::ublas::compressed_matrix<int> m_stoichiometryMatrix; ///< Stoichiometry matrix (species x reactions).
boost::numeric::ublas::compressed_matrix<double> m_jacobianMatrix; ///< Jacobian matrix (species x species).
CppAD::ADFun<double> m_rhsADFun; ///< CppAD function for the right-hand side of the ODE.
/**
* @brief Functor for ODE right-hand side evaluation.
*
* Used by ODE solvers to compute dY/dt and energy generation rate. This is the only functor used in the non-NSE case.
*/
struct ODETerm {
GraphNetwork& m_network; ///< Reference to the network
const double m_T9; ///< Temperature in 10^9 K
const double m_rho; ///< Density in g/cm^3
const size_t m_numSpecies; ///< Number of species
/**
* @brief Construct an ODETerm functor.
* @param network Reference to the GraphNetwork.
* @param T9 Temperature in 10^9 K.
* @param rho Density in g/cm^3.
*/
ODETerm(GraphNetwork& network, const double T9, double rho) :
m_network(network), m_T9(T9), m_rho(rho), m_numSpecies(m_network.m_networkSpecies.size()) {}
/**
* @brief Compute dY/dt and energy rate for the ODE solver.
* @param Y Input vector of abundances.
* @param dYdt Output vector for derivatives (resized).
*
* @note this functor does not need auto differentiation to it called the <double> version of calculateAllDerivatives.
*/
void operator()(const boost::numeric::ublas::vector<double>&Y, boost::numeric::ublas::vector<double>& dYdt, double /* t */) const {
const std::vector<double> y(Y.begin(), m_numSpecies + Y.begin());
StepDerivatives<double> derivatives = m_network.calculateAllDerivatives<double>(y, m_T9, m_rho);
dYdt.resize(m_numSpecies + 1);
std::ranges::copy(derivatives.dydt, dYdt.begin());
dYdt(m_numSpecies) = derivatives.specificEnergyRate; // Last element is the specific energy rate
}
};
/**
* @brief Functor for Jacobian evaluation for stiff ODE solvers. (used in the NSE case)
*/
struct JacobianTerm {
GraphNetwork& m_network; ///< Reference to the network
const double m_T9; ///< Temperature in 10^9 K
const double m_rho; ///< Density in g/cm^3
const size_t m_numSpecies; ///< Number of species
/**
* @brief Construct a JacobianTerm functor.
* @param network Reference to the GraphNetwork.
* @param T9 Temperature in 10^9 K.
* @param rho Density in g/cm^3.
*/
JacobianTerm(GraphNetwork& network, const double T9, const double rho) :
m_network(network), m_T9(T9), m_rho(rho), m_numSpecies(m_network.m_networkSpecies.size()) {}
/**
* @brief Compute the Jacobian matrix for the ODE solver.
* @param Y Input vector of abundances.
* @param J Output matrix for the Jacobian (resized).
*/
void operator()(const boost::numeric::ublas::vector<double>& Y, boost::numeric::ublas::matrix<double>& J, double /* t */, boost::numeric::ublas::vector<double>& /* dfdt */) const {
const std::vector<double> y_species(Y.begin(), Y.begin() + m_numSpecies);
m_network.generateJacobianMatrix(y_species, m_T9, m_rho);
J.resize(m_numSpecies + 1, m_numSpecies + 1);
J.clear(); // Zero out the matrix
// PERF: Could probably be optimized
for (auto it1 = m_network.m_jacobianMatrix.begin1(); it1 != m_network.m_jacobianMatrix.end1(); ++it1) {
for (auto it2 = it1.begin(); it2 != it1.end(); ++it2) {
J(it2.index1(), it2.index2()) = *it2;
}
}
}
};
/**
* @brief Struct holding derivatives for a single ODE step.
* @tparam T Scalar type (double or CppAD::AD<double>).
*/
template <IsArithmeticOrAD T>
struct StepDerivatives {
std::vector<T> dydt; ///< Derivatives of abundances.
T specificEnergyRate = T(0.0); ///< Specific energy generation rate.
};
private:
/**
* @brief Synchronize all internal maps and matrices after network changes.
*
* Called after the reaction set is updated to rebuild all derived data structures.
*
* @note This method must be called any time the network topology changes.
*/
void syncInternalMaps();
/**
* @brief Collect all unique species from the reaction set.
*
* Populates m_networkSpecies and m_networkSpeciesMap.
* @throws std::runtime_error If a species is not found in the global atomic species database.
*
* @note Called by syncInternalMaps() to ensure the species list is up-to-date.
*/
void collectNetworkSpecies();
/**
* @brief Populate the reaction ID map from the reaction set.
*
* @note Called by syncInternalMaps() to ensure the reaction ID map is up-to-date.
*/
void populateReactionIDMap();
/**
* @brief Populate the species-to-index map for matrix construction.
*
* @note Called by syncInternalMaps() to ensure the species-to-index map is up-to-date.
*/
void populateSpeciesToIndexMap();
/**
* @brief Reserve and resize the Jacobian matrix.
*
* @note Called by syncInternalMaps() to ensure the Jacobian matrix is ready for use.
*/
void reserveJacobianMatrix();
/**
* @brief Record the CppAD tape for automatic differentiation.
* @throws std::runtime_error If there are no species in the network.
*
* @note Called by syncInternalMaps() to ensure the AD tape is recorded for the current network state.
*/
void recordADTape();
// --- Validation methods ---
/**
* @brief Validate mass and charge conservation for all reactions.
* @return True if all reactions conserve mass and charge, false otherwise.
*
* @note Logs errors for any violations.
*/
[[nodiscard]] bool validateConservation() const;
/**
* @brief Validate and update the network composition if needed.
*
* If the composition or culling/temperature has changed, rebuilds the reaction set and synchronizes maps.
* This is primarily used to enable caching in dynamic network situations where the composition, temperature, and density
* may change but the relevant reaction set remains equivalent.
*
* @param composition The composition to validate.
* @param culling Culling threshold.
* @param T9 Temperature in 10^9 K.
*/
void validateComposition(const fourdst::composition::Composition &composition, double culling, double T9);
// --- Simple Derivative Calculations ---
/**
* @brief Calculate all derivatives (dY/dt and energy rate) for the current state.
* @tparam T Scalar type (double or CppAD::AD<double>).
* @param Y Vector of abundances.
* @param T9 Temperature in 10^9 K.
* @param rho Density in g/cm^3.
* @return StepDerivatives<T> containing dY/dt and energy rate.
*/
template <IsArithmeticOrAD T>
StepDerivatives<T> calculateAllDerivatives(const std::vector<T>& Y, T T9, T rho) const;
// --- Generate the system matrices ---
/**
* @brief Generate the stoichiometry matrix for the network.
*
* Populates m_stoichiometryMatrix based on the current reactions and species.
* @throws std::runtime_error If a species is not found in the species-to-index map.
*/
void generateStoichiometryMatrix();
/**
* @brief Generate the Jacobian matrix for the network.
*
* Populates m_jacobianMatrix using automatic differentiation via the precomputed CppAD tape.
*
* @param Y Vector of abundances.
* @param T9 Temperature in 10^9 K.
* @param rho Density in g/cm^3.
*/
void generateJacobianMatrix(const std::vector<double>& Y, double T9, const double rho);
/**
* @brief Calculate the right-hand side (dY/dt) for the ODE system.
* @tparam GeneralScalarType Scalar type (double or CppAD::AD<double>).
* @param Y Vector of abundances.
* @param T9 Temperature in 10^9 K.
* @param rho Density in g/cm^3.
* @return Vector of dY/dt values.
*/
template <IsArithmeticOrAD GeneralScalarType>
std::vector<GeneralScalarType> calculateRHS(const std::vector<GeneralScalarType> &Y, const GeneralScalarType T9,
GeneralScalarType rho) const;
/**
* @brief Calculate the reaction rate for a given reaction in dimensions of particles per cm^3 per second.
* @tparam GeneralScalarType Scalar type (double or CppAD::AD<double>).
* @param reaction The REACLIB reaction.
* @param Y Vector of abundances.
* @param T9 Temperature in 10^9 K.
* @param rho Density in g/cm^3.
* @return Reaction rate.
* @throws std::runtime_error If a reactant species is not found in the species-to-index map.
*
* @note reaclib uses molar units that vary depending on the number of reactants, It's pretty straightforward
* to convert these into particle based units. Specifically, we just need to divide the final result by
* Avogadro's number raised to the number of reactants - 1;
*/
template <IsArithmeticOrAD GeneralScalarType>
GeneralScalarType calculateReactionRate(const reaclib::REACLIBReaction &reaction,
const std::vector<GeneralScalarType> &Y, const GeneralScalarType T9,
const GeneralScalarType rho) const;
/**
* @brief Detect if the network is stiff and select the appropriate ODE solver.
*
* Heuristically determines stiffness based on the ratio of timescales. The stiffness heuristic is as follows:
*
* 1. For each species, compute the timescale as |Y_i / (dY_i/dt)|, where Y_i is the abundance and dY_i/dt is its time derivative.
* 2. Find the minimum and maximum timescales across all species.
* 3. Compute the stiffness ratio as (maximum timescale) / (minimum timescale).
* 4. If the stiffness ratio exceeds a threshold (default: 1e6), the system is considered stiff and a stiff ODE solver is used.
* Otherwise, a non-stiff ODE solver is used.
*
* This heuristic is based on the observation that stiff systems have widely varying timescales, which can cause explicit
* solvers to become unstable or inefficient. The threshold can be tuned based on the characteristics of the network.
*
* @param netIn The input structure.
* @param T9 Temperature in 10^9 K.
* @param numSpecies Number of species.
* @param Y Vector of abundances.
*/
void detectStiff(const NetIn &netIn, double T9, unsigned long numSpecies, const boost::numeric::ublas::vector<double>& Y);
};
template<IsArithmeticOrAD T>
GraphNetwork::StepDerivatives<T> GraphNetwork::calculateAllDerivatives(
const std::vector<T> &Y, T T9, T rho) const {
StepDerivatives<T> result;
result.dydt.resize(m_networkSpecies.size(), static_cast<T>(0.0));
if (rho < MIN_DENSITY_THRESHOLD) {
return result; // Return zero for everything if density is too low
}
const T u = static_cast<T>(m_constants.get("u").value); // Atomic mass unit in grams
const T MeV_to_erg = static_cast<T>(m_constants.get("MeV_to_erg").value);
T volumetricEnergyRate = static_cast<T>(0.0); // Accumulator for erg / (cm^3 * s)
// --- SINGLE LOOP OVER ALL REACTIONS ---
for (size_t reactionIndex = 0; reactionIndex < m_reactions.size(); ++reactionIndex) {
const auto& reaction = m_reactions[reactionIndex];
// 1. Calculate reaction rate
const T reactionRate = calculateReactionRate(reaction, Y, T9, rho);
// 2. Use the rate to update all relevant species derivatives (dY/dt)
for (size_t speciesIndex = 0; speciesIndex < m_networkSpecies.size(); ++speciesIndex) {
const T nu_ij = static_cast<T>(m_stoichiometryMatrix(speciesIndex, reactionIndex));
if (nu_ij != 0) {
const T speciesAtomicMassAMU = static_cast<T>(m_networkSpecies[speciesIndex].mass());
const T speciesAtomicMassGrams = speciesAtomicMassAMU * u;
result.dydt[speciesIndex] += (nu_ij * reactionRate * speciesAtomicMassGrams) / rho;
}
}
// 3. Use the same rate to update the energy generation rate
const T q_value_ergs = static_cast<T>(reaction.qValue()) * MeV_to_erg;
volumetricEnergyRate += reactionRate * q_value_ergs;
}
result.specificEnergyRate = volumetricEnergyRate / rho;
return result;
}
template <IsArithmeticOrAD GeneralScalarType>
GeneralScalarType GraphNetwork::calculateReactionRate(const reaclib::REACLIBReaction &reaction, const std::vector<GeneralScalarType> &Y,
const GeneralScalarType T9, const GeneralScalarType rho) const {
const auto &constants = fourdst::constant::Constants::getInstance();
const auto u = constants.get("u"); // Atomic mass unit in g/mol
const auto uValue = static_cast<GeneralScalarType>(u.value); // Convert to double for calculations
const GeneralScalarType k_reaction = reaction.calculate_rate(T9);
auto reactant_product_or_particle_densities = static_cast<GeneralScalarType>(1.0);
std::unordered_map<std::string, int> reactant_counts;
reactant_counts.reserve(reaction.reactants().size());
for (const auto& reactant : reaction.reactants()) {
reactant_counts[std::string(reactant.name())]++;
}
const auto minAbundanceThreshold = static_cast<GeneralScalarType>(MIN_ABUNDANCE_THRESHOLD);
const auto minDensityThreshold = static_cast<GeneralScalarType>(MIN_DENSITY_THRESHOLD);
if (rho < minDensityThreshold) {
// LOG_INFO(m_logger, "Density is below the minimum threshold ({} g/cm^3), returning zero reaction rate for reaction '{}'.",
// CppAD::Value(rho), reaction.id()); // CppAD::Value causes a compilation error here, not clear why...
return static_cast<GeneralScalarType>(0.0); // If density is below a threshold, return zero rate.
}
for (const auto& [species_name, count] : reactant_counts) {
auto species_it = m_speciesToIndexMap.find(m_networkSpeciesMap.at(species_name));
if (species_it == m_speciesToIndexMap.end()) {
LOG_ERROR(m_logger, "Reactant species '{}' not found in species to index map for reaction '{}'.",
species_name, reaction.id());
throw std::runtime_error("Reactant species not found in species to index map: " + species_name);
}
const size_t species_index = species_it->second;
const GeneralScalarType Yi = Y[species_index];
if (Yi < minAbundanceThreshold) {
return static_cast<GeneralScalarType>(0.0); // If any reactant is below a threshold, return zero rate.
}
auto atomicMassAMU = static_cast<GeneralScalarType>(m_networkSpecies[species_index].mass());
// Convert to number density
GeneralScalarType ni;
const GeneralScalarType denominator = atomicMassAMU * uValue;
assert (denominator > 0.0);
ni = (Yi * rho) / (denominator);
reactant_product_or_particle_densities *= ni;
// Apply factorial correction for identical reactions
if (count > 1) {
reactant_product_or_particle_densities /= static_cast<GeneralScalarType>(std::tgamma(static_cast<double>(count + 1))); // Gamma function for factorial
}
}
const GeneralScalarType Na = static_cast<GeneralScalarType>(constants.get("N_a").value); // Avogadro's number in mol^-1
const int numReactants = static_cast<int>(reaction.reactants().size());
auto molarCorrectionFactor = static_cast<GeneralScalarType>(1.0); // No correction needed for single reactant reactions
if (numReactants > 1) {
molarCorrectionFactor = CppAD::pow(Na, static_cast<GeneralScalarType>(reaction.reactants().size() - 1));
}
return (reactant_product_or_particle_densities * k_reaction) / molarCorrectionFactor; // reaction rate in per volume per time (particles/cm^3/s)
}
template <IsArithmeticOrAD GeneralScalarType>
std::vector<GeneralScalarType> GraphNetwork::calculateRHS(
const std::vector<GeneralScalarType>& Y,
const GeneralScalarType T9,
const GeneralScalarType rho) const {
auto derivatives = calculateAllDerivatives<GeneralScalarType>(Y, T9, rho);
return derivatives.dydt;
}
};

View File

@@ -25,12 +25,15 @@
#include "fourdst/logging/logging.h"
#include "fourdst/config/config.h"
#include "fourdst/composition/species.h"
#include "quill/Logger.h"
#include "fourdst/composition/composition.h"
#include "gridfire/reaclib.h"
#include "fourdst/constants/const.h"
#include "gridfire/reaction/reaction.h"
#include "quill/Logger.h"
#include <unordered_map>
#include "fourdst/constants/const.h"
namespace gridfire {
@@ -127,7 +130,7 @@ namespace gridfire {
* @param netIn Input parameters for the network evaluation.
* @return NetOut Output results from the network evaluation.
*/
virtual NetOut evaluate(const NetIn &netIn);
virtual NetOut evaluate(const NetIn &netIn) = 0;
virtual bool isStiff() const { return m_stiff; }
virtual void setStiff(const bool stiff) { m_stiff = stiff; }
@@ -143,105 +146,9 @@ namespace gridfire {
bool m_stiff = false; ///< Flag indicating if the network is stiff
};
class LogicalReaction {
public:
explicit LogicalReaction(const std::vector<reaclib::REACLIBReaction> &reactions);
explicit LogicalReaction(const reaclib::REACLIBReaction &reaction);
void add_reaction(const reaclib::REACLIBReaction& reaction);
template <typename T>
[[nodiscard]] T calculate_rate(const T T9) const {
T sum = static_cast<T>(0.0);
const T T913 = CppAD::pow(T9, 1.0/3.0);
const T T953 = CppAD::pow(T9, 5.0/3.0);
const T logT9 = CppAD::log(T9);
for (const auto& rate : m_rates) {
const T exponent = rate.a0 +
rate.a1 / T9 +
rate.a2 / T913 +
rate.a3 * T913 +
rate.a4 * T9 +
rate.a5 * T953 +
rate.a6 * logT9;
sum += CppAD::exp(exponent);
}
return sum;
}
[[nodiscard]] const std::string& id() const {return std::string(m_peID); }
[[nodiscard]] std::string_view peName() const { return m_peID; }
[[nodiscard]] int chapter() const { return m_chapter; }
[[nodiscard]] const std::vector<fourdst::atomic::Species>& reactants() const { return m_reactants; }
[[nodiscard]] const std::vector<fourdst::atomic::Species>& products() const { return m_products; }
[[nodiscard]] double qValue() const { return m_qValue; }
[[nodiscard]] bool is_reverse() const { return m_reverse; }
auto begin();
auto begin() const;
auto end();
auto end() const;
private:
const quill::Logger* m_logger = fourdst::logging::LogManager::getInstance().getLogger("log");
std::string_view m_peID;
std::vector<fourdst::atomic::Species> m_reactants; ///< Reactants of the reaction
std::vector<fourdst::atomic::Species> m_products; ///< Products of the reaction
double m_qValue = 0.0; ///< Q-value of the reaction
bool m_reverse = false; ///< True if the reaction is reverse
int m_chapter;
std::vector<reaclib::RateFitSet> m_rates;
};
class LogicalReactionSet {
public:
LogicalReactionSet() = default;
explicit LogicalReactionSet(const std::vector<LogicalReaction>& reactions);
explicit LogicalReactionSet(const std::vector<reaclib::REACLIBReaction>& reactions);
explicit LogicalReactionSet(const reaclib::REACLIBReactionSet& reactionSet);
void add_reaction(const LogicalReaction& reaction);
void add_reaction(const reaclib::REACLIBReaction& reaction);
void remove_reaction(const LogicalReaction& reaction);
[[nodiscard]] bool contains(const std::string_view& id) const;
[[nodiscard]] bool contains(const LogicalReaction& reactions) const;
[[nodiscard]] bool contains(const reaclib::REACLIBReaction& reaction) const;
[[nodiscard]] size_t size() const;
void sort(double T9=1.0);
bool contains_species(const fourdst::atomic::Species &species) const;
bool contains_reactant(const fourdst::atomic::Species &species) const;
bool contains_product(const fourdst::atomic::Species &species) const;
[[nodiscard]] const LogicalReaction& operator[](size_t index) const;
[[nodiscard]] const LogicalReaction& operator[](const std::string_view& id) const;
auto begin();
auto begin() const;
auto end();
auto end() const;
private:
const quill::Logger* m_logger = fourdst::logging::LogManager::getInstance().getLogger("log");
std::vector<LogicalReaction> m_reactions;
std::unordered_map<std::string_view, LogicalReaction&> m_reactionNameMap;
};
LogicalReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition);
LogicalReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition, double culling, double T9 = 1.0);
reaction::REACLIBLogicalReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition, bool reverse);
reaction::REACLIBLogicalReactionSet build_reaclib_nuclear_network_from_file(const std::string& filename, bool reverse);
} // namespace nuclearNetwork

View File

@@ -0,0 +1,270 @@
#pragma once
#include <string_view>
#include "fourdst/composition/atomicSpecies.h"
#include "fourdst/logging/logging.h"
#include "quill/Logger.h"
#include <unordered_map>
#include <vector>
#include <memory>
#include <unordered_set>
#include <cstdint>
#include "cppad/cppad.hpp"
namespace gridfire::reaction {
/**
* @struct REACLIBRateCoefficientSet
* @brief Holds the seven fitting parameters for a single REACLIB rate set.
* @details The thermonuclear reaction rate for a single set is calculated as:
* rate = exp(a0 + a1/T9 + a2/T9^(-1/3) + a3*T9^(1/3) + a4*T9 + a5*T9^(5/3) + a6*ln(T9))
* where T9 is the temperature in billions of Kelvin. The total rate for a
* reaction is the sum of the rates from all its sets.
*/
struct REACLIBRateCoefficientSet {
const double a0;
const double a1;
const double a2;
const double a3;
const double a4;
const double a5;
const double a6;
friend std::ostream& operator<<(std::ostream& os, const REACLIBRateCoefficientSet& r) {
os << "[" << r.a0 << ", " << r.a1 << ", " << r.a2 << ", "
<< r.a3 << ", " << r.a4 << ", " << r.a5 << ", " << r.a6 << "]";
return os;
}
};
class Reaction {
public:
Reaction(
const std::string_view id,
const double qValue,
const std::vector<fourdst::atomic::Species>& reactants,
const std::vector<fourdst::atomic::Species>& products,
const bool reverse = false
);
virtual ~Reaction() = default;
virtual std::unique_ptr<Reaction> clone() const = 0;
virtual double calculate_rate(double T9) const = 0;
virtual CppAD::AD<double> calculate_rate(const CppAD::AD<double> T9) const = 0;
virtual std::string_view peName() const { return ""; }
[[nodiscard]] bool contains(const fourdst::atomic::Species& species) const;
[[nodiscard]] bool contains_reactant(const fourdst::atomic::Species& species) const;
[[nodiscard]] bool contains_product(const fourdst::atomic::Species& species) const;
std::unordered_set<fourdst::atomic::Species> all_species() const;
std::unordered_set<fourdst::atomic::Species> reactant_species() const;
std::unordered_set<fourdst::atomic::Species> product_species() const;
size_t num_species() const;
int stoichiometry(const fourdst::atomic::Species& species) const;
std::unordered_map<fourdst::atomic::Species, int> stoichiometry() const;
std::string_view id() const { return m_id; }
double qValue() const { return m_qValue; }
const std::vector<fourdst::atomic::Species>& reactants() const { return m_reactants; }
const std::vector<fourdst::atomic::Species>& products() const { return m_products; }
bool is_reverse() const { return m_reverse; }
double excess_energy() const;
bool operator==(const Reaction& other) const { return m_id == other.m_id; }
bool operator!=(const Reaction& other) const { return !(*this == other); }
[[nodiscard]] uint64_t hash(uint64_t seed) const;
protected:
quill::Logger* m_logger = fourdst::logging::LogManager::getInstance().getLogger("log");
std::string m_id;
double m_qValue = 0.0; ///< Q-value of the reaction
std::vector<fourdst::atomic::Species> m_reactants; ///< Reactants of the reaction
std::vector<fourdst::atomic::Species> m_products; ///< Products of the reaction
bool m_reverse = false;
};
class ReactionSet {
public:
explicit ReactionSet(std::vector<std::unique_ptr<Reaction>> reactions);
ReactionSet(const ReactionSet& other);
ReactionSet& operator=(const ReactionSet& other);
virtual ~ReactionSet() = default;
virtual void add_reaction(std::unique_ptr<Reaction> reaction);
virtual void remove_reaction(const std::unique_ptr<Reaction>& reaction);
bool contains(const std::string_view& id) const;
bool contains(const Reaction& reaction) const;
size_t size() const { return m_reactions.size(); }
void sort(double T9=1.0);
bool contains_species(const fourdst::atomic::Species& species) const;
bool contains_reactant(const fourdst::atomic::Species& species) const;
bool contains_product(const fourdst::atomic::Species& species) const;
[[nodiscard]] const Reaction& operator[](size_t index) const;
[[nodiscard]] const Reaction& operator[](const std::string_view& id) const;
bool operator==(const ReactionSet& other) const;
bool operator!=(const ReactionSet& other) const;
[[nodiscard]] uint64_t hash(uint64_t seed = 0) const;
auto begin() { return m_reactions.begin(); }
auto begin() const { return m_reactions.cbegin(); }
auto end() { return m_reactions.end(); }
auto end() const { return m_reactions.cend(); }
protected:
quill::Logger* m_logger = fourdst::logging::LogManager::getInstance().getLogger("log");
std::vector<std::unique_ptr<Reaction>> m_reactions;
std::string m_id;
std::unordered_map<std::string, Reaction*> m_reactionNameMap; ///< Maps reaction IDs to Reaction objects for quick lookup
};
/**
* @struct REACLIBReaction
* @brief Represents a single nuclear reaction from the JINA REACLIB database.
* @details This struct is designed to be constructed at compile time (constexpr) from
* the data parsed by the Python generation script. It stores all necessary
* information to identify a reaction and calculate its rate.
*/
class REACLIBReaction final : public Reaction {
public:
/**
* @brief Constructs a REACLIBReaction object at compile time.
* @param id A unique string identifier generated by the Python script.
* @param peName
* @param chapter The REACLIB chapter number, defining the reaction structure.
* @param reactants A vector of strings with the names of the reactant species.
* @param products A vector of strings with the names of the product species.
* @param qValue The Q-value of the reaction in MeV.
* @param label The source label for the rate data (e.g., "wc12w", "st08").
* @param sets A vector of RateFitSet, containing the fitting coefficients for the rate.
* @param reverse A boolean indicating if the reaction is reversed (default is false).
*/
REACLIBReaction(
const std::string_view id,
const std::string_view peName,
const int chapter,
const std::vector<fourdst::atomic::Species> &reactants,
const std::vector<fourdst::atomic::Species> &products,
const double qValue,
const std::string_view label,
const REACLIBRateCoefficientSet &sets,
const bool reverse = false);
[[nodiscard]] std::unique_ptr<Reaction> clone() const override;
[[nodiscard]] double calculate_rate(const double T9) const override;
[[nodiscard]] CppAD::AD<double> calculate_rate(const CppAD::AD<double> T9) const override;
template <typename GeneralScalarType>
[[nodiscard]] GeneralScalarType calculate_rate(const GeneralScalarType T9) const {
const GeneralScalarType T913 = CppAD::pow(T9, 1.0/3.0);
const GeneralScalarType rateExponent = m_rateCoefficients.a0 +
m_rateCoefficients.a1 / T9 +
m_rateCoefficients.a2 / T913 +
m_rateCoefficients.a3 * T913 +
m_rateCoefficients.a4 * T9 +
m_rateCoefficients.a5 * CppAD::pow(T9, 5.0/3.0) +
m_rateCoefficients.a6 * CppAD::log(T9);
return CppAD::exp(rateExponent);
}
[[nodiscard]] std::string_view peName() const override { return m_peName; }
[[nodiscard]] int chapter() const { return m_chapter; }
[[nodiscard]] std::string_view sourceLabel() const { return m_sourceLabel; }
[[nodiscard]] const REACLIBRateCoefficientSet& rateCoefficients() const { return m_rateCoefficients; }
friend std::ostream& operator<<(std::ostream& os, const REACLIBReaction& reaction);
private:
std::string m_peName; ///< Name of the reaction in (projectile, ejectile) notation (e.g. p(p, g)d)
int m_chapter; ///< Chapter number from the REACLIB database, defining the reaction structure.
std::string m_sourceLabel; ///< Source label for the rate data, indicating the origin of the rate coefficients (e.g., "wc12w", "st08").
REACLIBRateCoefficientSet m_rateCoefficients;
};
class REACLIBReactionSet final : public ReactionSet {
public:
explicit REACLIBReactionSet(std::vector<REACLIBReaction>);
std::unordered_set<std::string> peNames() const;
friend std::ostream& operator<<(std::ostream& os, const REACLIBReactionSet& set);
};
class REACLIBLogicalReaction final : public Reaction {
public:
explicit REACLIBLogicalReaction(const std::vector<REACLIBReaction> &reactions);
explicit REACLIBLogicalReaction(const REACLIBReaction &reaction);
void add_reaction(const REACLIBReaction& reaction);
[[nodiscard]] std::unique_ptr<Reaction> clone() const override;
[[nodiscard]] std::string_view peName() const override { return m_id; };
[[nodiscard]] size_t size() const { return m_rates.size(); }
[[nodiscard]] std::vector<std::string> sources() const { return m_sources; }
[[nodiscard]] double calculate_rate(const double T9) const override;
[[nodiscard]] CppAD::AD<double> calculate_rate(const CppAD::AD<double> T9) const override;
template <typename T>
[[nodiscard]] T calculate_rate(const T T9) const {
T sum = static_cast<T>(0.0);
const T T913 = CppAD::pow(T9, 1.0/3.0);
const T T953 = CppAD::pow(T9, 5.0/3.0);
const T logT9 = CppAD::log(T9);
// ReSharper disable once CppUseStructuredBinding
for (const auto& rate : m_rates) {
const T exponent = rate.a0 +
rate.a1 / T9 +
rate.a2 / T913 +
rate.a3 * T913 +
rate.a4 * T9 +
rate.a5 * T953 +
rate.a6 * logT9;
sum += CppAD::exp(exponent);
}
return sum;
}
[[nodiscard]] int chapter() const { return m_chapter; }
auto begin() { return m_rates.begin(); }
auto begin() const { return m_rates.cbegin(); }
auto end() { return m_rates.end(); }
auto end() const { return m_rates.cend(); }
private:
int m_chapter;
std::vector<std::string> m_sources;
std::vector<REACLIBRateCoefficientSet> m_rates;
};
class REACLIBLogicalReactionSet final : public ReactionSet {
public:
REACLIBLogicalReactionSet() = delete;
explicit REACLIBLogicalReactionSet(const REACLIBReactionSet& reactionSet);
[[nodiscard]] std::unordered_set<std::string> peNames() const;
private:
std::unordered_set<std::string> m_peNames;
};
}

View File

@@ -0,0 +1,256 @@
#pragma once
#include "gridfire/engine/engine_graph.h"
#include "gridfire/engine/engine_abstract.h"
#include "gridfire/network.h"
#include "fourdst/logging/logging.h"
#include "fourdst/config/config.h"
#include "quill/Logger.h"
#include "Eigen/Dense"
#include <vector>
namespace gridfire::solver {
struct dynamicQSESpeciesIndices {
std::vector<size_t> dynamicSpeciesIndices; // Slow species that are not in QSE
std::vector<size_t> QSESpeciesIndices; // Fast species that are in QSE
};
template <typename EngineT>
class NetworkSolverStrategy {
public:
explicit NetworkSolverStrategy(EngineT& engine) : m_engine(engine) {};
virtual ~NetworkSolverStrategy() = default;
virtual NetOut evaluate(const NetIn& netIn) = 0;
protected:
EngineT& m_engine;
};
using DynamicNetworkSolverStrategy = NetworkSolverStrategy<DynamicEngine>;
using StaticNetworkSolverStrategy = NetworkSolverStrategy<Engine>;
class QSENetworkSolver final : public DynamicNetworkSolverStrategy {
public:
using DynamicNetworkSolverStrategy::DynamicNetworkSolverStrategy;
NetOut evaluate(const NetIn& netIn) override;
private: // methods
dynamicQSESpeciesIndices packSpeciesTypeIndexVectors(
const std::vector<double>& Y,
const double T9,
const double rho
) const;
Eigen::VectorXd calculateSteadyStateAbundances(
const std::vector<double>& Y,
const double T9,
const double rho,
const dynamicQSESpeciesIndices& indices
) const;
NetOut initializeNetworkWithShortIgnition(
const NetIn& netIn
) const;
private: // Nested functors for ODE integration
struct RHSFunctor {
DynamicEngine& m_engine;
const std::vector<size_t>& m_dynamicSpeciesIndices;
const std::vector<size_t>& m_QSESpeciesIndices;
const Eigen::VectorXd& m_Y_QSE;
const double m_T9;
const double m_rho;
RHSFunctor(
DynamicEngine& engine,
const std::vector<size_t>& dynamicSpeciesIndices,
const std::vector<size_t>& QSESpeciesIndices,
const Eigen::VectorXd& Y_QSE,
const double T9,
const double rho
) :
m_engine(engine),
m_dynamicSpeciesIndices(dynamicSpeciesIndices),
m_QSESpeciesIndices(QSESpeciesIndices),
m_Y_QSE(Y_QSE),
m_T9(T9),
m_rho(rho) {}
void operator()(
const boost::numeric::ublas::vector<double>& YDynamic,
boost::numeric::ublas::vector<double>& dYdtDynamic,
double t
) const;
};
struct JacobianFunctor {
DynamicEngine& m_engine;
const std::vector<size_t>& m_dynamicSpeciesIndices;
const std::vector<size_t>& m_QSESpeciesIndices;
const double m_T9;
const double m_rho;
JacobianFunctor(
DynamicEngine& engine,
const std::vector<size_t>& dynamicSpeciesIndices,
const std::vector<size_t>& QSESpeciesIndices,
const double T9,
const double rho
) :
m_engine(engine),
m_dynamicSpeciesIndices(dynamicSpeciesIndices),
m_QSESpeciesIndices(QSESpeciesIndices),
m_T9(T9),
m_rho(rho) {}
void operator()(
const boost::numeric::ublas::vector<double>& YDynamic,
boost::numeric::ublas::matrix<double>& JDynamic,
double t,
boost::numeric::ublas::vector<double>& dfdt
) const;
};
template<typename T>
struct EigenFunctor {
using InputType = Eigen::Matrix<T, Eigen::Dynamic, 1>;
using OutputType = Eigen::Matrix<T, Eigen::Dynamic, 1>;
using JacobianType = Eigen::Matrix<T, Eigen::Dynamic, Eigen::Dynamic>;
DynamicEngine& m_engine;
const std::vector<double>& m_YDynamic;
const std::vector<size_t>& m_dynamicSpeciesIndices;
const std::vector<size_t>& m_QSESpeciesIndices;
const double m_T9;
const double m_rho;
EigenFunctor(
DynamicEngine& engine,
const std::vector<double>& YDynamic,
const std::vector<size_t>& dynamicSpeciesIndices,
const std::vector<size_t>& QSESpeciesIndices,
const double T9,
const double rho
) :
m_engine(engine),
m_YDynamic(YDynamic),
m_dynamicSpeciesIndices(dynamicSpeciesIndices),
m_QSESpeciesIndices(QSESpeciesIndices),
m_T9(T9),
m_rho(rho) {}
int operator()(const InputType& v_QSE, OutputType& f_QSE) const;
int df(const InputType& v_QSE, JacobianType& J_QSE) const;
};
private:
quill::Logger* m_logger = fourdst::logging::LogManager::getInstance().getLogger("log");
fourdst::config::Config& m_config = fourdst::config::Config::getInstance();
};
class DirectNetworkSolver final : public DynamicNetworkSolverStrategy {
public:
using DynamicNetworkSolverStrategy::DynamicNetworkSolverStrategy;
NetOut evaluate(const NetIn& netIn) override;
private:
struct RHSFunctor {
DynamicEngine& m_engine;
const double m_T9;
const double m_rho;
const size_t m_numSpecies;
RHSFunctor(
DynamicEngine& engine,
const double T9,
const double rho
) :
m_engine(engine),
m_T9(T9),
m_rho(rho),
m_numSpecies(engine.getNetworkSpecies().size()) {}
void operator()(
const boost::numeric::ublas::vector<double>& Y,
boost::numeric::ublas::vector<double>& dYdt,
double t
) const;
};
struct JacobianFunctor {
DynamicEngine& m_engine;
const double m_T9;
const double m_rho;
const size_t m_numSpecies;
JacobianFunctor(
DynamicEngine& engine,
const double T9,
const double rho
) :
m_engine(engine),
m_T9(T9),
m_rho(rho),
m_numSpecies(engine.getNetworkSpecies().size()) {}
void operator()(
const boost::numeric::ublas::vector<double>& Y,
boost::numeric::ublas::matrix<double>& J,
double t,
boost::numeric::ublas::vector<double>& dfdt
) const;
};
private:
quill::Logger* m_logger = fourdst::logging::LogManager::getInstance().getLogger("log");
fourdst::config::Config& m_config = fourdst::config::Config::getInstance();
};
template<typename T>
int QSENetworkSolver::EigenFunctor<T>::operator()(const InputType &v_QSE, OutputType &f_QSE) const {
std::vector<double> YFull(m_engine.getNetworkSpecies().size(), 0.0);
Eigen::VectorXd Y_QSE(v_QSE.array().exp());
for (size_t i = 0; i < m_dynamicSpeciesIndices.size(); ++i) {
YFull[m_dynamicSpeciesIndices[i]] = m_YDynamic[i];
}
for (size_t i = 0; i < m_QSESpeciesIndices.size(); ++i) {
YFull[m_QSESpeciesIndices[i]] = Y_QSE(i);
}
const auto [full_dYdt, specificEnergyGenerationRate] = m_engine.calculateRHSAndEnergy(YFull, m_T9, m_rho);
f_QSE.resize(m_QSESpeciesIndices.size());
for (size_t i = 0; i < m_QSESpeciesIndices.size(); ++i) {
f_QSE(i) = full_dYdt[m_QSESpeciesIndices[i]];
}
return 0; // Success
}
template <typename T>
int QSENetworkSolver::EigenFunctor<T>::df(const InputType& v_QSE, JacobianType& J_QSE) const {
std::vector<double> YFull(m_engine.getNetworkSpecies().size(), 0.0);
Eigen::VectorXd Y_QSE(v_QSE.array().exp());
for (size_t i = 0; i < m_dynamicSpeciesIndices.size(); ++i) {
YFull[m_dynamicSpeciesIndices[i]] = m_YDynamic[i];
}
for (size_t i = 0; i < m_QSESpeciesIndices.size(); ++i) {
YFull[m_QSESpeciesIndices[i]] = Y_QSE(i);
}
m_engine.generateJacobianMatrix(YFull, m_T9, m_rho);
Eigen::MatrixXd J_orig(m_QSESpeciesIndices.size(), m_QSESpeciesIndices.size());
for (size_t i = 0; i < m_QSESpeciesIndices.size(); ++i) {
for (size_t j = 0; j < m_QSESpeciesIndices.size(); ++j) {
J_orig(i, j) = m_engine.getJacobianMatrixEntry(m_QSESpeciesIndices[i], m_QSESpeciesIndices[j]);
}
}
J_QSE = J_orig;
for (long j = 0; j < J_QSE.cols(); ++j) {
J_QSE.col(j) *= Y_QSE(j); // Chain rule for log space
}
return 0; // Success
}
}

View File

@@ -28,7 +28,7 @@
#include "fourdst/config/config.h"
#include "quill/LogMacros.h"
#include "gridfire/approx8.h"
#include "gridfire/engine/engine_approx8.h"
#include "gridfire/network.h"
/* Nuclear reaction network in cgs units based on Frank Timmes' "approx8".
@@ -131,14 +131,14 @@ namespace gridfire::approx8{
return rate_fit(T9,a);
}
// he3(he3,2p)he4
// he3(he3,2p)he4 ** (missing both coefficients but have a reaction)
double he3he4_rate(const vec7 &T9){
constexpr vec7 a1 = {1.560990e+01,0.000000e+00,-1.282710e+01,-3.082250e-02,-6.546850e-01,8.963310e-02,-6.666670e-01};
constexpr vec7 a2 = {1.770750e+01,0.000000e+00,-1.282710e+01,-3.812600e+00,9.422850e-02,-3.010180e-03,1.333330e+00};
return rate_fit(T9,a1) + rate_fit(T9,a2);
}
// he4 + he4 + he4 -> c12
// he4 + he4 + he4 -> c12 ** (missing middle coefficient but have other two)
double triple_alpha_rate(const vec7 &T9){
constexpr vec7 a1 = {-9.710520e-01,0.000000e+00,-3.706000e+01,2.934930e+01,-1.155070e+02,-1.000000e+01,-1.333330e+00};
constexpr vec7 a2 = {-1.178840e+01,-1.024460e+00,-2.357000e+01,2.048860e+01,-1.298820e+01,-2.000000e+01,-2.166670e+00};
@@ -153,7 +153,7 @@ namespace gridfire::approx8{
return rate_fit(T9,a1) + rate_fit(T9,a2);
}
// c12 + he4 -> o16
// c12 + he4 -> o16 ** (missing first coefficient but have the second)
double c12a_rate(const vec7 &T9){
constexpr vec7 a1={6.965260e+01,-1.392540e+00,5.891280e+01,-1.482730e+02,9.083240e+00,-5.410410e-01,7.035540e+01};
constexpr vec7 a2={2.546340e+02,-1.840970e+00,1.034110e+02,-4.205670e+02,6.408740e+01,-1.246240e+01,1.373030e+02};
@@ -508,27 +508,19 @@ namespace gridfire::approx8{
vector_type Approx8Network::convert_netIn(const NetIn &netIn) {
vector_type y(Approx8Net::nVar, 0.0);
y[Approx8Net::ih1] = netIn.composition.getMassFraction("H-1");
y[Approx8Net::ihe3] = netIn.composition.getMassFraction("He-3");
y[Approx8Net::ihe4] = netIn.composition.getMassFraction("He-4");
y[Approx8Net::ic12] = netIn.composition.getMassFraction("C-12");
y[Approx8Net::in14] = netIn.composition.getMassFraction("N-14");
y[Approx8Net::io16] = netIn.composition.getMassFraction("O-16");
y[Approx8Net::ine20] = netIn.composition.getMassFraction("Ne-20");
y[Approx8Net::img24] = netIn.composition.getMassFraction("Mg-24");
y[Approx8Net::ih1] = netIn.composition.getNumberFraction("H-1");
std::cout << "Approx8::convert_netIn -> H-1 fraction: " << y[Approx8Net::ih1] << std::endl;
y[Approx8Net::ihe3] = netIn.composition.getNumberFraction("He-3");
y[Approx8Net::ihe4] = netIn.composition.getNumberFraction("He-4");
y[Approx8Net::ic12] = netIn.composition.getNumberFraction("C-12");
y[Approx8Net::in14] = netIn.composition.getNumberFraction("N-14");
y[Approx8Net::io16] = netIn.composition.getNumberFraction("O-16");
y[Approx8Net::ine20] = netIn.composition.getNumberFraction("Ne-20");
y[Approx8Net::img24] = netIn.composition.getNumberFraction("Mg-24");
y[Approx8Net::iTemp] = netIn.temperature;
y[Approx8Net::iDensity] = netIn.density;
y[Approx8Net::iEnergy] = netIn.energy;
double ySum = 0.0;
for (int i = 0; i < Approx8Net::nIso; i++) {
y[i] /= Approx8Net::aIon[i];
ySum += y[i];
}
for (int i = 0; i < Approx8Net::nIso; i++) {
y[i] /= ySum;
}
return y;
}
};

View File

@@ -1,9 +1,9 @@
#include "gridfire/netgraph.h"
#include "fourdst/composition/atomicSpecies.h"
#include "fourdst/constants/const.h"
#include "gridfire/engine/engine_graph.h"
#include "gridfire/reaction/reaction.h"
#include "gridfire/network.h"
#include "gridfire/reaclib.h"
#include "fourdst/composition/species.h"
#include "fourdst/composition/atomicSpecies.h"
#include "quill/LogMacros.h"
@@ -14,6 +14,7 @@
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
#include <fstream>
@@ -22,31 +23,28 @@
namespace gridfire {
GraphNetwork::GraphNetwork(
GraphEngine::GraphEngine(
const fourdst::composition::Composition &composition
):
Network(REACLIB),
m_reactions(build_reaclib_nuclear_network(composition)) {
m_reactions(build_reaclib_nuclear_network(composition, false)) {
syncInternalMaps();
}
GraphNetwork::GraphNetwork(
const fourdst::composition::Composition &composition,
const double cullingThreshold,
const double T9
):
Network(REACLIB),
m_reactions(build_reaclib_nuclear_network(composition, cullingThreshold, T9)) {
syncInternalMaps();
}
GraphNetwork::GraphNetwork(const reaclib::REACLIBReactionSet &reactions) :
Network(REACLIB),
m_reactions(reactions) {
GraphEngine::GraphEngine(reaction::REACLIBLogicalReactionSet reactions) :
m_reactions(std::move(reactions)) {
syncInternalMaps();
}
void GraphNetwork::syncInternalMaps() {
StepDerivatives<double> GraphEngine::calculateRHSAndEnergy(
const std::vector<double> &Y,
const double T9,
const double rho
) const {
return calculateAllDerivatives<double>(Y, T9, rho);
}
void GraphEngine::syncInternalMaps() {
collectNetworkSpecies();
populateReactionIDMap();
populateSpeciesToIndexMap();
@@ -56,17 +54,17 @@ namespace gridfire {
}
// --- Network Graph Construction Methods ---
void GraphNetwork::collectNetworkSpecies() {
void GraphEngine::collectNetworkSpecies() {
m_networkSpecies.clear();
m_networkSpeciesMap.clear();
std::set<std::string_view> uniqueSpeciesNames;
for (const auto& reaction: m_reactions) {
for (const auto& reactant: reaction.reactants()) {
for (const auto& reactant: reaction->reactants()) {
uniqueSpeciesNames.insert(reactant.name());
}
for (const auto& product: reaction.products()) {
for (const auto& product: reaction->products()) {
uniqueSpeciesNames.insert(product.name());
}
}
@@ -84,84 +82,55 @@ namespace gridfire {
}
void GraphNetwork::populateReactionIDMap() {
LOG_INFO(m_logger, "Populating reaction ID map for REACLIB graph network (serif::network::GraphNetwork)...");
void GraphEngine::populateReactionIDMap() {
LOG_TRACE_L1(m_logger, "Populating reaction ID map for REACLIB graph network (serif::network::GraphNetwork)...");
m_reactionIDMap.clear();
for (const auto& reaction: m_reactions) {
m_reactionIDMap.insert({reaction.id(), reaction});
m_reactionIDMap.emplace(reaction->id(), reaction.get());
}
LOG_INFO(m_logger, "Populated {} reactions in the reaction ID map.", m_reactionIDMap.size());
LOG_TRACE_L1(m_logger, "Populated {} reactions in the reaction ID map.", m_reactionIDMap.size());
}
void GraphNetwork::populateSpeciesToIndexMap() {
void GraphEngine::populateSpeciesToIndexMap() {
m_speciesToIndexMap.clear();
for (size_t i = 0; i < m_networkSpecies.size(); ++i) {
m_speciesToIndexMap.insert({m_networkSpecies[i], i});
}
}
void GraphNetwork::reserveJacobianMatrix() {
void GraphEngine::reserveJacobianMatrix() {
// The implementation of this function (and others) constrains this nuclear network to a constant temperature and density during
// each evaluation.
size_t numSpecies = m_networkSpecies.size();
m_jacobianMatrix.clear();
m_jacobianMatrix.resize(numSpecies, numSpecies, false); // Sparse matrix, no initial values
LOG_INFO(m_logger, "Jacobian matrix resized to {} rows and {} columns.",
LOG_TRACE_L2(m_logger, "Jacobian matrix resized to {} rows and {} columns.",
m_jacobianMatrix.size1(), m_jacobianMatrix.size2());
}
// --- Basic Accessors and Queries ---
const std::vector<fourdst::atomic::Species>& GraphNetwork::getNetworkSpecies() const {
const std::vector<fourdst::atomic::Species>& GraphEngine::getNetworkSpecies() const {
// Returns a constant reference to the vector of unique species in the network.
LOG_DEBUG(m_logger, "Providing access to network species vector. Size: {}.", m_networkSpecies.size());
return m_networkSpecies;
}
const reaclib::REACLIBReactionSet& GraphNetwork::getNetworkReactions() const {
const reaction::REACLIBLogicalReactionSet& GraphEngine::getNetworkReactions() const {
// Returns a constant reference to the set of reactions in the network.
LOG_DEBUG(m_logger, "Providing access to network reactions set. Size: {}.", m_reactions.size());
return m_reactions;
}
bool GraphNetwork::involvesSpecies(const fourdst::atomic::Species& species) const {
bool GraphEngine::involvesSpecies(const fourdst::atomic::Species& species) const {
// Checks if a given species is present in the network's species map for efficient lookup.
const bool found = m_networkSpeciesMap.contains(species.name());
LOG_DEBUG(m_logger, "Checking if species '{}' is involved in the network: {}.", species.name(), found ? "Yes" : "No");
return found;
}
std::unordered_map<fourdst::atomic::Species, int> GraphNetwork::getNetReactionStoichiometry(const reaclib::REACLIBReaction& reaction) const {
// Calculates the net stoichiometric coefficients for species in a given reaction.
std::unordered_map<fourdst::atomic::Species, int> stoichiometry;
// Iterate through reactants, decrementing their counts
for (const auto& reactant : reaction.reactants()) {
auto it = m_networkSpeciesMap.find(reactant.name());
if (it != m_networkSpeciesMap.end()) {
stoichiometry[it->second]--; // Copy Species by value (PERF: Future performance improvements by using pointers or references (std::reference_wrapper<const ...>) or something like that)
} else {
LOG_WARNING(m_logger, "Reactant species '{}' in reaction '{}' not found in network species map during stoichiometry calculation.",
reactant.name(), reaction.id());
}
}
// Iterate through products, incrementing their counts
for (const auto& product : reaction.products()) {
auto it = m_networkSpeciesMap.find(product.name());
if (it != m_networkSpeciesMap.end()) {
stoichiometry[it->second]++; // Copy Species by value (PERF: Future performance improvements by using pointers or references (std::reference_wrapper<const ...>) or something like that)
} else {
LOG_WARNING(m_logger, "Product species '{}' in reaction '{}' not found in network species map during stoichiometry calculation.",
product.name(), reaction.id());
}
}
LOG_DEBUG(m_logger, "Calculated net stoichiometry for reaction '{}'. Total unique species in stoichiometry: {}.", reaction.id(), stoichiometry.size());
return stoichiometry;
}
// --- Validation Methods ---
bool GraphNetwork::validateConservation() const {
LOG_INFO(m_logger, "Validating mass (A) and charge (Z) conservation across all reactions in the network.");
bool GraphEngine::validateConservation() const {
LOG_TRACE_L1(m_logger, "Validating mass (A) and charge (Z) conservation across all reactions in the network.");
for (const auto& reaction : m_reactions) {
uint64_t totalReactantA = 0;
@@ -170,7 +139,7 @@ namespace gridfire {
uint64_t totalProductZ = 0;
// Calculate total A and Z for reactants
for (const auto& reactant : reaction.reactants()) {
for (const auto& reactant : reaction->reactants()) {
auto it = m_networkSpeciesMap.find(reactant.name());
if (it != m_networkSpeciesMap.end()) {
totalReactantA += it->second.a();
@@ -179,13 +148,13 @@ namespace gridfire {
// This scenario indicates a severe data integrity issue:
// a reactant is part of a reaction but not in the network's species map.
LOG_ERROR(m_logger, "CRITICAL ERROR: Reactant species '{}' in reaction '{}' not found in network species map during conservation validation.",
reactant.name(), reaction.id());
reactant.name(), reaction->id());
return false;
}
}
// Calculate total A and Z for products
for (const auto& product : reaction.products()) {
for (const auto& product : reaction->products()) {
auto it = m_networkSpeciesMap.find(product.name());
if (it != m_networkSpeciesMap.end()) {
totalProductA += it->second.a();
@@ -193,7 +162,7 @@ namespace gridfire {
} else {
// Similar critical error for product species
LOG_ERROR(m_logger, "CRITICAL ERROR: Product species '{}' in reaction '{}' not found in network species map during conservation validation.",
product.name(), reaction.id());
product.name(), reaction->id());
return false;
}
}
@@ -201,25 +170,24 @@ namespace gridfire {
// Compare totals for conservation
if (totalReactantA != totalProductA) {
LOG_ERROR(m_logger, "Mass number (A) not conserved for reaction '{}': Reactants A={} vs Products A={}.",
reaction.id(), totalReactantA, totalProductA);
reaction->id(), totalReactantA, totalProductA);
return false;
}
if (totalReactantZ != totalProductZ) {
LOG_ERROR(m_logger, "Atomic number (Z) not conserved for reaction '{}': Reactants Z={} vs Products Z={}.",
reaction.id(), totalReactantZ, totalProductZ);
reaction->id(), totalReactantZ, totalProductZ);
return false;
}
}
LOG_INFO(m_logger, "Mass (A) and charge (Z) conservation validated successfully for all reactions.");
LOG_TRACE_L1(m_logger, "Mass (A) and charge (Z) conservation validated successfully for all reactions.");
return true; // All reactions passed the conservation check
}
void GraphNetwork::validateComposition(const fourdst::composition::Composition &composition, double culling, double T9) {
void GraphEngine::validateComposition(const fourdst::composition::Composition &composition, double culling, double T9) {
// Check if the requested network has already been cached.
// PERF: Rebuilding this should be pretty fast but it may be a good point of optimization in the future.
const reaclib::REACLIBReactionSet validationReactionSet = build_reaclib_nuclear_network(composition, culling, T9);
const reaction::REACLIBLogicalReactionSet validationReactionSet = build_reaclib_nuclear_network(composition, false);
// TODO: need some more robust method here to
// A. Build the basic network from the composition's species with non zero mass fractions.
// B. rebuild a new composition from the reaction set's reactants + products (with the mass fractions from the things that are only products set to 0)
@@ -230,22 +198,22 @@ namespace gridfire {
// This allows for dynamic network modification while retaining caching for networks which are very similar.
if (validationReactionSet != m_reactions) {
LOG_INFO(m_logger, "Reaction set not cached. Rebuilding the reaction set for T9={} and culling={}.", T9, culling);
LOG_DEBUG(m_logger, "Reaction set not cached. Rebuilding the reaction set for T9={} and culling={}.", T9, culling);
m_reactions = validationReactionSet;
syncInternalMaps(); // Re-sync internal maps after updating reactions. Note this will also retrace the AD tape.
}
}
// --- Generate Stoichiometry Matrix ---
void GraphNetwork::generateStoichiometryMatrix() {
LOG_INFO(m_logger, "Generating stoichiometry matrix...");
void GraphEngine::generateStoichiometryMatrix() {
LOG_TRACE_L1(m_logger, "Generating stoichiometry matrix...");
// Task 1: Set dimensions and initialize the matrix
size_t numSpecies = m_networkSpecies.size();
size_t numReactions = m_reactions.size();
m_stoichiometryMatrix.resize(numSpecies, numReactions, false);
LOG_INFO(m_logger, "Stoichiometry matrix initialized with dimensions: {} rows (species) x {} columns (reactions).",
LOG_TRACE_L1(m_logger, "Stoichiometry matrix initialized with dimensions: {} rows (species) x {} columns (reactions).",
numSpecies, numReactions);
// Task 2: Populate the stoichiometry matrix
@@ -253,13 +221,10 @@ namespace gridfire {
size_t reactionColumnIndex = 0;
for (const auto& reaction : m_reactions) {
// Get the net stoichiometry for the current reaction
std::unordered_map<fourdst::atomic::Species, int> netStoichiometry = getNetReactionStoichiometry(reaction);
std::unordered_map<fourdst::atomic::Species, int> netStoichiometry = reaction->stoichiometry();
// Iterate through the species and their coefficients in the stoichiometry map
for (const auto& pair : netStoichiometry) {
const fourdst::atomic::Species& species = pair.first; // The Species object
const int coefficient = pair.second; // The stoichiometric coefficient
for (const auto& [species, coefficient] : netStoichiometry) {
// Find the row index for this species
auto it = m_speciesToIndexMap.find(species);
if (it != m_speciesToIndexMap.end()) {
@@ -269,21 +234,49 @@ namespace gridfire {
} else {
// This scenario should ideally not happen if m_networkSpeciesMap and m_speciesToIndexMap are correctly synced
LOG_ERROR(m_logger, "CRITICAL ERROR: Species '{}' from reaction '{}' stoichiometry not found in species to index map.",
species.name(), reaction.id());
species.name(), reaction->id());
throw std::runtime_error("Species not found in species to index map: " + std::string(species.name()));
}
}
reactionColumnIndex++; // Move to the next column for the next reaction
}
LOG_INFO(m_logger, "Stoichiometry matrix population complete. Number of non-zero elements: {}.",
LOG_TRACE_L1(m_logger, "Stoichiometry matrix population complete. Number of non-zero elements: {}.",
m_stoichiometryMatrix.nnz()); // Assuming nnz() exists for compressed_matrix
}
void GraphNetwork::generateJacobianMatrix(const std::vector<double> &Y, const double T9,
const double rho) {
StepDerivatives<double> GraphEngine::calculateAllDerivatives(
const std::vector<double> &Y_in,
const double T9,
const double rho
) const {
return calculateAllDerivatives<double>(Y_in, T9, rho);
}
LOG_INFO(m_logger, "Generating jacobian matrix for T9={}, rho={}..", T9, rho);
StepDerivatives<ADDouble> GraphEngine::calculateAllDerivatives(
const std::vector<ADDouble> &Y_in,
const ADDouble T9,
const ADDouble rho
) const {
return calculateAllDerivatives<ADDouble>(Y_in, T9, rho);
}
double GraphEngine::calculateMolarReactionFlow(
const reaction::Reaction &reaction,
const std::vector<double> &Y,
const double T9,
const double rho
) const {
return calculateMolarReactionFlow<double>(reaction, Y, T9, rho);
}
void GraphEngine::generateJacobianMatrix(
const std::vector<double> &Y,
const double T9,
const double rho
) {
LOG_TRACE_L1(m_logger, "Generating jacobian matrix for T9={}, rho={}..", T9, rho);
const size_t numSpecies = m_networkSpecies.size();
// 1. Pack the input variables into a vector for CppAD
@@ -307,51 +300,28 @@ namespace gridfire {
}
}
}
LOG_INFO(m_logger, "Jacobian matrix generated with dimensions: {} rows x {} columns.", m_jacobianMatrix.size1(), m_jacobianMatrix.size2());
LOG_DEBUG(m_logger, "Jacobian matrix generated with dimensions: {} rows x {} columns.", m_jacobianMatrix.size1(), m_jacobianMatrix.size2());
}
void GraphNetwork::detectStiff(const NetIn &netIn, const double T9, const unsigned long numSpecies, const boost::numeric::ublas::vector<double>& Y) {
// --- Heuristic for automatic stiffness detection ---
const std::vector<double> initial_y_stl(Y.begin(), Y.begin() + numSpecies);
const auto [dydt, specificEnergyRate] = calculateAllDerivatives<double>(initial_y_stl, T9, netIn.density);
const std::vector<double>& initial_dotY = dydt;
double min_destruction_timescale = std::numeric_limits<double>::max();
for (size_t i = 0; i < numSpecies; ++i) {
if (Y(i) > MIN_ABUNDANCE_THRESHOLD && initial_dotY[i] < 0.0) {
const double timescale = std::abs(Y(i) / initial_dotY[i]);
if (timescale < min_destruction_timescale) {
min_destruction_timescale = timescale;
}
}
}
// If no species are being destroyed, the system is not stiff.
if (min_destruction_timescale == std::numeric_limits<double>::max()) {
LOG_INFO(m_logger, "No species are undergoing net destruction. Network is considered non-stiff.");
m_stiff = false;
return;
}
constexpr double saftey_factor = 10;
const bool is_stiff = (netIn.dt0 > saftey_factor * min_destruction_timescale);
LOG_INFO(m_logger, "Fastest destruction timescale: {}. Initial dt0: {}. Stiffness detected: {}.",
min_destruction_timescale, netIn.dt0, is_stiff ? "Yes" : "No");
if (is_stiff) {
m_stiff = true;
LOG_INFO(m_logger, "Network is detected as stiff.");
} else {
m_stiff = false;
LOG_INFO(m_logger, "Network is detected as non-stiff.");
}
double GraphEngine::getJacobianMatrixEntry(const int i, const int j) const {
return m_jacobianMatrix(i, j);
}
void GraphNetwork::exportToDot(const std::string &filename) const {
LOG_INFO(m_logger, "Exporting network graph to DOT file: {}", filename);
std::unordered_map<fourdst::atomic::Species, int> GraphEngine::getNetReactionStoichiometry(
const reaction::Reaction &reaction
) const {
return reaction.stoichiometry();
}
int GraphEngine::getStoichiometryMatrixEntry(
const int speciesIndex,
const int reactionIndex
) const {
return m_stoichiometryMatrix(speciesIndex, reactionIndex);
}
void GraphEngine::exportToDot(const std::string &filename) const {
LOG_TRACE_L1(m_logger, "Exporting network graph to DOT file: {}", filename);
std::ofstream dotFile(filename);
if (!dotFile.is_open()) {
@@ -375,118 +345,104 @@ namespace gridfire {
dotFile << " // --- Reaction Edges ---\n";
for (const auto& reaction : m_reactions) {
// Create a unique ID for the reaction node
std::string reactionNodeId = "reaction_" + std::string(reaction.id());
std::string reactionNodeId = "reaction_" + std::string(reaction->id());
// Define the reaction node (small, black dot)
dotFile << " \"" << reactionNodeId << "\" [shape=point, fillcolor=black, width=0.1, height=0.1, label=\"\"];\n";
// Draw edges from reactants to the reaction node
for (const auto& reactant : reaction.reactants()) {
for (const auto& reactant : reaction->reactants()) {
dotFile << " \"" << reactant.name() << "\" -> \"" << reactionNodeId << "\";\n";
}
// Draw edges from the reaction node to products
for (const auto& product : reaction.products()) {
dotFile << " \"" << reactionNodeId << "\" -> \"" << product.name() << "\" [label=\"" << reaction.qValue() << " MeV\"];\n";
for (const auto& product : reaction->products()) {
dotFile << " \"" << reactionNodeId << "\" -> \"" << product.name() << "\" [label=\"" << reaction->qValue() << " MeV\"];\n";
}
dotFile << "\n";
}
dotFile << "}\n";
dotFile.close();
LOG_INFO(m_logger, "Successfully exported network to {}", filename);
LOG_TRACE_L1(m_logger, "Successfully exported network to {}", filename);
}
NetOut GraphNetwork::evaluate(const NetIn &netIn) {
namespace ublas = boost::numeric::ublas;
namespace odeint = boost::numeric::odeint;
void GraphEngine::exportToCSV(const std::string &filename) const {
LOG_TRACE_L1(m_logger, "Exporting network graph to CSV file: {}", filename);
const double T9 = netIn.temperature / 1e9; // Convert temperature from Kelvin to T9 (T9 = T / 1e9)
// validateComposition(netIn.composition, netIn.culling, T9);
const unsigned long numSpecies = m_networkSpecies.size();
constexpr double abs_tol = 1.0e-8;
constexpr double rel_tol = 1.0e-8;
size_t stepCount = 0;
// TODO: Pull these out into configuration options
ODETerm rhs_functor(*this, T9, netIn.density);
ublas::vector<double> Y(numSpecies + 1);
for (size_t i = 0; i < numSpecies; ++i) {
const auto& species = m_networkSpecies[i];
// Get the mass fraction for this specific species from the input object
try {
Y(i) = netIn.composition.getMassFraction(std::string(species.name()));
} catch (const std::runtime_error &e) {
LOG_INFO(m_logger, "Species {} not in base composition, adding...", species.name());
Y(i) = 0.0; // If the species is not in the composition, set its mass fraction to
std::ofstream csvFile(filename, std::ios::out | std::ios::trunc);
if (!csvFile.is_open()) {
LOG_ERROR(m_logger, "Failed to open file for writing: {}", filename);
throw std::runtime_error("Failed to open file for writing: " + filename);
}
csvFile << "Reaction;Reactants;Products;Q-value;sources;rates\n";
for (const auto& reaction : m_reactions) {
// Dynamic cast to REACLIBReaction to access specific properties
csvFile << reaction->id() << ";";
// Reactants
int count = 0;
for (const auto& reactant : reaction->reactants()) {
csvFile << reactant.name();
if (++count < reaction->reactants().size()) {
csvFile << ",";
}
}
csvFile << ";";
count = 0;
for (const auto& product : reaction->products()) {
csvFile << product.name();
if (++count < reaction->products().size()) {
csvFile << ",";
}
}
csvFile << ";" << reaction->qValue() << ";";
// Reaction coefficients
auto* reaclibReaction = dynamic_cast<const reaction::REACLIBLogicalReaction*>(reaction.get());
if (!reaclibReaction) {
LOG_ERROR(m_logger, "Failed to cast Reaction to REACLIBLogicalReaction in GraphNetwork::exportToCSV().");
throw std::runtime_error("Failed to cast Reaction to REACLIBLogicalReaction in GraphNetwork::exportToCSV(). This should not happen, please check your reaction setup.");
}
auto sources = reaclibReaction->sources();
count = 0;
for (const auto& source : sources) {
csvFile << source;
if (++count < sources.size()) {
csvFile << ",";
}
}
csvFile << ";";
// Reaction coefficients
count = 0;
for (const auto& rates : *reaclibReaction) {
csvFile << rates;
if (++count < reaclibReaction->size()) {
csvFile << ",";
}
}
csvFile << "\n";
}
Y(numSpecies) = 0; // initial specific energy rate, will be updated later
detectStiff(netIn, T9, numSpecies, Y);
m_stiff = false;
if (m_stiff) {
JacobianTerm jacobian_functor(*this, T9, netIn.density);
LOG_INFO(m_logger, "Making use of stiff ODE solver for network evaluation.");
auto stepper = odeint::make_controlled<odeint::rosenbrock4<double>>(abs_tol, rel_tol);
stepCount = odeint::integrate_adaptive(
stepper,
std::make_pair(rhs_functor, jacobian_functor),
Y,
0.0, // Start time
netIn.tMax,
netIn.dt0
);
} else {
LOG_INFO(m_logger, "Making use of ODE solver (non-stiff) for network evaluation.");
using state_type = ublas::vector<double>;
auto stepper = odeint::make_controlled<odeint::runge_kutta_dopri5<state_type>>(abs_tol, rel_tol);
stepCount = odeint::integrate_adaptive(
stepper,
rhs_functor,
Y,
0.0, // Start time
netIn.tMax,
netIn.dt0
);
}
double sumY = 0.0;
for (int i = 0; i < numSpecies; ++i) { sumY += Y(i); }
for (int i = 0; i < numSpecies; ++i) { Y(i) /= sumY; }
// --- Marshall output variables ---
// PERF: Im sure this step could be tuned to avoid so many copies, that is a job for another day
std::vector<std::string> speciesNames;
speciesNames.reserve(numSpecies);
for (const auto& species : m_networkSpecies) {
speciesNames.push_back(std::string(species.name()));
}
std::vector<double> finalAbundances(Y.begin(), Y.begin() + numSpecies);
fourdst::composition::Composition outputComposition(speciesNames, finalAbundances);
outputComposition.finalize(true);
NetOut netOut;
netOut.composition = outputComposition;
netOut.num_steps = stepCount;
netOut.energy = Y(numSpecies); // The last element in Y is the specific energy rate
return netOut;
csvFile.close();
LOG_TRACE_L1(m_logger, "Successfully exported network graph to {}", filename);
}
void GraphNetwork::recordADTape() {
LOG_INFO(m_logger, "Recording AD tape for the RHS calculation...");
std::unordered_map<fourdst::atomic::Species, double> GraphEngine::getSpeciesTimescales(const std::vector<double> &Y, const double T9,
const double rho) const {
auto [dydt, _] = calculateAllDerivatives<double>(Y, T9, rho);
std::unordered_map<fourdst::atomic::Species, double> speciesTimescales;
speciesTimescales.reserve(m_networkSpecies.size());
for (size_t i = 0; i < m_networkSpecies.size(); ++i) {
double timescale = std::numeric_limits<double>::infinity();
const auto species = m_networkSpecies[i];
if (std::abs(dydt[i]) > 0.0) {
timescale = std::abs(Y[i] / dydt[i]);
}
speciesTimescales.emplace(species, timescale);
}
return speciesTimescales;
}
void GraphEngine::recordADTape() {
LOG_TRACE_L1(m_logger, "Recording AD tape for the RHS calculation...");
// Task 1: Set dimensions and initialize the matrix
const size_t numSpecies = m_networkSpecies.size();
@@ -521,11 +477,11 @@ namespace gridfire {
// 5. Call the actual templated function
// We let T9 and rho be constant, so we pass them as fixed values.
auto derivatives = calculateAllDerivatives<CppAD::AD<double>>(adY, adT9, adRho);
auto [dydt, nuclearEnergyGenerationRate] = calculateAllDerivatives<CppAD::AD<double>>(adY, adT9, adRho);
m_rhsADFun.Dependent(adInput, derivatives.dydt);
m_rhsADFun.Dependent(adInput, dydt);
LOG_INFO(m_logger, "AD tape recorded successfully for the RHS calculation. Number of independent variables: {}.",
LOG_TRACE_L1(m_logger, "AD tape recorded successfully for the RHS calculation. Number of independent variables: {}.",
adInput.size());
}
}

View File

@@ -19,13 +19,13 @@
//
// *********************************************************************** */
#include "gridfire/network.h"
#include "gridfire/reactions.h"
#include "../include/gridfire/reaction/reaction.h"
#include <ranges>
#include <fstream>
#include "gridfire/approx8.h"
#include "quill/LogMacros.h"
#include "gridfire/reaclib.h"
#include "gridfire/reactions.h"
namespace gridfire {
Network::Network(const NetworkFormat format) :
@@ -50,231 +50,20 @@ namespace gridfire {
return oldFormat;
}
NetOut Network::evaluate(const NetIn &netIn) {
NetOut netOut;
switch (m_format) {
case APPROX8: {
approx8::Approx8Network network;
netOut = network.evaluate(netIn);
break;
}
case UNKNOWN: {
LOG_ERROR(m_logger, "Network format {} is not implemented.", FormatStringLookup.at(m_format));
throw std::runtime_error("Network format not implemented.");
}
default: {
LOG_ERROR(m_logger, "Unknown network format.");
throw std::runtime_error("Unknown network format.");
}
}
return netOut;
}
LogicalReaction::LogicalReaction(const std::vector<reaclib::REACLIBReaction> &reactions) {
if (reactions.empty()) {
LOG_ERROR(m_logger, "Empty reaction set provided to LogicalReaction constructor.");
throw std::runtime_error("Empty reaction set provided to LogicalReaction constructor.");
}
const auto& first_reaction = reactions.front();
m_peID = first_reaction.peName();
m_reactants = first_reaction.reactants();
m_products = first_reaction.products();
m_qValue = first_reaction.qValue();
m_reverse = first_reaction.is_reverse();
m_chapter = first_reaction.chapter();
m_rates.reserve(reactions.size());
for (const auto& reaction : reactions) {
m_rates.push_back(reaction.rateFits());
if (std::abs(reaction.qValue() - m_qValue) > 1e-6) {
LOG_ERROR(m_logger, "Inconsistent Q-values in reactions {}. All reactions must have the same Q-value.", m_peID);
throw std::runtime_error("Inconsistent Q-values in reactions. All reactions must have the same Q-value.");
}
}
}
LogicalReaction::LogicalReaction(const reaclib::REACLIBReaction &first_reaction) {
m_peID = first_reaction.peName();
m_reactants = first_reaction.reactants();
m_products = first_reaction.products();
m_qValue = first_reaction.qValue();
m_reverse = first_reaction.is_reverse();
m_chapter = first_reaction.chapter();
m_rates.reserve(1);
m_rates.push_back(first_reaction.rateFits());
}
void LogicalReaction::add_reaction(const reaclib::REACLIBReaction &reaction) {
if (std::abs(reaction.qValue() - m_qValue > 1e-6)) {
LOG_ERROR(m_logger, "Inconsistent Q-values in reactions {}. All reactions must have the same Q-value.", m_peID);
throw std::runtime_error("Inconsistent Q-values in reactions. All reactions must have the same Q-value.");
}
m_rates.push_back(reaction.rateFits());
}
auto LogicalReaction::begin() {
return m_rates.begin();
}
auto LogicalReaction::begin() const {
return m_rates.cbegin();
}
auto LogicalReaction::end() {
return m_rates.end();
}
auto LogicalReaction::end() const {
return m_rates.cend();
}
LogicalReactionSet::LogicalReactionSet(const std::vector<LogicalReaction> &reactions) {
if (reactions.empty()) {
LOG_ERROR(m_logger, "Empty reaction set provided to LogicalReactionSet constructor.");
throw std::runtime_error("Empty reaction set provided to LogicalReactionSet constructor.");
}
for (const auto& reaction : reactions) {
m_reactions.push_back(reaction);
}
m_reactionNameMap.reserve(m_reactions.size());
for (const auto& reaction : m_reactions) {
if (m_reactionNameMap.contains(reaction.id())) {
LOG_ERROR(m_logger, "Duplicate reaction ID '{}' found in LogicalReactionSet.", reaction.id());
throw std::runtime_error("Duplicate reaction ID found in LogicalReactionSet: " + std::string(reaction.id()));
}
m_reactionNameMap[reaction.id()] = reaction;
}
}
LogicalReactionSet::LogicalReactionSet(const std::vector<reaclib::REACLIBReaction> &reactions) {
std::vector<std::string_view> uniquePeNames;
for (const auto& reaction: reactions) {
if (uniquePeNames.empty()) {
uniquePeNames.emplace_back(reaction.peName());
} else if (std::ranges::find(uniquePeNames, reaction.peName()) == uniquePeNames.end()) {
uniquePeNames.emplace_back(reaction.peName());
}
}
for (const auto& peName : uniquePeNames) {
std::vector<reaclib::REACLIBReaction> reactionsForPe;
for (const auto& reaction : reactions) {
if (reaction.peName() == peName) {
reactionsForPe.push_back(reaction);
}
}
m_reactions.emplace_back(reactionsForPe);
}
m_reactionNameMap.reserve(m_reactions.size());
for (const auto& reaction : m_reactions) {
if (m_reactionNameMap.contains(reaction.id())) {
LOG_ERROR(m_logger, "Duplicate reaction ID '{}' found in LogicalReactionSet.", reaction.id());
throw std::runtime_error("Duplicate reaction ID found in LogicalReactionSet: " + std::string(reaction.id()));
}
m_reactionNameMap[reaction.id()] = reaction;
}
}
LogicalReactionSet::LogicalReactionSet(const reaclib::REACLIBReactionSet &reactionSet) {
LogicalReactionSet(reactionSet.get_reactions());
}
void LogicalReactionSet::add_reaction(const LogicalReaction& reaction) {
if (m_reactionNameMap.contains(reaction.id())) {
LOG_WARNING(m_logger, "Reaction {} already exists in the set. Not adding again.", reaction.id());
std::cerr << "Warning: Reaction " << reaction.id() << " already exists in the set. Not adding again." << std::endl;
return;
}
m_reactions.push_back(reaction);
m_reactionNameMap[reaction.id()] = reaction;
}
void LogicalReactionSet::add_reaction(const reaclib::REACLIBReaction& reaction) {
if (m_reactionNameMap.contains(reaction.id())) {
m_reactionNameMap[reaction.id()].add_reaction(reaction);
} else {
const LogicalReaction logicalReaction(reaction);
m_reactions.push_back(logicalReaction);
m_reactionNameMap[reaction.id()] = logicalReaction;
}
}
bool LogicalReactionSet::contains(const std::string_view &id) const {
for (const auto& reaction : m_reactions) {
if (reaction.id() == id) {
return true;
}
}
return false;
}
bool LogicalReactionSet::contains(const LogicalReaction &reactions) const {
for (const auto& reaction : m_reactions) {
if (reaction.id() == reactions.id()) {
return true;
}
}
return false;
}
bool LogicalReactionSet::contains_species(const fourdst::atomic::Species &species) const {
return contains_reactant(species) || contains_product(species);
}
bool LogicalReactionSet::contains_reactant(const fourdst::atomic::Species &species) const {
for (const auto& reaction : m_reactions) {
if (std::ranges::find(reaction.reactants(), species) != reaction.reactants().end()) {
return true;
}
}
return false;
}
bool LogicalReactionSet::contains_product(const fourdst::atomic::Species &species) const {
for (const auto& reaction : m_reactions) {
if (std::ranges::find(reaction.products(), species) != reaction.products().end()) {
return true;
}
}
return false;
}
const LogicalReaction & LogicalReactionSet::operator[](size_t index) const {
return m_reactions.at(index);
}
const LogicalReaction & LogicalReactionSet::operator[](const std::string_view &id) const {
return m_reactionNameMap.at(id);
}
auto LogicalReactionSet::begin() {
return m_reactions.begin();
}
auto LogicalReactionSet::begin() const {
return m_reactions.cbegin();
}
auto LogicalReactionSet::end() {
return m_reactions.end();
}
auto LogicalReactionSet::end() const {
return m_reactions.cend();
}
LogicalReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition) {
using namespace reaclib;
REACLIBReactionSet reactions;
reaction::REACLIBLogicalReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition, bool reverse) {
using namespace reaction;
std::vector<reaction::REACLIBReaction> reaclibReactions;
auto logger = fourdst::logging::LogManager::getInstance().getLogger("log");
if (!s_initialized) {
LOG_INFO(logger, "REACLIB reactions not initialized. Calling initializeAllReaclibReactions()...");
initializeAllReaclibReactions();
if (!reaclib::s_initialized) {
LOG_DEBUG(logger, "REACLIB reactions not initialized. Calling initializeAllReaclibReactions()...");
reaclib::initializeAllReaclibReactions();
}
for (const auto &reaction: s_all_reaclib_reactions | std::views::values) {
for (const auto &reaction: reaclib::s_all_reaclib_reactions | std::views::values) {
if (reaction.is_reverse() != reverse) {
continue; // Skip reactions that do not match the requested direction
}
bool gotReaction = true;
const auto& reactants = reaction.reactants();
for (const auto& reactant : reactants) {
@@ -284,24 +73,85 @@ namespace gridfire {
}
}
if (gotReaction) {
LOG_INFO(logger, "Adding reaction {} to REACLIB reaction set.", reaction.peName());
reactions.add_reaction(reaction);
LOG_TRACE_L3(logger, "Adding reaction {} to REACLIB reaction set.", reaction.peName());
reaclibReactions.push_back(reaction);
}
}
reactions.sort();
return LogicalReactionSet(reactions);
const REACLIBReactionSet reactionSet(reaclibReactions);
return REACLIBLogicalReactionSet(reactionSet);
}
LogicalReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition, const double culling, const double T9) {
using namespace reaclib;
LogicalReactionSet allReactions = build_reaclib_nuclear_network(composition);
LogicalReactionSet reactions;
for (const auto& reaction : allReactions) {
if (reaction.calculate_rate(T9) >= culling) {
reactions.add_reaction(reaction);
// Trim whitespace from both ends of a string
std::string trim_whitespace(const std::string& str) {
auto startIt = str.begin();
auto endIt = str.end();
while (startIt != endIt && std::isspace(static_cast<unsigned char>(*startIt))) {
++startIt;
}
if (startIt == endIt) {
return "";
}
auto ritr = std::find_if(str.rbegin(), std::string::const_reverse_iterator(startIt),
[](unsigned char ch){ return !std::isspace(ch); });
return std::string(startIt, ritr.base());
}
reaction::REACLIBLogicalReactionSet build_reaclib_nuclear_network_from_file(const std::string& filename, bool reverse) {
const auto logger = fourdst::logging::LogManager::getInstance().getLogger("log");
std::vector<std::string> reactionPENames;
std::ifstream infile(filename, std::ios::in);
if (!infile.is_open()) {
LOG_ERROR(logger, "Failed to open network definition file: {}", filename);
throw std::runtime_error("Failed to open network definition file: " + filename);
}
std::string line;
while (std::getline(infile, line)) {
std::string trimmedLine = trim_whitespace(line);
if (trimmedLine.empty() || trimmedLine.starts_with('#')) {
continue; // Skip empty lines and comments
}
reactionPENames.push_back(trimmedLine);
}
infile.close();
std::vector<reaction::REACLIBReaction> reaclibReactions;
if (reactionPENames.empty()) {
LOG_ERROR(logger, "No reactions found in the network definition file: {}", filename);
throw std::runtime_error("No reactions found in the network definition file: " + filename);
}
if (!reaclib::s_initialized) {
LOG_DEBUG(logger, "REACLIB reactions not initialized. Calling initializeAllReaclibReactions()...");
reaclib::initializeAllReaclibReactions();
} else {
LOG_DEBUG(logger, "REACLIB reactions already initialized.");
}
for (const auto& peName : reactionPENames) {
bool found = false;
for (const auto& reaction : reaclib::s_all_reaclib_reactions | std::views::values) {
if (reaction.peName() == peName && reaction.is_reverse() == reverse) {
reaclibReactions.push_back(reaction);
found = true;
LOG_TRACE_L3(logger, "Found reaction {} (version {}) in REACLIB database.", peName, reaction.sourceLabel());
}
}
if (!found) {
LOG_ERROR(logger, "Reaction {} not found in REACLIB database. Skipping...", peName);
throw std::runtime_error("Reaction not found in REACLIB database.");
}
}
return reactions;
// Log any reactions that were not found in the REACLIB database
for (const auto& peName : reactionPENames) {
if (std::ranges::find(reaclibReactions, peName, &reaction::REACLIBReaction::peName) == reaclibReactions.end()) {
LOG_WARNING(logger, "Reaction {} not found in REACLIB database.", peName);
}
}
const reaction::REACLIBReactionSet reactionSet(reaclibReactions);
return reaction::REACLIBLogicalReactionSet(reactionSet);
}
}

View File

@@ -0,0 +1,427 @@
#include "gridfire/reaction/reaction.h"
#include<string_view>
#include<string>
#include<vector>
#include<memory>
#include<unordered_set>
#include<algorithm>
#include "quill/LogMacros.h"
#include "fourdst/composition/atomicSpecies.h"
#include "xxhash64.h"
namespace gridfire::reaction {
using namespace fourdst::atomic;
Reaction::Reaction(
const std::string_view id,
const double qValue,
const std::vector<Species>& reactants,
const std::vector<Species>& products,
const bool reverse) :
m_id(id),
m_qValue(qValue),
m_reactants(std::move(reactants)),
m_products(std::move(products)),
m_reverse(reverse) {}
bool Reaction::contains(const Species &species) const {
return contains_reactant(species) || contains_product(species);
}
bool Reaction::contains_reactant(const Species& species) const {
for (const auto& reactant : m_reactants) {
if (reactant == species) {
return true;
}
}
return false;
}
bool Reaction::contains_product(const Species& species) const {
for (const auto& product : m_products) {
if (product == species) {
return true;
}
}
return false;
}
std::unordered_set<Species> Reaction::all_species() const {
auto rs = reactant_species();
auto ps = product_species();
rs.insert(ps.begin(), ps.end());
return rs;
}
std::unordered_set<Species> Reaction::reactant_species() const {
std::unordered_set<Species> reactantsSet;
for (const auto& reactant : m_reactants) {
reactantsSet.insert(reactant);
}
return reactantsSet;
}
std::unordered_set<Species> Reaction::product_species() const {
std::unordered_set<Species> productsSet;
for (const auto& product : m_products) {
productsSet.insert(product);
}
return productsSet;
}
int Reaction::stoichiometry(const Species& species) const {
int s = 0;
for (const auto& reactant : m_reactants) {
if (reactant == species) {
s--;
}
}
for (const auto& product : m_products) {
if (product == species) {
s++;
}
}
return s;
}
size_t Reaction::num_species() const {
return all_species().size();
}
std::unordered_map<Species, int> Reaction::stoichiometry() const {
std::unordered_map<Species, int> stoichiometryMap;
for (const auto& reactant : m_reactants) {
stoichiometryMap[reactant]--;
}
for (const auto& product : m_products) {
stoichiometryMap[product]++;
}
return stoichiometryMap;
}
double Reaction::excess_energy() const {
double reactantMass = 0.0;
double productMass = 0.0;
constexpr double AMU2MeV = 931.494893; // Conversion factor from atomic mass unit to MeV
for (const auto& reactant : m_reactants) {
reactantMass += reactant.mass();
}
for (const auto& product : m_products) {
productMass += product.mass();
}
return (reactantMass - productMass) * AMU2MeV;
}
uint64_t Reaction::hash(uint64_t seed) const {
return XXHash64::hash(m_id.data(), m_id.size(), seed);
}
ReactionSet::ReactionSet(
std::vector<std::unique_ptr<Reaction>> reactions) :
m_reactions(std::move(reactions)) {
if (m_reactions.empty()) {
return; // Case where the reactions will be added later.
}
m_reactionNameMap.reserve(reactions.size());
for (const auto& reaction : m_reactions) {
m_id += reaction->id();
m_reactionNameMap.emplace(reaction->id(), reaction.get());
}
}
ReactionSet::ReactionSet(const ReactionSet &other) {
m_reactions.reserve(other.m_reactions.size());
for (const auto& reaction_ptr: other.m_reactions) {
m_reactions.push_back(reaction_ptr->clone());
}
m_reactionNameMap.reserve(other.m_reactionNameMap.size());
for (const auto& reaction_ptr : m_reactions) {
m_reactionNameMap.emplace(reaction_ptr->id(), reaction_ptr.get());
}
}
ReactionSet & ReactionSet::operator=(const ReactionSet &other) {
if (this != &other) {
ReactionSet temp(other);
std::swap(m_reactions, temp.m_reactions);
std::swap(m_reactionNameMap, temp.m_reactionNameMap);
}
return *this;
}
void ReactionSet::add_reaction(std::unique_ptr<Reaction> reaction) {
m_reactions.emplace_back(std::move(reaction));
m_id += m_reactions.back()->id();
m_reactionNameMap.emplace(m_reactions.back()->id(), m_reactions.back().get());
}
void ReactionSet::remove_reaction(const std::unique_ptr<Reaction>& reaction) {
if (!m_reactionNameMap.contains(std::string(reaction->id()))) {
// LOG_INFO(m_logger, "Attempted to remove reaction {} that does not exist in ReactionSet. Skipping...", reaction->id());
return;
}
m_reactionNameMap.erase(std::string(reaction->id()));
std::erase_if(m_reactions, [&reaction](const std::unique_ptr<Reaction>& r) {
return *r == *reaction;
});
}
bool ReactionSet::contains(const std::string_view& id) const {
for (const auto& reaction : m_reactions) {
if (reaction->id() == id) {
return true;
}
}
return false;
}
bool ReactionSet::contains(const Reaction& reaction) const {
for (const auto& r : m_reactions) {
if (*r == reaction) {
return true;
}
}
return false;
}
void ReactionSet::sort(double T9) {
std::ranges::sort(m_reactions,
[&T9](const std::unique_ptr<Reaction>& r1, const std::unique_ptr<Reaction>& r2) {
return r1->calculate_rate(T9) < r2->calculate_rate(T9);
});
}
bool ReactionSet::contains_species(const Species& species) const {
for (const auto& reaction : m_reactions) {
if (reaction->contains(species)) {
return true;
}
}
return false;
}
bool ReactionSet::contains_reactant(const Species& species) const {
for (const auto& r : m_reactions) {
if (r->contains_reactant(species)) {
return true;
}
}
return false;
}
bool ReactionSet::contains_product(const Species& species) const {
for (const auto& r : m_reactions) {
if (r->contains_product(species)) {
return true;
}
}
return false;
}
const Reaction& ReactionSet::operator[](const size_t index) const {
if (index >= m_reactions.size()) {
throw std::out_of_range("Index" + std::to_string(index) + " out of range for ReactionSet of size " + std::to_string(m_reactions.size()) + ".");
}
return *m_reactions[index];
}
const Reaction& ReactionSet::operator[](const std::string_view& id) const {
if (auto it = m_reactionNameMap.find(std::string(id)); it != m_reactionNameMap.end()) {
return *it->second;
}
throw std::out_of_range("Species " + std::string(id) + " does not exist in ReactionSet.");
}
bool ReactionSet::operator==(const ReactionSet& other) const {
if (size() != other.size()) {
return false;
}
return hash() == other.hash();
}
bool ReactionSet::operator!=(const ReactionSet& other) const {
return !(*this == other);
}
uint64_t ReactionSet::hash(uint64_t seed) const {
if (m_reactions.empty()) {
return XXHash64::hash(nullptr, 0, seed);
}
std::vector<uint64_t> individualReactionHashes;
individualReactionHashes.reserve(m_reactions.size());
for (const auto& reaction : m_reactions) {
individualReactionHashes.push_back(reaction->hash(seed));
}
std::ranges::sort(individualReactionHashes);
const void* data = static_cast<const void*>(individualReactionHashes.data());
size_t sizeInBytes = individualReactionHashes.size() * sizeof(uint64_t);
return XXHash64::hash(data, sizeInBytes, seed);
}
REACLIBReaction::REACLIBReaction(
const std::string_view id,
const std::string_view peName,
const int chapter,
const std::vector<Species> &reactants,
const std::vector<Species> &products,
const double qValue,
const std::string_view label,
const REACLIBRateCoefficientSet &sets,
const bool reverse) :
Reaction(id, qValue, reactants, products, reverse),
m_peName(peName),
m_chapter(chapter),
m_sourceLabel(label),
m_rateCoefficients(sets) {}
std::unique_ptr<Reaction> REACLIBReaction::clone() const {
return std::make_unique<REACLIBReaction>(*this);
}
double REACLIBReaction::calculate_rate(const double T9) const {
return calculate_rate<double>(T9);
}
CppAD::AD<double> REACLIBReaction::calculate_rate(const CppAD::AD<double> T9) const {
return calculate_rate<CppAD::AD<double>>(T9);
}
REACLIBReactionSet::REACLIBReactionSet(std::vector<REACLIBReaction> reactions) :
ReactionSet(std::vector<std::unique_ptr<Reaction>>()) {
// Convert REACLIBReaction to unique_ptr<Reaction> and store in m_reactions
m_reactions.reserve(reactions.size());
m_reactionNameMap.reserve(reactions.size());
for (auto& reaction : reactions) {
m_reactions.emplace_back(std::make_unique<REACLIBReaction>(std::move(reaction)));
m_reactionNameMap.emplace(std::string(reaction.id()), m_reactions.back().get());
}
}
std::unordered_set<std::string> REACLIBReactionSet::peNames() const {
std::unordered_set<std::string> peNames;
for (const auto& reactionPtr: m_reactions) {
if (const auto* reaction = dynamic_cast<REACLIBReaction*>(reactionPtr.get())) {
peNames.insert(std::string(reaction->peName()));
} else {
// LOG_ERROR(m_logger, "Failed to cast Reaction to REACLIBReaction in REACLIBReactionSet::peNames().");
throw std::runtime_error("Failed to cast Reaction to REACLIBReaction in REACLIBReactionSet::peNames(). This should not happen, please check your reaction setup.");
}
}
return peNames;
}
REACLIBLogicalReaction::REACLIBLogicalReaction(const std::vector<REACLIBReaction>& reactants) :
Reaction(reactants.front().peName(),
reactants.front().qValue(),
reactants.front().reactants(),
reactants.front().products(),
reactants.front().is_reverse()),
m_chapter(reactants.front().chapter()) {
m_sources.reserve(reactants.size());
m_rates.reserve(reactants.size());
for (const auto& reaction : reactants) {
if (std::abs(reaction.qValue() - m_qValue) > 1e-6) {
LOG_ERROR(m_logger, "REACLIBLogicalReaction constructed with reactions having different Q-values. Expected {} got {}.", m_qValue, reaction.qValue());
throw std::runtime_error("REACLIBLogicalReaction constructed with reactions having different Q-values. Expected " + std::to_string(m_qValue) + " got " + std::to_string(reaction.qValue()) + ".");
}
m_sources.push_back(std::string(reaction.sourceLabel()));
m_rates.push_back(reaction.rateCoefficients());
}
}
REACLIBLogicalReaction::REACLIBLogicalReaction(const REACLIBReaction& reaction) :
Reaction(reaction.peName(),
reaction.qValue(),
reaction.reactants(),
reaction.products(),
reaction.is_reverse()),
m_chapter(reaction.chapter()) {
m_sources.reserve(1);
m_rates.reserve(1);
m_sources.push_back(std::string(reaction.sourceLabel()));
m_rates.push_back(reaction.rateCoefficients());
}
void REACLIBLogicalReaction::add_reaction(const REACLIBReaction& reaction) {
if (reaction.peName() != m_id) {
LOG_ERROR(m_logger, "Cannot add reaction with different peName to REACLIBLogicalReaction. Expected {} got {}.", m_id, reaction.peName());
throw std::runtime_error("Cannot add reaction with different peName to REACLIBLogicalReaction. Expected " + std::string(m_id) + " got " + std::string(reaction.peName()) + ".");
}
for (const auto& source : m_sources) {
if (source == reaction.sourceLabel()) {
LOG_ERROR(m_logger, "Cannot add reaction with duplicate source label {} to REACLIBLogicalReaction.", reaction.sourceLabel());
throw std::runtime_error("Cannot add reaction with duplicate source label " + std::string(reaction.sourceLabel()) + " to REACLIBLogicalReaction.");
}
}
if (std::abs(reaction.qValue() - m_qValue) > 1e-6) {
LOG_ERROR(m_logger, "REACLIBLogicalReaction constructed with reactions having different Q-values. Expected {} got {}.", m_qValue, reaction.qValue());
throw std::runtime_error("REACLIBLogicalReaction constructed with reactions having different Q-values. Expected " + std::to_string(m_qValue) + " got " + std::to_string(reaction.qValue()) + ".");
}
m_sources.push_back(std::string(reaction.sourceLabel()));
m_rates.push_back(reaction.rateCoefficients());
}
std::unique_ptr<Reaction> REACLIBLogicalReaction::clone() const {
return std::make_unique<REACLIBLogicalReaction>(*this);
}
double REACLIBLogicalReaction::calculate_rate(const double T9) const {
return calculate_rate<double>(T9);
}
CppAD::AD<double> REACLIBLogicalReaction::calculate_rate(const CppAD::AD<double> T9) const {
return calculate_rate<CppAD::AD<double>>(T9);
}
REACLIBLogicalReactionSet::REACLIBLogicalReactionSet(const REACLIBReactionSet &reactionSet) :
ReactionSet(std::vector<std::unique_ptr<Reaction>>()) {
std::unordered_map<std::string_view, std::vector<REACLIBReaction>> grouped_reactions;
for (const auto& reaction_ptr : reactionSet) {
if (const auto* reaclib_reaction = dynamic_cast<const REACLIBReaction*>(reaction_ptr.get())) {
grouped_reactions[reaclib_reaction->peName()].push_back(*reaclib_reaction);
}
}
m_reactions.reserve(grouped_reactions.size());
m_reactionNameMap.reserve(grouped_reactions.size());
for (const auto& [peName, reactions_for_peName] : grouped_reactions) {
m_peNames.insert(std::string(peName));
auto logical_reaction = std::make_unique<REACLIBLogicalReaction>(reactions_for_peName);
m_reactionNameMap.emplace(logical_reaction->id(), logical_reaction.get());
m_reactions.push_back(std::move(logical_reaction));
}
}
std::unordered_set<std::string> REACLIBLogicalReactionSet::peNames() const {
return m_peNames;
}
}
namespace std {
template<>
struct hash<gridfire::reaction::Reaction> {
size_t operator()(const gridfire::reaction::Reaction& r) const noexcept {
return r.hash(0);
}
};
template<>
struct hash<gridfire::reaction::ReactionSet> {
size_t operator()(const gridfire::reaction::ReactionSet& s) const noexcept {
return s.hash(0);
}
};
} // namespace std

View File

@@ -0,0 +1,384 @@
#include "gridfire/solver/solver.h"
#include "gridfire/engine/engine_graph.h"
#include "gridfire/network.h"
#include "fourdst/composition/atomicSpecies.h"
#include "fourdst/composition/composition.h"
#include "fourdst/config/config.h"
#include "Eigen/Dense"
#include "unsupported/Eigen/NonLinearOptimization"
#include <boost/numeric/odeint.hpp>
#include <vector>
#include <unordered_map>
#include <string>
#include <stdexcept>
#include "quill/LogMacros.h"
namespace gridfire::solver {
NetOut QSENetworkSolver::evaluate(const NetIn &netIn) {
using state_type = boost::numeric::ublas::vector<double>;
using namespace boost::numeric::odeint;
NetOut postIgnition = initializeNetworkWithShortIgnition(netIn);
constexpr double abundance_floor = 1.0e-30;
std::vector<double>Y_sanitized_initial;
Y_sanitized_initial.reserve(m_engine.getNetworkSpecies().size());
LOG_DEBUG(m_logger, "Sanitizing initial abundances with a floor of {:0.3E}...", abundance_floor);
for (const auto& species : m_engine.getNetworkSpecies()) {
double molar_abundance = 0.0;
if (postIgnition.composition.contains(species)) {
molar_abundance = postIgnition.composition.getMolarAbundance(std::string(species.name()));
}
if (molar_abundance < abundance_floor) {
molar_abundance = abundance_floor;
}
Y_sanitized_initial.push_back(molar_abundance);
}
const double T9 = netIn.temperature / 1e9; // Convert temperature from Kelvin to T9 (T9 = T / 1e9)
const double rho = netIn.density; // Density in g/cm^3
const auto indices = packSpeciesTypeIndexVectors(Y_sanitized_initial, T9, rho);
Eigen::VectorXd Y_QSE;
try {
Y_QSE = calculateSteadyStateAbundances(Y_sanitized_initial, T9, rho, indices);
} catch (const std::runtime_error& e) {
LOG_ERROR(m_logger, "Failed to calculate steady state abundances. Aborting QSE evaluation.");
m_logger->flush_log();
throw std::runtime_error("Failed to calculate steady state abundances: " + std::string(e.what()));
}
state_type YDynamic_ublas(indices.dynamicSpeciesIndices.size() + 1);
for (size_t i = 0; i < indices.dynamicSpeciesIndices.size(); ++i) {
YDynamic_ublas(i) = Y_sanitized_initial[indices.dynamicSpeciesIndices[i]];
}
YDynamic_ublas(indices.dynamicSpeciesIndices.size()) = 0.0; // Placeholder for specific energy rate
const RHSFunctor rhs_functor(m_engine, indices.dynamicSpeciesIndices, indices.QSESpeciesIndices, Y_QSE, T9, rho);
const auto stepper = make_controlled<runge_kutta_dopri5<state_type>>(1.0e-8, 1.0e-8);
size_t stepCount = integrate_adaptive(
stepper,
rhs_functor,
YDynamic_ublas,
0.0, // Start time
netIn.tMax,
netIn.dt0
);
std::vector<double> YFinal = Y_sanitized_initial;
for (size_t i = 0; i <indices.dynamicSpeciesIndices.size(); ++i) {
YFinal[indices.dynamicSpeciesIndices[i]] = YDynamic_ublas(i);
}
for (size_t i = 0; i < indices.QSESpeciesIndices.size(); ++i) {
YFinal[indices.QSESpeciesIndices[i]] = Y_QSE(i);
}
const double finalSpecificEnergyRate = YDynamic_ublas(indices.dynamicSpeciesIndices.size());
// --- Marshal output variables ---
std::vector<std::string> speciesNames(m_engine.getNetworkSpecies().size());
std::vector<double> finalMassFractions(m_engine.getNetworkSpecies().size());
double massFractionSum = 0.0;
for (size_t i = 0; i < speciesNames.size(); ++i) {
const auto& species = m_engine.getNetworkSpecies()[i];
speciesNames[i] = species.name();
finalMassFractions[i] = YFinal[i] * species.mass(); // Convert from molar abundance to mass fraction
massFractionSum += finalMassFractions[i];
}
for (auto& mf : finalMassFractions) {
mf /= massFractionSum; // Normalize to get mass fractions
}
fourdst::composition::Composition outputComposition(speciesNames, finalMassFractions);
NetOut netOut;
netOut.composition = outputComposition;
netOut.energy = finalSpecificEnergyRate; // Specific energy rate
netOut.num_steps = stepCount;
return netOut;
}
dynamicQSESpeciesIndices QSENetworkSolver::packSpeciesTypeIndexVectors(
const std::vector<double>& Y,
const double T9,
const double rho
) const {
constexpr double timescaleCutoff = 1.0e-5;
constexpr double abundanceCutoff = 1.0e-15;
LOG_INFO(m_logger, "Partitioning species using T9={:0.2f} and ρ={:0.2e}", T9, rho);
LOG_INFO(m_logger, "Timescale Cutoff: {:.1e} s, Abundance Cutoff: {:.1e}", timescaleCutoff, abundanceCutoff);
std::vector<size_t>dynamicSpeciesIndices; // Slow species that are not in QSE
std::vector<size_t>QSESpeciesIndices; // Fast species that are in QSE
std::unordered_map<fourdst::atomic::Species, double> speciesTimescale = m_engine.getSpeciesTimescales(Y, T9, rho);
for (size_t i = 0; i < m_engine.getNetworkSpecies().size(); ++i) {
const auto& species = m_engine.getNetworkSpecies()[i];
const double timescale = speciesTimescale[species];
const double abundance = Y[i];
if (std::isinf(timescale) || abundance < abundanceCutoff || timescale <= timescaleCutoff) {
QSESpeciesIndices.push_back(i);
} else {
dynamicSpeciesIndices.push_back(i);
}
}
LOG_INFO(m_logger, "Partitioning complete. Dynamical species: {}, QSE species: {}.", dynamicSpeciesIndices.size(), QSESpeciesIndices.size());
LOG_INFO(m_logger, "Dynamic species: {}", [dynamicSpeciesIndices](const DynamicEngine& engine_wrapper) -> std::string {
std::string result;
int count = 0;
for (const auto& i : dynamicSpeciesIndices) {
result += std::string(engine_wrapper.getNetworkSpecies()[i].name());
if (count < dynamicSpeciesIndices.size() - 2) {
result += ", ";
} else if (count == dynamicSpeciesIndices.size() - 2) {
result += " and ";
}
count++;
}
return result;
}(m_engine));
LOG_INFO(m_logger, "QSE species: {}", [QSESpeciesIndices](const DynamicEngine& engine_wrapper) -> std::string {
std::string result;
int count = 0;
for (const auto& i : QSESpeciesIndices) {
result += std::string(engine_wrapper.getNetworkSpecies()[i].name());
if (count < QSESpeciesIndices.size() - 2) {
result += ", ";
} else if (count == QSESpeciesIndices.size() - 2) {
result += " and ";
}
count++;
}
return result;
}(m_engine));
return {dynamicSpeciesIndices, QSESpeciesIndices};
}
Eigen::VectorXd QSENetworkSolver::calculateSteadyStateAbundances(
const std::vector<double> &Y,
const double T9,
const double rho,
const dynamicQSESpeciesIndices &indices
) const {
std::vector<double> Y_dyn;
Eigen::VectorXd Y_qse_initial(indices.QSESpeciesIndices.size());
for (const auto& i : indices.dynamicSpeciesIndices) { Y_dyn.push_back(Y[i]); }
for (size_t i = 0; i < indices.QSESpeciesIndices.size(); ++i) {
Y_qse_initial(i) = Y[indices.QSESpeciesIndices[i]];
if (Y_qse_initial(i) < 1.0e-99) { Y_qse_initial(i) = 1.0e-99; }
}
Eigen::VectorXd v_qse = Y_qse_initial.array().log();
EigenFunctor<double> qse_problem(m_engine, Y_dyn, indices.dynamicSpeciesIndices, indices.QSESpeciesIndices, T9, rho);
LOG_INFO(m_logger, "--- QSE Pre-Solve Diagnostics ---");
Eigen::VectorXd f_initial(indices.QSESpeciesIndices.size());
qse_problem(v_qse, f_initial);
LOG_INFO(m_logger, "Initial Guess ||f||: {:0.4e}", f_initial.norm());
Eigen::MatrixXd J_initial(indices.QSESpeciesIndices.size(), indices.QSESpeciesIndices.size());
qse_problem.df(v_qse, J_initial);
const Eigen::JacobiSVD<Eigen::MatrixXd> svd(J_initial);
double cond = svd.singularValues().maxCoeff() / svd.singularValues().minCoeff();
LOG_INFO(m_logger, "Initial Jacobian Condition Number: {:0.4e}", cond);
LOG_INFO(m_logger, "Starting QSE solve...");
Eigen::HybridNonLinearSolver<EigenFunctor<double>> solver(qse_problem);
solver.parameters.xtol = 1.0e-8; // Set tolerance
// 5. Run the solver. It will modify v_qse in place.
const int eigenStatus = solver.solve(v_qse);
// 6. Check for convergence and return the result
if(eigenStatus != Eigen::Success) {
LOG_WARNING(m_logger, "--- QSE SOLVER FAILED ---");
LOG_WARNING(m_logger, "Eigen status code: {}", eigenStatus);
LOG_WARNING(m_logger, "Iterations performed: {}", solver.iter);
// Log the final state that caused the failure
Eigen::VectorXd Y_qse_final_fail = v_qse.array().exp();
for(long i=0; i<v_qse.size(); ++i) {
LOG_WARNING(m_logger, "Final v_qse[{}]: {:0.4e} -> Y_qse[{}]: {:0.4e}", i, v_qse(i), i, Y_qse_final_fail(i));
}
// Log the residual at the final state
Eigen::VectorXd f_final(indices.QSESpeciesIndices.size());
qse_problem(v_qse, f_final);
LOG_WARNING(m_logger, "Final ||f||: {:0.4e}", f_final.norm());
throw std::runtime_error("Eigen QSE solver did not converge.");
}
LOG_INFO(m_logger, "Eigen QSE solver converged in {} iterations.", solver.iter);
return v_qse.array().exp();
}
NetOut QSENetworkSolver::initializeNetworkWithShortIgnition(const NetIn &netIn) const {
const auto ignitionTemperature = m_config.get<double>(
"gridfire:solver:QSE:ignition:temperature",
2e8
); // 0.2 GK
const auto ignitionDensity = m_config.get<double>(
"gridfire:solver:QSE:ignition:density",
1e6
); // 1e6 g/cm^3
const auto ignitionTime = m_config.get<double>(
"gridfire:solver:QSE:ignition:tMax",
1e-7
); // 0.1 μs
const auto ignitionStepSize = m_config.get<double>(
"gridfire:solver:QSE:ignition:dt0",
1e-15
); // 1e-15 seconds
LOG_INFO(
m_logger,
"Igniting network with T={:<5.3E}, ρ={:<5.3E}, tMax={:<5.3E}, dt0={:<5.3E}...",
ignitionTemperature,
ignitionDensity,
ignitionTime,
ignitionStepSize
);
NetIn preIgnition = netIn;
preIgnition.temperature = ignitionTemperature;
preIgnition.density = ignitionDensity;
preIgnition.tMax = ignitionTime;
preIgnition.dt0 = ignitionStepSize;
DirectNetworkSolver ignitionSolver(m_engine);
NetOut postIgnition = ignitionSolver.evaluate(preIgnition);
LOG_INFO(m_logger, "Network ignition completed in {} steps.", postIgnition.num_steps);
return postIgnition;
}
void QSENetworkSolver::RHSFunctor::operator()(
const boost::numeric::ublas::vector<double> &YDynamic,
boost::numeric::ublas::vector<double> &dYdtDynamic,
double t
) const {
// --- Populate the slow / dynamic species vector ---
std::vector<double> YFull(m_engine.getNetworkSpecies().size());
for (size_t i = 0; i < m_dynamicSpeciesIndices.size(); ++i) {
YFull[m_dynamicSpeciesIndices[i]] = YDynamic(i);
}
// --- Populate the QSE species vector ---
for (size_t i = 0; i < m_QSESpeciesIndices.size(); ++i) {
YFull[m_QSESpeciesIndices[i]] = m_Y_QSE(i);
}
auto [full_dYdt, specificEnergyRate] = m_engine.calculateRHSAndEnergy(YFull, m_T9, m_rho);
dYdtDynamic.resize(m_dynamicSpeciesIndices.size() + 1);
for (size_t i = 0; i < m_dynamicSpeciesIndices.size(); ++i) {
dYdtDynamic(i) = full_dYdt[m_dynamicSpeciesIndices[i]];
}
dYdtDynamic[m_dynamicSpeciesIndices.size()] = specificEnergyRate;
}
NetOut DirectNetworkSolver::evaluate(const NetIn &netIn) {
namespace ublas = boost::numeric::ublas;
namespace odeint = boost::numeric::odeint;
using fourdst::composition::Composition;
const double T9 = netIn.temperature / 1e9; // Convert temperature from Kelvin to T9 (T9 = T / 1e9)
const unsigned long numSpecies = m_engine.getNetworkSpecies().size();
const auto absTol = m_config.get<double>("gridfire:solver:DirectNetworkSolver:absTol", 1.0e-8);
const auto relTol = m_config.get<double>("gridfire:solver:DirectNetworkSolver:relTol", 1.0e-8);
size_t stepCount = 0;
RHSFunctor rhsFunctor(m_engine, T9, netIn.density);
JacobianFunctor jacobianFunctor(m_engine, T9, netIn.density);
ublas::vector<double> Y(numSpecies + 1);
for (size_t i = 0; i < numSpecies; ++i) {
const auto& species = m_engine.getNetworkSpecies()[i];
try {
Y(i) = netIn.composition.getMolarAbundance(std::string(species.name()));
} catch (const std::runtime_error) {
LOG_DEBUG(m_logger, "Species '{}' not found in composition. Setting abundance to 0.0.", species.name());
Y(i) = 0.0;
}
}
Y(numSpecies) = 0.0;
const auto stepper = odeint::make_controlled<odeint::rosenbrock4<double>>(absTol, relTol);
stepCount = odeint::integrate_adaptive(
stepper,
std::make_pair(rhsFunctor, jacobianFunctor),
Y,
0.0,
netIn.tMax,
netIn.dt0
);
std::vector<double> finalMassFractions(numSpecies);
for (size_t i = 0; i < numSpecies; ++i) {
const double molarMass = m_engine.getNetworkSpecies()[i].mass();
finalMassFractions[i] = Y(i) * molarMass; // Convert from molar abundance to mass fraction
if (finalMassFractions[i] < MIN_ABUNDANCE_THRESHOLD) {
finalMassFractions[i] = 0.0;
}
}
std::vector<std::string> speciesNames;
speciesNames.reserve(numSpecies);
for (const auto& species : m_engine.getNetworkSpecies()) {
speciesNames.push_back(std::string(species.name()));
}
Composition outputComposition(speciesNames, finalMassFractions);
outputComposition.finalize(true);
NetOut netOut;
netOut.composition = std::move(outputComposition);
netOut.energy = Y(numSpecies); // Specific energy rate
netOut.num_steps = stepCount;
return netOut;
}
void DirectNetworkSolver::RHSFunctor::operator()(
const boost::numeric::ublas::vector<double> &Y,
boost::numeric::ublas::vector<double> &dYdt,
double t
) const {
const std::vector<double> y(Y.begin(), m_numSpecies + Y.begin());
auto [dydt, eps] = m_engine.calculateRHSAndEnergy(y, m_T9, m_rho);
dYdt.resize(m_numSpecies + 1);
std::ranges::copy(dydt, dydt.begin());
dYdt(m_numSpecies) = eps;
}
void DirectNetworkSolver::JacobianFunctor::operator()(
const boost::numeric::ublas::vector<double> &Y,
boost::numeric::ublas::matrix<double> &J,
double t,
boost::numeric::ublas::vector<double> &dfdt
) const {
J.resize(m_numSpecies + 1, m_numSpecies + 1);
J.clear();
for (int i = 0; i < m_numSpecies + 1; ++i) {
for (int j = 0; j < m_numSpecies + 1; ++j) {
J(i, j) = m_engine.getJacobianMatrixEntry(i, j);
}
}
}
}

View File

@@ -1,8 +1,10 @@
# Define the library
network_sources = files(
'lib/network.cpp',
'lib/approx8.cpp',
'lib/netgraph.cpp'
'lib/engine/engine_approx8.cpp',
'lib/engine/engine_graph.cpp',
'lib/reaction/reaction.cpp',
'lib/solver/solver.cpp',
)
@@ -13,7 +15,9 @@ dependencies = [
composition_dep,
reaclib_reactions_dep,
cppad_dep,
log_dep
log_dep,
xxhash_dep,
eigen_dep,
]
# Define the libnetwork library so it can be linked against by other parts of the build system
@@ -33,7 +37,10 @@ network_dep = declare_dependency(
# Make headers accessible
network_headers = files(
'include/gridfire/network.h',
'include/gridfire/approx8.h',
'include/gridfire/netgraph.h',
'include/gridfire/engine/engine_abstract.h',
'include/gridfire/engine/engine_approx8.h',
'include/gridfire/engine/engine_graph.h',
'include/gridfire/reaction/reaction.h',
'include/gridfire/solver/solver.h',
)
install_headers(network_headers, subdir : 'gridfire/gridfire')
install_headers(network_headers, subdir : 'gridfire')

13
subprojects/eigen.wrap Normal file
View File

@@ -0,0 +1,13 @@
[wrap-file]
directory = eigen-3.4.0
source_url = https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.tar.bz2
source_filename = eigen-3.4.0.tar.bz2
source_hash = b4c198460eba6f28d34894e3a5710998818515104d6e74e5cc331ce31e46e626
patch_filename = eigen_3.4.0-2_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/eigen_3.4.0-2/get_patch
patch_hash = cb764fd9fec02d94aaa2ec673d473793c0d05da4f4154c142f76ef923ea68178
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/eigen_3.4.0-2/eigen-3.4.0.tar.bz2
wrapdb_version = 3.4.0-2
[provide]
eigen3 = eigen_dep

View File

@@ -1,4 +1,4 @@
[wrap-git]
url = https://github.com/4D-STAR/libcomposition.git
revision = v1.0.7
revision = v1.0.9
depth = 1

View File

View File

@@ -0,0 +1,48 @@
#define QUILL_COMPILE_ACTIVE_LOG_LEVEL QUILL_COMPILE_ACTIVE_LOG_LEVEL_DEBUG
#include <iostream>
#include "../../src/network/include/gridfire/engine/engine_graph.h"
#include "gridfire/network.h"
#include "../../src/network/include/gridfire/engine/engine_approx8.h"
#include "../../src/network/include/gridfire/solver/solver.h"
#include "fourdst/composition/composition.h"
int main() {
using namespace gridfire;
// reaction::REACLIBLogicalReactionSet reactions = build_reaclib_nuclear_network_from_file("approx8", false);
// GraphNetwork network(reactions);
const std::vector<double> comp = {0.708, 0.0, 2.94e-5, 0.276, 0.003, 0.0011, 9.62e-3, 1.62e-3, 5.16e-4};
const std::vector<std::string> symbols = {"H-1", "H-2", "He-3", "He-4", "C-12", "N-14", "O-16", "Ne-20", "Mg-24"};
fourdst::composition::Composition composition;
composition.registerSymbol(symbols, true);
composition.setMassFraction(symbols, comp);
composition.finalize(true);
NetIn netIn;
netIn.composition = composition;
netIn.temperature = 1e7;
netIn.density = 1e2;
netIn.energy = 0.0;
netIn.tMax = 3.15e17;
netIn.dt0 = 1e12;
NetOut netOut;
approx8::Approx8Network approx8Network;
netOut = approx8Network.evaluate(netIn);
std::cout << "Approx8 Network Output: " << netOut << std::endl;
// // Network Ignition
netIn.dt0 = 1e-15;
// netIn.temperature = 2.0e8;
// netIn.density = 1.0e6;
// netIn.tMax = 1e-1;
GraphEngine ReaclibEngine(composition);
// netOut = network.evaluate(netIn);
solver::QSENetworkSolver solver(ReaclibEngine);
netOut = solver.evaluate(netIn);
std::cout << "QSE Graph Network Output: " << netOut << std::endl;
}

View File

@@ -0,0 +1,5 @@
executable(
'graphnet_sandbox',
'main.cpp',
dependencies: [network_dep, composition_dep],
)

View File

@@ -5,3 +5,4 @@ gtest_nomain_dep = dependency('gtest', main: false, required : true)
# Subdirectories for unit and integration tests
subdir('network')
subdir('graphnet_sandbox')

View File

@@ -3,10 +3,9 @@
#include "fourdst/composition/composition.h"
#include "fourdst/config/config.h"
#include "gridfire/approx8.h"
#include "gridfire/netgraph.h"
#include "../../src/network/include/gridfire/engine/engine_approx8.h"
#include "../../src/network/include/gridfire/engine/engine_graph.h"
#include "gridfire/network.h"
#include "gridfire/reaclib.h"
#include <vector>
@@ -86,7 +85,7 @@ TEST_F(approx8Test, reaclib) {
netIn.tMax = 3.15e17;
netIn.dt0 = 1e12;
GraphNetwork network(composition);
GraphEngine network(composition);
NetOut netOut;
netOut = network.evaluate(netIn);
std::cout << netOut << std::endl;