Skip to content

Commit d4363bd

Browse files
authored
Add --smear-function option and Lorentzian smearing (#309)
* Add Lorentzian smearing * More verbose test descriptors * Fix headers for tests
1 parent 9981f82 commit d4363bd

13 files changed

Lines changed: 3332 additions & 18 deletions

File tree

docs/source/morphpy.rst

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,17 +139,28 @@ squeeze: list of float
139139
When this parameter is given, hshift is disabled.
140140
When n>1, stretch is disabled.
141141
smear: float
142-
Smear the peaks with a Gaussian of width smear.
142+
Smear the peaks with a function of width SMEAR.
143143

144+
The smearing function is chosen by the --smear-function option
145+
(default is Gaussian if that option is not enabled).
144146
This is done by convolving the function with a Gaussian
145147
with standard deviation smear. If both smear and
146148
smear_pdf are used, only smear_pdf will be
147149
applied.
148150
smear_pdf: float
149151
Convert PDF to RDF. Then, smear peaks with a Gaussian
150-
of width smear_pdf. Convert back to PDF. If both smear and
152+
of width smear_pdf.
153+
The smearing function is chosen by the --smear-function option
154+
(default is Gaussian if that option is not enabled).
155+
Convert back to PDF. If both smear and
151156
smear_pdf are used, only smear_pdf will be
152157
applied.
158+
smear_func: str
159+
Choose the function for the smear morph.
160+
Only used if --smear or --smear-pdf is enabled.
161+
Available options: Gaussian (default) and Lorentzian.
162+
If Gaussian, the SMEAR parameter is (+/-) the standard deviation.
163+
If Lorentzian, the SMEAR parameter is (+/-) the half width half maximum.
153164
slope: float
154165
Slope of the baseline used in converting from PDF to RDF.
155166

news/general-smear.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* New option `--smear-function` allows user to select either gaussian or lorentzian smearing. The default is still gaussian, so no workflow changes are needed for those who are using the gaussian smearing.
4+
5+
**Changed:**
6+
7+
* The choice of smear function is now saved to the file header when `--verbose` is enabled.
8+
9+
**Deprecated:**
10+
11+
* <news item>
12+
13+
**Removed:**
14+
15+
* <news item>
16+
17+
**Fixed:**
18+
19+
* <news item>
20+
21+
**Security:**
22+
23+
* <news item>

src/diffpy/morph/morph_io.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def build_morph_inputs_container(
3939
stretch,
4040
smear_pdf,
4141
smear,
42+
smear_func,
4243
hshift,
4344
vshift,
4445
squeeze,
@@ -104,11 +105,27 @@ def build_morph_inputs_container(
104105
smear_in = smear
105106
else:
106107
smear_in = smear_pdf
107-
morph_inputs = {
108-
"scale": scale_in,
109-
"stretch": stretch_in,
110-
"smear": smear_in,
111-
}
108+
if smear_func is not None:
109+
smear_func = smear_func.lower()
110+
if smear_func is None or smear_func == "gaussian":
111+
smear_func_in = "gaussian"
112+
elif smear_func == "lorentzian":
113+
smear_func_in = "lorentzian"
114+
else:
115+
smear_func_in = "unknown"
116+
if smear_in is None:
117+
morph_inputs = {
118+
"scale": scale_in,
119+
"stretch": stretch_in,
120+
"smear": smear_in,
121+
}
122+
else:
123+
morph_inputs = {
124+
"scale": scale_in,
125+
"stretch": stretch_in,
126+
"smear": smear_in,
127+
"smear-function": smear_func_in,
128+
}
112129

113130
if squeeze_poly_deg < 0:
114131
hshift_in = hshift
@@ -126,6 +143,11 @@ def build_morph_inputs_container(
126143

127144
def get_terminal_morph_output(mr_copy, uncertainties):
128145
morphs_out = "# Optimized morphing parameters:\n"
146+
147+
# Handle special inputs (strings)
148+
if "smear_func" in mr_copy:
149+
mr_copy.pop("smear_func")
150+
129151
# Handle special inputs (numerical)
130152
if "squeeze" in mr_copy:
131153
sq_dict = mr_copy.pop("squeeze")

src/diffpy/morph/morphapp.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,11 @@ def morph_error(self, msg, error):
245245
type="float",
246246
metavar="SMEAR",
247247
help=(
248-
"Smear the peaks with a Gaussian of width SMEAR. "
248+
"Smear the peaks with a function of width SMEAR. "
249+
"The smearing function is chosen by the --smear-function option "
250+
"(default is Gaussian if that option is not enabled). "
249251
"This is done by convolving the function with a "
250-
"Gaussian with standard deviation SMEAR. "
252+
"function with width SMEAR. "
251253
"If both --smear and --smear-pdf are enabled, "
252254
"only --smear-pdf will be applied."
253255
),
@@ -258,12 +260,29 @@ def morph_error(self, msg, error):
258260
metavar="SMEAR",
259261
help=(
260262
"Convert PDF to RDF. "
261-
"Then, smear peaks with a Gaussian of width SMEAR. "
263+
"Then, smear peaks with a function of width SMEAR. "
264+
"The smearing function is chosen by the --smear-function option "
265+
"(default is Gaussian if that option is not enabled). "
262266
"Convert back to PDF. "
263267
"If both --smear and --smear-pdf are enabled, "
264268
"only --smear-pdf will be applied."
265269
),
266270
)
271+
group.add_option(
272+
"--smear-function",
273+
"--smear-func",
274+
dest="smear_func",
275+
metavar="SMEARFUNCTION",
276+
help=(
277+
"Choose the function for the smear morph. "
278+
"Only used if --smear or --smear-pdf is enabled. "
279+
"Available options: Gaussian (default) and Lorentzian. "
280+
"If Gaussian, the SMEAR parameter is the standard "
281+
"deviation. "
282+
"If Lorentzian, the SMEAR parameter is the half width "
283+
"half maximum."
284+
),
285+
)
267286
group.add_option(
268287
"--slope",
269288
type="float",
@@ -665,13 +684,17 @@ def single_morph(
665684
config["stretch"] = stretch_in
666685
refpars.append("stretch")
667686
# Smear
687+
smear_func = "gaussian"
688+
if opts.smear_func is not None:
689+
smear_func = opts.smear_func.lower()
668690
if opts.smear_pdf is not None:
669691
smear_in = opts.smear_pdf
670692
chain.append(helpers.TransformXtalPDFtoRDF())
671693
chain.append(morphs.MorphSmear())
672694
chain.append(helpers.TransformXtalRDFtoPDF())
673695
refpars.append("smear")
674696
config["smear"] = smear_in
697+
config["smear_func"] = smear_func
675698
# Set baselineslope if not given
676699
config["baselineslope"] = opts.baselineslope
677700
if opts.baselineslope is None:
@@ -682,6 +705,7 @@ def single_morph(
682705
chain.append(morphs.MorphSmear())
683706
refpars.append("smear")
684707
config["smear"] = smear_in
708+
config["smear_func"] = smear_func
685709
# Shift
686710
# Only enable hshift is squeeze is not enabled
687711
shift_morph = None
@@ -825,6 +849,7 @@ def single_morph(
825849
opts.stretch,
826850
opts.smear_pdf,
827851
opts.smear,
852+
opts.smear_func,
828853
opts.hshift,
829854
opts.vshift,
830855
opts.squeeze,
@@ -1060,6 +1085,7 @@ def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
10601085
opts.stretch,
10611086
opts.smear_pdf,
10621087
opts.smear,
1088+
opts.smear_func,
10631089
opts.hshift,
10641090
opts.vshift,
10651091
opts.squeeze,
@@ -1254,6 +1280,7 @@ def multiple_morphs(parser, opts, pargs, stdout_flag=True, python_wrap=False):
12541280
opts.stretch,
12551281
opts.smear_pdf,
12561282
opts.smear,
1283+
opts.smear_func,
12571284
opts.hshift,
12581285
opts.vshift,
12591286
opts.squeeze,

src/diffpy/morph/morphs/morphsmear.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class MorphSmear(Morph):
3737
yinlabel = LABEL_RR
3838
xoutlabel = LABEL_RA
3939
youtlabel = LABEL_RR
40-
parnames = ["smear"]
40+
parnames = ["smear", "smear_func"]
4141

4242
def morph(self, x_morph, y_morph, x_target, y_target):
4343
"""Resample arrays onto specified grid."""
@@ -51,10 +51,23 @@ def morph(self, x_morph, y_morph, x_target, y_target):
5151
r = self.x_morph_in
5252
rr = self.y_morph_in
5353
r0 = r[len(r) // 2]
54-
gaussian = numpy.exp(-0.5 * ((r - r0) / self.smear) ** 2)
54+
55+
function = None
56+
# FIXME: Add a condition here that checks if smear_func is a function.
57+
# FIXME: If smear_func is a function, set function = self.smear_func.
58+
if self.smear_func is None or self.smear_func == "gaussian":
59+
function = numpy.exp(-0.5 * ((r - r0) / self.smear) ** 2)
60+
elif self.smear_func.lower() == "lorentzian":
61+
function = numpy.abs(self.smear) / ((r - r0) ** 2 + self.smear**2)
62+
else:
63+
raise ValueError(
64+
"Could not process smearing function. "
65+
"Smearing function must be either "
66+
"'gaussian' or 'lorentzian'."
67+
)
5568

5669
# Get the full convolution
57-
c = numpy.convolve(rr, gaussian, mode="full")
70+
c = numpy.convolve(rr, function, mode="full")
5871
# Find the centroid of the RDF, we don't want this to change from the
5972
# convolution.
6073
x1 = numpy.arange(len(rr), dtype=float)
@@ -69,7 +82,7 @@ def morph(self, x_morph, y_morph, x_target, y_target):
6982
rrbroad = numpy.interp(x1, xc, c)
7083

7184
# Normalize so that the integrated magnitude of the RDF doesn't change.
72-
rrbroad /= sum(gaussian)
85+
rrbroad /= sum(function)
7386

7487
self.y_morph_out = rrbroad
7588

tests/test_morph_func.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def test_smear_with_morph_func():
9393
x_morph = x_target.copy()
9494
y_morph = np.exp(-0.5 * ((x_morph - r0) / sigma0) ** 2)
9595
cfg = morph_default_config(smear=0.1, scale=1.1, stretch=0.1) # off init
96+
cfg.update({"smear_func": None})
9697
morph_rv = morph(x_morph, y_morph, x_target, y_target, **cfg)
9798
morphed_cfg = morph_rv["morphed_config"]
9899
# verified they are morphable

tests/test_morphapp.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,21 @@ def test_parser_errors(self, capsys, setup_parser):
243243
single_morph(self.parser, opts, pargs, stdout_flag=False)
244244
assert "a could not be converted to float." in str(excinfo.value)
245245

246+
# Non-sensical smearing function
247+
opts, pargs = self.parser.parse_args(
248+
[
249+
f"{nickel_PDF}",
250+
f"{nickel_PDF}",
251+
"--smear",
252+
"0",
253+
"--smear-function",
254+
"not-a-smearing-function",
255+
]
256+
)
257+
with pytest.raises(ValueError) as excinfo:
258+
single_morph(self.parser, opts, pargs, stdout_flag=False)
259+
assert "Could not process smearing function." in str(excinfo.value)
260+
246261
def test_morphsequence(self, setup_morphsequence):
247262
# Parse arguments sorting by field
248263
opts, pargs = self.parser.parse_args(

0 commit comments

Comments
 (0)