Skip to content

Commit d1129d7

Browse files
Add uncertainties for main parameters. (#299)
* Add uncertainties for main parameters. * 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 1977c00 commit d1129d7

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
@@ -92,6 +92,11 @@ original_grid: bool
9292
verbose: bool
9393
Print additional header details to saved files. These include details about the morph
9494
inputs and outputs.
95+
uncertainty: bool
96+
Estimate uncertainties for each refined morphing parameter. This is done by estimating
97+
the Hessian of the fit. Caution should be taken as this is not the true uncertainty
98+
of the fit, and the user should make their own judgement about what measure of uncertainty
99+
to use for their particular use case.
95100
xmin: float
96101
Minimum x-value (abscissa) to use for function comparisons.
97102
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

@@ -122,6 +123,21 @@ def morph_error(self, msg, error):
122123
action="store_true",
123124
help="Print additional header details to saved files.",
124125
)
126+
parser.add_option(
127+
"-u",
128+
"--uncertainty",
129+
"--estimate-uncertainty",
130+
dest="estimate_uncertainty",
131+
action="store_true",
132+
help=(
133+
"Estimate uncertainties for each refined morphing parameter. "
134+
"This is done by estimating the Hessian of the fit. "
135+
"Caution should be taken as this is not the true uncertainty "
136+
"of the fit, and the user should make their own judgement "
137+
"about what measure of uncertainty to use for their particular "
138+
"use case."
139+
),
140+
)
125141
parser.add_option(
126142
"--xmin",
127143
type="float",
@@ -734,6 +750,7 @@ def single_morph(
734750
refiner.residual = refiner._pearson
735751
if opts.addpearson:
736752
refiner.residual = refiner._add_pearson
753+
unc = None
737754
if opts.refine and refpars:
738755
try:
739756
# This works better when we adjust scale and smear first.
@@ -743,17 +760,39 @@ def single_morph(
743760
rptemp.append("scale")
744761
refiner.refine(*rptemp)
745762
# Adjust all parameters
746-
refiner.refine(*refpars)
763+
unc = refiner.refine(*refpars, estimate_uncertainty=True)
764+
# If one parameter is causing trouble with uncertainty estimate
765+
# compute all uncertainties individually
766+
if unc is None:
767+
unc = {}
768+
for param in refpars:
769+
refiner_single_param = type(refiner)(
770+
refiner.chain,
771+
refiner.x_morph,
772+
refiner.y_morph,
773+
refiner.x_target,
774+
refiner.y_target,
775+
tolerance=refiner.tolerance,
776+
)
777+
refiner_single_param.chain.config = copy.deepcopy(config)
778+
unc_param = refiner_single_param.refine(
779+
*[param], estimate_uncertainty=True
780+
)
781+
if unc_param is not None:
782+
unc.update(unc_param)
747783
except ValueError as e:
748784
parser.morph_error(str(e), ValueError)
749785
# Smear is not being refined, but baselineslope needs to refined to apply
750786
# smear
751787
# Note that baselineslope is only added to the refine list if smear is
752788
# applied
753789
elif "baselineslope" in refpars:
790+
# Note, you cannot estimate uncertainty here as the baselineslope
791+
# does not change the fit
754792
try:
755793
refiner.refine(
756-
"baselineslope", baselineslope=config["baselineslope"]
794+
"baselineslope",
795+
baselineslope=config["baselineslope"],
757796
)
758797
except ValueError as e:
759798
parser.morph_error(str(e), ValueError)
@@ -839,6 +878,7 @@ def single_morph(
839878
io.single_morph_output(
840879
morph_inputs,
841880
morph_results,
881+
uncertainties=None if opts.estimate_uncertainty is None else unc,
842882
save_file=opts.slocation,
843883
morph_file=pargs[0],
844884
xy_out=xy_save,
@@ -880,10 +920,12 @@ def single_morph(
880920
# Return different things depending on whether it is python interfaced
881921
if python_wrap:
882922
morph_info = morph_results
923+
if opts.estimate_uncertainty is not None and unc is not None:
924+
morph_info.update({"Uncertainties": unc})
883925
morph_table = numpy.array(xy_save).T
884926
return morph_info, morph_table
885927
else:
886-
return morph_results
928+
return morph_results, unc
887929

888930

889931
def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
@@ -993,6 +1035,7 @@ def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
9931035

9941036
# Morph morph_file against all other files in target_directory
9951037
morph_results = {}
1038+
uncs = {}
9961039
for target_file in target_list:
9971040
if target_file.is_file:
9981041
# Set the save file destination to be a file within the SLOC
@@ -1002,13 +1045,11 @@ def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
10021045
opts.slocation = Path(save_morphs_here).joinpath(save_as)
10031046
# Perform a morph of morph_file against target_file
10041047
pargs = [morph_file, target_file]
1005-
morph_results.update(
1006-
{
1007-
target_file.name: single_morph(
1008-
parser, opts, pargs, stdout_flag=False
1009-
),
1010-
}
1048+
morph_result, unc = single_morph(
1049+
parser, opts, pargs, stdout_flag=False
10111050
)
1051+
morph_results.update({target_file.name: morph_result})
1052+
uncs.update({target_file.name: unc})
10121053

10131054
target_file_names = []
10141055
for key in morph_results.keys():
@@ -1030,6 +1071,7 @@ def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
10301071
morph_inputs,
10311072
morph_results,
10321073
target_file_names,
1074+
uncertainties_dict=uncs,
10331075
save_directory=save_directory,
10341076
morph_file=morph_file,
10351077
target_directory=target_directory,
@@ -1187,6 +1229,7 @@ def multiple_morphs(parser, opts, pargs, stdout_flag=True, python_wrap=False):
11871229

11881230
# Morph morph_file against all other files in target_directory
11891231
morph_results = {}
1232+
uncs = {}
11901233
for morph_file in morph_list:
11911234
if morph_file.is_file:
11921235
# Set the save file destination to be a file within the SLOC
@@ -1196,13 +1239,11 @@ def multiple_morphs(parser, opts, pargs, stdout_flag=True, python_wrap=False):
11961239
opts.slocation = Path(save_morphs_here).joinpath(save_as)
11971240
# Perform a morph of morph_file against target_file
11981241
pargs = [morph_file, target_file]
1199-
morph_results.update(
1200-
{
1201-
morph_file.name: single_morph(
1202-
parser, opts, pargs, stdout_flag=False
1203-
),
1204-
}
1242+
morph_result, unc = single_morph(
1243+
parser, opts, pargs, stdout_flag=False
12051244
)
1245+
morph_results.update({morph_file.name: morph_result})
1246+
uncs.update({morph_file.name: unc})
12061247

12071248
morph_file_names = []
12081249
for key in morph_results.keys():
@@ -1224,6 +1265,7 @@ def multiple_morphs(parser, opts, pargs, stdout_flag=True, python_wrap=False):
12241265
morph_inputs,
12251266
morph_results,
12261267
morph_file_names,
1268+
uncertainties_dict=uncs,
12271269
save_directory=save_directory,
12281270
morph_file=target_file,
12291271
target_directory=morph_directory,

0 commit comments

Comments
 (0)