diff --git a/news/smear-update.rst b/news/smear-update.rst new file mode 100644 index 00000000..89e0e559 --- /dev/null +++ b/news/smear-update.rst @@ -0,0 +1,23 @@ +**Added:** + +* New --smear option applies the smear morph directly to the function (without transforming to RDF). + +**Changed:** + +* Former --smear option renamed to --smear-pdf (converts PDF to RDF before applying the smear morph). + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/morph/morphapp.py b/src/diffpy/morph/morphapp.py index 2b63b473..891ce652 100755 --- a/src/diffpy/morph/morphapp.py +++ b/src/diffpy/morph/morphapp.py @@ -196,16 +196,31 @@ def custom_error(self, msg): type="float", metavar="SMEAR", help=( - "Smear peaks with a Gaussian of width SMEAR. " - "This convolves the function with a Gaussian of width SMEAR." + "Smear the peaks with a Gaussian of width SMEAR. " + "This is done by convolving the function with a " + "Gaussian with standard deviation SMEAR. " + "If both --smear and --smear-pdf are enabled, " + "only --smear-pdf will be applied." + ), + ) + group.add_option( + "--smear-pdf", + type="float", + metavar="SMEAR", + help=( + "Convert PDF to RDF. " + "Then, smear peaks with a Gaussian of width SMEAR. " + "Convert back to PDF. " + "If both --smear and --smear-pdf are enabled, " + "only --smear-pdf will be applied." ), ) group.add_option( "--slope", type="float", dest="baselineslope", - help="""Slope of the baseline. This is used when applying the smear - factor. It will be estimated if not provided.""", + help="""Slope of the baseline. This is used with the option --smear-pdf + to convert from the PDF to RDF. It will be estimated if not provided.""", ) group.add_option( "--hshift", @@ -520,8 +535,8 @@ def single_morph(parser, opts, pargs, stdout_flag=True): config["vshift"] = vshift_in refpars.append("vshift") # Smear - if opts.smear is not None: - smear_in = opts.smear + if opts.smear_pdf is not None: + smear_in = opts.smear_pdf chain.append(helpers.TransformXtalPDFtoRDF()) chain.append(morphs.MorphSmear()) chain.append(helpers.TransformXtalRDFtoPDF()) @@ -532,6 +547,11 @@ def single_morph(parser, opts, pargs, stdout_flag=True): if opts.baselineslope is None: config["baselineslope"] = -0.5 refpars.append("baselineslope") + elif opts.smear is not None: + smear_in = opts.smear + chain.append(morphs.MorphSmear()) + refpars.append("smear") + config["smear"] = smear_in # Size radii = [opts.radius, opts.pradius] nrad = 2 - radii.count(None) @@ -806,7 +826,7 @@ def multiple_targets(parser, opts, pargs, stdout_flag=True): morph_inputs = { "scale": opts.scale, "stretch": opts.stretch, - "smear": opts.smear, + "smear": opts.smear_pdf, } morph_inputs.update({"hshift": opts.hshift, "vshift": opts.vshift}) @@ -989,7 +1009,7 @@ def multiple_morphs(parser, opts, pargs, stdout_flag=True): morph_inputs = { "scale": opts.scale, "stretch": opts.stretch, - "smear": opts.smear, + "smear": opts.smear_pdf, } morph_inputs.update({"hshift": opts.hshift, "vshift": opts.vshift}) diff --git a/tests/test_morphapp.py b/tests/test_morphapp.py index b66ec0e7..3d6fc63f 100644 --- a/tests/test_morphapp.py +++ b/tests/test_morphapp.py @@ -2,6 +2,7 @@ from pathlib import Path +import numpy as np import pytest from diffpy.morph.morphapp import ( @@ -255,6 +256,68 @@ def test_morphsequence(self, setup_morphsequence): ) assert s_sequence_results == sequence_results + def test_morphsmear(self, setup_parser, tmp_path): + def gaussian(x, mu, sigma): + return np.exp(-((x - mu) ** 2) / (2 * sigma**2)) / ( + sigma * np.sqrt(2 * np.pi) + ) + + # Generate the test files + x_grid = np.linspace(1, 101, 1001) + # Gaussian with STD 3 (morph) + g2 = gaussian(x_grid, 51, 3) + mf = tmp_path / "morph.txt" + with open(mf, "w") as f: + np.savetxt(f, np.array([x_grid, g2]).T) + # Gaussian with STD 5 (target) + g3 = gaussian(x_grid, 51, 5) + tf = tmp_path / "target.txt" + with open(tf, "w") as f: + np.savetxt(f, np.array([x_grid, g3]).T) + # Gaussian with STD 3 and baseline slope -0.5 (PDF morph) + g2_bl = gaussian(x_grid, 51, 3) / x_grid - 0.5 * x_grid + pmf = tmp_path / "pdf_morph.txt" + with open(pmf, "w") as f: + np.savetxt(f, np.array([x_grid, g2_bl]).T) + # Gaussian with STD 5 with baseline slope -0.5 (PDF target) + g3_bl = gaussian(x_grid, 51, 5) / x_grid - 0.5 * x_grid + ptf = tmp_path / "pdf_target.txt" + with open(ptf, "w") as f: + np.savetxt(f, np.array([x_grid, g3_bl]).T) + + # No PDF smear (should not activate baseline slope) + (opts, _) = self.parser.parse_args( + [ + "--smear", + "1", + "-n", + ] + ) + pargs = [mf, tf] + smear_results = single_morph( + self.parser, opts, pargs, stdout_flag=False + ) + # Variances add, and 3^2+4^2=5^2 + assert pytest.approx(abs(smear_results["smear"])) == 4.0 + assert pytest.approx(smear_results["Rw"]) == 0.0 + + # PDF-specific smear (should activate baseline slope) + (opts, _) = self.parser.parse_args( + [ + "--smear", + "100", + "--smear-pdf", + "1", + "-n", + ] + ) + pargs = [pmf, ptf] + pdf_smear_results = single_morph( + self.parser, opts, pargs, stdout_flag=False + ) + assert pytest.approx(abs(pdf_smear_results["smear"])) == 4.0 + assert pytest.approx(pdf_smear_results["Rw"]) == 0.0 + if __name__ == "__main__": TestApp()