Skip to content

adding peak_voigt model - #743

Open
marimperorclerc wants to merge 9 commits into
masterfrom
peak_voigt
Open

marimperorclerc wants to merge 9 commits into
masterfrom
peak_voigt

Conversation

@marimperorclerc

Copy link
Copy Markdown
Contributor

No description provided.

@marimperorclerc

Copy link
Copy Markdown
Contributor Author

Purpose of this PR is to add pseudo Voigt peak shape model.
It is a pure python model based on the one writen for the marketplace :
https://marketplace.sasview.org/uploads/619

If someone could help with the RUFF issue, I would be grateful !

@krzywon

krzywon commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

@marimperorclerc - I've applied the ruff fixes. If you would like to not have this type of issue in the future, please follow the instructions at https://github.com/orgs/SasView/discussions/3171#discussioncomment-17148772 to get ruff and pre-commit set up.

Comment thread sasmodels/models/peak_pseudo_voigt.py
Comment thread sasmodels/models/peak_pseudo_voigt.py Outdated
# 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)))

@pkienzle pkienzle Jul 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The w_f parameter gives the relative portion of gaussian to lorentzian. The fit can adjust center, width and proportion.

The gaussian and the lorentzian have the same peak_hwhm value, so adjusting w_f effectively changes the tail weight without changing the width.

I haven't checked in detail that every choice of (σ, γ) for voigt can be approximated by some choice of peak_hwhm and w_f and vice versa, but the parameter count is the same.

Comment thread sasmodels/models/peak_pseudo_voigt.py Outdated
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.

Copy link
Copy Markdown
Contributor

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_hwhm parameter value.

@butlerpd butlerpd Aug 25, 2026

Copy link
Copy Markdown
Member

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)?

Copy link
Copy Markdown
Member

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?

Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Contributor

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_hwhm value.

For the other models:

  • broad peak uses correlation_length but the arbitrary power changes the width
  • gaussian peak uses sigma with measured FWHM = 2.35σ + resolution broadening
  • lorentz uses cor_length with measured HWHM = γ + resolution broadening
  • two lorentzian uses lorentz_length_[12] but the arbitrary power changes the width

@krzywon
krzywon requested a review from butlerpd August 25, 2026 13:42
Comment thread sasmodels/models/peak_pseudo_voigt.py
Comment thread sasmodels/models/peak_pseudo_voigt.py Outdated
Comment on lines +60 to +62
1. L A Feigin, D I Svergun, G W Taylor
Structure Analysis by Small-Angle X-ray and Neutron Scattering
Springer (1987)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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?

codescene-access[bot]

This comment was marked as outdated.

@pkienzle

Copy link
Copy Markdown
Contributor

Oops... I accidentally clicked on merge with master.

@marimperorclerc, you will need to pull before making more changes. Sorry!

Comment thread sasmodels/models/peak_pseudo_voigt.py

@butlerpd butlerpd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Voigt function is a commonly used function which was never implemented in sasmodels and requires relying on a marketplace plugin. As such this is a great addition and it would be nice to see it included in the next point release. I've tested functionally and it performs as advertised.

Despite the voluminous comments, all of which are useful, The important issues that should be addressed before a merge IMO are fairly simple and I list them here.

  • Correct the documentation about asymmetry (unless of course the choice is to rewrite the function to include asymmetry).
  • Correct the references and authorship as appropriate (see comments/questions)
  • Remove the Ipeak (actually it becomes Iq and the old Iq code is just deleted) -- or explain why it is really necessary to keep separate.. in which case other fixes will be needed in that function IMO.

@marimperorclerc

Copy link
Copy Markdown
Contributor Author

Thank you for reviewing this model !
I will address all your comments carefully, but only in about two weeks in mid-September.
See you soon ! Marianne

@smk78

smk78 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

I would hazard a guess that the pseudo-Voigt gained traction amongst crystallographers in those heady days when processors were slow and memory was expensive and limited... I recall it was implemented in the CCP13 XFIX program at the start of the 90's.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

Name of the model changed to peak_pseudo_Voigt
Comment about assymetry is removed.
Author's list rectified.
codescene-access[bot]

This comment was marked as outdated.

@marimperorclerc

marimperorclerc commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for all your comments and suggestions.
Well, I'm still using it for fitting diffraction peaks in SAS ... even if it was probably more popular in the 90's !
It is something handy because it can adapt to any experimental profile.
The version with the same HWHM for both the Gaussian and Lorentzian is convenient.

  • To be consistent, I changed the name of the model to: peak_pseudo_Voigt.

  • Comment about assymetry is removed.
    (no need to do something more complicated yet)

  • Author's list is rectified

I will test it one last time with the master student who worked on this model with me, Anirban Mandal, and then it should be ready by the end of next week for a last round of review.
Thank you for your patience !

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No quality gates enabled for this code.

See analysis details in CodeScene

Quality Gate Profile: Custom Configuration
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

@marimperorclerc

Copy link
Copy Markdown
Contributor Author

Please review again the updated version of 'peak_pseudo_voigt.py' model at your earliest convenience.

documented here and any multi-peak model built on it evaluate *identical* code for
the line shape, so the two can never drift apart. ``Iq`` is the thin wrapper that
exposes the single-peak case through the sasmodels interface.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This documentation is more about the details of the implementation than the use of the peak_pseudo_voigt model. I don't think it helps the user with the function.

In order to reuse the peak function in other models you should move it to sasmodels.special. I suggest calling it sas_pseudovoigt.

A similar sas_voigt function could be written as:

from scipy.special import voigt_profile
def sas_voigt(q, sigma, gamma, center):
    return voigt_profile(q - center, sigma, gamma) / voigt_profile(0, sigma, gamma)

Note the division: the scipy voigt profile function returns a PDF with unit area but sasmodels peak functions are unnormalized, with height one at the center.

``peak_hwhm = 0`` the line shape divides by zero away from the peak centre
(``divide``) and evaluates 0/0 at the centre itself (``invalid``). Both limits
are well defined and numpy already returns them correctly, so only the warnings
need suppressing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more of a code comment then a function docstring.

I was going to suggest that you say that Iq evaluates to NaN at the bad point, but I don't see a bad point. Both the lorentzian and the gaussian are in (0, 1], so you don't need the errstate guard. (If you do need the guard you should put it in the pseudo-voigt function itself, not in the caller.)

For zero width peaks you need special handling, as I suggested above.

((1.0 - wf) * np.exp((-0.5 * (q - q0)**2.0) / (sigma**2.0)))
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)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have more parentheses than needed. Also, for a perfect square you can use **2 rather than **2.0.

I find the following easier to read:

intensity = (
    wf * 1.0 / (1.0 + (q-q0)**2 / hwhm**2)
    + (1.0 - wf) * np.exp(-0.5 * (q - q0)**2 / sigma**2)
)

You may want to protect against hwhm = 0 and sigma = 0, which are zero everywhere except the center:

# Protect against zero width peaks (hwhm == 0, sigma == 0)
lorentzian = 1.0 / (1.0 + (q - q0)**2 / hwhm**2 if hwhm > 0 else 1.0 * (q == q0)
gaussian = np.exp(-0.5 * (q - q0)**2 / sigma**2) if sigma > 0 else 1.0 * (q == q0)
return wf*lorentzian + (1.0 - wf)*gaussian

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants