From d2956895002ee9cab072d11e5120b91e10bb8f2f Mon Sep 17 00:00:00 2001 From: marimperorclerc <95975874+marimperorclerc@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:52:00 +0200 Subject: [PATCH 1/9] adding peak_voigt model --- sasmodels/models/peak_voigt.py | 149 +++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 sasmodels/models/peak_voigt.py diff --git a/sasmodels/models/peak_voigt.py b/sasmodels/models/peak_voigt.py new file mode 100644 index 00000000..68dda782 --- /dev/null +++ b/sasmodels/models/peak_voigt.py @@ -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) + +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 + +* **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 inf, errstate + +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. + """ + 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))) + return intensity + + +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 + +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], +] From 44f9c50ab9fc6754cd54e14309a62e72c416144f Mon Sep 17 00:00:00 2001 From: krzywon Date: Tue, 7 Jul 2026 09:50:31 -0400 Subject: [PATCH 2/9] ruff fix --- sasmodels/models/peak_voigt.py | 298 ++++++++++++++++----------------- 1 file changed, 149 insertions(+), 149 deletions(-) diff --git a/sasmodels/models/peak_voigt.py b/sasmodels/models/peak_voigt.py index 68dda782..b1ffcfa1 100644 --- a/sasmodels/models/peak_voigt.py +++ b/sasmodels/models/peak_voigt.py @@ -1,149 +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) - -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 - -* **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 inf, errstate - -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. - """ - 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))) - return intensity - - -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 - -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], -] +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) + +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 + +* **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. + """ + 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))) + return intensity + + +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 + +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], +] From 9d37d957d62530ae7958141f6a73ea0f7a957718 Mon Sep 17 00:00:00 2001 From: marimperorclerc <95975874+marimperorclerc@users.noreply.github.com> Date: Mon, 7 Sep 2026 12:13:35 +0200 Subject: [PATCH 3/9] Rename peak_voigt.py to peak_pseudo_voigt.py --- sasmodels/models/{peak_voigt.py => peak_pseudo_voigt.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename sasmodels/models/{peak_voigt.py => peak_pseudo_voigt.py} (100%) diff --git a/sasmodels/models/peak_voigt.py b/sasmodels/models/peak_pseudo_voigt.py similarity index 100% rename from sasmodels/models/peak_voigt.py rename to sasmodels/models/peak_pseudo_voigt.py From 43db59255834421a7ab40b67c5da64fde31fdc29 Mon Sep 17 00:00:00 2001 From: marimperorclerc <95975874+marimperorclerc@users.noreply.github.com> Date: Mon, 7 Sep 2026 12:16:03 +0200 Subject: [PATCH 4/9] Update peak_pseudo_voigt.py --- sasmodels/models/peak_pseudo_voigt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sasmodels/models/peak_pseudo_voigt.py b/sasmodels/models/peak_pseudo_voigt.py index b1ffcfa1..e1911fff 100644 --- a/sasmodels/models/peak_pseudo_voigt.py +++ b/sasmodels/models/peak_pseudo_voigt.py @@ -7,7 +7,7 @@ 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. +It can be tailored to any experimental peak shape. The scattering intensity $I(q)$ is calculated as From 663286449409357e9e98150bfdb88f18302480bc Mon Sep 17 00:00:00 2001 From: marimperorclerc <95975874+marimperorclerc@users.noreply.github.com> Date: Mon, 7 Sep 2026 12:21:38 +0200 Subject: [PATCH 5/9] Update peak_pseudo_voigt.py Name of the model changed to peak_pseudo_Voigt Comment about assymetry is removed. Author's list rectified. --- sasmodels/models/peak_pseudo_voigt.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/sasmodels/models/peak_pseudo_voigt.py b/sasmodels/models/peak_pseudo_voigt.py index e1911fff..c46a8540 100644 --- a/sasmodels/models/peak_pseudo_voigt.py +++ b/sasmodels/models/peak_pseudo_voigt.py @@ -57,11 +57,7 @@ References ---------- -1. L A Feigin, D I Svergun, G W Taylor - Structure Analysis by Small-Angle X-ray and Neutron Scattering - Springer (1987) - -2. Aaron L. Stancik, Eric B. Brauns +1. Aaron L. Stancik, Eric B. Brauns A simple asymmetric lineshape for fitting infrared absorption spectra Vibrational Spectroscopy 47 (2008) 66-69 @@ -72,11 +68,10 @@ * **Author:** Steve King **Date:** 24 June 2020 * **Authors:** Marianne Imperor-Clerc (marianne.imperor@cnrs.fr) - Anirban Mandal (mandalanirban2023@gmail.com) -* **Last Modified by:** Anirban Mandal **Date:** 06 July 2026 +* **Last Modified by:** Anirban Mandal (mandalanirban2023@gmail.com) **Date:** 06 July 2026 -* **Last Reviewed by:** Steve King **Date:** +* **Last Reviewed by:** Marianne Imperor-Clerc (marianne.imperor@cnrs.fr) **Date:** 07 September 2026 """ From 3aa1416fd1ecbd6c7f50c580f1b65566a937c121 Mon Sep 17 00:00:00 2001 From: marimperorclerc <95975874+marimperorclerc@users.noreply.github.com> Date: Mon, 7 Sep 2026 12:35:17 +0200 Subject: [PATCH 6/9] Update peak_pseudo_voigt.py --- sasmodels/models/peak_pseudo_voigt.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sasmodels/models/peak_pseudo_voigt.py b/sasmodels/models/peak_pseudo_voigt.py index c46a8540..5f48ff47 100644 --- a/sasmodels/models/peak_pseudo_voigt.py +++ b/sasmodels/models/peak_pseudo_voigt.py @@ -67,8 +67,6 @@ * **Author:** Steve King **Date:** 24 June 2020 -* **Authors:** Marianne Imperor-Clerc (marianne.imperor@cnrs.fr) - * **Last Modified by:** Anirban Mandal (mandalanirban2023@gmail.com) **Date:** 06 July 2026 * **Last Reviewed by:** Marianne Imperor-Clerc (marianne.imperor@cnrs.fr) **Date:** 07 September 2026 @@ -78,7 +76,7 @@ import numpy as np from numpy import errstate, inf -name = "peak_voigt" +name = "peak_pseudo_voigt" title = "Single pseudo-Voigt peak" description = """\ I(q) = scale*peak + background From 14ced69c81d79aeb1083b64548a30d4035a025ff Mon Sep 17 00:00:00 2001 From: marimperorclerc <95975874+marimperorclerc@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:06:17 +0200 Subject: [PATCH 7/9] updated version with Anirban comments --- sasmodels/models/peak_pseudo_voigt.py | 62 ++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/sasmodels/models/peak_pseudo_voigt.py b/sasmodels/models/peak_pseudo_voigt.py index 5f48ff47..5606ebcc 100644 --- a/sasmodels/models/peak_pseudo_voigt.py +++ b/sasmodels/models/peak_pseudo_voigt.py @@ -43,6 +43,42 @@ q = \sqrt{q_x^2 + q_y^2} +Reuse of the peak shape in multi-peak models +-------------------------------------------- + +The line shape is implemented in a separate helper function, ``Ipeak(q, wf, q0, +hwhm)``, which ``Iq`` then calls. The reason is that a single diffraction peak is +rarely measured on its own: an ordered phase produces a *series* of Bragg peaks +whose positions are all fixed by one lattice parameter, while every peak in the +series shares the same line shape. Writing the line shape once, as a function of an +arbitrary peak position and width, is what allows the same code to be reused for +those multi-peak models. For example, for a lamellar phase of period $d$ + +.. math:: + + q_n = n\,q_1, \qquad q_1 = \frac{2\pi}{d}, \qquad n = 1, 2, 3 \ldots + +and for a two-dimensional hexagonal phase of cell parameter $a$ + +.. math:: + + q_{hk} = q_{10}\,\sqrt{h^2 + hk + k^2}, \qquad q_{10} = \frac{4\pi}{\sqrt{3}\,a} + +giving peaks in the ratios $1 : \sqrt{3} : 2 : \sqrt{7} : 3 \ldots$ for the +$(10), (11), (20), (21), (30) \ldots$ reflections. A model of such a phase is then +a sum over reflections, + +.. math:: + + I(q) = scale \cdot \sum_{hk} A_{hk}\, \mathrm{Ipeak}(q, w_f, q_{hk}, HWHM_{hk}) + + background + +in which only the amplitudes $A_{hk}$, the widths and the single lattice parameter +are fitted. Keeping ``Ipeak`` separate from ``Iq`` means that the single-peak model +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. + Validation ---------- @@ -89,8 +125,15 @@ ["peak_hwhm", "1/Ang", 0.01, [0, 1], "", "HWHM of the peak"]] +# The line shape is kept in its own function, taking the peak position and width as +# plain arguments, so that models of ordered phases (lamellar, 2D hexagonal, ...) can +# call it once per reflection and share exactly this code. See the section "Reuse of +# the peak shape in multi-peak models" in the module documentation above. def Ipeak(q, wf, q0, hwhm): """ + Pseudo-Voigt line shape at an arbitrary peak position, for reuse by + multi-peak models as well as by ``Iq`` below. + When $w_f$ = 1 a Lorentzian peak is returned, and when $w_f$ = 0 a Gaussian peak is returned. @@ -103,8 +146,10 @@ def Ipeak(q, wf, q0, hwhm): 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))) + 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))) + ) return intensity @@ -114,12 +159,15 @@ def Iq(q, w_f, peak_pos, peak_hwhm): 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 + The ``errstate`` guard covers the lower limit of the ``peak_hwhm`` range: at + ``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. + """ + with errstate(divide='ignore', invalid='ignore'): + return Ipeak(q, w_f, peak_pos, peak_hwhm) Iq.vectorized = True # Iq accepts an array of q values From 49e1444457f2a13530ddeaa84a89b26e7db897b1 Mon Sep 17 00:00:00 2001 From: Bakshi-gif Date: Fri, 25 Sep 2026 19:14:58 +0530 Subject: [PATCH 8/9] Add files via upload Updated version according to the thread. --- sasmodels/models/peak_pseudo_voigt.py | 104 +++++++++----------------- 1 file changed, 37 insertions(+), 67 deletions(-) diff --git a/sasmodels/models/peak_pseudo_voigt.py b/sasmodels/models/peak_pseudo_voigt.py index 5606ebcc..31214a82 100644 --- a/sasmodels/models/peak_pseudo_voigt.py +++ b/sasmodels/models/peak_pseudo_voigt.py @@ -35,6 +35,15 @@ When $w_f = 1$ a Lorentzian peak is returned, and when $w_f = 0$ a Gaussian peak is returned. +The pseudo-Voigt is an approximation to the true Voigt profile, which is the +convolution of a Lorentzian with a Gaussian. Because the two widths are coupled +here, the lineshape is controlled by the single weighting factor $w_f$, so the model +has the same number of free parameters as a true Voigt would: peak position, width +and $w_f$, against peak position, $\sigma$ (Gaussian) and $\gamma$ (Lorentzian). +Varying $w_f$ at fixed HWHM changes the weight in the tails rather than the width of +the peak. The advantage over the true Voigt is that the expression is analytic and +inexpensive to evaluate. + For 2D data the scattering intensity is calculated in the same way as 1D, where the $q$ vector is defined as @@ -43,41 +52,16 @@ q = \sqrt{q_x^2 + q_y^2} -Reuse of the peak shape in multi-peak models --------------------------------------------- - -The line shape is implemented in a separate helper function, ``Ipeak(q, wf, q0, -hwhm)``, which ``Iq`` then calls. The reason is that a single diffraction peak is -rarely measured on its own: an ordered phase produces a *series* of Bragg peaks -whose positions are all fixed by one lattice parameter, while every peak in the -series shares the same line shape. Writing the line shape once, as a function of an -arbitrary peak position and width, is what allows the same code to be reused for -those multi-peak models. For example, for a lamellar phase of period $d$ - -.. math:: - - q_n = n\,q_1, \qquad q_1 = \frac{2\pi}{d}, \qquad n = 1, 2, 3 \ldots - -and for a two-dimensional hexagonal phase of cell parameter $a$ - -.. math:: +Note on instrumental resolution +------------------------------- - q_{hk} = q_{10}\,\sqrt{h^2 + hk + k^2}, \qquad q_{10} = \frac{4\pi}{\sqrt{3}\,a} +As for the other peak models in sasmodels, $HWHM$ is the width of the peak produced +by the sample alone. The width actually observed is larger, because the measured +intensity is the model convolved with the instrumental resolution function. The +fitted $HWHM$ therefore only corresponds to the measured peak width when the +resolution is negligible; otherwise resolution smearing should be applied during the +fit, so that the fitted value remains the physical width. -giving peaks in the ratios $1 : \sqrt{3} : 2 : \sqrt{7} : 3 \ldots$ for the -$(10), (11), (20), (21), (30) \ldots$ reflections. A model of such a phase is then -a sum over reflections, - -.. math:: - - I(q) = scale \cdot \sum_{hk} A_{hk}\, \mathrm{Ipeak}(q, w_f, q_{hk}, HWHM_{hk}) - + background - -in which only the amplitudes $A_{hk}$, the widths and the single lattice parameter -are fitted. Keeping ``Ipeak`` separate from ``Iq`` means that the single-peak model -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. Validation ---------- @@ -110,7 +94,7 @@ """ import numpy as np -from numpy import errstate, inf +from numpy import inf name = "peak_pseudo_voigt" title = "Single pseudo-Voigt peak" @@ -125,32 +109,20 @@ ["peak_hwhm", "1/Ang", 0.01, [0, 1], "", "HWHM of the peak"]] -# The line shape is kept in its own function, taking the peak position and width as -# plain arguments, so that models of ordered phases (lamellar, 2D hexagonal, ...) can -# call it once per reflection and share exactly this code. See the section "Reuse of -# the peak shape in multi-peak models" in the module documentation above. +# Kept as a separate function so that models of ordered phases (lamellar, +# 2D hexagonal, ...) can call it once per reflection. Moving it to +# sasmodels.special as sas_pseudovoigt would be the cleaner home for it. def Ipeak(q, wf, q0, hwhm): - """ - Pseudo-Voigt line shape at an arbitrary peak position, for reuse by - multi-peak models as well as by ``Iq`` below. - - 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. - """ - 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))) - ) - return intensity + # wf = 1 gives a Lorentzian, wf = 0 a Gaussian; the two widths are coupled + # through sigma = hwhm / sqrt(2 ln 2) so that both have the same HWHM. + sigma = hwhm / np.sqrt(2 * np.log(2)) + # Protect against zero width peaks (hwhm == 0, sigma == 0), which are zero + # everywhere except at the centre. + 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 def Iq(q, w_f, peak_pos, peak_hwhm): @@ -159,15 +131,8 @@ def Iq(q, w_f, peak_pos, peak_hwhm): 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 - - The ``errstate`` guard covers the lower limit of the ``peak_hwhm`` range: at - ``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. """ - with errstate(divide='ignore', invalid='ignore'): - return Ipeak(q, w_f, peak_pos, peak_hwhm) + return Ipeak(q, w_f, peak_pos, peak_hwhm) Iq.vectorized = True # Iq accepts an array of q values @@ -187,4 +152,9 @@ def Iq(q, w_f, peak_pos, peak_hwhm): # 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], + # zero width: unity at the centre, zero elsewhere + [{"scale": 1.0, "background": 0.0, "w_f": 0.8, + "peak_pos": 0.05, "peak_hwhm": 0.0}, 0.05, 1.0], + [{"scale": 1.0, "background": 0.0, "w_f": 0.8, + "peak_pos": 0.05, "peak_hwhm": 0.0}, 0.06, 0.0], ] From fc9846787f6d722eb71271fc21e7c6b2c4c44f7a Mon Sep 17 00:00:00 2001 From: Bakshi-gif Date: Wed, 30 Sep 2026 18:50:11 +0530 Subject: [PATCH 9/9] Add files via upload --- sasmodels/models/peak_pseudo_voigt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sasmodels/models/peak_pseudo_voigt.py b/sasmodels/models/peak_pseudo_voigt.py index 31214a82..82822342 100644 --- a/sasmodels/models/peak_pseudo_voigt.py +++ b/sasmodels/models/peak_pseudo_voigt.py @@ -41,8 +41,7 @@ has the same number of free parameters as a true Voigt would: peak position, width and $w_f$, against peak position, $\sigma$ (Gaussian) and $\gamma$ (Lorentzian). Varying $w_f$ at fixed HWHM changes the weight in the tails rather than the width of -the peak. The advantage over the true Voigt is that the expression is analytic and -inexpensive to evaluate. +the peak. The advantage of the pseudo-voigt is, it is a sum of two functions. For 2D data the scattering intensity is calculated in the same way as 1D, where the $q$ vector is defined as