-
Notifications
You must be signed in to change notification settings - Fork 32
adding peak_voigt model #743
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| r""" | ||
| This model describes a pseudo-Voigt shaped peak on a flat background. | ||
|
|
||
| Definition | ||
| ---------- | ||
|
|
||
| This pseudo-Voigt peak function is a weighted linear summation of | ||
| Lorentzian (L) and Gaussian (G) peak shapes. | ||
| It is a popular function for modelling peak shape. | ||
| It can be tailored to any specific peak shape and it can also produce a peak shape with asymmetry. | ||
|
|
||
| The scattering intensity $I(q)$ is calculated as | ||
|
|
||
| .. math:: | ||
|
|
||
| I(q) = scale \cdot \left[ w_f \cdot I(q)_L + (1 - w_f) \cdot I(q)_G \right] + background | ||
|
|
||
| where $w_f$ is a weighting factor and | ||
|
|
||
| .. math:: | ||
|
|
||
| I(q)_L = \frac{1}{1 + \left( \frac{q - q_0}{HWHM} \right)^2} | ||
|
|
||
| I(q)_G = \exp\left[ -\frac{1}{2} (q - q_0)^2 / \sigma^2 \right] | ||
|
|
||
| The peak is taken to be centered at $q_0$ with a HWHM (half-width | ||
| half-maximum) of $1.17741\,\sigma$, where $\sigma$ is the standard deviation | ||
| of the Gaussian. In other words, the widths of the Lorentzian and the | ||
| Gaussian have been coupled for convenience of parameterisation: | ||
|
|
||
| .. math:: | ||
|
|
||
| \sigma = HWHM / \sqrt{2 \ln 2} = HWHM / 1.17741 | ||
|
|
||
| When $w_f = 1$ a Lorentzian peak is returned, and when $w_f = 0$ a | ||
| Gaussian peak is returned. | ||
|
|
||
| For 2D data the scattering intensity is calculated in the same way as 1D, | ||
| where the $q$ vector is defined as | ||
|
|
||
| .. math:: | ||
|
|
||
| q = \sqrt{q_x^2 + q_y^2} | ||
|
|
||
|
|
||
| Validation | ||
| ---------- | ||
|
|
||
| The pseudo-Voigt peak reduces exactly to a pure Lorentzian for $w_f = 1$ | ||
| and to a pure Gaussian for $w_f = 0$; both limits were checked against their | ||
| analytic values (see tests section at the end). | ||
| The full pseudo-Voigt shape has also been compared, for identical | ||
| parameters, against a slightly different SasView implementation (https://marketplace.sasview.org/models/127/) | ||
| of the same function and gives the same result. | ||
|
|
||
|
|
||
| References | ||
| ---------- | ||
|
|
||
| 1. L A Feigin, D I Svergun, G W Taylor | ||
| Structure Analysis by Small-Angle X-ray and Neutron Scattering | ||
| Springer (1987) | ||
|
Comment on lines
+60
to
+62
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this reference truly relevant to this model? I don't find any Voigt or Pseudo Voigt in that book. Could you had a page number if it is? and if not maybe remove. It is an excellent textbook on scattering but Voigt peaks are fundamentally just math functions with no intrinsic scattering knowledge needed to write this? |
||
|
|
||
| 2. Aaron L. Stancik, Eric B. Brauns | ||
| A simple asymmetric lineshape for fitting infrared absorption spectra | ||
| Vibrational Spectroscopy 47 (2008) 66-69 | ||
|
|
||
|
|
||
| Authorship and Verification | ||
| ---------------------------- | ||
|
|
||
| * **Author:** Steve King **Date:** 24 June 2020 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There should only be one Author line. Otherwise it gets confusing. In principle I would say that if this was just a minor edits to the code in the marketplace, the author normally would remain the original author with the new authors as "last modified by." On the other hand, if the code was inspired by the marketplace but significantly rewritten here then I would just replace the marketplace authors with both your names. I do not think there is precedent in sasmodels yet, but one could add a line at the top called "original author." This could then be used for the marketplace author and authors would be the two of you (normally one line with "and") and a date which could just be "June 2026" if a specific date doesn't make sense. |
||
|
|
||
| * **Authors:** Marianne Imperor-Clerc (marianne.imperor@cnrs.fr) | ||
| Anirban Mandal (mandalanirban2023@gmail.com) | ||
|
|
||
| * **Last Modified by:** Anirban Mandal **Date:** 06 July 2026 | ||
|
|
||
| * **Last Reviewed by:** Steve King **Date:** | ||
|
|
||
| """ | ||
|
|
||
| import numpy as np | ||
| from numpy import errstate, inf | ||
|
|
||
| name = "peak_voigt" | ||
| title = "Single pseudo-Voigt peak" | ||
| description = """\ | ||
| I(q) = scale*peak + background | ||
| """ | ||
|
|
||
| category = "shape-independent" | ||
|
|
||
| parameters = [["w_f", "", 0.8, [0, 1], "", "lorentzian/gaussian weighting factor"], | ||
| ["peak_pos", "1/Ang", 0.05, [0, inf], "", "Position of the peak"], | ||
| ["peak_hwhm", "1/Ang", 0.01, [0, 1], "", "HWHM of the peak"]] | ||
|
|
||
|
|
||
| def Ipeak(q, wf, q0, hwhm): | ||
| """ | ||
| When $w_f$ = 1 a Lorentzian peak is returned, and when $w_f$ = 0 a | ||
| Gaussian peak is returned. | ||
|
|
||
| The peak is taken to be centered at $q_0$ with a HWHM (half-width | ||
| half-maximum) for the Lorentzian and sigma = HWHM / 1.17741 for the | ||
| Gaussian, where sigma is the standard deviation of the Gaussian. In | ||
| other words, the widths of the Lorentzian and the Gaussian have been | ||
| coupled for convenience of parameterisation. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Convenient for whom? The width of the Gaussian and Lorentzian are easier to interpret physically. I don't think the optimizer will care which pair it is fitting. The only advantage seems to be that it is easier to guess the initial value of HWHM from the graph.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I note that the reference whence this was taken (according to the reference cited here) does exactly this (create a single HFHM term for both line shapes) as does the marketplace model. It also reduces the number of fit parameters. Not sure "convenience of parameterization" is the correct wording. I also don't know if this coupling is general enough for most scattering use cases or if there is a real call for decoupling them.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The number of fit parameters is the same: center, width, gauss:lorentz ratio vs. center, σ (gauss), γ (lorentz)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Except that there is only the center and width no ratio right? Unless you are talking about weighting ratio and are comparing the Voigt to Pseudo Voigt? I'm confused.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The The gaussian and the lorentzian have the same I haven't checked in detail that every choice of (σ, γ) for voigt can be approximated by some choice of |
||
| """ | ||
| cste = np.sqrt(2 * np.log(2)) | ||
| # cste = 1.17741 | ||
| sigma = hwhm / cste | ||
| intensity = (wf * (1 / (1 + ((q - q0)**2.0 / hwhm**2.0)))) + \ | ||
| ((1.0 - wf) * np.exp((-0.5 * (q - q0)**2.0) / (sigma**2.0))) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than the "\" line extender use parentheses around the entire expression so it can break across multiple lines without the backslash. Move the "+" operator to the beginning of the second line. |
||
| return intensity | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can use Voigt rather than pseudo-Voigt: For a 1000 point function on my mac this takes 47.5 μs vs 12.5 μs for the pseudo-Voigt function.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we are going full Voigt why not just from scipy.special import voigt_profile
V = voigt_profile(x, sigma, gamma)
Return VThat said, Pseudo Voigt seems to be quite popular as @marimperorclerc points out, probably due to the factor of 4 you measured. Going where the users are does not seem like a terrible plan to me? Perhaps we could add the full Voigt as an option to minimize the number of new models? Though I think that may get too complicated.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wrote voigt before voigt_profile was available. I didn't know they added it to scipy (Dec. 2019). The pseudo-Voigt was popular because it was readily available. The linear combination of Gauss and Lorentz distributions was easier to calculate than the w(z) function. Other than the rare situation where you have pure Gaussian and Lorentzian peak broadening from different parts of the sample adding incoherently, I don't know that it has any physical meaning. The parameterization of pseudo-Voigt using HWHM was a further convenience, being something that can be read directly off the graph. Now that we can fit the Gaussian and Lorentzian parameters of the Voigt profile directly, that seems more useful than continuing with pseudo-Voigt based on peak width.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would argue that that decision is "above my pay grade." It really needs input from the community that uses it. That said I would argue a Pseudo Voigt is better than none at all and currently we only have a PR for a Pseudo Voigt - though as we just showed above writing a true Voigt would be trivial should someone be so inclined? Then the question would become a different one: do we need both? |
||
|
|
||
|
|
||
| def Iq(q, w_f, peak_pos, peak_hwhm): | ||
| """ | ||
| w_f: weighting coefficient in the pseudo-Voigt peak function; | ||
| w_f = 1 for a Lorentzian and w_f = 0 for a Gaussian peak. | ||
| peak_pos: position of the peak | ||
| peak_hwhm: HWHM of the peak | ||
| """ | ||
|
|
||
| with errstate(divide='ignore'): | ||
| L = Ipeak(q, w_f, peak_pos, peak_hwhm) | ||
|
|
||
| return L | ||
|
|
||
|
Comment on lines
+122
to
+130
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Helper functions are certainly allowed, but in this case it seems completely redundant and just makes it slightly more convoluted to read. Just rename Ipeak to I(q) and delete this function which does nothing at all from what I can tell? That function already returns a sensible variable (intensity instead of L) and doesn't try to "ignore divide by zero which cannot happen from what I can tell s the denominator is always >1? The only other thing that will need to be done when changing the Ipeak function name to Iq is to also change the parameter list names. For some reason Ipeak renames the variables currently. |
||
| Iq.vectorized = True # Iq accepts an array of q values | ||
|
|
||
| tests = [ | ||
| # pure Lorentzian (w_f = 1): peak centre, half-width, and 2 x HWHM | ||
| [{"scale": 1.0, "background": 0.0, "w_f": 1.0, | ||
| "peak_pos": 0.05, "peak_hwhm": 0.01}, 0.05, 1.0], | ||
| [{"scale": 1.0, "background": 0.0, "w_f": 1.0, | ||
| "peak_pos": 0.05, "peak_hwhm": 0.01}, 0.06, 0.5], | ||
| [{"scale": 1.0, "background": 0.0, "w_f": 1.0, | ||
| "peak_pos": 0.05, "peak_hwhm": 0.01}, 0.07, 0.2], | ||
| # pure Gaussian (w_f = 0): half-width is 0.5 by definition, 2 x HWHM = 1/16 | ||
| [{"scale": 1.0, "background": 0.0, "w_f": 0.0, | ||
| "peak_pos": 0.05, "peak_hwhm": 0.01}, 0.06, 0.5], | ||
| [{"scale": 1.0, "background": 0.0, "w_f": 0.0, | ||
| "peak_pos": 0.05, "peak_hwhm": 0.01}, 0.07, 0.0625], | ||
| # mixed pseudo-Voigt (w_f = 0.8) away from the centre | ||
| [{"scale": 1.0, "background": 0.0, "w_f": 0.8, | ||
| "peak_pos": 0.05, "peak_hwhm": 0.01}, 0.07, 0.1725], | ||
| ] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both the gaussian and the lorentzian are symmetric about
peak_pos, and so is the linear combination. It make look asymmetric due to log q scale, or because the resolution function Δq is increasing with q.Mention that the voigt function is a convolution$(L_γ \star G_σ)(q)$ . pseudo-Voigt approximates this with a linear combination of a lorentzian and a gaussian.
Add a note that the instrument resolution function contributes to the convolution. That is, if the resolution is a gaussian of approximately constant width Δq then$f(q) = (L_γ \star G_σ \star G_{Δq})(q) = (L_γ \star G_σ')(q)$ with σ'=√(σ² + Δq²) where σ is the width returned from the fit. The fitted σ is intrinsic to the sample. You will need to subtract the modified resolution width Δq/√(2 \ln 2) from the HWHM when guessing the
peak_hwhmparameter value.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed -- asymmetric Pseudo Voigt is a thing but requires some modification such as a q dependent FWHM. see for example https://analyticalsciencejournals.onlinelibrary.wiley.com/doi/10.1002/sia.5521 or chrome-extension://efaidnbmnnnibpcajpcglclefindmkaj/https://arxiv.org/pdf/1804.06083. This function does not seem to be that.
On the other hand, I see that the reference from which @smk78 took the equation he placed in the marketplace and from which this seems to be derived, is specifically about an asymmetric Pseudo Voigt which does exactly what I state above (https://www.sciencedirect.com/science/article/pii/S0924203108000453). I it is probably also why that paper also couples the width of the two peaks?
However I suspect that is more complexity than we may want here. At least for now I'd recommend keeping it simple as done in the marketplace did, and if there is a clamoring for allowing asymmetry we can add that later (the asymmetry parameter should be adjustable so that it has no effect)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding convolution with resolution... isn't that generically true for any peak function? Is there a standard blurb we use for all those?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Specifically, I'm asking that the clause "it can also produce a peak shape with asymmetry" be removed from the documentation. No combination of parameters will lead to an asymmetric peak.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding peak broadening through resolution it is not mentioned in other models. It seemed relevant here because the measured peak HWHM will be larger than the fitted
peak_hwhmvalue.For the other models:
correlation_lengthbut the arbitrary power changes the widthsigmawith measured FWHM = 2.35σ + resolution broadeningcor_lengthwith measured HWHM = γ + resolution broadeninglorentz_length_[12]but the arbitrary power changes the width