diff --git a/news/utils-updates.rst b/news/utils-updates.rst new file mode 100644 index 0000000..fd6d1fc --- /dev/null +++ b/news/utils-updates.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* Functions that use DiffractionObject` in `diffpy.utils` to follow the new API. + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/labpdfproc/functions.py b/src/diffpy/labpdfproc/functions.py index 35d587c..7f93e10 100644 --- a/src/diffpy/labpdfproc/functions.py +++ b/src/diffpy/labpdfproc/functions.py @@ -5,11 +5,13 @@ import pandas as pd from scipy.interpolate import interp1d -from diffpy.utils.scattering_objects.diffraction_objects import XQUANTITIES, Diffraction_object +from diffpy.utils.diffraction_objects import XQUANTITIES, DiffractionObject RADIUS_MM = 1 N_POINTS_ON_DIAMETER = 300 TTH_GRID = np.arange(1, 180.1, 0.1) +# Round down the last element if it's slightly above 180 due to floating point precision +TTH_GRID[-1] = 180.00 CVE_METHODS = ["brute_force", "polynomial_interpolation"] # pre-computed datasets for polynomial interpolation (fast calculation) @@ -191,14 +193,14 @@ def _cve_brute_force(diffraction_data, mud): muls = np.array(muls) / abs_correction.total_points_in_grid cve = 1 / muls - cve_do = Diffraction_object(wavelength=diffraction_data.wavelength) - cve_do.insert_scattering_quantity( - TTH_GRID, - cve, - "tth", - metadata=diffraction_data.metadata, - name=f"absorption correction, cve, for {diffraction_data.name}", + cve_do = DiffractionObject( + xarray=TTH_GRID, + yarray=cve, + xtype="tth", + wavelength=diffraction_data.wavelength, scat_quantity="cve", + name=f"absorption correction, cve, for {diffraction_data.name}", + metadata=diffraction_data.metadata, ) return cve_do @@ -211,7 +213,7 @@ def _cve_polynomial_interpolation(diffraction_data, mud): if mud > 6 or mud < 0.5: raise ValueError( f"mu*D is out of the acceptable range (0.5 to 6) for polynomial interpolation. " - f"Please rerun with a value within this range or specifying another method from {* CVE_METHODS, }." + f"Please rerun with a value within this range or specifying another method from {*CVE_METHODS, }." ) coeff_a, coeff_b, coeff_c, coeff_d, coeff_e = [ interpolation_function(mud) for interpolation_function in INTERPOLATION_FUNCTIONS @@ -219,14 +221,14 @@ def _cve_polynomial_interpolation(diffraction_data, mud): muls = np.array(coeff_a * MULS**4 + coeff_b * MULS**3 + coeff_c * MULS**2 + coeff_d * MULS + coeff_e) cve = 1 / muls - cve_do = Diffraction_object(wavelength=diffraction_data.wavelength) - cve_do.insert_scattering_quantity( - TTH_GRID, - cve, - "tth", - metadata=diffraction_data.metadata, - name=f"absorption correction, cve, for {diffraction_data.name}", + cve_do = DiffractionObject( + xarray=TTH_GRID, + yarray=cve, + xtype="tth", + wavelength=diffraction_data.wavelength, scat_quantity="cve", + name=f"absorption correction, cve, for {diffraction_data.name}", + metadata=diffraction_data.metadata, ) return cve_do @@ -257,7 +259,7 @@ def compute_cve(diffraction_data, mud, method="polynomial_interpolation", xtype= xtype str the quantity on the independent variable axis, allowed values are {*XQUANTITIES, } method str - the method used to calculate cve, must be one of {* CVE_METHODS, } + the method used to calculate cve, must be one of {*CVE_METHODS, } Returns ------- @@ -270,14 +272,14 @@ def compute_cve(diffraction_data, mud, method="polynomial_interpolation", xtype= global_xtype = cve_do_on_global_grid.on_xtype(xtype)[0] cve_on_global_xtype = cve_do_on_global_grid.on_xtype(xtype)[1] newcve = np.interp(orig_grid, global_xtype, cve_on_global_xtype) - cve_do = Diffraction_object(wavelength=diffraction_data.wavelength) - cve_do.insert_scattering_quantity( - orig_grid, - newcve, - xtype, - metadata=diffraction_data.metadata, - name=f"absorption correction, cve, for {diffraction_data.name}", + cve_do = DiffractionObject( + xarray=orig_grid, + yarray=newcve, + xtype=xtype, + wavelength=diffraction_data.wavelength, scat_quantity="cve", + name=f"absorption correction, cve, for {diffraction_data.name}", + metadata=diffraction_data.metadata, ) return cve_do diff --git a/src/diffpy/labpdfproc/labpdfprocapp.py b/src/diffpy/labpdfproc/labpdfprocapp.py index 607d772..c17b8fb 100644 --- a/src/diffpy/labpdfproc/labpdfprocapp.py +++ b/src/diffpy/labpdfproc/labpdfprocapp.py @@ -5,8 +5,8 @@ from diffpy.labpdfproc.functions import CVE_METHODS, apply_corr, compute_cve from diffpy.labpdfproc.tools import known_sources, load_metadata, preprocessing_args +from diffpy.utils.diffraction_objects import XQUANTITIES, DiffractionObject from diffpy.utils.parsers.loaddata import loadData -from diffpy.utils.scattering_objects.diffraction_objects import XQUANTITIES, Diffraction_object def define_arguments(): @@ -170,12 +170,12 @@ def main(): f"exists. Please rerun specifying -f if you want to overwrite it." ) - input_pattern = Diffraction_object(wavelength=args.wavelength) xarray, yarray = loadData(filepath, unpack=True) - input_pattern.insert_scattering_quantity( - xarray, - yarray, - args.xtype, + input_pattern = DiffractionObject( + xarray=xarray, + yarray=yarray, + xtype=args.xtype, + wavelength=args.wavelength, scat_quantity="x-ray", name=filepath.stem, metadata=load_metadata(args, filepath), diff --git a/src/diffpy/labpdfproc/mud_calculator.py b/src/diffpy/labpdfproc/mud_calculator.py deleted file mode 100644 index 604d167..0000000 --- a/src/diffpy/labpdfproc/mud_calculator.py +++ /dev/null @@ -1,109 +0,0 @@ -import numpy as np -from scipy.optimize import dual_annealing -from scipy.signal import convolve - -from diffpy.utils.parsers.loaddata import loadData - - -def _top_hat(z, half_slit_width): - """ - Create a top-hat function, return 1.0 for values within the specified slit width and 0 otherwise - """ - return np.where((z >= -half_slit_width) & (z <= half_slit_width), 1.0, 0.0) - - -def _model_function(z, diameter, z0, I0, mud, slope): - """ - Compute the model function with the following steps: - 1. Let dz = z-z0, so that dz is centered at 0 - 2. Compute length l that is the effective length for computing intensity I = I0 * e^{-mu * l}: - - For dz within the capillary diameter, l is the chord length of the circle at position dz - - For dz outside this range, l = 0 - 3. Apply a linear adjustment to I0 by taking I0 as I0 - slope * z - """ - min_radius = -diameter / 2 - max_radius = diameter / 2 - dz = z - z0 - length = np.piecewise( - dz, - [dz < min_radius, (min_radius <= dz) & (dz <= max_radius), dz > max_radius], - [0, lambda dz: 2 * np.sqrt((diameter / 2) ** 2 - dz**2), 0], - ) - return (I0 - slope * z) * np.exp(-mud / diameter * length) - - -def _extend_z_and_convolve(z, diameter, half_slit_width, z0, I0, mud, slope): - """ - extend z values and I values for padding (so that we don't have tails in convolution), then perform convolution - (note that the convolved I values are the same as modeled I values if slit width is close to 0) - """ - n_points = len(z) - z_left_pad = np.linspace(z.min() - n_points * (z[1] - z[0]), z.min(), n_points) - z_right_pad = np.linspace(z.max(), z.max() + n_points * (z[1] - z[0]), n_points) - z_extended = np.concatenate([z_left_pad, z, z_right_pad]) - I_extended = _model_function(z_extended, diameter, z0, I0, mud, slope) - kernel = _top_hat(z_extended - z_extended.mean(), half_slit_width) - I_convolved = I_extended # this takes care of the case where slit width is close to 0 - if kernel.sum() != 0: - kernel /= kernel.sum() - I_convolved = convolve(I_extended, kernel, mode="same") - padding_length = len(z_left_pad) - return I_convolved[padding_length:-padding_length] - - -def _objective_function(params, z, observed_data): - """ - Compute the objective function for fitting a model to the observed/experimental data - by minimizing the sum of squared residuals between the observed data and the convolved model data - """ - diameter, half_slit_width, z0, I0, mud, slope = params - convolved_model_data = _extend_z_and_convolve(z, diameter, half_slit_width, z0, I0, mud, slope) - residuals = observed_data - convolved_model_data - return np.sum(residuals**2) - - -def _compute_single_mud(z_data, I_data): - """ - Perform dual annealing optimization and extract the parameters - """ - bounds = [ - (1e-5, z_data.max() - z_data.min()), # diameter: [small positive value, upper bound] - (0, (z_data.max() - z_data.min()) / 2), # half slit width: [0, upper bound] - (z_data.min(), z_data.max()), # z0: [min z, max z] - (1e-5, I_data.max()), # I0: [small positive value, max observed intensity] - (1e-5, 20), # muD: [small positive value, upper bound] - (-100000, 100000), # slope: [lower bound, upper bound] - ] - result = dual_annealing(_objective_function, bounds, args=(z_data, I_data)) - diameter, half_slit_width, z0, I0, mud, slope = result.x - convolved_fitted_signal = _extend_z_and_convolve(z_data, diameter, half_slit_width, z0, I0, mud, slope) - residuals = I_data - convolved_fitted_signal - rmse = np.sqrt(np.mean(residuals**2)) - return mud, rmse - - -def compute_mud(filepath): - """Compute the best-fit mu*D value from a z-scan file, removing the sample holder effect. - - This function loads z-scan data and fits it to a model - that convolves a top-hat function with I = I0 * e^{-mu * l}. - The fitting procedure is run multiple times, and we return the best-fit parameters based on the lowest rmse. - - The full mathematical details are described in the paper: - An ad hoc Absorption Correction for Reliable Pair-Distribution Functions from Low Energy x-ray Sources, - Yucong Chen, Till Schertenleib, Andrew Yang, Pascal Schouwink, Wendy L. Queen and Simon J. L. Billinge, - in preparation. - - Parameters - ---------- - filepath : str - The path to the z-scan file. - - Returns - ------- - mu*D : float - The best-fit mu*D value. - """ - z_data, I_data = loadData(filepath, unpack=True) - best_mud, _ = min((_compute_single_mud(z_data, I_data) for _ in range(20)), key=lambda pair: pair[1]) - return best_mud diff --git a/src/diffpy/labpdfproc/tools.py b/src/diffpy/labpdfproc/tools.py index 02b5451..8fcf0c3 100644 --- a/src/diffpy/labpdfproc/tools.py +++ b/src/diffpy/labpdfproc/tools.py @@ -1,11 +1,10 @@ import copy from pathlib import Path -from diffpy.labpdfproc.mud_calculator import compute_mud -from diffpy.utils.scattering_objects.diffraction_objects import QQUANTITIES, XQUANTITIES -from diffpy.utils.tools import get_package_info, get_user_info +from diffpy.utils.diffraction_objects import ANGLEQUANTITIES, QQUANTITIES, XQUANTITIES +from diffpy.utils.tools import check_and_build_global_config, compute_mud, get_package_info, get_user_info -WAVELENGTHS = {"Mo": 0.71, "Ag": 0.59, "Cu": 1.54} +WAVELENGTHS = {"Mo": 0.71073, "Ag": 0.59, "Cu": 1.5406} known_sources = [key for key in WAVELENGTHS.keys()] # Exclude wavelength from metadata to prevent duplication, @@ -154,7 +153,9 @@ def set_xtype(args): """ if args.xtype.lower() not in XQUANTITIES: raise ValueError(f"Unknown xtype: {args.xtype}. Allowed xtypes are {*XQUANTITIES, }.") - args.xtype = "q" if args.xtype.lower() in QQUANTITIES else "tth" + args.xtype = ( + "q" if args.xtype.lower() in QQUANTITIES else "tth" if args.xtype.lower() in ANGLEQUANTITIES else "d" + ) return args @@ -224,7 +225,8 @@ def load_user_metadata(args): def load_user_info(args): """ - Update username and email using get_user_info function from diffpy.utils + Load user info into args. If args are not provided, call check_and_build_global_config function from + diffpy.utils to prompt the user for inputs. Otherwise, call get_user_info with the provided arguments. Parameters ---------- @@ -236,10 +238,11 @@ def load_user_info(args): the updated argparse Namespace with username and email inserted """ - config = {"username": args.username, "email": args.email} - config = get_user_info(config) - args.username = config["username"] - args.email = config["email"] + if args.username is None or args.email is None: + check_and_build_global_config() + config = get_user_info(owner_name=args.username, owner_email=args.email) + args.username = config.get("owner_name") + args.email = config.get("owner_email") return args diff --git a/tests/conftest.py b/tests/conftest.py index 603ae8a..d3b4bdc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,7 +49,11 @@ def user_filesystem(tmp_path): f.write("good_data.xy \n") f.write(f"{str(input_dir.resolve() / 'good_data.txt')}\n") - home_config_data = {"username": "home_username", "email": "home@email.com"} + home_config_data = { + "owner_name": "home_username", + "owner_email": "home@email.com", + "owner_orcid": "home_orcid", + } with open(home_dir / "diffpyconfig.json", "w") as f: json.dump(home_config_data, f) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 8107bb1..d7a05d2 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -2,7 +2,7 @@ import pytest -from diffpy.utils.parsers import loadData +from diffpy.utils.parsers.loaddata import loadData # Test that our readable and unreadable files are indeed readable and diff --git a/tests/test_functions.py b/tests/test_functions.py index f23c32d..93a067a 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -4,7 +4,7 @@ import pytest from diffpy.labpdfproc.functions import CVE_METHODS, Gridded_circle, apply_corr, compute_cve -from diffpy.utils.scattering_objects.diffraction_objects import Diffraction_object +from diffpy.utils.diffraction_objects import DiffractionObject params1 = [ ([0.5, 3, 1], {(0.0, -0.5), (0.0, 0.0), (0.5, 0.0), (-0.5, 0.0), (0.0, 0.5)}), @@ -59,11 +59,11 @@ def test_set_muls_at_angle(inputs, expected): def _instantiate_test_do(xarray, yarray, xtype="tth", name="test", scat_quantity="x-ray"): - test_do = Diffraction_object(wavelength=1.54) - test_do.insert_scattering_quantity( - xarray, - yarray, - xtype, + test_do = DiffractionObject( + xarray=xarray, + yarray=yarray, + xtype=xtype, + wavelength=1.54, scat_quantity=scat_quantity, name=name, metadata={"thing1": 1, "thing2": "thing2"}, @@ -81,14 +81,13 @@ def _instantiate_test_do(xarray, yarray, xtype="tth", name="test", scat_quantity def test_compute_cve(inputs, expected, mocker): xarray, yarray = np.array([90, 90.1, 90.2]), np.array([2, 2, 2]) expected_cve = np.array([0.5, 0.5, 0.5]) - mocker.patch("diffpy.labpdfproc.functions.TTH_GRID", xarray) mocker.patch("numpy.interp", return_value=expected_cve) input_pattern = _instantiate_test_do(xarray, yarray) actual_cve_do = compute_cve(input_pattern, mud=1, method="polynomial_interpolation", xtype=inputs[0]) expected_cve_do = _instantiate_test_do( - expected[0], - expected[1], - expected[2], + xarray=expected[0], + yarray=expected[1], + xtype=expected[2], name="absorption correction, cve, for test", scat_quantity="cve", ) @@ -126,8 +125,8 @@ def test_apply_corr(mocker): mocker.patch("numpy.interp", return_value=expected_cve) input_pattern = _instantiate_test_do(xarray, yarray) absorption_correction = _instantiate_test_do( - xarray, - expected_cve, + xarray=xarray, + yarray=expected_cve, name="absorption correction, cve, for test", scat_quantity="cve", ) diff --git a/tests/test_mud_calculator.py b/tests/test_mud_calculator.py deleted file mode 100644 index 551adb5..0000000 --- a/tests/test_mud_calculator.py +++ /dev/null @@ -1,22 +0,0 @@ -from pathlib import Path - -import numpy as np -import pytest - -from diffpy.labpdfproc.mud_calculator import _extend_z_and_convolve, compute_mud - - -def test_compute_mud(tmp_path): - diameter, slit_width, z0, I0, mud, slope = 1, 0.1, 0, 1e5, 3, 0 - z_data = np.linspace(-1, 1, 50) - convolved_I_data = _extend_z_and_convolve(z_data, diameter, slit_width, z0, I0, mud, slope) - - directory = Path(tmp_path) - file = directory / "testfile" - with open(file, "w") as f: - for x, I in zip(z_data, convolved_I_data): - f.write(f"{x}\t{I}\n") - - expected_mud = 3 - actual_mud = compute_mud(file) - assert actual_mud == pytest.approx(expected_mud, rel=1e-4, abs=1e-3) diff --git a/tests/test_tools.py b/tests/test_tools.py index f7c6425..bcee89d 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -18,7 +18,7 @@ set_wavelength, set_xtype, ) -from diffpy.utils.scattering_objects.diffraction_objects import XQUANTITIES +from diffpy.utils.diffraction_objects import XQUANTITIES # Use cases can be found here: https://github.com/diffpy/diffpy.labpdfproc/issues/48 @@ -150,7 +150,7 @@ def test_set_output_directory_bad(user_filesystem): params2 = [ - ([], [0.71, "Mo"]), + ([], [0.71073, "Mo"]), (["--anode-type", "Ag"], [0.59, "Ag"]), (["--wavelength", "0.25"], [0.25, None]), (["--wavelength", "0.25", "--anode-type", "Ag"], [0.25, None]), @@ -194,7 +194,7 @@ def test_set_wavelength_bad(inputs, msg): params4 = [ ([], ["tth"]), (["--xtype", "2theta"], ["tth"]), - (["--xtype", "d"], ["tth"]), + (["--xtype", "d"], ["d"]), (["--xtype", "q"], ["q"]), ]