From 90be271f0061a3ee00813d2c61dc224bc47aee42 Mon Sep 17 00:00:00 2001 From: Simon Billinge Date: Tue, 3 Dec 2024 23:14:10 -0500 Subject: [PATCH 1/3] DiffractionObject can now be instantiated directlyand basic data structure is a 2D array with everything in it --- news/constructor.rst | 25 ++ src/diffpy/utils/diffraction_objects.py | 475 +++--------------------- src/diffpy/utils/transforms.py | 22 +- tests/test_diffraction_objects.py | 466 +++++++++++++---------- tests/test_transforms.py | 4 +- 5 files changed, 361 insertions(+), 631 deletions(-) create mode 100644 news/constructor.rst diff --git a/news/constructor.rst b/news/constructor.rst new file mode 100644 index 00000000..9738c7a8 --- /dev/null +++ b/news/constructor.rst @@ -0,0 +1,25 @@ +**Added:** + +* + +**Changed:** + +* arrays and attributes now can be inserted when a DiffractionObject is instantiated +* data are now stored as a (len(x),4) numpy array with intensity in column 0, the q, then tth, then d +* `DiffractionObject.on_q`, on_tth and on_d are now methods and called as DiffractionObject.on_q() etc.` + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/utils/diffraction_objects.py b/src/diffpy/utils/diffraction_objects.py index 4351c537..e628beed 100644 --- a/src/diffpy/utils/diffraction_objects.py +++ b/src/diffpy/utils/diffraction_objects.py @@ -1,11 +1,10 @@ import datetime -import warnings from copy import deepcopy import numpy as np from diffpy.utils.tools import get_package_info -from diffpy.utils.transforms import q_to_tth, tth_to_q +from diffpy.utils.transforms import d_to_q, d_to_tth, q_to_d, q_to_tth, tth_to_d, tth_to_q QQUANTITIES = ["q"] ANGLEQUANTITIES = ["angle", "tth", "twotheta", "2theta"] @@ -19,398 +18,17 @@ ) -class Diffraction_object: - """A class to represent and manipulate data associated with diffraction experiments. - - .. deprecated:: 3.5.1 - `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` 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, - ) - - def __init__(self, name="", wavelength=None): - self.name = name - self.wavelength = wavelength - self.scat_quantity = "" - self.on_q = np.array([np.empty(0), np.empty(0)]) - self.on_tth = np.array([np.empty(0), np.empty(0)]) - self.on_d = np.array([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, Diffraction_object): - 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, Diffraction_object): - 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, Diffraction_object): - 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, Diffraction_object): - 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, Diffraction_object): - 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, Diffraction_object): - 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, Diffraction_object): - 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, yarray]) - elif xtype.lower() in ANGLEQUANTITIES: - self.on_tth = np.array([xarray, yarray]) - elif xtype.lower() in DQUANTITIES: - self.on_tth = np.array([xarray, yarray]) - self.set_all_arrays() - - def set_all_arrays(self): - master_array, xtype = self._get_original_array() - if xtype == "q": - self.on_tth = q_to_tth(self.on_q, self.wavelength) - elif xtype == "tth": - self.on_q = tth_to_q(self.on_tth, self.wavelength) - 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: Diffraction_object - 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 Diffraction_object 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=" ") - - class DiffractionObject: - def __init__(self, name="", wavelength=None): - self.name = name - self.wavelength = wavelength - self.scat_quantity = "" - self.on_q = np.empty((2, 0), dtype=np.float64) - self.on_tth = np.empty((2, 0), dtype=np.float64) - self.on_d = np.empty((2, 0), dtype=np.float64) - self._all_arrays = [self.on_q, self.on_tth] - self.metadata = {} + def __init__( + self, name="", wavelength=None, scat_quantity="", metadata={}, xarray=None, yarray=None, xtype="" + ): + if xarray is None: + xarray = np.empty(0) + if yarray is None: + yarray = np.empty(0) + self.insert_scattering_quantity( + xarray, yarray, xtype, metadata=metadata, scat_quantity=scat_quantity, name=name, wavelength=wavelength + ) def __eq__(self, other): if not isinstance(other, DiffractionObject): @@ -629,7 +247,7 @@ def insert_scattering_quantity( yarray, xtype, metadata={}, - scat_quantity=None, + scat_quantity="", name=None, wavelength=None, ): @@ -652,42 +270,47 @@ def insert_scattering_quantity( """ 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 + self.metadata = metadata + self.scat_quantity = scat_quantity + self.name = name + self.wavelength = wavelength + self.all_arrays = np.empty(shape=(len(yarray), 4)) + self.all_arrays[:, 0] = yarray if xtype.lower() in QQUANTITIES: - self.on_q = np.array([xarray, yarray]) + self.all_arrays[:, 1] = xarray + self.all_arrays[:, 2] = q_to_tth(xarray, wavelength) + self.all_arrays[:, 3] = q_to_d(xarray) elif xtype.lower() in ANGLEQUANTITIES: - self.on_tth = np.array([xarray, yarray]) - elif xtype.lower() in DQUANTITIES: # Fixme when d is implemented. This here as a placeholder - self.on_tth = np.array([xarray, yarray]) - self.set_all_arrays() - - def set_all_arrays(self): - master_array, xtype = self._get_original_array() - if xtype == "q": - self.on_tth = q_to_tth(self.on_q, self.wavelength) - elif xtype == "tth": - self.on_q = tth_to_q(self.on_tth, self.wavelength) - 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] + self.all_arrays[:, 2] = xarray + self.all_arrays[:, 1] = tth_to_q(xarray, wavelength) + self.all_arrays[:, 3] = tth_to_d(xarray, wavelength) + elif xtype.lower() in DQUANTITIES: + self.all_arrays[:, 3] = xarray + self.all_arrays[:, 1] = d_to_q(xarray) + self.all_arrays[:, 2] = d_to_tth(xarray, wavelength) + self.qmin = np.nanmin(self.all_arrays[:, 1], initial=np.inf) + self.qmax = np.nanmax(self.all_arrays[:, 1], initial=0.0) + self.tthmin = np.nanmin(self.all_arrays[:, 2], initial=np.inf) + self.tthmax = np.nanmax(self.all_arrays[:, 2], initial=0.0) + self.dmin = np.nanmin(self.all_arrays[:, 3], initial=np.inf) + self.dmax = np.nanmax(self.all_arrays[:, 3], initial=0.0) def _get_original_array(self): if self.input_xtype in QQUANTITIES: - return self.on_q, "q" + return self.on_q(), "q" elif self.input_xtype in ANGLEQUANTITIES: - return self.on_tth, "tth" + return self.on_tth(), "tth" elif self.input_xtype in DQUANTITIES: - return self.on_d, "d" + return self.on_d(), "d" + + def on_q(self): + return [self.all_arrays[:, 1], self.all_arrays[:, 0]] + + def on_tth(self): + return [self.all_arrays[:, 2], self.all_arrays[:, 0]] + + def on_d(self): + return [self.all_arrays[:, 3], self.all_arrays[:, 0]] def scale_to(self, target_diff_object, xtype=None, xvalue=None): f""" @@ -748,11 +371,11 @@ 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])) + 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])) + 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])) + 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)) diff --git a/src/diffpy/utils/transforms.py b/src/diffpy/utils/transforms.py index 773aa607..71e50599 100644 --- a/src/diffpy/utils/transforms.py +++ b/src/diffpy/utils/transforms.py @@ -61,7 +61,7 @@ def q_to_tth(q, wavelength): This is the correct format for loading into diffpy.utils.DiffractionOject.on_tth """ _validate_inputs(q, wavelength) - q.astype(np.float64) + q.astype(float) tth = copy(q) # initialize output array of same shape if wavelength is not None: tth = np.rad2deg(2.0 * np.arcsin(q * wavelength / (4 * np.pi))) @@ -108,7 +108,7 @@ def tth_to_q(tth, wavelength): The units for the q-values are the inverse of the units of the provided wavelength. This is the correct format for loading into diffpy.utils.DiffractionOject.on_q """ - tth.astype(np.float64) + tth.astype(float) if np.any(np.deg2rad(tth) > np.pi): raise ValueError(invalid_tth_emsg) q = copy(tth) @@ -119,3 +119,21 @@ def tth_to_q(tth, wavelength): for i, _ in enumerate(q): q[i] = i return q + + +def q_to_d(qarray): + return 2.0 * np.pi / copy(qarray) + + +def tth_to_d(ttharray, wavelength): + qarray = tth_to_q(ttharray, wavelength) + return 2.0 * np.pi / copy(qarray) + + +def d_to_q(darray): + return 2.0 * np.pi / copy(darray) + + +def d_to_tth(darray, wavelength): + qarray = d_to_q(darray) + return q_to_tth(qarray, wavelength) diff --git a/tests/test_diffraction_objects.py b/tests/test_diffraction_objects.py index 1bc5220e..d04f5284 100644 --- a/tests/test_diffraction_objects.py +++ b/tests/test_diffraction_objects.py @@ -7,215 +7,179 @@ from diffpy.utils.diffraction_objects import DiffractionObject from diffpy.utils.transforms import wavelength_warning_emsg + +def compare_dicts(dict1, dict2): + assert dict1.keys() == dict2.keys(), "Keys mismatch" + for key in dict1: + val1, val2 = dict1[key], dict2[key] + if isinstance(val1, np.ndarray) and isinstance(val2, np.ndarray): + assert np.allclose(val1, val2), f"Arrays for key '{key}' differ" + elif isinstance(val1, np.float64) and isinstance(val2, np.float64): + assert np.isclose(val1, val2), f"Float64 values for key '{key}' differ" + else: + assert val1 == val2, f"Values for key '{key}' differ: {val1} != {val2}" + + +def dicts_equal(dict1, dict2): + equal = True + if not dict1.keys() == dict2.keys(): + equal = False + for key in dict1: + val1, val2 = dict1[key], dict2[key] + if isinstance(val1, np.ndarray) and isinstance(val2, np.ndarray): + if not np.allclose(val1, val2): + equal = False + elif isinstance(val1, np.float64) and isinstance(val2, np.float64): + if not np.isclose(val1, val2): + equal = False + else: + print(key, val1, val2) + if not val1 == val2: + equal = False + return equal + + 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"}, - ], + { + "name": "same", + "scat_quantity": "x-ray", + "wavelength": 0.71, + "xtype": "q", + "xarray": np.array([1.0, 2.0]), + "yarray": np.array([100.0, 200.0]), + "metadata": {"thing1": 1}, + }, + { + "name": "same", + "scat_quantity": "x-ray", + "wavelength": 0.71, + "xtype": "q", + "xarray": np.array([1.0, 2.0]), + "yarray": np.array([100.0, 200.0]), + "metadata": {"thing1": 1}, + }, 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)], - {}, - ], + { + "name": "something", + "scat_quantity": "", + "wavelength": None, + "xtype": "", + "xarray": np.empty(0), + "yarray": np.empty(0), + "metadata": {"thing1": 1, "thing2": "thing2"}, + }, + { + "name": "something else", + "scat_quantity": "", + "wavelength": None, + "xtype": "", + "xarray": np.empty(0), + "yarray": np.empty(0), + "metadata": {"thing1": 1, "thing2": "thing2"}, + }, 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)], - {}, - ], + { + "scat_quantity": "", + "wavelength": 0.71, + "xtype": "", + "xarray": np.empty(0), + "yarray": np.empty(0), + "metadata": {"thing1": 1, "thing2": "thing2"}, + }, + { + "scat_quantity": "", + "wavelength": None, + "xtype": "", + "xarray": np.empty(0), + "yarray": np.empty(0), + "metadata": {"thing1": 1, "thing2": "thing2"}, + }, 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)], - {}, - ], + { + "scat_quantity": "", + "wavelength": 0.71, + "xtype": "", + "xarray": np.empty(0), + "yarray": np.empty(0), + "metadata": {"thing1": 1, "thing2": "thing2"}, + }, + { + "scat_quantity": "", + "wavelength": 0.711, + "xtype": "", + "xarray": np.empty(0), + "yarray": np.empty(0), + "metadata": {"thing1": 1, "thing2": "thing2"}, + }, 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)], - {}, - ], + { + "scat_quantity": "x-ray", + "wavelength": None, + "xtype": "", + "xarray": np.empty(0), + "yarray": np.empty(0), + "metadata": {"thing1": 1, "thing2": "thing2"}, + }, + { + "scat_quantity": "neutron", + "wavelength": None, + "xtype": "", + "xarray": np.empty(0), + "yarray": np.empty(0), + "metadata": {"thing1": 1, "thing2": "thing2"}, + }, 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])], - {}, - ], + { + "scat_quantity": "", + "wavelength": None, + "xtype": "q", + "xarray": np.array([1.0, 2.0]), + "yarray": np.array([100.0, 200.0]), + "metadata": {}, + }, + { + "scat_quantity": "", + "wavelength": None, + "xtype": "q", + "xarray": np.array([3.0, 4.0]), + "yarray": np.array([100.0, 200.0]), + "metadata": {"thing1": 1, "thing2": "thing2"}, + }, 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"}, - ], + { + "scat_quantity": "", + "wavelength": None, + "xtype": "", + "xarray": np.empty(0), + "yarray": np.empty(0), + "metadata": {"thing1": 0, "thing2": "thing2"}, + }, + { + "scat_quantity": "", + "wavelength": None, + "xtype": "", + "xarray": np.empty(0), + "yarray": np.empty(0), + "metadata": {"thing1": 1, "thing2": "thing2"}, + }, False, ), ] @@ -223,13 +187,13 @@ @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 + diffraction_object1 = DiffractionObject(inputs1) + diffraction_object2 = DiffractionObject(inputs2) + # 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 dicts_equal(diffraction_object1.__dict__, diffraction_object2.__dict__) == expected def _test_valid_diffraction_objects(actual_diffraction_object, function, expected_array): @@ -245,14 +209,13 @@ 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( - np.array(x), - np.array(y), - "q", + test = DiffractionObject( + wavelength=1.54, + name="test", + scat_quantity="x-ray", + xarray=np.array(x), + yarray=np.array(y), + xtype="q", metadata={"thing1": 1, "thing2": "thing2", "package_info": {"package2": "3.4.5"}}, ) mocker.patch("importlib.metadata.version", return_value="3.3.0") @@ -273,3 +236,104 @@ def test_dump(tmp_path, mocker): ) assert actual == expected + + +tc_params = [ + ( + {}, + { + "all_arrays": np.empty(shape=(0, 4)), # instantiate empty + "metadata": {}, + "input_xtype": "", + "name": "", + "scat_quantity": "", + "qmin": np.float64(np.inf), + "qmax": np.float64(0.0), + "tthmin": np.float64(np.inf), + "tthmax": np.float64(0.0), + "dmin": np.float64(np.inf), + "dmax": np.float64(0.0), + "wavelength": None, + }, + ), + ( # instantiate just non-array attributes + {"name": "test", "scat_quantity": "x-ray", "metadata": {"thing": "1", "another": "2"}}, + { + "all_arrays": np.empty(shape=(0, 4)), + "metadata": {"thing": "1", "another": "2"}, + "input_xtype": "", + "name": "test", + "scat_quantity": "x-ray", + "qmin": np.float64(np.inf), + "qmax": np.float64(0.0), + "tthmin": np.float64(np.inf), + "tthmax": np.float64(0.0), + "dmin": np.float64(np.inf), + "dmax": np.float64(0.0), + "wavelength": None, + }, + ), + ( # instantiate just array attributes + { + "xarray": np.array([0.0, 90.0, 180.0]), + "yarray": np.array([1.0, 2.0, 3.0]), + "xtype": "tth", + "wavelength": 4.0 * np.pi, + }, + { + "all_arrays": np.array( + [ + [1.0, 0.0, 0.0, np.float64(np.inf)], + [2.0, 1.0 / np.sqrt(2), 90.0, np.sqrt(2) * 2 * np.pi], + [3.0, 1.0, 180.0, 1.0 * 2 * np.pi], + ] + ), + "metadata": {}, + "input_xtype": "tth", + "name": "", + "scat_quantity": "", + "qmin": np.float64(0.0), + "qmax": np.float64(1.0), + "tthmin": np.float64(0.0), + "tthmax": np.float64(180.0), + "dmin": np.float64(2 * np.pi), + "dmax": np.float64(np.inf), + "wavelength": 4.0 * np.pi, + }, + ), + ( # instantiate just array attributes + { + "xarray": np.array([np.inf, 2 * np.sqrt(2) * np.pi, 2 * np.pi]), + "yarray": np.array([1.0, 2.0, 3.0]), + "xtype": "d", + "wavelength": 4.0 * np.pi, + "scat_quantity": "x-ray", + }, + { + "all_arrays": np.array( + [ + [1.0, 0.0, 0.0, np.float64(np.inf)], + [2.0, 1.0 / np.sqrt(2), 90.0, np.sqrt(2) * 2 * np.pi], + [3.0, 1.0, 180.0, 1.0 * 2 * np.pi], + ] + ), + "metadata": {}, + "input_xtype": "d", + "name": "", + "scat_quantity": "x-ray", + "qmin": np.float64(0.0), + "qmax": np.float64(1.0), + "tthmin": np.float64(0.0), + "tthmax": np.float64(180.0), + "dmin": np.float64(2 * np.pi), + "dmax": np.float64(np.inf), + "wavelength": 4.0 * np.pi, + }, + ), +] + + +@pytest.mark.parametrize("inputs, expected", tc_params) +def test_constructor(inputs, expected): + actualdo = DiffractionObject(**inputs) + compare_dicts(actualdo.__dict__, expected) diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 7e3eae3f..e8b15492 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -5,9 +5,9 @@ params_q_to_tth = [ # UC1: Empty q values, no wavelength, return empty arrays - ([None, np.empty((1,))], np.empty((1,))), + ([None, np.empty((0))], np.empty((0))), # UC2: Empty q values, wavelength specified, return empty arrays - ([4 * np.pi, np.empty((1,))], np.empty((1,))), + ([4 * np.pi, np.empty((0))], np.empty(0)), # UC3: User specified valid q values, no wavelength, return empty arrays ( [None, np.array([0, 0.2, 0.4, 0.6, 0.8, 1])], From 828b2a69e696dc357061f9610ddb9a145a188f0a Mon Sep 17 00:00:00 2001 From: Simon Billinge Date: Wed, 4 Dec 2024 08:18:33 -0500 Subject: [PATCH 2/3] now passing tests --- src/diffpy/utils/diffraction_objects.py | 98 ++++++++++++++----------- tests/test_diffraction_objects.py | 16 ++-- 2 files changed, 65 insertions(+), 49 deletions(-) diff --git a/src/diffpy/utils/diffraction_objects.py b/src/diffpy/utils/diffraction_objects.py index e628beed..1daa62d0 100644 --- a/src/diffpy/utils/diffraction_objects.py +++ b/src/diffpy/utils/diffraction_objects.py @@ -1,4 +1,5 @@ import datetime +import warnings from copy import deepcopy import numpy as np @@ -18,17 +19,31 @@ ) +def _xtype_wmsg(xtype): + return ( + f"WARNING: I don't know how to handle the xtype, '{xtype}'. Please rerun specifying and " + f"xtype from {*XQUANTITIES, }" + ) + + class DiffractionObject: def __init__( - self, name="", wavelength=None, scat_quantity="", metadata={}, xarray=None, yarray=None, xtype="" + self, name=None, wavelength=None, scat_quantity=None, metadata=None, xarray=None, yarray=None, xtype="" ): + if name is None: + name = "" + self.name = name + if metadata is None: + metadata = {} + self.metadata = metadata + self.scat_quantity = scat_quantity + self.wavelength = wavelength + if xarray is None: xarray = np.empty(0) if yarray is None: yarray = np.empty(0) - self.insert_scattering_quantity( - xarray, yarray, xtype, metadata=metadata, scat_quantity=scat_quantity, name=name, wavelength=wavelength - ) + self.insert_scattering_quantity(xarray, yarray, xtype) def __eq__(self, other): if not isinstance(other, DiffractionObject): @@ -241,15 +256,32 @@ def get_angle_index(self, angle): if count >= len(self.angles): raise IndexError(f"WARNING: no angle {angle} found in angles list") + def _set_xarrays(self, xarray, xtype): + self.all_arrays = np.empty(shape=(len(xarray), 4)) + if xtype.lower() in QQUANTITIES: + self.all_arrays[:, 1] = xarray + self.all_arrays[:, 2] = q_to_tth(xarray, self.wavelength) + self.all_arrays[:, 3] = q_to_d(xarray) + elif xtype.lower() in ANGLEQUANTITIES: + self.all_arrays[:, 2] = xarray + self.all_arrays[:, 1] = tth_to_q(xarray, self.wavelength) + self.all_arrays[:, 3] = tth_to_d(xarray, self.wavelength) + elif xtype.lower() in DQUANTITIES: + self.all_arrays[:, 3] = xarray + self.all_arrays[:, 1] = d_to_q(xarray) + self.all_arrays[:, 2] = d_to_tth(xarray, self.wavelength) + self.qmin = np.nanmin(self.all_arrays[:, 1], initial=np.inf) + self.qmax = np.nanmax(self.all_arrays[:, 1], initial=0.0) + self.tthmin = np.nanmin(self.all_arrays[:, 2], initial=np.inf) + self.tthmax = np.nanmax(self.all_arrays[:, 2], initial=0.0) + self.dmin = np.nanmin(self.all_arrays[:, 3], initial=np.inf) + self.dmax = np.nanmax(self.all_arrays[:, 3], initial=0.0) + def insert_scattering_quantity( self, xarray, yarray, xtype, - metadata={}, - scat_quantity="", - name=None, - wavelength=None, ): f""" insert a new scattering quantity into the scattering object @@ -262,38 +294,14 @@ def insert_scattering_quantity( 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 - self.metadata = metadata - self.scat_quantity = scat_quantity - self.name = name - self.wavelength = wavelength - self.all_arrays = np.empty(shape=(len(yarray), 4)) + self._set_xarrays(xarray, xtype) self.all_arrays[:, 0] = yarray - if xtype.lower() in QQUANTITIES: - self.all_arrays[:, 1] = xarray - self.all_arrays[:, 2] = q_to_tth(xarray, wavelength) - self.all_arrays[:, 3] = q_to_d(xarray) - elif xtype.lower() in ANGLEQUANTITIES: - self.all_arrays[:, 2] = xarray - self.all_arrays[:, 1] = tth_to_q(xarray, wavelength) - self.all_arrays[:, 3] = tth_to_d(xarray, wavelength) - elif xtype.lower() in DQUANTITIES: - self.all_arrays[:, 3] = xarray - self.all_arrays[:, 1] = d_to_q(xarray) - self.all_arrays[:, 2] = d_to_tth(xarray, wavelength) - self.qmin = np.nanmin(self.all_arrays[:, 1], initial=np.inf) - self.qmax = np.nanmax(self.all_arrays[:, 1], initial=0.0) - self.tthmin = np.nanmin(self.all_arrays[:, 2], initial=np.inf) - self.tthmax = np.nanmax(self.all_arrays[:, 2], initial=0.0) - self.dmin = np.nanmin(self.all_arrays[:, 3], initial=np.inf) - self.dmax = np.nanmax(self.all_arrays[:, 3], initial=0.0) + self.input_xtype = xtype def _get_original_array(self): if self.input_xtype in QQUANTITIES: @@ -319,7 +327,7 @@ 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 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 @@ -351,6 +359,7 @@ def scale_to(self, target_diff_object, xtype=None, xvalue=None): 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 @@ -360,24 +369,25 @@ def on_xtype(self, xtype): """ if xtype.lower() in ANGLEQUANTITIES: - return self.on_tth + return self.on_tth() elif xtype.lower() in QQUANTITIES: - return self.on_q + return self.on_q() elif xtype.lower() in DQUANTITIES: - return self.on_d - pass + return self.on_d() + else: + warnings.warn(_xtype_wmsg(xtype)) def dump(self, filepath, xtype=None): if xtype is None: - xtype = " q" - if xtype == "q": + xtype = "q" + if xtype in QQUANTITIES: data_to_save = np.column_stack((self.on_q()[0], self.on_q()[1])) - elif xtype == "tth": + elif xtype in ANGLEQUANTITIES: data_to_save = np.column_stack((self.on_tth()[0], self.on_tth()[1])) - elif xtype == "d": + elif xtype in DQUANTITIES: data_to_save = np.column_stack((self.on_d()[0], self.on_d()[1])) else: - print(f"WARNING: cannot handle the xtype '{xtype}'") + warnings.warn(_xtype_wmsg(xtype)) self.metadata.update(get_package_info("diffpy.utils", metadata=self.metadata)) self.metadata["creation_time"] = datetime.datetime.now() diff --git a/tests/test_diffraction_objects.py b/tests/test_diffraction_objects.py index d04f5284..9db6932e 100644 --- a/tests/test_diffraction_objects.py +++ b/tests/test_diffraction_objects.py @@ -22,6 +22,9 @@ def compare_dicts(dict1, dict2): def dicts_equal(dict1, dict2): equal = True + print("") + print(dict1) + print(dict2) if not dict1.keys() == dict2.keys(): equal = False for key in dict1: @@ -29,11 +32,13 @@ def dicts_equal(dict1, dict2): if isinstance(val1, np.ndarray) and isinstance(val2, np.ndarray): if not np.allclose(val1, val2): equal = False + elif isinstance(val1, list) and isinstance(val2, list): + if not val1.all() == val2.all(): + equal = False elif isinstance(val1, np.float64) and isinstance(val2, np.float64): if not np.isclose(val1, val2): equal = False else: - print(key, val1, val2) if not val1 == val2: equal = False return equal @@ -187,12 +192,13 @@ def dicts_equal(dict1, dict2): @pytest.mark.parametrize("inputs1, inputs2, expected", params) def test_diffraction_objects_equality(inputs1, inputs2, expected): - diffraction_object1 = DiffractionObject(inputs1) - diffraction_object2 = DiffractionObject(inputs2) + diffraction_object1 = DiffractionObject(**inputs1) + diffraction_object2 = DiffractionObject(**inputs2) # 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]) + print(dicts_equal(diffraction_object1.__dict__, diffraction_object2.__dict__), expected) assert dicts_equal(diffraction_object1.__dict__, diffraction_object2.__dict__) == expected @@ -246,7 +252,7 @@ def test_dump(tmp_path, mocker): "metadata": {}, "input_xtype": "", "name": "", - "scat_quantity": "", + "scat_quantity": None, "qmin": np.float64(np.inf), "qmax": np.float64(0.0), "tthmin": np.float64(np.inf), @@ -291,7 +297,7 @@ def test_dump(tmp_path, mocker): "metadata": {}, "input_xtype": "tth", "name": "", - "scat_quantity": "", + "scat_quantity": None, "qmin": np.float64(0.0), "qmax": np.float64(1.0), "tthmin": np.float64(0.0), From daa9c3f1b311c5dfdd0216c39ff094d00d5be445 Mon Sep 17 00:00:00 2001 From: Simon Billinge Date: Wed, 4 Dec 2024 08:40:04 -0500 Subject: [PATCH 3/3] small tweaks and typo fixes --- news/constructor.rst | 2 +- src/diffpy/utils/diffraction_objects.py | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/news/constructor.rst b/news/constructor.rst index 9738c7a8..39ffa77f 100644 --- a/news/constructor.rst +++ b/news/constructor.rst @@ -6,7 +6,7 @@ * arrays and attributes now can be inserted when a DiffractionObject is instantiated * data are now stored as a (len(x),4) numpy array with intensity in column 0, the q, then tth, then d -* `DiffractionObject.on_q`, on_tth and on_d are now methods and called as DiffractionObject.on_q() etc.` +* `DiffractionObject.on_q`, `...on_tth` and `...on_d` are now methods and called as `DiffractionObject.on_q()` etc.` **Deprecated:** diff --git a/src/diffpy/utils/diffraction_objects.py b/src/diffpy/utils/diffraction_objects.py index 1daa62d0..54374fa7 100644 --- a/src/diffpy/utils/diffraction_objects.py +++ b/src/diffpy/utils/diffraction_objects.py @@ -21,14 +21,14 @@ def _xtype_wmsg(xtype): return ( - f"WARNING: I don't know how to handle the xtype, '{xtype}'. Please rerun specifying and " + f"WARNING: I don't know how to handle the xtype, '{xtype}'. Please rerun specifying an " f"xtype from {*XQUANTITIES, }" ) class DiffractionObject: def __init__( - self, name=None, wavelength=None, scat_quantity=None, metadata=None, xarray=None, yarray=None, xtype="" + self, name=None, wavelength=None, scat_quantity=None, metadata=None, xarray=None, yarray=None, xtype=None ): if name is None: name = "" @@ -36,6 +36,8 @@ def __init__( if metadata is None: metadata = {} self.metadata = metadata + if xtype is None: + xtype = "" self.scat_quantity = scat_quantity self.wavelength = wavelength @@ -282,6 +284,10 @@ def insert_scattering_quantity( xarray, yarray, xtype, + metadata={}, + scat_quantity=None, + name=None, + wavelength=None, ): f""" insert a new scattering quantity into the scattering object @@ -294,14 +300,27 @@ def insert_scattering_quantity( the dependent variable array xtype string the type of quantity for the independent variable from {*XQUANTITIES, } + metadata, scat_quantity, name and wavelength are optional. They have the same + meaning as in the constructor. Values will only be overwritten if non-empty values are passed. Returns ------- + Nothing. Updates the object in place. """ self._set_xarrays(xarray, xtype) self.all_arrays[:, 0] = yarray self.input_xtype = xtype + # only update these optional values if non-empty quantities are passed to avoid overwriting + # valid data inadvertently + 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 def _get_original_array(self): if self.input_xtype in QQUANTITIES: