diff --git a/doc/source/examples/diffraction_objects_example.rst b/doc/source/examples/diffraction_objects_example.rst index 8621f50f..4494fcfc 100644 --- a/doc/source/examples/diffraction_objects_example.rst +++ b/doc/source/examples/diffraction_objects_example.rst @@ -104,7 +104,16 @@ we would replace the code above with plt.show() The ``scale_to()`` method returns a new ``DiffractionObject`` which we can assign to a new -variable and make use of, +variable and make use of. + +The default behavior is to align the objects based on the maximal value of each diffraction object. + +.. code-block:: python + + scaled_measured = measured.scale_to(calculated) + +If this doesn't give the desirable results, you can specify an ``xtype=value`` to scale +based on the closest x-value in both objects. For example: .. code-block:: python diff --git a/news/scaleto-max.rst b/news/scaleto-max.rst new file mode 100644 index 00000000..d70e9d7f --- /dev/null +++ b/news/scaleto-max.rst @@ -0,0 +1,23 @@ +**Added:** + +* new feature in `scale_to()`: default scaling is based on the max q-value in each diffraction object. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/utils/diffraction_objects.py b/src/diffpy/utils/diffraction_objects.py index 25e3e28c..1b32e1d7 100644 --- a/src/diffpy/utils/diffraction_objects.py +++ b/src/diffpy/utils/diffraction_objects.py @@ -396,37 +396,48 @@ def on_tth(self): def on_d(self): return [self.all_arrays[:, 3], self.all_arrays[:, 0]] - def scale_to(self, target_diff_object, q=None, tth=None, d=None, offset=0): + def scale_to(self, target_diff_object, q=None, tth=None, d=None, offset=None): """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. + By default, if `q`, `tth`, or `d` are not provided, scaling is based on the max intensity from each object. + Otherwise, 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. + If multiple values of `q`, `tth`, or `d` are provided, an error will be raised. Parameters ---------- target_diff_object: DiffractionObject - the diffraction object you want to scale the current one onto + The diffraction object you want to scale the current one onto. - q, tth, d : float, optional, must specify exactly one of them + q, tth, d : float, optional, default is None 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 + offset : float, optional, default is None + The offset to add to the scaled y-values. Returns ------- - the rescaled DiffractionObject as a new object + scaled : DiffractionObject + The rescaled DiffractionObject as a new object. """ + if offset is None: + offset = 0 scaled = self.copy() count = sum([q is not None, tth is not None, d is not None]) - if count != 1: + if count > 1: raise ValueError( - "You must specify exactly one of 'q', 'tth', or 'd'. Please rerun specifying only one." + "You must specify none or exactly one of 'q', 'tth', or 'd'. " + "Please provide either none or one value." ) + if count == 0: + q_target_max = max(target_diff_object.on_q()[1]) + q_self_max = max(self.on_q()[1]) + scaled._all_arrays[:, 0] = scaled._all_arrays[:, 0] * q_target_max / q_self_max + offset + return scaled + 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) diff --git a/tests/test_diffraction_objects.py b/tests/test_diffraction_objects.py index c6355882..9f0d9a0d 100644 --- a/tests/test_diffraction_objects.py +++ b/tests/test_diffraction_objects.py @@ -191,7 +191,23 @@ def test_init_invalid_xtype(): "org_do_args, target_do_args, scale_inputs, expected", [ # Test whether the original y-array is scaled as expected - ( # C1: Same x-arrays + ( # C1: none of q, tth, d, provided, expect to scale on the maximal intensity from each object + { + "xarray": np.array([0.1, 0.2, 0.3]), + "yarray": np.array([1, 2, 3]), + "xtype": "q", + "wavelength": 2 * np.pi, + }, + { + "xarray": np.array([0.05, 0.1, 0.2, 0.3]), + "yarray": np.array([5, 10, 20, 30]), + "xtype": "q", + "wavelength": 2 * np.pi, + }, + {}, + {"xtype": "q", "yarray": np.array([10, 20, 30])}, + ), + ( # C2: Same x-arrays # x-value has exact matches at tth=60 (y=60) and tth=60 (y=6), # for original and target diffraction objects, # expect original y-array to multiply by 6/60=1/10 @@ -207,15 +223,10 @@ def test_init_invalid_xtype(): "xtype": "tth", "wavelength": 2 * np.pi, }, - { - "q": None, - "tth": 60, - "d": None, - "offset": 0, - }, + {"tth": 60}, {"xtype": "tth", "yarray": np.array([1, 2, 2.5, 3, 6, 10])}, ), - ( # C2: Different x-arrays with same length, + ( # C3: Different x-arrays with same length, # x-value has closest match at q=0.12 (y=10) and q=0.14 (y=1) # for original and target diffraction objects, # expect original y-array to multiply by 1/10 @@ -231,15 +242,10 @@ def test_init_invalid_xtype(): "xtype": "q", "wavelength": 2 * np.pi, }, - { - "q": 0.1, - "tth": None, - "d": None, - "offset": 0, - }, + {"q": 0.1}, {"xtype": "q", "yarray": np.array([1, 2, 4, 6])}, ), - ( # C3: Different x-array lengths + ( # C4: Different x-array lengths # x-value has closest matches at tth=61 (y=50) and tth=62 (y=5), # for original and target diffraction objects, # expect original y-array to multiply by 5/50=1/10 @@ -255,43 +261,48 @@ def test_init_invalid_xtype(): "xtype": "tth", "wavelength": 2 * np.pi, }, - { - "q": None, - "tth": 60, - "d": None, - "offset": 0, - }, + {"tth": 60}, {"xtype": "tth", "yarray": np.array([1, 2, 3, 4, 5, 6, 10])}, ), - ( # C4: Same x-array and y-array with 2.1 offset, expect y-array to shift up by 2.1 + ( # C5.1: Reuse test case from C1, none of q, tth, d, provided, but include an offset, + # expect scaled y-array in C1 to shift up by 2 { - "xarray": np.array([10, 15, 25, 30, 60, 140]), - "yarray": np.array([2, 3, 4, 5, 6, 7]), - "xtype": "tth", + "xarray": np.array([0.1, 0.2, 0.3]), + "yarray": np.array([1, 2, 3]), + "xtype": "q", "wavelength": 2 * np.pi, }, { - "xarray": np.array([10, 15, 25, 30, 60, 140]), - "yarray": np.array([2, 3, 4, 5, 6, 7]), + "xarray": np.array([0.05, 0.1, 0.2, 0.3]), + "yarray": np.array([5, 10, 20, 30]), + "xtype": "q", + "wavelength": 2 * np.pi, + }, + {"offset": 2}, + {"xtype": "q", "yarray": np.array([12, 22, 32])}, + ), + ( # C5.2: Reuse test case from C4, but include an offset, expect scaled y-array in C4 to shift up by 2 + { + "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, }, { - "q": None, - "tth": 60, - "d": None, - "offset": 2.1, + "xarray": np.array([20, 25.5, 32, 45, 50, 62, 100, 125, 140]), + "yarray": np.array([1.1, 2, 3, 3.5, 4, 5, 10, 12, 13]), + "xtype": "tth", + "wavelength": 2 * np.pi, }, - {"xtype": "tth", "yarray": np.array([4.1, 5.1, 6.1, 7.1, 8.1, 9.1])}, + {"tth": 60, "offset": 2}, + {"xtype": "tth", "yarray": np.array([3, 4, 5, 6, 7, 8, 12])}, ), ], ) def test_scale_to(org_do_args, target_do_args, scale_inputs, expected): original_do = DiffractionObject(**org_do_args) target_do = DiffractionObject(**target_do_args) - scaled_do = original_do.scale_to( - target_do, q=scale_inputs["q"], tth=scale_inputs["tth"], d=scale_inputs["d"], offset=scale_inputs["offset"] - ) + scaled_do = original_do.scale_to(target_do, **scale_inputs) # Check the intensity data is the same as expected assert np.allclose(scaled_do.on_xtype(expected["xtype"])[1], expected["yarray"]) @@ -300,27 +311,7 @@ def test_scale_to(org_do_args, target_do_args, scale_inputs, expected): "org_do_args, target_do_args, scale_inputs", [ # Test expected errors produced from scale_to() with invalid inputs - ( # C1: none of q, tth, d, provided, expect ValueError - { - "xarray": np.array([0.1, 0.2, 0.3]), - "yarray": np.array([1, 2, 3]), - "xtype": "q", - "wavelength": 2 * np.pi, - }, - { - "xarray": np.array([0.05, 0.1, 0.2, 0.3]), - "yarray": np.array([5, 10, 20, 30]), - "xtype": "q", - "wavelength": 2 * np.pi, - }, - { - "q": None, - "tth": None, - "d": None, - "offset": 0, - }, - ), - ( # C2: tth and d both provided, expect ValueErrort + ( # C2: tth and d both provided, expect ValueError { "xarray": np.array([10, 25, 30.1, 40.2, 61, 120, 140]), "yarray": np.array([10, 20, 30, 40, 50, 60, 100]), @@ -334,10 +325,8 @@ def test_scale_to(org_do_args, target_do_args, scale_inputs, expected): "wavelength": 2 * np.pi, }, { - "q": None, "tth": 60, "d": 10, - "offset": 0, }, ), ], @@ -346,15 +335,11 @@ def test_scale_to_bad(org_do_args, target_do_args, scale_inputs): original_do = DiffractionObject(**org_do_args) target_do = DiffractionObject(**target_do_args) with pytest.raises( - ValueError, match="You must specify exactly one of 'q', 'tth', or 'd'. Please rerun specifying only one." + ValueError, + match="You must specify none or exactly one of 'q', 'tth', or 'd'. " + "Please provide either none or one value.", ): - original_do.scale_to( - target_do, - q=scale_inputs["q"], - tth=scale_inputs["tth"], - d=scale_inputs["d"], - offset=scale_inputs["offset"], - ) + original_do.scale_to(target_do, **scale_inputs) @pytest.mark.parametrize(