"""
General lineshape function - combines Gaussian and Lorentzian shapes
Modern implementation with pseudo-Voigt capability
"""
from typing import Tuple, Union
import numpy as np
[docs]
def lshape(
x: np.ndarray,
center: float,
width: Union[float, Tuple[float, float]],
derivative: int = 0,
alpha: float = 1.0,
phase: float = 0.0,
) -> np.ndarray:
"""
General normalized lineshape function.
Computes a linear combination of Gaussian and Lorentzian lineshapes:
alpha * Gaussian + (1-alpha) * Lorentzian
This creates pseudo-Voigt profiles commonly used in spectroscopy.
Parameters:
-----------
x : array
Abscissa points
center : float
Peak center position
width : float or (float, float)
Full width at half maximum
If single value: same width for both components
If tuple: (gaussian_width, lorentzian_width)
derivative : int, default=0
Derivative order (0=function, 1=first derivative, 2=second, -1=integral)
alpha : float, default=1.0
Mixing parameter (0=pure Lorentzian, 1=pure Gaussian)
phase : float, default=0.0
Phase rotation (0=absorption, π/2=dispersion)
Returns:
--------
array
Lineshape values
Examples:
---------
>>> x = np.linspace(-10, 10, 1000)
>>> # Pure Gaussian
>>> gauss = lshape(x, 0, 5, alpha=1.0)
>>> # Pure Lorentzian
>>> lorentz = lshape(x, 0, 5, alpha=0.0)
>>> # 50/50 mix (pseudo-Voigt)
>>> mixed = lshape(x, 0, 5, alpha=0.5)
>>> # Different widths for each component
>>> mixed_widths = lshape(x, 0, (3, 7), alpha=0.3)
"""
x = np.asarray(x)
# Validate inputs
if not isinstance(center, (int, float)):
raise ValueError("center must be a number")
if not isinstance(alpha, (int, float)) or not 0 <= alpha <= 1:
raise ValueError("alpha must be between 0 and 1")
if not isinstance(derivative, int) or derivative < -1:
raise ValueError("derivative must be integer >= -1")
if not isinstance(phase, (int, float)):
raise ValueError("phase must be a number")
# Handle width parameter
if isinstance(width, (list, tuple)):
if len(width) != 2:
raise ValueError("width tuple must have exactly 2 values")
width_gauss, width_lorentz = width
if width_gauss <= 0 or width_lorentz <= 0:
raise ValueError("all widths must be positive")
else:
if width <= 0:
raise ValueError("width must be positive")
width_gauss = width_lorentz = width
from .gaussian import gaussian as _gaussian
from .lorentzian import lorentzian as _lorentzian
result = np.zeros_like(x, dtype=float)
# Compute Gaussian component using the canonical implementation
if alpha > 0:
result += alpha * _gaussian(
x, center, width_gauss, derivative=derivative, phase=phase
)
# Compute Lorentzian component using the canonical implementation
if alpha < 1:
result += (1 - alpha) * _lorentzian(
x, center, width_lorentz, derivative=derivative, phase=phase
)
return result
# Convenience functions for common cases
[docs]
def pseudo_voigt(x, center, width, eta=0.5, derivative=0, phase=0.0):
"""
Pseudo-Voigt profile: η*Lorentzian + (1-η)*Gaussian
Parameters:
-----------
x : array
Abscissa points
center : float
Peak center position
width : float
Full width at half maximum
eta : float, default=0.5
Mixing parameter (0=Gaussian, 1=Lorentzian)
derivative : int, default=0
Derivative order (0=function, 1=first derivative, 2=second)
phase : float, default=0.0
Phase rotation (0=absorption, π/2=dispersion)
Returns:
--------
array
Pseudo-Voigt profile values
Examples:
---------
>>> x = np.linspace(-10, 10, 1000)
>>> # Standard pseudo-Voigt
>>> y = pseudo_voigt(x, 0, 5, eta=0.5)
>>> # First derivative
>>> dy = pseudo_voigt(x, 0, 5, eta=0.5, derivative=1)
"""
return lshape(x, center, width, derivative=derivative, alpha=1 - eta, phase=phase)
def demo():
"""Demonstrate different lineshape combinations"""
import matplotlib.pyplot as plt
x = np.linspace(-15, 15, 1000)
# Set1 colors
colors = ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00"]
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# Pure shapes
ax = axes[0, 0]
gauss = lshape(x, 0, 8, alpha=1.0)
lorentz = lshape(x, 0, 8, alpha=0.0)
ax.plot(x, gauss, color=colors[0], linewidth=2.5, label="Gaussian (α=1)")
ax.plot(x, lorentz, color=colors[1], linewidth=2.5, label="Lorentzian (α=0)")
ax.set_title("Pure Lineshapes", fontweight="bold")
ax.legend()
ax.grid(alpha=0.3)
# Mixed shapes (pseudo-Voigt)
ax = axes[0, 1]
alphas = [0.25, 0.5, 0.75]
for i, alpha in enumerate(alphas):
mixed = lshape(x, 0, 8, alpha=alpha)
ax.plot(x, mixed, color=colors[i], linewidth=2.5, label=f"α = {alpha}")
ax.set_title("Pseudo-Voigt Profiles", fontweight="bold")
ax.legend()
ax.grid(alpha=0.3)
# Different widths
ax = axes[1, 0]
narrow_gauss = lshape(x, -3, (4, 8), alpha=0.7) # Narrow Gaussian + wide Lorentzian
wide_gauss = lshape(x, 3, (12, 4), alpha=0.7) # Wide Gaussian + narrow Lorentzian
ax.plot(
x,
narrow_gauss,
color=colors[0],
linewidth=2.5,
label="Narrow Gauss + Wide Lorentz",
)
ax.plot(
x,
wide_gauss,
color=colors[1],
linewidth=2.5,
label="Wide Gauss + Narrow Lorentz",
)
ax.set_title("Different Component Widths", fontweight="bold")
ax.legend()
ax.grid(alpha=0.3)
# Derivatives
ax = axes[1, 1]
center_func = lshape(x, 0, 8, derivative=0, alpha=0.5)
first_deriv = lshape(x, 0, 8, derivative=1, alpha=0.5)
second_deriv = lshape(x, 0, 8, derivative=2, alpha=0.5)
ax.plot(x, center_func, color=colors[0], linewidth=2.5, label="Function")
ax.plot(x, first_deriv, color=colors[1], linewidth=2.5, label="1st derivative")
ax.plot(x, second_deriv, color=colors[2], linewidth=2.5, label="2nd derivative")
ax.set_title("Derivatives (α=0.5)", fontweight="bold")
ax.legend()
ax.grid(alpha=0.3)
# Style all subplots
for ax in axes.flat:
ax.set_xlabel("Position")
ax.set_ylabel("Intensity")
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.tight_layout()
plt.show()
if __name__ == "__main__":
demo()