Source code for pycif.plugins.chemistries.lmdz.utils

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
[docs] def format_reaction( reactant_list: list[str], product_list: list[str], stoi_list: list[int], rate_type: RATE_TYPE, rate_terms: list[str], ) -> str: reactants_str = " + ".join(reactant_list) stoi_product_list = [ prod if stoi == 1 else f"{stoi}*{prod}" for stoi, prod in zip(stoi_list, product_list) ] products_str = " + ".join(stoi_product_list) formula = f"{reactants_str} -> {products_str}" if rate_type == "constant": rate_str = f"k = {rate_terms[0]}" elif rate_type == "simplified_arrhenius": rate_str = f"k(T) = Aexp(-B/T), A = {rate_terms[0]}, B = {rate_terms[1]}" elif rate_type == "arrhenius": rate_str = ( f"k(T) = Aexp(-B/T)(300/T)**N, A = {rate_terms[0]}, " f"B = {rate_terms[1]}, N = {rate_terms[2]}" ) elif rate_type == "pressure": rate_str = ( f"k(P) = A(B+C*P/Pref), A = {rate_terms[0]}, " f"B = {rate_terms[1]}, C = {rate_terms[2]}" ) elif rate_type == "photolysis": rate_str = f"J = {rate_terms[0]}" else: raise ValueError(f"unknown rate type: {rate_type}") return f"{formula} {rate_str}"