Skip to content

Commit db89596

Browse files
authored
Merge branch 'main' into no-empty-object
2 parents e3c5208 + 75e0ef8 commit db89596

File tree

5 files changed

+273
-80
lines changed

5 files changed

+273
-80
lines changed

news/scaleto.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* functionality to rescale diffraction objects, placing one on top of another at a specified point
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/utils/diffraction_objects.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -384,40 +384,46 @@ def on_tth(self):
384384
def on_d(self):
385385
return [self.all_arrays[:, 3], self.all_arrays[:, 0]]
386386

387-
def scale_to(self, target_diff_object, xtype=None, xvalue=None):
387+
def scale_to(self, target_diff_object, q=None, tth=None, d=None, offset=0):
388388
"""
389-
Return a new diffraction object which is the current object but recaled in y to the target
389+
returns a new diffraction object which is the current object but rescaled in y to the target
390+
391+
The y-value in the target at the closest specified x-value will be used as the factor to scale to.
392+
The entire array is scaled by this factor so that one object places on top of the other at that point.
393+
If multiple values of `q`, `tth`, or `d` are provided, or none are provided, an error will be raised.
390394
391395
Parameters
392396
----------
393397
target_diff_object: DiffractionObject
394-
the diffraction object you want to scale the current one on to
395-
xtype: string, optional. Default is Q
396-
the xtype, from {XQUANTITIES}, that you will specify a point from to scale to
397-
xvalue: float. Default is the midpoint of the array
398-
the y-value in the target at this x-value will be used as the factor to scale to.
399-
The entire array is scaled be the factor that places on on top of the other at that point.
400-
xvalue does not have to be in the x-array, the point closest to this point will be used for the scaling.
398+
the diffraction object you want to scale the current one onto
399+
400+
q, tth, d : float, optional, must specify exactly one of them
401+
The value of the x-array where you want the curves to line up vertically.
402+
Specify a value on one of the allowed grids, q, tth, or d), e.g., q=10.
403+
404+
offset : float, optional, default is 0
405+
an offset to add to the scaled y-values
401406
402407
Returns
403408
-------
404409
the rescaled DiffractionObject as a new object
405-
406410
"""
407-
scaled = deepcopy(self)
408-
if xtype is None:
409-
xtype = "q"
411+
scaled = self.copy()
412+
count = sum([q is not None, tth is not None, d is not None])
413+
if count != 1:
414+
raise ValueError(
415+
"You must specify exactly one of 'q', 'tth', or 'd'. Please rerun specifying only one."
416+
)
410417

418+
xtype = "q" if q is not None else "tth" if tth is not None else "d"
411419
data = self.on_xtype(xtype)
412420
target = target_diff_object.on_xtype(xtype)
413-
if xvalue is None:
414-
xvalue = data[0][0] + (data[0][-1] - data[0][0]) / 2.0
415-
416-
xindex = (np.abs(data[0] - xvalue)).argmin()
417-
ytarget = target[1][xindex]
418-
yself = data[1][xindex]
419-
scaled.on_tth[1] = data[1] * ytarget / yself
420-
scaled.on_q[1] = data[1] * ytarget / yself
421+
422+
xvalue = q if xtype == "q" else tth if xtype == "tth" else d
423+
424+
xindex_data = (np.abs(data[0] - xvalue)).argmin()
425+
xindex_target = (np.abs(target[0] - xvalue)).argmin()
426+
scaled._all_arrays[:, 0] = data[1] * target[1][xindex_target] / data[1][xindex_data] + offset
421427
return scaled
422428

423429
def on_xtype(self, xtype):

src/diffpy/utils/transforms.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@
44
import numpy as np
55

66
wavelength_warning_emsg = (
7-
"INFO: no wavelength has been specified. You can continue "
8-
"to use the DiffractionObject but some of its powerful features "
9-
"will not be available. To specify a wavelength, set "
10-
"diffraction_object.wavelength = [number], "
11-
"where diffraction_object is the variable name of you Diffraction Object, "
12-
"and number is the wavelength in angstroms."
7+
"No wavelength has been specified. You can continue to use the DiffractionObject, but "
8+
"some of its powerful features will not be available. "
9+
"To specify a wavelength, if you have do = DiffractionObject(xarray, yarray, 'tth'), "
10+
"you may set do.wavelength = 1.54 for a wavelength of 1.54 angstroms."
1311
)
1412
invalid_tth_emsg = "Two theta exceeds 180 degrees. Please check the input values for errors."
1513
invalid_q_or_d_or_wavelength_emsg = (

tests/test_diffraction_objects.py

Lines changed: 172 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,11 @@ def test_diffraction_objects_equality(inputs1, inputs2, expected):
159159

160160

161161
def test_on_xtype():
162-
test = DiffractionObject(wavelength=2 * np.pi, xarray=np.array([30, 60]), yarray=np.array([1, 2]), xtype="tth")
163-
assert np.allclose(test.on_xtype("tth"), [np.array([30, 60]), np.array([1, 2])])
164-
assert np.allclose(test.on_xtype("2theta"), [np.array([30, 60]), np.array([1, 2])])
165-
assert np.allclose(test.on_xtype("q"), [np.array([0.51764, 1]), np.array([1, 2])])
166-
assert np.allclose(test.on_xtype("d"), [np.array([12.13818, 6.28319]), np.array([1, 2])])
162+
do = DiffractionObject(wavelength=2 * np.pi, xarray=np.array([30, 60]), yarray=np.array([1, 2]), xtype="tth")
163+
assert np.allclose(do.on_xtype("tth"), [np.array([30, 60]), np.array([1, 2])])
164+
assert np.allclose(do.on_xtype("2theta"), [np.array([30, 60]), np.array([1, 2])])
165+
assert np.allclose(do.on_xtype("q"), [np.array([0.51764, 1]), np.array([1, 2])])
166+
assert np.allclose(do.on_xtype("d"), [np.array([12.13818, 6.28319]), np.array([1, 2])])
167167

168168

169169
def test_init_invalid_xtype(do_minimal):
@@ -177,36 +177,188 @@ def test_init_invalid_xtype(do_minimal):
177177
return DiffractionObject(xarray=np.empty(0), yarray=np.empty(0), xtype="invalid_type", wavelength=1.54)
178178

179179

180+
params_scale_to = [
181+
# UC1: same x-array and y-array, check offset
182+
(
183+
{
184+
"xarray": np.array([10, 15, 25, 30, 60, 140]),
185+
"yarray": np.array([2, 3, 4, 5, 6, 7]),
186+
"xtype": "tth",
187+
"wavelength": 2 * np.pi,
188+
"target_xarray": np.array([10, 15, 25, 30, 60, 140]),
189+
"target_yarray": np.array([2, 3, 4, 5, 6, 7]),
190+
"target_xtype": "tth",
191+
"target_wavelength": 2 * np.pi,
192+
"q": None,
193+
"tth": 60,
194+
"d": None,
195+
"offset": 2.1,
196+
},
197+
{"xtype": "tth", "yarray": np.array([4.1, 5.1, 6.1, 7.1, 8.1, 9.1])},
198+
),
199+
# UC2: same length x-arrays with exact x-value match
200+
(
201+
{
202+
"xarray": np.array([10, 15, 25, 30, 60, 140]),
203+
"yarray": np.array([10, 20, 25, 30, 60, 100]),
204+
"xtype": "tth",
205+
"wavelength": 2 * np.pi,
206+
"target_xarray": np.array([10, 20, 25, 30, 60, 140]),
207+
"target_yarray": np.array([2, 3, 4, 5, 6, 7]),
208+
"target_xtype": "tth",
209+
"target_wavelength": 2 * np.pi,
210+
"q": None,
211+
"tth": 60,
212+
"d": None,
213+
"offset": 0,
214+
},
215+
{"xtype": "tth", "yarray": np.array([1, 2, 2.5, 3, 6, 10])},
216+
),
217+
# UC3: same length x-arrays with approximate x-value match
218+
(
219+
{
220+
"xarray": np.array([0.12, 0.24, 0.31, 0.4]),
221+
"yarray": np.array([10, 20, 40, 60]),
222+
"xtype": "q",
223+
"wavelength": 2 * np.pi,
224+
"target_xarray": np.array([0.14, 0.24, 0.31, 0.4]),
225+
"target_yarray": np.array([1, 3, 4, 5]),
226+
"target_xtype": "q",
227+
"target_wavelength": 2 * np.pi,
228+
"q": 0.1,
229+
"tth": None,
230+
"d": None,
231+
"offset": 0,
232+
},
233+
{"xtype": "q", "yarray": np.array([1, 2, 4, 6])},
234+
),
235+
# UC4: different x-array lengths with approximate x-value match
236+
(
237+
{
238+
"xarray": np.array([10, 25, 30.1, 40.2, 61, 120, 140]),
239+
"yarray": np.array([10, 20, 30, 40, 50, 60, 100]),
240+
"xtype": "tth",
241+
"wavelength": 2 * np.pi,
242+
"target_xarray": np.array([20, 25.5, 32, 45, 50, 62, 100, 125, 140]),
243+
"target_yarray": np.array([1.1, 2, 3, 3.5, 4, 5, 10, 12, 13]),
244+
"target_xtype": "tth",
245+
"target_wavelength": 2 * np.pi,
246+
"q": None,
247+
"tth": 60,
248+
"d": None,
249+
"offset": 0,
250+
},
251+
# scaling factor is calculated at index = 4 (tth=61) for self and index = 5 for target (tth=62)
252+
{"xtype": "tth", "yarray": np.array([1, 2, 3, 4, 5, 6, 10])},
253+
),
254+
]
255+
256+
257+
@pytest.mark.parametrize("inputs, expected", params_scale_to)
258+
def test_scale_to(inputs, expected):
259+
orig_diff_object = DiffractionObject(
260+
xarray=inputs["xarray"], yarray=inputs["yarray"], xtype=inputs["xtype"], wavelength=inputs["wavelength"]
261+
)
262+
target_diff_object = DiffractionObject(
263+
xarray=inputs["target_xarray"],
264+
yarray=inputs["target_yarray"],
265+
xtype=inputs["target_xtype"],
266+
wavelength=inputs["target_wavelength"],
267+
)
268+
scaled_diff_object = orig_diff_object.scale_to(
269+
target_diff_object, q=inputs["q"], tth=inputs["tth"], d=inputs["d"], offset=inputs["offset"]
270+
)
271+
# Check the intensity data is the same as expected
272+
assert np.allclose(scaled_diff_object.on_xtype(expected["xtype"])[1], expected["yarray"])
273+
274+
275+
params_scale_to_bad = [
276+
# UC1: user did not specify anything
277+
(
278+
{
279+
"xarray": np.array([0.1, 0.2, 0.3]),
280+
"yarray": np.array([1, 2, 3]),
281+
"xtype": "q",
282+
"wavelength": 2 * np.pi,
283+
"target_xarray": np.array([0.05, 0.1, 0.2, 0.3]),
284+
"target_yarray": np.array([5, 10, 20, 30]),
285+
"target_xtype": "q",
286+
"target_wavelength": 2 * np.pi,
287+
"q": None,
288+
"tth": None,
289+
"d": None,
290+
"offset": 0,
291+
}
292+
),
293+
# UC2: user specified more than one of q, tth, and d
294+
(
295+
{
296+
"xarray": np.array([10, 25, 30.1, 40.2, 61, 120, 140]),
297+
"yarray": np.array([10, 20, 30, 40, 50, 60, 100]),
298+
"xtype": "tth",
299+
"wavelength": 2 * np.pi,
300+
"target_xarray": np.array([20, 25.5, 32, 45, 50, 62, 100, 125, 140]),
301+
"target_yarray": np.array([1.1, 2, 3, 3.5, 4, 5, 10, 12, 13]),
302+
"target_xtype": "tth",
303+
"target_wavelength": 2 * np.pi,
304+
"q": None,
305+
"tth": 60,
306+
"d": 10,
307+
"offset": 0,
308+
}
309+
),
310+
]
311+
312+
313+
@pytest.mark.parametrize("inputs", params_scale_to_bad)
314+
def test_scale_to_bad(inputs):
315+
orig_diff_object = DiffractionObject(
316+
xarray=inputs["xarray"], yarray=inputs["yarray"], xtype=inputs["xtype"], wavelength=inputs["wavelength"]
317+
)
318+
target_diff_object = DiffractionObject(
319+
xarray=inputs["target_xarray"],
320+
yarray=inputs["target_yarray"],
321+
xtype=inputs["target_xtype"],
322+
wavelength=inputs["target_wavelength"],
323+
)
324+
with pytest.raises(
325+
ValueError, match="You must specify exactly one of 'q', 'tth', or 'd'. Please rerun specifying only one."
326+
):
327+
orig_diff_object.scale_to(
328+
target_diff_object, q=inputs["q"], tth=inputs["tth"], d=inputs["d"], offset=inputs["offset"]
329+
)
330+
331+
180332
params_index = [
181333
# UC1: exact match
182-
([4 * np.pi, np.array([30.005, 60]), np.array([1, 2]), "tth", "tth", 30.005], [0]),
334+
(4 * np.pi, np.array([30.005, 60]), np.array([1, 2]), "tth", "tth", 30.005, [0]),
183335
# UC2: target value lies in the array, returns the (first) closest index
184-
([4 * np.pi, np.array([30, 60]), np.array([1, 2]), "tth", "tth", 45], [0]),
185-
([4 * np.pi, np.array([30, 60]), np.array([1, 2]), "tth", "q", 0.25], [0]),
336+
(4 * np.pi, np.array([30, 60]), np.array([1, 2]), "tth", "tth", 45, [0]),
337+
(4 * np.pi, np.array([30, 60]), np.array([1, 2]), "tth", "q", 0.25, [0]),
186338
# UC3: target value out of the range, returns the closest index
187-
([4 * np.pi, np.array([0.25, 0.5, 0.71]), np.array([1, 2, 3]), "q", "q", 0.1], [0]),
188-
([4 * np.pi, np.array([30, 60]), np.array([1, 2]), "tth", "tth", 63], [1]),
339+
(4 * np.pi, np.array([0.25, 0.5, 0.71]), np.array([1, 2, 3]), "q", "q", 0.1, [0]),
340+
(4 * np.pi, np.array([30, 60]), np.array([1, 2]), "tth", "tth", 63, [1]),
189341
]
190342

191343

192-
@pytest.mark.parametrize("inputs, expected", params_index)
193-
def test_get_array_index(inputs, expected):
194-
test = DiffractionObject(wavelength=inputs[0], xarray=inputs[1], yarray=inputs[2], xtype=inputs[3])
195-
actual = test.get_array_index(value=inputs[5], xtype=inputs[4])
196-
assert actual == expected[0]
344+
@pytest.mark.parametrize("wavelength, xarray, yarray, xtype_1, xtype_2, value, expected_index", params_index)
345+
def test_get_array_index(wavelength, xarray, yarray, xtype_1, xtype_2, value, expected_index):
346+
do = DiffractionObject(wavelength=wavelength, xarray=xarray, yarray=yarray, xtype=xtype_1)
347+
actual_index = do.get_array_index(value=value, xtype=xtype_2)
348+
assert actual_index == expected_index
197349

198350

199351
def test_get_array_index_bad():
200-
test = DiffractionObject(wavelength=2 * np.pi, xarray=np.array([]), yarray=np.array([]), xtype="tth")
352+
do = DiffractionObject(wavelength=2 * np.pi, xarray=np.array([]), yarray=np.array([]), xtype="tth")
201353
with pytest.raises(ValueError, match=re.escape("The 'tth' array is empty. Please ensure it is initialized.")):
202-
test.get_array_index(value=30)
354+
do.get_array_index(value=30)
203355

204356

205357
def test_dump(tmp_path, mocker):
206358
x, y = np.linspace(0, 5, 6), np.linspace(0, 5, 6)
207359
directory = Path(tmp_path)
208360
file = directory / "testfile"
209-
test = DiffractionObject(
361+
do = DiffractionObject(
210362
wavelength=1.54,
211363
name="test",
212364
scat_quantity="x-ray",
@@ -217,7 +369,7 @@ def test_dump(tmp_path, mocker):
217369
)
218370
mocker.patch("importlib.metadata.version", return_value="3.3.0")
219371
with freeze_time("2012-01-14"):
220-
test.dump(file, "q")
372+
do.dump(file, "q")
221373
with open(file, "r") as f:
222374
actual = f.read()
223375
expected = (
@@ -322,14 +474,13 @@ def test_all_array_getter():
322474

323475
def test_all_array_setter(do_minimal):
324476
actual_do = do_minimal
325-
326477
# Attempt to directly modify the property
327478
with pytest.raises(
328479
AttributeError,
329480
match="Direct modification of attribute 'all_arrays' is not allowed. "
330481
"Please use 'input_data' to modify 'all_arrays'.",
331482
):
332-
actual_do.all_arrays = np.empty((4, 4))
483+
do.all_arrays = np.empty((4, 4))
333484

334485

335486
def test_id_getter(do_minimal):

0 commit comments

Comments
 (0)