Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8deab1c
Minor refactor of kinetics _make_rule and rate averaging comments.
rwest Jul 3, 2025
d262550
Minor refactor of kinetics _make_rule: outdent an "if True:" block
rwest Apr 3, 2026
1d5455b
Don't trust Blowers-Masel rate rule with abs(n) > 5.0
rwest Jul 3, 2025
3e5253a
ArrheniusBM.get_activation_energy can cope with negative E0
davidfarinajr Dec 14, 2021
0fa20ee
Reuse dHrxn since we're in a loop.
rwest Jul 2, 2025
7817c92
Better starting guesses for optimization of lnA, n, E0.
davidfarinajr Dec 14, 2021
ae48f6e
Simplify and hopefully optimize exponential in fitting.
rwest Jul 2, 2025
dd64341
Revised Blowers-Masel ArrheniusBM fitting.
davidfarinajr Dec 14, 2021
d49b569
modifying BM fitting so w0 is always at least 2 E0
May 28, 2024
2fc779a
ArrheniuschargeTransferBM.get_activation_energy can cope with negativ…
rwest Jul 3, 2025
b6ddb4d
Update ArrheniusChargeTransferBM.fit_to_reactions
rwest Jul 3, 2025
98ac807
Handle negative E0 in ArrheniusBM and ArrheniusChargeTransferBM by ad…
rwest Apr 6, 2026
3b1461a
Refactor ArrheniusBM and ArrheniusChargeTransferBM
rwest Apr 6, 2026
3422d0b
Use constants.R instead of 8.314 or 8.314472
rwest Apr 6, 2026
42718a8
Fix w0 bug in Blowers-Masel fitting.
rwest Apr 6, 2026
1590699
Simplify some array slicing for leave-one-out selection.
rwest Apr 6, 2026
3d0b9b1
Refactoring to avoid AttributeError in _make_rule
Nora-Khalil Apr 8, 2026
5cbdf83
Update the cross_validate method to match the _make_rule method, for …
rwest Apr 10, 2026
d9ab313
Modernize display.py IPython detection
rwest Apr 10, 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
157 changes: 88 additions & 69 deletions rmgpy/data/kinetics/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -3678,19 +3678,22 @@ def cross_validate(self, folds=5, template_rxn_map=None, test_rxn_inds=None, T=1

uncertainties[rxn] = self.rules.entries[entry.label][0].data.uncertainty


L = list(set(template_rxn_map[entry.label]) - set(rxns_test))

if L != []:
if isinstance(L[0].kinetics, Arrhenius):
kinetics = ArrheniusBM().fit_to_reactions(L, recipe=self.forward_recipe.actions)
if kinetics.E0.value_si < 0.0 or len(L) == 1:
# These conditions (eg. n>5) for rejecting the BM fit should correspond
# to those in _marke_rule, and those just below:
if len(L) == 1 or kinetics.E0.value_si < 0.0 or abs(kinetics.n.value_si) > 5.0:
kinetics = average_kinetics([r.kinetics for r in L])
else:
kinetics = kinetics.to_arrhenius(rxn.get_enthalpy_of_reaction(298.))
else:
kinetics = ArrheniusChargeTransferBM().fit_to_reactions(L, recipe=self.forward_recipe.actions)
if kinetics.E0.value_si < 0.0 or len(L) == 1:
# These conditions (eg. n>5) for rejecting the BM fit should correspond
# to those in _marke_rule, and those just above:
if len(L) == 1 or kinetics.E0.value_si < 0.0 or abs(kinetics.n.value_si) > 5.0:
kinetics = average_kinetics([r.kinetics for r in L])
else:
kinetics = kinetics.to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(298.))
Expand Down Expand Up @@ -4596,79 +4599,93 @@ def _make_rule(rr):
for i, rxn in enumerate(rxns):
rxn.rank = ranks[i]
rxns = np.array(rxns)
rs = np.array([r for r in rxns if type(r.kinetics) != KineticsModel])
rs = np.array([r for r in rxns if type(r.kinetics) is not KineticsModel]) # KineticsModel is the base class with no data.
n = len(rs)
if n > 0 and isinstance(rs[0].kinetics, Marcus):
if n == 0:
return None

if isinstance(rs[0].kinetics, Marcus):
kin = average_kinetics([r.kinetics for r in rs])
return kin
data_mean = np.mean(np.log([r.kinetics.get_rate_coefficient(Tref) for r in rs]))
if n > 0:
if isinstance(rs[0].kinetics, Arrhenius):
arr = ArrheniusBM
else:
arr = ArrheniusChargeTransferBM
if n > 1:
kin = arr().fit_to_reactions(rs, recipe=recipe)
if n == 1 or kin.E0.value_si < 0.0:
kin = average_kinetics([r.kinetics for r in rs])
#kin.comment = "Only one reaction or Arrhenius BM fit bad. Instead averaged from {} reactions.".format(n)
if n == 1:
kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label)

if isinstance(rs[0].kinetics, Arrhenius):
arr = ArrheniusBM
else:
arr = ArrheniusChargeTransferBM
if n > 1:
kin = arr().fit_to_reactions(rs, recipe=recipe)

# If you update the following conditions, also update the cross_validate function.
if n == 1 or kin.E0.value_si < 0.0 or abs(kin.n.value_si) > 5.0:
if n > 1: #we have an ArrheniusBM
if kin.E0.value_si < 0.0:
reason = "E0<0"
elif abs(kin.n.value_si) > 5.0:
reason = "abs(n)>5"
else:
reason = "?"

# still run it through the averaging function when n=1 to standardize the units and run checks
kin = average_kinetics([r.kinetics for r in rs])

if n == 1:
kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label)
else:
kin.comment = f"Blowers-Masel fit was bad ({reason}) so instead averaged from {n} reactions."
dlnks = np.array([
np.log(
average_kinetics([r.kinetics for r in np.delete(rs, i)]).get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref)
) for i, rxn in enumerate(rs)
]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref
varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * constants.R * Tref)) ** 2
# weighted average calculations
ws = 1.0 / varis
V1 = ws.sum()
V2 = (ws ** 2).sum()
mu = np.dot(ws, dlnks) / V1
s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1))
kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label)
else:
if n == 1:
kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label)
else:
if isinstance(rs[0].kinetics, Arrhenius):
dlnks = np.array([
np.log(
average_kinetics([r.kinetics for r in rs[list(set(range(len(rs))) - {i})]]).get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref)
) for i, rxn in enumerate(rs)
]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref
varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2
# weighted average calculations
ws = 1.0 / varis
V1 = ws.sum()
V2 = (ws ** 2).sum()
mu = np.dot(ws, dlnks) / V1
s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1))
kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label)
else:
if n == 1:
kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label)
arr().fit_to_reactions(np.delete(rs, i), recipe=recipe)
.to_arrhenius(rxn.get_enthalpy_of_reaction(Tref))
.get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref)
) for i, rxn in enumerate(rs)
]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref
else:
if isinstance(rs[0].kinetics, Arrhenius):
dlnks = np.array([
np.log(
arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe)
.to_arrhenius(rxn.get_enthalpy_of_reaction(Tref))
.get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref)
) for i, rxn in enumerate(rs)
]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref
else:
dlnks = np.array([
np.log(
arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe)
.to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(Tref))
.get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref)
) for i, rxn in enumerate(rs)
]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref
varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2
# weighted average calculations
ws = 1.0 / varis
V1 = ws.sum()
V2 = (ws ** 2).sum()
mu = np.dot(ws, dlnks) / V1
s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1))
kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label)

#site solute parameters
site_datas = [get_site_solute_data(rxn) for rxn in rxns]
site_datas = [sdata for sdata in site_datas if sdata is not None]
if len(site_datas) > 0:
site_data = SoluteTSData()
for sdata in site_datas:
site_data += sdata
site_data = site_data * (1.0/len(site_datas))
kin.solute = site_data
return kin
else:
return None
dlnks = np.array([
np.log(
arr().fit_to_reactions(np.delete(rs, i), recipe=recipe)
.to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(Tref))
.get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref)
) for i, rxn in enumerate(rs)
]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref
varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * constants.R * Tref)) ** 2
# weighted average calculations
ws = 1.0 / varis
V1 = ws.sum()
V2 = (ws ** 2).sum()
mu = np.dot(ws, dlnks) / V1
s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1))
kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label)

#site solute parameters
site_datas = [get_site_solute_data(rxn) for rxn in rxns]
site_datas = [sdata for sdata in site_datas if sdata is not None]
if len(site_datas) > 0:
site_data = SoluteTSData()
for sdata in site_datas:
site_data += sdata
site_data = site_data * (1.0/len(site_datas))
kin.solute = site_data
return kin



def _spawn_tree_process(family, template_rxn_map, obj, T, nprocs, depth, min_splitable_entry_num, min_rxns_to_spawn, extension_iter_max, extension_iter_item_cap):
Expand Down Expand Up @@ -4772,7 +4789,7 @@ def average_kinetics(kinetics_list):
beta=(beta,"1/m"),
wr=(wr * 0.001, "kJ/mol"),
wp=(wp * 0.001, "kJ/mol"),
comment="Averaged from {} reactions.".format(len(kinetics_list)),
comment=f"Averaged from {len(kinetics_list)} rate expressions.",
)
elif isinstance(kinetics, SurfaceChargeTransfer):
averaged_kinetics = SurfaceChargeTransfer(
Expand All @@ -4782,6 +4799,7 @@ def average_kinetics(kinetics_list):
alpha=alpha,
V0=(V0,'V'),
Ea=(Ea * 0.001, "kJ/mol"),
comment=f"Averaged from {len(kinetics_list)} rate expressions.",
)
elif isinstance(kinetics, ArrheniusChargeTransfer):
averaged_kinetics = ArrheniusChargeTransfer(
Expand All @@ -4791,6 +4809,7 @@ def average_kinetics(kinetics_list):
alpha=alpha,
V0=(V0,'V'),
Ea=(Ea * 0.001, "kJ/mol"),
comment=f"Averaged from {len(kinetics_list)} rate expressions.",
)
else:
averaged_kinetics = Arrhenius(
Expand Down
18 changes: 9 additions & 9 deletions rmgpy/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@

"""
This module contains only a display() function, which, if you are running in
the IPython --pylab mode, will render things inline in pretty SVG or PNG graphics.
If you are NOT running in IPython --pylab mode, it will do nothing.
a Jupyter notebook or IPython session, will render things inline in pretty SVG or PNG graphics.
If you are NOT running in such an environment, it will do nothing.
"""

try:
from IPython.core.interactiveshell import InteractiveShell
from IPython import get_ipython
_ipython = get_ipython()
except ImportError:
_ipython = None

if _ipython is not None:
from IPython.display import display
else:
def display(obj):
pass
else:
if InteractiveShell.initialized():
from IPython.core.display import display
else:
def display(obj):
pass
Loading
Loading