Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
3045231
chore(deps-dev): add hypothesis
schneiderfelipe Oct 16, 2026
0169205
refactor(core)!: store reactants, products and transition states in S…
schneiderfelipe Oct 16, 2026
bfa7081
fix(simulate): use reactant coefficients as reaction orders
schneiderfelipe Oct 16, 2026
d68059d
fix(thermo)!: count reactant coefficients in molecularity
schneiderfelipe Oct 16, 2026
5cf87d8
fix(thermo)!: compute reaction entropies from initial and final states
schneiderfelipe Oct 17, 2026
24f752a
test(rates): add property tests for conversions and Eyring rates
schneiderfelipe Oct 17, 2026
39489d6
test(thermo): add property tests for deltas, equilibria, molar volume…
schneiderfelipe Oct 17, 2026
e9b1ce8
test(tunnel): add property tests for Eckart symmetry and Wigner corre…
schneiderfelipe Oct 17, 2026
94528b5
test(misc): check numerical derivatives against polynomials
schneiderfelipe Oct 17, 2026
c76a1d1
test(api): sample conditions when comparing calc_* with get_*
schneiderfelipe Oct 17, 2026
3591497
docs(tunnel): note NaN Eckart corrections for large barriers
schneiderfelipe Oct 17, 2026
6b3ac85
refactor(core)!: make Scheme operations methods
schneiderfelipe Oct 17, 2026
11fb7a5
feat: log symmetry entropies and fixed concentrations
schneiderfelipe Oct 17, 2026
9d9b06f
refactor(core): derive Scheme data lazily
schneiderfelipe Oct 17, 2026
3d76b21
refactor: avoid needless intermediate collections and copies
schneiderfelipe Oct 17, 2026
dbd893b
chore(release): bump version to 2.0.0
schneiderfelipe Oct 17, 2026
8535c31
refactor(core): cache Scheme.equilibria as a tuple
schneiderfelipe Oct 17, 2026
10c0392
refactor: inline single-use private helpers
schneiderfelipe Oct 17, 2026
2e45872
chore(io): note that _unparse_source has no callers
schneiderfelipe Oct 17, 2026
d007f7f
refactor: simplify Scheme internals, deltas reporting and logging
schneiderfelipe Oct 17, 2026
62a17e1
test: simplify strategies and share fixtures
schneiderfelipe Oct 17, 2026
1c32f44
fix(core): harden Scheme validation and fixing
schneiderfelipe Oct 17, 2026
96aad97
style(misc): keep the totuple source link above totuple
schneiderfelipe Oct 17, 2026
502fc88
refactor(core): pair equilibria explicitly and number compounds by re…
schneiderfelipe Oct 17, 2026
11c63fc
refactor(api): derive equilibria rate constants from equilibrium cons…
schneiderfelipe Oct 17, 2026
221bc4c
fix(simulate): keep reaction orders shaped for schemes without compounds
schneiderfelipe Oct 17, 2026
7504987
test: check against independent oracles instead of replicating logic
schneiderfelipe Oct 17, 2026
0700525
fix(api): reject equilibria whose reverse half does not undo the forw…
schneiderfelipe Oct 17, 2026
9ed2a90
fix(core): reject transition states that do not link reactants to pro…
schneiderfelipe Oct 17, 2026
b58e1c8
refactor: simplify Scheme storage, parsing and CLI deltas
schneiderfelipe Oct 17, 2026
676071b
test(api): bound pressure test examples and cancellation error
schneiderfelipe Oct 17, 2026
f6c490c
fix(core): reject misplaced transition states, repeated names and unk…
schneiderfelipe Oct 17, 2026
ca8ff01
fix(io): list compiled model compounds in the order of their scheme
schneiderfelipe Oct 17, 2026
f024526
fix(api): let invalid free energies through the equilibrium consisten…
schneiderfelipe Oct 17, 2026
d1b7355
test(regressions): drop a resolved TODO
schneiderfelipe Oct 17, 2026
ce2c947
test: tighten tolerances to the measured errors
schneiderfelipe Oct 18, 2026
8dfa347
test(simulate): check the Jacobian against finite differences
schneiderfelipe Oct 18, 2026
a17353d
perf(simulate): compile the Jacobian once with JIT
schneiderfelipe Oct 18, 2026
cfdcd04
refactor(api): take equilibrium rate constants from equilibrium const…
schneiderfelipe Oct 18, 2026
5b92477
feat(core): add reaction and activation free energies with symmetry e…
schneiderfelipe Oct 18, 2026
15a0578
refactor(core)!: store transition states apart from compounds
schneiderfelipe Oct 18, 2026
0ce9eef
fix(cli): reject starting concentrations of names that are not compounds
schneiderfelipe Oct 18, 2026
2f1e5c4
refactor: share the unknown-compound check and drop redundant work
schneiderfelipe Oct 18, 2026
3b16ecd
fix(cli): check plotted names and reject doubly given concentrations
schneiderfelipe Oct 18, 2026
8c4c0c1
fix(api): match compounds to scheme names in get_k and get_kappa
schneiderfelipe Oct 18, 2026
89db76e
fix(api): accept equilibrium mismatches within 1e-5 J/mol
schneiderfelipe Oct 18, 2026
7b0d46f
fix(cli): check the plotted name before simulating and reject specs w…
schneiderfelipe Oct 18, 2026
4db3a1e
chore(cli): note that get_bias ignores fixed-concentration factors
schneiderfelipe Oct 18, 2026
3e24f54
chore(core): note that reaction deltas could accept compound-only values
schneiderfelipe Oct 18, 2026
345f548
refactor(misc): restore the _is_prime helper for readability
schneiderfelipe Oct 18, 2026
0fe7249
refactor(core)!: make is_transition_state private
schneiderfelipe Oct 18, 2026
5324721
refactor(core)!: make Scheme.is_half_equilibrium private
schneiderfelipe Oct 18, 2026
39e99a0
refactor(core)!: make Scheme.A private
schneiderfelipe Oct 18, 2026
65b3bc5
refactor(io)!: make InterfaceFormatter private
schneiderfelipe Oct 18, 2026
d8d3c8c
fix(api): accept one temperature per reaction in get_k again
schneiderfelipe Oct 18, 2026
fedf235
feat(core)!: broadcast Scheme deltas over leading axes
schneiderfelipe Oct 18, 2026
ebc5235
feat(misc): format arrays of more than one dimension for logging
schneiderfelipe Oct 18, 2026
bb38d8b
fix(rates): broadcast conversion factors against molecularities
schneiderfelipe Oct 18, 2026
881f306
feat(api): broadcast energies, rate constants and tunneling over cond…
schneiderfelipe Oct 18, 2026
9ce752e
perf(simulate): evaluate rates of many states at once
schneiderfelipe Oct 19, 2026
9f6fbc2
refactor(core): inline the net coefficients into their only user
schneiderfelipe Oct 19, 2026
5d94e4e
perf(tunnel): vectorize the Eckart quadratures over their arguments
schneiderfelipe Oct 19, 2026
07b6783
refactor(api): vectorize Eckart tunneling over temperatures
schneiderfelipe Oct 19, 2026
3c6d3c0
docs(api): mark the environment reused across compounds in get_entropies
schneiderfelipe Oct 19, 2026
aac65d8
perf(constants): look van der Waals radii up as arrays
schneiderfelipe Oct 19, 2026
b9228d2
perf(misc): broaden spectra with a single broadcast pdf call
schneiderfelipe Oct 19, 2026
31cb4d3
perf(coords): query van der Waals spheres of all atoms at once
schneiderfelipe Oct 19, 2026
9c41d28
perf(coords): build rotational Eckart vectors with broadcast cross pr…
schneiderfelipe Oct 19, 2026
6e03625
refactor(io): list atomic masses with a comprehension
schneiderfelipe Oct 19, 2026
c0e747a
feat(core): add Scheme.concentrations to build concentrations by name
schneiderfelipe Oct 19, 2026
26a91e3
fix(core): broadcast Scheme.fix over arrays of concentrations
schneiderfelipe Oct 19, 2026
218a87d
docs(simulate): note vectorized solve_ivp calls for #817
schneiderfelipe Oct 19, 2026
bebdb01
feat(tunnel): accept arrays of barriers in eckart
schneiderfelipe Oct 19, 2026
11dd6fb
docs(thermo): document scalar temperatures and pressures where arrays…
schneiderfelipe Oct 19, 2026
48b36e8
refactor(tunnel): compute both Eckart alphas in one expression
schneiderfelipe Oct 19, 2026
5b04194
test(tunnel): merge Eckart broadcasting tests over temperatures and b…
schneiderfelipe Oct 19, 2026
387b6f5
fix(thermo)!: convert gas-phase free energies to 1 M in equilibrium_c…
schneiderfelipe Oct 19, 2026
2fb13d5
fix(thermo): import solvation models so they work right after importi…
schneiderfelipe Oct 19, 2026
e12a271
fix(api): take each compound's environment from its own name in get_e…
schneiderfelipe Oct 19, 2026
6267370
fix(api)!: give the same rate constants from compounds and from their…
schneiderfelipe Oct 19, 2026
81c027c
docs(api): state the standard states of entropies and free energies
schneiderfelipe Oct 19, 2026
1f2fef3
feat(api)!: rename get_kappa method to tunneling, and turn tunneling …
schneiderfelipe Oct 19, 2026
e79d878
feat(simulate)!: take initial concentrations by name in get_bias, and…
schneiderfelipe Oct 19, 2026
2b738d7
fixup! fix(api)!: give the same rate constants from compounds and fro…
schneiderfelipe Oct 19, 2026
8993e66
feat!: return floats for scalar inputs in eyring, equilibrium_constan…
schneiderfelipe Oct 19, 2026
92d29f2
fix(simulate): reject rate constants that are not one per reaction in…
schneiderfelipe Oct 19, 2026
af3fc8c
feat(simulate)!: rename get_dydt's ef to equilibrium_factor
schneiderfelipe Oct 19, 2026
2589025
fix(thermo): raise TypeError instead of asserting when solvation meth…
schneiderfelipe Oct 19, 2026
ef2580c
docs(simulate): note an implicit default solver for #817
schneiderfelipe Oct 19, 2026
a206b39
refactor!: keep only the workflow at the top level of overreact
schneiderfelipe Oct 19, 2026
6a160b6
fix(rates): convert rate constants through one table of units
schneiderfelipe Oct 19, 2026
b9e044d
fix(api): apply bias in get_k also when delta free energies are given
schneiderfelipe Oct 19, 2026
e8a487d
refactor(api): evaluate thermochemistry of compounds through one broa…
schneiderfelipe Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 2 additions & 17 deletions overreact/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,23 @@
get_k,
get_kappa,
)
from overreact.core import (
Scheme,
get_transition_states,
is_transition_state,
parse_reactions,
unparse_reactions,
)
from overreact.core import Scheme
from overreact.io import parse_compounds, parse_model
from overreact.simulate import get_bias, get_dydt, get_fixed_scheme, get_y
from overreact.thermo import change_reference_state, get_delta, get_reaction_entropies
from overreact.simulate import get_bias, get_dydt, get_y

__all__ = [
"Scheme",
"change_reference_state",
"get_bias",
"get_delta",
"get_dydt",
"get_enthalpies",
"get_entropies",
"get_fixed_scheme",
"get_freeenergies",
"get_internal_energies",
"get_k",
"get_kappa",
"get_reaction_entropies",
"get_transition_states",
"get_y",
"is_transition_state",
"parse_compounds",
"parse_model",
"parse_reactions",
"unparse_reactions",
]

__version__ = version(__name__)
Expand Down
192 changes: 100 additions & 92 deletions overreact/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import argparse
import logging
import os
import re
import shutil
import sys

Expand Down Expand Up @@ -204,16 +203,10 @@ def _yield_scheme(self):
show_header=False,
)
raw_table.add_column(justify="left")
for r in rx.core.unparse_reactions(scheme).split("\n"):
for r in str(scheme).splitlines():
raw_table.add_row(r)
yield Panel(raw_table, expand=False)

transition_states = rx.core.get_transition_states(
scheme.A,
scheme.B,
scheme.is_half_equilibrium,
)

parsed_table = Table(
Column("no", justify="right"),
Column("reactant(s)", justify="left"),
Expand All @@ -223,14 +216,22 @@ def _yield_scheme(self):
title="(parsed) reactions",
box=self.box_style,
)
for i, reaction in enumerate(scheme.reactions):
reactants, _, products = re.split(r"\s*(->|<=>|<-)\s*", reaction)
row = [f"{i:d}", reactants, None, products, "No"]
if transition_states[i] is not None:
row[2] = scheme.compounds[transition_states[i]]
elif scheme.is_half_equilibrium[i]:
row[4] = "Yes"
parsed_table.add_row(*row)
for i, (reaction, transition_state, is_half_equilibrium) in enumerate(
zip(
scheme.reactions,
scheme.transition_states,
scheme._is_half_equilibrium,
strict=True,
),
):
reactants, _, products = reaction.partition("->")
parsed_table.add_row(
f"{i:d}",
reactants.strip(),
transition_state,
products.strip(),
"Yes" if is_half_equilibrium else "No",
)
yield parsed_table

def _yield_compounds(self):
Expand Down Expand Up @@ -366,57 +367,57 @@ def _yield_thermochemistry(self):
)
yield compounds_table

delta_mass = rx.get_delta(scheme.A, molecular_masses)
delta_energies = rx.get_delta(scheme.A, energies)
delta_internal_energies = rx.get_delta(scheme.A, internal_energies)
delta_enthalpies = rx.get_delta(scheme.A, enthalpies)
# TODO(schneiderfelipe): log the contribution of reaction symmetry
delta_entropies = rx.get_delta(scheme.A, entropies) + rx.get_reaction_entropies(
scheme.A,
temperature=self.temperature,
pressure=self.pressure,
logger.info(
"reaction symmetry entropies: "
f"{rx._misc.format_numbers(scheme.reaction_entropies)} J·mol⁻¹·K⁻¹",
)
delta_freeenergies = delta_enthalpies - self.temperature * delta_entropies
assert np.allclose(
delta_freeenergies,
rx.get_delta(scheme.A, freeenergies)
- self.temperature
* rx.get_reaction_entropies(
scheme.A,
temperature=self.temperature,
pressure=self.pressure,
),
), (
"reaction free energies do not match reaction enthalpies and reaction entropies"
quantities = np.stack(
[
molecular_masses,
energies,
internal_energies,
enthalpies,
entropies,
],
)

delta_activation_mass = rx.get_delta(scheme.B, molecular_masses)
delta_activation_energies = rx.get_delta(scheme.B, energies)
delta_activation_internal_energies = rx.get_delta(scheme.B, internal_energies)
delta_activation_enthalpies = rx.get_delta(scheme.B, enthalpies)
# TODO(schneiderfelipe): log the contribution of reaction symmetry
delta_activation_entropies = rx.get_delta(
scheme.B,
entropies,
) + rx.get_reaction_entropies(
scheme.B,
temperature=self.temperature,
pressure=self.pressure,
)
delta_activation_freeenergies = (
delta_activation_enthalpies - self.temperature * delta_activation_entropies
def _deltas(deltas, symmetry_entropies, freeenergy_deltas):
"""Return deltas of quantities, with symmetry in entropies and free energies."""
mass, energies, internal_energies, enthalpies, entropies = deltas(
quantities
)
return (
mass,
energies,
internal_energies,
enthalpies,
entropies + symmetry_entropies,
freeenergy_deltas(freeenergies, self.temperature),
)

(
delta_mass,
delta_energies,
delta_internal_energies,
delta_enthalpies,
delta_entropies,
delta_freeenergies,
) = _deltas(
scheme.reaction_deltas,
scheme.reaction_entropies,
scheme.reaction_freeenergies,
)
assert np.allclose(
(
delta_activation_mass,
delta_activation_energies,
delta_activation_internal_energies,
delta_activation_enthalpies,
delta_activation_entropies,
delta_activation_freeenergies,
rx.get_delta(scheme.B, freeenergies)
- self.temperature
* rx.get_reaction_entropies(
scheme.B,
temperature=self.temperature,
pressure=self.pressure,
),
), (
"activation free energies do not match activation enthalpies and activation entropies"
) = _deltas(
scheme.activation_deltas,
scheme.activation_entropies,
scheme.activation_freeenergies,
)

circ_table = Table(
Expand Down Expand Up @@ -444,7 +445,7 @@ def _yield_thermochemistry(self):
box=self.box_style,
)
for i, reaction in enumerate(scheme.reactions):
if scheme.is_half_equilibrium[i]:
if scheme._is_half_equilibrium[i]:
circ_row = [
f"{i:d}",
reaction,
Expand Down Expand Up @@ -505,6 +506,7 @@ def _yield_kinetics(self):
data = np.genfromtxt(self.bias, names=True, delimiter=",")
data = {name: data[name] for name in data.dtype.names}

# TODO(schneiderfelipe): get_bias ignores the rate constant factors of fixed concentrations.
scheme, _, y0 = _prepare_simulation(
self.model.scheme,
rx.get_k(
Expand Down Expand Up @@ -571,7 +573,7 @@ def _yield_kinetics(self):
kappa = rx.get_kappa(
self.model.scheme,
self.model.compounds,
method=self.tunneling,
tunneling=self.tunneling,
qrrho=self.qrrho,
temperature=self.temperature,
)
Expand All @@ -595,7 +597,7 @@ def _yield_kinetics(self):
+ [f"{k[scale][i]:.3g}" for scale in k]
+ [f"{kappa[i]:.3g}"]
)
if self.model.scheme.is_half_equilibrium[i]:
if self.model.scheme._is_half_equilibrium[i]:
row[2] = "Yes"
row[-1] = None # hide transmission coefficient

Expand All @@ -615,14 +617,16 @@ def _yield_kinetics(self):
k["M⁻ⁿ⁺¹·s⁻¹"],
self.concentrations,
)
if self.plot not in {"none", "all", "active", None}:
scheme._check_compounds([self.plot])

# TODO(schneiderfelipe): encapsulate everything in a function that depends
# on the freeenergies as first parameter
dydt = rx.get_dydt(scheme, k)

y, r = rx.get_y(
dydt,
y0=y0,
y0=scheme.concentrations(y0),
method=self.method,
rtol=self.rtol,
atol=self.atol,
Expand Down Expand Up @@ -656,7 +660,7 @@ def _yield_kinetics(self):
rtol=0.01,
)
if self.plot == "all" or not np.any(active):
active = np.array([True for _ in scheme.compounds])
active = np.ones(len(scheme.compounds), dtype=bool)

factor = y(y.t_max)[active].max()
reference = y(y.t_max)[active] / factor
Expand All @@ -679,21 +683,20 @@ def _yield_kinetics(self):

num = 100
t = set(np.linspace(y.t_min, t_max, num=num))
for i, name in enumerate(scheme.compounds):
if not rx.is_transition_state(name):
res = minimize_scalar(
lambda t: -r(t)[i],
bounds=(y.t_min, (t_max + y.t_max) / 2),
method="bounded",
)
if y.t_min < res.x < t_max:
t.update(np.linspace(y.t_min, res.x, num=num // 2))
t.update(np.linspace(res.x, t_max, num=num // 2))
active[i] = True
for i in range(len(scheme.compounds)):
res = minimize_scalar(
lambda t: -r(t)[i],
bounds=(y.t_min, (t_max + y.t_max) / 2),
method="bounded",
)
if y.t_min < res.x < t_max:
t.update(np.linspace(y.t_min, res.x, num=num // 2))
t.update(np.linspace(res.x, t_max, num=num // 2))
active[i] = True

t.update(
np.geomspace(
np.min([_t for _t in t if _t > 0.0]),
min(_t for _t in t if _t > 0.0),
t_max,
num=num,
),
Expand All @@ -705,7 +708,7 @@ def _yield_kinetics(self):
plt.plot(t, y(t)[scheme.compounds.index(name)], label=name, lw=3)
else:
for i, name in enumerate(scheme.compounds):
if active[i] and not rx.is_transition_state(name):
if active[i]:
plt.plot(t, y(t)[i], label=name, lw=3)

plt.legend()
Expand Down Expand Up @@ -735,8 +738,7 @@ def _prepare_simulation(scheme, k, concentrations):
free_y0 = {}
fixed_y0 = {}
for spec in concentrations:
fields = spec.split(":", 1)
name, quantity = fields[0].strip(), fields[1].strip()
name, _, quantity = (field.strip() for field in spec.partition(":"))

if quantity.startswith("!"):
d = fixed_y0
Expand All @@ -746,20 +748,26 @@ def _prepare_simulation(scheme, k, concentrations):

try:
quantity = float(quantity)
except (IndexError, ValueError) as err:
except ValueError as err:
msg = f"badly formatted concentrations: '{' '.join(concentrations)}'"
raise ValueError(msg) from err

d[name] = quantity

# TODO(schneiderfelipe): log stuff related to get_fixed_scheme
scheme, k = rx.get_fixed_scheme(scheme, k, fixed_y0)

y0 = np.zeros(len(scheme.compounds))
for compound in free_y0:
y0[scheme.compounds.index(compound)] = free_y0[compound]
both = free_y0.keys() & fixed_y0.keys()
if both:
msg = f"{', '.join(sorted(both))} given both free and fixed concentrations"
raise ValueError(msg)
scheme, factors = scheme.fix(fixed_y0)
k = k * factors
logger.info(f"fixed concentrations: {fixed_y0}")
logger.info(
"reaction rate constant factors from fixed concentrations: "
f"{rx._misc.format_numbers(factors)}",
)

return scheme, k, y0
scheme._check_compounds(free_y0)
return scheme, k, free_y0


def main(arguments=None):
Expand Down Expand Up @@ -937,7 +945,7 @@ def main(arguments=None):
stream=sys.stdout,
)
for handler in logging.root.handlers:
handler.setFormatter(rx.io.InterfaceFormatter("%(message)s"))
handler.setFormatter(rx.io._InterfaceFormatter("%(message)s"))

model = rx.io.parse_model(args.path, force_compile=args.compile)
report = Report(
Expand All @@ -949,7 +957,7 @@ def main(arguments=None):
temperature=args.temperature,
pressure=args.pressure,
bias=args.bias,
tunneling=args.tunneling,
tunneling=None if args.tunneling == "none" else args.tunneling,
method=args.method,
max_time=args.max_time,
rtol=args.rtol,
Expand Down
17 changes: 4 additions & 13 deletions overreact/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,18 +172,6 @@
]


@np.vectorize
def _vdw_radius(atomno):
"""Select reasonable estimates for the van der Waals radii.

This is a helper for `vdw_radius`.
"""
radius = _vdw_radius_crc[atomno - 1]
if radius is None:
return 2.0
return radius * centi


def vdw_radius(atomno):
"""Select reasonable estimates for the van der Waals radii.

Expand Down Expand Up @@ -222,5 +210,8 @@ def vdw_radius(atomno):
array([1.1 , 1.4 , 1.82, 1.53, 1.92, 1.7 , 1.55, 1.52, 1.47, 1.54])
>>> vdw_radius(range(21, 31)) # Sc, Ti, V, ..., Ni, Cu, Zn
array([2.15, 2.11, 2.07, 2.06, 2.05, 2.04, 2. , 1.97, 1.96, 2.01])
>>> vdw_radius(104) # Rf
array(2.)
"""
return _vdw_radius(atomno)
radii = np.array(_vdw_radius_crc, dtype=float)[np.asarray(atomno) - 1]
return np.where(np.isnan(radii), 2.0, radii * centi)
Loading
Loading