This entailed a major rewrite of the composition handling from each engine and engine view along with the solver and primer. The intent here is to let Compositions be constructed from the same extensive property which the solver tracks internally. This addressed C0 discontinuity issues in the tracked molar abundances of species which were introduced by repeadidly swaping from molar abundance space to mass fraction space and back. This also allowed for a simplification of the primeNetwork method. Specifically the mass borrowing system was dramatically simplified as molar abundances are extensive.
718 lines
26 KiB
Python
718 lines
26 KiB
Python
"""
|
||
REACLIB Reaction Formatting and Parsing Utilities
|
||
================================================
|
||
|
||
This module provides utilities for parsing, formatting, and analyzing nuclear reaction
|
||
data in the REACLIB format. It includes tools for extracting reaction information,
|
||
computing reaction rates, and exporting data for use in C++ or binary formats.
|
||
|
||
Algorithms and Quantum Number Accounting
|
||
----------------------------------------
|
||
The core of this module is the quantum number bookkeeping performed in
|
||
`determine_reaction_type`, which ensures conservation of baryon number (A),
|
||
charge (Z), and handles leptonic and photonic processes:
|
||
|
||
- **Baryon Number Conservation**: The difference in total mass number (A) between
|
||
reactants and products must be zero. If not, an assertion error is raised.
|
||
- **Charge Conservation**: The difference in total charge (Z) is checked:
|
||
- |ΔZ| = 1: Indicates a weak process (beta decay or electron/positron capture).
|
||
The code distinguishes between electron/positron as projectile or ejectile
|
||
based on the sign of ΔZ and the Q-value.
|
||
- ΔZ = 0: If the number of nuclei changes by ±1, photons are involved
|
||
(radiative capture or photodisintegration).
|
||
- **Projectiles and Ejectiles**: The heaviest reactant is considered the target,
|
||
and the heaviest product is the residual. Other nuclei are projectiles/ejectiles.
|
||
Special handling is provided for light nuclei (p, d, t, n, a).
|
||
|
||
Usage Examples
|
||
--------------
|
||
Parse a REACLIB file and export to CSV:
|
||
|
||
python format.py path/to/reaclib/file -o output.csv -f csv
|
||
|
||
Programmatic usage:
|
||
|
||
from utils.reaclib.format import parse_reaclib_entry, extract_groups
|
||
|
||
entry = '''1
|
||
h1 he4 c12 mg24 wkb 1.234e+00
|
||
1.234e+01 2.345e+02 3.456e+03 4.567e+04 5.678e+05 6.789e+06 7.890e+07
|
||
'''
|
||
match, reverse = parse_reaclib_entry(entry)
|
||
if match:
|
||
reaction = extract_groups(match, reverse)
|
||
print(reaction)
|
||
|
||
Functions
|
||
---------
|
||
- `parse_reaclib_entry(entry)`: Parse a REACLIB entry string.
|
||
- `extract_groups(match, reverse)`: Extracts a Reaction object from a regex match.
|
||
- `determine_reaction_type(reactants, products, qValue)`: Determines projectiles,
|
||
ejectiles, and reaction type using quantum number accounting.
|
||
- `evaluate_rate(coeffs, T9)`: Computes the reaction rate at temperature T9.
|
||
- `format_cpp_identifier(name)`: Formats a species name for C++ code.
|
||
- `create_reaction_dataframe(reactions)`: Converts a list of Reaction objects to a DataFrame.
|
||
- `write_reactions_binary(reactions, output_path)`: Writes reactions to a binary file.
|
||
|
||
Classes
|
||
-------
|
||
- `Reaction`: Dataclass representing a nuclear reaction.
|
||
- `ReaclibParseError`: Exception for parsing errors.
|
||
|
||
See function/class docstrings for further details.
|
||
|
||
"""
|
||
|
||
import re
|
||
import sys
|
||
from collections import defaultdict
|
||
from re import Match
|
||
from typing import List, Tuple, Any, LiteralString
|
||
import numpy as np
|
||
|
||
from fourdst.atomic import species
|
||
from fourdst.atomic import Species
|
||
from fourdst.constants import Constants
|
||
|
||
import hashlib
|
||
from collections import Counter
|
||
import math
|
||
import argparse
|
||
import pandas as pd
|
||
import struct
|
||
|
||
from dataclasses import dataclass
|
||
|
||
@dataclass
|
||
class Reaction:
|
||
reactants: List[str]
|
||
products: List[str]
|
||
label: str
|
||
chapter: int
|
||
qValue: float
|
||
coeffs: List[float]
|
||
projectile: str
|
||
ejectile: str
|
||
rpName: str
|
||
reactionType: str
|
||
reverse: bool
|
||
def format_rp_name(self) -> str:
|
||
return self.rpName
|
||
|
||
|
||
def __repr__(self):
|
||
return f"Reaction({self.format_rp_name()})"
|
||
|
||
|
||
def evaluate_rate(coeffs: List[float], T9: float) -> float:
|
||
"""
|
||
Evaluate the REACLIB reaction rate at a given temperature.
|
||
|
||
Parameters
|
||
----------
|
||
coeffs : list of float
|
||
The 7 REACLIB coefficients (a0..a6) for the reaction.
|
||
T9 : float
|
||
Temperature in units of 10^9 K.
|
||
|
||
Returns
|
||
-------
|
||
float
|
||
The reaction rate at the specified temperature.
|
||
|
||
Notes
|
||
-----
|
||
The rate is computed as:
|
||
rate = exp(a0 + a1/T9 + a2/T9^{1/3} + a3*T9^{1/3} + a4*T9 + a5*T9^{5/3} + a6*ln(T9))
|
||
"""
|
||
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):
|
||
"""
|
||
Exception raised for errors encountered while parsing REACLIB entries.
|
||
|
||
Parameters
|
||
----------
|
||
message : str
|
||
Description of the error.
|
||
line_num : int, optional
|
||
Line number where the error occurred.
|
||
line_content : str, optional
|
||
Content of the problematic line.
|
||
"""
|
||
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:
|
||
"""
|
||
Convert a REACLIB species name to a C++-friendly identifier.
|
||
|
||
Parameters
|
||
----------
|
||
name : str
|
||
The REACLIB species name (e.g., 'h1', 'he4', 'c12', 'p', 'a').
|
||
|
||
Returns
|
||
-------
|
||
str
|
||
The formatted C++ identifier (e.g., 'H-1', 'He-4', 'C-12', etc.).
|
||
"""
|
||
name_map = {'p': 'H-1', 'd': 'H-2', 't': 'H-3', 'n': 'n-1', 'a': 'He-4'}
|
||
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[Match[str] | None, bool]:
|
||
"""
|
||
Parse a single REACLIB entry string using a regular expression.
|
||
|
||
Parameters
|
||
----------
|
||
entry : str
|
||
The REACLIB entry as a string (typically 4 lines).
|
||
|
||
Returns
|
||
-------
|
||
match : re.Match or None
|
||
The regex match object if parsing was successful, else None.
|
||
reverse : bool
|
||
True if the entry is marked as a reverse reaction, else False.
|
||
|
||
Notes
|
||
-----
|
||
The function uses a regular expression to extract chapter, reactants/products,
|
||
label, Q-value, and coefficients. The 'reverse' flag is determined by the
|
||
character at a fixed position in the entry.
|
||
"""
|
||
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]]:
|
||
"""
|
||
Split a REACLIB group string into reactants and products based on chapter.
|
||
|
||
Parameters
|
||
----------
|
||
group : str
|
||
The whitespace-separated list of species.
|
||
chapter : int
|
||
The REACLIB chapter number (determines reactant/product count).
|
||
|
||
Returns
|
||
-------
|
||
reactants : list of str
|
||
List of reactant species names.
|
||
products : list of str
|
||
List of product species names.
|
||
"""
|
||
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 translate_names_to_species(names: List[str]) -> List[Species]:
|
||
"""
|
||
Convert a list of REACLIB species names to Species objects.
|
||
|
||
Parameters
|
||
----------
|
||
names : list of str
|
||
List of REACLIB species names (e.g., 'h1', 'he4', 'c12', etc.).
|
||
|
||
Returns
|
||
-------
|
||
list of Species
|
||
List of Species objects corresponding to the names.
|
||
|
||
Raises
|
||
------
|
||
ReaclibParseError
|
||
If a species name cannot be found in the species database.
|
||
"""
|
||
sp = list()
|
||
split_alpha_digits = lambda inputString: re.match(r'([A-Za-z]+)[-+*]?(\d+)$', inputString).groups()
|
||
for name in names:
|
||
if name in ('t', 'a', 'd', 'n', 'p'):
|
||
name = {'t': 'H-3', 'a': 'He-4', 'd': 'H-2', 'n': 'n-1', 'p': 'H-1'}[name]
|
||
else:
|
||
name = '-'.join(split_alpha_digits(name)).capitalize()
|
||
try:
|
||
sp.append(species[name])
|
||
except Exception as e:
|
||
raise ReaclibParseError(f"Species '{name}' not found in species database.", line_content=name)
|
||
return sp
|
||
|
||
def determine_reaction_type(reactants: List[str],
|
||
products: List[str],
|
||
qValue: float,
|
||
chapter: int
|
||
) -> Tuple[str, List[str], List[str], str, str, str]:
|
||
"""
|
||
Analyze a reaction for quantum number conservation and classify projectiles/ejectiles.
|
||
|
||
Parameters
|
||
----------
|
||
reactants : list of str
|
||
List of reactant species names.
|
||
products : list of str
|
||
List of product species names.
|
||
qValue : float
|
||
Q-value of the reaction (MeV).
|
||
|
||
Returns
|
||
-------
|
||
targetToken : str
|
||
Name of the heaviest reactant (target nucleus).
|
||
projectiles : list of str
|
||
List of projectile names (including leptons/photons if present).
|
||
ejectiles : list of str
|
||
List of ejectile names (including leptons/photons if present).
|
||
residualToken : str
|
||
Name of the heaviest product (residual nucleus).
|
||
reactionKey : str
|
||
Unique string key for the reaction.
|
||
rType : str
|
||
String representation of the reaction type.
|
||
|
||
Notes
|
||
-----
|
||
This function performs quantum number bookkeeping:
|
||
- Checks baryon and charge conservation.
|
||
- Identifies weak (leptonic) and photonic processes.
|
||
- Determines projectiles/ejectiles based on mass and reaction type.
|
||
"""
|
||
# --- helper look-ups ----------------------------------------------------
|
||
reactantSpecies = translate_names_to_species(reactants)
|
||
productSpecies = translate_names_to_species(products)
|
||
|
||
# Heaviest reactant → target (A); heaviest product → residual (D)
|
||
targetSpecies = max(reactantSpecies, key=lambda s: s.mass())
|
||
residualSpecies = max(productSpecies, key=lambda s: s.mass())
|
||
|
||
# Any other nuclear reactant is the normal projectile candidate
|
||
nuclearProjectiles = [x for x in reactantSpecies]
|
||
nuclearProjectiles.remove(targetSpecies)
|
||
|
||
nuclearEjectiles = [x for x in productSpecies]
|
||
nuclearEjectiles.remove(residualSpecies)
|
||
|
||
# --- bulk bookkeeping (nuclei only) -------------------------------------
|
||
aReact = sum(s.a() for s in reactantSpecies)
|
||
zReact = sum(s.z() for s in reactantSpecies)
|
||
nReact = len(reactantSpecies)
|
||
|
||
aProd = sum(s.a() for s in productSpecies)
|
||
zProd = sum(s.z() for s in productSpecies)
|
||
nProd = len(productSpecies)
|
||
|
||
dA = aReact - aProd # must be 0 – abort if not
|
||
dZ = zReact - zProd # ≠0 ⇒ leptons needed
|
||
dN = nReact - nProd # ±1 ⇒ photon candidate
|
||
|
||
assert dA == 0, (
|
||
f"Baryon number mismatch: A₍react₎={aReact}, A₍prod₎={aProd}"
|
||
)
|
||
|
||
projectiles: List[str] = []
|
||
ejectiles: List[str] = []
|
||
|
||
|
||
debug = False
|
||
if reactants == ['b8'] and products == ['be8']:
|
||
debug = True
|
||
|
||
BETA_PLUS_THRESHOLD_ENERGY = 1.022 # MeV
|
||
|
||
# -----------------------------------------------------------------------
|
||
# 1. Charged-lepton bookkeeping (|ΔZ| = 1) ------------------------------
|
||
# -----------------------------------------------------------------------
|
||
if debug:
|
||
print("============")
|
||
print("Reactant Species: ", reactantSpecies)
|
||
print("Product Species: ", productSpecies)
|
||
print("Target Species: ", targetSpecies)
|
||
print("Residual Species: ", residualSpecies)
|
||
print("Nuclear Projectiles: ", nuclearProjectiles)
|
||
print("Nuclear Ejectiles: ", nuclearEjectiles)
|
||
print("aReact, aProd: ", aReact, aProd)
|
||
print("zReact, zProd: ", zReact, zProd)
|
||
print("nReact, nProd: ", nReact, nProd)
|
||
print("dA, dZ, dN: ", dA, dZ, dN)
|
||
print("qValue: ", qValue)
|
||
|
||
if dZ == -1 and chapter == 1:
|
||
ejectiles.append("e-") # β- decay
|
||
if dZ == -1 and chapter != 1:
|
||
projectiles.append("e+") # positron capture
|
||
if dZ == 1 and chapter == 1 and qValue >= BETA_PLUS_THRESHOLD_ENERGY:
|
||
ejectiles.append("e+") # β+ Decay
|
||
if dZ == 1 and chapter == 1 and qValue < BETA_PLUS_THRESHOLD_ENERGY:
|
||
projectiles.append("e-") # electron capture
|
||
if dZ == 1 and chapter != 1:
|
||
ejectiles.append("e+") # Positron as byproduct of two body reaction
|
||
|
||
|
||
# -----------------------------------------------------------------------
|
||
# 2. Photon bookkeeping (ΔZ = 0) ----------------------------------------
|
||
# -----------------------------------------------------------------------
|
||
if dZ == 0:
|
||
# Two → one nucleus and exothermic ⇒ radiative capture (γ ejectile, (seems to normally be implicit, but I am writing it explicitly))
|
||
if dN == 1 and qValue > 0:
|
||
ejectiles.append("g")
|
||
|
||
# One → two nuclei and endothermic ⇒ photodisintegration (γ projectile, explicit)
|
||
elif dN == -1 and qValue < 0:
|
||
projectiles.append("g")
|
||
|
||
# -----------------------------------------------------------------------
|
||
# 3. Add the ordinary nuclear projectile (if any) -----------------------
|
||
# -----------------------------------------------------------------------
|
||
if nuclearProjectiles:
|
||
for nucP in nuclearProjectiles:
|
||
name = nucP.name().replace("-", "").lower()
|
||
if name in ('h1', 'h2', 'h3', 'he4', 'n1', 'p'):
|
||
name = name.replace('h1', 'p').replace('h2', 'd').replace('h3', 't').replace('he4', 'a').replace('n1', 'n')
|
||
projectiles.append(name) # REACLIB allows exactly one
|
||
|
||
if nuclearEjectiles:
|
||
for nucE in nuclearEjectiles:
|
||
name = nucE.name().replace("-", "").lower()
|
||
if name in ('h1', 'h2', 'h3', 'he4', 'n1', 'p'):
|
||
name = name.replace('h1', 'p').replace('h2', 'd').replace('h3', 't').replace('he4', 'a').replace('n1', 'n')
|
||
ejectiles.append(name)
|
||
|
||
# -----------------------------------------------------------------------
|
||
# 4. Build return values -------------------------------------------------
|
||
# -----------------------------------------------------------------------
|
||
targetToken = targetSpecies.name().replace("-", "").lower()
|
||
residualToken = residualSpecies.name().replace("-", "").lower()
|
||
|
||
if targetToken in ('h1', 'h2', 'h3', 'n1', 'p'):
|
||
targetToken = targetToken.replace('h1', 'p').replace('h2', 'd').replace('h3', 't').replace('n1', 'n')
|
||
if residualToken in ('h1', 'h2', 'h3', 'n1', 'p'):
|
||
residualToken = residualToken.replace('h1', 'p').replace('h2', 'd').replace('h3', 't').replace('n1', 'n')
|
||
|
||
uniqueProjectiles = set(projectiles)
|
||
uniqueEjectiles = set(ejectiles)
|
||
numPerUniqueProjectiles = {x: projectiles.count(x) for x in uniqueProjectiles}
|
||
numPerUniqueEjectiles = {x: ejectiles.count(x) for x in uniqueEjectiles}
|
||
formatedProjectileNames = [f"{numPerUniqueProjectiles[x]}{x}" if numPerUniqueProjectiles[x] > 1 else x for x in uniqueProjectiles]
|
||
formatedEjectileNames = [f"{numPerUniqueEjectiles[x]}{x}" if numPerUniqueEjectiles[x] > 1 else x for x in uniqueEjectiles]
|
||
rType = f"({" ".join(formatedProjectileNames)},{' '.join(formatedEjectileNames)})"
|
||
reactionKey = f"{targetToken}{rType}{residualToken}"
|
||
|
||
|
||
return targetToken, projectiles, ejectiles, residualToken, reactionKey, rType
|
||
|
||
|
||
def extract_groups(match: re.Match, reverse: bool) -> Reaction:
|
||
"""
|
||
Extract a Reaction object from a regex match of a REACLIB entry.
|
||
|
||
Parameters
|
||
----------
|
||
match : re.Match
|
||
The regex match object from `parse_reaclib_entry`.
|
||
reverse : bool
|
||
Whether the reaction is a reverse reaction.
|
||
|
||
Returns
|
||
-------
|
||
Reaction
|
||
The parsed Reaction dataclass instance.
|
||
"""
|
||
groups = match.groups()
|
||
chapter = int(groups[0].strip())
|
||
rawGroup = groups[1].strip()
|
||
rList, pList = get_rp(rawGroup, chapter)
|
||
if reverse:
|
||
rList, pList = pList, rList
|
||
qValue = float(groups[3].strip())
|
||
target, proj, ejec, residual, key, rType = determine_reaction_type(rList, pList, qValue, chapter)
|
||
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,
|
||
rpName=key,
|
||
reactionType=rType,
|
||
reverse=reverse
|
||
)
|
||
return reaction
|
||
|
||
def format_emplacment(reaction: Reaction) -> str:
|
||
"""
|
||
Format a Reaction object as a C++ emplacement statement.
|
||
|
||
Parameters
|
||
----------
|
||
reaction : Reaction
|
||
The Reaction object to format.
|
||
|
||
Returns
|
||
-------
|
||
str
|
||
The C++ code string for emplacing the reaction.
|
||
"""
|
||
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}\", \"{reaction.format_rp_name()}\", {chapter_str}, {{{reactants_str}}}, {{{products_str}}}, {q_value_str}, \"{reaction.label}\", {{{rate_sets_str}}}, {"true" if reaction.reverse else "false"}));"
|
||
|
||
return emplacment
|
||
|
||
def calculate_peak_importance(reaction: Reaction) -> float:
|
||
"""
|
||
Estimate the peak energy importance of a reaction over a grid of T and rho.
|
||
|
||
Parameters
|
||
----------
|
||
reaction : Reaction
|
||
The Reaction object to analyze.
|
||
|
||
Returns
|
||
-------
|
||
float
|
||
The maximum energy proxy (rate * |Q|) found over the grid.
|
||
|
||
Notes
|
||
-----
|
||
The function evaluates the reaction rate over a grid of temperature (T9)
|
||
and density (rho), multiplies by |Q|, and returns the maximum value.
|
||
"""
|
||
TGrid = np.logspace(-3, 2, 100)
|
||
RhoGrid = np.logspace(0.0, 6.0, 100)
|
||
N_A: float = Constants['N_a'].value
|
||
u: float = Constants['u'].value
|
||
max_energy_proxy: float = 0.0
|
||
|
||
if not reaction.reactants:
|
||
return 0.0
|
||
|
||
numReactants: int = len(reaction.reactants)
|
||
maxRate: float = 0.0
|
||
|
||
reactantCount: Counter = Counter(reaction.reactants)
|
||
factorial_correction: float = 1.0
|
||
for count in reactantCount.values():
|
||
if count > 1:
|
||
factorial_correction *= math.factorial(count)
|
||
|
||
molar_correction_factor = 1.0
|
||
if numReactants > 1:
|
||
molar_correction_factor = N_A ** (numReactants - 1)
|
||
|
||
Y_ideal = 1.0 / numReactants
|
||
|
||
mass_term = 1.0
|
||
split_alpha_digits = lambda inputString: re.match(r'([A-Za-z]+)(\d+)$', inputString).groups()
|
||
for reactant in reaction.reactants:
|
||
try:
|
||
if reactant in ('t', 'a', 'he4', 'd', 'n', 'p'):
|
||
reactant = {'t': 'H-3', 'a': 'He-4', 'he4': 'He-4', 'd': 'H-2', 'n': 'n-1', 'p': 'H-1'}[reactant]
|
||
else:
|
||
reactant = '-'.join(split_alpha_digits(reactant)).capitalize()
|
||
reactantMassAMU = species[reactant].mass()
|
||
reactantMassG = reactantMassAMU * u
|
||
mass_term *= (Y_ideal/ reactantMassG)
|
||
except Exception as e:
|
||
return 0.0
|
||
|
||
for T9 in TGrid:
|
||
k_reaclib = evaluate_rate(reaction.coeffs, T9)
|
||
for rho in RhoGrid:
|
||
n_product_no_rho = mass_term / factorial_correction
|
||
full_rate = (n_product_no_rho *( rho ** numReactants) * k_reaclib) / molar_correction_factor
|
||
energy_proxy = full_rate * abs(reaction.qValue)
|
||
if energy_proxy > max_energy_proxy:
|
||
max_energy_proxy = energy_proxy
|
||
print(f"For reaction {reaction.format_rp_name()}, max energy proxy: {max_energy_proxy:.6e} MeV")
|
||
return max_energy_proxy
|
||
|
||
def create_reaction_dataframe(reactions: List[Reaction]) -> pd.DataFrame:
|
||
"""
|
||
Convert a list of Reaction objects into a pandas DataFrame.
|
||
|
||
Parameters
|
||
----------
|
||
reactions : list of Reaction
|
||
List of Reaction objects.
|
||
|
||
Returns
|
||
-------
|
||
pd.DataFrame
|
||
DataFrame with columns for all reaction properties.
|
||
"""
|
||
reaction_data = []
|
||
for reaction in reactions:
|
||
record = {
|
||
'id': f"{'_'.join(reaction.reactants)}_to_{'_'.join(reaction.products)}_{reaction.label.upper()}",
|
||
'rpName': reaction.rpName,
|
||
'chapter': reaction.chapter,
|
||
'reactants': ' '.join(reaction.reactants),
|
||
'products': ' '.join(reaction.products),
|
||
'qValue': reaction.qValue,
|
||
'is_reverse': reaction.reverse,
|
||
'label': reaction.label,
|
||
'a0': reaction.coeffs[0],
|
||
'a1': reaction.coeffs[1],
|
||
'a2': reaction.coeffs[2],
|
||
'a3': reaction.coeffs[3],
|
||
'a4': reaction.coeffs[4],
|
||
'a5': reaction.coeffs[5],
|
||
'a6': reaction.coeffs[6]
|
||
}
|
||
reaction_data.append(record)
|
||
|
||
return pd.DataFrame(reaction_data)
|
||
|
||
def write_reactions_binary(reactions: List[Reaction], output_path: str):
|
||
"""
|
||
Write a list of Reaction objects to a binary file.
|
||
|
||
Parameters
|
||
----------
|
||
reactions : list of Reaction
|
||
List of Reaction objects to write.
|
||
output_path : str
|
||
Path to the output binary file.
|
||
|
||
Notes
|
||
-----
|
||
Each reaction is packed using struct with a fixed format for chapter, Q-value,
|
||
coefficients, reverse flag, label, rpName, reactants, and products.
|
||
"""
|
||
record_format = '<i d 7d ? 8s 64s 128s 128s'
|
||
|
||
with open(output_path, 'wb') as f:
|
||
for reaction in reactions:
|
||
label_bytes = reaction.label.encode('utf-8')[:7].ljust(8, b'\0')
|
||
rpName_bytes = reaction.rpName.encode('utf-8')[:63].ljust(64, b'\0')
|
||
reactants_str = ' '.join([format_cpp_identifier(x) for x in reaction.reactants])
|
||
products_str = ' '.join([format_cpp_identifier(x) for x in reaction.products])
|
||
reactants_bytes = reactants_str.encode('utf-8')[:127].ljust(128, b'\0')
|
||
products_bytes = products_str.encode('utf-8')[:127].ljust(128, b'\0')
|
||
|
||
packed_data = struct.pack(
|
||
record_format,
|
||
reaction.chapter,
|
||
reaction.qValue,
|
||
*reaction.coeffs,
|
||
reaction.reverse,
|
||
label_bytes,
|
||
rpName_bytes,
|
||
reactants_bytes,
|
||
products_bytes
|
||
)
|
||
f.write(packed_data)
|
||
|
||
if __name__ == '__main__':
|
||
parser = argparse.ArgumentParser(description="Generate a CSV file 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="reactions.csv", help="Output CSV file path.")
|
||
parser.add_argument('-v', '--verbose', action='store_true', help="Enable verbose output.")
|
||
parser.add_argument('-f', '--format', type=str, choices=['csv', 'bin'], default='bin', help="Output format")
|
||
args = parser.parse_args()
|
||
|
||
try:
|
||
with open(args.reaclib_file, 'r') as file:
|
||
content = file.read()
|
||
|
||
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 and lines[i].strip()]
|
||
|
||
parsed_reactions = []
|
||
for i, entry in enumerate(entries):
|
||
m, r = parse_reaclib_entry(entry)
|
||
if m is not None:
|
||
try:
|
||
reac = extract_groups(m, r)
|
||
parsed_reactions.append(reac)
|
||
except ReaclibParseError as e:
|
||
if args.verbose:
|
||
print(f"Skipping entry starting at line {i*4 + 1} due to parsing error: {e}", file=sys.stderr)
|
||
continue
|
||
|
||
print(f"Successfully parsed {len(parsed_reactions)} reactions from {args.reaclib_file}")
|
||
|
||
reaction_df = create_reaction_dataframe(parsed_reactions)
|
||
|
||
|
||
if args.format == 'csv':
|
||
reaction_df.to_csv(args.output, index=False)
|
||
print("--- CSV Generation (Success!) ---")
|
||
print(f"Reaction data written to {args.output}")
|
||
else:
|
||
write_reactions_binary(parsed_reactions, args.output)
|
||
|
||
print("--- Binary File Generation (Success!) ---")
|
||
print(f"Reaction data written to {args.output}")
|
||
|
||
except FileNotFoundError:
|
||
print(f"Error: Input file not found at {args.reaclib_file}", file=sys.stderr)
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print(f"\n--- AN UNEXPECTED ERROR OCCURRED ---")
|
||
print(e, file=sys.stderr)
|
||
sys.exit(1)
|