Skip to content

Commit 87f1bff

Browse files
authored
Merge pull request #190 from Luiskitsu/refinement
Enable refinement of lists and dictionary input parameters
2 parents a450cd7 + a991072 commit 87f1bff

File tree

9 files changed

+154
-26
lines changed

9 files changed

+154
-26
lines changed

news/dict_lists_refinement.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* Functionality for refining lists and dictionaries
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_api.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,17 @@
3939
morph_helpers.TransformXtalRDFtoPDF,
4040
],
4141
qdamp=morphs.MorphResolutionDamping,
42+
squeeze=morphs.MorphSqueeze,
43+
funcy=morphs.MorphFuncy,
4244
)
4345
_default_config = dict(
44-
scale=None, stretch=None, smear=None, baselineslope=None, qdamp=None
46+
scale=None,
47+
stretch=None,
48+
smear=None,
49+
baselineslope=None,
50+
qdamp=None,
51+
squeeze=None,
52+
funcy=None,
4553
)
4654

4755

@@ -135,6 +143,8 @@ def morph(
135143
- 'smear'
136144
- 'baselineslope'
137145
- 'qdamp'
146+
- 'squeeze'
147+
- 'funcy'
138148
139149
Returns
140150
-------
@@ -197,6 +207,14 @@ def morph(
197207
if k == "smear":
198208
[chain.append(el()) for el in morph_cls]
199209
refpars.append("baselineslope")
210+
elif k == "funcy":
211+
morph_inst = morph_cls()
212+
morph_inst.function = rv_cfg.get("function", None)
213+
if morph_inst.function is None:
214+
raise ValueError(
215+
"Must provide a 'function' when using 'parameters'"
216+
)
217+
chain.append(morph_inst)
200218
else:
201219
chain.append(morph_cls())
202220
refpars.append(k)

src/diffpy/morph/morphs/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919

2020
from diffpy.morph.morphs.morph import Morph # noqa: F401
2121
from diffpy.morph.morphs.morphchain import MorphChain # noqa: F401
22+
from diffpy.morph.morphs.morphfuncy import MorphFuncy
2223
from diffpy.morph.morphs.morphishape import MorphISphere, MorphISpheroid
2324
from diffpy.morph.morphs.morphresolution import MorphResolutionDamping
2425
from diffpy.morph.morphs.morphrgrid import MorphRGrid
2526
from diffpy.morph.morphs.morphscale import MorphScale
2627
from diffpy.morph.morphs.morphshape import MorphSphere, MorphSpheroid
2728
from diffpy.morph.morphs.morphshift import MorphShift
2829
from diffpy.morph.morphs.morphsmear import MorphSmear
30+
from diffpy.morph.morphs.morphsqueeze import MorphSqueeze
2931
from diffpy.morph.morphs.morphstretch import MorphStretch
3032

3133
# List of morphs
@@ -40,6 +42,8 @@
4042
MorphISpheroid,
4143
MorphResolutionDamping,
4244
MorphShift,
45+
MorphSqueeze,
46+
MorphFuncy,
4347
]
4448

4549
# End of file

src/diffpy/morph/morphs/morphfuncy.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class MorphFuncy(Morph):
1111
yinlabel = LABEL_GR
1212
xoutlabel = LABEL_RA
1313
youtlabel = LABEL_GR
14+
parnames = ["funcy"]
1415

1516
def morph(self, x_morph, y_morph, x_target, y_target):
1617
"""General morph function that applies a user-supplied function to the
@@ -49,7 +50,7 @@ def morph(self, x_morph, y_morph, x_target, y_target):
4950
and target array (x_target, y_target):
5051
>>> morph = MorphFuncy()
5152
>>> morph.function = sine_function
52-
>>> morph.parameters = parameters
53+
>>> morph.funcy = parameters
5354
>>> x_morph_out, y_morph_out, x_target_out, y_target_out = morph.morph(
5455
... x_morph, y_morph, x_target, y_target)
5556
@@ -58,11 +59,11 @@ def morph(self, x_morph, y_morph, x_target, y_target):
5859
>>> y_morph_in = morph.y_morph_in
5960
>>> x_target_in = morph.x_target_in
6061
>>> y_target_in = morph.y_target_in
61-
>>> parameters_out = morph.parameters
62+
>>> parameters_out = morph.funcy
6263
"""
6364
Morph.morph(self, x_morph, y_morph, x_target, y_target)
6465

6566
self.y_morph_out = self.function(
66-
self.x_morph_in, self.y_morph_in, **self.parameters
67+
self.x_morph_in, self.y_morph_in, **self.funcy
6768
)
6869
return self.xyallout

src/diffpy/morph/morphs/morphsqueeze.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ def morph(self, x_morph, y_morph, x_target, y_target):
2828
2929
Configuration Variables
3030
-----------------------
31-
squeeze : list
32-
The polynomial coefficients [a0, a1, ..., an] for the squeeze
31+
squeeze : Dictionary
32+
The polynomial coefficients {a0, a1, ..., an} for the squeeze
3333
function where the polynomial would be of the form
3434
a0 + a1*x + a2*x^2 and so on. The order of the polynomial is
35-
determined by the length of the list.
35+
determined by the length of the dictionary.
3636
3737
Returns
3838
-------
@@ -46,7 +46,7 @@ def morph(self, x_morph, y_morph, x_target, y_target):
4646
Import the squeeze morph function:
4747
>>> from diffpy.morph.morphs.morphsqueeze import MorphSqueeze
4848
Provide initial guess for squeezing coefficients:
49-
>>> squeeze_coeff = [0.1, -0.01, 0.005]
49+
>>> squeeze_coeff = {"a0":0.1, "a1":-0.01, "a2":0.005}
5050
Run the squeeze morph given input morph array (x_morph, y_morph)
5151
and target array (x_target, y_target):
5252
>>> morph = MorphSqueeze()
@@ -62,7 +62,8 @@ def morph(self, x_morph, y_morph, x_target, y_target):
6262
"""
6363
Morph.morph(self, x_morph, y_morph, x_target, y_target)
6464

65-
squeeze_polynomial = Polynomial(self.squeeze)
65+
coeffs = [self.squeeze[f"a{i}"] for i in range(len(self.squeeze))]
66+
squeeze_polynomial = Polynomial(coeffs)
6667
x_squeezed = self.x_morph_in + squeeze_polynomial(self.x_morph_in)
6768
self.y_morph_out = CubicSpline(x_squeezed, self.y_morph_in)(
6869
self.x_morph_in

src/diffpy/morph/refine.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,23 @@ def __init__(self, chain, x_morph, y_morph, x_target, y_target):
5151
self.y_target = y_target
5252
self.pars = []
5353
self.residual = self._residual
54+
self.flat_to_grouped = {}
5455
return
5556

5657
def _update_chain(self, pvals):
5758
"""Update the parameters in the chain."""
58-
pairs = zip(self.pars, pvals)
59-
self.chain.config.update(pairs)
59+
updated = {}
60+
for idx, value in enumerate(pvals):
61+
param, subkey = self.flat_to_grouped[idx]
62+
if subkey is None: # Scalar
63+
updated[param] = value
64+
else:
65+
if param not in updated:
66+
updated[param] = {}
67+
updated[param][subkey] = value
68+
69+
# Apply the reconstructed grouped parameter back to config
70+
self.chain.config.update(updated)
6071
return
6172

6273
def _residual(self, pvals):
@@ -118,11 +129,25 @@ def refine(self, *args, **kw):
118129
if not self.pars:
119130
return 0.0
120131

121-
initial = [config[p] for p in self.pars]
132+
# Build flat list of initial parameters and flat_to_grouped mapping
133+
initial = []
134+
self.flat_to_grouped = {}
135+
136+
for p in self.pars:
137+
val = config[p]
138+
if isinstance(val, dict):
139+
for k, v in val.items():
140+
initial.append(v)
141+
self.flat_to_grouped[len(initial) - 1] = (p, k)
142+
else:
143+
initial.append(val)
144+
self.flat_to_grouped[len(initial) - 1] = (p, None)
145+
122146
sol, cov_sol, infodict, emesg, ier = leastsq(
123147
self.residual, initial, full_output=1
124148
)
125149
fvec = infodict["fvec"]
150+
126151
if ier not in (1, 2, 3, 4):
127152
emesg
128153
raise ValueError(emesg)
@@ -131,7 +156,7 @@ def refine(self, *args, **kw):
131156
vals = sol
132157
if not hasattr(vals, "__iter__"):
133158
vals = [vals]
134-
self.chain.config.update(zip(self.pars, vals))
159+
self._update_chain(vals)
135160

136161
return dot(fvec, fvec)
137162

tests/test_morph_func.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,58 @@ def test_smear_with_morph_func():
101101
assert np.allclose(y0, y1, atol=1e-3) # numerical error -> 1e-4
102102
# verify morphed param
103103
assert np.allclose(smear, morphed_cfg["smear"], atol=1e-1)
104+
105+
106+
def test_squeeze_with_morph_func():
107+
squeeze_init = {"a0": 0, "a1": -0.001, "a2": -0.0001, "a3": 0.0001}
108+
x_morph = np.linspace(0, 10, 101)
109+
y_morph = 2 * np.sin(
110+
x_morph + x_morph * 0.01 + 0.0001 * x_morph**2 + 0.001 * x_morph**3
111+
)
112+
expected_squeeze = {"a0": 0, "a1": 0.01, "a2": 0.0001, "a3": 0.001}
113+
expected_scale = 1 / 2
114+
x_target = np.linspace(0, 10, 101)
115+
y_target = np.sin(x_target)
116+
cfg = morph_default_config(scale=1.1, squeeze=squeeze_init)
117+
morph_rv = morph(x_morph, y_morph, x_target, y_target, **cfg)
118+
morphed_cfg = morph_rv["morphed_config"]
119+
x_morph_out, y_morph_out, x_target_out, y_target_out = morph_rv[
120+
"morph_chain"
121+
].xyallout
122+
assert np.allclose(x_morph_out, x_target_out)
123+
assert np.allclose(y_morph_out, y_target_out, atol=1e-6)
124+
assert np.allclose(
125+
expected_squeeze["a0"], morphed_cfg["squeeze"]["a0"], atol=1e-6
126+
)
127+
assert np.allclose(
128+
expected_squeeze["a1"], morphed_cfg["squeeze"]["a1"], atol=1e-6
129+
)
130+
assert np.allclose(
131+
expected_squeeze["a2"], morphed_cfg["squeeze"]["a2"], atol=1e-6
132+
)
133+
assert np.allclose(
134+
expected_squeeze["a3"], morphed_cfg["squeeze"]["a3"], atol=1e-6
135+
)
136+
assert np.allclose(expected_scale, morphed_cfg["scale"], atol=1e-6)
137+
138+
139+
def test_funcy_with_morph_func():
140+
def linear_function(x, y, scale, offset):
141+
return (scale * x) * y + offset
142+
143+
x_morph = np.linspace(0, 10, 101)
144+
y_morph = np.sin(x_morph)
145+
x_target = x_morph.copy()
146+
y_target = np.sin(x_target) * 2 * x_target + 0.4
147+
cfg = morph_default_config(funcy={"scale": 1.2, "offset": 0.1})
148+
cfg["function"] = linear_function
149+
morph_rv = morph(x_morph, y_morph, x_target, y_target, **cfg)
150+
morphed_cfg = morph_rv["morphed_config"]
151+
x_morph_out, y_morph_out, x_target_out, y_target_out = morph_rv[
152+
"morph_chain"
153+
].xyallout
154+
assert np.allclose(x_morph_out, x_target_out)
155+
assert np.allclose(y_morph_out, y_target_out, atol=1e-6)
156+
fitted_parameters = morphed_cfg["funcy"]
157+
assert np.allclose(fitted_parameters["scale"], 2, atol=1e-6)
158+
assert np.allclose(fitted_parameters["offset"], 0.4, atol=1e-6)

tests/test_morphfuncy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def test_funcy(function, parameters, expected_function):
6363
y_morph_expected = expected_function(x_morph, y_morph)
6464
morph = MorphFuncy()
6565
morph.function = function
66-
morph.parameters = parameters
66+
morph.funcy = parameters
6767
x_morph_actual, y_morph_actual, x_target_actual, y_target_actual = (
6868
morph.morph(x_morph, y_morph, x_target, y_target)
6969
)

tests/test_morphsqueeze.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,24 @@
44

55
from diffpy.morph.morphs.morphsqueeze import MorphSqueeze
66

7-
squeeze_coeffs_list = [
8-
# The order of coefficients is [a0, a1, a2, ..., an]
7+
squeeze_coeffs_dic = [
8+
# The order of coefficients is {a0, a1, a2, ..., an}
99
# Negative cubic squeeze coefficients
10-
[-0.01, -0.0005, -0.0005, -1e-6],
10+
{"a0": -0.01, "a1": -0.0005, "a2": -0.0005, "a3": -1e-6},
1111
# Positive cubic squeeze coefficients
12-
[0.2, 0.01, 0.001, 0.0001],
12+
{"a0": 0.2, "a1": 0.01, "a2": 0.001, "a3": 0.0001},
1313
# Positive and negative cubic squeeze coefficients
14-
[0.2, -0.01, 0.002, -0.0001],
14+
{"a0": 0.2, "a1": -0.01, "a2": 0.002, "a3": -0.0001},
1515
# Quadratic squeeze coefficients
16-
[-0.2, 0.005, -0.0004],
16+
{"a0": -0.2, "a1": 0.005, "a2": -0.0004},
1717
# Linear squeeze coefficients
18-
[0.1, 0.3],
18+
{"a0": 0.1, "a1": 0.3},
1919
# 4th order squeeze coefficients
20-
[0.2, -0.01, 0.001, -0.001, 0.0001],
20+
{"a0": 0.2, "a1": -0.01, "a2": 0.001, "a3": -0.001, "a4": 0.0001},
2121
# Zeros and non-zeros, the full polynomial is applied
22-
[0, 0.03, 0, -0.0001],
22+
{"a0": 0, "a1": 0.03, "a2": 0, "a3": -0.0001},
2323
# Testing zeros, expect no squeezing
24-
[0, 0, 0, 0, 0, 0],
24+
{"a0": 0, "a1": 0, "a2": 0, "a3": 0, "a4": 0, "a5": 0},
2525
]
2626
morph_target_grids = [
2727
# UCs from issue 181: https://github.com/diffpy/diffpy.morph/issues/181
@@ -41,10 +41,11 @@
4141

4242

4343
@pytest.mark.parametrize("x_morph, x_target", morph_target_grids)
44-
@pytest.mark.parametrize("squeeze_coeffs", squeeze_coeffs_list)
44+
@pytest.mark.parametrize("squeeze_coeffs", squeeze_coeffs_dic)
4545
def test_morphsqueeze(x_morph, x_target, squeeze_coeffs):
4646
y_target = np.sin(x_target)
47-
squeeze_polynomial = Polynomial(squeeze_coeffs)
47+
coeffs = [squeeze_coeffs[f"a{i}"] for i in range(len(squeeze_coeffs))]
48+
squeeze_polynomial = Polynomial(coeffs)
4849
x_squeezed = x_morph + squeeze_polynomial(x_morph)
4950
y_morph = np.sin(x_squeezed)
5051
low_extrap = np.where(x_morph < x_squeezed[0])[0]

0 commit comments

Comments
 (0)