Skip to content

Commit d7a0e30

Browse files
committed
Merge assign_parameter_uncertainties and intermediate_uncertainties
There's a lot of repeated code between the old UQ's assign_parameter_uncertainties and the new UQ's assign_intermediate_uncertainties, so this combines them into a single function in order to reuse a lot of that repeated code and make it easier to keep them consistent.
1 parent 004d62c commit d7a0e30

1 file changed

Lines changed: 25 additions & 122 deletions

File tree

rmgpy/tools/uncertainty.py

Lines changed: 25 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -916,43 +916,56 @@ def assign_parameter_uncertainties(self, g_param_engine=None, k_param_engine=Non
916916
self.thermo_input_uncertainties = []
917917
self.kinetic_input_uncertainties = []
918918

919+
self.dG_dqs = [] # a list of dictionaries to store the intermediate derivatives dG_i/dq for each parameter q that contributes to the uncertainty of species i's G
920+
self.dlnk_dqs = [] # a list of dictionaries to store the intermediate derivatives dlnk_i/dq for each parameter q that contributes to the uncertainty of reaction i's k
921+
919922
for species in self.species_list:
920923
if not correlated:
921924
dG = g_param_engine.get_uncertainty_value(self.species_sources_dict[species])
922925
self.thermo_input_uncertainties.append(dG)
923926
else:
924927
source = self.species_sources_dict[species]
925928
dG = {}
929+
dG_dq = {}
926930
if 'Library' in source:
927931
pdG = g_param_engine.get_partial_uncertainty_value(source, 'Library', corr_param=source['Library'])
928932
label = source['Library'][2] # the label we added to the end of the library source tuple in extract_sources_from_model
929933
dG[label] = pdG
934+
dG_dq[label] = 1 # derivative of G with respect to the library parameter is 1, since it's a direct contribution
930935
if 'Surface_Library' in source:
931936
pdG = g_param_engine.get_partial_uncertainty_value(source, 'Surface_Library', corr_param=source['Surface_Library'])
932937
label = source['Surface_Library'][2]
933938
dG[label] = pdG
939+
dG_dq[label] = 1 # derivative of G with respect to the surface library parameter is 1, since it's a direct contribution
934940
if 'QM' in source:
935941
pdG = g_param_engine.get_partial_uncertainty_value(source, 'QM', corr_param=source['QM'])
936942
label = source['QM'][2]
937943
dG[label] = pdG
944+
dG_dq[label] = 1 # derivative of G with respect to the QM parameter is 1, since it's a direct contribution
938945
if 'ADS' in source:
939946
for adsGroupType, groupList in source['ADS'].items():
940947
for group, weight in groupList:
941948
pdG = g_param_engine.get_partial_uncertainty_value(source, 'ADS', group, adsGroupType)
942949
label = 'AdsorptionCorrection({}) {}'.format(adsGroupType, group.label)
943950
dG[label] = pdG
951+
if weight != 1:
952+
raise ValueError('Weight for adsorption group contribution to thermo should be 1, but got weight={weight} for {adsGroupType} in species {species}'.format(weight=weight, adsGroupType=adsGroupType, species=species))
953+
dG_dq[label] = weight # This should be 1 because there's only one group contribution per adsorption correction
944954
if 'GAV' in source:
945955
for groupType, groupList in source['GAV'].items():
946956
for group, weight in groupList:
947957
pdG = g_param_engine.get_partial_uncertainty_value(source, 'GAV', group, groupType)
948958
label = 'Group({}) {}'.format(groupType, group.label)
949959
dG[label] = pdG
960+
dG_dq[label] = weight # the derivative of G with respect to the group contribution is the weight of that group contribution to the overall G
950961
# We also know if there is group additivity used, there will be uncorrelated estimation error
951962
est_pdG = g_param_engine.get_partial_uncertainty_value(source, 'Estimation')
952963
if est_pdG:
953964
label = 'Estimation {}'.format(species.to_chemkin())
954965
dG[label] = est_pdG
966+
dG_dq[label] = 1 # the derivative of G with respect to the estimation error is 1, since we add the term directly
955967
self.thermo_input_uncertainties.append(dG)
968+
self.dG_dqs.append(dG_dq)
956969

957970
for reaction in self.reaction_list:
958971
if not correlated:
@@ -961,6 +974,7 @@ def assign_parameter_uncertainties(self, g_param_engine=None, k_param_engine=Non
961974
else:
962975
source = self.reaction_sources_dict[reaction]
963976
dlnk = {}
977+
dlnk_dq = {}
964978
if 'Rate Rules' in source:
965979
family = source['Rate Rules'][0]
966980
source_dict = source['Rate Rules'][1]
@@ -975,166 +989,55 @@ def assign_parameter_uncertainties(self, g_param_engine=None, k_param_engine=Non
975989
corr_family=family)
976990
label = '{}Rate Rule {} {}'.format(surface_prefix, family, ruleEntry)
977991
dlnk[label] = dplnk
978-
992+
dlnk_dq[label] = weight # the derivative of ln(k) with respect to the rate rule contribution
979993
for ruleEntry, trainingEntry, weight in training:
980994
dplnk = k_param_engine.get_partial_uncertainty_value(source, 'Rate Rules', corr_param=ruleEntry,
981995
corr_family=family)
982996
label = '{}Rate Rule {} {}'.format(surface_prefix, family, ruleEntry)
983997
dlnk[label] = dplnk
998+
dlnk_dq[label] = weight # the derivative of ln(k) with respect to the training rule contribution
999+
1000+
N = len(source_dict['rules']) + len(source_dict['training'])
9841001

9851002
# There is also estimation error if rate rules are used (nonexact and family contribute to this)
9861003
nonexact_dplnk = k_param_engine.get_partial_uncertainty_value(source, 'Estimation Nonexact', corr_family=family)
9871004
if nonexact_dplnk:
9881005
label = 'Estimation Nonexact {}'.format(reaction.to_chemkin(self.species_list, kinetics=False))
9891006
dlnk[label] = nonexact_dplnk
1007+
dlnk_dq[label] = np.log10(N + 1) # the derivative of ln(k) with respect to the nonexact estimation error
9901008

9911009
family_dplnk = k_param_engine.get_partial_uncertainty_value(source, 'Estimation Family', corr_family=family)
9921010
if family_dplnk:
9931011
label = 'Estimation Family {}'.format(reaction.to_chemkin(self.species_list, kinetics=False))
9941012
dlnk[label] = family_dplnk
995-
1013+
dlnk_dq[label] = 1 # the derivative of ln(k) with respect to the family estimation error
9961014
elif 'PDep' in source:
9971015
dplnk = k_param_engine.get_partial_uncertainty_value(source, 'PDep', source['PDep'])
9981016
label = 'PDep {}'.format(reaction.to_chemkin(self.species_list, kinetics=False))
9991017
dlnk[label] = dplnk
1018+
dlnk_dq[label] = 1
10001019

10011020
elif 'Library' in source:
10021021
dplnk = k_param_engine.get_partial_uncertainty_value(source, 'Library', source['Library'])
10031022
label = source['Library'][2]
10041023
dlnk[label] = dplnk
1024+
dlnk_dq[label] = 1
10051025

10061026
elif 'Surface_Library' in source:
10071027
dplnk = k_param_engine.get_partial_uncertainty_value(source, 'Surface_Library', source['Surface_Library'])
10081028
label = source['Surface_Library'][2]
10091029
dlnk[label] = dplnk
1030+
dlnk_dq[label] = 1
10101031

10111032
elif 'Training' in source:
10121033
dplnk = k_param_engine.get_partial_uncertainty_value(source, 'Training', source['Training'])
10131034
family = source['Training'][0]
10141035
label = 'Training {} {}'.format(family, reaction.to_chemkin(self.species_list, kinetics=False))
10151036
dlnk[label] = dplnk
1037+
dlnk_dq[label] = 1
10161038

10171039
self.kinetic_input_uncertainties.append(dlnk)
1018-
1019-
def assign_intermediate_uncertainties(self, g_param_engine=None, k_param_engine=None, correlated=False):
1020-
"""
1021-
Assign uncertainties to the intermediate parameters based on the sources of the species thermo and reaction kinetics.
1022-
1023-
This fills out the class variables thermo_intermediate_uncertainties and kinetic_intermediate_uncertainties
1024-
these are each list of dictionaries. For every species or reaction, it lists all the intermediate sources contributing to that parameter's uncertainty.
1025-
1026-
So for example, thermo_intermediate_uncertainties might look something like this:
1027-
1028-
thermo_intermediate_uncertainties = [
1029-
{'Group(group) Cds-CdsHH': 2.0, 'Group(radical) CCJ': 1.0, 'Estimation CH(4)': 1.0},
1030-
{'Library CH2(5)': 1.0},
1031-
]
1032-
The keys of the dictionaries are the label names for the intermediate parameters.
1033-
and the values are partial derivatives dG_i/dq_w, how the species i Gibbs uncertainty changes with the intermediate parameter w.
1034-
1035-
This function is the new formulation's equivalent to assign_parameter_uncertainties and similarly handles both correlated and uncorrelated cases.
1036-
But instead of assuming all underlying parameters are independent, here we can allow for dependence as long as we have the covariance
1037-
"""
1038-
if g_param_engine is None:
1039-
g_param_engine = ThermoParameterUncertainty(other_covariances=self.thermo_covariances_dict)
1040-
if k_param_engine is None:
1041-
k_param_engine = KineticParameterUncertainty()
1042-
1043-
self.thermo_intermediate_uncertainties = [] # store the intermediate dG_i/dq for each parameter q that contributes to the uncertainty of G_i, for use in correlated uncertainty analysis
1044-
self.kinetic_intermediate_uncertainties = []
1045-
1046-
for species in self.species_list:
1047-
if not correlated:
1048-
dG = g_param_engine.get_uncertainty_value(self.species_sources_dict[species])
1049-
self.thermo_intermediate_uncertainties.append(dG) # in the uncorrelated case, the intermediate is just the uncertainty value itself, since there is only one parameter that contributes to the uncertainty
1050-
else:
1051-
source = self.species_sources_dict[species]
1052-
dGdq = {}
1053-
if 'Library' in source:
1054-
label = source['Library'][2]
1055-
dGdq[label] = 1 # dG/dG_lib = 1, because the parameter is never scaled by anything other than 1 when it is used
1056-
if 'Surface_Library' in source:
1057-
label = source['Surface_Library'][2]
1058-
dGdq[label] = 1 # dG/dG_surf = 1, because the parameter is never scaled by anything other than 1 when it is used
1059-
if 'QM' in source:
1060-
label = source['QM'][2]
1061-
dGdq[label] = 1
1062-
if 'ADS' in source:
1063-
for adsGroupType, groupList in source['ADS'].items():
1064-
for group, weight in groupList:
1065-
label = 'AdsorptionCorrection({}) {}'.format(adsGroupType, group.label)
1066-
if weight != 1:
1067-
raise ValueError('Weight for adsorption group contribution to thermo should be 1, but got weight={weight} for {adsGroupType} in species {species}'.format(weight=weight, adsGroupType=adsGroupType, species=species))
1068-
dGdq[label] = weight # This should be 1
1069-
if 'GAV' in source:
1070-
for groupType, groupList in source['GAV'].items():
1071-
for group, weight in groupList:
1072-
label = 'Group({}) {}'.format(groupType, group.label)
1073-
dGdq[label] = weight # dG/dG_group = weight, because the group contribution is scaled by the weight when it is used in the thermo estimation
1074-
# We also know if there is group additivity used, there will be uncorrelated estimation error
1075-
label = 'Estimation {}'.format(species.to_chemkin())
1076-
dGdq[label] = 1 # dG/dG_est = 1, because the estimation error is added on top of the group additivity value, so it is never scaled by anything other than 1 when it is used
1077-
1078-
self.thermo_intermediate_uncertainties.append(dGdq)
1079-
1080-
for reaction in self.reaction_list:
1081-
if not correlated:
1082-
dlnk = k_param_engine.get_uncertainty_value(self.reaction_sources_dict[reaction])
1083-
self.kinetic_intermediate_uncertainties.append(dlnk) # in the uncorrelated case, the intermediate is just the uncertainty value itself, since there is only one parameter that contributes to the uncertainty
1084-
else:
1085-
source = self.reaction_sources_dict[reaction]
1086-
dlnkdq = {}
1087-
if 'Rate Rules' in source:
1088-
family = source['Rate Rules'][0]
1089-
source_dict = source['Rate Rules'][1]
1090-
rules = source_dict['rules']
1091-
training = source_dict['training']
1092-
exact = source_dict['exact']
1093-
surface_prefix = ''
1094-
if reaction.is_surface_reaction():
1095-
surface_prefix = 'Surface '
1096-
for ruleEntry, weight in rules:
1097-
label = '{}Rate Rule {} {}'.format(surface_prefix, family, ruleEntry)
1098-
dlnkdq[label] = weight # dlnk/dlnk_rule = weight, because the rate rule is scaled by the weight when it is used in the kinetics estimation
1099-
1100-
for ruleEntry, trainingEntry, weight in training:
1101-
# TODO - test that training reactions in a tree are correlated with the exact match kind of training reaction
1102-
# for now, we follow the old convention of treating these as rate rules
1103-
label = '{}Rate Rule {} {}'.format(surface_prefix, family, ruleEntry) # ruleEntry should probably be the reaction equation itself
1104-
dlnkdq[label] = weight # dlnk/dlnk_training = weight, because the training entry is scaled by the weight when it is used in the kinetics estimation
1105-
1106-
# There is also estimation error if rate rules are used
1107-
# Record dlnk/dlnk_family, the derivative with respect to the family estimation uncertainty
1108-
label = 'Estimation Family {}'.format(reaction.to_chemkin(self.species_list, kinetics=False))
1109-
dlnkdq[label] = 1 # dlnk/dlnk_family = 1, because the family estimation uncertainty is added on top of the rate rule values, so it is never scaled by anything other than 1 when it is used
1110-
1111-
# Record the non-exact estimation error if not an exact match for a rate rule
1112-
if not exact:
1113-
N = len(source_dict['rules']) + len(source_dict['training'])
1114-
label = 'Estimation Nonexact {}'.format(reaction.to_chemkin(self.species_list, kinetics=False))
1115-
dlnkdq[label] = np.log10(N + 1)
1116-
1117-
elif 'PDep' in source:
1118-
label = 'PDep {}'.format(reaction.to_chemkin(self.species_list, kinetics=False))
1119-
dlnkdq[label] = 1.0 # dlnk/dlnk_PDep = 1, because the PDep kinetics is never scaled by anything other than 1 when it is used
1120-
1121-
elif 'Library' in source:
1122-
label = 'Library {}'.format(reaction.to_chemkin(self.species_list, kinetics=False))
1123-
dlnkdq[label] = 1.0 # dlnk/dlnk_lib = 1, because the library kinetics is never scaled by anything other than 1 when it is used
1124-
1125-
elif 'Surface_Library' in source:
1126-
label = 'Surface_Library {}'.format(reaction.to_chemkin(self.species_list, kinetics=False))
1127-
dlnkdq[label] = 1.0 # dlnk/dlnk_surf_lib = 1, because the surface library kinetics is never scaled by anything other than 1 when it is used
1128-
1129-
elif 'Training' in source:
1130-
family = source['Training'][0]
1131-
surface_prefix = ''
1132-
if reaction.is_surface_reaction():
1133-
surface_prefix = 'Surface '
1134-
label = '{}Training {} {}'.format(surface_prefix, family, reaction.to_chemkin(self.species_list, kinetics=False))
1135-
dlnkdq[label] = 1.0
1136-
1137-
self.kinetic_intermediate_uncertainties.append(dlnkdq)
1040+
self.dlnk_dqs.append(dlnk_dq)
11381041

11391042
def sensitivity_analysis(self, initial_mole_fractions, sensitive_species, T, P, termination_time,
11401043
sensitivity_threshold=1e-3, number=10, fileformat='.png', initial_surface_coverages=None,

0 commit comments

Comments
 (0)