overreact
---
---
overreact is a library and a command-line tool for building and analyzing homogeneous microkinetic models from first-principles calculations:
In [1]: from overreact import api # the api
In [2]: api.get_k("S -> E‡ -> S", # your model
...: {"S": "data/ethane/B97-3c/staggered.out", # your data
...: "E‡": "data/ethane/B97-3c/eclipsed.out"})
Out[2]: array([8.16880917e+10]) # your results
The user specifies a set of elementary reactions that are believed to be relevant for the overall chemical phenomena. overreact offers a hopefully complete but simple environment for hypothesis testing in first-principles chemical kinetics.
Microkinetic modeling is a technique used to predict the outcome
of complex chemical reactions.
It can be used
to investigate the catalytic transformations
of molecules.
overreact makes it easy to create
and analyze microkinetic models built
from computational chemistry data.
🤔 What is microkinetic modeling?
We use the term first-principles calculations to refer to
calculations performed using quantum chemical modern methods such as
Wavefunction
and
Density Functional
theories.
For instance, the three-line example code above calculates the rate of methyl rotation in ethane (at
B97-3c).
(Rather surprisingly, the error found is less than 2%
when compared to available experimental results.)
🧐 What do you mean by first-principles calculations?
overreact uses precise thermochemical partition funtions, tunneling
corrections and data is parsed directly from computational chemistry
output files thanks to cclib
(see the
list of its supported programs).
Installation
overreact is a Python package, so you can easily install it with
pip
:
$ pip install "overreact[cli,fast]"
See the installation guide for more details.
🚀 Where to go from here? Take a look at the short introduction. Or see below for more guidance.
Citing overreact
If you use overreact in your research, please cite:
Schneider, F. S. S.; Caramori, G. F. _Overreact, an in Silico Lab: Automative Quantum Chemical Microkinetic Simulations for Complex Chemical Reactions_. Journal of Computational Chemistry 2022, 44 (3), 209–217. doi:10.1002/jcc.26861.
Here's the reference in BibTeX format:
@article{overreact_paper2022,
title = {Overreact, an in silico lab: Automative quantum chemical microkinetic simulations for complex chemical reactions},
author = {Schneider, Felipe S. S. and Caramori, Giovanni F.},
year = {2022},
month = {Apr},
journal = {Journal of Computational Chemistry},
publisher = {Wiley},
volume = {44},
number = {3},
pages = {209–217},
doi = {10.1002/jcc.26861},
issn = {1096-987x},
url = {http://dx.doi.org/10.1002/jcc.26861},
}
@software{overreact_software2021,
title = {geem-lab/overreact: v1.2.0 \vert{} Zenodo},
author = {Felipe S. S. Schneider and Let\'{\i}cia M. P. Madureira and Giovanni F. Caramori},
year = {2023},
month = {Jan},
publisher = {Zenodo},
doi = {10.5281/zenodo.7865357},
url = {https://doi.org/10.5281/zenodo.7865357},
version = {v1.2.0},
howpublished = {\url{https://github.com/geem-lab/overreact}},
}
License
overreact is open-source, released under the permissive MIT license. See the LICENSE agreement.
Funding
This project was developed at the GEEM lab (Federal University of Santa Catarina, Brazil), and was partially funded by the Brazilian National Council for Scientific and Technological Development (CNPq), grant number 140485/2017-1.
1""".. include:: ../README.md""" # noqa: D400 2 3from __future__ import annotations 4 5__docformat__ = "restructuredtext" 6 7import pkg_resources as _pkg_resources 8 9from overreact.api import ( 10 get_enthalpies, 11 get_entropies, 12 get_freeenergies, 13 get_internal_energies, 14 get_k, 15 get_kappa, 16) 17from overreact.core import ( 18 Scheme, 19 get_transition_states, 20 is_transition_state, 21 parse_reactions, 22 unparse_reactions, 23) 24from overreact.io import parse_compounds, parse_model 25from overreact.simulate import get_bias, get_dydt, get_fixed_scheme, get_y 26from overreact.thermo import change_reference_state, get_delta, get_reaction_entropies 27 28__all__ = [ 29 "Scheme", 30 "change_reference_state", 31 "get_bias", 32 "get_delta", 33 "get_dydt", 34 "get_enthalpies", 35 "get_entropies", 36 "get_fixed_scheme", 37 "get_freeenergies", 38 "get_internal_energies", 39 "get_k", 40 "get_kappa", 41 "get_reaction_entropies", 42 "get_transition_states", 43 "get_y", 44 "is_transition_state", 45 "parse_compounds", 46 "parse_model", 47 "parse_reactions", 48 "unparse_reactions", 49] 50 51__version__ = _pkg_resources.get_distribution(__name__).version 52__license__ = "MIT" # I'm too lazy to get it from setup.py... 53 54__headline__ = "📈 Create and analyze chemical microkinetic models built from computational chemistry data." 55 56__url_repo__ = "https://github.com/geem-lab/overreact" 57__url_issues__ = f"{__url_repo__}/issues" 58__url_discussions__ = f"{__url_repo__}/discussions" 59__url_pypi__ = "https://pypi.org/project/overreact/" 60__url_guide__ = "https://geem-lab.github.io/overreact-guide/" 61 62__doi__ = "10.1002/jcc.26861" 63__zenodo_doi__ = "10.5281/zenodo.7865357" 64__citations__ = ( # TODO(schneiderfelipe): read from CITATION.bib 65 r""" 66 @article{overreact_paper2022, 67 title = {Overreact, an in silico lab: Automative quantum chemical microkinetic simulations for complex chemical reactions}, 68 author = {Schneider, Felipe S. S. and Caramori, Giovanni F.}, 69 year = {2022}, 70 month = {Apr}, 71 journal = {Journal of Computational Chemistry}, 72 publisher = {Wiley}, 73 volume = {44}, 74 number = {3}, 75 pages = {209-217}, 76 doi = {DOI_PLACEHOLDER}, 77 issn = {1096-987x}, 78 url = {http://dx.doi.org/DOI_PLACEHOLDER}, 79} 80@software{overreact_software2021, 81 title = {geem-lab/overreact: vVERSION_PLACEHOLDER \vert{} Zenodo}, 82 author = {Felipe S. S. Schneider and Let\'{\i}cia M. P. Madureira and Giovanni F. Caramori}, 83 year = {2023}, 84 month = {Jan}, 85 publisher = {Zenodo}, 86 doi = {ZENODO_DOI_PLACEHOLDER}, 87 url = {https://doi.org/ZENODO_DOI_PLACEHOLDER}, 88 version = {vVERSION_PLACEHOLDER}, 89 howpublished = {\url{URL_REPO_PLACEHOLDER}}, 90} 91""".replace( 92 "ZENODO_DOI_PLACEHOLDER", 93 __zenodo_doi__, 94 ) 95 .replace("DOI_PLACEHOLDER", __doi__) 96 .replace("URL_REPO_PLACEHOLDER", __url_repo__) 97 .replace("VERSION_PLACEHOLDER", __version__) 98)
19class Scheme(NamedTuple): 20 """ 21 A descriptor of a chemical reaction network. 22 23 Mostly likely, this comes from a parsed input file. 24 See `overreact.io.parse_model`. 25 """ 26 27 compounds: list[str] 28 """A descriptor of compounds.""" 29 reactions: list[str] 30 """A descriptor of reactions.""" 31 is_half_equilibrium: list[bool] 32 """An indicator of whether a reaction is half-equilibrium.""" 33 A: np.ndarray 34 """A matrix of stoichiometric coefficients between reactants and products.""" 35 B: np.ndarray 36 """A matrix of stoichiometric coefficients between reactants and transition states."""
A descriptor of a chemical reaction network.
Mostly likely, this comes from a parsed input file.
See parse_model
.
Create new instance of Scheme(compounds, reactions, is_half_equilibrium, A, B)
Inherited Members
- builtins.tuple
- index
- count
759def change_reference_state( 760 new_reference: float = 1.0 / constants.liter, 761 old_reference: float | None = None, 762 sign: int = 1, 763 temperature: float | np.ndarray = 298.15, 764 pressure: float = constants.atm, 765 volume: float | None = None, 766): 767 r"""Calculate an additive entropy correction to a change in reference states. 768 769 .. math:: 770 \Delta G_\text{corr} = 771 R T \ln \left( \frac{\chi_\text{new}}{\chi_\text{old}} \right) 772 773 The value returned can be directly multiplied by temperature and summed to 774 the old reference free energies to obtain free energies with respect to a 775 new reference. See notes below. 776 777 For instance, the concentration correction to Gibbs free energy for a 778 gas-to-liquid standard state change is simply 779 (:math:`c^\circ = \frac{\text{1 atm}}{R T}`), 780 781 .. math:: 782 \Delta G_\text{conc} = 783 R T \ln \left( \frac{\text{1 M}}{c^\circ} \right) 784 785 Parameters 786 ---------- 787 new_reference : array-like, optional 788 New reference state. Default value corresponds to 1 mol/liter. 789 old_reference : array-like, optional 790 Old reference state. Default value corresponds to the concentration of 791 an ideal gas at the given temperature and 1 atm. 792 sign : float, optional 793 Sign of the change in reference state. Default value is 1. This only 794 multiplies the final result. 795 temperature : array-like, optional 796 Absolute temperature in Kelvin. 797 pressure : array-like, optional 798 Reference gas pressure. 799 volume : float, optional 800 Molar volume. 801 802 Returns 803 ------- 804 correction : array-like 805 Entropy correction in J/mol·K. 806 807 Notes 808 ----- 809 This function can be used to add any entropy correction in the form above. 810 The only drawback is that, sometimes, those corrections are written with a 811 minus sign in front of them (this implies switching the roles of 812 `old_reference` and `new_reference`). The easiest way to accomplish this is 813 by using ``sign=-1`` or multiplying the result by ``-1``. 814 815 Examples 816 -------- 817 By default, the correction returns a change in concentration from the gas 818 phase standard concentration to the solvated-state standard concentration: 819 820 >>> -rx.change_reference_state() / constants.calorie 821 -6.4 822 >>> 298.15 * rx.change_reference_state() / constants.kcal 823 1.89 824 >>> 273.15 * rx.change_reference_state(temperature=273.15) / constants.kcal 825 1.69 826 827 But this function can also be used to adjust symmetry effects from C1 828 calculations (symmetry number equals to one). For D7h, for instance, the 829 symmetry number is 14: 830 831 >>> -298.15 * rx.change_reference_state(14, 1) / constants.kcal 832 -1.56 833 834 >>> rx.change_reference_state(sign=-1) == -rx.change_reference_state() 835 True 836 837 """ 838 temperature = np.asarray(temperature) 839 840 if old_reference is None: 841 if volume is None: 842 volume = molar_volume(temperature=temperature, pressure=pressure) 843 old_reference = 1.0 / volume 844 845 return sign * constants.R * np.log(new_reference / old_reference)
Calculate an additive entropy correction to a change in reference states.
$$\Delta G_\text{corr} = R T \ln \left( \frac{\chi_\text{new}}{\chi_\text{old}} \right)$$
The value returned can be directly multiplied by temperature and summed to the old reference free energies to obtain free energies with respect to a new reference. See notes below.
For instance, the concentration correction to Gibbs free energy for a gas-to-liquid standard state change is simply (\( c^\circ = \frac{\text{1 atm}}{R T} \)),
$$\Delta G_\text{conc} = R T \ln \left( \frac{\text{1 M}}{c^\circ} \right)$$
Parameters
new_reference : array-like, optional New reference state. Default value corresponds to 1 mol/liter. old_reference : array-like, optional Old reference state. Default value corresponds to the concentration of an ideal gas at the given temperature and 1 atm. sign : float, optional Sign of the change in reference state. Default value is 1. This only multiplies the final result. temperature : array-like, optional Absolute temperature in Kelvin. pressure : array-like, optional Reference gas pressure. volume : float, optional Molar volume.
Returns
correction : array-like Entropy correction in J/mol·K.
Notes
This function can be used to add any entropy correction in the form above.
The only drawback is that, sometimes, those corrections are written with a
minus sign in front of them (this implies switching the roles of
old_reference
and new_reference
). The easiest way to accomplish this is
by using sign=-1
or multiplying the result by -1
.
Examples
By default, the correction returns a change in concentration from the gas phase standard concentration to the solvated-state standard concentration:
>>> -rx.change_reference_state() / constants.calorie
-6.4
>>> 298.15 * rx.change_reference_state() / constants.kcal
1.89
>>> 273.15 * rx.change_reference_state(temperature=273.15) / constants.kcal
1.69
But this function can also be used to adjust symmetry effects from C1 calculations (symmetry number equals to one). For D7h, for instance, the symmetry number is 14:
>>> -298.15 * rx.change_reference_state(14, 1) / constants.kcal
-1.56
>>> rx.change_reference_state(sign=-1) == -rx.change_reference_state()
True
578def get_bias( 579 scheme, 580 compounds, 581 data, 582 y0, 583 tunneling="eckart", 584 qrrho=True, 585 temperature=298.15, 586 pressure=constants.atm, 587 method="RK23", 588 rtol=1e-3, 589 atol=1e-6, 590): 591 r"""Estimate a energy bias for a given set of reference data points. 592 593 Parameters 594 ---------- 595 scheme : Scheme 596 A descriptor of the reaction scheme. 597 Mostly likely, this comes from a parsed model input file. 598 See `overreact.io.parse_model`. 599 compounds : dict-like 600 A descriptor of the compounds. 601 Mostly likely, this comes from a parsed model input file. 602 See `overreact.io.parse_model`. 603 data : dict-like of array-like 604 y0: array-like 605 tunneling : str or None, optional 606 Choose between "eckart", "wigner" or None (or "none"). 607 qrrho : bool or tuple-like, optional 608 Apply both the quasi-rigid rotor harmonic oscillator (QRRHO) 609 approximations of M. Head-Gordon and others (enthalpy correction, see 610 [*J. Phys. Chem. C* **2015**, 119, 4, 1840-1850](http://dx.doi.org/10.1021/jp509921r)) and S. Grimme (entropy correction, see 611 [*Theory. Chem. Eur. J.*, **2012**, 18: 9955-9964](https://doi.org/10.1002/chem.201200497)) on top of the classical RRHO. 612 temperature : array-like, optional 613 Absolute temperature in Kelvin. 614 pressure : array-like, optional 615 Reference gas pressure. 616 delta_freeenergies : array-like, optional 617 Use this instead of obtaining delta free energies from the compounds. 618 molecularity : array-like, optional 619 Reaction order, i.e., number of molecules that come together to react. 620 If set, this is used to calculate `delta_moles` for 621 `equilibrium_constant`, which effectively calculates a solution 622 equilibrium constant between reactants and the transition state for 623 gas phase data. You should set this to `None` if your free energies 624 were already adjusted for solution Gibbs free energies. 625 volume : float, optional 626 Molar volume. 627 628 Returns 629 ------- 630 array-like 631 632 Examples 633 -------- 634 >>> model = rx.parse_model("data/tanaka1996/UMP2/cc-pVTZ/model.jk") 635 636 The following are some estimates on actual atmospheric concentrations: 637 638 >>> y0 = [4.8120675684099e-5, 639 ... 2.8206357713029e-5, 640 ... 0.0, 641 ... 0.0, 642 ... 2.7426565371219e-5] 643 >>> data = {"t": [1.276472128376942246e-6, 644 ... 1.446535794555581743e-4, 645 ... 1.717069678525567564e-2], 646 ... "CH3·": [9.694916853338366211e-9, 647 ... 1.066033349343709026e-6, 648 ... 2.632179124780495175e-5]} 649 >>> get_bias(model.scheme, model.compounds, data, y0) / constants.kcal 650 -1.4 651 """ 652 max_time = np.max(data["t"]) 653 654 def f(bias): 655 k = rx.get_k( 656 scheme, 657 compounds, 658 bias=bias, 659 tunneling=tunneling, 660 qrrho=qrrho, 661 temperature=temperature, 662 pressure=pressure, 663 ) 664 665 # TODO(schneiderfelipe): support schemes with fixed concentrations 666 dydt = rx.get_dydt(scheme, k) 667 y, _ = rx.get_y( 668 dydt, 669 y0=y0, 670 method=method, 671 rtol=rtol, 672 atol=atol, 673 max_time=max_time, 674 ) 675 676 yhat = y(data["t"]) 677 return np.sum( 678 [ 679 (yhat[i] - data[name]) ** 2 680 for (i, name) in enumerate(compounds) 681 if name in data 682 ], 683 ) 684 685 res = minimize_scalar(f) 686 return res.x
Estimate a energy bias for a given set of reference data points.
Parameters
scheme : Scheme
A descriptor of the reaction scheme.
Mostly likely, this comes from a parsed model input file.
See parse_model
.
compounds : dict-like
A descriptor of the compounds.
Mostly likely, this comes from a parsed model input file.
See parse_model
.
data : dict-like of array-like
y0: array-like
tunneling : str or None, optional
Choose between "eckart", "wigner" or None (or "none").
qrrho : bool or tuple-like, optional
Apply both the quasi-rigid rotor harmonic oscillator (QRRHO)
approximations of M. Head-Gordon and others (enthalpy correction, see
J. Phys. Chem. C 2015, 119, 4, 1840-1850) and S. Grimme (entropy correction, see
Theory. Chem. Eur. J., 2012, 18: 9955-9964) on top of the classical RRHO.
temperature : array-like, optional
Absolute temperature in Kelvin.
pressure : array-like, optional
Reference gas pressure.
delta_freeenergies : array-like, optional
Use this instead of obtaining delta free energies from the compounds.
molecularity : array-like, optional
Reaction order, i.e., number of molecules that come together to react.
If set, this is used to calculate delta_moles
for
equilibrium_constant
, which effectively calculates a solution
equilibrium constant between reactants and the transition state for
gas phase data. You should set this to None
if your free energies
were already adjusted for solution Gibbs free energies.
volume : float, optional
Molar volume.
Returns
array-like
Examples
>>> model = rx.parse_model("data/tanaka1996/UMP2/cc-pVTZ/model.jk")
The following are some estimates on actual atmospheric concentrations:
>>> y0 = [4.8120675684099e-5,
... 2.8206357713029e-5,
... 0.0,
... 0.0,
... 2.7426565371219e-5]
>>> data = {"t": [1.276472128376942246e-6,
... 1.446535794555581743e-4,
... 1.717069678525567564e-2],
... "CH3·": [9.694916853338366211e-9,
... 1.066033349343709026e-6,
... 2.632179124780495175e-5]}
>>> get_bias(model.scheme, model.compounds, data, y0) / constants.kcal
-1.4
572def get_delta(transform, property): 573 """Calculate deltas according to reactions. 574 575 Delta properties are differences in a property between the final and 576 initial state of a chemical transformation. They are calculated from 577 matrices representing the transformation and the absolute properties. 578 Transformation matrices are expected to have column-wise transformation 579 defined. 580 581 Very useful for the calculation of reaction and activation free energies 582 from absolute free energies of compounds. Matrices ``A`` and ``B`` of a 583 `Scheme` represent the transformations associated with reaction and 584 activation free energies, respectively. 585 586 Parameters 587 ---------- 588 transform : array-like 589 property : array-like 590 591 Returns 592 ------- 593 delta_property : array-like 594 595 Examples 596 -------- 597 >>> get_delta([-1, 1], [-10, 10]) 598 20 599 600 You must ensure the transformation is properly defined, as no test is made 601 to ensure, e.g., conservation of matter: 602 603 >>> get_delta([-1, 0], [-10, 20]) 604 10 605 606 Normally, transformations are given as columns in a matrix: 607 608 >>> get_delta([[-1, -2], 609 ... [ 1, 3]], [-5, 12]) 610 array([17, 46]) 611 """ 612 return np.asarray(transform).T @ np.asarray(property)
Calculate deltas according to reactions.
Delta properties are differences in a property between the final and initial state of a chemical transformation. They are calculated from matrices representing the transformation and the absolute properties. Transformation matrices are expected to have column-wise transformation defined.
Very useful for the calculation of reaction and activation free energies
from absolute free energies of compounds. Matrices A
and B
of a
Scheme
represent the transformations associated with reaction and
activation free energies, respectively.
Parameters
transform : array-like property : array-like
Returns
delta_property : array-like
Examples
>>> get_delta([-1, 1], [-10, 10])
20
You must ensure the transformation is properly defined, as no test is made to ensure, e.g., conservation of matter:
>>> get_delta([-1, 0], [-10, 20])
10
Normally, transformations are given as columns in a matrix:
>>> get_delta([[-1, -2],
... [ 1, 3]], [-5, 12])
array([17, 46])
200def get_dydt(scheme, k, ef=EF): 201 """Generate a rate function that models a reaction scheme. 202 203 Parameters 204 ---------- 205 scheme : Scheme 206 A descriptor of the reaction scheme. 207 Mostly likely, this comes from a parsed model input file. 208 See `overreact.io.parse_model`. 209 k : array-like 210 Reaction rate constant(s). Units match the concentration units given to 211 the returned function ``dydt``. 212 ef : float, optional 213 Equilibrium factor. This is a parameter that can be used to scale the 214 reaction rates associated to half-equilibrium reactions such that they 215 are faster than the other reactions. 216 217 Returns 218 ------- 219 dydt : callable 220 Reaction rate function. The actual reaction rate constants employed 221 are stored in the attribute `k` of the returned function. If JAX is 222 available, the attribute `jac` will hold the Jacobian function of 223 `dydt`. 224 225 Notes 226 ----- 227 The returned function is suited to be used by ODE solvers such as 228 `scipy.integrate.solve_ivp` or the older `scipy.integrate.ode` (see 229 examples below). This is actually what the function `get_y` from the 230 current module does. 231 232 Examples 233 -------- 234 >>> import numpy as np 235 >>> import overreact as rx 236 237 >>> scheme = rx.parse_reactions("A <=> B") 238 >>> dydt = get_dydt(scheme, np.array([1, 1])) 239 >>> dydt(0.0, np.array([1., 1.])) 240 Array([0., 0.], ...) 241 242 If available, JAX is used for JIT compilation. This will make `dydt` 243 complain if given lists instead of numpy arrays. So stick to the safer, 244 faster side as above. 245 246 The actually used reaction rate constants can be inspected with the `k` 247 attribute of `dydt`: 248 249 >>> dydt.k 250 Array([1., 1.], ...) 251 252 If JAX is available, the Jacobian function will be available as 253 `dydt.jac`: 254 255 >>> dydt.jac(0.0, np.array([1., 1.])) 256 Array([[-1., 1.], 257 [ 1., -1.]], ...) 258 259 """ 260 scheme = rx.core._check_scheme(scheme) 261 A = jnp.asarray(scheme.A) 262 M = jnp.where(A > 0, 0, -A).T 263 k_adj = _adjust_k(scheme, k, ef=ef) 264 265 def _dydt(_t, y): 266 r = k_adj * jnp.prod(jnp.power(y, M), axis=1) 267 return jnp.dot(A, r) 268 269 if _found_jax: 270 # Using JAX for JIT compilation is much faster. 271 _dydt = jit(_dydt) 272 273 # NOTE(schneiderfelipe): the following function is defined 274 # such that _jac(t, y)[i, j] == d f_i / d y_j, 275 # with shape of (n_compounds, n_compounds). 276 def _jac(t, y): 277 logger.warning(f"\x1b[A@t = \x1b[94m{t:10.3f} \x1b[ms\x1b[K") 278 return jacfwd(lambda _y: _dydt(t, _y))(y) 279 280 _dydt.jac = _jac 281 282 _dydt.k = k_adj 283 return _dydt
Generate a rate function that models a reaction scheme.
Parameters
scheme : Scheme
A descriptor of the reaction scheme.
Mostly likely, this comes from a parsed model input file.
See parse_model
.
k : array-like
Reaction rate constant(s). Units match the concentration units given to
the returned function dydt
.
ef : float, optional
Equilibrium factor. This is a parameter that can be used to scale the
reaction rates associated to half-equilibrium reactions such that they
are faster than the other reactions.
Returns
dydt : callable
Reaction rate function. The actual reaction rate constants employed
are stored in the attribute k
of the returned function. If JAX is
available, the attribute jac
will hold the Jacobian function of
dydt
.
Notes
The returned function is suited to be used by ODE solvers such as
scipy.integrate.solve_ivp
or the older scipy.integrate.ode
(see
examples below). This is actually what the function get_y
from the
current module does.
Examples
>>> import numpy as np
>>> import overreact as rx
>>> scheme = rx.parse_reactions("A <=> B")
>>> dydt = get_dydt(scheme, np.array([1, 1]))
>>> dydt(0.0, np.array([1., 1.]))
Array([0., 0.], ...)
If available, JAX is used for JIT compilation. This will make dydt
complain if given lists instead of numpy arrays. So stick to the safer,
faster side as above.
The actually used reaction rate constants can be inspected with the k
attribute of dydt
:
>>> dydt.k
Array([1., 1.], ...)
If JAX is available, the Jacobian function will be available as
dydt.jac
:
>>> dydt.jac(0.0, np.array([1., 1.]))
Array([[-1., 1.],
[ 1., -1.]], ...)
97def get_enthalpies( 98 compounds: dict, 99 qrrho: bool = True, 100 temperature: float = 298.15, 101): 102 """Obtain enthalpies for compounds at a given temperature. 103 104 Parameters 105 ---------- 106 compounds : dict-like 107 A descriptor of the compounds. 108 Mostly likely, this comes from a parsed input file. 109 See `overreact.io.parse_model`. 110 qrrho : bool, optional 111 Apply the quasi-rigid rotor harmonic oscillator (QRRHO) approximation of 112 M. Head-Gordon and others (see 113 [*J. Phys. Chem. C* **2015**, 119, 4, 1840-1850](http://dx.doi.org/10.1021/jp509921r)) 114 on top of the classical RRHO. 115 temperature : array-like, optional 116 Absolute temperature in Kelvin. 117 118 Returns 119 ------- 120 array-like 121 122 Examples 123 -------- 124 >>> import overreact as rx 125 >>> from overreact import _constants as constants 126 >>> model = rx.parse_model("data/ethane/B97-3c/model.k") 127 >>> enthalpies = get_enthalpies(model.compounds) 128 >>> (enthalpies - enthalpies.min()) / constants.kcal 129 array([0. , 2.20053981]) 130 131 The enthalpies at absolute zero can easily be obtained (this is used, 132 e.g., in the calculation of the Eckart tunneling coefficient, see 133 `overreact.tunnel.eckart`). We can use this to calculate, for instance, 134 the thermal contributions to the enthalpy: 135 136 >>> zero_enthalpies = get_enthalpies(model.compounds, temperature=0) 137 >>> (enthalpies - zero_enthalpies) / constants.kcal 138 array([2.78, 2.50]) 139 """ 140 compounds = rx.io._check_compounds(compounds) 141 enthalpies = [] 142 for name in compounds: 143 logger.info(f"calculate enthalpy: {name}") 144 145 # TODO(schneiderfelipe): inertia might benefit from caching 146 moments, _, _ = coords.inertia( 147 compounds[name].atommasses, 148 compounds[name].atomcoords, 149 ) 150 151 enthalpy = rx.thermo.calc_enthalpy( 152 energy=compounds[name].energy, 153 degeneracy=compounds[name].mult, 154 moments=moments, 155 vibfreqs=compounds[name].vibfreqs, 156 qrrho=qrrho, 157 temperature=temperature, 158 ) 159 enthalpies.append(enthalpy) 160 return np.array(enthalpies)
Obtain enthalpies for compounds at a given temperature.
Parameters
compounds : dict-like
A descriptor of the compounds.
Mostly likely, this comes from a parsed input file.
See parse_model
.
qrrho : bool, optional
Apply the quasi-rigid rotor harmonic oscillator (QRRHO) approximation of
M. Head-Gordon and others (see
J. Phys. Chem. C 2015, 119, 4, 1840-1850)
on top of the classical RRHO.
temperature : array-like, optional
Absolute temperature in Kelvin.
Returns
array-like
Examples
>>> import overreact as rx
>>> from overreact import _constants as constants
>>> model = rx.parse_model("data/ethane/B97-3c/model.k")
>>> enthalpies = get_enthalpies(model.compounds)
>>> (enthalpies - enthalpies.min()) / constants.kcal
array([0. , 2.20053981])
The enthalpies at absolute zero can easily be obtained (this is used,
e.g., in the calculation of the Eckart tunneling coefficient, see
overreact.tunnel.eckart
). We can use this to calculate, for instance,
the thermal contributions to the enthalpy:
>>> zero_enthalpies = get_enthalpies(model.compounds, temperature=0)
>>> (enthalpies - zero_enthalpies) / constants.kcal
array([2.78, 2.50])
163def get_entropies( 164 compounds: dict, 165 environment: str | None = None, 166 method: str = "standard", 167 qrrho: bool = True, 168 temperature: float = 298.15, 169 pressure: float = constants.atm, 170): 171 """Obtain entropies for compounds at a given temperature and pressure. 172 173 Parameters 174 ---------- 175 compounds : dict-like 176 A descriptor of the compounds. 177 Mostly likely, this comes from a parsed input file. 178 See `overreact.io.parse_model`. 179 environment : str or None, optional 180 Choose between "gas" and a solvent. This is chosen for you by default, 181 based on the names of each compound (e.g. `A(g)` or `A` is gas, 182 `A(w)` or `A(...)` is solvated). In case this is given, all compounds 183 will have the same behavior. 184 method : str, optional 185 This is a placeholder for future functionality. 186 There are plans to implement more sophisticated methods for calculating 187 entropies such as in 188 [*Phys. Chem. Chem. Phys.*, **2019**, 21, 18920-18929](https://doi.org/10.1039/C9CP03226F) 189 and 190 [*J. Chem. Theory Comput.* **2019**, 15, 5, 3204-3214](https://doi.org/10.1021/acs.jctc.9b00214). 191 Head over to the 192 [discussions](https://github.com/geem-lab/overreact/discussions) if 193 you're interested and would like to contribute. 194 Leave this as "standard" for now. 195 qrrho : bool, optional 196 Apply the quasi-rigid rotor harmonic oscillator (QRRHO) approximation of 197 S. Grimme (see 198 [*Theory. Chem. Eur. J.*, **2012**, 18: 9955-9964](https://doi.org/10.1002/chem.201200497)) 199 on top of the classical RRHO. 200 temperature : array-like, optional 201 Absolute temperature in Kelvin. 202 pressure : array-like, optional 203 Reference gas pressure. 204 205 Returns 206 ------- 207 array-like 208 209 Examples 210 -------- 211 >>> import overreact as rx 212 >>> from overreact import _constants as constants 213 >>> model = rx.parse_model("data/ethane/B97-3c/model.k") 214 >>> entropies = get_entropies(model.compounds) 215 >>> (entropies - entropies.min()) / constants.calorie 216 array([1.4, 0. ]) 217 218 You can consider all compounds as solvated if you want: 219 220 >>> sol_entropies = get_entropies(model.compounds, environment="solvent") 221 >>> (sol_entropies - entropies) / constants.calorie 222 array([-6.35360874, -6.35360874]) 223 """ 224 compounds = rx.io._check_compounds(compounds) 225 entropies = [] 226 for name in compounds: 227 logger.info(f"calculate entropy: {name}") 228 229 if "point_group" in compounds[name]: 230 point_group = compounds[name].point_group 231 else: 232 point_group = coords.find_point_group( 233 compounds[name].atommasses, 234 compounds[name].atomcoords, 235 ) 236 symmetry_number = coords.symmetry_number(point_group) 237 238 # TODO(schneiderfelipe): inertia might benefit from caching 239 moments, _, _ = coords.inertia( 240 compounds[name].atommasses, 241 compounds[name].atomcoords, 242 ) 243 244 if environment is None: 245 environment = rx.core._get_environment(name) 246 entropy = rx.thermo.calc_entropy( 247 atommasses=compounds[name].atommasses, 248 atomcoords=compounds[name].atomcoords, 249 energy=compounds[name].energy, 250 degeneracy=compounds[name].mult, 251 moments=moments, 252 symmetry_number=symmetry_number, 253 vibfreqs=compounds[name].vibfreqs, 254 environment=environment, 255 method=method, 256 qrrho=qrrho, 257 temperature=temperature, 258 pressure=pressure, 259 ) 260 261 if compounds[name].symmetry is not None: 262 # The negative sign here seems correct. See equations (9) and (10) 263 # of doi:10.1002/qua.25686. 264 entropy -= rx.change_reference_state( 265 compounds[name].symmetry, 266 1, 267 temperature=temperature, 268 pressure=pressure, 269 ) 270 271 entropies.append(entropy) 272 return np.array(entropies)
Obtain entropies for compounds at a given temperature and pressure.
Parameters
compounds : dict-like
A descriptor of the compounds.
Mostly likely, this comes from a parsed input file.
See parse_model
.
environment : str or None, optional
Choose between "gas" and a solvent. This is chosen for you by default,
based on the names of each compound (e.g. A(g)
or A
is gas,
A(w)
or A(...)
is solvated). In case this is given, all compounds
will have the same behavior.
method : str, optional
This is a placeholder for future functionality.
There are plans to implement more sophisticated methods for calculating
entropies such as in
Phys. Chem. Chem. Phys., 2019, 21, 18920-18929
and
J. Chem. Theory Comput. 2019, 15, 5, 3204-3214.
Head over to the
discussions if
you're interested and would like to contribute.
Leave this as "standard" for now.
qrrho : bool, optional
Apply the quasi-rigid rotor harmonic oscillator (QRRHO) approximation of
S. Grimme (see
Theory. Chem. Eur. J., 2012, 18: 9955-9964)
on top of the classical RRHO.
temperature : array-like, optional
Absolute temperature in Kelvin.
pressure : array-like, optional
Reference gas pressure.
Returns
array-like
Examples
>>> import overreact as rx
>>> from overreact import _constants as constants
>>> model = rx.parse_model("data/ethane/B97-3c/model.k")
>>> entropies = get_entropies(model.compounds)
>>> (entropies - entropies.min()) / constants.calorie
array([1.4, 0. ])
You can consider all compounds as solvated if you want:
>>> sol_entropies = get_entropies(model.compounds, environment="solvent")
>>> (sol_entropies - entropies) / constants.calorie
array([-6.35360874, -6.35360874])
365def get_fixed_scheme(scheme, k, fixed_y0): 366 """Generate an alternative scheme with some concentrations fixed. 367 368 This function returns data that allow the microkinetic simulation of a 369 reaction network under constraints, namely when some compounds have fixed 370 concentrations. This works by 1. removing all references to the fixed 371 compounds and by 2. properly multiplying the reaction rate constants by 372 the respective concentrations. 373 374 Parameters 375 ---------- 376 scheme : Scheme 377 A descriptor of the reaction scheme. 378 Mostly likely, this comes from a parsed model input file. 379 See `overreact.io.parse_model`. 380 k : array-like 381 Reaction rate constant(s). Units match the concentration units given to 382 the returned function ``dydt``. 383 fixed_y0 : dict-like 384 Fixed initial state. Units match the concentration units given to 385 the returned function ``dydt``. 386 387 Returns 388 ------- 389 scheme : Scheme 390 Associated reaction scheme with all references to fixed compounds 391 removed. 392 k : array-like 393 Associated (effective) reaction rate constants that model the fixed 394 concentrations. 395 396 Notes 397 ----- 398 Keep in mind that when a compound get its concentration fixed, the 399 reaction scheme no longer conserves matter. You can think of it as 400 reacting close to an infinite source of the compound, but it accumulates 401 in the milleu at the given concentration. 402 403 Examples 404 -------- 405 >>> import numpy as np 406 >>> import overreact as rx 407 408 Equilibria under a specific pH can be easily modeled: 409 410 >>> pH = 7 411 >>> scheme = rx.parse_reactions("AH <=> A- + H+") 412 >>> k = np.array([1, 1]) 413 >>> scheme, k = rx.get_fixed_scheme(scheme, k, {"H+": 10**-pH}) 414 >>> scheme 415 Scheme(compounds=('AH', 'A-'), 416 reactions=('AH -> A-', 417 'A- -> AH'), 418 is_half_equilibrium=(True, True), 419 A=((-1.0, 1.0), 420 (1.0, -1.0)), 421 B=((-1.0, 0.0), 422 (1.0, 0.0))) 423 >>> k 424 array([1.e+00, 1.e-07]) 425 426 It is also possible to model the fixed activity of a solvent, for 427 instance: 428 429 >>> scheme = rx.parse_reactions("A + 2H2O -> B") 430 >>> k = np.array([1.0]) 431 >>> scheme, k = rx.get_fixed_scheme(scheme, k, {"H2O": 55.6}) 432 >>> scheme 433 Scheme(compounds=('A', 'B'), 434 reactions=('A -> B',), 435 is_half_equilibrium=(False,), 436 A=((-1.0,), 437 (1.0,)), 438 B=((-1.0,), 439 (1.0,))) 440 >>> k 441 array([3091.36]) 442 443 Multiple reactions work fine, see both examples below: 444 445 >>> pH = 12 446 >>> scheme = rx.parse_reactions("B <- AH <=> A- + H+") 447 >>> k = np.array([10.0, 1, 1]) 448 >>> scheme, k = rx.get_fixed_scheme(scheme, k, {"H+": 10**-pH}) 449 >>> scheme 450 Scheme(compounds=('AH', 'B', 'A-'), 451 reactions=('AH -> B', 452 'AH -> A-', 453 'A- -> AH'), 454 is_half_equilibrium=(False, True, True), 455 A=((-1.0, -1.0, 1.0), 456 (1.0, 0.0, 0.0), 457 (0.0, 1.0, -1.0)), 458 B=((-1.0, -1.0, 0.0), 459 (1.0, 0.0, 0.0), 460 (0.0, 1.0, 0.0))) 461 >>> k 462 array([1.e+01, 1.e+00, 1.e-12]) 463 464 >>> pH = 2 465 >>> scheme = rx.parse_reactions(["AH <=> A- + H+", "B- + H+ <=> BH"]) 466 >>> k = np.array([1, 1, 2, 2]) 467 >>> scheme, k = rx.get_fixed_scheme(scheme, k, {"H+": 10**-pH}) 468 >>> scheme 469 Scheme(compounds=('AH', 'A-', 'B-', 'BH'), 470 reactions=('AH -> A-', 471 'A- -> AH', 472 'B- -> BH', 473 'BH -> B-'), 474 is_half_equilibrium=(True, True, True, True), 475 A=((-1.0, 1.0, 0.0, 0.0), 476 (1.0, -1.0, 0.0, 0.0), 477 (0.0, 0.0, -1.0, 1.0), 478 (0.0, 0.0, 1.0, -1.0)), 479 B=((-1.0, 0.0, 0.0, 0.0), 480 (1.0, 0.0, 0.0, 0.0), 481 (0.0, 0.0, -1.0, 0.0), 482 (0.0, 0.0, 1.0, 0.0))) 483 >>> k 484 array([1. , 0.01, 0.02, 2. ]) 485 486 Multiple fixed compounds also work fine: 487 488 >>> pH = 6 489 >>> scheme = rx.parse_reactions("A + H2O -> B <=> B- + H+") 490 >>> k = np.array([1.0, 100.0, 2.0]) 491 >>> scheme, k = rx.get_fixed_scheme(scheme, k, {"H+": 10**-pH, "H2O": 55.6}) 492 >>> scheme 493 Scheme(compounds=('A', 'B', 'B-'), 494 reactions=('A -> B', 495 'B -> B-', 496 'B- -> B'), 497 is_half_equilibrium=(False, True, True), 498 A=((-1.0, 0.0, 0.0), 499 (1.0, -1.0, 1.0), 500 (0.0, 1.0, -1.0)), 501 B=((-1.0, 0.0, 0.0), 502 (1.0, -1.0, 0.0), 503 (0.0, 1.0, 0.0))) 504 >>> k 505 array([5.56e+01, 1.00e+02, 2.00e-06]) 506 507 This function is a no-op if `fixed_y0` is empty, which is very important 508 for overall code consistency: 509 510 >>> scheme = rx.parse_reactions(["AH <=> A- + H+", "B- + H+ <=> BH"]) 511 >>> k = np.array([1, 1, 2, 2]) 512 >>> new_scheme, new_k = rx.get_fixed_scheme(scheme, k, {}) 513 >>> new_scheme == scheme 514 True 515 >>> np.allclose(new_k, k) 516 True 517 518 """ 519 new_k = np.asarray(k, dtype=np.float64).copy() 520 new_reactions = [] 521 for i, (reaction, is_half_equilibrium) in enumerate( 522 zip(scheme.reactions, scheme.is_half_equilibrium), 523 ): 524 for reactants, products, _ in rx.core._parse_reactions( 525 reaction, 526 ): 527 new_reactants = tuple( 528 (coeff, compound) 529 for (coeff, compound) in reactants 530 if compound not in fixed_y0 531 ) 532 new_products = tuple( 533 (coeff, compound) 534 for (coeff, compound) in products 535 if compound not in fixed_y0 536 ) 537 538 for fixed_compound in fixed_y0: 539 for coeff, compound in reactants: 540 if fixed_compound == compound: 541 new_k[i] *= fixed_y0[fixed_compound] ** coeff 542 543 new_reactions.append((new_reactants, new_products, is_half_equilibrium)) 544 545 new_reactions = tuple(r for r in rx.core._unparse_reactions(new_reactions)) 546 new_is_half_equilibrium = scheme.is_half_equilibrium 547 548 new_A = [] 549 new_B = [] 550 new_compounds = [] 551 for compound, row_A, row_B in zip( 552 scheme.compounds, 553 scheme.A, 554 scheme.B, 555 ): 556 if compound not in fixed_y0: 557 new_compounds.append(compound) 558 new_A.append(row_A) 559 new_B.append(row_B) 560 561 new_compounds = tuple(new_compounds) 562 new_A = tuple(new_A) 563 new_B = tuple(new_B) 564 565 return ( 566 rx.core.Scheme( 567 compounds=new_compounds, 568 reactions=new_reactions, 569 is_half_equilibrium=new_is_half_equilibrium, 570 A=new_A, 571 B=new_B, 572 ), 573 new_k, 574 )
Generate an alternative scheme with some concentrations fixed.
This function returns data that allow the microkinetic simulation of a reaction network under constraints, namely when some compounds have fixed concentrations. This works by 1. removing all references to the fixed compounds and by 2. properly multiplying the reaction rate constants by the respective concentrations.
Parameters
scheme : Scheme
A descriptor of the reaction scheme.
Mostly likely, this comes from a parsed model input file.
See parse_model
.
k : array-like
Reaction rate constant(s). Units match the concentration units given to
the returned function dydt
.
fixed_y0 : dict-like
Fixed initial state. Units match the concentration units given to
the returned function dydt
.
Returns
scheme : Scheme Associated reaction scheme with all references to fixed compounds removed. k : array-like Associated (effective) reaction rate constants that model the fixed concentrations.
Notes
Keep in mind that when a compound get its concentration fixed, the reaction scheme no longer conserves matter. You can think of it as reacting close to an infinite source of the compound, but it accumulates in the milleu at the given concentration.
Examples
>>> import numpy as np
>>> import overreact as rx
Equilibria under a specific pH can be easily modeled:
>>> pH = 7
>>> scheme = rx.parse_reactions("AH <=> A- + H+")
>>> k = np.array([1, 1])
>>> scheme, k = rx.get_fixed_scheme(scheme, k, {"H+": 10**-pH})
>>> scheme
Scheme(compounds=('AH', 'A-'),
reactions=('AH -> A-',
'A- -> AH'),
is_half_equilibrium=(True, True),
A=((-1.0, 1.0),
(1.0, -1.0)),
B=((-1.0, 0.0),
(1.0, 0.0)))
>>> k
array([1.e+00, 1.e-07])
It is also possible to model the fixed activity of a solvent, for instance:
>>> scheme = rx.parse_reactions("A + 2H2O -> B")
>>> k = np.array([1.0])
>>> scheme, k = rx.get_fixed_scheme(scheme, k, {"H2O": 55.6})
>>> scheme
Scheme(compounds=('A', 'B'),
reactions=('A -> B',),
is_half_equilibrium=(False,),
A=((-1.0,),
(1.0,)),
B=((-1.0,),
(1.0,)))
>>> k
array([3091.36])
Multiple reactions work fine, see both examples below:
>>> pH = 12
>>> scheme = rx.parse_reactions("B <- AH <=> A- + H+")
>>> k = np.array([10.0, 1, 1])
>>> scheme, k = rx.get_fixed_scheme(scheme, k, {"H+": 10**-pH})
>>> scheme
Scheme(compounds=('AH', 'B', 'A-'),
reactions=('AH -> B',
'AH -> A-',
'A- -> AH'),
is_half_equilibrium=(False, True, True),
A=((-1.0, -1.0, 1.0),
(1.0, 0.0, 0.0),
(0.0, 1.0, -1.0)),
B=((-1.0, -1.0, 0.0),
(1.0, 0.0, 0.0),
(0.0, 1.0, 0.0)))
>>> k
array([1.e+01, 1.e+00, 1.e-12])
>>> pH = 2
>>> scheme = rx.parse_reactions(["AH <=> A- + H+", "B- + H+ <=> BH"])
>>> k = np.array([1, 1, 2, 2])
>>> scheme, k = rx.get_fixed_scheme(scheme, k, {"H+": 10**-pH})
>>> scheme
Scheme(compounds=('AH', 'A-', 'B-', 'BH'),
reactions=('AH -> A-',
'A- -> AH',
'B- -> BH',
'BH -> B-'),
is_half_equilibrium=(True, True, True, True),
A=((-1.0, 1.0, 0.0, 0.0),
(1.0, -1.0, 0.0, 0.0),
(0.0, 0.0, -1.0, 1.0),
(0.0, 0.0, 1.0, -1.0)),
B=((-1.0, 0.0, 0.0, 0.0),
(1.0, 0.0, 0.0, 0.0),
(0.0, 0.0, -1.0, 0.0),
(0.0, 0.0, 1.0, 0.0)))
>>> k
array([1. , 0.01, 0.02, 2. ])
Multiple fixed compounds also work fine:
>>> pH = 6
>>> scheme = rx.parse_reactions("A + H2O -> B <=> B- + H+")
>>> k = np.array([1.0, 100.0, 2.0])
>>> scheme, k = rx.get_fixed_scheme(scheme, k, {"H+": 10**-pH, "H2O": 55.6})
>>> scheme
Scheme(compounds=('A', 'B', 'B-'),
reactions=('A -> B',
'B -> B-',
'B- -> B'),
is_half_equilibrium=(False, True, True),
A=((-1.0, 0.0, 0.0),
(1.0, -1.0, 1.0),
(0.0, 1.0, -1.0)),
B=((-1.0, 0.0, 0.0),
(1.0, -1.0, 0.0),
(0.0, 1.0, 0.0)))
>>> k
array([5.56e+01, 1.00e+02, 2.00e-06])
This function is a no-op if fixed_y0
is empty, which is very important
for overall code consistency:
>>> scheme = rx.parse_reactions(["AH <=> A- + H+", "B- + H+ <=> BH"])
>>> k = np.array([1, 1, 2, 2])
>>> new_scheme, new_k = rx.get_fixed_scheme(scheme, k, {})
>>> new_scheme == scheme
True
>>> np.allclose(new_k, k)
True
321def get_freeenergies( 322 compounds: dict, 323 bias: float = 0.0, 324 environment: str | None = None, 325 method: str = "standard", 326 qrrho: bool | tuple[bool, bool] = True, 327 temperature: float = 298.15, 328 pressure: float = constants.atm, 329): 330 """Obtain free energies for compounds at a given temperature and pressure. 331 332 Parameters 333 ---------- 334 compounds : dict-like 335 A descriptor of the compounds. 336 Mostly likely, this comes from a parsed input file. 337 See `overreact.io.parse_model`. 338 bias : array-like, optional 339 Energy to be added to free energies. 340 environment : str or None, optional 341 Choose between "gas" and a solvent. This is chosen for you by default, 342 based on the names of each compound. If given, all compounds will 343 have the same behavior. 344 method : str, optional 345 This is a placeholder for future functionality. 346 There are plans to implement more sophisticated methods for calculating 347 entropies such as in 348 [*Phys. Chem. Chem. Phys.*, **2019**, 21, 18920-18929](https://doi.org/10.1039/C9CP03226F) 349 and 350 [*J. Chem. Theory Comput.* **2019**, 15, 5, 3204-3214](https://doi.org/10.1021/acs.jctc.9b00214). 351 Head over to the 352 [discussions](https://github.com/geem-lab/overreact/discussions) if 353 you're interested and would like to contribute. 354 Leave this as "standard" for now. 355 qrrho : bool or tuple-like, optional 356 Apply both the quasi-rigid rotor harmonic oscillator (QRRHO) 357 approximations of M. Head-Gordon and others (enthalpy correction, see 358 [*J. Phys. Chem. C* **2015**, 119, 4, 1840-1850](http://dx.doi.org/10.1021/jp509921r)) 359 and S. Grimme (entropy correction, see 360 [*Theory. Chem. Eur. J.*, **2012**, 18: 9955-9964](https://doi.org/10.1002/chem.201200497)) 361 on top of the classical RRHO. 362 temperature : array-like, optional 363 Absolute temperature in Kelvin. 364 pressure : array-like, optional 365 Reference gas pressure. 366 367 Returns 368 ------- 369 array-like 370 371 Examples 372 -------- 373 >>> import overreact as rx 374 >>> from overreact import _constants as constants 375 >>> model = rx.parse_model("data/ethane/B97-3c/model.k") 376 >>> freeenergies = get_freeenergies(model.compounds, qrrho=(False, True)) 377 >>> (freeenergies - freeenergies.min()) / constants.kcal 378 array([0. , 2.62281461]) 379 >>> freeenergies = get_freeenergies(model.compounds) 380 >>> (freeenergies - freeenergies.min()) / constants.kcal 381 array([0. , 2.62862818]) 382 383 You can consider all compounds as solvated if you want: 384 385 >>> sol_freeenergies = get_freeenergies(model.compounds, environment="solvent") 386 >>> (sol_freeenergies - freeenergies) / constants.kcal 387 array([1.89432845, 1.89432845]) 388 389 You can set a simple energy bias, either as a constant or compound-wise: 390 391 >>> get_freeenergies(model.compounds, bias=1.0) - freeenergies 392 array([1., 1.]) 393 >>> get_freeenergies(model.compounds, bias=-1.0) - freeenergies 394 array([-1., -1.]) 395 >>> get_freeenergies(model.compounds, bias=[1.0, -1.0]) - freeenergies 396 array([ 1., -1.]) 397 """ 398 qrrho_enthalpy, qrrho_entropy = _check_qrrho(qrrho) 399 enthalpies = get_enthalpies( 400 compounds, 401 qrrho=qrrho_enthalpy, 402 temperature=temperature, 403 ) 404 entropies = get_entropies( 405 compounds, 406 environment=environment, 407 method=method, 408 qrrho=qrrho_entropy, 409 temperature=temperature, 410 pressure=pressure, 411 ) 412 # TODO(schneiderfelipe): log the contribution of bias 413 return enthalpies - temperature * entropies + np.asarray(bias)
Obtain free energies for compounds at a given temperature and pressure.
Parameters
compounds : dict-like
A descriptor of the compounds.
Mostly likely, this comes from a parsed input file.
See parse_model
.
bias : array-like, optional
Energy to be added to free energies.
environment : str or None, optional
Choose between "gas" and a solvent. This is chosen for you by default,
based on the names of each compound. If given, all compounds will
have the same behavior.
method : str, optional
This is a placeholder for future functionality.
There are plans to implement more sophisticated methods for calculating
entropies such as in
Phys. Chem. Chem. Phys., 2019, 21, 18920-18929
and
J. Chem. Theory Comput. 2019, 15, 5, 3204-3214.
Head over to the
discussions if
you're interested and would like to contribute.
Leave this as "standard" for now.
qrrho : bool or tuple-like, optional
Apply both the quasi-rigid rotor harmonic oscillator (QRRHO)
approximations of M. Head-Gordon and others (enthalpy correction, see
J. Phys. Chem. C 2015, 119, 4, 1840-1850)
and S. Grimme (entropy correction, see
Theory. Chem. Eur. J., 2012, 18: 9955-9964)
on top of the classical RRHO.
temperature : array-like, optional
Absolute temperature in Kelvin.
pressure : array-like, optional
Reference gas pressure.
Returns
array-like
Examples
>>> import overreact as rx
>>> from overreact import _constants as constants
>>> model = rx.parse_model("data/ethane/B97-3c/model.k")
>>> freeenergies = get_freeenergies(model.compounds, qrrho=(False, True))
>>> (freeenergies - freeenergies.min()) / constants.kcal
array([0. , 2.62281461])
>>> freeenergies = get_freeenergies(model.compounds)
>>> (freeenergies - freeenergies.min()) / constants.kcal
array([0. , 2.62862818])
You can consider all compounds as solvated if you want:
>>> sol_freeenergies = get_freeenergies(model.compounds, environment="solvent")
>>> (sol_freeenergies - freeenergies) / constants.kcal
array([1.89432845, 1.89432845])
You can set a simple energy bias, either as a constant or compound-wise:
>>> get_freeenergies(model.compounds, bias=1.0) - freeenergies
array([1., 1.])
>>> get_freeenergies(model.compounds, bias=-1.0) - freeenergies
array([-1., -1.])
>>> get_freeenergies(model.compounds, bias=[1.0, -1.0]) - freeenergies
array([ 1., -1.])
39def get_internal_energies( 40 compounds: dict, 41 qrrho: bool = True, 42 temperature: float = 298.15, 43): 44 """Obtain internal energies for compounds at a given temperature. 45 46 Parameters 47 ---------- 48 compounds : dict-like 49 A descriptor of the compounds. 50 Mostly likely, this comes from a parsed input file. 51 See `overreact.io.parse_model`. 52 qrrho : bool, optional 53 Apply the quasi-rigid rotor harmonic oscillator (QRRHO) approximation of 54 M. Head-Gordon and others (see 55 [*J. Phys. Chem. C* **2015**, 119, 4, 1840-1850](http://dx.doi.org/10.1021/jp509921r)) 56 on top of the classical RRHO. 57 temperature : array-like, optional 58 Absolute temperature in Kelvin. 59 60 Returns 61 ------- 62 array-like 63 64 Examples 65 -------- 66 >>> import overreact as rx 67 >>> from overreact import _constants as constants 68 >>> model = rx.parse_model("data/ethane/B97-3c/model.k") 69 >>> internal_energies = get_internal_energies(model.compounds) 70 >>> (internal_energies - internal_energies.min()) / constants.kcal 71 array([0. , 2.20053981]) 72 73 """ 74 compounds = rx.io._check_compounds(compounds) 75 internal_energies = [] 76 for name in compounds: 77 logger.info(f"calculate internal energy: {name}") 78 79 # TODO(schneiderfelipe): inertia might benefit from caching 80 moments, _, _ = coords.inertia( 81 compounds[name].atommasses, 82 compounds[name].atomcoords, 83 ) 84 85 internal_energy = rx.thermo.calc_internal_energy( 86 energy=compounds[name].energy, 87 degeneracy=compounds[name].mult, 88 moments=moments, 89 vibfreqs=compounds[name].vibfreqs, 90 qrrho=qrrho, 91 temperature=temperature, 92 ) 93 internal_energies.append(internal_energy) 94 return np.array(internal_energies)
Obtain internal energies for compounds at a given temperature.
Parameters
compounds : dict-like
A descriptor of the compounds.
Mostly likely, this comes from a parsed input file.
See parse_model
.
qrrho : bool, optional
Apply the quasi-rigid rotor harmonic oscillator (QRRHO) approximation of
M. Head-Gordon and others (see
J. Phys. Chem. C 2015, 119, 4, 1840-1850)
on top of the classical RRHO.
temperature : array-like, optional
Absolute temperature in Kelvin.
Returns
array-like
Examples
>>> import overreact as rx
>>> from overreact import _constants as constants
>>> model = rx.parse_model("data/ethane/B97-3c/model.k")
>>> internal_energies = get_internal_energies(model.compounds)
>>> (internal_energies - internal_energies.min()) / constants.kcal
array([0. , 2.20053981])
416def get_k( 417 scheme: Scheme, 418 compounds: dict | None = None, 419 bias: float = 0.0, 420 tunneling: str = "eckart", 421 qrrho: bool | tuple[bool, bool] = True, 422 scale: str = "l mol-1 s-1", 423 temperature: float = 298.15, 424 pressure: float = constants.atm, 425 delta_freeenergies: float | None = None, 426 molecularity: float | None = None, 427 volume: float | None = None, 428) -> float: 429 r"""Obtain reaction rate constants for a given reaction scheme. 430 431 Parameters 432 ---------- 433 scheme : Scheme 434 A descriptor of the reaction scheme. 435 Mostly likely, this comes from a parsed input file. 436 See `overreact.io.parse_model`. 437 compounds : dict-like, optional 438 A descriptor of the compounds. 439 Mostly likely, this comes from a parsed input file. 440 See `overreact.io.parse_model`. 441 bias : array-like, optional 442 Energy to be added to free energies. 443 tunneling : str or None, optional 444 Choose between "eckart", "wigner" or None (or "none"). 445 qrrho : bool or tuple-like, optional 446 Apply both the quasi-rigid rotor harmonic oscillator (QRRHO) 447 approximations of M. Head-Gordonand others (enthalpy correction, see 448 [*J. Phys. Chem. C* **2015**, 119, 4, 1840-1850](http://dx.doi.org/10.1021/jp509921r)) 449 and S. Grimme (entropy correction, see 450 [*Theory. Chem. Eur. J.*, **2012**, 18: 9955-9964](https://doi.org/10.1002/chem.201200497)) 451 on top of the classical RRHO. 452 scale : str, optional 453 Reaction rate units. Possible values are "cm3 mol-1 s-1", 454 "l mol-1 s-1", "m3 mol-1 s-1", "cm3 particle-1 s-1", "mmHg-1 s-1" and 455 "atm-1 s-1". 456 temperature : array-like, optional 457 Absolute temperature in Kelvin. 458 pressure : array-like, optional 459 Reference gas pressure. 460 delta_freeenergies : array-like, optional 461 Use this instead of obtaining delta free energies from the compounds. 462 molecularity : array-like, optional 463 Reaction order, i.e., number of molecules that come together to react. 464 If set, this is used to calculate `delta_moles` for 465 `overreact.thermo.equilibrium_constant`, which effectively calculates a solution 466 equilibrium constant between reactants and the transition state for 467 gas phase data. You should set this to `None` if your free energies 468 were already adjusted for solution Gibbs free energies. 469 volume : float, optional 470 Molar volume. 471 472 Returns 473 ------- 474 array-like 475 476 Notes 477 ----- 478 Some symbols are accepted as alternatives in "scale": "M-1", "ml" and 479 "torr-1" are understood as "l mol-1", "cm3" and "mmHg-1", respectively. 480 481 Examples 482 -------- 483 Below is an example of an estimate for the rate of methyl rotation in 484 ethane (a trivial attempt to reproduce 485 [*Science*, **2006**, 313, 5795, 1951-1955](https://doi.org/10.1126/science.1132178)). 486 **How many turns it does per second?** 487 488 >>> model = rx.parse_model("data/ethane/B97-3c/model.k") 489 >>> get_k(model.scheme, model.compounds) 490 array([8.16e+10]) 491 >>> get_k(model.scheme, model.compounds, qrrho=(False, True)) 492 array([8.24968117e+10]) 493 >>> get_k(model.scheme, model.compounds, qrrho=False) 494 array([8.26909266e+10]) 495 >>> get_k(model.scheme, model.compounds, tunneling="wigner") 496 array([7.99e+10]) 497 >>> get_k(model.scheme, model.compounds, tunneling=None) 498 array([7.35e+10]) 499 500 The calculated value is off by less than 2% from the experimental value 501 (:math:`\frac{1}{12 \times 10^{-12}} \text{s}^{-1} = 8.33 \times 10^{10} \text{s}^{-1}`). 502 We use Eckart tunneling by default, but see the effect of changing it 503 above. 504 505 The units of the returned reaction rate constants can be selected for 506 non-unimolecular processes. The following is an attempt to reproduce 507 [*J Atmos Chem*, **1996** 23, 37-49](https://doi.org/10.1007/BF00058703) for 508 the reaction of proton-withdrawal by a chloride radical from the methane 509 molecule 510 :math:`\ce{CH4 + \cdot Cl -> [H3C\cdots H\cdots Cl]^\ddagger -> H3C\cdot + HCl}`: 511 512 >>> model = rx.parse_model("data/tanaka1996/UMP2/cc-pVTZ/model.jk") 513 >>> get_k(model.scheme, model.compounds, temperature=300, 514 ... scale="cm3 particle-1 s-1") 515 array([9.60e-14]) 516 517 (By the way, according to the Jet Propulsion Laboratory, 518 [Publication No. 19-5](https://jpldataeval.jpl.nasa.gov/), 519 the experimental reaction rate constant for this reaction is 520 :math:`1.0 \times 10^{-13} \text{cm}^3 \text{particle}^{-1} \text{s}^{-1}`.) 521 522 The returned units are "M-1 s-1" by default: 523 524 >>> get_k(model.scheme, model.compounds) \ 525 ... == get_k(model.scheme, model.compounds, scale="l mol-1 s-1") 526 array([ True]) 527 528 You can also turn the tunneling correction off by using the string "none": 529 530 >>> get_k(model.scheme, model.compounds, tunneling="none") \ 531 ... == get_k(model.scheme, model.compounds, tunneling=None) 532 array([ True]) 533 534 You can set a simple energy bias, either as a constant or compound-wise: 535 536 >>> get_k(model.scheme, model.compounds, bias=1.0 * constants.kcal, 537 ... temperature=300.0, scale="cm3 particle-1 s-1") 538 array([5.14e-13]) 539 >>> get_k(model.scheme, model.compounds, 540 ... bias=np.array([0.0, 0.0, -1.4, 0.0, 0.0]) * constants.kcal, 541 ... temperature=300.0, scale="cm3 particle-1 s-1") 542 array([1.1e-12]) 543 """ 544 qrrho_enthalpy, qrrho_entropy = _check_qrrho(qrrho) 545 scheme = rx.core._check_scheme(scheme) 546 if compounds is not None: 547 compounds = rx.io._check_compounds(compounds) 548 if delta_freeenergies is None: 549 assert compounds is not None, "compounds could not be inferred" 550 freeenergies = get_freeenergies( 551 compounds, 552 bias=bias, 553 qrrho=(qrrho_enthalpy, qrrho_entropy), 554 # NOTE(schneiderfelipe): ensure we get rate constants in M-1 s-1. 555 # TODO(schneiderfelipe): this strategy will have to change 556 # somewhat when we improve solvation entropy models. 557 environment="solvent", 558 temperature=temperature, 559 pressure=pressure, 560 ) 561 562 # TODO(schneiderfelipe): log the contribution of reaction symmetry 563 delta_freeenergies = rx.get_delta( 564 scheme.B, 565 freeenergies, 566 ) - temperature * rx.get_reaction_entropies( 567 scheme.B, 568 temperature=temperature, 569 pressure=pressure, 570 ) 571 572 if molecularity is None: 573 molecularity = rx.thermo.get_molecularity(scheme.A) 574 575 # NOTE(schneiderfelipe): passing molecularity here to rates.eyring messes up 576 # rate constant units (by a factor of M-1 s-1 to atm-1 s-1), so we leave it as is. 577 k = rates.eyring( 578 delta_freeenergies, 579 temperature=temperature, 580 pressure=pressure, 581 volume=volume, 582 ) 583 584 # make reaction rate constants for equilibria as close as possible to one 585 i = 0 586 while i < len(scheme.is_half_equilibrium): 587 if scheme.is_half_equilibrium[i]: 588 pair = k[i : i + 2] 589 _K = pair[0] / pair[1] 590 591 denom = pair.min() 592 if denom == 0.0: 593 logger.warning( 594 "found half-equilibrium reaction with zero rate constant: skipping equilibrium normalization", 595 ) 596 denom = 1.0 597 598 k[i : i + 2] = pair / denom 599 assert np.isclose(_K, k[i] / k[i + 1]), ( 600 f"reaction rate constants {k[i]} and {k[i + 1]} for " 601 "equilibria could not be made to match the expected " 602 f"equilibrium constant value {_K}" 603 ) 604 605 # loop over pairs of equilibria 606 i += 1 607 i += 1 608 609 logger.info( 610 "(classical) reaction rate constants: " 611 f"{', '.join([f'{v:7.3g}' for v in k])} atm⁻ⁿ⁺¹·s⁻¹", 612 ) 613 if tunneling not in {"none", None}: 614 if compounds is not None: 615 kappa = get_kappa( 616 scheme, 617 compounds, 618 method=tunneling, 619 qrrho=qrrho_enthalpy, 620 temperature=temperature, 621 ) 622 k *= kappa 623 else: 624 # TODO(schneiderfelipe): when get_kappa accept deltas, this will 625 # be probably unnecessary. 626 logger.warning( 627 "assuming unitary tunneling coefficients due to incomplete compound data", 628 ) 629 logger.info( 630 "(tunneling) reaction rate constants: " 631 f"{', '.join([f'{v:7.3g}' for v in k])} atm⁻ⁿ⁺¹·s⁻¹", 632 ) 633 634 # TODO(schneiderfelipe): ensure diffusional limit for reactions in 635 # solvation using Collins-Kimball theory. This includes half-equilibria. 636 return rates.convert_rate_constant( 637 k, 638 new_scale=scale, 639 # NOTE(schneiderfelipe): all the code above should always generate 640 # rate constants in M-1 s-1, so that we convert them here. 641 old_scale="l mol-1 s-1", 642 molecularity=molecularity, 643 temperature=temperature, 644 pressure=pressure, 645 )
Obtain reaction rate constants for a given reaction scheme.
Parameters
scheme : Scheme
A descriptor of the reaction scheme.
Mostly likely, this comes from a parsed input file.
See parse_model
.
compounds : dict-like, optional
A descriptor of the compounds.
Mostly likely, this comes from a parsed input file.
See parse_model
.
bias : array-like, optional
Energy to be added to free energies.
tunneling : str or None, optional
Choose between "eckart", "wigner" or None (or "none").
qrrho : bool or tuple-like, optional
Apply both the quasi-rigid rotor harmonic oscillator (QRRHO)
approximations of M. Head-Gordonand others (enthalpy correction, see
J. Phys. Chem. C 2015, 119, 4, 1840-1850)
and S. Grimme (entropy correction, see
Theory. Chem. Eur. J., 2012, 18: 9955-9964)
on top of the classical RRHO.
scale : str, optional
Reaction rate units. Possible values are "cm3 mol-1 s-1",
"l mol-1 s-1", "m3 mol-1 s-1", "cm3 particle-1 s-1", "mmHg-1 s-1" and
"atm-1 s-1".
temperature : array-like, optional
Absolute temperature in Kelvin.
pressure : array-like, optional
Reference gas pressure.
delta_freeenergies : array-like, optional
Use this instead of obtaining delta free energies from the compounds.
molecularity : array-like, optional
Reaction order, i.e., number of molecules that come together to react.
If set, this is used to calculate delta_moles
for
overreact.thermo.equilibrium_constant
, which effectively calculates a solution
equilibrium constant between reactants and the transition state for
gas phase data. You should set this to None
if your free energies
were already adjusted for solution Gibbs free energies.
volume : float, optional
Molar volume.
Returns
array-like
Notes
Some symbols are accepted as alternatives in "scale": "M-1", "ml" and "torr-1" are understood as "l mol-1", "cm3" and "mmHg-1", respectively.
Examples
Below is an example of an estimate for the rate of methyl rotation in ethane (a trivial attempt to reproduce Science, 2006, 313, 5795, 1951-1955). How many turns it does per second?
>>> model = rx.parse_model("data/ethane/B97-3c/model.k")
>>> get_k(model.scheme, model.compounds)
array([8.16e+10])
>>> get_k(model.scheme, model.compounds, qrrho=(False, True))
array([8.24968117e+10])
>>> get_k(model.scheme, model.compounds, qrrho=False)
array([8.26909266e+10])
>>> get_k(model.scheme, model.compounds, tunneling="wigner")
array([7.99e+10])
>>> get_k(model.scheme, model.compounds, tunneling=None)
array([7.35e+10])
The calculated value is off by less than 2% from the experimental value (\( \frac{1}{12 \times 10^{-12}} \text{s}^{-1} = 8.33 \times 10^{10} \text{s}^{-1} \)). We use Eckart tunneling by default, but see the effect of changing it above.
The units of the returned reaction rate constants can be selected for non-unimolecular processes. The following is an attempt to reproduce J Atmos Chem, 1996 23, 37-49 for the reaction of proton-withdrawal by a chloride radical from the methane molecule \( \ce{CH4 + \cdot Cl -> [H3C\cdots H\cdots Cl]^\ddagger -> H3C\cdot + HCl} \):
>>> model = rx.parse_model("data/tanaka1996/UMP2/cc-pVTZ/model.jk")
>>> get_k(model.scheme, model.compounds, temperature=300,
... scale="cm3 particle-1 s-1")
array([9.60e-14])
(By the way, according to the Jet Propulsion Laboratory, Publication No. 19-5, the experimental reaction rate constant for this reaction is \( 1.0 \times 10^{-13} \text{cm}^3 \text{particle}^{-1} \text{s}^{-1} \).)
The returned units are "M-1 s-1" by default:
>>> get_k(model.scheme, model.compounds) \
... == get_k(model.scheme, model.compounds, scale="l mol-1 s-1")
array([ True])
You can also turn the tunneling correction off by using the string "none":
>>> get_k(model.scheme, model.compounds, tunneling="none") \
... == get_k(model.scheme, model.compounds, tunneling=None)
array([ True])
You can set a simple energy bias, either as a constant or compound-wise:
>>> get_k(model.scheme, model.compounds, bias=1.0 * constants.kcal,
... temperature=300.0, scale="cm3 particle-1 s-1")
array([5.14e-13])
>>> get_k(model.scheme, model.compounds,
... bias=np.array([0.0, 0.0, -1.4, 0.0, 0.0]) * constants.kcal,
... temperature=300.0, scale="cm3 particle-1 s-1")
array([1.1e-12])
649def get_kappa( 650 scheme: Scheme, 651 compounds: dict, 652 method: str = "eckart", 653 qrrho: bool = True, 654 temperature: float = 298.15, 655): 656 r"""Obtain tunneling transmission coefficients at a given temperature. 657 658 One tunneling transmission coefficient is calculated for each reaction. If 659 a reaction lacks a transition state (i.e., a half-equilibrium reaction), 660 its transmission coefficient is set to unity. 661 662 Parameters 663 ---------- 664 scheme : Scheme 665 A descriptor of the reaction scheme. 666 Mostly likely, this comes from a parsed input file. 667 See `overreact.io.parse_model`. 668 compounds : dict-like 669 A descriptor of the compounds. 670 Mostly likely, this comes from a parsed input file. 671 See `overreact.io.parse_model`. 672 method : str or None, optional 673 Choose between "eckart", "wigner" or None (or "none"). 674 qrrho : bool, optional 675 Apply both the quasi-rigid rotor harmonic oscillator (QRRHO) 676 approximations of M. Head-Gordon and others (enthalpy correction, see 677 [*J. Phys. Chem. C* **2015**, 119, 4, 1840-1850](http://dx.doi.org/10.1021/jp509921r)) 678 and S. Grimme (entropy correction, see 679 [*Theory. Chem. Eur. J.*, **2012**, 18: 9955-9964](https://doi.org/10.1002/chem.201200497)) 680 on top of the classical RRHO. 681 temperature : array-like, optional 682 Absolute temperature in Kelvin. 683 684 Returns 685 ------- 686 array-like 687 688 Raises 689 ------ 690 ValueError 691 If `method` is not supported. 692 693 Examples 694 -------- 695 Below is an example of an estimate of how much quantum tunneling 696 contributes to the rate of methyl rotation in ethane (see 697 [*Science*, **2006**, 313, 5795, 1951-1955](https://doi.org/10.1126/science.1132178) 698 for some interesting experimental data on this reaction). 699 700 >>> model = rx.parse_model("data/ethane/B97-3c/model.k") 701 >>> kappa = get_kappa(model.scheme, model.compounds) 702 >>> kappa 703 array([1.110]) 704 >>> get_kappa(model.scheme, model.compounds, method="none") 705 array([1.0]) 706 >>> get_kappa(model.scheme, model.compounds, method="none") \ 707 ... == get_kappa(model.scheme, model.compounds, method=None) 708 array([ True]) 709 710 You can calculate each piece of the reaction rate constant by hand, 711 if you want. Just make sure that you don't calculate the tunneling 712 coefficient twice: 713 714 >>> kappa * get_k(model.scheme, model.compounds, tunneling=None) 715 array([8.e+10]) 716 """ 717 scheme = rx.core._check_scheme(scheme) 718 compounds = rx.io._check_compounds(compounds) 719 720 if method == "eckart": 721 # NOTE(schneiderfelipe): We need electronic energies + ZPE here, so we 722 # get smaller transmission coefficients. 723 energies = get_enthalpies(compounds, qrrho=qrrho, temperature=0.0) 724 delta_forward = rx.get_delta(scheme.B, energies) # B - A 725 delta_backward = delta_forward - rx.get_delta( 726 scheme.A, 727 energies, 728 ) # B - C == B - A - (C - A) 729 730 kappas = [] 731 for i, ts in enumerate( 732 rx.get_transition_states(scheme.A, scheme.B, scheme.is_half_equilibrium), 733 ): 734 if ts is None: 735 kappas.append(1.0) 736 else: 737 transition_state = scheme.compounds[ts] 738 vibfreq = compounds[transition_state].vibfreqs[0] 739 740 if method == "eckart": 741 with warnings.catch_warnings(): 742 warnings.filterwarnings("error") 743 try: 744 kappa = tunnel.eckart( 745 vibfreq, 746 delta_forward[i], 747 delta_backward[i], 748 temperature=temperature, 749 ) 750 except RuntimeWarning as e: 751 logger.warning( 752 f"using Wigner tunneling correction: {e}", 753 ) 754 kappa = tunnel.wigner(vibfreq, temperature=temperature) 755 elif method == "wigner": 756 kappa = tunnel.wigner(vibfreq, temperature=temperature) 757 elif method in {"none", None}: 758 kappa = 1.0 759 else: 760 msg = f"unavailable method: '{method}'" 761 raise ValueError(msg) 762 763 kappas.append(kappa) 764 765 # TODO(schneiderfelipe): is this correct? shouldn't we correct shapes 766 # somewhere else? 767 vec_kappas = np.asarray(kappas).flatten() 768 logger.info( 769 "(quantum) tunneling coefficients: " 770 f"{', '.join([f'{kappa:7.3g}' for kappa in vec_kappas])}", 771 ) 772 return vec_kappas
Obtain tunneling transmission coefficients at a given temperature.
One tunneling transmission coefficient is calculated for each reaction. If a reaction lacks a transition state (i.e., a half-equilibrium reaction), its transmission coefficient is set to unity.
Parameters
scheme : Scheme
A descriptor of the reaction scheme.
Mostly likely, this comes from a parsed input file.
See parse_model
.
compounds : dict-like
A descriptor of the compounds.
Mostly likely, this comes from a parsed input file.
See parse_model
.
method : str or None, optional
Choose between "eckart", "wigner" or None (or "none").
qrrho : bool, optional
Apply both the quasi-rigid rotor harmonic oscillator (QRRHO)
approximations of M. Head-Gordon and others (enthalpy correction, see
J. Phys. Chem. C 2015, 119, 4, 1840-1850)
and S. Grimme (entropy correction, see
Theory. Chem. Eur. J., 2012, 18: 9955-9964)
on top of the classical RRHO.
temperature : array-like, optional
Absolute temperature in Kelvin.
Returns
array-like
Raises
ValueError
If method
is not supported.
Examples
Below is an example of an estimate of how much quantum tunneling contributes to the rate of methyl rotation in ethane (see Science, 2006, 313, 5795, 1951-1955 for some interesting experimental data on this reaction).
>>> model = rx.parse_model("data/ethane/B97-3c/model.k")
>>> kappa = get_kappa(model.scheme, model.compounds)
>>> kappa
array([1.110])
>>> get_kappa(model.scheme, model.compounds, method="none")
array([1.0])
>>> get_kappa(model.scheme, model.compounds, method="none") \
... == get_kappa(model.scheme, model.compounds, method=None)
array([ True])
You can calculate each piece of the reaction rate constant by hand, if you want. Just make sure that you don't calculate the tunneling coefficient twice:
>>> kappa * get_k(model.scheme, model.compounds, tunneling=None)
array([8.e+10])
849def get_reaction_entropies(transform, temperature=298.15, pressure=constants.atm): 850 r"""Calculate entropy contributions from the overall reaction structure. 851 852 This function currently implements the reaction translational entropy, a 853 result of the indistinguishability of reactants or products, i.e., a 854 difference in entropy of :math:`R ln 2!` for the reactions 855 856 .. math:: 857 \ce{A + B -> C} 858 859 and 860 861 .. math:: 862 \ce{2A -> C} 863 864 Parameters 865 ---------- 866 transform : array-like 867 temperature : array-like, optional 868 Absolute temperature in Kelvin. 869 pressure : array-like, optional 870 Reference gas pressure. 871 872 Returns 873 ------- 874 delta_entropy : array-like 875 876 Examples 877 -------- 878 >>> import overreact as rx 879 880 >>> scheme = rx.parse_reactions("A + B <=> C") 881 >>> get_reaction_entropies(scheme.A) 882 array([0.0, 0.0]) 883 >>> scheme = rx.parse_reactions("2A <=> C") 884 >>> get_reaction_entropies(scheme.A) 885 array([-5.763, 5.763]) 886 >>> get_reaction_entropies(scheme.A, temperature=400.0) 887 array([-5.763, 5.763]) 888 889 >>> scheme = rx.parse_reactions("A <=> B + C") 890 >>> get_reaction_entropies(scheme.A) 891 array([0.0, 0.0]) 892 >>> scheme = rx.parse_reactions("A <=> 2B") 893 >>> get_reaction_entropies(scheme.A) 894 array([ 5.763, -5.763]) 895 896 >>> scheme = rx.parse_reactions("A + B -> E# -> C") 897 >>> get_reaction_entropies(scheme.A) 898 array([0.0]) 899 >>> get_reaction_entropies(scheme.B) 900 array([0.0]) 901 >>> scheme = rx.parse_reactions("2A -> E# -> C") 902 >>> get_reaction_entropies(scheme.A) 903 array([-5.763]) 904 >>> get_reaction_entropies(scheme.B) 905 array([-5.763]) 906 """ 907 sym = factorial(np.abs(np.asarray(transform))) 908 return np.sum( 909 np.sign(transform) 910 * change_reference_state(sym, 1, temperature=temperature, pressure=pressure), 911 axis=0, 912 )
Calculate entropy contributions from the overall reaction structure.
This function currently implements the reaction translational entropy, a result of the indistinguishability of reactants or products, i.e., a difference in entropy of \( R ln 2! \) for the reactions
$$\ce{A + B -> C}$$
and
$$\ce{2A -> C}$$
Parameters
transform : array-like temperature : array-like, optional Absolute temperature in Kelvin. pressure : array-like, optional Reference gas pressure.
Returns
delta_entropy : array-like
Examples
>>> import overreact as rx
>>> scheme = rx.parse_reactions("A + B <=> C")
>>> get_reaction_entropies(scheme.A)
array([0.0, 0.0])
>>> scheme = rx.parse_reactions("2A <=> C")
>>> get_reaction_entropies(scheme.A)
array([-5.763, 5.763])
>>> get_reaction_entropies(scheme.A, temperature=400.0)
array([-5.763, 5.763])
>>> scheme = rx.parse_reactions("A <=> B + C")
>>> get_reaction_entropies(scheme.A)
array([0.0, 0.0])
>>> scheme = rx.parse_reactions("A <=> 2B")
>>> get_reaction_entropies(scheme.A)
array([ 5.763, -5.763])
>>> scheme = rx.parse_reactions("A + B -> E# -> C")
>>> get_reaction_entropies(scheme.A)
array([0.0])
>>> get_reaction_entropies(scheme.B)
array([0.0])
>>> scheme = rx.parse_reactions("2A -> E# -> C")
>>> get_reaction_entropies(scheme.A)
array([-5.763])
>>> get_reaction_entropies(scheme.B)
array([-5.763])
85def get_transition_states(A, B, is_half_equilibrium): 86 """Return the indices of transition states for each reaction. 87 88 Parameters 89 ---------- 90 A, B : array-like 91 is_half_equilibrium : sequence 92 93 Returns 94 ------- 95 sequence 96 97 Examples 98 -------- 99 >>> scheme = parse_reactions("A -> B") 100 >>> print(scheme) 101 Scheme(compounds=('A', 'B'), 102 reactions=('A -> B',), 103 is_half_equilibrium=(False,), 104 A=((-1.,), (1.,)), 105 B=((-1.,), (1.,))) 106 >>> get_transition_states(scheme.A, scheme.B, scheme.is_half_equilibrium) 107 (None,) 108 109 >>> scheme = parse_reactions("S -> E‡ -> S") 110 >>> print(scheme) 111 Scheme(compounds=('S', 'E‡'), 112 reactions=('S -> S',), 113 is_half_equilibrium=(False,), 114 A=((0.,), (0.,)), 115 B=((-1.,), (1.,))) 116 >>> get_transition_states(scheme.A, scheme.B, scheme.is_half_equilibrium) 117 (1,) 118 119 >>> scheme = parse_reactions("E + S <=> ES -> ES‡ -> E + P") 120 >>> print(scheme) 121 Scheme(compounds=('E', 'S', 'ES', 'ES‡', 'P'), 122 reactions=('E + S -> ES', 'ES -> E + S', 'ES -> E + P'), 123 is_half_equilibrium=(True, True, False), 124 A=((-1., 1., 1.), 125 (-1., 1., 0.), 126 (1., -1., -1.), 127 (0., 0., 0.), 128 (0., 0., 1.)), 129 B=((-1., 0., 0.), 130 (-1., 0., 0.), 131 (1., 0., -1.), 132 (0., 0., 1.), 133 (0., 0., 0.))) 134 >>> get_transition_states(scheme.A, scheme.B, scheme.is_half_equilibrium) 135 (None, None, 3) 136 137 """ 138 tau = np.asarray(B) - np.asarray(A) > 0 # transition state matrix 139 return tuple( 140 x if not is_half_equilibrium[i] and tau[:, i].any() else None 141 for i, x in enumerate(np.argmax(tau, axis=0)) 142 )
Return the indices of transition states for each reaction.
Parameters
A, B : array-like is_half_equilibrium : sequence
Returns
sequence
Examples
>>> scheme = parse_reactions("A -> B")
>>> print(scheme)
Scheme(compounds=('A', 'B'),
reactions=('A -> B',),
is_half_equilibrium=(False,),
A=((-1.,), (1.,)),
B=((-1.,), (1.,)))
>>> get_transition_states(scheme.A, scheme.B, scheme.is_half_equilibrium)
(None,)
>>> scheme = parse_reactions("S -> E‡ -> S")
>>> print(scheme)
Scheme(compounds=('S', 'E‡'),
reactions=('S -> S',),
is_half_equilibrium=(False,),
A=((0.,), (0.,)),
B=((-1.,), (1.,)))
>>> get_transition_states(scheme.A, scheme.B, scheme.is_half_equilibrium)
(1,)
>>> scheme = parse_reactions("E + S <=> ES -> ES‡ -> E + P")
>>> print(scheme)
Scheme(compounds=('E', 'S', 'ES', 'ES‡', 'P'),
reactions=('E + S -> ES', 'ES -> E + S', 'ES -> E + P'),
is_half_equilibrium=(True, True, False),
A=((-1., 1., 1.),
(-1., 1., 0.),
(1., -1., -1.),
(0., 0., 0.),
(0., 0., 1.)),
B=((-1., 0., 0.),
(-1., 0., 0.),
(1., 0., -1.),
(0., 0., 1.),
(0., 0., 0.)))
>>> get_transition_states(scheme.A, scheme.B, scheme.is_half_equilibrium)
(None, None, 3)
58def get_y( 59 dydt, 60 y0, 61 t_span=None, 62 method="RK23", 63 max_step=np.inf, 64 first_step=np.finfo(np.float64).eps, 65 rtol=1e-3, 66 atol=1e-6, 67 max_time=1 * 60 * 60, 68): 69 """Simulate a reaction scheme from its rate function. 70 71 This function provides two functions that calculate the concentrations and 72 the rates of formation at any point in time for any compound. It does that 73 by solving an initial value problem (IVP) through scipy's ``solve_ivp`` 74 under the hood. 75 76 Parameters 77 ---------- 78 dydt : callable 79 Right-hand side of the system. 80 y0 : array-like 81 Initial state. 82 t_span : array-like, optional 83 Interval of integration (t0, tf). The solver starts with t=t0 and 84 integrates until it reaches t=tf. If not given, a conservative value 85 is chosen based on the system at hand (the method of choice works for 86 any zeroth-, first- or second-order reactions). 87 method : str, optional 88 Integration method to use. See `scipy.integrate.solve_ivp` for details. 89 Kinetics problems are very often stiff and, as such, "RK23" and "RK45" may be 90 unsuited. "LSODA", "BDF", and "Radau" are worth a try if things go bad. 91 max_step : float, optional 92 Maximum step to be performed by the integrator. 93 Defaults to half the total time span. 94 first_step : float, optional 95 First step size. 96 Defaults to half the maximum step, or `np.finfo(np.float64).eps`, 97 whichever is smallest. 98 rtol, atol : array-like, optional 99 See `scipy.integrate.solve_ivp` for details. 100 max_time : float, optional 101 If `t_span` is not given, an interval will be estimated, but it can't 102 be larger than this parameter. 103 104 Returns 105 ------- 106 y, r : callable 107 Concentrations and reaction rates as functions of time. The y object 108 is an OdeSolution and stores attributes t_min and t_max. 109 110 111 Examples 112 -------- 113 >>> import numpy as np 114 >>> import overreact as rx 115 116 A toy simulation can be performed in just two lines: 117 118 >>> scheme = rx.parse_reactions("A <=> B") 119 >>> y, r = get_y(get_dydt(scheme, np.array([1, 1])), y0=[1, 0]) 120 121 The `y` object stores information about the simulation time, which can be 122 used to produce a suitable vector of timepoints for, e.g., plotting: 123 124 >>> y.t_min, y.t_max 125 (0.0, 3.0) 126 >>> t = np.linspace(y.t_min, y.t_max) 127 >>> t 128 array([0. , 0.06122449, ..., 2.93877551, 3. ]) 129 130 Both `y` and `r` can be used to check concentrations and rates in any 131 point in time. In particular, both are vectorized: 132 133 >>> y(t) 134 array([[1. , ...], 135 [0. , ...]]) 136 >>> r(t) 137 array([[-1. , ...], 138 [ 1. , ...]]) 139 """ 140 # TODO(schneiderfelipe): raise a meaningful error when y0 has the wrong shape. 141 y0 = np.asarray(y0) 142 143 if t_span is None: 144 # We defined alpha such that 1.0 - alpha is an (under)estimate of the extend 145 # to which the reaction is simulated. And then we apply the Pareto principle. 146 alpha = 0.2 147 n_halflives = np.ceil(-np.log(alpha) / np.log(2)) 148 149 halflife_estimate = 1.0 150 if hasattr(dydt, "k"): 151 halflife_estimate = np.max( 152 [ 153 np.max(y0) / 2.0, # zeroth-order halflife 154 np.log(2.0), # first-order halflife 155 1.0 / np.min(y0[np.nonzero(y0)]), # second-order halflife 156 ], 157 ) / np.min(dydt.k) 158 logger.info(f"largest halflife guess = {halflife_estimate} s") 159 160 t_span = [0.0, min(n_halflives * halflife_estimate, max_time)] 161 logger.info(f"simulation time span = {t_span} s") 162 163 max_step = np.min([max_step, (t_span[1] - t_span[0]) / 2.0]) 164 logger.warning(f"max step = {max_step} s") 165 166 first_step = np.min([first_step, max_step / 2.0]) 167 logger.warning(f"first step = {first_step} s") 168 169 jac = None 170 if hasattr(dydt, "jac"): 171 jac = dydt.jac # noqa: F841 172 173 logger.warning(f"@t = \x1b[94m{0:10.3f} \x1b[ms\x1b[K") 174 res = solve_ivp( 175 dydt, 176 t_span, 177 y0, 178 method=method, 179 dense_output=True, 180 max_step=max_step, 181 first_step=first_step, 182 rtol=rtol, 183 atol=atol, 184 # jac=jac, # noqa: ERA001 185 ) 186 logger.warning(res) 187 y = res.sol 188 189 def r(t): 190 # TODO(schneiderfelipe): this is probably not the best way to 191 # vectorize a function! 192 try: 193 return np.array([dydt(_t, _y) for _t, _y in zip(t, y(t).T)]).T 194 except TypeError: 195 return dydt(t, y(t)) 196 197 return y, r
Simulate a reaction scheme from its rate function.
This function provides two functions that calculate the concentrations and
the rates of formation at any point in time for any compound. It does that
by solving an initial value problem (IVP) through scipy's solve_ivp
under the hood.
Parameters
dydt : callable
Right-hand side of the system.
y0 : array-like
Initial state.
t_span : array-like, optional
Interval of integration (t0, tf). The solver starts with t=t0 and
integrates until it reaches t=tf. If not given, a conservative value
is chosen based on the system at hand (the method of choice works for
any zeroth-, first- or second-order reactions).
method : str, optional
Integration method to use. See scipy.integrate.solve_ivp
for details.
Kinetics problems are very often stiff and, as such, "RK23" and "RK45" may be
unsuited. "LSODA", "BDF", and "Radau" are worth a try if things go bad.
max_step : float, optional
Maximum step to be performed by the integrator.
Defaults to half the total time span.
first_step : float, optional
First step size.
Defaults to half the maximum step, or np.finfo(np.float64).eps
,
whichever is smallest.
rtol, atol : array-like, optional
See scipy.integrate.solve_ivp
for details.
max_time : float, optional
If t_span
is not given, an interval will be estimated, but it can't
be larger than this parameter.
Returns
y, r : callable Concentrations and reaction rates as functions of time. The y object is an OdeSolution and stores attributes t_min and t_max.
Examples
>>> import numpy as np
>>> import overreact as rx
A toy simulation can be performed in just two lines:
>>> scheme = rx.parse_reactions("A <=> B")
>>> y, r = get_y(get_dydt(scheme, np.array([1, 1])), y0=[1, 0])
The y
object stores information about the simulation time, which can be
used to produce a suitable vector of timepoints for, e.g., plotting:
>>> y.t_min, y.t_max
(0.0, 3.0)
>>> t = np.linspace(y.t_min, y.t_max)
>>> t
array([0. , 0.06122449, ..., 2.93877551, 3. ])
Both y
and r
can be used to check concentrations and rates in any
point in time. In particular, both are vectorized:
>>> y(t)
array([[1. , ...],
[0. , ...]])
>>> r(t)
array([[-1. , ...],
[ 1. , ...]])
351def is_transition_state(name): 352 """Check whether a name specifies a transition state. 353 354 Parameters 355 ---------- 356 name : str 357 358 Returns 359 ------- 360 bool 361 362 Examples 363 -------- 364 >>> is_transition_state("A#") 365 True 366 >>> is_transition_state("A‡") 367 True 368 >>> is_transition_state("pyrrole#") 369 True 370 >>> is_transition_state("pyrrole‡") 371 True 372 >>> is_transition_state("A") 373 False 374 >>> is_transition_state("A~") 375 False 376 >>> is_transition_state("pyrrole") 377 False 378 >>> is_transition_state("pyrrole~") 379 False 380 381 This function also works for names that specify environment: 382 383 >>> is_transition_state("A#(w)") 384 True 385 >>> is_transition_state("A‡(w)") 386 True 387 >>> is_transition_state("TS#(w)") 388 True 389 >>> is_transition_state("TS‡(w)") 390 True 391 >>> is_transition_state("A(w)") 392 False 393 >>> is_transition_state("A~(w)") 394 False 395 >>> is_transition_state("TS(w)") 396 False 397 >>> is_transition_state("TS~(w)") 398 False 399 """ 400 return any(marker in name for marker in {"‡", "#"})
Check whether a name specifies a transition state.
Parameters
name : str
Returns
bool
Examples
>>> is_transition_state("A#")
True
>>> is_transition_state("A‡")
True
>>> is_transition_state("pyrrole#")
True
>>> is_transition_state("pyrrole‡")
True
>>> is_transition_state("A")
False
>>> is_transition_state("A~")
False
>>> is_transition_state("pyrrole")
False
>>> is_transition_state("pyrrole~")
False
This function also works for names that specify environment:
>>> is_transition_state("A#(w)")
True
>>> is_transition_state("A‡(w)")
True
>>> is_transition_state("TS#(w)")
True
>>> is_transition_state("TS‡(w)")
True
>>> is_transition_state("A(w)")
False
>>> is_transition_state("A~(w)")
False
>>> is_transition_state("TS(w)")
False
>>> is_transition_state("TS~(w)")
False
443def parse_compounds(text, path=("",), select=None): 444 """Parse a set of compounds. 445 446 Parameters 447 ---------- 448 text : str, sequence of str or dict-like 449 Compound descriptions or sequence of lines of it. 450 path : sequence of str, optional 451 Paths for searching logfiles. 452 select : sequence of str, optional 453 If defined, only those compounds will be returned. 454 455 Returns 456 ------- 457 compounds : immutable dict-like 458 459 Raises 460 ------ 461 FileNotFoundError 462 If a logfile is not found. 463 464 Examples 465 -------- 466 >>> import overreact as rx 467 468 >>> compounds = rx.parse_compounds("S: data/ethane/B97-3c/staggered.out") 469 >>> compounds 470 {'S': {'logfile': 'data/ethane/B97-3c/staggered.out', 471 'energy': -209483812.77142256, 472 'mult': 1, 473 'atomnos': (6, 6, 1, 1, 1, 1, 1, 1), 474 ...}} 475 476 >>> compounds = rx.parse_compounds({"S": "data/ethane/B97-3c/staggered.out"}) 477 >>> compounds 478 {'S': {'logfile': 'data/ethane/B97-3c/staggered.out', 479 'energy': -209483812.77142256, 480 'mult': 1, 481 'atomnos': (6, 6, 1, 1, 1, 1, 1, 1), 482 ...}} 483 484 >>> compounds = rx.parse_compounds(["S: data/ethane/B97-3c/staggered.out", 485 ... "E‡: data/ethane/B97-3c/eclipsed.out"]) 486 >>> compounds 487 {'S': {'logfile': 'data/ethane/B97-3c/staggered.out', 488 'energy': -209483812.77142256, 489 'mult': 1, 490 'atomnos': (6, 6, 1, 1, 1, 1, 1, 1), 491 ...}, 492 'E‡': {'logfile': 'data/ethane/B97-3c/eclipsed.out', 493 'energy': -209472585.3539883, 494 'mult': 1, 495 'atomnos': (6, 6, 1, 1, 1, 1, 1, 1), 496 ...}} 497 498 >>> compounds = rx.parse_compounds('''S: data/ethane/B97-3c/staggered.out 499 ... E‡: data/ethane/B97-3c/eclipsed.out''') 500 >>> compounds 501 {'S': {'logfile': 'data/ethane/B97-3c/staggered.out', 502 'energy': -209483812.77142256, 503 'mult': 1, 504 'atomnos': (6, 6, 1, 1, 1, 1, 1, 1), 505 ...}, 506 'E‡': {'logfile': 'data/ethane/B97-3c/eclipsed.out', 507 'energy': -209472585.3539883, 508 'mult': 1, 509 'atomnos': (6, 6, 1, 1, 1, 1, 1, 1), 510 ...}} 511 512 >>> compounds = rx.parse_compounds({"S": "data/ethane/B97-3c/staggered.out", 513 ... "E‡": "data/ethane/B97-3c/eclipsed.out"}) 514 >>> compounds 515 {'S': {'logfile': 'data/ethane/B97-3c/staggered.out', 516 'energy': -209483812.77142256, 517 'mult': 1, 518 'atomnos': (6, 6, 1, 1, 1, 1, 1, 1), 519 ...}, 520 'E‡': {'logfile': 'data/ethane/B97-3c/eclipsed.out', 521 'energy': -209472585.3539883, 522 'mult': 1, 523 'atomnos': (6, 6, 1, 1, 1, 1, 1, 1), 524 ...}} 525 """ 526 if isinstance(text, dict): 527 return _check_compounds(text) 528 try: 529 lines = text.split("\n") 530 except AttributeError: 531 lines = text 532 name = None 533 compounds = defaultdict(dict) 534 for line in lines: 535 if ":" in line: 536 name, line = (x.strip() for x in line.split(":", 1)) 537 if not line: 538 continue 539 540 if name is not None: 541 if "=" in line: 542 key, value = (x.strip() for x in line.split("=", 1)) 543 else: 544 key, value = "logfile", line 545 546 if key == "logfile": 547 success = False 548 value = value.strip('"') 549 for p in path: 550 try: 551 # TODO(schneiderfelipe): move on to use pathlib. 552 logger.info( 553 f"trying to read {os.path.join(p, value)}", 554 ) 555 compounds[name].update( 556 read_logfile(os.path.join(p, value)), 557 ) 558 except FileNotFoundError: 559 continue 560 success = True 561 break 562 if not success: 563 msg = f"could not find logfile '{value}' in path: {path}" 564 raise FileNotFoundError(msg) 565 else: 566 # one-line JSON-encoded object 567 compounds[name][key] = json.loads(value) 568 if select is not None: 569 # TODO(schneiderfelipe): this workaround still allow unused compounds 570 # to be parsed! This should change in the future. 571 compounds = {name: compounds[name] for name in select} 572 573 # Apply `extra_energy_term`s 574 for name in compounds: 575 if "extra_energy_term" in compounds[name]: 576 # TODO(schneiderfelipe): this assumes that 1. there's a single `extra_energy_term` and 2. `energy` is present 577 compounds[name]["energy"] += compounds[name]["extra_energy_term"] 578 579 return DotDict(compounds)
Parse a set of compounds.
Parameters
text : str, sequence of str or dict-like Compound descriptions or sequence of lines of it. path : sequence of str, optional Paths for searching logfiles. select : sequence of str, optional If defined, only those compounds will be returned.
Returns
compounds : immutable dict-like
Raises
FileNotFoundError If a logfile is not found.
Examples
>>> import overreact as rx
>>> compounds = rx.parse_compounds("S: data/ethane/B97-3c/staggered.out")
>>> compounds
{'S': {'logfile': 'data/ethane/B97-3c/staggered.out',
'energy': -209483812.77142256,
'mult': 1,
'atomnos': (6, 6, 1, 1, 1, 1, 1, 1),
...}}
>>> compounds = rx.parse_compounds({"S": "data/ethane/B97-3c/staggered.out"})
>>> compounds
{'S': {'logfile': 'data/ethane/B97-3c/staggered.out',
'energy': -209483812.77142256,
'mult': 1,
'atomnos': (6, 6, 1, 1, 1, 1, 1, 1),
...}}
>>> compounds = rx.parse_compounds(["S: data/ethane/B97-3c/staggered.out",
... "E‡: data/ethane/B97-3c/eclipsed.out"])
>>> compounds
{'S': {'logfile': 'data/ethane/B97-3c/staggered.out',
'energy': -209483812.77142256,
'mult': 1,
'atomnos': (6, 6, 1, 1, 1, 1, 1, 1),
...},
'E‡': {'logfile': 'data/ethane/B97-3c/eclipsed.out',
'energy': -209472585.3539883,
'mult': 1,
'atomnos': (6, 6, 1, 1, 1, 1, 1, 1),
...}}
>>> compounds = rx.parse_compounds('''S: data/ethane/B97-3c/staggered.out
... E‡: data/ethane/B97-3c/eclipsed.out''')
>>> compounds
{'S': {'logfile': 'data/ethane/B97-3c/staggered.out',
'energy': -209483812.77142256,
'mult': 1,
'atomnos': (6, 6, 1, 1, 1, 1, 1, 1),
...},
'E‡': {'logfile': 'data/ethane/B97-3c/eclipsed.out',
'energy': -209472585.3539883,
'mult': 1,
'atomnos': (6, 6, 1, 1, 1, 1, 1, 1),
...}}
>>> compounds = rx.parse_compounds({"S": "data/ethane/B97-3c/staggered.out",
... "E‡": "data/ethane/B97-3c/eclipsed.out"})
>>> compounds
{'S': {'logfile': 'data/ethane/B97-3c/staggered.out',
'energy': -209483812.77142256,
'mult': 1,
'atomnos': (6, 6, 1, 1, 1, 1, 1, 1),
...},
'E‡': {'logfile': 'data/ethane/B97-3c/eclipsed.out',
'energy': -209472585.3539883,
'mult': 1,
'atomnos': (6, 6, 1, 1, 1, 1, 1, 1),
...}}
30def parse_model(path: str, force_compile: bool = False): 31 """Parse either a source or model input file, whichever is available. 32 33 A **source input file** (also known as a `.k` file) contains all the information needed 34 to *create a model input file*. 35 A **model input file** (also known as a `.jk` file) is a JSON encoded file with all the 36 information needed to study microkinetic simulations from first principles. 37 38 You probably won't need to use model input files directly, they are 39 automatically created based on source input files. 40 [**Take a look at our guide on how to write an source input file**](https://geem-lab.github.io/overreact-guide/input.html). 41 42 This function attempts to parse a model input file if available. If not, a source 43 input file is parsed and a model input file is generated from it. Extensions are 44 guessed if none given (i.e., if only the base name given). 45 46 Parameters 47 ---------- 48 path : str 49 Path to the model or source input file. 50 If the final extension is not `.jk` or `.k`, it is guessed. 51 force_compile : bool 52 If True, a `.k` file will take precedence over any `.jk` file for reading. A 53 `.jk` file is thus either generated or overwritten. This is sometimes 54 needed to force an update with new data. 55 56 Returns 57 ------- 58 model : immutable dict-like 59 60 Raises 61 ------ 62 FileNotFoundError 63 If the model or source input file is not found. 64 65 Examples 66 -------- 67 Some examples of how overreact "sees" your data below 😄: 68 69 >>> model = parse_model("data/ethane/B97-3c/model.jk") 70 >>> model.scheme 71 Scheme(compounds=('S', 'E‡'), 72 reactions=('S -> S',), 73 is_half_equilibrium=(False,), 74 A=((0.0,), (0.0,)), 75 B=((-1.0,), (1.0,))) 76 >>> model.compounds["S"] 77 {'logfile': 'data/ethane/B97-3c/staggered.out', 78 'energy': -209483812.77142256, 79 'mult': 1, 80 'atomnos': (6, 6, 1, 1, 1, 1, 1, 1), 81 'atommasses': (12.011, 12.011, 1.008, 1.008, 1.008, 1.008, 1.008, 1.008), 82 'atomcoords': ((-7.633588, 2.520693, -4.8e-05), 83 ..., 84 (-5.832852, 3.674431, 0.363239)), 85 'vibfreqs': (307.57, 825.42, ..., 3071.11, 3071.45), 86 'vibdisps': (((-1.7e-05, 3.4e-05, 5.4e-05), 87 ..., 88 (-0.011061, -0.030431, -0.027036)))} 89 >>> model_from_source = parse_model("data/ethane/B97-3c/model.k", 90 ... force_compile=True) 91 >>> model_from_source == model 92 True 93 >>> model_from_source = parse_model("data/ethane/B97-3c/model") 94 >>> model_from_source == model 95 True 96 """ 97 if not path.endswith((".k", ".jk")): 98 path = f"{path}.jk" 99 logger.warning(f"assuming `.jk` file in {path}") 100 name, _ = os.path.splitext(path) 101 102 path_jk = f"{name}.jk" 103 if not force_compile and os.path.isfile(path_jk): 104 logger.info(f"parsing `.jk` file in {path_jk}") 105 return _parse_model(path_jk) 106 107 path_k = f"{name}.k" 108 logger.info(f"parsing `.k` file in {path_k}") 109 if not os.path.isfile(path_k): 110 # TODO(schneiderfelipe): add a nice error message here and everywhere? 111 msg = f"no `.k` file found in {path_k}" 112 raise FileNotFoundError(msg) 113 114 model = _parse_source(path_k) 115 with open(path_jk, "w") as f: 116 logger.info(f"writing `.jk` file in {path_jk}") 117 f.write(_unparse_model(model)) 118 119 return model
Parse either a source or model input file, whichever is available.
A source input file (also known as a .k
file) contains all the information needed
to create a model input file.
A model input file (also known as a .jk
file) is a JSON encoded file with all the
information needed to study microkinetic simulations from first principles.
You probably won't need to use model input files directly, they are automatically created based on source input files. Take a look at our guide on how to write an source input file.
This function attempts to parse a model input file if available. If not, a source input file is parsed and a model input file is generated from it. Extensions are guessed if none given (i.e., if only the base name given).
Parameters
path : str
Path to the model or source input file.
If the final extension is not .jk
or .k
, it is guessed.
force_compile : bool
If True, a .k
file will take precedence over any .jk
file for reading. A
.jk
file is thus either generated or overwritten. This is sometimes
needed to force an update with new data.
Returns
model : immutable dict-like
Raises
FileNotFoundError If the model or source input file is not found.
Examples
Some examples of how overreact "sees" your data below 😄:
>>> model = parse_model("data/ethane/B97-3c/model.jk")
>>> model.scheme
Scheme(compounds=('S', 'E‡'),
reactions=('S -> S',),
is_half_equilibrium=(False,),
A=((0.0,), (0.0,)),
B=((-1.0,), (1.0,)))
>>> model.compounds["S"]
{'logfile': 'data/ethane/B97-3c/staggered.out',
'energy': -209483812.77142256,
'mult': 1,
'atomnos': (6, 6, 1, 1, 1, 1, 1, 1),
'atommasses': (12.011, 12.011, 1.008, 1.008, 1.008, 1.008, 1.008, 1.008),
'atomcoords': ((-7.633588, 2.520693, -4.8e-05),
...,
(-5.832852, 3.674431, 0.363239)),
'vibfreqs': (307.57, 825.42, ..., 3071.11, 3071.45),
'vibdisps': (((-1.7e-05, 3.4e-05, 5.4e-05),
...,
(-0.011061, -0.030431, -0.027036)))}
>>> model_from_source = parse_model("data/ethane/B97-3c/model.k",
... force_compile=True)
>>> model_from_source == model
True
>>> model_from_source = parse_model("data/ethane/B97-3c/model")
>>> model_from_source == model
True
403def parse_reactions(text: str | list[str]) -> Scheme: 404 """ 405 Parse a kinetic model as a chemical reaction scheme. 406 407 This is an essential part of the parsing process. 408 See `overreact.io.parse_model` other details. 409 410 Parameters 411 ---------- 412 text : str or sequence of str 413 Model description or sequence of lines of it. 414 415 Returns 416 ------- 417 scheme : Scheme 418 A descriptor of the reaction scheme. 419 420 Notes 421 ----- 422 The model description should comply with the mini-language for systems of 423 reactions. A semi-formal definition of the grammar in 424 [Backus-Naur form](https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form) 425 is given below: 426 427 equation ::= equation_side arrow equation_side 428 equation_side ::= coefficient compound ['+' coefficient compound]* 429 coefficient ::= [integers] (defaults to 1) 430 compound ::= mix of printable characters 431 arrow ::= '->' | '<=>' | '<-' 432 433 Blank lines and comments (starting with `//`) are ignored. Repeated 434 reactions are ignored. Furthermore, reactions can be chained one after 435 another and, if a single compound (with either a `‡` or a `#` at the end) 436 appears alone on one side of a reaction, it's considered a transition 437 state. Transition states have zero lifetime during the simulation. 438 439 Examples 440 -------- 441 What follows is a rather long tour over the parsing process and its 442 output in general. You can skip it if you are not interested in the 443 details. 444 445 >>> scheme = parse_reactions("A -> B // a direct reaction") 446 447 The reaction above is a direct one (observe that comments are ignored). The 448 returned object has the following attributes: 449 450 >>> scheme.compounds 451 ('A', 'B') 452 >>> scheme.reactions 453 ('A -> B',) 454 >>> scheme.is_half_equilibrium 455 (False,) 456 >>> scheme.A 457 ((-1.,), (1.,)) 458 >>> scheme.B 459 ((-1.,), (1.,)) 460 461 The same reaction can be specified in reverse order: 462 463 >>> parse_reactions("B <- A // reverse reaction of the above") 464 Scheme(compounds=('A', 'B'), 465 reactions=('A -> B',), 466 is_half_equilibrium=(False,), 467 A=((-1.,), (1.,)), 468 B=((-1.,), (1.,))) 469 470 Equilibria produce twice as many direct reactions, while the $B$ matrix 471 defines an energy relationship for only one of each pair: 472 473 >>> parse_reactions("A <=> B // an equilibrium") 474 Scheme(compounds=('A', 'B'), 475 reactions=('A -> B', 'B -> A'), 476 is_half_equilibrium=(True, True), 477 A=((-1., 1.), 478 (1., -1.)), 479 B=((-1., 0.), 480 (1., 0.))) 481 482 Adding twice the same reaction results in a single reaction being added. 483 This of course also works with equilibria (extra whitespaces are ignored): 484 485 >>> parse_reactions(''' 486 ... A <=> B -> A 487 ... A -> B <=> A 488 ... A -> B <- A 489 ... B <- A -> B 490 ... ''') 491 Scheme(compounds=('A', 'B'), 492 reactions=('A -> B', 'B -> A'), 493 is_half_equilibrium=(True, True), 494 A=((-1., 1.), 495 (1., -1.)), 496 B=((-1., 0.), 497 (1., 0.))) 498 499 Transition states are specified with a special symbol at the end (either 500 `‡` or `#`). They are shown among compounds, but the matrix $A$ ensures 501 they'll never have a non-zero rate of formation/consumption. On the other 502 hand, they are needed in the $B$ matrix: 503 504 >>> parse_reactions("A -> A‡ -> B") 505 Scheme(compounds=('A', 'A‡', 'B'), 506 reactions=('A -> B',), 507 is_half_equilibrium=(False,), 508 A=((-1.,), (0.,), (1.,)), 509 B=((-1.,), (1.,), (0.,))) 510 511 This gives the same result as above: 512 513 >>> parse_reactions("A -> A‡ -> B <- A‡ <- A") 514 Scheme(compounds=('A', 'A‡', 'B'), 515 reactions=('A -> B',), 516 is_half_equilibrium=(False,), 517 A=((-1.,), (0.,), (1.,)), 518 B=((-1.,), (1.,), (0.,))) 519 520 It is possible to define a reaction whose product is the same as the 521 reactant. This is found in isomerization processes (e.g., ammonia 522 inversion or the methyl rotation in ethane): 523 524 >>> parse_reactions("S -> E‡ -> S") 525 Scheme(compounds=('S', 'E‡'), 526 reactions=('S -> S',), 527 is_half_equilibrium=(False,), 528 A=((0.,), (0.,)), 529 B=((-1.,), (1.,))) 530 531 As such, a column full of zeros in the $A$ matrix corresponds to a reaction 532 with zero net change. As can be seen, overreact allows for very general 533 models. An interesting feature is that a single transition state can link 534 many different compounds (whether it is useful is a matter of debate): 535 536 >>> parse_reactions(''' 537 ... B -> B‡ -> C // chained reactions and transition states 538 ... B‡ -> D // this is a bifurcation 539 ... B -> B'‡ -> E // this is a classical competitive reaction 540 ... A -> B‡ 541 ... ''') 542 Scheme(compounds=('B', 'B‡', 'C', 'D', "B'‡", 'E', 'A'), 543 reactions=('B -> C', 'B -> D', 'B -> E', 'A -> C', 'A -> D'), 544 is_half_equilibrium=(False, False, False, False, False), 545 A=((-1., -1., -1., 0., 0.), 546 (0., 0., 0., 0., 0.), 547 (1., 0., 0., 1., 0.), 548 (0., 1., 0., 0., 1.), 549 (0., 0., 0., 0., 0.), 550 (0., 0., 1., 0., 0.), 551 (0., 0., 0., -1., -1.)), 552 B=((-1., -1., -1., 0., 0.), 553 (1., 1., 0., 1., 1.), 554 (0., 0., 0., 0., 0.), 555 (0., 0., 0., 0., 0.), 556 (0., 0., 1., 0., 0.), 557 (0., 0., 0., 0., 0.), 558 (0., 0., 0., -1., -1.))) 559 560 The following is a borderline case but both reactions should be considered 561 different since they define different processes: 562 563 >>> parse_reactions(''' 564 ... A -> A‡ -> B 565 ... A -> B 566 ... ''') 567 Scheme(compounds=('A', 'A‡', 'B'), 568 reactions=('A -> B', 'A -> B'), 569 is_half_equilibrium=(False, False), 570 A=((-1., -1.), 571 (0., 0.), 572 (1., 1.)), 573 B=((-1., -1.), 574 (1., 0.), 575 (0., 1.))) 576 577 The following is correct behavior. In fact, the reactions are badly 578 defined: if more than one transition state are chained, the following 579 happens, which is correct since it's the most physically plausible model 580 that can be extracted. It can be seen as a feature that the product B is 581 ignored and not the reactant A, since the user would easily see the mistake 582 in graphs of concentration over time (the alternative would be no 583 reaction happening at all, which is rather cryptic to debug). 584 585 >>> parse_reactions("A -> A‡ -> A'‡ -> B") 586 Scheme(compounds=('A', 'A‡', "A'‡", 'B'), 587 reactions=("A -> A'‡",), 588 is_half_equilibrium=(False,), 589 A=((-1.,), (0.,), (1.,), (0.,)), 590 B=((-1.,), (1.,), (0.,), (0.,))) 591 592 In any case, it's not clear how a reaction barrier be defined in such a 593 case. If you have a use case, don't hesitate to 594 [open an issue](https://github.com/geem-lab/overreact/issues/), we'll be 595 happy to hear from you. 596 """ 597 compounds: dict[str, int] = {} 598 reactions: dict[ 599 tuple[str, str, bool, str], 600 tuple[tuple[tuple[int, str], ...], bool], 601 ] = {} 602 A = [] # coefficients between reactants and products 603 B = [] # coefficients between reactants and transition states 604 605 def _add_reaction(reactants, products, is_half_equilibrium, transition): 606 """Local helper function with side-effects.""" 607 # TODO(schneiderfelipe): what if reaction is defined, then redefined 608 # as equilibrium, or vice-versa? 609 if ( 610 transition is not None 611 and (reactants, products, is_half_equilibrium, transition) in reactions 612 ): 613 return 614 615 # found new reaction 616 reactions[(reactants, products, is_half_equilibrium, transition)] = None 617 618 A_vector = np.zeros(len(compounds)) 619 for coefficient, reactant in reactants: 620 A_vector[compounds[reactant]] = -coefficient 621 B_vector = A_vector 622 623 if transition is not None: 624 B_vector = A_vector.copy() 625 626 # it's assumed that 627 # 1. there's a singe transition compound, and 628 # 2. its coefficient equals one 629 B_vector[compounds[transition[-1][-1]]] = 1 630 631 for coefficient, product in products: 632 A_vector[compounds[product]] += coefficient 633 634 if ( 635 is_half_equilibrium 636 and (products, reactants, is_half_equilibrium, transition) in reactions 637 ): 638 B_vector = np.zeros(len(compounds)) 639 640 A.append(A_vector) 641 B.append(B_vector) 642 643 after_transitions: dict[tuple[tuple[int, str], ...], list[tuple[int, str]]] = {} 644 before_transitions: dict[tuple[tuple[int, str], ...], list[tuple[int, str]]] = {} 645 646 for reactants, products, is_half_equilibrium in _parse_reactions(text): 647 if (reactants, products, False, None) in reactions or ( 648 reactants, 649 products, 650 True, 651 None, 652 ) in reactions: 653 continue 654 655 for _, compound in itertools.chain(reactants, products): 656 if compound not in compounds: 657 # found new compound 658 compounds[compound] = len(compounds) 659 660 # TODO(schneiderfelipe): what if a transition state is used in an 661 # equilibrium? 662 663 # it's assumed that if a transition shows up, 664 # 1. it's the only compound in its side of the reaction, and 665 # 2. its coefficient equals one 666 if is_transition_state(reactants[-1][-1]): 667 for before_reactants in before_transitions.get(reactants, []): 668 _add_reaction( 669 before_reactants, 670 products, 671 is_half_equilibrium, 672 reactants, 673 ) 674 675 if reactants in after_transitions: 676 after_transitions[reactants].append(products) 677 else: 678 after_transitions[reactants] = [products] 679 continue 680 elif is_transition_state(products[-1][-1]): 681 for after_products in after_transitions.get(products, []): 682 _add_reaction(reactants, after_products, is_half_equilibrium, products) 683 684 if products in before_transitions: 685 before_transitions[products].append(reactants) 686 else: 687 before_transitions[products] = [reactants] 688 continue 689 690 _add_reaction(reactants, products, is_half_equilibrium, None) 691 692 return Scheme( 693 compounds=tuple(compounds), 694 reactions=tuple(_unparse_reactions(reactions)), 695 is_half_equilibrium=rx._misc.totuple( 696 [reaction[2] for reaction in reactions], 697 ), 698 A=rx._misc.totuple( 699 np.block( 700 [[vector, np.zeros(len(compounds) - len(vector))] for vector in A], 701 ).T, 702 ), 703 B=rx._misc.totuple( 704 np.block( 705 [[vector, np.zeros(len(compounds) - len(vector))] for vector in B], 706 ).T, 707 ), 708 )
Parse a kinetic model as a chemical reaction scheme.
This is an essential part of the parsing process.
See parse_model
other details.
Parameters
text : str or sequence of str Model description or sequence of lines of it.
Returns
scheme : Scheme A descriptor of the reaction scheme.
Notes
The model description should comply with the mini-language for systems of reactions. A semi-formal definition of the grammar in Backus-Naur form is given below:
equation ::= equation_side arrow equation_side
equation_side ::= coefficient compound ['+' coefficient compound]*
coefficient ::= [integers] (defaults to 1)
compound ::= mix of printable characters
arrow ::= '->' | '<=>' | '<-'
Blank lines and comments (starting with //
) are ignored. Repeated
reactions are ignored. Furthermore, reactions can be chained one after
another and, if a single compound (with either a ‡
or a #
at the end)
appears alone on one side of a reaction, it's considered a transition
state. Transition states have zero lifetime during the simulation.
Examples
What follows is a rather long tour over the parsing process and its output in general. You can skip it if you are not interested in the details.
>>> scheme = parse_reactions("A -> B // a direct reaction")
The reaction above is a direct one (observe that comments are ignored). The returned object has the following attributes:
>>> scheme.compounds
('A', 'B')
>>> scheme.reactions
('A -> B',)
>>> scheme.is_half_equilibrium
(False,)
>>> scheme.A
((-1.,), (1.,))
>>> scheme.B
((-1.,), (1.,))
The same reaction can be specified in reverse order:
>>> parse_reactions("B <- A // reverse reaction of the above")
Scheme(compounds=('A', 'B'),
reactions=('A -> B',),
is_half_equilibrium=(False,),
A=((-1.,), (1.,)),
B=((-1.,), (1.,)))
Equilibria produce twice as many direct reactions, while the $B$ matrix defines an energy relationship for only one of each pair:
>>> parse_reactions("A <=> B // an equilibrium")
Scheme(compounds=('A', 'B'),
reactions=('A -> B', 'B -> A'),
is_half_equilibrium=(True, True),
A=((-1., 1.),
(1., -1.)),
B=((-1., 0.),
(1., 0.)))
Adding twice the same reaction results in a single reaction being added. This of course also works with equilibria (extra whitespaces are ignored):
>>> parse_reactions('''
... A <=> B -> A
... A -> B <=> A
... A -> B <- A
... B <- A -> B
... ''')
Scheme(compounds=('A', 'B'),
reactions=('A -> B', 'B -> A'),
is_half_equilibrium=(True, True),
A=((-1., 1.),
(1., -1.)),
B=((-1., 0.),
(1., 0.)))
Transition states are specified with a special symbol at the end (either
‡
or #
). They are shown among compounds, but the matrix $A$ ensures
they'll never have a non-zero rate of formation/consumption. On the other
hand, they are needed in the $B$ matrix:
>>> parse_reactions("A -> A‡ -> B")
Scheme(compounds=('A', 'A‡', 'B'),
reactions=('A -> B',),
is_half_equilibrium=(False,),
A=((-1.,), (0.,), (1.,)),
B=((-1.,), (1.,), (0.,)))
This gives the same result as above:
>>> parse_reactions("A -> A‡ -> B <- A‡ <- A")
Scheme(compounds=('A', 'A‡', 'B'),
reactions=('A -> B',),
is_half_equilibrium=(False,),
A=((-1.,), (0.,), (1.,)),
B=((-1.,), (1.,), (0.,)))
It is possible to define a reaction whose product is the same as the reactant. This is found in isomerization processes (e.g., ammonia inversion or the methyl rotation in ethane):
>>> parse_reactions("S -> E‡ -> S")
Scheme(compounds=('S', 'E‡'),
reactions=('S -> S',),
is_half_equilibrium=(False,),
A=((0.,), (0.,)),
B=((-1.,), (1.,)))
As such, a column full of zeros in the $A$ matrix corresponds to a reaction with zero net change. As can be seen, overreact allows for very general models. An interesting feature is that a single transition state can link many different compounds (whether it is useful is a matter of debate):
>>> parse_reactions('''
... B -> B‡ -> C // chained reactions and transition states
... B‡ -> D // this is a bifurcation
... B -> B'‡ -> E // this is a classical competitive reaction
... A -> B‡
... ''')
Scheme(compounds=('B', 'B‡', 'C', 'D', "B'‡", 'E', 'A'),
reactions=('B -> C', 'B -> D', 'B -> E', 'A -> C', 'A -> D'),
is_half_equilibrium=(False, False, False, False, False),
A=((-1., -1., -1., 0., 0.),
(0., 0., 0., 0., 0.),
(1., 0., 0., 1., 0.),
(0., 1., 0., 0., 1.),
(0., 0., 0., 0., 0.),
(0., 0., 1., 0., 0.),
(0., 0., 0., -1., -1.)),
B=((-1., -1., -1., 0., 0.),
(1., 1., 0., 1., 1.),
(0., 0., 0., 0., 0.),
(0., 0., 0., 0., 0.),
(0., 0., 1., 0., 0.),
(0., 0., 0., 0., 0.),
(0., 0., 0., -1., -1.)))
The following is a borderline case but both reactions should be considered different since they define different processes:
>>> parse_reactions('''
... A -> A‡ -> B
... A -> B
... ''')
Scheme(compounds=('A', 'A‡', 'B'),
reactions=('A -> B', 'A -> B'),
is_half_equilibrium=(False, False),
A=((-1., -1.),
(0., 0.),
(1., 1.)),
B=((-1., -1.),
(1., 0.),
(0., 1.)))
The following is correct behavior. In fact, the reactions are badly defined: if more than one transition state are chained, the following happens, which is correct since it's the most physically plausible model that can be extracted. It can be seen as a feature that the product B is ignored and not the reactant A, since the user would easily see the mistake in graphs of concentration over time (the alternative would be no reaction happening at all, which is rather cryptic to debug).
>>> parse_reactions("A -> A‡ -> A'‡ -> B")
Scheme(compounds=('A', 'A‡', "A'‡", 'B'),
reactions=("A -> A'‡",),
is_half_equilibrium=(False,),
A=((-1.,), (0.,), (1.,), (0.,)),
B=((-1.,), (1.,), (0.,), (0.,)))
In any case, it's not clear how a reaction barrier be defined in such a case. If you have a use case, don't hesitate to open an issue, we'll be happy to hear from you.
147def unparse_reactions(scheme: Scheme) -> str: 148 """Unparse a kinetic model. 149 150 Parameters 151 ---------- 152 scheme : Scheme 153 A descriptor of the reaction scheme. 154 Mostly likely, this comes from a parsed input file. 155 See `overreact.io.parse_model`. 156 157 Returns 158 ------- 159 text : str 160 161 Notes 162 ----- 163 This function assumes complimentary half equilibria are located one after 164 the other in ``scheme.reactions``, which is to be expected from 165 `parse_reactions`. 166 167 Examples 168 -------- 169 >>> unparse_reactions(Scheme(compounds=('A', 'B'), reactions=('A -> B',), 170 ... is_half_equilibrium=(False,), 171 ... A=((-1.,), 172 ... ( 1.,)), 173 ... B=((-1.,), 174 ... ( 1.,)))) 175 'A -> B' 176 >>> unparse_reactions(Scheme(compounds=('A', 'B'), 177 ... reactions=('A -> B', 'B -> A'), 178 ... is_half_equilibrium=(True, True), 179 ... A=((-1., 1.), 180 ... ( 1., -1.)), 181 ... B=((-1., 0.), 182 ... ( 1., 0.)))) 183 'A <=> B' 184 >>> unparse_reactions(Scheme(compounds=('A', 'A‡', 'B'), 185 ... reactions=('A -> B',), 186 ... is_half_equilibrium=(False,), 187 ... A=((-1.,), 188 ... ( 0.,), 189 ... ( 1.,)), 190 ... B=((-1.,), 191 ... ( 1.,), 192 ... ( 0.,)))) 193 'A -> A‡ -> B' 194 >>> print(unparse_reactions(Scheme(compounds=('B', 'B‡', 'C', 'D', "B'‡", 195 ... 'E', 'A'), 196 ... reactions=('B -> C', 'B -> D', 'B -> E', 197 ... 'A -> C', 'A -> D'), 198 ... is_half_equilibrium=(False, False, False, 199 ... False, False), 200 ... A=((-1., -1., -1., 0., 0.), 201 ... ( 0., 0., 0., 0., 0.), 202 ... ( 1., 0., 0., 1., 0.), 203 ... ( 0., 1., 0., 0., 1.), 204 ... ( 0., 0., 0., 0., 0.), 205 ... ( 0., 0., 1., 0., 0.), 206 ... ( 0., 0., 0., -1., -1.)), 207 ... B=((-1., -1., -1., 0., 0.), 208 ... ( 1., 1., 0., 1., 1.), 209 ... ( 0., 0., 0., 0., 0.), 210 ... ( 0., 0., 0., 0., 0.), 211 ... ( 0., 0., 1., 0., 0.), 212 ... ( 0., 0., 0., 0., 0.), 213 ... ( 0., 0., 0., -1., -1.))))) 214 B -> B‡ -> C 215 B -> B‡ -> D 216 B -> B'‡ -> E 217 A -> B‡ -> C 218 A -> B‡ -> D 219 >>> print(unparse_reactions(Scheme(compounds=('A', 'A‡', 'B'), 220 ... reactions=('A -> B', 'A -> B'), 221 ... is_half_equilibrium=(False, False), 222 ... A=((-1., -1.), 223 ... ( 0., 0.), 224 ... ( 1., 1.)), 225 ... B=((-1., -1.), 226 ... ( 1., 0.), 227 ... ( 0., 1.))))) 228 A -> A‡ -> B 229 A -> B 230 >>> unparse_reactions(Scheme(compounds=('A', 'A‡', "A'‡", 'B'), 231 ... reactions=("A -> A'‡",), 232 ... is_half_equilibrium=(False,), 233 ... A=((-1.,), 234 ... ( 0.,), 235 ... ( 1.,), 236 ... ( 0.,)), 237 ... B=((-1.,), 238 ... ( 1.,), 239 ... ( 0.,), 240 ... ( 0.,)))) 241 "A -> A‡ -> A'‡" 242 >>> unparse_reactions(Scheme(compounds=('S', 'E‡'), 243 ... reactions=('S -> S',), 244 ... is_half_equilibrium=(False,), 245 ... A=((0.0,), 246 ... (0.0,)), 247 ... B=((-1.0,), 248 ... (1.0,)))) 249 'S -> E‡ -> S' 250 """ 251 scheme = _check_scheme(scheme) 252 transition_states = get_transition_states( 253 scheme.A, 254 scheme.B, 255 scheme.is_half_equilibrium, 256 ) 257 lines = [] 258 i = 0 259 while i < len(scheme.reactions): 260 if transition_states[i] is not None: 261 lines.append( 262 scheme.reactions[i].replace( 263 "->", 264 f"-> {scheme.compounds[transition_states[i]]} ->", 265 ), 266 ) 267 elif scheme.is_half_equilibrium[i]: 268 lines.append(scheme.reactions[i].replace("->", "<=>")) 269 i += 1 # avoid backward reaction, which comes next 270 else: 271 lines.append(scheme.reactions[i]) 272 i += 1 273 return "\n".join(lines)
Unparse a kinetic model.
Parameters
scheme : Scheme
A descriptor of the reaction scheme.
Mostly likely, this comes from a parsed input file.
See parse_model
.
Returns
text : str
Notes
This function assumes complimentary half equilibria are located one after
the other in scheme.reactions
, which is to be expected from
parse_reactions
.
Examples
>>> unparse_reactions(Scheme(compounds=('A', 'B'), reactions=('A -> B',),
... is_half_equilibrium=(False,),
... A=((-1.,),
... ( 1.,)),
... B=((-1.,),
... ( 1.,))))
'A -> B'
>>> unparse_reactions(Scheme(compounds=('A', 'B'),
... reactions=('A -> B', 'B -> A'),
... is_half_equilibrium=(True, True),
... A=((-1., 1.),
... ( 1., -1.)),
... B=((-1., 0.),
... ( 1., 0.))))
'A <=> B'
>>> unparse_reactions(Scheme(compounds=('A', 'A‡', 'B'),
... reactions=('A -> B',),
... is_half_equilibrium=(False,),
... A=((-1.,),
... ( 0.,),
... ( 1.,)),
... B=((-1.,),
... ( 1.,),
... ( 0.,))))
'A -> A‡ -> B'
>>> print(unparse_reactions(Scheme(compounds=('B', 'B‡', 'C', 'D', "B'‡",
... 'E', 'A'),
... reactions=('B -> C', 'B -> D', 'B -> E',
... 'A -> C', 'A -> D'),
... is_half_equilibrium=(False, False, False,
... False, False),
... A=((-1., -1., -1., 0., 0.),
... ( 0., 0., 0., 0., 0.),
... ( 1., 0., 0., 1., 0.),
... ( 0., 1., 0., 0., 1.),
... ( 0., 0., 0., 0., 0.),
... ( 0., 0., 1., 0., 0.),
... ( 0., 0., 0., -1., -1.)),
... B=((-1., -1., -1., 0., 0.),
... ( 1., 1., 0., 1., 1.),
... ( 0., 0., 0., 0., 0.),
... ( 0., 0., 0., 0., 0.),
... ( 0., 0., 1., 0., 0.),
... ( 0., 0., 0., 0., 0.),
... ( 0., 0., 0., -1., -1.)))))
B -> B‡ -> C
B -> B‡ -> D
B -> B'‡ -> E
A -> B‡ -> C
A -> B‡ -> D
>>> print(unparse_reactions(Scheme(compounds=('A', 'A‡', 'B'),
... reactions=('A -> B', 'A -> B'),
... is_half_equilibrium=(False, False),
... A=((-1., -1.),
... ( 0., 0.),
... ( 1., 1.)),
... B=((-1., -1.),
... ( 1., 0.),
... ( 0., 1.)))))
A -> A‡ -> B
A -> B
>>> unparse_reactions(Scheme(compounds=('A', 'A‡', "A'‡", 'B'),
... reactions=("A -> A'‡",),
... is_half_equilibrium=(False,),
... A=((-1.,),
... ( 0.,),
... ( 1.,),
... ( 0.,)),
... B=((-1.,),
... ( 1.,),
... ( 0.,),
... ( 0.,))))
"A -> A‡ -> A'‡"
>>> unparse_reactions(Scheme(compounds=('S', 'E‡'),
... reactions=('S -> S',),
... is_half_equilibrium=(False,),
... A=((0.0,),
... (0.0,)),
... B=((-1.0,),
... (1.0,))))
'S -> E‡ -> S'