From 25a089eba1c3cb220e19789b4ea254c5647040db Mon Sep 17 00:00:00 2001 From: Sangjoon Bob Lee Date: Sat, 9 Nov 2024 12:26:04 -0500 Subject: [PATCH 01/11] Diffraction_object deprecation warning --- .../scattering_objects/diffraction_objects.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/diffpy/utils/scattering_objects/diffraction_objects.py b/src/diffpy/utils/scattering_objects/diffraction_objects.py index 94d2831..9bc3d5d 100644 --- a/src/diffpy/utils/scattering_objects/diffraction_objects.py +++ b/src/diffpy/utils/scattering_objects/diffraction_objects.py @@ -1,6 +1,6 @@ import datetime from copy import deepcopy - +import warnings import numpy as np from diffpy.utils.tools import get_package_info @@ -18,6 +18,18 @@ class Diffraction_object: + """FIXME: Add class docstring. + + .. deprecated:: 3.5.1 + `Diffraction_object` will be removed in diffpy.utils 3.6.0, it is replaced by + `DiffractionObject` to follow the class naming convention. + """ + warnings.warn( + "Diffraction_object` will be removed in diffpy.utils 3.6.0, it is replaced by " + "DiffractionObject` to follow the class naming convention.", + DeprecationWarning, + stacklevel=2, + ) def __init__(self, name="", wavelength=None): self.name = name self.wavelength = wavelength From 558cc1eef7453aa2cf37e44d9c23b1dff21f68a1 Mon Sep 17 00:00:00 2001 From: Sangjoon Bob Lee Date: Sat, 9 Nov 2024 12:26:57 -0500 Subject: [PATCH 02/11] apply pre-commit --- src/diffpy/utils/scattering_objects/diffraction_objects.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/diffpy/utils/scattering_objects/diffraction_objects.py b/src/diffpy/utils/scattering_objects/diffraction_objects.py index 9bc3d5d..a7f3ceb 100644 --- a/src/diffpy/utils/scattering_objects/diffraction_objects.py +++ b/src/diffpy/utils/scattering_objects/diffraction_objects.py @@ -1,6 +1,7 @@ import datetime -from copy import deepcopy import warnings +from copy import deepcopy + import numpy as np from diffpy.utils.tools import get_package_info @@ -24,12 +25,14 @@ class Diffraction_object: `Diffraction_object` will be removed in diffpy.utils 3.6.0, it is replaced by `DiffractionObject` to follow the class naming convention. """ + warnings.warn( "Diffraction_object` will be removed in diffpy.utils 3.6.0, it is replaced by " "DiffractionObject` to follow the class naming convention.", DeprecationWarning, stacklevel=2, ) + def __init__(self, name="", wavelength=None): self.name = name self.wavelength = wavelength From 6e9b46568cd777bcf07d0a4af4e16fda0e3bbfb9 Mon Sep 17 00:00:00 2001 From: Sangjoon Bob Lee Date: Sun, 10 Nov 2024 10:28:39 -0500 Subject: [PATCH 03/11] Include the word deprecated in the warning msg --- src/diffpy/utils/scattering_objects/diffraction_objects.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/diffpy/utils/scattering_objects/diffraction_objects.py b/src/diffpy/utils/scattering_objects/diffraction_objects.py index a7f3ceb..58e290e 100644 --- a/src/diffpy/utils/scattering_objects/diffraction_objects.py +++ b/src/diffpy/utils/scattering_objects/diffraction_objects.py @@ -19,15 +19,15 @@ class Diffraction_object: - """FIXME: Add class docstring. + """A class to represent and manipulate data associated with diffraction experiments. .. deprecated:: 3.5.1 - `Diffraction_object` will be removed in diffpy.utils 3.6.0, it is replaced by + `Diffraction_object` is deprecated and will be removed in diffpy.utils 3.6.0. It is replaced by `DiffractionObject` to follow the class naming convention. """ warnings.warn( - "Diffraction_object` will be removed in diffpy.utils 3.6.0, it is replaced by " + "Diffraction_object` is deprecated and will be removed in diffpy.utils 3.6.0, It is replaced by " "DiffractionObject` to follow the class naming convention.", DeprecationWarning, stacklevel=2, From 2b0b1128a64570708080746449e070f772f67584 Mon Sep 17 00:00:00 2001 From: Sangjoon Bob Lee Date: Sun, 10 Nov 2024 10:28:59 -0500 Subject: [PATCH 04/11] Create a new object named DiffractionObject --- .../scattering_objects/diffraction_object.py | 519 ++++++++++++++++++ 1 file changed, 519 insertions(+) create mode 100644 src/diffpy/utils/scattering_objects/diffraction_object.py diff --git a/src/diffpy/utils/scattering_objects/diffraction_object.py b/src/diffpy/utils/scattering_objects/diffraction_object.py new file mode 100644 index 0000000..92137c2 --- /dev/null +++ b/src/diffpy/utils/scattering_objects/diffraction_object.py @@ -0,0 +1,519 @@ +import datetime +import warnings +from copy import deepcopy + +import numpy as np + +from diffpy.utils.tools import get_package_info + +QQUANTITIES = ["q"] +ANGLEQUANTITIES = ["angle", "tth", "twotheta", "2theta"] +DQUANTITIES = ["d", "dspace"] +XQUANTITIES = ANGLEQUANTITIES + DQUANTITIES + QQUANTITIES +XUNITS = ["degrees", "radians", "rad", "deg", "inv_angs", "inv_nm", "nm-1", "A-1"] + +x_grid_emsg = ( + "objects are not on the same x-grid. You may add them using the self.add method " + "and specifying how to handle the mismatch." +) + + +class DiffractionObject: + """ + A class to manipulate diffraction data, supporting basic arithmetic operations and + conversions between different diffraction data metrics such as Q, two-theta, and d-spacing. + + Parameters + ---------- + name : str, optional + Identifier for the diffraction object. Defaults to an empty string. + wavelength : float, optional + Wavelength of the radiation used in the diffraction experiment. Defaults to None. + + Attributes + ---------- + name : str + Name or identifier for the diffraction experiment. + wavelength : float + Wavelength of the incoming x-rays. + scat_quantity : str + Description of the type of scattering data stored. + on_q : list of numpy.ndarray + A list containing two numpy arrays: [Q values, Intensities]. + on_tth : list of numpy.ndarray + A list containing two numpy arrays: [two-theta angles, Intensities]. + on_d : list of numpy.ndarray + A list containing two numpy arrays: [d-spacing values, Intensities]. + metadata : dict + Dictionary containing additional metadata about the diffraction data. + + Methods + ------- + set_angles_from_list(angles_list) + Set the angular data for the experiment from a predefined list of angles. + set_qs_from_range(begin_q, end_q, step_size=None, n_steps=None) + Define Q values linearly spaced within the specified range. If step_size is provided, + n_steps is ignored. + set_angles_from_range(begin_angle, end_angle, step_size=None, n_steps=None) + Define angle values linearly spaced within the specified range. If step_size is provided, + n_steps is ignored. + insert_scattering_quantity(xarray, yarray, xtype, metadata=None, scat_quantity=None, name=None, + wavelength=None) + Insert a new scattering quantity into the object with optional metadata. Additional optional + parameters include scat_quantity, name, and wavelength. + q_to_tth() + Convert Q values to two-theta angles using the specified wavelength. + tth_to_q() + Convert two-theta angles to Q values using the specified wavelength. + scale_to(target_diff_object, xtype=None, xvalue=None) + Return a new diffraction object with rescaled intensity values. + on_xtype(xtype) + Return the data arrays associated with a given x-type (Q, two-theta, or d-spacing). + dump(filepath, xtype=None) + Save the data to a specified file, allowing selection of the data type to be saved. + xtype defaults to 'q' if not specified. + """ + + def __init__(self, name="", wavelength=None): + self.name = name + self.wavelength = wavelength + self.scat_quantity = "" + self.on_q = [np.empty(0), np.empty(0)] + self.on_tth = [np.empty(0), np.empty(0)] + self.on_d = [np.empty(0), np.empty(0)] + self._all_arrays = [self.on_q, self.on_tth] + self.metadata = {} + + def __eq__(self, other): + if not isinstance(other, DiffractionObject): + return NotImplemented + self_attributes = [key for key in self.__dict__ if not key.startswith("_")] + other_attributes = [key for key in other.__dict__ if not key.startswith("_")] + if not sorted(self_attributes) == sorted(other_attributes): + return False + for key in self_attributes: + value = getattr(self, key) + other_value = getattr(other, key) + if isinstance(value, float): + if ( + not (value is None and other_value is None) + and (value is None) + or (other_value is None) + or not np.isclose(value, other_value, rtol=1e-5) + ): + return False + elif isinstance(value, list) and all(isinstance(i, np.ndarray) for i in value): + if not all(np.allclose(i, j, rtol=1e-5) for i, j in zip(value, other_value)): + return False + else: + if value != other_value: + return False + return True + + def __add__(self, other): + summed = deepcopy(self) + if isinstance(other, int) or isinstance(other, float) or isinstance(other, np.ndarray): + summed.on_tth[1] = self.on_tth[1] + other + summed.on_q[1] = self.on_q[1] + other + elif not isinstance(other, DiffractionObject): + raise TypeError("I only know how to sum two Diffraction_object objects") + elif self.on_tth[0].all() != other.on_tth[0].all(): + raise RuntimeError(x_grid_emsg) + else: + summed.on_tth[1] = self.on_tth[1] + other.on_tth[1] + summed.on_q[1] = self.on_q[1] + other.on_q[1] + return summed + + def __radd__(self, other): + summed = deepcopy(self) + if isinstance(other, int) or isinstance(other, float) or isinstance(other, np.ndarray): + summed.on_tth[1] = self.on_tth[1] + other + summed.on_q[1] = self.on_q[1] + other + elif not isinstance(other, DiffractionObject): + raise TypeError("I only know how to sum two Scattering_object objects") + elif self.on_tth[0].all() != other.on_tth[0].all(): + raise RuntimeError(x_grid_emsg) + else: + summed.on_tth[1] = self.on_tth[1] + other.on_tth[1] + summed.on_q[1] = self.on_q[1] + other.on_q[1] + return summed + + def __sub__(self, other): + subtracted = deepcopy(self) + if isinstance(other, int) or isinstance(other, float) or isinstance(other, np.ndarray): + subtracted.on_tth[1] = self.on_tth[1] - other + subtracted.on_q[1] = self.on_q[1] - other + elif not isinstance(other, DiffractionObject): + raise TypeError("I only know how to subtract two Scattering_object objects") + elif self.on_tth[0].all() != other.on_tth[0].all(): + raise RuntimeError(x_grid_emsg) + else: + subtracted.on_tth[1] = self.on_tth[1] - other.on_tth[1] + subtracted.on_q[1] = self.on_q[1] - other.on_q[1] + return subtracted + + def __rsub__(self, other): + subtracted = deepcopy(self) + if isinstance(other, int) or isinstance(other, float) or isinstance(other, np.ndarray): + subtracted.on_tth[1] = other - self.on_tth[1] + subtracted.on_q[1] = other - self.on_q[1] + elif not isinstance(other, DiffractionObject): + raise TypeError("I only know how to subtract two Scattering_object objects") + elif self.on_tth[0].all() != other.on_tth[0].all(): + raise RuntimeError(x_grid_emsg) + else: + subtracted.on_tth[1] = other.on_tth[1] - self.on_tth[1] + subtracted.on_q[1] = other.on_q[1] - self.on_q[1] + return subtracted + + def __mul__(self, other): + multiplied = deepcopy(self) + if isinstance(other, int) or isinstance(other, float) or isinstance(other, np.ndarray): + multiplied.on_tth[1] = other * self.on_tth[1] + multiplied.on_q[1] = other * self.on_q[1] + elif not isinstance(other, DiffractionObject): + raise TypeError("I only know how to multiply two Scattering_object objects") + elif self.on_tth[0].all() != other.on_tth[0].all(): + raise RuntimeError(x_grid_emsg) + else: + multiplied.on_tth[1] = self.on_tth[1] * other.on_tth[1] + multiplied.on_q[1] = self.on_q[1] * other.on_q[1] + return multiplied + + def __rmul__(self, other): + multiplied = deepcopy(self) + if isinstance(other, int) or isinstance(other, float) or isinstance(other, np.ndarray): + multiplied.on_tth[1] = other * self.on_tth[1] + multiplied.on_q[1] = other * self.on_q[1] + elif self.on_tth[0].all() != other.on_tth[0].all(): + raise RuntimeError(x_grid_emsg) + else: + multiplied.on_tth[1] = self.on_tth[1] * other.on_tth[1] + multiplied.on_q[1] = self.on_q[1] * other.on_q[1] + return multiplied + + def __truediv__(self, other): + divided = deepcopy(self) + if isinstance(other, int) or isinstance(other, float) or isinstance(other, np.ndarray): + divided.on_tth[1] = other / self.on_tth[1] + divided.on_q[1] = other / self.on_q[1] + elif not isinstance(other, DiffractionObject): + raise TypeError("I only know how to multiply two Scattering_object objects") + elif self.on_tth[0].all() != other.on_tth[0].all(): + raise RuntimeError(x_grid_emsg) + else: + divided.on_tth[1] = self.on_tth[1] / other.on_tth[1] + divided.on_q[1] = self.on_q[1] / other.on_q[1] + return divided + + def __rtruediv__(self, other): + divided = deepcopy(self) + if isinstance(other, int) or isinstance(other, float) or isinstance(other, np.ndarray): + divided.on_tth[1] = other / self.on_tth[1] + divided.on_q[1] = other / self.on_q[1] + elif self.on_tth[0].all() != other.on_tth[0].all(): + raise RuntimeError(x_grid_emsg) + else: + divided.on_tth[1] = other.on_tth[1] / self.on_tth[1] + divided.on_q[1] = other.on_q[1] / self.on_q[1] + return divided + + def set_angles_from_list(self, angles_list): + self.angles = angles_list + self.n_steps = len(angles_list) - 1.0 + self.begin_angle = self.angles[0] + self.end_angle = self.angles[-1] + + def set_qs_from_range(self, begin_q, end_q, step_size=None, n_steps=None): + """ + create an array of linear spaced Q-values + + Parameters + ---------- + begin_q float + the beginning angle + end_q float + the ending angle + step_size float + the size of the step between points. Only specify step_size or n_steps, not both + n_steps integer + the number of steps. Odd numbers are preferred. Only specify step_size or n_steps, not both + + Returns + ------- + Sets self.qs + self.qs array of floats + the q values in the independent array + + """ + self.qs = self._set_array_from_range(begin_q, end_q, step_size=step_size, n_steps=n_steps) + + def set_angles_from_range(self, begin_angle, end_angle, step_size=None, n_steps=None): + """ + create an array of linear spaced angle-values + + Parameters + ---------- + begin_angle float + the beginning angle + end_angle float + the ending angle + step_size float + the size of the step between points. Only specify step_size or n_steps, not both + n_steps integer + the number of steps. Odd numbers are preferred. Only specify step_size or n_steps, not both + + Returns + ------- + Sets self.angles + self.angles array of floats + the q values in the independent array + + """ + self.angles = self._set_array_from_range(begin_angle, end_angle, step_size=step_size, n_steps=n_steps) + + def _set_array_from_range(self, begin, end, step_size=None, n_steps=None): + if step_size is not None and n_steps is not None: + print( + "WARNING: both step_size and n_steps have been given. n_steps will be used and step_size will be " + "reset." + ) + array = np.linspace(begin, end, n_steps) + elif step_size is not None: + array = np.arange(begin, end, step_size) + elif n_steps is not None: + array = np.linspace(begin, end, n_steps) + return array + + def get_angle_index(self, angle): + count = 0 + for i, target in enumerate(self.angles): + if angle == target: + return i + else: + count += 1 + if count >= len(self.angles): + raise IndexError(f"WARNING: no angle {angle} found in angles list") + + def insert_scattering_quantity( + self, + xarray, + yarray, + xtype, + metadata={}, + scat_quantity=None, + name=None, + wavelength=None, + ): + f""" + insert a new scattering quantity into the scattering object + + Parameters + ---------- + xarray array-like of floats + the independent variable array + yarray array-like of floats + the dependent variable array + xtype string + the type of quantity for the independent variable from {*XQUANTITIES, } + metadata: dict + the metadata in the form of a dictionary of user-supplied key:value pairs + + Returns + ------- + + """ + self.input_xtype = xtype + # empty attributes have been defined in the __init__ method so only + # set the attributes that are not empty to avoid emptying them by mistake + if metadata: + self.metadata = metadata + if scat_quantity is not None: + self.scat_quantity = scat_quantity + if name is not None: + self.name = name + if wavelength is not None: + self.wavelength = wavelength + if xtype.lower() in QQUANTITIES: + self.on_q = [np.array(xarray), np.array(yarray)] + elif xtype.lower() in ANGLEQUANTITIES: + self.on_tth = [np.array(xarray), np.array(yarray)] + elif xtype.lower() in DQUANTITIES: + self.on_tth = [np.array(xarray), np.array(yarray)] + self.set_all_arrays() + + def q_to_tth(self): + r""" + Helper function to convert q to two-theta. + + By definition the relationship is: + + .. math:: + + \sin\left(\frac{2\theta}{2}\right) = \frac{\lambda q}{4 \pi} + + thus + + .. math:: + + 2\theta_n = 2 \arcsin\left(\frac{\lambda q}{4 \pi}\right) + + Parameters + ---------- + q : array + An array of :math:`q` values + + wavelength : float + Wavelength of the incoming x-rays + + Function adapted from scikit-beam. Thanks to those developers + + Returns + ------- + two_theta : array + An array of :math:`2\theta` values in radians + """ + q = self.on_q[0] + q = np.asarray(q) + wavelength = float(self.wavelength) + pre_factor = wavelength / (4 * np.pi) + return np.rad2deg(2.0 * np.arcsin(q * pre_factor)) + + def tth_to_q(self): + r""" + Helper function to convert two-theta to q + + By definition the relationship is + + .. math:: + + \sin\left(\frac{2\theta}{2}\right) = \frac{\lambda q}{4 \pi} + + thus + + .. math:: + + q = \frac{4 \pi \sin\left(\frac{2\theta}{2}\right)}{\lambda} + + + + Parameters + ---------- + two_theta : array + An array of :math:`2\theta` values in units of degrees + + wavelength : float + Wavelength of the incoming x-rays + + Function adapted from scikit-beam. Thanks to those developers. + + Returns + ------- + q : array + An array of :math:`q` values in the inverse of the units + of ``wavelength`` + """ + two_theta = np.asarray(np.deg2rad(self.on_tth[0])) + wavelength = float(self.wavelength) + pre_factor = (4 * np.pi) / wavelength + return pre_factor * np.sin(two_theta / 2) + + def set_all_arrays(self): + master_array, xtype = self._get_original_array() + if xtype == "q": + self.on_tth[0] = self.q_to_tth() + self.on_tth[1] = master_array[1] + if xtype == "tth": + self.on_q[0] = self.tth_to_q() + self.on_q[1] = master_array[1] + self.tthmin = self.on_tth[0][0] + self.tthmax = self.on_tth[0][-1] + self.qmin = self.on_q[0][0] + self.qmax = self.on_q[0][-1] + + def _get_original_array(self): + if self.input_xtype in QQUANTITIES: + return self.on_q, "q" + elif self.input_xtype in ANGLEQUANTITIES: + return self.on_tth, "tth" + elif self.input_xtype in DQUANTITIES: + return self.on_d, "d" + + def scale_to(self, target_diff_object, xtype=None, xvalue=None): + f""" + returns a new diffraction object which is the current object but recaled in y to the target + + Parameters + ---------- + target_diff_object: DiffractionObject + the diffractoin 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. + + Returns + ------- + the rescaled DiffractionObject as a new object + + """ + scaled = deepcopy(self) + if xtype is None: + xtype = "q" + + 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 + return scaled + + def on_xtype(self, xtype): + """ + return a 2D np array with x in the first column and y in the second for x of type type + Parameters + ---------- + xtype + + Returns + ------- + + """ + if xtype.lower() in ANGLEQUANTITIES: + return self.on_tth + elif xtype.lower() in QQUANTITIES: + return self.on_q + elif xtype.lower() in DQUANTITIES: + return self.on_d + pass + + def dump(self, filepath, xtype=None): + if xtype is None: + xtype = " q" + if xtype == "q": + data_to_save = np.column_stack((self.on_q[0], self.on_q[1])) + elif xtype == "tth": + data_to_save = np.column_stack((self.on_tth[0], self.on_tth[1])) + elif xtype == "d": + data_to_save = np.column_stack((self.on_d[0], self.on_d[1])) + else: + print(f"WARNING: cannot handle the xtype '{xtype}'") + self.metadata.update(get_package_info("diffpy.utils", metadata=self.metadata)) + self.metadata["creation_time"] = datetime.datetime.now() + + with open(filepath, "w") as f: + f.write( + f"[Diffraction_object]\nname = {self.name}\nwavelength = {self.wavelength}\n" + f"scat_quantity = {self.scat_quantity}\n" + ) + for key, value in self.metadata.items(): + f.write(f"{key} = {value}\n") + f.write("\n#### start data\n") + np.savetxt(f, data_to_save, delimiter=" ") From 0161a8b4c59c09d7cb945f5e4a44fbaccbd2f814 Mon Sep 17 00:00:00 2001 From: Sangjoon Bob Lee Date: Sun, 10 Nov 2024 10:29:19 -0500 Subject: [PATCH 05/11] Add news and a new test file for DiffractionObject --- news/deprecate.rst | 23 +++ tests/test_diffraction_object.py | 266 +++++++++++++++++++++++++++++++ 2 files changed, 289 insertions(+) create mode 100644 news/deprecate.rst create mode 100644 tests/test_diffraction_object.py diff --git a/news/deprecate.rst b/news/deprecate.rst new file mode 100644 index 0000000..31dd10e --- /dev/null +++ b/news/deprecate.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* Diffraction_object class, renamed to DiffractionObject + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/tests/test_diffraction_object.py b/tests/test_diffraction_object.py new file mode 100644 index 0000000..ff2dd8f --- /dev/null +++ b/tests/test_diffraction_object.py @@ -0,0 +1,266 @@ +from pathlib import Path + +import numpy as np +import pytest +from freezegun import freeze_time + +from diffpy.utils.scattering_objects.diffraction_object import DiffractionObject + +params = [ + ( # Default + [ + "", + None, + "", + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + {}, + ], + [ + "", + None, + "", + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + {}, + ], + True, + ), + ( # Compare same attributes + [ + "test", + 0.71, + "x-ray", + [np.array([1, 2]), np.array([3, 4])], + [np.array([1, 2]), np.array([3, 4])], + [np.array([1, 2]), np.array([3, 4])], + {"thing1": 1, "thing2": "thing2"}, + ], + [ + "test", + 0.7100001, + "x-ray", + [np.array([1.00001, 2.00001]), np.array([3.00001, 4.00001])], + [np.array([1.00001, 2.00001]), np.array([3.00001, 4.00001])], + [np.array([1.00001, 2.00001]), np.array([3.00001, 4.00001])], + {"thing1": 1, "thing2": "thing2"}, + ], + True, + ), + ( # Different names + [ + "test1", + None, + "", + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + {}, + ], + [ + "test2", + None, + "", + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + {}, + ], + False, + ), + ( # Different wavelengths + [ + "", + 0.71, + "", + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + {}, + ], + [ + "", + 0.711, + "", + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + {}, + ], + False, + ), + ( # Different wavelengths + [ + "", + 0.71, + "", + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + {}, + ], + [ + "", + None, + "", + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + {}, + ], + False, + ), + ( # Different scat_quantity + [ + "", + None, + "", + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + {}, + ], + [ + "", + None, + "x-ray", + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + {}, + ], + False, + ), + ( # Different on_q + [ + "", + None, + "", + [np.array([1, 2]), np.array([3, 4])], + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + {}, + ], + [ + "", + None, + "", + [np.array([1.01, 2]), np.array([3, 4])], + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + {}, + ], + False, + ), + ( # Different on_tth + [ + "", + None, + "", + [np.empty(0), np.empty(0)], + [np.array([1, 2]), np.array([3, 4])], + [np.empty(0), np.empty(0)], + {}, + ], + [ + "", + None, + "", + [np.empty(0), np.empty(0)], + [np.array([1.01, 2]), np.array([3, 4])], + [np.empty(0), np.empty(0)], + {}, + ], + False, + ), + ( # Different on_d + [ + "", + None, + "", + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + [np.array([1, 2]), np.array([3, 4])], + {}, + ], + [ + "", + None, + "", + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + [np.array([1.01, 2]), np.array([3, 4])], + {}, + ], + False, + ), + ( # Different metadata + [ + "", + None, + "", + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + {"thing1": 0, "thing2": "thing2"}, + ], + [ + "", + None, + "", + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + [np.empty(0), np.empty(0)], + {"thing1": 1, "thing2": "thing2"}, + ], + False, + ), +] + + +@pytest.mark.parametrize("inputs1, inputs2, expected", params) +def test_diffraction_objects_equality(inputs1, inputs2, expected): + diffraction_object1 = DiffractionObject() + diffraction_object2 = DiffractionObject() + diffraction_object1_attributes = [key for key in diffraction_object1.__dict__ if not key.startswith("_")] + for i, attribute in enumerate(diffraction_object1_attributes): + setattr(diffraction_object1, attribute, inputs1[i]) + setattr(diffraction_object2, attribute, inputs2[i]) + assert (diffraction_object1 == diffraction_object2) == expected + + +def test_dump(tmp_path, mocker): + x, y = np.linspace(0, 5, 6), np.linspace(0, 5, 6) + directory = Path(tmp_path) + file = directory / "testfile" + test = DiffractionObject() + test.wavelength = 1.54 + test.name = "test" + test.scat_quantity = "x-ray" + test.insert_scattering_quantity( + x, y, "q", metadata={"thing1": 1, "thing2": "thing2", "package_info": {"package2": "3.4.5"}} + ) + + mocker.patch("importlib.metadata.version", return_value="3.3.0") + + with freeze_time("2012-01-14"): + test.dump(file, "q") + + with open(file, "r") as f: + actual = f.read() + expected = ( + "[Diffraction_object]\nname = test\nwavelength = 1.54\nscat_quantity = x-ray\nthing1 = 1\n" + "thing2 = thing2\npackage_info = {'package2': '3.4.5', 'diffpy.utils': '3.3.0'}\n" + "creation_time = 2012-01-14 00:00:00\n\n" + "#### start data\n0.000000000000000000e+00 0.000000000000000000e+00\n" + "1.000000000000000000e+00 1.000000000000000000e+00\n" + "2.000000000000000000e+00 2.000000000000000000e+00\n" + "3.000000000000000000e+00 3.000000000000000000e+00\n" + "4.000000000000000000e+00 4.000000000000000000e+00\n" + "5.000000000000000000e+00 5.000000000000000000e+00\n" + ) + + assert actual == expected + From da60a894d3723f25526e9f0302d41cff3bcd4e28 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 10 Nov 2024 15:29:28 +0000 Subject: [PATCH 06/11] [pre-commit.ci] auto fixes from pre-commit hooks --- tests/test_diffraction_object.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_diffraction_object.py b/tests/test_diffraction_object.py index ff2dd8f..e4d7d60 100644 --- a/tests/test_diffraction_object.py +++ b/tests/test_diffraction_object.py @@ -263,4 +263,3 @@ def test_dump(tmp_path, mocker): ) assert actual == expected - From 71c9f99889da53af1edc79fd4204a23546cfa5e5 Mon Sep 17 00:00:00 2001 From: Sangjoon Bob Lee Date: Sun, 10 Nov 2024 10:30:07 -0500 Subject: [PATCH 07/11] Remove the unused warnings in DiffractionObject --- src/diffpy/utils/scattering_objects/diffraction_object.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/diffpy/utils/scattering_objects/diffraction_object.py b/src/diffpy/utils/scattering_objects/diffraction_object.py index 92137c2..b7c890f 100644 --- a/src/diffpy/utils/scattering_objects/diffraction_object.py +++ b/src/diffpy/utils/scattering_objects/diffraction_object.py @@ -1,5 +1,4 @@ import datetime -import warnings from copy import deepcopy import numpy as np From 86838de26fb5bc6b1b4aaeb9c81703075f6a7022 Mon Sep 17 00:00:00 2001 From: Sangjoon Bob Lee Date: Sun, 10 Nov 2024 10:45:29 -0500 Subject: [PATCH 08/11] Remove methods in docstring --- .../api/diffpy.utils.scattering_objects.rst | 9 +++++++ .../scattering_objects/diffraction_object.py | 26 ------------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/doc/source/api/diffpy.utils.scattering_objects.rst b/doc/source/api/diffpy.utils.scattering_objects.rst index fe511bf..6caf5cf 100644 --- a/doc/source/api/diffpy.utils.scattering_objects.rst +++ b/doc/source/api/diffpy.utils.scattering_objects.rst @@ -26,3 +26,12 @@ diffpy.utils.scattering_objects.diffraction_objects module :members: :undoc-members: :show-inheritance: + + +diffpy.utils.scattering_objects.diffraction_object module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.utils.scattering_objects.diffraction_object + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/src/diffpy/utils/scattering_objects/diffraction_object.py b/src/diffpy/utils/scattering_objects/diffraction_object.py index b7c890f..053cc7b 100644 --- a/src/diffpy/utils/scattering_objects/diffraction_object.py +++ b/src/diffpy/utils/scattering_objects/diffraction_object.py @@ -45,32 +45,6 @@ class DiffractionObject: A list containing two numpy arrays: [d-spacing values, Intensities]. metadata : dict Dictionary containing additional metadata about the diffraction data. - - Methods - ------- - set_angles_from_list(angles_list) - Set the angular data for the experiment from a predefined list of angles. - set_qs_from_range(begin_q, end_q, step_size=None, n_steps=None) - Define Q values linearly spaced within the specified range. If step_size is provided, - n_steps is ignored. - set_angles_from_range(begin_angle, end_angle, step_size=None, n_steps=None) - Define angle values linearly spaced within the specified range. If step_size is provided, - n_steps is ignored. - insert_scattering_quantity(xarray, yarray, xtype, metadata=None, scat_quantity=None, name=None, - wavelength=None) - Insert a new scattering quantity into the object with optional metadata. Additional optional - parameters include scat_quantity, name, and wavelength. - q_to_tth() - Convert Q values to two-theta angles using the specified wavelength. - tth_to_q() - Convert two-theta angles to Q values using the specified wavelength. - scale_to(target_diff_object, xtype=None, xvalue=None) - Return a new diffraction object with rescaled intensity values. - on_xtype(xtype) - Return the data arrays associated with a given x-type (Q, two-theta, or d-spacing). - dump(filepath, xtype=None) - Save the data to a specified file, allowing selection of the data type to be saved. - xtype defaults to 'q' if not specified. """ def __init__(self, name="", wavelength=None): From 7c731400d14dd01ce1e436abfc9ac7343cbf3b95 Mon Sep 17 00:00:00 2001 From: Sangjoon Bob Lee Date: Sun, 10 Nov 2024 10:45:48 -0500 Subject: [PATCH 09/11] Apply pre-commit --- doc/source/api/diffpy.utils.scattering_objects.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/api/diffpy.utils.scattering_objects.rst b/doc/source/api/diffpy.utils.scattering_objects.rst index 6caf5cf..026a1ca 100644 --- a/doc/source/api/diffpy.utils.scattering_objects.rst +++ b/doc/source/api/diffpy.utils.scattering_objects.rst @@ -34,4 +34,4 @@ diffpy.utils.scattering_objects.diffraction_object module .. automodule:: diffpy.utils.scattering_objects.diffraction_object :members: :undoc-members: - :show-inheritance: \ No newline at end of file + :show-inheritance: From 16b8bbee9e2cd4aaa2bddbaeddf306caece33847 Mon Sep 17 00:00:00 2001 From: Sangjoon Bob Lee Date: Wed, 13 Nov 2024 12:58:29 -0500 Subject: [PATCH 10/11] Add The in front of each parameter description in docstring --- .../scattering_objects/diffraction_object.py | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/diffpy/utils/scattering_objects/diffraction_object.py b/src/diffpy/utils/scattering_objects/diffraction_object.py index 053cc7b..93e41ae 100644 --- a/src/diffpy/utils/scattering_objects/diffraction_object.py +++ b/src/diffpy/utils/scattering_objects/diffraction_object.py @@ -20,31 +20,32 @@ class DiffractionObject: """ A class to manipulate diffraction data, supporting basic arithmetic operations and - conversions between different diffraction data metrics such as Q, two-theta, and d-spacing. + conversions between independent variables such as Q, two-theta, + and d-spacing. Parameters ---------- name : str, optional - Identifier for the diffraction object. Defaults to an empty string. + The name or identifier for the diffraction experiment. wavelength : float, optional - Wavelength of the radiation used in the diffraction experiment. Defaults to None. + The wavelength of the radiation used in the diffraction experiment. Defaults to None. Attributes ---------- name : str - Name or identifier for the diffraction experiment. + The name or identifier for the diffraction experiment. wavelength : float - Wavelength of the incoming x-rays. + The wavelength of the incoming x-rays. scat_quantity : str - Description of the type of scattering data stored. + The description of the type of scattering data stored. on_q : list of numpy.ndarray - A list containing two numpy arrays: [Q values, Intensities]. + The list containing two numpy arrays: [Q values, Intensities]. on_tth : list of numpy.ndarray - A list containing two numpy arrays: [two-theta angles, Intensities]. + The list containing two numpy arrays: [two-theta angles, Intensities]. on_d : list of numpy.ndarray - A list containing two numpy arrays: [d-spacing values, Intensities]. + The list containing two numpy arrays: [d-spacing values, Intensities]. metadata : dict - Dictionary containing additional metadata about the diffraction data. + The dictionary containing additional metadata about the diffraction data. """ def __init__(self, name="", wavelength=None): @@ -284,13 +285,13 @@ def insert_scattering_quantity( Parameters ---------- xarray array-like of floats - the independent variable array + The independent variable array yarray array-like of floats - the dependent variable array + The dependent variable array xtype string - the type of quantity for the independent variable from {*XQUANTITIES, } + The type of quantity for the independent variable from {*XQUANTITIES, } metadata: dict - the metadata in the form of a dictionary of user-supplied key:value pairs + The metadata in the form of a dictionary of user-supplied key:value pairs Returns ------- @@ -334,10 +335,10 @@ def q_to_tth(self): Parameters ---------- q : array - An array of :math:`q` values + The array of :math:`q` values wavelength : float - Wavelength of the incoming x-rays + The Wavelength of the incoming x-rays Function adapted from scikit-beam. Thanks to those developers @@ -373,17 +374,17 @@ def tth_to_q(self): Parameters ---------- two_theta : array - An array of :math:`2\theta` values in units of degrees + The array of :math:`2\theta` values in units of degrees wavelength : float - Wavelength of the incoming x-rays + The wavelength of the incoming x-rays Function adapted from scikit-beam. Thanks to those developers. Returns ------- q : array - An array of :math:`q` values in the inverse of the units + The array of :math:`q` values in the inverse of the units of ``wavelength`` """ two_theta = np.asarray(np.deg2rad(self.on_tth[0])) @@ -419,11 +420,11 @@ def scale_to(self, target_diff_object, xtype=None, xvalue=None): Parameters ---------- target_diff_object: DiffractionObject - the diffractoin object you want to scale the current one on to + The diffractoin 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 + 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 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. From ca872c0709c44f1cfc285b615b879fe40dd9c55a Mon Sep 17 00:00:00 2001 From: Sangjoon Bob Lee Date: Wed, 13 Nov 2024 12:59:31 -0500 Subject: [PATCH 11/11] Add linear to linearly spaced... --- src/diffpy/utils/scattering_objects/diffraction_object.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/diffpy/utils/scattering_objects/diffraction_object.py b/src/diffpy/utils/scattering_objects/diffraction_object.py index 93e41ae..ef9be6b 100644 --- a/src/diffpy/utils/scattering_objects/diffraction_object.py +++ b/src/diffpy/utils/scattering_objects/diffraction_object.py @@ -200,7 +200,7 @@ def set_angles_from_list(self, angles_list): def set_qs_from_range(self, begin_q, end_q, step_size=None, n_steps=None): """ - create an array of linear spaced Q-values + create an array of linearly spaced Q-values Parameters ---------- @@ -224,7 +224,7 @@ def set_qs_from_range(self, begin_q, end_q, step_size=None, n_steps=None): def set_angles_from_range(self, begin_angle, end_angle, step_size=None, n_steps=None): """ - create an array of linear spaced angle-values + create an array of linearly spaced angle-values Parameters ----------