diff --git a/news/scaleto.rst b/news/scaleto.rst new file mode 100644 index 00000000..6f6b0635 --- /dev/null +++ b/news/scaleto.rst @@ -0,0 +1,23 @@ +**Added:** + +* functionality to rescale diffraction objects, placing one on top of another at a specified point + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/utils/diffraction_objects.py b/src/diffpy/utils/diffraction_objects.py index 59aac37b..7c8e3aee 100644 --- a/src/diffpy/utils/diffraction_objects.py +++ b/src/diffpy/utils/diffraction_objects.py @@ -391,40 +391,46 @@ def on_tth(self): def on_d(self): return [self.all_arrays[:, 3], self.all_arrays[:, 0]] - def scale_to(self, target_diff_object, xtype=None, xvalue=None): + def scale_to(self, target_diff_object, q=None, tth=None, d=None, offset=0): """ - Return a new diffraction object which is the current object but recaled in y to the target + returns a new diffraction object which is the current object but rescaled in y to the target + + The y-value in the target at the closest specified x-value will be used as the factor to scale to. + The entire array is scaled by this factor so that one object places on top of the other at that point. + If multiple values of `q`, `tth`, or `d` are provided, or none are provided, an error will be raised. Parameters ---------- target_diff_object: DiffractionObject - the diffraction object you want to scale the current one on to - xtype: string, optional. Default is Q - the xtype, from {XQUANTITIES}, that you will specify a point from to scale to - xvalue: float. Default is the midpoint of the array - the y-value in the target at this x-value will be used as the factor to scale to. - The entire array is scaled be the factor that places on on top of the other at that point. - xvalue does not have to be in the x-array, the point closest to this point will be used for the scaling. + the diffraction object you want to scale the current one onto + + q, tth, d : float, optional, must specify exactly one of them + The value of the x-array where you want the curves to line up vertically. + Specify a value on one of the allowed grids, q, tth, or d), e.g., q=10. + + offset : float, optional, default is 0 + an offset to add to the scaled y-values Returns ------- the rescaled DiffractionObject as a new object - """ - scaled = deepcopy(self) - if xtype is None: - xtype = "q" + scaled = self.copy() + count = sum([q is not None, tth is not None, d is not None]) + if count != 1: + raise ValueError( + "You must specify exactly one of 'q', 'tth', or 'd'. Please rerun specifying only one." + ) + xtype = "q" if q is not None else "tth" if tth is not None else "d" data = self.on_xtype(xtype) target = target_diff_object.on_xtype(xtype) - if xvalue is None: - xvalue = data[0][0] + (data[0][-1] - data[0][0]) / 2.0 - - xindex = (np.abs(data[0] - xvalue)).argmin() - ytarget = target[1][xindex] - yself = data[1][xindex] - scaled.on_tth[1] = data[1] * ytarget / yself - scaled.on_q[1] = data[1] * ytarget / yself + + xvalue = q if xtype == "q" else tth if xtype == "tth" else d + + xindex_data = (np.abs(data[0] - xvalue)).argmin() + xindex_target = (np.abs(target[0] - xvalue)).argmin() + scaled._all_arrays[:, 0] = data[1] * target[1][xindex_target] / data[1][xindex_data] + offset return scaled def on_xtype(self, xtype): diff --git a/tests/test_diffraction_objects.py b/tests/test_diffraction_objects.py index 328bb512..8a2ed670 100644 --- a/tests/test_diffraction_objects.py +++ b/tests/test_diffraction_objects.py @@ -180,6 +180,158 @@ def test_init_invalid_xtype(): DiffractionObject(xtype="invalid_type") +params_scale_to = [ + # UC1: same x-array and y-array, check offset + ( + { + "xarray": np.array([10, 15, 25, 30, 60, 140]), + "yarray": np.array([2, 3, 4, 5, 6, 7]), + "xtype": "tth", + "wavelength": 2 * np.pi, + "target_xarray": np.array([10, 15, 25, 30, 60, 140]), + "target_yarray": np.array([2, 3, 4, 5, 6, 7]), + "target_xtype": "tth", + "target_wavelength": 2 * np.pi, + "q": None, + "tth": 60, + "d": None, + "offset": 2.1, + }, + {"xtype": "tth", "yarray": np.array([4.1, 5.1, 6.1, 7.1, 8.1, 9.1])}, + ), + # UC2: same length x-arrays with exact x-value match + ( + { + "xarray": np.array([10, 15, 25, 30, 60, 140]), + "yarray": np.array([10, 20, 25, 30, 60, 100]), + "xtype": "tth", + "wavelength": 2 * np.pi, + "target_xarray": np.array([10, 20, 25, 30, 60, 140]), + "target_yarray": np.array([2, 3, 4, 5, 6, 7]), + "target_xtype": "tth", + "target_wavelength": 2 * np.pi, + "q": None, + "tth": 60, + "d": None, + "offset": 0, + }, + {"xtype": "tth", "yarray": np.array([1, 2, 2.5, 3, 6, 10])}, + ), + # UC3: same length x-arrays with approximate x-value match + ( + { + "xarray": np.array([0.12, 0.24, 0.31, 0.4]), + "yarray": np.array([10, 20, 40, 60]), + "xtype": "q", + "wavelength": 2 * np.pi, + "target_xarray": np.array([0.14, 0.24, 0.31, 0.4]), + "target_yarray": np.array([1, 3, 4, 5]), + "target_xtype": "q", + "target_wavelength": 2 * np.pi, + "q": 0.1, + "tth": None, + "d": None, + "offset": 0, + }, + {"xtype": "q", "yarray": np.array([1, 2, 4, 6])}, + ), + # UC4: different x-array lengths with approximate x-value match + ( + { + "xarray": np.array([10, 25, 30.1, 40.2, 61, 120, 140]), + "yarray": np.array([10, 20, 30, 40, 50, 60, 100]), + "xtype": "tth", + "wavelength": 2 * np.pi, + "target_xarray": np.array([20, 25.5, 32, 45, 50, 62, 100, 125, 140]), + "target_yarray": np.array([1.1, 2, 3, 3.5, 4, 5, 10, 12, 13]), + "target_xtype": "tth", + "target_wavelength": 2 * np.pi, + "q": None, + "tth": 60, + "d": None, + "offset": 0, + }, + # scaling factor is calculated at index = 4 (tth=61) for self and index = 5 for target (tth=62) + {"xtype": "tth", "yarray": np.array([1, 2, 3, 4, 5, 6, 10])}, + ), +] + + +@pytest.mark.parametrize("inputs, expected", params_scale_to) +def test_scale_to(inputs, expected): + orig_diff_object = DiffractionObject( + xarray=inputs["xarray"], yarray=inputs["yarray"], xtype=inputs["xtype"], wavelength=inputs["wavelength"] + ) + target_diff_object = DiffractionObject( + xarray=inputs["target_xarray"], + yarray=inputs["target_yarray"], + xtype=inputs["target_xtype"], + wavelength=inputs["target_wavelength"], + ) + scaled_diff_object = orig_diff_object.scale_to( + target_diff_object, q=inputs["q"], tth=inputs["tth"], d=inputs["d"], offset=inputs["offset"] + ) + # Check the intensity data is the same as expected + assert np.allclose(scaled_diff_object.on_xtype(expected["xtype"])[1], expected["yarray"]) + + +params_scale_to_bad = [ + # UC1: user did not specify anything + ( + { + "xarray": np.array([0.1, 0.2, 0.3]), + "yarray": np.array([1, 2, 3]), + "xtype": "q", + "wavelength": 2 * np.pi, + "target_xarray": np.array([0.05, 0.1, 0.2, 0.3]), + "target_yarray": np.array([5, 10, 20, 30]), + "target_xtype": "q", + "target_wavelength": 2 * np.pi, + "q": None, + "tth": None, + "d": None, + "offset": 0, + } + ), + # UC2: user specified more than one of q, tth, and d + ( + { + "xarray": np.array([10, 25, 30.1, 40.2, 61, 120, 140]), + "yarray": np.array([10, 20, 30, 40, 50, 60, 100]), + "xtype": "tth", + "wavelength": 2 * np.pi, + "target_xarray": np.array([20, 25.5, 32, 45, 50, 62, 100, 125, 140]), + "target_yarray": np.array([1.1, 2, 3, 3.5, 4, 5, 10, 12, 13]), + "target_xtype": "tth", + "target_wavelength": 2 * np.pi, + "q": None, + "tth": 60, + "d": 10, + "offset": 0, + } + ), +] + + +@pytest.mark.parametrize("inputs", params_scale_to_bad) +def test_scale_to_bad(inputs): + orig_diff_object = DiffractionObject( + xarray=inputs["xarray"], yarray=inputs["yarray"], xtype=inputs["xtype"], wavelength=inputs["wavelength"] + ) + target_diff_object = DiffractionObject( + xarray=inputs["target_xarray"], + yarray=inputs["target_yarray"], + xtype=inputs["target_xtype"], + wavelength=inputs["target_wavelength"], + ) + with pytest.raises( + ValueError, match="You must specify exactly one of 'q', 'tth', or 'd'. Please rerun specifying only one." + ): + orig_diff_object.scale_to( + target_diff_object, q=inputs["q"], tth=inputs["tth"], d=inputs["d"], offset=inputs["offset"] + ) + + params_index = [ # UC1: exact match ([4 * np.pi, np.array([30.005, 60]), np.array([1, 2]), "tth", "tth", 30.005], [0]),