Skip to content

Commit cb901e8

Browse files
Add uncertainties for main parameters. (#299)
* Add uncertainties for main parameters. # Conflicts: # src/diffpy/morph/morph_io.py * Works for all parameters now * Fix other tests * loosen tolerance to 1e-4 from 1e-5 * Lower tolerance more * Remove comment * [pre-commit.ci] auto fixes from pre-commit hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 71010e2 commit cb901e8

17 files changed

Lines changed: 70562 additions & 66 deletions

File tree

docs/source/morphpy.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ get_diff: bool
8686
verbose: bool
8787
Print additional header details to saved files. These include details about the morph
8888
inputs and outputs.
89+
uncertainty: bool
90+
Estimate uncertainties for each refined morphing parameter. This is done by estimating
91+
the Hessian of the fit. Caution should be taken as this is not the true uncertainty
92+
of the fit, and the user should make their own judgement about what measure of uncertainty
93+
to use for their particular use case.
8994
xmin: float
9095
Minimum x-value (abscissa) to use for function comparisons.
9196
xmax: float

docs/source/quickstart.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,13 @@ Basic diffpy.morph Workflow
202202

203203
The optimal fit after applying the scale, smear, and stretch morphs.
204204

205-
9. Now, try it on your own! If you have personally collected or
205+
9. We are also able to estimate the uncertainties of each of the fitted parameters.
206+
This is done by using the ``uncertainty`` parameter.
207+
Below we have replaced the ``-a`` from the previous step with a ``-u`` to obtain uncertainty estimates ::
208+
209+
diffpy.morph --scale=0.8 --smear-pdf=-0.08 --stretch=0.005 --xmin=1.5 --xmax=30 -u darkSub_rh20_C_01.gr darkSub_rh20_C_44.gr
210+
211+
10. Now, try it on your own! If you have personally collected or
206212
otherwise readily available PDF data, try this process to see if
207213
you can morph your PDFs to one another. Many of the parameters
208214
provided in this tutorial are unique to it, so be cautious about

news/uncertainty.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* New option "uncertainty" to allow user to estimate uncertainty of the refined morphing parameters.
4+
5+
**Changed:**
6+
7+
* <news item>
8+
9+
**Deprecated:**
10+
11+
* <news item>
12+
13+
**Removed:**
14+
15+
* <news item>
16+
17+
**Fixed:**
18+
19+
* <news item>
20+
21+
**Security:**
22+
23+
* <news item>

src/diffpy/morph/morph_io.py

Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,61 @@ def build_morph_inputs_container(
124124
return morph_inputs
125125

126126

127+
def get_terminal_morph_output(mr_copy, uncertainties):
128+
morphs_out = "# Optimized morphing parameters:\n"
129+
# Handle special inputs (numerical)
130+
if "squeeze" in mr_copy:
131+
sq_dict = mr_copy.pop("squeeze")
132+
rw_pos = list(mr_copy.keys()).index("Rw")
133+
morph_results_list = list(mr_copy.items())
134+
for idx, _ in enumerate(sq_dict):
135+
morph_results_list.insert(
136+
rw_pos + idx, (f"squeeze a{idx}", sq_dict[f"a{idx}"])
137+
)
138+
mr_copy = dict(morph_results_list)
139+
140+
# Handle special inputs (functional remove)
141+
func_dicts = {
142+
"funcxy": [None, None],
143+
"funcx": [None, None],
144+
"funcy": [None, None],
145+
}
146+
for func in func_dicts.keys():
147+
if f"{func}_function" in mr_copy:
148+
func_dicts[func][0] = mr_copy.pop(f"{func}_function")
149+
if func in mr_copy:
150+
func_dicts[func][1] = mr_copy.pop(func)
151+
rw_pos = list(mr_copy.keys()).index("Rw")
152+
morph_results_list = list(mr_copy.items())
153+
for idx, key in enumerate(func_dicts[func][1]):
154+
morph_results_list.insert(
155+
rw_pos + idx, (f"{func} {key}", func_dicts[func][1][key])
156+
)
157+
mr_copy = dict(morph_results_list)
158+
159+
# Get uncertainties
160+
if uncertainties is None:
161+
morphs_out += "\n".join(
162+
f"# {key} = {mr_copy[key]:.6f}" for key in mr_copy.keys()
163+
)
164+
else:
165+
morphs_out += "\n".join(
166+
f"# {key} = {mr_copy[key]:.6f}"
167+
+ (
168+
f" +/- {uncertainties[key]:.6f}"
169+
if key in uncertainties
170+
else ""
171+
)
172+
for key in mr_copy
173+
)
174+
175+
return morphs_out, func_dicts
176+
177+
127178
def single_morph_output(
128179
morph_inputs,
129180
morph_results,
181+
uncertainties=None,
130182
save_file=None,
131183
morph_file=None,
132184
xy_out=None,
@@ -142,6 +194,8 @@ def single_morph_output(
142194
Parameters given by the user.
143195
morph_results: dict
144196
Resulting data after morphing.
197+
uncertainties: dict
198+
Uncertainties of all morphed parameters.
145199
save_file
146200
Name of file to print to. If None (default) print to terminal.
147201
morph_file
@@ -165,41 +219,7 @@ def single_morph_output(
165219
)
166220

167221
mr_copy = morph_results.copy()
168-
morphs_out = "# Optimized morphing parameters:\n"
169-
# Handle special inputs (numerical)
170-
if "squeeze" in mr_copy:
171-
sq_dict = mr_copy.pop("squeeze")
172-
rw_pos = list(mr_copy.keys()).index("Rw")
173-
morph_results_list = list(mr_copy.items())
174-
for idx, _ in enumerate(sq_dict):
175-
morph_results_list.insert(
176-
rw_pos + idx, (f"squeeze a{idx}", sq_dict[f"a{idx}"])
177-
)
178-
mr_copy = dict(morph_results_list)
179-
180-
# Handle special inputs (functional remove)
181-
func_dicts = {
182-
"funcxy": [None, None],
183-
"funcx": [None, None],
184-
"funcy": [None, None],
185-
}
186-
for func in func_dicts.keys():
187-
if f"{func}_function" in mr_copy:
188-
func_dicts[func][0] = mr_copy.pop(f"{func}_function")
189-
if func in mr_copy:
190-
func_dicts[func][1] = mr_copy.pop(func)
191-
rw_pos = list(mr_copy.keys()).index("Rw")
192-
morph_results_list = list(mr_copy.items())
193-
for idx, key in enumerate(func_dicts[func][1]):
194-
morph_results_list.insert(
195-
rw_pos + idx, (f"{func} {key}", func_dicts[func][1][key])
196-
)
197-
mr_copy = dict(morph_results_list)
198-
199-
# Normal inputs
200-
morphs_out += "\n".join(
201-
f"# {key} = {mr_copy[key]:.6f}" for key in mr_copy.keys()
202-
)
222+
morphs_out, func_dicts = get_terminal_morph_output(mr_copy, uncertainties)
203223

204224
# Handle special inputs (functional add)
205225
for func in func_dicts.keys():
@@ -338,6 +358,7 @@ def multiple_morph_output(
338358
morph_inputs,
339359
morph_results,
340360
target_files,
361+
uncertainties_dict=None,
341362
field=None,
342363
field_list=None,
343364
save_directory=None,
@@ -358,6 +379,8 @@ def multiple_morph_output(
358379
Resulting data after morphing.
359380
target_files: list
360381
Files that acted as targets to morphs.
382+
uncertainties_dict: dict
383+
Dictionary of uncertainties for each morph.
361384
save_directory
362385
Name of directory to save morphs in.
363386
field
@@ -396,11 +419,11 @@ def multiple_morph_output(
396419
output = f"\n# Target: {target}\n"
397420
else:
398421
output = f"\n# Morph: {target}\n"
399-
output += "# Optimized morphing parameters:\n"
400-
output += "\n".join(
401-
f"# {param} = {morph_results[target][param]:.6f}"
402-
for param in morph_results[target]
403-
)
422+
423+
mr_copy = morph_results[target].copy()
424+
uncertainties = uncertainties_dict[target]
425+
output_body, _ = get_terminal_morph_output(mr_copy, uncertainties)
426+
output += output_body
404427
verbose_outputs += f"{output}\n"
405428

406429
# Get items we want to put in table

src/diffpy/morph/morphapp.py

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from __future__ import print_function
1717

18+
import copy
1819
import sys
1920
from pathlib import Path
2021

@@ -109,6 +110,21 @@ def morph_error(self, msg, error):
109110
action="store_true",
110111
help="Print additional header details to saved files.",
111112
)
113+
parser.add_option(
114+
"-u",
115+
"--uncertainty",
116+
"--estimate-uncertainty",
117+
dest="estimate_uncertainty",
118+
action="store_true",
119+
help=(
120+
"Estimate uncertainties for each refined morphing parameter. "
121+
"This is done by estimating the Hessian of the fit. "
122+
"Caution should be taken as this is not the true uncertainty "
123+
"of the fit, and the user should make their own judgement "
124+
"about what measure of uncertainty to use for their particular "
125+
"use case."
126+
),
127+
)
112128
parser.add_option(
113129
"--xmin",
114130
type="float",
@@ -721,6 +737,7 @@ def single_morph(
721737
refiner.residual = refiner._pearson
722738
if opts.addpearson:
723739
refiner.residual = refiner._add_pearson
740+
unc = None
724741
if opts.refine and refpars:
725742
try:
726743
# This works better when we adjust scale and smear first.
@@ -730,17 +747,39 @@ def single_morph(
730747
rptemp.append("scale")
731748
refiner.refine(*rptemp)
732749
# Adjust all parameters
733-
refiner.refine(*refpars)
750+
unc = refiner.refine(*refpars, estimate_uncertainty=True)
751+
# If one parameter is causing trouble with uncertainty estimate
752+
# compute all uncertainties individually
753+
if unc is None:
754+
unc = {}
755+
for param in refpars:
756+
refiner_single_param = type(refiner)(
757+
refiner.chain,
758+
refiner.x_morph,
759+
refiner.y_morph,
760+
refiner.x_target,
761+
refiner.y_target,
762+
tolerance=refiner.tolerance,
763+
)
764+
refiner_single_param.chain.config = copy.deepcopy(config)
765+
unc_param = refiner_single_param.refine(
766+
*[param], estimate_uncertainty=True
767+
)
768+
if unc_param is not None:
769+
unc.update(unc_param)
734770
except ValueError as e:
735771
parser.morph_error(str(e), ValueError)
736772
# Smear is not being refined, but baselineslope needs to refined to apply
737773
# smear
738774
# Note that baselineslope is only added to the refine list if smear is
739775
# applied
740776
elif "baselineslope" in refpars:
777+
# Note, you cannot estimate uncertainty here as the baselineslope
778+
# does not change the fit
741779
try:
742780
refiner.refine(
743-
"baselineslope", baselineslope=config["baselineslope"]
781+
"baselineslope",
782+
baselineslope=config["baselineslope"],
744783
)
745784
except ValueError as e:
746785
parser.morph_error(str(e), ValueError)
@@ -825,6 +864,7 @@ def single_morph(
825864
io.single_morph_output(
826865
morph_inputs,
827866
morph_results,
867+
uncertainties=None if opts.estimate_uncertainty is None else unc,
828868
save_file=opts.slocation,
829869
morph_file=pargs[0],
830870
xy_out=xy_save,
@@ -866,10 +906,12 @@ def single_morph(
866906
# Return different things depending on whether it is python interfaced
867907
if python_wrap:
868908
morph_info = morph_results
909+
if opts.estimate_uncertainty is not None and unc is not None:
910+
morph_info.update({"Uncertainties": unc})
869911
morph_table = numpy.array(xy_save).T
870912
return morph_info, morph_table
871913
else:
872-
return morph_results
914+
return morph_results, unc
873915

874916

875917
def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
@@ -979,6 +1021,7 @@ def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
9791021

9801022
# Morph morph_file against all other files in target_directory
9811023
morph_results = {}
1024+
uncs = {}
9821025
for target_file in target_list:
9831026
if target_file.is_file:
9841027
# Set the save file destination to be a file within the SLOC
@@ -988,13 +1031,11 @@ def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
9881031
opts.slocation = Path(save_morphs_here).joinpath(save_as)
9891032
# Perform a morph of morph_file against target_file
9901033
pargs = [morph_file, target_file]
991-
morph_results.update(
992-
{
993-
target_file.name: single_morph(
994-
parser, opts, pargs, stdout_flag=False
995-
),
996-
}
1034+
morph_result, unc = single_morph(
1035+
parser, opts, pargs, stdout_flag=False
9971036
)
1037+
morph_results.update({target_file.name: morph_result})
1038+
uncs.update({target_file.name: unc})
9981039

9991040
target_file_names = []
10001041
for key in morph_results.keys():
@@ -1016,6 +1057,7 @@ def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
10161057
morph_inputs,
10171058
morph_results,
10181059
target_file_names,
1060+
uncertainties_dict=uncs,
10191061
save_directory=save_directory,
10201062
morph_file=morph_file,
10211063
target_directory=target_directory,
@@ -1173,6 +1215,7 @@ def multiple_morphs(parser, opts, pargs, stdout_flag=True, python_wrap=False):
11731215

11741216
# Morph morph_file against all other files in target_directory
11751217
morph_results = {}
1218+
uncs = {}
11761219
for morph_file in morph_list:
11771220
if morph_file.is_file:
11781221
# Set the save file destination to be a file within the SLOC
@@ -1182,13 +1225,11 @@ def multiple_morphs(parser, opts, pargs, stdout_flag=True, python_wrap=False):
11821225
opts.slocation = Path(save_morphs_here).joinpath(save_as)
11831226
# Perform a morph of morph_file against target_file
11841227
pargs = [morph_file, target_file]
1185-
morph_results.update(
1186-
{
1187-
morph_file.name: single_morph(
1188-
parser, opts, pargs, stdout_flag=False
1189-
),
1190-
}
1228+
morph_result, unc = single_morph(
1229+
parser, opts, pargs, stdout_flag=False
11911230
)
1231+
morph_results.update({morph_file.name: morph_result})
1232+
uncs.update({morph_file.name: unc})
11921233

11931234
morph_file_names = []
11941235
for key in morph_results.keys():
@@ -1210,6 +1251,7 @@ def multiple_morphs(parser, opts, pargs, stdout_flag=True, python_wrap=False):
12101251
morph_inputs,
12111252
morph_results,
12121253
morph_file_names,
1254+
uncertainties_dict=uncs,
12131255
save_directory=save_directory,
12141256
morph_file=target_file,
12151257
target_directory=morph_directory,

0 commit comments

Comments
 (0)