Skip to content

Commit

Permalink
Merge pull request #289 from yucongalicechen/scaleto2
Browse files Browse the repository at this point in the history
add new features in `scale_to()` so user can run without an x-value
  • Loading branch information
sbillinge authored Dec 30, 2024
2 parents 3f10ca9 + 0691e54 commit 88d8d73
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 77 deletions.
11 changes: 10 additions & 1 deletion doc/source/examples/diffraction_objects_example.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions news/scaleto-max.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Added:**

* new feature in `scale_to()`: default scaling is based on the max q-value in each diffraction object.

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
31 changes: 21 additions & 10 deletions src/diffpy/utils/diffraction_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,37 +377,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)
Expand Down
117 changes: 51 additions & 66 deletions tests/test_diffraction_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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"])

Expand All @@ -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]),
Expand All @@ -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,
},
),
],
Expand All @@ -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(
Expand Down

0 comments on commit 88d8d73

Please sign in to comment.