268 lines
10 KiB
Python
268 lines
10 KiB
Python
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)
|