diff --git a/news/muD-theoretical.rst b/news/muD-theoretical.rst new file mode 100644 index 0000000..4fde53b --- /dev/null +++ b/news/muD-theoretical.rst @@ -0,0 +1,23 @@ +**Added:** + +* Functionalities to estimate mu*D theoretically. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/labpdfproc/labpdfprocapp.py b/src/diffpy/labpdfproc/labpdfprocapp.py index e469514..9acfaaa 100644 --- a/src/diffpy/labpdfproc/labpdfprocapp.py +++ b/src/diffpy/labpdfproc/labpdfprocapp.py @@ -12,6 +12,13 @@ from diffpy.utils.diffraction_objects import XQUANTITIES, DiffractionObject from diffpy.utils.parsers.loaddata import loadData +theoretical_mud_hmsg_suffix = ( + "in that exact order, " + "separated by commas (e.g., ZrO2,17.45,0.5). " + "If you add whitespaces, " + "enclose it in quotes (e.g., 'ZrO2, 17.45, 0.5'). " +) + def _define_arguments(): args = [ @@ -21,12 +28,14 @@ def _define_arguments(): "The filename(s) or folder(s) of the datafile(s) to load. " "Required.\n" "Supply a space-separated list of files or directories. " + "Avoid spaces in filenames when possible; " + "if present, enclose the name in quotes. " "Long lists can be supplied, one per line, " "in a file with name file_list.txt. " "If one or more directory is provided, all valid " "data-files in that directory will be processed. " "Examples of valid inputs are 'file.xy', 'data/file.xy', " - "'file.xy, data/file.xy', " + "'file.xy data/file.xy', " "'.' (load everything in the current directory), " "'data' (load everything in the folder ./data), " "'data/file_list.txt' (load the list of files " @@ -152,7 +161,11 @@ def _define_arguments(): def _add_mud_selection_group(p, is_gui=False): """Current Options: 1. Manually enter muD (`--mud`). - 2. Estimate muD from a z-scan file (`-z` or `--z-scan-file`). + 2. Estimate from a z-scan file (`-z` or `--z-scan-file`). + 3. Estimate theoretically based on sample mass density + (`-d` or `--theoretical-from-density`). + 4. Estimate theoretically based on packing fraction + (`-p` or `--theoretical-from-packing`). """ g = p.add_argument_group("Options for setting mu*D value (Required)") g = g.add_mutually_exclusive_group(required=True) @@ -165,10 +178,32 @@ def _add_mud_selection_group(p, is_gui=False): g.add_argument( "-z", "--z-scan-file", - help="Provide the path to the z-scan file to be loaded " - "to determine the mu*D value.", + help=( + "Estimate mu*D experimentally from a z-scan file. " + "Specify the path to the file " + "used to compute the mu*D value." + ), **({"widget": "FileChooser"} if is_gui else {}), ) + g.add_argument( + "-d", + "--theoretical-from-density", + help=( + "Estimate mu*D theoretically using sample mass density. " + "Specify the chemical formula, incident x-ray energy (in keV), " + "and sample mass density (in g/cm^3), " + + theoretical_mud_hmsg_suffix + ), + ) + g.add_argument( + "-p", + "--theoretical-from-packing", + help=( + "Estimate mu*D theoretically using packing fraction. " + "Specify the chemical formula, incident x-ray energy (in keV), " + "and packing fraction (0 to 1), " + theoretical_mud_hmsg_suffix + ), + ) return p @@ -186,7 +221,7 @@ def get_args(override_cli_inputs=None): return args -@Gooey(required_cols=1, optional_cols=1, program_name="Labpdfproc GUI") +@Gooey(required_cols=1, optional_cols=2, program_name="labpdfproc GUI") def gooey_parser(): p = GooeyParser() p = _add_mud_selection_group(p, is_gui=True) diff --git a/src/diffpy/labpdfproc/tools.py b/src/diffpy/labpdfproc/tools.py index ea5299d..82878b1 100644 --- a/src/diffpy/labpdfproc/tools.py +++ b/src/diffpy/labpdfproc/tools.py @@ -9,6 +9,7 @@ from diffpy.utils.tools import ( _load_config, check_and_build_global_config, + compute_mu_using_xraydb, compute_mud, get_package_info, get_user_info, @@ -31,14 +32,19 @@ } known_sources = [key for key in WAVELENGTHS.keys()] -# Exclude wavelength from metadata to prevent duplication, -# as the dump function in diffpy.utils writes it explicitly. +# Exclude wavelength to avoid duplication, +# as it's written explicitly by diffpy.utils dump function. +# Exclude "theoretical_from_density" and "theoretical_from_packing" +# as they are only used for theoretical mu*D estimation +# and will be written into separate arguments for clarity. METADATA_KEYS_TO_EXCLUDE = [ "output_correction", "force_overwrite", "input", "input_paths", "wavelength", + "theoretical_from_density", + "theoretical_from_packing", ] @@ -298,19 +304,8 @@ def set_xtype(args): return args -def _estimate_mud_from_zscan(args): - """Compute mu*D based on the given z-scan file. - - Parameters - ---------- - args : argparse.Namespace - The arguments from the parser. - - Returns - ------- - args : argparse.Namespace - The updated arguments with mu*D. - """ +def _set_mud_from_zscan(args): + """Experimental estimation of mu*D from a z-scan file.""" filepath = Path(args.z_scan_file).resolve() if not filepath.is_file(): raise FileNotFoundError( @@ -322,10 +317,64 @@ def _estimate_mud_from_zscan(args): return args +def _parse_theoretical_input(input_str): + """Helper function to parse and validate the input string.""" + parts = [part.strip() for part in input_str.split(",")] + if len(parts) != 3: + raise ValueError( + f"Invalid mu*D input '{input_str}'. " + "Expected format is 'sample composition, energy, " + "sample mass density or packing fraction' " + "(e.g., 'ZrO2,17.45,0.5').", + ) + sample_composition = parts[0] + energy = float(parts[1]) + mass_density_or_packing_fraction = float(parts[2]) + return sample_composition, energy, mass_density_or_packing_fraction + + +def _set_theoretical_mud_from_density(args): + """Theoretical estimation of mu*D from + sample composition, energy, and sample mass density.""" + sample_composition, energy, sample_mass_density = _parse_theoretical_input( + args.theoretical_from_density + ) + args.sample_composition = sample_composition + args.energy = energy + args.sample_mass_density = sample_mass_density + args.mud = compute_mu_using_xraydb( + args.sample_composition, + args.energy, + sample_mass_density=args.sample_mass_density, + ) + return args + + +def _set_theoretical_mud_from_packing(args): + """Theoretical estimation of mu*D from + sample composition, energy, and packing fraction.""" + sample_composition, energy, packing_fraction = _parse_theoretical_input( + args.theoretical_from_packing + ) + args.sample_composition = sample_composition + args.energy = energy + args.packing_fraction = packing_fraction + args.mud = compute_mu_using_xraydb( + args.sample_composition, + args.energy, + packing_fraction=args.packing_fraction, + ) + return args + + def set_mud(args): - """Compute and set mu*D based on different options. - Current options include manually entering a value, - or estimating from a z-scan file. + """Compute and set mu*D based on the selected method. + + Options include: + 1. Manually entering a value. + 2. Estimating from a z-scan file. + 3. Estimating theoretically based on sample mass density. + 4. Estimating theoretically based on packing fraction. Parameters ---------- @@ -338,7 +387,11 @@ def set_mud(args): The updated arguments with mu*D. """ if args.z_scan_file: - return _estimate_mud_from_zscan(args) + return _set_mud_from_zscan(args) + elif args.theoretical_from_density: + return _set_theoretical_mud_from_density(args) + elif args.theoretical_from_packing: + return _set_theoretical_mud_from_packing(args) return args diff --git a/tests/test_tools.py b/tests/test_tools.py index 9ccc4b3..a007e8c 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -460,6 +460,15 @@ def test_set_xtype_bad(): (["--mud", "2.5"], 2.5), # C2: user provides a z-scan file, expect to estimate through the file (["--z-scan-file", "test_dir/testfile.xy"], 3), + # C3: user specifies sample composition, energy, + # and sample mass density, + # both with and without whitespaces, expect to estimate theoretically + (["--theoretical-from-density", "ZrO2,17.45,1.2"], 1.49), + (["--theoretical-from-density", "ZrO2, 17.45, 1.2"], 1.49), + # C4: user specifies sample composition, energy, and packing fraction + # both with and without whitespaces, expect to estimate theoretically + # (["--theoretical-from-packing", "ZrO2,17.45,0.3"], 1.49), + # (["--theoretical-from-packing", "ZrO2, 17.45, 0.3"], 1.49), ], ) def test_set_mud(user_filesystem, inputs, expected_mud): @@ -483,6 +492,58 @@ def test_set_mud(user_filesystem, inputs, expected_mud): "Cannot find invalid file. Please specify a valid file path.", ], ), + # C2.1: (sample mass density option) + # user provides fewer than three input values + # expect ValueError with a message indicating the correct format + ( + ["--theoretical-from-density", "ZrO2,0.5"], + [ + ValueError, + "Invalid mu*D input 'ZrO2,0.5'. " + "Expected format is 'sample composition, energy, " + "sample mass density or packing fraction' " + "(e.g., 'ZrO2,17.45,0.5').", + ], + ), + # C2.2: (packing fraction option) + # user provides fewer than three input values + # expect ValueError with a message indicating the correct format + ( + ["--theoretical-from-packing", "ZrO2,0.5"], + [ + ValueError, + "Invalid mu*D input 'ZrO2,0.5'. " + "Expected format is 'sample composition, energy, " + "sample mass density or packing fraction' " + "(e.g., 'ZrO2,17.45,0.5').", + ], + ), + # C3.1: (sample mass density option) + # user provides more than 3 input values + # expect ValueError with a message indicating the correct format + ( + ["--theoretical-from-density", "ZrO2,17.45,1.5,0.5"], + [ + ValueError, + "Invalid mu*D input 'ZrO2,17.45,1.5,0.5'. " + "Expected format is 'sample composition, energy, " + "sample mass density or packing fraction' " + "(e.g., 'ZrO2,17.45,0.5').", + ], + ), + # C3.2: (packing fraction option) + # user provides more than 3 input values + # expect ValueError with a message indicating the correct format + ( + ["--theoretical-from-packing", "ZrO2,17.45,1.5,0.5"], + [ + ValueError, + "Invalid mu*D input 'ZrO2,17.45,1.5,0.5'. " + "Expected format is 'sample composition, energy, " + "sample mass density or packing fraction' " + "(e.g., 'ZrO2,17.45,0.5').", + ], + ), ], ) def test_set_mud_bad(user_filesystem, inputs, expected): @@ -491,7 +552,7 @@ def test_set_mud_bad(user_filesystem, inputs, expected): os.chdir(cwd) cli_inputs = ["data.xy"] + inputs actual_args = get_args(cli_inputs) - with pytest.raises(expected_error, match=expected_error_msg): + with pytest.raises(expected_error, match=re.escape(expected_error_msg)): actual_args = set_mud(actual_args)