Source code for sandlerchemeq.reaction

# Author: Cameron F. Abrams, <cfa22@drexel.edu>
"""
Reaction class for chemical reactions
"""

import numpy as np
import fractions as fr
from dataclasses import dataclass, field
from scipy.linalg import null_space
from .component import Component

[docs] @dataclass class Reaction: """ A chemical reaction consisting of reactant and product Components. Automatically balances the reaction upon initialization. """ components: list[Component] = field(default_factory=list[Component]) """ List of all components in the reaction (reactants + products) """ nu: np.ndarray = field(default_factory=lambda: np.array([])) """ Stoichiometric coefficients (negative for reactants, positive for products) """ stoProps: dict = field(default_factory=dict) """ Dictionary of stoichiometric property sums """ def __post_init__(self): if len(self.components) > 0: self._balance() self._computeStoSums() else: print('Empty Reaction created.') def _balance(self): ''' Uses nullspace of (element)x(count-in-molecule) matrix to balance reaction ''' self.nComponents = len(self.components) atomset = set() for c in self.components: atomset.update(c.atomset) self.atomList = list(atomset) self.nAtoms = len(self.atomList) # make element x count-in-compound matrix mat = np.zeros((self.nComponents, self.nAtoms)) for i, c in enumerate(self.components): for j, a in enumerate(self.atomList): mat[i][j] = c.countAtoms(a) # find its nullspace vector ns = null_space(mat.T) ns *= np.sign(ns[0, 0]) # set nu; scale so lowest value is 1 making all integers self.nu = -np.array([a[0] for a in ns/min([np.abs(x) for x in ns])]) # split into reactants and products self.nReactants = sum(1 for n in self.nu if n < 0) self.nProducts = self.nComponents - self.nReactants self.R = self.components[:self.nReactants] self.P = self.components[self.nReactants:]
[docs] def stosum(self, propname: str) -> float | np.ndarray: """ compute stoichiometric sum of property named propname """ s = 0 for i, c in enumerate(self.components): s += getattr(c, propname) * self.nu[i] return s
def _computeStoSums(self): propNames = ['dGf', 'dHf', 'Cp'] for p in propNames: self.stoProps[p] = self.stosum(p)
[docs] def __str__(self): ''' spoof nu if reaction is not yet balanced ''' nuR=['*']*self.nReactants if len(self.nu)==0 else self.nu[:self.nReactants] nuP=['*']*self.nProducts if len(self.nu)==0 else self.nu[self.nReactants:] reactant_string = ' + '.join([f'{n:.0f} {str(s)}' for n,s in zip(-nuR,self.R)]) product_string = ' + '.join([f'{n:.0f} {str(s)}' for n,s in zip(nuP,self.P)]) return reactant_string+' <-> '+product_string
[docs] def show(self): retstr=str(self)+'\n' infolist=[] for p,v in self.stoProps.items(): if hasattr(v, '__iter__'): infolist.append(f{p} = ['+', '.join([f'{vv:.4e}' for vv in v])+']') else: infolist.append(f{p} = {v:.2f}') return retstr+' '.join(infolist)