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)