feat(GridFire): brought gridfire up to where network module in SERiF was before splitting it off
This commit is contained in:
267
utils/reaclib/generateEmbeddedReaclibHeader.py
Normal file
267
utils/reaclib/generateEmbeddedReaclibHeader.py
Normal 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
63
utils/reaclib/readme.md
Normal 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.
|
||||
Reference in New Issue
Block a user