# -*- coding: utf-8 -*-
# written by Ralf Biehl at the Forschungszentrum Jülich ,
# Jülich Center for Neutron Science 1 and Institute of Complex Systems 1
# Jscatter is a program to read, analyse and plot data
# Copyright (C) 2015-2024 Ralf Biehl
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Physical equations and useful formulas as quadrature of vector functions, parallel execution,
viscosity, compressibility of water, scatteringLengthDensityCalc or sedimentationProfile.
Use scipy.constants for physical constants.
- Each topic is not enough for a single module, so this is a collection.
- All scipy functions can be used. See http://docs.scipy.org/doc/scipy/reference/special.html.
- Statistical functions http://docs.scipy.org/doc/scipy/reference/stats.html.
Mass and scattering length of all elements in Elements are taken from :
- Mass: http://www.chem.qmul.ac.uk/iupac/AtWt/
- Neutron scattering length: http://www.ncnr.nist.gov/resources/n-lengths/list.html
Units converted to amu for mass and nm for scattering length.
"""
import inspect
import math
import os
import numpy as np
import scipy.constants as constants
import scipy.special as special
from scipy import stats
from ..dataarray import dataArray as dA
from ..dataarray import dataList as dL
from ..libs import ml_internal
_path_ = os.path.realpath(os.path.dirname(__file__))
__all__ = ['eijk', 'box', 'gauss', 'lorentz', 'schulzDistribution', 'lognorm', 'voigt', 'Ea', 'boseDistribution']
#: Variable to allow printout for debugging as if debug:print('message')
debug = False
#: Antisymmetric Levi-Civita symbol
eijk = np.zeros((3, 3, 3))
eijk[0, 1, 2] = eijk[1, 2, 0] = eijk[2, 0, 1] = 1
eijk[0, 2, 1] = eijk[2, 1, 0] = eijk[1, 0, 2] = -1
[docs]
def box(x, edges=None, edgevalue=0, rtol=1e-05, atol=1e-08):
"""
Box function.
For equal edges and edge value > 0 the delta function is given.
Parameters
----------
x : array
edges : list of float, float, default=[0]
Edges of the box.
If only one number is given the box goes from [-edge:edge]
edgevalue : float, default=0
Value to use if x==edge for both edges.
rtol,atol : float
The relative/absolute tolerance parameter for the edge detection.
See numpy.isclose.
Returns
-------
dataArray
Notes
-----
Edges may be smoothed by convolution with a Gaussian.::
import jscatter as js
import numpy as np
edge=2
x=np.r_[-4*edge:4*edge:200j]
f=js.formel.box(x,edges=edge)
res=js.formel.convolve(f,js.formel.gauss(x,0,0.2))
#
p=js.mplot()
p.Plot(f,li=1,le='box')
p.Plot(res,li=2,le='smooth box')
p.Legend()
#p.savefig(js.examples.imagepath+'/box.jpg')
.. image:: ../../examples/images/box.jpg
:align: center
:height: 300px
:alt: smooth
"""
if edges is None:
edges = [0]
edges = np.atleast_1d(edges)
if edges.shape[0] < 2: edges = np.r_[-abs(edges[0]), abs(edges[0])]
v = np.zeros_like(x)
v[(x > edges[0]) & (x < edges[1])] = 1
v[(np.isclose(x, edges[0], rtol, atol)) | (np.isclose(x, edges[1], rtol, atol))] = edgevalue
box = dA(np.c_[x, v].T)
box.setColumnIndex(iey=None)
box.modelname = inspect.currentframe().f_code.co_name
return box
[docs]
def gauss(x, mean=1, sigma=1):
r"""
Normalized Gaussian function.
.. math:: g(x)= \frac{1}{sigma\sqrt{2\pi}} e^{-0.5(\frac{x-mean}{sigma})^2}
Parameters
----------
x : float
Values
mean : float
Mean value
sigma : float
1/e width.
Negative values result in negative amplitude.
Returns
-------
dataArray
"""
x = np.atleast_1d(x)
result = dA(np.c_[x, np.exp(-0.5 * (x - mean) ** 2 / sigma ** 2) / sigma / np.sqrt(2 * np.pi)].T)
result.setColumnIndex(iey=None)
result.mean = mean
result.sigma = sigma
result.modelname = inspect.currentframe().f_code.co_name
return result
[docs]
def lorentz(x, mean=1, gamma=1):
r"""
Normalized Lorentz function
.. math :: f(x) = \frac{gamma}{\pi((x-mean)^2+gamma^2)}
Parameters
----------
x : array
X values
gamma : float
Half width half maximum
mean : float
Mean value
Returns
-------
dataArray
"""
x = np.atleast_1d(x)
result = dA(np.c_[x, gamma / ((x - mean) ** 2 + gamma ** 2) / np.pi].T)
result.setColumnIndex(iey=None)
result.mean = mean
result.gamma = gamma
result.modelname = inspect.currentframe().f_code.co_name
return result
def _schulz(r, m, z):
# not used anymore
z1 = z + 1
if np.all(z < 150):
f = z1 ** z1 * (r / m) ** z / m / special.gamma(z1) * np.exp(-z1 * r / m)
else:
# using Stirling equation and exp(log(....))
# f = (r / m) ** z / m / (2 * np.pi) ** 0.5 * z1 ** 0.5 * np.exp(z1 * (1 - r / m))
f = np.exp(z * np.log(r / m) + z1 * (1 - r / m)) / m / (2 * np.pi) ** 0.5 * z1 ** 0.5
return f
[docs]
def schulzDistribution(r, mean, sigma):
r"""
Schulz (or Gamma) distribution for polymeric particles/chains.
Distribution describing a polymerisation like radical polymerization:
- constant number of chains growth till termination.
- concentration of active centers constant.
- start of chain growth not necessarily at the same time.
- In polymer physics sometimes called Schulz-Zimm distribution. Same as Gamma distribution.
Parameters
----------
r : array
Distribution variable such as relative molecular mass or degree of polymerization, number of monomers.
mean : float
Mean :math:`<r>`
sigma : float
Width as standard deviation :math:`s=\sqrt{<r^2>-<r>^2}` of the distribution.
:math:`z = (<r>/s)^2 -1 < 600`
Returns
-------
dataArray : Columns [x,p]
- .z ==> z+1 = k is degree of coupling = number of chain combined to dead chain in termination reaction
z = (<r>/s)² -1
Notes
-----
The Schulz distribution [1]_
.. math:: h(r) = \frac{(z+1)^{z+1}r^z}{(mean^{z+1}\Gamma(z+1)}e^{-(z+1)\frac{r}{mean}}
alternatively with :math:`a=<r>^2/s^2` and :math:`b=a/<r>`
.. math:: h(r) = \frac{b^a r^(a-1)}{(\Gamma(a)}e^{-br}
Normalized to :math:`\int h(r)dr=1`.
Nth order average :math:`<r>^n = \frac{z+n}{z+1} <r>`
- number average :math:`<r>^1 = <r>`
- weight average :math:`<r>^2 = \frac{z+2}{z+1} <r>`
- z average :math:`<r>^3 = \frac{z+3}{z+1} <r>`
Examples
--------
::
import jscatter as js
import numpy as np
N=np.r_[1:200]
p=js.grace(1.4,1)
p.multi(1,2)
m=50
for i,s in enumerate([10,20,40,50,75,100,150],1):
SZ = js.formel.schulzDistribution(N,mean=m,sigma=s)
p[0].plot(SZ.X/m,SZ.Y,sy=0,li=[1,3,i],le=f'sigma/mean={s/m:.1f}')
p[1].plot(SZ.X/m,SZ.Y*SZ.X,sy=0,li=[1,3,i],le=f'sigma/mean={s/m:.1f}')
p[0].xaxis(label='N/mean')
p[0].yaxis(label='h(N)')
p[0].subtitle('number distribution')
p[1].xaxis(label='N/mean')
p[1].yaxis(label='N h(N)')
p[1].subtitle('mass distribution')
p[1].legend(x=2,y=1.5)
p[0].title('Schulz distribution')
#p.save(js.examples.imagepath+'/schulzZimm.jpg')
.. image:: ../../examples/images/schulzZimm.jpg
:align: center
:height: 300px
:alt: schulzZimm
References
----------
.. [1] Schulz, G. V. Z. Phys. Chem. 1939, 43, 25
.. [2] Theory of dynamic light scattering from polydisperse systems
S. R. Aragón and R. Pecora
The Journal of Chemical Physics, 64, 2395 (1976)
"""
z = (mean / sigma) ** 2 - 1
a=mean ** 2 / sigma ** 2
scale=sigma ** 2 / mean
result = dA(np.c_[r, stats.gamma.pdf(r, a=a, scale=scale)].T)
result.setColumnIndex(iey=None)
result.columnname = 'x; p'
result.mean = mean
result.sigma = sigma
result.z = z
result.modelname = inspect.currentframe().f_code.co_name
return result
[docs]
def lognorm(x, mean=1, sigma=1):
r"""
Lognormal distribution function.
.. math:: f(x>0)= \frac{1}{\sqrt{2\pi}\sigma x }\,e^{ -\frac{(\ln(x)-\mu)^2}{2\sigma^2}}
Parameters
----------
x : array
x values
mean : float
mean
sigma : float
sigma
Returns
-------
dataArray
"""
mu = math.log(mean ** 2 / (sigma + mean ** 2) ** 0.5)
nu = (math.log(sigma / mean ** 2 + 1)) ** 0.5
distrib = stats.lognorm(s=nu, scale=math.exp(mu))
result = dA(np.c_[x, distrib.pdf(x)].T)
result.setColumnIndex(iey=None)
result.mean = mean
result.sigma = sigma
result.modelname = inspect.currentframe().f_code.co_name
return result
[docs]
def voigt(x, center=0, fwhm=1, lg=1, asym=0, amplitude=1):
r"""
Voigt function for peak analysis (normalized).
The Voigt function is a convolution of gaussian and lorenzian shape peaks for peak analysis.
The Lorenzian shows a stronger contribution outside FWHM with a sharper peak.
Asymmetry of the shape can be added by a sigmoidal change of the FWHM [2]_.
Parameters
----------
x : array
Axis values.
center : float
Center of the distribution.
fwhm : float
Full width half maximum of the Voigt function.
lg : float, default = 1
Lorenzian/gaussian fraction of both FWHM, describes the contributions of gaussian and lorenzian shape.
- lorenzian/gaussian >> 1 lorenzian,
- lorenzian/gaussian ~ 1 central part gaussian, outside lorenzian wings
- lorenzian/gaussian << 1. gaussian
asym : float, default=0
Asymmetry factor in sigmoidal as :math:`fwhm_{asym} = 2*fwhm/(1+np.exp(asym*(x-center)))` .
For a=0 the Voigt is symmetric.
amplitude : float, default = 1
amplitude
Returns
-------
dataArray
.center
.sigma
.gamma
.fwhm
.asymmetry
.lorenzianOverGaussian (lg)
Notes
-----
The Voigt function is a convolution of Gaussian and Lorentz functions
.. math:: G(x;\sigma) = e^{-x^2/(2\sigma^2)}/(\sigma \sqrt{2\pi})\ and \
L(x;\gamma) = \gamma/(\pi(x^2+\gamma^2))
resulting in
.. math:: V(x;\sigma,\gamma)=\frac{\operatorname{Re}[w(z)]}{\sigma\sqrt{2 \pi}}
with :math:`z=(x+i\gamma)/(\sigma\sqrt{2})` and :math:`Re[w(z)]` is the real part of the Faddeeva function.
:math:`\gamma` is the Lorentz fwhm width and :math:`fwhm=(2\sqrt{2\ln 2})\sigma` the Gaussian fwhm width.
The FWHM in Lorentz and Gaussian dependent on the fwhm of the Voigt function is
:math:`fwhm_{Gauss,Lorentz} \approx fwhm / (0.5346 lg + (0.2166 lg^2 + 1)^{1/2})` (accuracy 0.02%).
References
----------
.. [1] https://en.wikipedia.org/wiki/Voigt_profile
.. [2] A simple asymmetric lineshape for fitting infrared absorption spectra
Aaron L. Stancik, Eric B. Brauns
Vibrational Spectroscopy 47 (2008) 66–69
.. [3] Empirical fits to the Voigt line width: A brief review
Olivero, J. J.; R. L. Longbothum
Journal of Quantitative Spectroscopy and Radiative Transfer. 17, 233–236. doi:10.1016/0022-4073(77)90161-3
"""
ln2 = math.log(2)
# calc the fwhm in gauss and lorenz to get the final FWHM in the Voigt function with an accuracy of 0.02%
# as given in Olivero, J. J.; R. L. Longbothum (February 1977).
# Empirical fits to the Voigt line width: A brief review".
# Journal of Quantitative Spectroscopy and Radiative Transfer. 17 (2): 233–236.
# doi:10.1016/0022-4073(77)90161-3
FWHM = fwhm / (0.5346 * lg + (0.2166 * lg ** 2 + 1) ** 0.5)
def z(fwhm):
return ((x - center) + 1j * lg * fwhm / 2.) / math.sqrt(2) / (fwhm / (2 * np.sqrt(2 * ln2)))
# the sigmoidal fwhm for asymmetry
def afwhm(fwhm, a):
return 2 * fwhm / (1 + np.exp(a * (x - center)))
# calc values with asymmetric FWHM
val = amplitude / (afwhm(FWHM, asym) / (2 * np.sqrt(2 * ln2))) / math.sqrt(2 * np.pi) * \
special.wofz(z(afwhm(FWHM, asym))).real
result = dA(np.c_[x, val].T)
result.setColumnIndex(iey=None)
result.center = center
result.sigma = (FWHM / (2 * np.sqrt(2 * ln2)))
result.gamma = FWHM / 2.
result.fwhm = fwhm
result.asymmetry = asym
result.lorenzianOverGaussian = lg
result.modelname = inspect.currentframe().f_code.co_name
return result
[docs]
def Ea(z, a, b=1):
r"""
Mittag-Leffler function for real z and real a,b with 0<a, b<0.
Evaluation of the Mittag-Leffler (ML) function with 1 or 2 parameters by means of the OPC algorithm [1].
The routine evaluates an approximation Et of the ML function E such that
:math:`|E-Et|/(1+|E|) \approx 10^{-15}`
Parameters
----------
z : real array
Values
a : float, real positive
Parameter alpha
b : float, real positive, default=1
Parameter beta
Returns
-------
array
Notes
-----
- Mittag Leffler function defined as
.. math:: E(x,a,b)=\sum_{k=0}^{\inf} \frac{z^k}{\Gamma(b+ak)}
- The code uses code from K.Hinsen at https://github.com/khinsen/mittag-leffler
which is a Python port of
`Matlab implementation <https://se.mathworks.com/matlabcentral/fileexchange/48154-the-mittag-leffler-function>`_
of the generalized Mittag-Leffler function as described in [1]_.
- The function cannot be simply calculated by using the above summation.
This fails for a,b<0.7 because of various numerical problems.
The above implementation of K.Hinsen is the best availible approximation in Python.
Examples
--------
::
import numpy as np
import jscatter as js
from scipy import special
x=np.r_[-10:10:0.1]
# tests
np.all(js.formel.Ea(x,1,1)-np.exp(x)<1e-10)
z = np.linspace(0., 2., 50)
np.allclose(js.formel.Ea(np.sqrt(z), 0.5), np.exp(z)*special.erfc(-np.sqrt(z)))
z = np.linspace(-2., 2., 50)
np.allclose(js.formel.Ea(z**2, 2.), np.cosh(z))
References
----------
.. [1] R. Garrappa, Numerical evaluation of two and three parameter Mittag-Leffler functions,
SIAM Journal of Numerical Analysis, 2015, 53(3), 1350-1369
"""
if a <= 0 or b <= 0:
raise ValueError('a and b must be real and positive.')
g = 1 # only use gamma=1
log_epsilon = np.log(1.e-15)
# definition through Laplace transform inversion
# we use for this the code from K.Hinsen, see header in ml_internal
_eaLPI = lambda z: np.vectorize(ml_internal.LTInversion, [np.float64])(1, z, a, b, g, log_epsilon)
res = np.zeros_like(z, dtype=np.float64)
eps = 1.e-15
choose = np.abs(z) <= eps
res[choose] = 1 / special.gamma(b)
res[~choose] = _eaLPI(z[~choose])
return res
[docs]
def boseDistribution(w, temp):
r"""
Bose distribution for integer spin particles in non-condensed state (hw>0).
.. math::
n(w) &= \frac{1}{e^{hw/kT}-1} &\ hw>0
&= 0 &\: hw=0 \: This is not real just for convenience!
Parameters
----------
w : array
Frequencies in units 1/ns
temp : float
Temperature in K
Returns
-------
dataArray
"""
h = constants.h
k = constants.k
bose = np.piecewise(w, [w == 0], [0, 1 / (np.exp(h * w[w != 0] * 1e9 / (k * temp)) - 1)])
result = dA(np.c_[w, bose].T)
result.setColumnIndex(iey=None)
result.columnname = 'w; n'
result.temperature = temp
result.modelname = inspect.currentframe().f_code.co_name
return result