Skip to content

Commit 6a9c86e

Browse files
committed
Works for all parameters now
1 parent 5bad7c9 commit 6a9c86e

15 files changed

Lines changed: 70506 additions & 73 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: 65 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -124,53 +124,7 @@ def build_morph_inputs_container(
124124
return morph_inputs
125125

126126

127-
def single_morph_output(
128-
morph_inputs,
129-
morph_results,
130-
uncertainties=None,
131-
save_file=None,
132-
morph_file=None,
133-
xy_out=None,
134-
verbose=False,
135-
stdout_flag=False,
136-
):
137-
"""Helper function for printing details about a single morph.
138-
Handles both printing to terminal and printing to a file.
139-
140-
Parameters
141-
----------
142-
morph_inputs: dict
143-
Parameters given by the user.
144-
morph_results: dict
145-
Resulting data after morphing.
146-
uncertainties: dict
147-
Uncertainties of all morphed parameters.
148-
save_file
149-
Name of file to print to. If None (default) print to terminal.
150-
morph_file
151-
Name of the morphed function file. Required when printing to a
152-
non-terminal file.
153-
param xy_out: list
154-
List of the form [x_morph_out, y_morph_out]. x_morph_out is a List of
155-
r values and y_morph_out is a List of gr values.
156-
verbose: bool
157-
Print additional details about the morph when True (default False).
158-
stdout_flag: bool
159-
Print to terminal when True (default False).
160-
"""
161-
162-
print(uncertainties)
163-
164-
# Input and output parameters
165-
morphs_in = "\n# Input morphing parameters:\n"
166-
morphs_in += (
167-
"\n".join(
168-
f"# {key} = {morph_inputs[key]}" for key in morph_inputs.keys()
169-
)
170-
+ "\n"
171-
)
172-
173-
mr_copy = morph_results.copy()
127+
def get_terminal_morph_output(mr_copy, uncertainties):
174128
morphs_out = "# Optimized morphing parameters:\n"
175129
# Handle special inputs (numerical)
176130
if "squeeze" in mr_copy:
@@ -202,7 +156,7 @@ def single_morph_output(
202156
)
203157
mr_copy = dict(morph_results_list)
204158

205-
# Normal inputs
159+
# Get uncertainties
206160
if uncertainties is None:
207161
morphs_out += "\n".join(
208162
f"# {key} = {mr_copy[key]:.6f}" for key in mr_copy.keys()
@@ -218,6 +172,56 @@ def single_morph_output(
218172
for key in mr_copy
219173
)
220174

175+
return morphs_out, func_dicts
176+
177+
178+
def single_morph_output(
179+
morph_inputs,
180+
morph_results,
181+
uncertainties=None,
182+
save_file=None,
183+
morph_file=None,
184+
xy_out=None,
185+
verbose=False,
186+
stdout_flag=False,
187+
):
188+
"""Helper function for printing details about a single morph.
189+
Handles both printing to terminal and printing to a file.
190+
191+
Parameters
192+
----------
193+
morph_inputs: dict
194+
Parameters given by the user.
195+
morph_results: dict
196+
Resulting data after morphing.
197+
uncertainties: dict
198+
Uncertainties of all morphed parameters.
199+
save_file
200+
Name of file to print to. If None (default) print to terminal.
201+
morph_file
202+
Name of the morphed function file. Required when printing to a
203+
non-terminal file.
204+
param xy_out: list
205+
List of the form [x_morph_out, y_morph_out]. x_morph_out is a List of
206+
r values and y_morph_out is a List of gr values.
207+
verbose: bool
208+
Print additional details about the morph when True (default False).
209+
stdout_flag: bool
210+
Print to terminal when True (default False).
211+
"""
212+
213+
# Input and output parameters
214+
morphs_in = "\n# Input morphing parameters:\n"
215+
morphs_in += (
216+
"\n".join(
217+
f"# {key} = {morph_inputs[key]}" for key in morph_inputs.keys()
218+
)
219+
+ "\n"
220+
)
221+
222+
mr_copy = morph_results.copy()
223+
morphs_out, func_dicts = get_terminal_morph_output(mr_copy, uncertainties)
224+
221225
# Handle special inputs (functional add)
222226
for func in func_dicts.keys():
223227
if func_dicts[func][0] is not None:
@@ -356,6 +360,7 @@ def multiple_morph_output(
356360
morph_inputs,
357361
morph_results,
358362
target_files,
363+
uncertainties_dict=None,
359364
field=None,
360365
field_list=None,
361366
save_directory=None,
@@ -376,6 +381,8 @@ def multiple_morph_output(
376381
Resulting data after morphing.
377382
target_files: list
378383
Files that acted as targets to morphs.
384+
uncertainties_dict: dict
385+
Dictionary of uncertainties for each morph.
379386
save_directory
380387
Name of directory to save morphs in.
381388
field
@@ -415,11 +422,16 @@ def multiple_morph_output(
415422
output = f"\n# Target: {target}\n"
416423
else:
417424
output = f"\n# Morph: {target}\n"
418-
output += "# Optimized morphing parameters:\n"
419-
output += "\n".join(
420-
f"# {param} = {morph_results[target][param]:.6f}"
421-
for param in morph_results[target]
422-
)
425+
# output += "# Optimized morphing parameters:\n"
426+
# output += "\n".join(
427+
# f"# {param} = {morph_results[target][param]:.6f}"
428+
# for param in morph_results[target]
429+
# )
430+
431+
mr_copy = morph_results[target].copy()
432+
uncertainties = uncertainties_dict[target]
433+
output_body, _ = get_terminal_morph_output(mr_copy, uncertainties)
434+
output += output_body
423435
verbose_outputs += f"{output}\n"
424436

425437
# Get items we want to put in table

src/diffpy/morph/morphapp.py

Lines changed: 35 additions & 13 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

@@ -747,6 +748,25 @@ def single_morph(
747748
refiner.refine(*rptemp)
748749
# Adjust all parameters
749750
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)
750770
except ValueError as e:
751771
parser.morph_error(str(e), ValueError)
752772
# Smear is not being refined, but baselineslope needs to refined to apply
@@ -886,10 +906,12 @@ def single_morph(
886906
# Return different things depending on whether it is python interfaced
887907
if python_wrap:
888908
morph_info = morph_results
909+
if opts.estimate_uncertainty is not None and unc is not None:
910+
morph_info.update({"Uncertainties": unc})
889911
morph_table = numpy.array(xy_save).T
890912
return morph_info, morph_table
891913
else:
892-
return morph_results
914+
return morph_results, unc
893915

894916

895917
def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
@@ -999,6 +1021,7 @@ def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
9991021

10001022
# Morph morph_file against all other files in target_directory
10011023
morph_results = {}
1024+
uncs = {}
10021025
for target_file in target_list:
10031026
if target_file.is_file:
10041027
# Set the save file destination to be a file within the SLOC
@@ -1008,13 +1031,11 @@ def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
10081031
opts.slocation = Path(save_morphs_here).joinpath(save_as)
10091032
# Perform a morph of morph_file against target_file
10101033
pargs = [morph_file, target_file]
1011-
morph_results.update(
1012-
{
1013-
target_file.name: single_morph(
1014-
parser, opts, pargs, stdout_flag=False
1015-
),
1016-
}
1034+
morph_result, unc = single_morph(
1035+
parser, opts, pargs, stdout_flag=False
10171036
)
1037+
morph_results.update({target_file.name: morph_result})
1038+
uncs.update({target_file.name: unc})
10181039

10191040
target_file_names = []
10201041
for key in morph_results.keys():
@@ -1036,6 +1057,7 @@ def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
10361057
morph_inputs,
10371058
morph_results,
10381059
target_file_names,
1060+
uncertainties_dict=uncs,
10391061
save_directory=save_directory,
10401062
morph_file=morph_file,
10411063
target_directory=target_directory,
@@ -1193,6 +1215,7 @@ def multiple_morphs(parser, opts, pargs, stdout_flag=True, python_wrap=False):
11931215

11941216
# Morph morph_file against all other files in target_directory
11951217
morph_results = {}
1218+
uncs = {}
11961219
for morph_file in morph_list:
11971220
if morph_file.is_file:
11981221
# Set the save file destination to be a file within the SLOC
@@ -1202,13 +1225,11 @@ def multiple_morphs(parser, opts, pargs, stdout_flag=True, python_wrap=False):
12021225
opts.slocation = Path(save_morphs_here).joinpath(save_as)
12031226
# Perform a morph of morph_file against target_file
12041227
pargs = [morph_file, target_file]
1205-
morph_results.update(
1206-
{
1207-
morph_file.name: single_morph(
1208-
parser, opts, pargs, stdout_flag=False
1209-
),
1210-
}
1228+
morph_result, unc = single_morph(
1229+
parser, opts, pargs, stdout_flag=False
12111230
)
1231+
morph_results.update({morph_file.name: morph_result})
1232+
uncs.update({morph_file.name: unc})
12121233

12131234
morph_file_names = []
12141235
for key in morph_results.keys():
@@ -1230,6 +1251,7 @@ def multiple_morphs(parser, opts, pargs, stdout_flag=True, python_wrap=False):
12301251
morph_inputs,
12311252
morph_results,
12321253
morph_file_names,
1254+
uncertainties_dict=uncs,
12331255
save_directory=save_directory,
12341256
morph_file=target_file,
12351257
target_directory=morph_directory,

src/diffpy/morph/refine.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -204,12 +204,22 @@ def refine(self, *args, estimate_uncertainty=False, **kw):
204204
self._update_chain(vals)
205205

206206
if estimate_uncertainty:
207+
par_names = list(self.pars)
207208
if hess_inv_sol is None:
208-
warnings.warn(
209-
"Warning: Could not estimate "
210-
"uncertainty as estimated Hessian is singular.",
211-
UserWarning,
212-
)
209+
if len(par_names) == 1:
210+
warnings.warn(
211+
"Warning: Could not estimate "
212+
f"uncertainty of {par_names[0].lower()} parameter "
213+
"as estimated Hessian is singular.",
214+
UserWarning,
215+
)
216+
else:
217+
warnings.warn(
218+
"Warning: Certain parameters may be "
219+
"missing uncertainties as estimated Hessian "
220+
"is singular.",
221+
UserWarning,
222+
)
213223
return None
214224
dof = len(fvec) - len(sol)
215225
if dof <= 0:
@@ -220,7 +230,23 @@ def refine(self, *args, estimate_uncertainty=False, **kw):
220230
UserWarning,
221231
)
222232
return None
223-
par_names = list(self.pars)
233+
# Handle squeeze morph params
234+
if "squeeze" in par_names and "squeeze" in config.keys():
235+
squeeze_par_names = [
236+
f"squeeze a{i}"
237+
for i in range(len(config["squeeze"].keys()))
238+
]
239+
squeeze_idx = par_names.index("squeeze")
240+
par_names[squeeze_idx : squeeze_idx + 1] = squeeze_par_names
241+
# Handle funcx, funcy, funcxy morph params
242+
func_types = ["funcx", "funcy", "funcxy"]
243+
for func in func_types:
244+
if func in par_names and func in config.keys():
245+
func_par_names = [
246+
f"{func} {fparam}" for fparam in config[func].keys()
247+
]
248+
func_idx = par_names.index(func)
249+
par_names[func_idx : func_idx + 1] = func_par_names
224250
cov_sol = hess_inv_sol * dot(fvec, fvec) / dof
225251
unc = sqrt(diag(cov_sol))
226252

0 commit comments

Comments
 (0)