fix(GraphNetwork): working on loads of small bugs

Fized stoichiometry matrix initialization, added penames to reablib reactions, began work on LogicalReaction to sum the contributions of different fitting functions provided by reaclib
This commit is contained in:
2025-06-23 15:18:56 -04:00
parent 9f6e360b0f
commit dd03873bc9
31 changed files with 101449 additions and 19120 deletions

View File

@@ -156,9 +156,17 @@ int main() {
```
Save that file to `main.cpp` and compile it with the following command:
```bash
g++ main.cpp $(pkg-config --cflags --libs gridfire) -o main
clang++ main.cpp $(pkg-config --cflags --libs gridfire) -I/opt/homebrew/include -o main -std=c++23
./main
```
> Note that here I have included the `-I/opt/homebrew/include` flag to specify the header location for boost
> on my system. This very well may already be in your compiler's include path, or if not it might be in a different
> location. That is all very system dependent. You can try to get a sense of where the boost headers are located
> by looking at the `build/compile-commmands.json` file generate by meson after running `meson setup build`.
> -std=c++23 or -std=c++20 is required as we use some C++20 feature in GridFire (specifically concepts). Compiling with
> C++23 is **strongly** recommended.
Using a different network is as simple as changing the type of the `network` variable to the desired network type. For
example, if you wanted to use the `gridfire::Approx8Network`, you would change the line

View File

@@ -5,7 +5,7 @@
#include <string>
#include <iostream>
#include <unordered_map>
#include "atomicSpecies.h"
#include "fourdst/composition/atomicSpecies.h"
#include "cppad/cppad.hpp"
@@ -32,6 +32,7 @@ namespace gridfire::reaclib {
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.
@@ -56,6 +57,7 @@ namespace gridfire::reaclib {
*/
REACLIBReaction(
std::string id,
std::string_view peName,
int chapter,
std::vector<fourdst::atomic::Species> reactants,
std::vector<fourdst::atomic::Species> products,
@@ -64,6 +66,7 @@ namespace gridfire::reaclib {
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)),
@@ -87,6 +90,8 @@ namespace gridfire::reaclib {
[[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; }
@@ -99,8 +104,10 @@ namespace gridfire::reaclib {
[[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_id << ", "
os << "REACLIBReaction(" << reaction.m_peName << ", "
<< "Chapter: " << reaction.m_chapter << ")";
return os;
}
@@ -234,6 +241,7 @@ namespace gridfire::reaclib {
inline bool operator!=(const REACLIBReactionSet& lhs, const REACLIBReactionSet& rhs) {
return !(lhs == rhs);
}
}
namespace std {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,20 @@
required_headers = [
'reaclib.h',
'reactions.h',
'gridfire/reaclib.h',
'gridfire/reactions.h',
]
foreach h : required_headers
if not cpp.has_header(h, include_directories: include_directories('include'))
error('SERiF requires the header file ' + h + ' to be present in the assets/static/reaclib/include directory.')
error('GridFire requires the header file ' + h + ' to be present in the assets/static/reaclib/include/gridfire directory.')
endif
endforeach
reaclib_reactions_dep = declare_dependency(
include_directories: include_directories('include'),
)
message('✅ SERiF reaclib_reactions dependency declared')
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

@@ -2,4 +2,8 @@ cppad_dep = declare_dependency(
include_directories: include_directories('include'),
)
message('Registering CppAD headers for installation...')
install_subdir('include/cppad', install_dir: get_option('includedir'))
message('Done registering CppAD headers for installation!')

View File

@@ -1,2 +1,3 @@
config_p = subproject('libconfig')
config_dep = config_p.get_variable('config_dep')
config_dep = config_p.get_variable('config_dep')
libconfig = config_p.get_variable('libconfig')

View File

@@ -1,2 +1,3 @@
const_p = subproject('libconstants')
const_dep = const_p.get_variable('const_dep')
libconst = const_p.get_variable('libconst')

View File

@@ -1,4 +1,5 @@
logging_p = subproject('liblogging')
liblogging = logging_p.get_variable('liblogging')
logging_dep = logging_p.get_variable('logging_dep')
quill_dep = logging_p.get_variable('quill_dep')

View File

@@ -39,7 +39,7 @@ pkg.generate(
name: 'gridfire',
description: 'GridFire nuclear reaction network solver',
version: meson.project_version(),
libraries: [libnetwork, libcomposition],
libraries: [libnetwork, libcomposition, libconfig, libconst, liblogging],
subdirs: ['gridfire'],
filebase: 'gridfire',
install_dir: join_paths(get_option('libdir'), 'pkgconfig')

View File

@@ -24,7 +24,7 @@
#include <boost/numeric/odeint.hpp>
#include "network.h"
#include "gridfire/network.h"
/**
* @file approx8.h

View File

@@ -1,9 +1,10 @@
#pragma once
#include "atomicSpecies.h"
#include "composition.h"
#include "network.h"
#include "reaclib.h"
#include "fourdst/composition/atomicSpecies.h"
#include "fourdst/composition/composition.h"
#include "gridfire/network.h"
#include "gridfire/reaclib.h"
#include <string>
#include <unordered_map>
@@ -93,6 +94,8 @@ namespace gridfire {
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.
*
@@ -171,14 +174,14 @@ namespace gridfire {
* @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")]] std::vector<std::vector<std::string>> detectCycles() const = delete;
[[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")]] std::vector<std::string> topologicalSortReactions() const = delete;
[[deprecated("not implimented")]] [[nodiscard]] std::vector<std::string> topologicalSortReactions() const = delete;
/**
* @brief Export the network to a DOT file for visualization.
@@ -227,7 +230,7 @@ namespace gridfire {
* @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(), Y.begin() + m_numSpecies);
const std::vector<double> y(Y.begin(), m_numSpecies + Y.begin());
StepDerivatives<double> derivatives = m_network.calculateAllDerivatives<double>(y, m_T9, m_rho);
@@ -344,7 +347,7 @@ namespace gridfire {
*
* @note Logs errors for any violations.
*/
bool validateConservation() const;
[[nodiscard]] bool validateConservation() const;
/**
* @brief Validate and update the network composition if needed.
@@ -391,7 +394,7 @@ namespace gridfire {
* @param T9 Temperature in 10^9 K.
* @param rho Density in g/cm^3.
*/
void generateJacobianMatrix(const std::vector<double>& Y, const double T9, const double rho);
void generateJacobianMatrix(const std::vector<double>& Y, double T9, const double rho);
/**
* @brief Calculate the right-hand side (dY/dt) for the ODE system.
@@ -403,7 +406,7 @@ namespace gridfire {
*/
template <IsArithmeticOrAD GeneralScalarType>
std::vector<GeneralScalarType> calculateRHS(const std::vector<GeneralScalarType> &Y, const GeneralScalarType T9,
const GeneralScalarType rho) const;
GeneralScalarType rho) const;
/**
* @brief Calculate the reaction rate for a given reaction in dimensions of particles per cm^3 per second.
@@ -443,7 +446,7 @@ namespace gridfire {
* @param numSpecies Number of species.
* @param Y Vector of abundances.
*/
void detectStiff(const NetIn &netIn, double T9, double numSpecies, const boost::numeric::ublas::vector<double>& Y);
void detectStiff(const NetIn &netIn, double T9, unsigned long numSpecies, const boost::numeric::ublas::vector<double>& Y);
};
@@ -497,10 +500,10 @@ namespace gridfire {
const auto &constants = fourdst::constant::Constants::getInstance();
const auto u = constants.get("u"); // Atomic mass unit in g/mol
const GeneralScalarType uValue = static_cast<GeneralScalarType>(u.value); // Convert to double for calculations
const auto uValue = static_cast<GeneralScalarType>(u.value); // Convert to double for calculations
const GeneralScalarType k_reaction = reaction.calculate_rate(T9);
GeneralScalarType reactant_product_or_particle_densities = static_cast<GeneralScalarType>(1.0);
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());
@@ -508,8 +511,8 @@ namespace gridfire {
reactant_counts[std::string(reactant.name())]++;
}
const GeneralScalarType minAbundanceThreshold = static_cast<GeneralScalarType>(MIN_ABUNDANCE_THRESHOLD);
const GeneralScalarType minDensityThreshold = static_cast<GeneralScalarType>(MIN_DENSITY_THRESHOLD);
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 '{}'.",
@@ -531,16 +534,13 @@ namespace gridfire {
return static_cast<GeneralScalarType>(0.0); // If any reactant is below a threshold, return zero rate.
}
GeneralScalarType atomicMassAMU = static_cast<GeneralScalarType>(m_networkSpecies[species_index].mass());
auto atomicMassAMU = static_cast<GeneralScalarType>(m_networkSpecies[species_index].mass());
// Convert to number density
GeneralScalarType ni;
const GeneralScalarType denominator = atomicMassAMU * uValue;
if (denominator > minDensityThreshold) {
ni = (Yi * rho) / (denominator);
} else {
ni = static_cast<GeneralScalarType>(0.0);
}
assert (denominator > 0.0);
ni = (Yi * rho) / (denominator);
reactant_product_or_particle_densities *= ni;
@@ -551,7 +551,7 @@ namespace gridfire {
}
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());
GeneralScalarType molarCorrectionFactor = static_cast<GeneralScalarType>(1.0); // No correction needed for single reactant reactions
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));
}

View File

@@ -22,17 +22,19 @@
#include <vector>
#include "logging.h"
#include "config.h"
#include "fourdst/logging/logging.h"
#include "fourdst/config/config.h"
#include "fourdst/composition/species.h"
#include "quill/Logger.h"
#include "composition.h"
#include "reaclib.h"
#include "fourdst/composition/composition.h"
#include "gridfire/reaclib.h"
#include <unordered_map>
#include "const.h"
#include "fourdst/constants/const.h"
namespace gridfire {
enum NetworkFormat {
APPROX8, ///< Approx8 nuclear reaction network format.
REACLIB, ///< General REACLIB nuclear reaction network format.
@@ -141,19 +143,105 @@ namespace gridfire {
bool m_stiff = false; ///< Flag indicating if the network is stiff
};
class ReaclibNetwork final : public Network {
class LogicalReaction {
public:
explicit ReaclibNetwork(const NetworkFormat format = NetworkFormat::APPROX8);
explicit LogicalReaction(const std::vector<reaclib::REACLIBReaction> &reactions);
explicit LogicalReaction(const reaclib::REACLIBReaction &reaction);
void add_reaction(const reaclib::REACLIBReaction& reaction);
explicit ReaclibNetwork(fourdst::composition::Composition composition, const NetworkFormat format = NetworkFormat::APPROX8);
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;
NetOut evaluate(const NetIn &netIn) override;
private:
reaclib::REACLIBReactionSet m_reactions; ///< Set of REACLIB reactions
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;
};
reaclib::REACLIBReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition);
reaclib::REACLIBReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition, double culling, double T9 = 1.0);
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);
} // namespace nuclearNetwork

View File

@@ -24,12 +24,12 @@
#include <boost/numeric/odeint.hpp>
#include "const.h"
#include "config.h"
#include "fourdst/constants/const.h"
#include "fourdst/config/config.h"
#include "quill/LogMacros.h"
#include "approx8.h"
#include "network.h"
#include "gridfire/approx8.h"
#include "gridfire/network.h"
/* Nuclear reaction network in cgs units based on Frank Timmes' "approx8".
At this time it does neither screening nor neutrino losses. It includes
@@ -80,10 +80,6 @@ namespace gridfire::approx8{
//helper functions
// a function to multiply two arrays and then sum the resulting elements: sum(a*b)
double sum_product( const vec7 &a, const vec7 &b){
if (a.size() != b.size()) {
throw std::runtime_error("Error: array size mismatch in sum_product");
}
double sum=0;
for (size_t i=0; i < a.size(); i++) {
sum += a[i] * b[i];

View File

@@ -1,9 +1,9 @@
#include "netgraph.h"
#include "atomicSpecies.h"
#include "const.h"
#include "network.h"
#include "reaclib.h"
#include "species.h"
#include "gridfire/netgraph.h"
#include "fourdst/composition/atomicSpecies.h"
#include "fourdst/constants/const.h"
#include "gridfire/network.h"
#include "gridfire/reaclib.h"
#include "fourdst/composition/species.h"
#include "quill/LogMacros.h"
@@ -40,10 +40,17 @@ namespace gridfire {
syncInternalMaps();
}
GraphNetwork::GraphNetwork(const reaclib::REACLIBReactionSet &reactions) :
Network(REACLIB),
m_reactions(reactions) {
syncInternalMaps();
}
void GraphNetwork::syncInternalMaps() {
collectNetworkSpecies();
populateReactionIDMap();
populateSpeciesToIndexMap();
generateStoichiometryMatrix();
reserveJacobianMatrix();
recordADTape();
}
@@ -303,36 +310,44 @@ namespace gridfire {
LOG_INFO(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 double numSpecies, const boost::numeric::ublas::vector<double>& Y) {
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); // Copy only the species abundances, not the specific energy rate
const auto derivatives = calculateAllDerivatives<double>(initial_y_stl, T9, netIn.density);
const std::vector<double>& initial_dotY = derivatives.dydt;
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_timescale = std::numeric_limits<double>::max();
double max_timescale = 0.0;
double min_destruction_timescale = std::numeric_limits<double>::max();
for (size_t i = 0; i < numSpecies; ++i) {
if (std::abs(initial_dotY[i]) > 0) {
if (Y(i) > MIN_ABUNDANCE_THRESHOLD && initial_dotY[i] < 0.0) {
const double timescale = std::abs(Y(i) / initial_dotY[i]);
if (timescale > max_timescale) {max_timescale = timescale;}
if (timescale < min_timescale) {min_timescale = timescale;}
if (timescale < min_destruction_timescale) {
min_destruction_timescale = timescale;
}
}
}
const double stiffnessRatio = max_timescale / min_timescale;
// TODO: Pull this out into a configuration option or a more sophisticated heuristic.
constexpr double stiffnessThreshold = 1.0e6; // This is a heuristic threshold, can be tuned based on network characteristics.
LOG_INFO(m_logger, "Stiffness ratio is {} (max timescale: {}, min timescale: {}).", stiffnessRatio, max_timescale, min_timescale);
if (stiffnessRatio > stiffnessThreshold) {
LOG_INFO(m_logger, "Network is detected to be stiff. Using stiff ODE solver.");
m_stiff = true;
} else {
LOG_INFO(m_logger, "Network is detected to be non-stiff. Using non-stiff ODE solver.");
// 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.");
}
}
void GraphNetwork::exportToDot(const std::string &filename) const {
@@ -387,13 +402,13 @@ namespace gridfire {
namespace odeint = boost::numeric::odeint;
const double T9 = netIn.temperature / 1e9; // Convert temperature from Kelvin to T9 (T9 = T / 1e9)
validateComposition(netIn.composition, netIn.culling, T9);
// validateComposition(netIn.composition, netIn.culling, T9);
const double numSpecies = m_networkSpecies.size();
const unsigned long numSpecies = m_networkSpecies.size();
constexpr double abs_tol = 1.0e-8;
constexpr double rel_tol = 1.0e-8;
int stepCount = 0;
size_t stepCount = 0;
// TODO: Pull these out into configuration options
@@ -404,11 +419,17 @@ namespace gridfire {
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
Y(i) = netIn.composition.getMassFraction(std::string(species.name()));
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
}
}
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);

307
src/network/lib/network.cpp Normal file
View File

@@ -0,0 +1,307 @@
/* ***********************************************************************
//
// Copyright (C) 2025 -- The 4D-STAR Collaboration
// File Authors: Aaron Dotter, Emily Boudreaux
// Last Modified: March 21, 2025
//
// 4DSSE is free software; you can use it and/or modify
// it under the terms and restrictions the GNU General Library Public
// License version 3 (GPLv3) as published by the Free Software Foundation.
//
// 4DSSE is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Library General Public License for more details.
//
// You should have received a copy of the GNU Library General Public License
// along with this software; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// *********************************************************************** */
#include "gridfire/network.h"
#include <ranges>
#include "gridfire/approx8.h"
#include "quill/LogMacros.h"
#include "gridfire/reaclib.h"
#include "gridfire/reactions.h"
namespace gridfire {
Network::Network(const NetworkFormat format) :
m_config(fourdst::config::Config::getInstance()),
m_logManager(fourdst::logging::LogManager::getInstance()),
m_logger(m_logManager.getLogger("log")),
m_format(format),
m_constants(fourdst::constant::Constants::getInstance()){
if (format == NetworkFormat::UNKNOWN) {
LOG_ERROR(m_logger, "nuclearNetwork::Network::Network() called with UNKNOWN format");
throw std::runtime_error("nuclearNetwork::Network::Network() called with UNKNOWN format");
}
}
NetworkFormat Network::getFormat() const {
return m_format;
}
NetworkFormat Network::setFormat(const NetworkFormat format) {
const NetworkFormat oldFormat = m_format;
m_format = format;
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;
auto logger = fourdst::logging::LogManager::getInstance().getLogger("log");
if (!s_initialized) {
LOG_INFO(logger, "REACLIB reactions not initialized. Calling initializeAllReaclibReactions()...");
initializeAllReaclibReactions();
}
for (const auto &reaction: s_all_reaclib_reactions | std::views::values) {
bool gotReaction = true;
const auto& reactants = reaction.reactants();
for (const auto& reactant : reactants) {
if (!composition.contains(reactant)) {
gotReaction = false;
break; // If any reactant is not in the composition, skip this reaction
}
}
if (gotReaction) {
LOG_INFO(logger, "Adding reaction {} to REACLIB reaction set.", reaction.peName());
reactions.add_reaction(reaction);
}
}
reactions.sort();
return LogicalReactionSet(reactions);
}
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);
}
}
return reactions;
}
}

View File

@@ -1,15 +1,10 @@
# Define the library
network_sources = files(
'private/network.cpp',
'private/approx8.cpp',
'private/netgraph.cpp'
'lib/network.cpp',
'lib/approx8.cpp',
'lib/netgraph.cpp'
)
network_headers = files(
'public/network.h',
'public/approx8.h',
'public/netgraph.h',
)
dependencies = [
boost_dep,
@@ -24,16 +19,21 @@ dependencies = [
# Define the libnetwork library so it can be linked against by other parts of the build system
libnetwork = library('network',
network_sources,
include_directories: include_directories('public'),
include_directories: include_directories('include'),
dependencies: dependencies,
install : true)
network_dep = declare_dependency(
include_directories: include_directories('public'),
include_directories: include_directories('include'),
link_with: libnetwork,
sources: network_sources,
dependencies: dependencies,
)
# Make headers accessible
network_headers = files(
'include/gridfire/network.h',
'include/gridfire/approx8.h',
'include/gridfire/netgraph.h',
)
install_headers(network_headers, subdir : 'gridfire/gridfire')

View File

@@ -1,113 +0,0 @@
/* ***********************************************************************
//
// Copyright (C) 2025 -- The 4D-STAR Collaboration
// File Authors: Aaron Dotter, Emily Boudreaux
// Last Modified: March 21, 2025
//
// 4DSSE is free software; you can use it and/or modify
// it under the terms and restrictions the GNU General Library Public
// License version 3 (GPLv3) as published by the Free Software Foundation.
//
// 4DSSE is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Library General Public License for more details.
//
// You should have received a copy of the GNU Library General Public License
// along with this software; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// *********************************************************************** */
#include "network.h"
#include <ranges>
#include "approx8.h"
#include "quill/LogMacros.h"
#include "reaclib.h"
#include "reactions.h"
namespace gridfire {
Network::Network(const NetworkFormat format) :
m_config(fourdst::config::Config::getInstance()),
m_logManager(fourdst::logging::LogManager::getInstance()),
m_logger(m_logManager.getLogger("log")),
m_format(format),
m_constants(fourdst::constant::Constants::getInstance()){
if (format == NetworkFormat::UNKNOWN) {
LOG_ERROR(m_logger, "nuclearNetwork::Network::Network() called with UNKNOWN format");
throw std::runtime_error("nuclearNetwork::Network::Network() called with UNKNOWN format");
}
}
NetworkFormat Network::getFormat() const {
return m_format;
}
NetworkFormat Network::setFormat(const NetworkFormat format) {
const NetworkFormat oldFormat = m_format;
m_format = format;
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;
}
reaclib::REACLIBReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition) {
using namespace reaclib;
REACLIBReactionSet reactions;
auto logger = fourdst::logging::LogManager::getInstance().getLogger("log");
if (!s_initialized) {
LOG_INFO(logger, "REACLIB reactions not initialized. Calling initializeAllReaclibReactions()...");
initializeAllReaclibReactions();
}
for (const auto &reaction: s_all_reaclib_reactions | std::views::values) {
bool gotReaction = true;
const auto& reactants = reaction.reactants();
for (const auto& reactant : reactants) {
if (!composition.contains(reactant)) {
gotReaction = false;
break; // If any reactant is not in the composition, skip this reaction
}
}
if (gotReaction) {
LOG_INFO(logger, "Adding reaction {} to REACLIB reaction set.", reaction.id());
reactions.add_reaction(reaction);
}
}
reactions.sort();
return reactions;
}
reaclib::REACLIBReactionSet build_reaclib_nuclear_network(const fourdst::composition::Composition &composition, const double culling, const double T9) {
using namespace reaclib;
REACLIBReactionSet allReactions = build_reaclib_nuclear_network(composition);
REACLIBReactionSet reactions;
for (const auto& reaction : allReactions) {
if (reaction.calculate_rate(T9) >= culling) {
reactions.add_reaction(reaction);
}
}
return reactions;
}
}

View File

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

View File

@@ -1,7 +1,4 @@
[wrap-git]
url = https://github.com/4D-STAR/libconfig.git
revision = v1.0.3
depth = 1
[provide]
libconfig = config_dep
revision = v1.0.8
depth = 1

View File

@@ -1,7 +1,4 @@
[wrap-git]
url = https://github.com/4D-STAR/libconstants.git
revision = v1.0.5
revision = v1.0.6
depth = 1
[provide]
libconstants = const_dep

View File

@@ -1,7 +1,4 @@
[wrap-git]
url = https://github.com/4D-STAR/liblogging.git
revision = v1.0.5
depth = 1
[provide]
liblogging = logging_dep
revision = v1.0.8
depth = 1

View File

@@ -1,16 +1,16 @@
#include <gtest/gtest.h>
#include <string>
#include <gtest/gtest.h>
#include "approx8.h"
#include "config.h"
#include "network.h"
#include "composition.h"
#include "reaclib.h"
#include "netgraph.h"
#include "fourdst/composition/composition.h"
#include "fourdst/config/config.h"
#include "gridfire/approx8.h"
#include "gridfire/netgraph.h"
#include "gridfire/network.h"
#include "gridfire/reaclib.h"
#include <vector>
#include "reactions.h"
#include "gridfire/reactions.h"
std::string TEST_CONFIG = std::string(getenv("MESON_SOURCE_ROOT")) + "/tests/testsConfig.yaml";
class approx8Test : public ::testing::Test {};
@@ -68,14 +68,15 @@ TEST_F(approx8Test, evaluate) {
TEST_F(approx8Test, reaclib) {
using namespace gridfire;
const std::vector<double> comp = {0.708, 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", "He-3", "He-4", "C-12", "N-14", "O-16", "Ne-20", "Mg-24"};
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;
@@ -86,7 +87,6 @@ TEST_F(approx8Test, reaclib) {
netIn.dt0 = 1e12;
GraphNetwork network(composition);
network.exportToDot("Test.dot");
NetOut netOut;
netOut = network.evaluate(netIn);
std::cout << netOut << std::endl;

View File

@@ -12,8 +12,6 @@ foreach test_file : test_sources
exe_name,
test_file,
dependencies: [gtest_dep, gtest_main, network_dep, composition_dep],
include_directories: include_directories('../../src/network/public'),
link_with: libnetwork, # Link the dobj library
install_rpath: '@loader_path/../../src' # Ensure runtime library path resolves correctly
)

7333
utils/reaclib/energy.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,15 @@
import re
import sys
from collections import defaultdict
from typing import List, Tuple
from re import Match
from typing import List, Tuple, Any, LiteralString
import numpy as np
from serif.atomic import species
from serif.atomic import Species
from serif.constants import Constants
import hashlib
from collections import Counter
import math
#import dataclasses
from dataclasses import dataclass
@@ -18,18 +24,11 @@ class Reaction:
coeffs: List[float]
projectile: str # added
ejectile: str # added
rpName: str # added
reactionType: str # added (e.g. "(p,γ)")
reverse: bool
def format_rp_name(self) -> str:
# radiative or particle captures: 2 reactants -> 1 product
if len(self.reactants) == 2 and len(self.products) == 1:
target = self.reactants[0]
prod = self.products[0]
return f"{target}({self.projectile},{self.ejectile}){prod}"
# fallback: join lists
react_str = '+'.join(self.reactants)
prod_str = '+'.join(self.products)
return f"{react_str}->{prod_str}"
return self.rpName
def __repr__(self):
@@ -61,7 +60,7 @@ class ReaclibParseError(Exception):
def format_cpp_identifier(name: str) -> str:
name_map = {'p': 'H_1', 'n': 'n_1', 'd': 'd', 't': 't', 'a': 'a'}
name_map = {'p': 'H_1', 'd': 'H_2', 't': 'H_3', 'n': 'n_1', 'a': 'He_4'}
if name.lower() in name_map:
return name_map[name.lower()]
match = re.match(r"([a-zA-Z]+)(\d+)", name)
@@ -70,7 +69,7 @@ def format_cpp_identifier(name: str) -> str:
return f"{element.capitalize()}_{mass}"
return f"{name.capitalize()}_1"
def parse_reaclib_entry(entry: str) -> Tuple[List[str], str, float, List[float], bool]:
def parse_reaclib_entry(entry: str) -> tuple[Match[str] | None, bool]:
pattern = re.compile(r"""^([1-9]|1[0-1])\r?\n
[ \t]*
((?:[A-Za-z0-9-*]+[ \t]+)*
@@ -104,35 +103,161 @@ def get_rp(group: str, chapter: int) -> Tuple[List[str], List[str]]:
products = species[nReact:nReact + nProd]
return reactants, products
def translate_names_to_species(names: List[str]) -> List[Species]:
sp = list()
split_alpha_digits = lambda inputString: re.match(r'([A-Za-z]+)[-+*]?(\d+)$', inputString).groups()
for name in names:
if name in ('t', 'a', 'd', 'n', 'p'):
name = {'t': 'H-3', 'a': 'He-4', 'd': 'H-2', 'n': 'n-1', 'p': 'H-1'}[name]
else:
name = '-'.join(split_alpha_digits(name)).capitalize()
try:
sp.append(species[name])
except Exception as e:
print("Error: Species not found in database:", name, e)
raise ReaclibParseError(f"Species '{name}' not found in species database.", line_content=name)
return sp
def determine_reaction_type(reactants: List[str],
products: List[str],
qValue: float
) -> Tuple[str, List[str], List[str], str]:
"""
Return (targetToken, projectiles, ejectiles, residualToken)
• targetToken the nucleus that appears before the parenthesis (A)
• projectiles every explicit projectile that must be written inside ( … )
• ejectiles every explicit ejectile that must be written after the comma
• residualToken the main heavy product that appears after the parenthesis (D)
Photons and neutrinos are added / omitted exactly the way JINA REACLIB expects:
γ is explicit only when it is a **projectile** (photodisintegration)
ν/ν̄ are never explicit
"""
if abs(qValue - 4.621) < 1e-6:
print("Looking at he3(he3, 2p)he4")
# --- helper look-ups ----------------------------------------------------
reactantSpecies = translate_names_to_species(reactants)
productSpecies = translate_names_to_species(products)
# Heaviest reactant → target (A); heaviest product → residual (D)
targetSpecies = max(reactantSpecies, key=lambda s: s.mass())
residualSpecies = max(productSpecies, key=lambda s: s.mass())
# Any other nuclear reactant is the normal projectile candidate
nuclearProjectiles = [x for x in reactantSpecies]
nuclearProjectiles.remove(targetSpecies)
nuclearEjectiles = [x for x in productSpecies]
nuclearEjectiles.remove(residualSpecies)
# --- bulk bookkeeping (nuclei only) -------------------------------------
aReact = sum(s.a() for s in reactantSpecies)
zReact = sum(s.z() for s in reactantSpecies)
nReact = len(reactantSpecies)
aProd = sum(s.a() for s in productSpecies)
zProd = sum(s.z() for s in productSpecies)
nProd = len(productSpecies)
dA = aReact - aProd # must be 0 abort if not
dZ = zReact - zProd # ≠0 ⇒ leptons needed
dN = nReact - nProd # ±1 ⇒ photon candidate
assert dA == 0, (
f"Baryon number mismatch: A₍react₎={aReact}, A₍prod₎={aProd}"
)
projectiles: List[str] = []
ejectiles: List[str] = []
# -----------------------------------------------------------------------
# 1. Charged-lepton bookkeeping (|ΔZ| = 1) ------------------------------
# -----------------------------------------------------------------------
if abs(dZ) == 1:
# Proton → neutron (β⁻ / e- capture)
if dZ == -1:
# Electron capture when (i) exo-thermic and (ii) nucleus count unchanged
if qValue > 0 and dN == 0:
projectiles.append("e-") # write e- as projectile
else:
ejectiles.append("e-") # β⁻ decay: e- is an ejectile
# Neutron → proton (β⁺ / positron capture capture is vanishingly rare)
elif dZ == 1:
ejectiles.append("e+") # β⁺ / weak-proton capture
# Neutrino companion is implicit never written
# (dL is automatically fixed by hiding ν or ν̄)
# -----------------------------------------------------------------------
# 2. Photon bookkeeping (ΔZ = 0) ----------------------------------------
# -----------------------------------------------------------------------
if dZ == 0:
# Two → one nucleus and exothermic ⇒ radiative capture (γ ejectile, implicit)
if dN == 1 and qValue > 0:
ejectiles.append("g")
pass # γ is implicit; nothing to write
# One → two nuclei and endothermic ⇒ photodisintegration (γ projectile, explicit)
elif dN == -1 and qValue < 0:
projectiles.append("g")
# -----------------------------------------------------------------------
# 3. Add the ordinary nuclear projectile (if any) -----------------------
# -----------------------------------------------------------------------
if nuclearProjectiles:
for nucP in nuclearProjectiles:
name = nucP.name().replace("-", "").lower()
if name in ('h1', 'h2', 'h3', 'he4', 'n1', 'p'):
name = name.replace('h1', 'p').replace('h2', 'd').replace('h3', 't').replace('he4', 'a').replace('n1', 'n')
projectiles.append(name) # REACLIB allows exactly one
if nuclearEjectiles:
for nucE in nuclearEjectiles:
name = nucE.name().replace("-", "").lower()
if name in ('h1', 'h2', 'h3', 'he4', 'n1', 'p'):
name = name.replace('h1', 'p').replace('h2', 'd').replace('h3', 't').replace('he4', 'a').replace('n1', 'n')
ejectiles.append(name)
# -----------------------------------------------------------------------
# 4. Build return values -------------------------------------------------
# -----------------------------------------------------------------------
targetToken = targetSpecies.name().replace("-", "").lower()
residualToken = residualSpecies.name().replace("-", "").lower()
if targetToken in ('h1', 'h2', 'h3', 'n1', 'p'):
targetToken = targetToken.replace('h1', 'p').replace('h2', 'd').replace('h3', 't').replace('n1', 'n')
if residualToken in ('h1', 'h2', 'h3', 'n1', 'p'):
residualToken = residualToken.replace('h1', 'p').replace('h2', 'd').replace('h3', 't').replace('n1', 'n')
uniqueProjectiles = set(projectiles)
uniqueEjectiles = set(ejectiles)
numPerUniqueProjectiles = {x: projectiles.count(x) for x in uniqueProjectiles}
numPerUniqueEjectiles = {x: ejectiles.count(x) for x in uniqueEjectiles}
formatedProjectileNames = [f"{numPerUniqueProjectiles[x]}{x}" if numPerUniqueProjectiles[x] > 1 else x for x in uniqueProjectiles]
formatedEjectileNames = [f"{numPerUniqueEjectiles[x]}{x}" if numPerUniqueEjectiles[x] > 1 else x for x in uniqueEjectiles]
rType = f"({" ".join(formatedProjectileNames)},{' '.join(formatedEjectileNames)})"
reactionKey = f"{targetToken}{rType}{residualToken}"
return targetToken, projectiles, ejectiles, residualToken, reactionKey, rType
def determine_reaction_type(reactants: List[str], products: List[str]) -> Tuple[str, str, str]:
# assumes no reverse flag applied
projectile = ''
ejectile = ''
# projectile is the lighter reactant (p, n, he4)
for sp in reactants:
if sp in ('p', 'n', 'he4'):
projectile = sp
break
# ejectile logic
if len(products) == 1:
ejectile = 'g'
elif 'he4' in products:
ejectile = 'a'
elif 'p' in products:
ejectile = 'p'
elif 'n' in products:
ejectile = 'n'
reactionType = f"({projectile},{ejectile})"
return projectile, ejectile, reactionType
def extract_groups(match: re.Match, reverse: bool) -> Reaction:
groups = match.groups()
chapter = int(groups[0].strip())
rawGroup = groups[1].strip()
rList, pList = get_rp(rawGroup, chapter)
if 'c12' in rList and 'mg24' in pList:
print("Found it!")
if reverse:
rList, pList = pList, rList
proj, ejec, rType = determine_reaction_type(rList, pList)
qValue = float(groups[3].strip())
target, proj, ejec, residual, key, rType = determine_reaction_type(rList, pList, qValue)
reaction = Reaction(
reactants=rList,
products=pList,
@@ -142,10 +267,12 @@ def extract_groups(match: re.Match, reverse: bool) -> Reaction:
coeffs=[float(groups[i].strip()) for i in range(4, 11)],
projectile=proj,
ejectile=ejec,
rpName=key,
reactionType=rType,
reverse=reverse
)
return reaction
def format_emplacment(reaction: Reaction) -> str:
reactantNames = [f'{format_cpp_identifier(r)}' for r in reaction.reactants]
productNames = [f'{format_cpp_identifier(p)}' for p in reaction.products]
@@ -163,19 +290,23 @@ def format_emplacment(reaction: Reaction) -> str:
chapter_str = reaction.chapter
rate_sets_str = ', '.join([str(x) for x in reaction.coeffs])
emplacment: str = f"s_all_reaclib_reactions.emplace(\"{label}\", REACLIBReaction(\"{label}\", {chapter_str}, {{{reactants_str}}}, {{{products_str}}}, {q_value_str}, \"{reaction.label}\", {{{rate_sets_str}}}, {"true" if reaction.reverse else "false"}));"
emplacment: str = f"s_all_reaclib_reactions.emplace(\"{label}\", REACLIBReaction(\"{label}\", \"{reaction.format_rp_name()}\", {chapter_str}, {{{reactants_str}}}, {{{products_str}}}, {q_value_str}, \"{reaction.label}\", {{{rate_sets_str}}}, {"true" if reaction.reverse else "false"}));"
return emplacment
def generate_reaclib_header(reaclib_filepath: str, culling: float, T9: float, verbose: bool) -> str:
def generate_reaclib_header(reaclib_filepath: str, culling: float, T9: float, verbose: bool) -> tuple[
LiteralString, int | Any, int | Any]:
"""
Parses a JINA REACLIB file using regular expressions and generates a C++ header file string.
Args:
reaclib_filepath: The path to the REACLIB data file.
culling: The threshold for culling reactions based on their rates at T9.
T9: The temperature in billions of Kelvin to evaluate the reaction rates for culling.
verbose: If True, prints additional information about skipped reactions.
Returns:
A string containing the complete C++ header content.
@@ -190,7 +321,12 @@ def generate_reaclib_header(reaclib_filepath: str, culling: float, T9: float, ve
for entry in entries:
m, r = parse_reaclib_entry(entry)
if m is not None:
reac = extract_groups(m, r)
try:
reac = extract_groups(m, r)
except ReaclibParseError as e:
continue
if verbose:
print(f"Parsed reaction: {reac.format_rp_name()} ({reac.coeffs}) with label {reac.label} (reverse: {reac.reverse})")
reactions.append(reac)
# --- Generate the C++ Header String ---
@@ -204,8 +340,8 @@ def generate_reaclib_header(reaclib_filepath: str, culling: float, T9: float, ve
"// Includes %%TOTAL%% reactions.",
"// Note: Only reactions with species defined in the atomicSpecies.h header will be included at compile time.",
"#pragma once",
"#include \"atomicSpecies.h\"",
"#include \"species.h\"",
"#include \"fourdst/composition/atomicSpecies.h\"",
"#include \"fourdst/composition/species.h\"",
"#include \"reaclib.h\"",
"\nnamespace gridfire::reaclib {\n",
"""
@@ -218,7 +354,13 @@ def generate_reaclib_header(reaclib_filepath: str, culling: float, T9: float, ve
]
totalSkipped = 0
totalIncluded = 0
energy = list()
energyFile = open('energy.txt', 'w')
energyFile.write("name;maxEnergy;QValue,reactants;products;a0;a1;a2;a3;a4;a5;a6\n")
for reaction in reactions:
maxEnergy = calculate_peak_importance(reaction)
energyFile.write(f"{reaction.format_rp_name()};{maxEnergy};{reaction.qValue};{' '.join(reaction.reactants)};{' '.join(reaction.products)};{';'.join([str(x) for x in reaction.coeffs])}\n")
energy.append(maxEnergy)
reactantNames = [f'{format_cpp_identifier(r)}' for r in reaction.reactants]
productNames = [f'{format_cpp_identifier(p)}' for p in reaction.products]
reactionName = f"{'_'.join(reactantNames)}_to_{'_'.join(productNames)}_{reaction.label.upper()}"
@@ -226,11 +368,13 @@ def generate_reaclib_header(reaclib_filepath: str, culling: float, T9: float, ve
rate = evaluate_rate(reaction.coeffs, T9)
if rate < culling:
if verbose:
print(f"Skipping reaction {reactionName} with rate {rate:.6e} at T9={T9} (culling threshold: {culling} at T9={T9})")
print(f"Skipping reaction {reaction.format_rp_name()} ({reactionName}) with rate {rate:.6e} at T9={T9} (culling threshold: {culling} at T9={T9})")
totalSkipped += 1
continue
else:
totalIncluded += 1
else:
totalIncluded += 1
defines = ' && '.join(set([f"defined(SERIF_SPECIES_{name.upper().replace('-', '_min_').replace('+', '_add_').replace('*', '_mult_')})" for name in reactantNames + productNames]))
cpp_lines.append(f" #if {defines}")
@@ -238,8 +382,64 @@ def generate_reaclib_header(reaclib_filepath: str, culling: float, T9: float, ve
cpp_lines.append(f" {emplacment}")
cpp_lines.append(f" #endif // {defines}")
cpp_lines.append("\n }\n} // namespace gridfire::reaclib\n")
energyFile.close()
#save energy data to a file
return "\n".join(cpp_lines), totalSkipped, totalIncluded
def calculate_peak_importance(reaction: Reaction) -> float:
TGrid = np.logspace(-3, 2, 100) # Temperature grid from 0.001 to 100 T9
RhoGrid = np.logspace(0.0, 6.0, 100) # Density grid from 1e0 to 1e3 g/cm^3
N_A: float = Constants['N_a'].value
u: float = Constants['u'].value
max_energy_proxy: float = 0.0
if not reaction.reactants:
return 0.0
numReactants: int = len(reaction.reactants)
maxRate: float = 0.0
reactantCount: Counter = Counter(reaction.reactants)
factorial_correction: float = 1.0
for count in reactantCount.values():
if count > 1:
factorial_correction *= math.factorial(count)
molar_correction_factor = 1.0
if numReactants > 1:
molar_correction_factor = N_A ** (numReactants - 1)
Y_ideal = 1.0 / numReactants
mass_term = 1.0
split_alpha_digits = lambda inputString: re.match(r'([A-Za-z]+)(\d+)$', inputString).groups()
for reactant in reaction.reactants:
try:
if reactant in ('t', 'a', 'he4', 'd', 'n', 'p'):
reactant = {'t': 'H-3', 'a': 'He-4', 'he4': 'He-4', 'd': 'H-2', 'n': 'n-1', 'p': 'H-1'}[reactant]
else:
reactant = '-'.join(split_alpha_digits(reactant)).capitalize()
# print(f"Parsing reactant {reactant} using split_alpha_digits")
reactantMassAMU = species[reactant].mass()
reactantMassG = reactantMassAMU * u
mass_term *= (Y_ideal/ reactantMassG)
except Exception as e:
# print(f"Error: Reactant {reactant} not found in species database. (what: {e})")
return 0.0
for T9 in TGrid:
k_reaclib = evaluate_rate(reaction.coeffs, T9)
for rho in RhoGrid:
n_product_no_rho = mass_term / factorial_correction
full_rate = (n_product_no_rho *( rho ** numReactants) * k_reaclib) / molar_correction_factor
energy_proxy = full_rate * abs(reaction.qValue)
if energy_proxy > max_energy_proxy:
max_energy_proxy = energy_proxy
print(f"For reaction {reaction.format_rp_name()}, max energy proxy: {max_energy_proxy:.6e} MeV")
return max_energy_proxy
# def smart_cull(reactions: List[Reaction], verbose: bool = False):
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description="Generate a C++ header from a REACLIB file.")

7427
utils/reaclib/log Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

22021
utils/reaclib/reactions.h Normal file

File diff suppressed because it is too large Load Diff

10458
utils/reaclib/reactionsAll.dat Normal file

File diff suppressed because it is too large Load Diff

31399
utils/reaclib/test.h Normal file

File diff suppressed because it is too large Load Diff