Files
GridFire/utils/reaclib/format.py
Emily Boudreaux a7a4a30028 feat(Comoposition-Tracking): updated GridFire to use new, molar-abundance based, version of libcomposition (v2.0.6)
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.
2025-11-10 10:40:03 -05:00

718 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)