Files
GridFire/utils/reaclib/generateEmbeddedReaclibHeader.py

268 lines
10 KiB
Python
Raw Normal View History

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)