from __future__ import annotations
import re
from typing import Literal
RATE_TYPE = Literal[
"constant",
"simplified_arrhenius",
"arrhenius",
"pressure",
"photolysis",
]
[docs]
def parse_reaction(
self,
reac_str: str,
) -> tuple[list[str], list[str], list[int], RATE_TYPE, str | list[float]]:
# (.*?) Capture reactants (not greedy to strip trailing spaces)
# \s+ Match one or more spaces
# (k|k\([T|P]\)|J) Capture reaction rate type k=, k(T)=, k(P)= or J=
# \s*=\s* Match equal sign with optional spaces
# (.*) Capture rate formula
pattern = r"(.*?)\s+(k|k\([T|P]\)|J)\s*=\s*(.*)"
re_match = re.fullmatch(pattern, reac_str.strip())
if re_match is None:
raise ValueError(f"failed to parse reaction: '{reac_str}'")
groups: tuple[str, ...] = re_match.groups()
formula, rate_type_str, rate_str = groups
full_rate_str = f"{rate_type_str} = {rate_str}"
# Parsing reactants and products
formula = formula.replace(" ", "")
parts = formula.split("->")
if len(parts) != 2:
raise ValueError(f"failed to parse formula from reaction: '{reac_str}'")
reactants_str, products_str = parts
reactant_list = reactants_str.split("+")
stoi_product_list = products_str.split("+")
if "hv" in reactant_list:
expect_photolysis = True
reactant_list.remove("hv")
else:
expect_photolysis = False
for spec in reactant_list:
if spec not in self.active_species and spec not in self.prescribed_species:
raise ValueError(
f"reactant '{spec}' in reaction '{formula}' is neither an "
"active species nor a prescribed species"
)
stoi_list = []
product_list = []
for stoi_prod in stoi_product_list:
if "*" in stoi_prod:
parts = stoi_prod.split("*")
if len(parts) != 2:
raise ValueError(f"failed to parse products from reaction: '{formula}'")
stoi, prod = parts
stoi = int(stoi)
if stoi < 1:
raise ValueError(
f"non-positive stoichiometric number in reaction: '{formula}'"
)
else:
stoi = 1
prod = stoi_prod
stoi_list.append(stoi)
product_list.append(prod)
# Parsing rates
def match_formula_terms(string: str, formula: str, *args: str) -> list[float]:
parts = [re.escape(formula)] + [v + r"\s*=\s*(.*?)" for v in args]
pattern = r"\s*,\s*".join(parts)
re_match = re.fullmatch(pattern, string)
if re_match is None:
expected = formula + ", " + ", ".join(f"{v} = [value]" for v in args)
raise ValueError(
f"failed to parse rate: '{full_rate_str}'. "
f"Expected format '{expected}', got '{string}'"
)
return list(map(float, re_match.groups()))
if expect_photolysis is not (rate_type_str == "J"):
raise ValueError(
f"failed to parse rate from reaction: '{reac_str}'. "
"Photolysis reactions must have a 'hv' reactant and 'J = [var_name]' as rate, "
f"got '{full_rate_str}'"
)
# Type 1: Constant rate
if rate_type_str == "k":
rate_type = "constant"
try:
rate_terms = [float(rate_str)]
except TypeError as e:
raise ValueError(
f"failed to convert constant to float from rate: '{full_rate_str}'"
) from e
elif rate_type_str == "k(T)":
# Type 2: Simplified Arrhenius
if rate_str.startswith("Aexp(-B/T)"):
rate_type = "simplified_arrhenius"
rate_terms = match_formula_terms(rate_str, "Aexp(-B/T)", "A", "B")
# Type 3: Complete Arrhenius
elif rate_str.startswith("Aexp(-B/T)(300/T)**N"):
rate_type = "arrhenius"
rate_terms = match_formula_terms(rate_str, "Aexp(-B/T)", "A", "B", "N")
else:
raise ValueError(
f"failed to parse rate: '{full_rate_str}'. "
"Expected rate to start with 'k(T) = Aexp(-B/T)' or "
f"'k(T) = Aexp(-B/T)(300/T)**N'"
)
# Type 4: Relative pressure
elif rate_type_str == "k(P)":
if rate_str.startswith("A(B+C*P/Pref)"):
rate_type = "pressure"
rate_terms = match_formula_terms(rate_str, "A(B+C*P/Pref)", "A", "B", "C")
else:
raise ValueError(
f"failed to parse rate: '{full_rate_str}'. "
"Expected rate to start with 'k(P) = A(B+C*P/Pref)'"
)
# Type 5: Simple Photolysis
elif rate_type_str == "J":
rate_type = "photolysis"
rate_terms = rate_str
else:
raise ValueError(
f"failed to parse rate: '{full_rate_str}'. "
"Rates should start with one of the following: "
"'k = ', 'k(T) = ', 'k(P) = ', 'J = '"
)
return reactant_list, product_list, stoi_list, rate_type, rate_terms