Repository: wright-group/WrightTools

Issue:
Gaussian, Lorentzian, Voight functions in kit
We should offer area and height normalized options for each. And a complex form of the Lorentzian.

Gaussian
![image](https://user-images.githubusercontent.com/15792657/52025869-9d728580-24cb-11e9-944a-642cde548691.png)
![image](https://user-images.githubusercontent.com/15792657/52025878-a4999380-24cb-11e9-98ab-c88180511696.png)
![image](https://user-images.githubusercontent.com/15792657/52025892-af542880-24cb-11e9-8f66-414273ac1f21.png)
Lorentzian
![image](https://user-images.githubusercontent.com/15792657/52025943-e6c2d500-24cb-11e9-8c4a-4abdd973c007.png)




Discussion:
@p770193 what is your favorite form for a complex Lorentzian?

Resolving patch:
diff --git a/WrightTools/kit/__init__.py b/WrightTools/kit/__init__.py
--- a/WrightTools/kit/__init__.py
+++ b/WrightTools/kit/__init__.py
@@ -7,6 +7,7 @@
 from ._ini import *
 from ._interpolate import *
 from ._leastsq import *
+from ._lineshapes import *
 from ._list import *
 from ._path import *
 from ._timestamp import *
diff --git a/WrightTools/kit/_lineshapes.py b/WrightTools/kit/_lineshapes.py
new file mode 100644
--- /dev/null
+++ b/WrightTools/kit/_lineshapes.py
@@ -0,0 +1,135 @@
+"""Lineshapes."""
+
+
+# --- import -------------------------------------------------------------
+
+
+import numpy as np
+
+from scipy.special import wofz
+
+
+# --- define -------------------------------------------------------------
+
+
+__all__ = ["gaussian", "lorentzian_complex", "lorentzian_real", "voigt"]
+
+
+# --- functions ----------------------------------------------------------
+
+
+def gaussian(x, x0, FWHM, norm="height"):
+    """Calculate a normalized Gaussian lineshape
+
+    Parameters
+    ----------
+    x : array_like or number
+        Input array.
+    x0 : array_like or number
+        Center of lineshape.
+    FHWM : array_like or number
+        Full-width-at-half-maximum of lineshape.
+    norm : string (optional):
+        Type of normalization.
+        height specifies that the maximum value is 1.
+        area specifies that the lineshape integrates to 1.
+        Default is height.        
+
+    Returns
+    -------
+    array_like or number
+        Gaussian lineshape.
+    """
+    c = FWHM / (2 * np.sqrt(2 * np.log(2)))
+    arr = np.exp(-1 * (x - x0) ** 2 / (2 * c ** 2))
+    if norm == "height":
+        return arr
+    elif norm == "area":
+        return arr / (c * np.sqrt(2 * np.pi))
+
+
+def lorentzian_complex(x, x0, G, norm="height_imag"):
+    """Calculate a normalized complex Lorentzian lineshape
+
+    Parameters
+    ----------
+    x : array_like or number
+        Input array.
+    x0 : array_like or number
+        Center of lineshape.
+    G : array_like or number
+        Half-width-at-half-maximum of lineshape.
+    norm : string (optional):
+        Type of normalization.
+        height_imag specifies that the maximum value of the imaginary component is 1.
+        area_int specifies that the square magnitude of the lineshape integrates to 1.
+        Default is height_imag.       
+
+    Returns
+    -------
+    array_like or number
+        Complex Lorentzian lineshape.
+    """
+    arr = 1 / (x0 - x - 1j * G)
+    if norm == "height_imag":
+        return arr * G
+    elif norm == "area_int":
+        return arr * np.sqrt(G / np.pi)
+
+
+def lorentzian_real(x, x0, G, norm="height"):
+    """Calculate a normalized Lorentzian lineshape
+
+    Parameters
+    ----------
+    x : array_like or number
+        Input array.
+    x0 : array_like or number
+        Center of lineshape.
+    G : array_like or number
+        Half-width-at-half-maximum of lineshape.
+    norm : string (optional):
+        Type of normalization.
+        height specifies that the maximum value is 1.
+        area specifies that the lineshape integrates to 1.
+        Default is height.        
+
+    Returns
+    -------
+    array_like or number
+        Lorentzian lineshape.
+    """
+    arr = G ** 2 / ((x - x0) ** 2 + G ** 2)
+    if norm == "height":
+        return arr
+    if norm == "area":
+        return arr / (G * np.pi)
+
+
+def voigt(x, x0, FWHM, G):
+    """Calculate an unnormalized Voigt lineshape using Scipy's Faddeeva function
+    
+    `Link to Voigt article on Wikipedia`__
+
+    __ https://en.wikipedia.org/wiki/Voigt_profile  
+
+    Parameters
+    ----------
+    x : array_like or number
+        Input array.
+    x0 : array_like or number
+        Center of lineshape.
+    FHWM : array_like or number
+        Full-width-at-half-maximum of Gaussian part of lineshape.
+    G : array_like or number
+        Half-width-at-half-maximum of Lorentzian part of lineshape.  
+
+    Returns
+    -------
+    array_like or number
+        Voigt lineshape.
+    """
+    c = FWHM / (2 * np.sqrt(2 * np.log(2)))
+    arr = (x - x0 + 1j * G) / (c * np.sqrt(2))
+    w = wofz(arr)
+    return np.real(w) / (c * np.sqrt(2 * np.pi))
diff --git a/examples/lineshapes.py b/examples/lineshapes.py
new file mode 100644
--- /dev/null
+++ b/examples/lineshapes.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+"""
+Lineshapes
+==========
+
+Common lineshapes included in kit
+"""
+
+import matplotlib.pyplot as plt
+
+import WrightTools as wt
+
+import numpy as np
+
+
+# initiate figure
+fig, gs = wt.artists.create_figure(nrows=2, default_aspect=0.6)
+axs = [plt.subplot(gs[i]) for i in range(2)]
+
+# initial parameters
+x = np.linspace(-2, 2, 1001)
+x0 = 0
+FWHM = 1
+G = 0.5
+# plot all of the real versions
+ax = axs[0]
+y = wt.kit.gaussian(x, x0, FWHM, norm="height")
+ax.plot(x, y, label="Gaussian, height")
+y = wt.kit.gaussian(x, x0, FWHM, norm="area")
+ax.plot(x, y, label="Gaussian, area")
+y = wt.kit.lorentzian_real(x, x0, G, norm="height")
+ax.plot(x, y, label="Lorentzian, height")
+y = wt.kit.lorentzian_real(x, x0, G, norm="area")
+ax.plot(x, y, label="Lorentzian, area")
+y = wt.kit.voigt(x, x0, FWHM, G)
+ax.plot(x, y / y.max(), label="Voigt")
+# plot the complex variants
+ax = axs[1]
+y = wt.kit.lorentzian_complex(x, x0, G, norm="height_imag")
+ax.plot(x, y.real, label="Re[L], height_imag")
+ax.plot(x, y.imag, label="Im[L], height_imag")
+y = wt.kit.lorentzian_complex(x, x0, G, norm="area_int")
+ax.plot(x, y.real, label="Re[L], area_int")
+ax.plot(x, y.imag, label="Im[L], area_int")
+# finish
+for ax in axs:
+    ax.legend(
+        bbox_to_anchor=(1.04, 0.5),
+        loc="center left",
+        borderaxespad=0,
+        title="Lineshape, normalization",
+    )
+    ax.grid()
+    ax.set_xlim(-2, 2)
+wt.artists.set_fig_labels(fig, "X", "Y")


Test patch:
diff --git a/tests/kit/lineshapes.py b/tests/kit/lineshapes.py
new file mode 100644
--- /dev/null
+++ b/tests/kit/lineshapes.py
@@ -0,0 +1,55 @@
+"""Test lineshapes."""
+
+
+# --- import --------------------------------------------------------------------------------------
+
+
+import numpy as np
+
+import WrightTools as wt
+
+
+# --- test ----------------------------------------------------------------------------------------
+
+
+def test_gaussian():
+    x = np.linspace(-2, 2, 1001)
+    x0 = 0
+    FWHM = 1
+    y = wt.kit.gaussian(x, x0, FWHM, norm="area")
+    assert np.isclose(np.trapz(y, x), 1, rtol=1e-3, atol=1e-3)
+    y = wt.kit.gaussian(x, x0, FWHM, norm="height")
+    assert np.isclose(y.max(), 1, rtol=1e-3, atol=1e-3)
+
+
+def test_lorentzian_complex():
+    x = np.linspace(-50, 50, 100001)
+    x0 = 0
+    G = 0.1
+    y = wt.kit.lorentzian_complex(x, x0, G, norm="area_int")
+    assert np.isclose(np.trapz(np.abs(y) ** 2, x), 1, rtol=1e-3, atol=1e-3)
+    y = wt.kit.lorentzian_complex(x, x0, G, norm="height_imag")
+    assert np.isclose(y.imag.max(), 1, rtol=1e-3, atol=1e-3)
+
+
+def test_lorentzian_real():
+    x = np.linspace(-50, 50, 100001)
+    x0 = 0
+    G = 0.1
+    y = wt.kit.lorentzian_real(x, x0, G, norm="area")
+    assert np.isclose(np.trapz(y, x), 1, rtol=1e-3, atol=1e-3)
+    y = wt.kit.lorentzian_real(x, x0, G, norm="height")
+    assert np.isclose(y.max(), 1, rtol=1e-3, atol=1e-3)
+
+
+def test_voigt():
+    x = np.linspace(-2, 2, 1001)
+    x0 = 0
+    G = 0.5
+    FWHM = 1
+    gauss = wt.kit.gaussian(x, x0, FWHM, norm="height")
+    lor = wt.kit.lorentzian_real(x, x0, G, norm="height")
+    y = wt.kit.voigt(x, x0, FWHM, 0)
+    assert np.all(np.isclose(y / y.max(), gauss, rtol=1e-3, atol=1e-3))
+    y = wt.kit.voigt(x, x0, 1e-6, G)
+    assert np.all(np.isclose(y / y.max(), lor, rtol=1e-3, atol=1e-3))