Skip to content

Commit 5bad7c9

Browse files
committed
Add uncertainties for main parameters.
1 parent 268435c commit 5bad7c9

3 files changed

Lines changed: 73 additions & 10 deletions

File tree

src/diffpy/morph/morph_io.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ def build_morph_inputs_container(
127127
def single_morph_output(
128128
morph_inputs,
129129
morph_results,
130+
uncertainties=None,
130131
save_file=None,
131132
morph_file=None,
132133
xy_out=None,
@@ -142,6 +143,8 @@ def single_morph_output(
142143
Parameters given by the user.
143144
morph_results: dict
144145
Resulting data after morphing.
146+
uncertainties: dict
147+
Uncertainties of all morphed parameters.
145148
save_file
146149
Name of file to print to. If None (default) print to terminal.
147150
morph_file
@@ -156,6 +159,8 @@ def single_morph_output(
156159
Print to terminal when True (default False).
157160
"""
158161

162+
print(uncertainties)
163+
159164
# Input and output parameters
160165
morphs_in = "\n# Input morphing parameters:\n"
161166
morphs_in += (
@@ -198,9 +203,20 @@ def single_morph_output(
198203
mr_copy = dict(morph_results_list)
199204

200205
# Normal inputs
201-
morphs_out += "\n".join(
202-
f"# {key} = {mr_copy[key]:.6f}" for key in mr_copy.keys()
203-
)
206+
if uncertainties is None:
207+
morphs_out += "\n".join(
208+
f"# {key} = {mr_copy[key]:.6f}" for key in mr_copy.keys()
209+
)
210+
else:
211+
morphs_out += "\n".join(
212+
f"# {key} = {mr_copy[key]:.6f}"
213+
+ (
214+
f" +/- {uncertainties[key]:.6f}"
215+
if key in uncertainties
216+
else ""
217+
)
218+
for key in mr_copy
219+
)
204220

205221
# Handle special inputs (functional add)
206222
for func in func_dicts.keys():

src/diffpy/morph/morphapp.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,21 @@ def morph_error(self, msg, error):
109109
action="store_true",
110110
help="Print additional header details to saved files.",
111111
)
112+
parser.add_option(
113+
"-u",
114+
"--uncertainty",
115+
"--estimate-uncertainty",
116+
dest="estimate_uncertainty",
117+
action="store_true",
118+
help=(
119+
"Estimate uncertainties for each refined morphing parameter. "
120+
"This is done by estimating the Hessian of the fit. "
121+
"Caution should be taken as this is not the true uncertainty "
122+
"of the fit, and the user should make their own judgement "
123+
"about what measure of uncertainty to use for their particular "
124+
"use case."
125+
),
126+
)
112127
parser.add_option(
113128
"--xmin",
114129
type="float",
@@ -721,6 +736,7 @@ def single_morph(
721736
refiner.residual = refiner._pearson
722737
if opts.addpearson:
723738
refiner.residual = refiner._add_pearson
739+
unc = None
724740
if opts.refine and refpars:
725741
try:
726742
# This works better when we adjust scale and smear first.
@@ -730,17 +746,20 @@ def single_morph(
730746
rptemp.append("scale")
731747
refiner.refine(*rptemp)
732748
# Adjust all parameters
733-
refiner.refine(*refpars)
749+
unc = refiner.refine(*refpars, estimate_uncertainty=True)
734750
except ValueError as e:
735751
parser.morph_error(str(e), ValueError)
736752
# Smear is not being refined, but baselineslope needs to refined to apply
737753
# smear
738754
# Note that baselineslope is only added to the refine list if smear is
739755
# applied
740756
elif "baselineslope" in refpars:
757+
# Note, you cannot estimate uncertainty here as the baselineslope
758+
# does not change the fit
741759
try:
742760
refiner.refine(
743-
"baselineslope", baselineslope=config["baselineslope"]
761+
"baselineslope",
762+
baselineslope=config["baselineslope"],
744763
)
745764
except ValueError as e:
746765
parser.morph_error(str(e), ValueError)
@@ -825,6 +844,7 @@ def single_morph(
825844
io.single_morph_output(
826845
morph_inputs,
827846
morph_results,
847+
uncertainties=None if opts.estimate_uncertainty is None else unc,
828848
save_file=opts.slocation,
829849
morph_file=pargs[0],
830850
xy_out=xy_save,

src/diffpy/morph/refine.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
"""refine -- Refine a morph or morph chain
1616
"""
1717

18-
from numpy import array, concatenate, dot, exp, ones_like
18+
import warnings
19+
20+
from numpy import array, concatenate, diag, dot, exp, ones_like, sqrt
1921
from scipy.optimize import leastsq
2022
from scipy.stats import pearsonr
2123

@@ -134,7 +136,7 @@ def _add_pearson(self, pvals):
134136
res = concatenate([res1, res2])
135137
return res
136138

137-
def refine(self, *args, **kw):
139+
def refine(self, *args, estimate_uncertainty=False, **kw):
138140
"""Refine the chain.
139141
140142
Additional arguments are used to specify which parameters are to be
@@ -145,7 +147,9 @@ def refine(self, *args, **kw):
145147
146148
This uses the leastsq algorithm from scipy.optimize.
147149
148-
This returns the final scalar residual value.
150+
If estimate_uncertainty is True, return an estimated uncertainty
151+
for each parameter.
152+
Otherwise, return the final scalar residual value (used for testing).
149153
The parameters from the fit can be retrieved from the config
150154
dictionary of the morph or morph chain.
151155
@@ -180,7 +184,7 @@ def refine(self, *args, **kw):
180184
initial.append(val)
181185
self.flat_to_grouped[len(initial) - 1] = (p, None)
182186

183-
sol, cov_sol, infodict, emesg, ier = leastsq(
187+
sol, hess_inv_sol, infodict, emesg, ier = leastsq(
184188
self.residual,
185189
array(initial),
186190
full_output=True,
@@ -199,7 +203,30 @@ def refine(self, *args, **kw):
199203
vals = [vals]
200204
self._update_chain(vals)
201205

202-
return dot(fvec, fvec)
206+
if estimate_uncertainty:
207+
if hess_inv_sol is None:
208+
warnings.warn(
209+
"Warning: Could not estimate "
210+
"uncertainty as estimated Hessian is singular.",
211+
UserWarning,
212+
)
213+
return None
214+
dof = len(fvec) - len(sol)
215+
if dof <= 0:
216+
warnings.warn(
217+
"Warning: Could not estimate "
218+
"uncertainty as the number of fit parameters "
219+
"exceeds the number of data points.",
220+
UserWarning,
221+
)
222+
return None
223+
par_names = list(self.pars)
224+
cov_sol = hess_inv_sol * dot(fvec, fvec) / dof
225+
unc = sqrt(diag(cov_sol))
226+
227+
return dict(zip(par_names, unc))
228+
else:
229+
return dot(fvec, fvec)
203230

204231

205232
# End class Refiner

0 commit comments

Comments
 (0)