feat(GridFire): brought gridfire up to where network module in SERiF was before splitting it off

This commit is contained in:
2025-06-21 13:18:38 -04:00
parent a6bab8f037
commit 8bc48b8d19
37 changed files with 18155 additions and 39796 deletions

View File

@@ -0,0 +1,267 @@
import re
import sys
from collections import defaultdict
from typing import List, Tuple
import numpy as np
import hashlib
#import dataclasses
from dataclasses import dataclass
@dataclass
class Reaction:
reactants: List[str]
products: List[str]
label: str
chapter: int
qValue: float
coeffs: List[float]
projectile: str # added
ejectile: 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}"
def __repr__(self):
return f"Reaction({self.format_rp_name()})"
def evaluate_rate(coeffs: List[float], T9: float) -> float:
rateExponent: float = coeffs[0] + \
coeffs[1] / T9 + \
coeffs[2] / (T9 ** (1/3)) + \
coeffs[3] * (T9 ** (1/3)) + \
coeffs[4] * T9 + \
coeffs[5] * (T9 ** (5/3)) + \
coeffs[6] * (np.log(T9))
return np.exp(rateExponent)
class ReaclibParseError(Exception):
"""Custom exception for parsing errors."""
def __init__(self, message, line_num=None, line_content=None):
self.line_num = line_num
self.line_content = line_content
full_message = f"Error"
if line_num is not None:
full_message += f" on line {line_num}"
full_message += f": {message}"
if line_content is not None:
full_message += f"\n -> Line content: '{line_content}'"
super().__init__(full_message)
def format_cpp_identifier(name: str) -> str:
name_map = {'p': 'H_1', 'n': 'n_1', 'd': 'd', 't': 't', 'a': 'a'}
if name.lower() in name_map:
return name_map[name.lower()]
match = re.match(r"([a-zA-Z]+)(\d+)", name)
if match:
element, mass = match.groups()
return f"{element.capitalize()}_{mass}"
return f"{name.capitalize()}_1"
def parse_reaclib_entry(entry: str) -> Tuple[List[str], str, float, List[float], bool]:
pattern = re.compile(r"""^([1-9]|1[0-1])\r?\n
[ \t]*
((?:[A-Za-z0-9-*]+[ \t]+)*
[A-Za-z0-9-*]+)
[ \t]+
([A-Za-z0-9+]+)
[ \t]+
([+-]?(?:\d+\.\d*|\.\d+)(?:[eE][+-]?\d+))
[ \t\r\n]+
[ \t\r\n]*([+-]?\d+\.\d*e[+-]?\d+)\s*
([+-]?\d+\.\d*e[+-]?\d+)\s*
([+-]?\d+\.\d*e[+-]?\d+)\s*
([+-]?\d+\.\d*e[+-]?\d+)\s*
([+-]?\d+\.\d*e[+-]?\d+)\s*
([+-]?\d+\.\d*e[+-]?\d+)\s*
([+-]?\d+\.\d*e[+-]?\d+)
""", re.MULTILINE | re.VERBOSE)
match = pattern.match(entry)
reverse = True if entry.split('\n')[1][48] == 'v' else False
return match, reverse
def get_rp(group: str, chapter: int) -> Tuple[List[str], List[str]]:
rpGroupings = {
1: (1, 1), 2: (1, 2), 3: (1, 3), 4: (2, 1), 5: (2, 2),
6: (2, 3), 7: (2, 4), 8: (3, 1), 9: (3, 2), 10: (4, 2), 11: (1, 4)
}
species = group.split()
nReact, nProd = rpGroupings[chapter]
reactants = species[:nReact]
products = species[nReact:nReact + nProd]
return reactants, products
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 reverse:
rList, pList = pList, rList
proj, ejec, rType = determine_reaction_type(rList, pList)
reaction = Reaction(
reactants=rList,
products=pList,
label=groups[2].strip(),
chapter=chapter,
qValue=float(groups[3].strip()),
coeffs=[float(groups[i].strip()) for i in range(4, 11)],
projectile=proj,
ejectile=ejec,
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]
reactants_cpp = [f'fourdst::atomic::{format_cpp_identifier(r)}' for r in reaction.reactants]
products_cpp = [f'fourdst::atomic::{format_cpp_identifier(p)}' for p in reaction.products]
label = f"{'_'.join(reactantNames)}_to_{'_'.join(productNames)}_{reaction.label.upper()}"
reactants_str = ', '.join(reactants_cpp)
products_str = ', '.join(products_cpp)
q_value_str = f"{reaction.qValue:.6e}"
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"}));"
return emplacment
def generate_reaclib_header(reaclib_filepath: str, culling: float, T9: float, verbose: bool) -> str:
"""
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.
Returns:
A string containing the complete C++ header content.
"""
with open(reaclib_filepath, 'r') as file:
content = file.read()
fileHash = hashlib.sha256(content.encode('utf-8')).hexdigest()
# split the file into blocks of 4 lines each
lines = content.split('\n')
entries = ['\n'.join(lines[i:i+4]) for i in range(0, len(lines), 4) if len(lines[i:i+4]) == 4]
reactions = list()
for entry in entries:
m, r = parse_reaclib_entry(entry)
if m is not None:
reac = extract_groups(m, r)
reactions.append(reac)
# --- Generate the C++ Header String ---
cpp_lines = [
"// This file is automatically generated. Do not edit!",
"// Generated on: " + str(np.datetime64('now')),
"// REACLIB file hash (sha256): " + fileHash,
"// Generated from REACLIB data file: " + reaclib_filepath,
"// Culling threshold: rate >" + str(culling) + " at T9 = " + str(T9),
"// Note that if the culling threshold is set to 0.0, no reactions are culled.",
"// 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 \"reaclib.h\"",
"\nnamespace gridfire::reaclib {\n",
"""
inline void initializeAllReaclibReactions() {
if (s_initialized) return; // already initialized
s_initialized = true;
s_all_reaclib_reactions.clear();
s_all_reaclib_reactions.reserve(%%TOTAL%%); // reserve space for total reactions
"""
]
totalSkipped = 0
totalIncluded = 0
for reaction in reactions:
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()}"
if culling > 0.0:
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})")
totalSkipped += 1
continue
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}")
emplacment = format_emplacment(reaction)
cpp_lines.append(f" {emplacment}")
cpp_lines.append(f" #endif // {defines}")
cpp_lines.append("\n }\n} // namespace gridfire::reaclib\n")
return "\n".join(cpp_lines), totalSkipped, totalIncluded
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description="Generate a C++ header from a REACLIB file.")
parser.add_argument("reaclib_file", type=str, help="Path to the REACLIB data file.")
parser.add_argument("-o", "--output", type=str, default=None, help="Output file path (default: stdout).")
parser.add_argument('-c', "--culling", type=float, default=0.0, help="Culling threshold for reaction rates at T9 (when 0.0, no culling is applied).")
parser.add_argument('-T', '--T9', type=float, default=0.01, help="Temperature in billions of Kelvin (default: 0.01) to evaluate the reaction rates for culling.")
parser.add_argument('-v', '--verbose', action='store_true', help="Enable verbose output.")
args = parser.parse_args()
try:
cpp_header_string, skipped, included = generate_reaclib_header(args.reaclib_file, args.culling, args.T9, args.verbose)
cpp_header_string = cpp_header_string.replace("%%TOTAL%%", str(included))
print("--- Generated C++ Header (Success!) ---")
if args.output:
with open(args.output, 'w') as f:
f.write(cpp_header_string)
print(f"Header written to {args.output}")
print(f"Total reactions included: {included}, Total reactions skipped: {skipped}")
else:
print(cpp_header_string)
except ReaclibParseError as e:
print(f"\n--- PARSING FAILED ---")
print(e, file=sys.stderr)
sys.exit(1)

63
utils/reaclib/readme.md Normal file
View File

@@ -0,0 +1,63 @@
# Reaclib to Header File Utility
This utility module provides a script to convert reaclib2 format (with chapters
9, 10, and 11) data into a c++ header file formated for use within SERiF. This
script uses preprocessor directives to ensure that the only reactions included
are ones for which all reactants and products are defined. That is to say that
at compile time this will cull any reaction for which we do not have data from
AME2020. One effect of this is that the non-elemental reactions which reaclib
traces (such as things like nrf, and pkrf) are not included in the reaction
library.
## Usage
There are no dependencies which are not part of a standard python installation
You will however need to provide the reaclib2 formated file. This can be downloaded from
the [reaclib snapshot library](https://reaclib.jinaweb.org/library.php?action=viewsnapshots).
Assuming you download that file to your ~/Downloads directory, and it is called something like
`results123` then usage is as simple as
```bash
python generateEmbeddedReaclibHeader.py ~/Downloads/results123 -o reaclib.h -c 1e-8 -T 0.1
```
This will generate the `reaclib.h` header file in your current directory.
> The c and T flags are optional, but they are used to set the culling parameter. c is the minimum rate
that a reaction must have to be included in the header file and T is the T9 temperature to evaluate that
rate at. Without culling (when -c is set to 0 or not set) the header file may be very large.
In order to make a translation unit `reaclib.h` depends on `atomicSpecies.h`
which is another header file automatically generated by a utility module (utils/atomic).
> `atomicSpecies.h` provides the Species structs which are used to ensure that only reactions
where the reactants and products are species in AME2020 are included. This pretty significantly
cuts down on compiled binary size. This is done using preprocessor directives.
Once `reaclib.h` has been generated you can recompile `SERiF` with it simply by
running the following (from your `SERiF` root directory and assuming
`reaclib.h` is in `SERiF/utils/reaclib`)
```bash
mv assets/static/reaclib/reaclib.h assets/static/reaclib/reaclib.h.bak
cp utils/reaclib/reaclib.h assets/static/reaclib/reaclib.h
meson compile -C build
```
> All tests are run with the bundled reaclib.h based on a checkout of the
reaclib default snapshot on June 17th, 2025 where the most recent documented
change was on June 24th, 2021. This means that if you update reaclib.h some
tests may fail.
## For Developers
If you are updating the default reaclib.h file, please ensure that you also
update any relevant tests and documentation to reflect the changes made.
Further, if in the future the parser needs updating, please ensure that
those changes get upstreamed into this utility script. It is a key development guideline
that all of our tools are well documented and easy for non `SERiF` developers
to use.
## Citations
REACLIB:
- Rauscher, T., Heger, A., Hoffman, R. D., & Woosley, S. E. 2010, ApJS, 189, 240.