# -*- 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 numbers
import warnings
import multiprocessing as mp
import itertools
import numpy as np
from scipy import stats
from scipy.special.orthogonal import roots_legendre
from ..dataarray import dataArray as dA
from ..dataarray import dataList as dL
from ..libs.cubature import cubature
from .. import parallel
_path_ = os.path.realpath(os.path.dirname(__file__))
__all__ = ['convolve', 'parQuadratureFixedGauss', 'parQuadratureFixedGaussxD', 'parQuadratureAdaptiveGauss',
'parQuadratureAdaptiveGauss', 'parAdaptiveCubature', 'parQuadratureAdaptiveClenshawCurtis',
'parQuadratureSimpson', 'simpleQuadratureSimpson', 'parDistributedAverage',
'multiParDistributedAverage', 'scatteringFromSizeDistribution', '_getFuncCode']
#: Variable to allow printout for debugging as if debug:print('message')
debug = False
class AccuracyWarning(Warning):
pass
def _getFuncCode(func):
"""
Get code object of a function
"""
try:
return func.__code__
except AttributeError:
return None
[docs]
def convolve(A, B, mode='same', normA=False, normB=False):
r"""
Convolve A and B with proper tracking of the output X axis.
Approximate the convolution integral as the discrete, linear convolution of two one-dimensional sequences.
Missing values are linear interpolated to have matching steps. Values outside of X ranges are set to zero.
Parameters
----------
A,B : dataArray, ndarray
To be convolved arrays (length N and M).
- dataArray convolves Y with Y values
- ndarray A[0,:] is X and A[1,:] is Y
normA,normB : bool, default False
Determines if A or B should be normalised that :math:`\int_{x_{min}}^{x_{max}} A(x) dx = 1`.
mode : 'full','same','valid', default 'same'
See example for the difference in range.
- 'full' Returns the convolution at each point of overlap,
with an output shape of (N+M-1,).
At the end-points of the convolution, the signals do not overlap completely,
and boundary effects may be seen.
- 'same' Returns output of length max(M, N).
Boundary effects are still visible.
- 'valid' Returns output of length M-N+1.
Returns
-------
dataArray
with attributes from A
Notes
-----
- :math:`A\circledast B (t)= \int_{-\infty}^{\infty} A(x) B(t-x) dx = \int_{x_{min}}^{x_{max}} A(x) B(t-x) dx`
- If A,B are only 1d array use np.convolve.
- If attributes of B are needed later use .setattr(B,'B-') to prepend 'B-' for B attributes.
Examples
--------
Demonstrate the difference between modes
::
import jscatter as js;import numpy as np
s1=3;s2=4;m1=50;m2=10
G1=js.formel.gauss(np.r_[0:100.1:0.1],mean=m1,sigma=s1)
G2=js.formel.gauss(np.r_[-30:30.1:0.2],mean=m2,sigma=s2)
p=js.grace()
p.title('Convolution of Gaussians (width s mean m)')
p.subtitle(r's1\S2\N+s2\S2\N=s_conv\S2\N ; m1+m2=mean_conv')
p.plot(G1,le='mean 50 sigma 3')
p.plot(G2,le='mean 10 sigma 4')
ggf=js.formel.convolve(G1,G2,'full')
p.plot(ggf,le='full')
gg=js.formel.convolve(G1,G2,'same')
p.plot(gg,le='same')
gg=js.formel.convolve(G1,G2,'valid')
p.plot(gg,le='valid')
gg.fit(js.formel.gauss,{'mean':40,'sigma':1},{},{'x':'X'})
p.plot(gg.modelValues(),li=1,sy=0,le='fit m=$mean s=$sigma')
p.legend(x=100,y=0.1)
p.xaxis(max=150,label='x axis')
p.yaxis(min=0,max=0.15,label='y axis')
p.save(js.examples.imagepath+'/convolve.jpg')
.. image:: ../../examples/images/convolve.jpg
:align: center
:height: 300px
:alt: convolve
References
----------
.. [1] Wikipedia, "Convolution", http://en.wikipedia.org/wiki/Convolution.
"""
# convert to array
if hasattr(A, '_isdataArray'):
AY = A.Y
AX = A.X
else:
AX = A[0, :]
AY = A[1, :]
if normA:
AY = AY /np.trapz(AY, AX)
if hasattr(B, '_isdataArray'):
BY = B.Y
BX = B.X
else:
BX = B[0, :]
BY = B[1, :]
if normB:
BY = BY /np.trapz(BY, BX)
# create a combined x scale
dx = min(np.diff(AX).min(), np.diff(BX).min())
ddx = 0.1 * dx # this accounts for the later >= BX.min() to catch problems with numerical precision
XX = np.r_[min(AX.min(), BX.min()):max(AX.max(), BX.max()) + dx:dx]
# interpolate missing values
# if x scale is equal this is nearly no overhead
AXX = XX[(XX >= AX.min() - ddx) & (XX <= AX.max() + ddx)]
AY_xx = np.interp(AXX, AX, AY, left=0, right=0)
BXX = XX[(XX >= BX.min() - ddx) & (XX <= BX.max() + ddx)]
BY_xx = np.interp(BXX, BX, BY, left=0, right=0)
if len(AXX) < len(BXX):
# AXX always the larger one; this is also done in C source
AXX, BXX = BXX, AXX
# convolve
res = np.convolve(AY_xx, BY_xx, mode=mode) * dx
# define x scale
# n, nleft, nright, length to reproduce C-source of convolve
n = BXX.shape[0]
l = AXX.shape[0]
xx = np.r_[AX.min() + BX.min() : AX.max() + BX.max() + dx : dx]
if mode == 'full': # length=l+n-1
nleft = 0
nright = l + n - 1
elif mode == 'valid': # length=l-n+1
nleft = n - 1
nright = l
else: # mode=='same' # length=l
nleft = (n - 1) // 2
nright = nleft + l
xx = xx[nleft:nright]
result = dA(np.c_[xx, res].T)
result.setattr(A)
return result
def _cached_roots_legendre(n):
"""
Cache roots_legendre results to speed up calls of the fixed_quad function.
"""
# scipy.integrate.quadrature
if n in _cached_roots_legendre.cache:
return _cached_roots_legendre.cache[n]
_cached_roots_legendre.cache[n] = roots_legendre(n)
return _cached_roots_legendre.cache[n]
_cached_roots_legendre.cache = dict()
# noinspection PyIncorrectDocstring
[docs]
def parQuadratureFixedGauss(func, lowlimit, uplimit, parname, n=25, weights=None, **kwargs):
r"""
Vectorized definite integral using fixed-order Gaussian quadrature. Shortcut pQFG.
Integrate func over parname from `lowlimit` to `uplimit` using Gauss-Legendre quadrature [1]_ of order `n`.
All columns are integrated.
For func return values as dataArray the .X is recovered (unscaled) while for array
also the X are integrated and weighted.
Parameters
----------
func : callable
A Python function or method returning a vector array of dimension 1.
If func returns dataArray .Y is integrated.
lowlimit : float
Lower limit of integration.
uplimit : float
Upper limit of integration.
parname : string
Name of the integration variable which should be a scalar.
After evaluation the corresponding attribute has the mean value with weights.
weights : ndarray shape(2,N),default=None
- Weights for integration along parname with lowlimit<weights[0]<uplimit and weights[1] as weight values.
- Missing values are linear interpolated.
If None equal weights=1 are used.
To use normalised weights normalise it or use scipy.stats distributions.
kwargs : dict, optional
Extra keyword arguments to pass to function, if any.
n : int, optional
Order of quadrature integration. Default is 15.
ncpu : int,default=1, optional
Use parallel processing for the function with ncpu parallel processes in the pool.
Set this to 1 if the function is already fast enough or if the integrated function uses multiprocessing.
- 0 -> all cpus are used
- int>0 min (ncpu, mp.cpu_count)
- int<0 ncpu not to use
Returns
-------
array or dataArray
Notes
-----
Gauss-Legendre quadrature of function :math:`f(x,p)` over parameter :math:`p` with a weight function :math:`v(p)`
Reimplementation of scipy.integrate.quadrature.fixed_quad to work with vector output
of the integrand function and weights.
:math:`w_i` and :math:`p_i` are the associated weights and knots of the Legendre polynominals.
.. math:: \int_{-1}^1 f(x,p) v(p) dp \approx \sum_{i=1}^n w_i v_i f(x,p_i)
Change of interval to [a,b] is done as
.. math:: \int_a^b f(x, p)\,dp = \int_{-1}^1 \left(\frac{b-a}{2}\xi + \frac{a+b}{2}\right) \frac{dx}{d\xi}d\xi
with :math:`\frac{dx}{d\xi} = \frac{b-a}{2}` .
Examples
--------
A simple polydispersity of spheres: integrate size distribution with weights.
We see that Rg and I0 at low Q also change because of polydispersity. :math:`I_0~R^6`.
Minima are smeared out.
For a Gaussian distribution the edge values are less weighted but broader and the central
values are stronger weighted. The effect on I0 is stronger and the minima are more smeared.
The uniform distribution is the same as weights=None, but normalised to 1.
Using None we get the same (weight=1) if the result is normalised by the width 2*sig.
::
import jscatter as js
import scipy
q=js.loglist(0.01,5,500)
p=js.grace()
p.multi(2,1)
mean=5
# use a uniform distribution
for sig in [0.1,0.5,0.8,1]: # distribution width
distrib = scipy.stats.uniform(loc=mean-sig,scale=2*sig)
x = np.r_[mean-3*sig:mean+3*sig:30j]
pdf = np.c_[x,distrib.pdf(x)].T
sp2 = js.formel.pQFG(js.ff.sphere,mean-sig,mean+sig,'radius',q=q,radius=mean,n=20, weights=pdf)
p[0].plot(sp2.X, sp2.Y,sy=[-1,0.2,-1])
p[0].yaxis(label='I(Q)',scale='l')
p[0].xaxis(label=r'')
p[0].text('uniform distribution',x=2,y=1e5)
# use a Gaussian distribution
for sig in [0.1,0.5,0.8,1]: # distribution width
distrib = scipy.stats.norm(loc=mean,scale=sig)
x = np.r_[mean-3*sig:mean+3*sig:30j]
pdf = np.c_[x,distrib.pdf(x)].T
sp2 = js.formel.pQFG(js.ff.sphere,mean-2*sig,mean+2*sig,'radius',q=q,radius=mean,n=25,weights=pdf)
p[1].plot(sp2.X, sp2.Y,sy=[-1,0.2,-1])
p[1].yaxis(label='I(Q)',scale='l')
p[1].xaxis(label=r'Q / nm\S-1')
p[1].text('Gaussian distribution',x=2,y=1e5)
Integrate Gaussian as test case. This is not the intended usage.
As we integrate over .X the final .X will be the last integration point .X, here the last Legendre knot.
The integral is 1 as the gaussian is normalised.
::
import jscatter as js
import numpy as np
import scipy
# testcase: integrate over x of a function
# area under normalized gaussian is 1
js.formel.pQFG(js.formel.gauss,-10,10,'x',mean=0,sigma=1)
References
----------
.. [1] https://en.wikipedia.org/wiki/Gaussian_quadrature
"""
x, w = _cached_roots_legendre(n)
x = np.real(x)
if np.isinf(lowlimit) or np.isinf(uplimit):
raise ValueError("Gaussian quadrature is only available for finite limits.")
y = (uplimit - lowlimit) * (x + 1) / 2.0 + lowlimit
if weights is None:
wy = np.ones_like(y)
parmean = (uplimit + lowlimit) / 2
else:
wy = np.interp(y, weights[0], weights[1])
wy = wy
# integrate y*wy to get mean y value
parmean = np.sum(y * w * wy)
# use Gauss quadrature to integrate weight in limits and normalise
# normfactor = (uplimit - lowlimit) / 2.0 * np.sum(w * wy)
# set default for ncpu to use only one process.
if 'ncpu' not in kwargs:
kwargs.update({'ncpu': 1, 'output': False})
# calc the function values
res = parallel.doForList(func, looplist=y, loopover=parname, **kwargs)
if isinstance(res[0], dA):
res[0][:, :] = (uplimit - lowlimit) / 2.0 * np.sum(w * wy * np.atleast_2d(res).T, axis=-1).T
res[0].X = res[-1].X # retrieve unweighted X values
setattr(res[0], parname, parmean)
return res[0]
else:
return (uplimit - lowlimit) / 2.0 * np.sum(w * wy * np.atleast_2d(res).T, axis=-1).T
[docs]
def parQuadratureFixedGaussxD(func, lowlimit, uplimit, parnames, n=5,
weights0=None, weights1=None, weights2=None, **kwargs):
r"""
Vectorized fixed-order Gauss-Legendre quadrature in definite interval in 1,2,3 dimensions. Shortcut pQFGxD.
Integrate func over parnames between limits using Gauss-Legendre quadrature [1]_ of order `n`.
Parameters
----------
func : callable
Function to integrate.
The return value should be 2 dimensional array with first dimension along integration variable
and second along array to calculate. See examples.
parnames : list of string, max len=3
Name of the integration variables which should be scalar in the function.
lowlimit : list of float
Lower limits a of integration for parnames.
uplimit : list of float
Upper limits b of integration for parnames.
weights0,weights1,weights3 : ndarray shape(2,N), default=None
- Weights for integration along parname with lowlimit<weightsi<uplimit and weightsi[1] contains weight values.
- Missing values are linear interpolated (faster).
- None: equal weights=1 are used.
If None equal weights=1 are used.
To use normalised weights normalise it or use scipy.stats distributions.
kwargs : dict, optional
Extra keyword arguments to pass to function, if any.
n : int, optional
Order of quadrature integration for all parnames. Default is 5.
Returns
-------
array
Notes
-----
- To get a speedy integration the function should use numpy ufunctions which operate on numpy arrays with
compiled code.
Examples
--------
The following integrals in 1-3 dimensions over a normalised Gaussian give always 1
which achieved with reasonable accuracy with n=15.
The examples show different ways to return 2dim arrays with x,y,z in first dimension and vector q in second.
`x[:,None]` adds a second dimension to array x.
::
import jscatter as js
import numpy as np
q=np.r_[0.1:5.1:0.1]
def gauss(x,mean,sigma):
return np.exp(-0.5 * (x - mean) ** 2 / sigma ** 2) / sigma / np.sqrt(2 * np.pi)
# 1dimensional
def gauss1(q,x,mean=0,sigma=1):
g=np.exp(-0.5 * (x[:,None] - mean) ** 2 / sigma ** 2) / sigma / np.sqrt(2 * np.pi)
return g*q
js.formel.pQFGxD(gauss1,0,100,parnames='x',mean=50,sigma=10,q=q,n=15)
# 2 dimensional
def gauss2(q,x,y=0,mean=0,sigma=1):
g=gauss(x[:,None],mean,sigma)*gauss(y[:,None],mean,sigma)
return g*q
js.formel.pQFGxD(gauss2,[0,0],[100,100],parnames=['x','y'],mean=50,sigma=10,q=q,n=15)
# 3 dimensional
def gauss3(q,x,y=0,z=0,mean=0,sigma=1):
g=gauss(x,mean,sigma)*gauss(y,mean,sigma)*gauss(z,mean,sigma)
return g[:,None]*q
js.formel.pQFGxD(gauss3,[0,0,0],[100,100,100],parnames=['x','y','z'],mean=50,sigma=10,q=q,n=15)
Usage of weights allows weights for dimensions e.g. to realise a spherical average with weight
:math:`sin(\theta)d\theta` in the integral
:math:`P(q) = \int_0^{2\pi}\int_0^{\pi} f(q,\theta,\phi) sin(\theta) d\theta d\phi`.
(Using the weight in the function is more accurate.)
The weight needs to be normalised by unit sphere area :math:`4\pi`.
::
import jscatter as js
import numpy as np
q=np.r_[0,0.1:5.1:0.1]
def cuboid(q, phi, theta, a, b, c):
pi2 = np.pi * 2
fa = (np.sinc(q * a * np.sin(theta[:,None]) * np.cos(phi[:,None]) / pi2) *
np.sinc(q * b * np.sin(theta[:,None]) * np.sin(phi[:,None]) / pi2) *
np.sinc(q * c * np.cos(theta[:,None]) / pi2))
return fa**2*(a*b*c)**2
# generate weight for sin(theta) dtheta integration (better to integrate in cuboid function)
# and normalise for unit sphere
t = np.r_[0:np.pi:180j]
wt = np.c_[t,np.sin(t)/np.pi/4].T
Fq=js.formel.pQFGxD(cuboid,[0,0],[2*np.pi,np.pi],parnames=['phi','theta'],weights1=wt,q=q,n=15,a=1.9,b=2,c=2)
# compare the result to the ff solution (which does the same with weights in the function).
p=js.grace()
p.plot(q,Fq)
p.plot(js.ff.cuboid(q,1.9,2,2),li=1,sy=0)
p.yaxis(scale='l')
References
----------
.. [1] https://en.wikipedia.org/wiki/Gaussian_quadrature
"""
v, w = _cached_roots_legendre(n) # value and weights
normfactor = []
parmean = []
if isinstance(parnames, str): parnames = [parnames]
if isinstance(lowlimit, numbers.Number): lowlimit = [lowlimit]
if isinstance(uplimit, numbers.Number): uplimit = [uplimit]
if len(parnames) == 0 or len(parnames) > 3:
raise AttributeError('Missing parnames or to many.')
if len(parnames) > 0:
if np.isinf(lowlimit[0]) or np.isinf(uplimit[0]):
raise ValueError("Gaussian quadrature is only available for finite limits.")
vol = (uplimit[0] - lowlimit[0]) / 2
x = (uplimit[0] - lowlimit[0]) * (v + 1) / 2.0 + lowlimit[0]
points = [[xx] for xx in x]
if weights0 is None:
wx = np.ones_like(x)
parmean.append((uplimit[0] + lowlimit[0]) / 2)
else:
wx = np.interp(x, weights0[0], weights0[1])
wx = wx
parmean.append(np.sum(x * w * wx))
weights = [w0[0]*w0[1] for w0 in zip(w, wx)]
if len(parnames) > 1:
vol = vol * (uplimit[1] - lowlimit[1]) / 2
y = (uplimit[1] - lowlimit[1]) * (v + 1) / 2.0 + lowlimit[1]
points = [[xx, yy] for xx in x for yy in y]
if weights1 is None:
wy = np.ones_like(y)
parmean.append( (uplimit[1] + lowlimit[1]) / 2)
else:
wy = np.interp(y, weights1[0], weights1[1])
wy = wy
parmean.append(np.sum(y * w * wy))
weights = [w0[0]*w0[1]*w1[0]*w1[1] for w0 in zip(w, wx) for w1 in zip(w, wy)]
if len(parnames) > 2:
vol = vol * (uplimit[2] - lowlimit[2]) / 2
z = (uplimit[2] - lowlimit[2]) * (v + 1) / 2.0 + lowlimit[2]
points = [[xx, yy, zz] for xx in x for yy in y for zz in z]
if weights2 is None:
wz = np.ones_like(z)
parmean.append( (uplimit[1] + lowlimit[1]) / 2)
else:
wz = np.interp(z, weights2[0], weights2[1])
wz = wz
parmean.append(np.sum(z * w * wz))
weights = [w0[0]*w0[1]*w1[0]*w1[1]*w2[0]*w2[1] for w0 in zip(w, wx) for w1 in zip(w, wy) for w2 in zip(w, wz)]
# calc values for all points
res = func(**dict(kwargs, **dict(zip(parnames, np.array(points).T))))
# do the integration by summing with weights
return vol * np.sum(weights * np.atleast_2d(res).T, axis=-1)
# noinspection PyIncorrectDocstring
[docs]
def parQuadratureAdaptiveGauss(func, lowlimit, uplimit, parname, weights=None, tol=1.e-8, rtol=1.e-8, maxiter=150,
miniter=8, **kwargs):
"""
Vectorized definite integral using fixed-tolerance Gaussian quadrature. Shortcut pQAG.
parQuadratureAdaptiveClenshawCurtis is more efficient.
Adaptive integration of func from `a` to `b` using Gaussian quadrature adaptivly increasing number of points by 8.
All columns are integrated. For func return values as dataArray the .X is recovered (unscaled) while for array
also the X are integrated and weighted.
Parameters
----------
func : function
A function or method to integrate returning an array or dataArray.
lowlimit : float
Lower limit of integration.
uplimit : float
Upper limit of integration.
parname : string
name of the integration variable which should be a scalar.
weights : ndarray shape(2,N),default=None
- Weights for integration along parname as a Gaussian with lowlimit<weights[0]<uplimit
and weights[1] contains weight values.
- Missing values are linear interpolated (faster).
If None equal weights=1 are used.
kwargs : dict, optional
Extra keyword arguments to pass to function, if any.
tol, rtol : float, optional
Iteration stops when error between last two iterates is less than
`tol` OR the relative change is less than `rtol`.
maxiter : int, default 150, optional
Maximum order of Gaussian quadrature.
miniter : int, default 8, optional
Minimum order of Gaussian quadrature.
ncpu : int, default=1, optional
Number of cpus in the pool.
Set this to 1 if the integrated function uses multiprocessing to avoid errors.
- 0 -> all cpus are used
- int>0 min (ncpu, mp.cpu_count)
- int<0 ncpu not to use
Returns
-------
val : float
Gaussian quadrature approximation (within tolerance) to integral for all vector elements.
err : float
Difference between last two estimates of the integral.
Examples
--------
A simple polydispersity: integrate size distribution of equal weight. Normalisation by 4sig.
We see that Rg and I0 at low Q also change because of polydispersity. Minima are smeared out.
::
import jscatter as js
q=js.loglist(0.01,5,500)
p=js.grace()
mean=5
for sig in [0.01,0.1,0.3,0.4]: # distribution width
sp2=js.formel.pQAG(js.ff.sphere,mean-2*sig,mean+2*sig,'radius',q=q,radius=mean)
p.plot(sp2.X, sp2.Y/(4*sig),sy=[-1,0.2,-1])
p.yaxis(scale='l')
Integrate Gaussian as test case.
As we integrate over .X the final .X will be the first integration point .X, here the first Legendre knot.
::
t=np.r_[1:100]
gg=js.formel.gauss(t,50,10)
js.formel.pQAG(js.formel.gauss,0,100,'x',mean=50,sigma=10)
Notes
-----
Reimplementation of scipy.integrate.quadrature.quadrature to work with vector output of the integrand function.
References
----------
.. [1] https://en.wikipedia.org/wiki/Gaussian_quadrature
"""
val = np.inf
err = np.inf
maxiter = max(miniter + 1, maxiter)
for n in np.arange(maxiter, miniter, -8)[::-1]:
result = parQuadratureFixedGauss(func, lowlimit, uplimit, parname, n, weights, **kwargs)
if isinstance(result, dA):
newval = result.Y
else:
newval = result
err = abs(newval - val)
val = newval
if np.all(err < tol) or np.all(err < rtol * abs(val)):
break
else:
warnings.warn("maxiter (%d) exceeded in %s. Latest maximum abs. error %e and rel error = %e"
% (maxiter, _getFuncCode(func).co_name, err.flatten().max(), np.max(abs(err) / abs(val))),
AccuracyWarning)
if isinstance(result, dA):
result.IntegralErr_funktEval = err, n
return result
def _wrappedIntegrand(xarray, *args, **kwargs):
func = kwargs.pop('func')
parnames = kwargs.pop('parnames')
test = kwargs.pop('_test_', False)
kwargs.update({key: xarray[:, i] for i, key in enumerate(parnames)})
result = func(*args, **kwargs)
if test:
return np.iscomplexobj(result), result.shape
if np.iscomplexobj(result):
# split complex to 2xfloat
res = np.zeros((result.shape[0], 2*result.shape[1])) # prepare for consecutive real,img parts in second dim
# redistribute
res[:, ::2] = result.real # res2r[::2, :]
res[:, 1::2] = result.imag # res2r[1::2, :]
return res
else:
return result
[docs]
def parAdaptiveCubature(func, lowlimit, uplimit, parnames, fdim=None, adaptive='p',
abserr=1e-8, relerr=1e-3, *args, **kwargs):
r"""
Vectorized adaptive multidimensional integration (cubature). Shortcut pAC.
We use the cubature module written by SG Johnson [2]_ for *h-adaptive* (recursively partitioning the
integration domain into smaller subdomains) and *p-adaptive* (Clenshaw-Curtis quadrature,
repeatedly doubling the degree of the quadrature rules).
This function is a wrapper around the package cubature which can be used also directly.
Parameters
----------
func : function
The function to integrate.
The return array needs to be an 2-dim array with the last dimension as vectorized return (=len(fdim))
and first along the points of the *parnames* parameters to integrate.
Use numpy functions for array functions to speedup computations. See example.
parnames : list of string
Parameter names of variables to integrate. Should be scalar.
lowlimit : list of float
Lower limits of the integration variables with same length as parnames.
uplimit: list of float
Upper limits of the integration variables with same length as parnames.
fdim : int, None, optional
Second dimension size of the func return array.
If None, the function is evaluated with the uplimit values to determine the size.
For complex valued function it is twice the complex array length.
adaptive : 'h', 'p', default='p'
Type of adaption algorithm.
- 'h' Multidimensional h-adaptive integration by subdividing the integration interval into smaller intervals
where the same rule is applied.
The value and error in each interval is calculated from 7-point rule and difference to 5-point rule.
For higher dimensions only the worst dimension is subdivided [3]_.
This algorithm is best suited for a moderate number of dimensions (say, < 7), and is superseded for
high-dimensional integrals by other methods (e.g. Monte Carlo variants or sparse grids).
- 'p' Multidimensional p-adaptive integration by increasing the degree of the quadrature rule according to
Clenshaw-Curtis quadrature
(in each iteration the number of points is doubled and the previous values are reused).
Clenshaw-Curtis has similar error compared to Gaussian quadrature even if the used error estimate is worse.
This algorithm is often superior to h-adaptive integration for smooth integrands in a few (≤ 3) dimensions,
abserr, relerr : float default = 1e-8, 1e-3
Absolute and relative error to stop.
The integration will terminate when either the relative OR the absolute error tolerances are met.
abserr=0, which means that it is ignored.
The real error is much smaller than this stop criterion.
maxEval : int, default 0, optional
Maximum number of function evaluations. 0 is infinite.
norm : int, default=None, optional
Norm to evaluate the error.
- None: 0,1 automatically choosen for real or complex functions.
- 0: individual for each integrand (real valued functions)
- 1: paired error (L2 distance) for complex values as distance in complex plane.
- Other values as mentioned in cubature documentation.
args,kwargs : optional
Additional arguments and keyword arguments passed to func.
Returns
-------
arrays values , error
Examples
--------
Integration of the sphere to get the sphere formfactor.
In the first example the symmetry is used to return real valued amplitude.
In the second the complex amplitude is used.
Both can be compared to the analytic formfactor. Errors are much smaller than the abserr/relerr stop criterion.
The stop seems to be related to the minimal point at q=2.8 as critical point.
h-adaptive is for dim=3 less accurate and slower than p-adaptive.
The integrands contain patterns of scheme ``q[:,None]*theta``
(with later .T to transpose, alternative ``q*theta[:,None]``)
to result in a 2-dim array with the last dimension as vectorized return.
The first dimension goes along the points to evaluate as determined from the algorithm.
::
import jscatter as js
import numpy as np
R=5
q = np.r_[0.01:3:0.02]
def sphere_real(r, theta, phi, b, q):
res = b*np.cos(q[:,None]*r*np.cos(theta))*r**2*np.sin(theta)*2
return res.T
pn = ['r','theta','phi']
fa_r,err = js.formel.pAC(sphere_real, [0,0,0], [R,np.pi/2,np.pi*2], pn, b=1, q=q)
fa_rh,errh = js.formel.pAC(sphere_real, [0,0,0], [R,np.pi/2,np.pi*2], pn, b=1, q=q,adaptive='h')
# As complex function
def sphere_complex(r, theta, phi, b, q):
fac = b * np.exp(1j * q[:, None] * r * np.cos(theta)) * r ** 2 * np.sin(theta)
return fac.T
fa_c, err = js.formel.pAC(sphere_complex, [0, 0, 0], [R, np.pi, np.pi * 2], pn, b=1, q=q)
sp = js.ff.sphere(q, R)
p = js.grace()
p.multi(2,1,vgap=0)
# integrals
p[0].plot(q, fa_r ** 2, le='real integrand p-adaptive')
p[0].plot(q, fa_rh ** 2, le='real integrand h-adaptive')
p[0].plot(q, np.real(fa_c * np.conj(fa_c)),sy=[8,0.5,3], le='complex integrand')
p[0].plot(q, sp.Y, li=1, sy=0, le='analytic')
# errors
p[1].plot(q,np.abs(fa_r**2 -sp.Y), le='real integrand')
p[1].plot(q,np.abs(fa_rh**2 -sp.Y), le='real integrand h-adaptive')
p[1].plot(q,np.abs(np.real(fa_c * np.conj(fa_c)) -sp.Y),sy=[8,0.5,3],le='complex p-adaptive')
p[0].yaxis(scale='l',label='F(q)',ticklabel=['power',0])
p[0].xaxis(ticklabel=0)
p[0].legend(x=2,y=1e6)
p[1].legend(x=2.1,y=5e-9,charsize=0.8)
p[1].yaxis(scale='l',label=r'error', ticklabel=['power',0],min=1e-13,max=5e-6)
p[1].xaxis(label=r'q / nm\S-1')
p[1].text(r'error = abs(F(Q) - F(q)\sanalytic\N)',x=0.8,y=1e-9,charsize=1)
p[0].title('Numerical quadrature sphere formfactor ')
p[0].subtitle('stop criterion relerror=1e-3, real errors are smaller')
#p.save(js.examples.imagepath+'/cubature.jpg')
.. image:: ../../examples/images/cubature.jpg
:width: 50 %
:align: center
:alt: sphere ff cubature
Notes
-----
- The here used module jscatter.libs.cubature is an adaption of the Python interface of S.G.P. Castro [1]_
(vers. 0.14.5) to access the C-module of S.G. Johnson [2]_ (vers. 1.0.3).
Only the vectorized form is realized here. The advantage here are fewer dependencies during install.
Check the original packages for detailed documentation or look in jscatter.libs.cubature
how to use it for your own things.
- Internal: For complex valued functions the complex has to be split in real and imaginary to pass to the
integration and later the result has to be converted to complex again.
This is done automatically dependent on the return value of the function.
For the example the real valued function is about 9 times faster
References
----------
.. [1] https://github.com/saullocastro/cubature
.. [2] https://github.com/stevengj/cubature
.. [3] An adaptive algorithm for numeric integration over an N-dimensional rectangular region
A. C. Genz and A. A. Malik,
J. Comput. Appl. Math. 6 (4), 295–302 (1980).
.. [4] https://en.wikipedia.org/wiki/Clenshaw-Curtis_quadrature
"""
# default values
norm = kwargs.pop('norm', None)
maxEval = kwargs.pop('maxEval', 0)
kwargs.update(func=func, parnames=parnames)
# test for typ and shape of func result using the uplimit values
iscomplex, resultshape = _wrappedIntegrand(np.r_[uplimit][None, :], _test_=True, *args, **kwargs)
if norm is None:
if iscomplex:
norm = 1
else:
norm = 0
if fdim is None:
if iscomplex:
fdim = 2*resultshape[1]
else:
fdim = resultshape[1]
val, err = cubature(func=_wrappedIntegrand, ndim=len(parnames), fdim=fdim, vectorized=True,
abserr=abserr, relerr=relerr, norm=norm, maxEval=maxEval, adaptive=adaptive,
xmin=lowlimit, xmax=uplimit, args=args, kwargs=kwargs)
if iscomplex:
return val.view(complex), np.abs(err.view(complex))
else:
return val, err
def _CCKnotsWeights(n):
"""
Clenshaw Curtis quadrature nodes in interval x=[-1,1] and corresponding weights w
uses cache dict to store calculated x,w
Returns : knots x, weights w
To calc integral : sum(w * f(x)) *(xmax-xmin)
"""
if n < 2:
# x,w central role
return 0, 2
elif n in _CCKnotsWeights.cache:
return _CCKnotsWeights.cache[n]
else:
# assume n is even
N = n + 1
c = np.zeros((N, 2))
k = np.r_[2.:n + 1:2]
c[::2, 0] = 2 / np.hstack((1, 1 - k * k))
c[1, 1] = -n
v = np.vstack((c, np.flipud(c[1:n, :])))
f = np.real(np.fft.ifft(v, axis=0))
x = f[0:N, 1]
w = np.hstack((f[0, 0], 2 * f[1:n, 0], f[n, 0]))
_CCKnotsWeights.cache[n] = (x, w)
return _CCKnotsWeights.cache[n]
_CCKnotsWeights.cache = dict()
[docs]
def parQuadratureAdaptiveClenshawCurtis(func, lowlimit, uplimit, parnames,
weights0=None, weights1=None, weights2=None, rtol=1.e-6, tol=1.e-12,
maxiter=520, miniter=8, **kwargs):
r"""
Vectorized adaptive multidimensional Clenshaw-Curtis quadrature for 1-3 dimensions. Shortcut pQACC.
Clenshaw-Curtis is superior to adaptive Gauss as for increased order the already calculated function
values are reused. Convergence is similar to adaptive Gauss.
In the cuboid example CC is about 3 times faster.
Parameters
----------
func : function
A function or method to integrate.
The return array needs to be a 2-dim array with the last dimension as vectorized return
and first along the points of the *parnames* to integrate.
Use numpy functions for array functions to speedup computations.
See example.
lowlimit : list of float
Lower limits of integration.
uplimit : list of float
Upper limits of integration.
parnames : list of strings
Names of the integration variables which should be scalar.
weights0,weights1,weights2 : ndarray shape(2,N),default=None
- Weights for integration along parname as a e.g. Gaussian distribution
with a<weights[0]<b and weights[1] contains weight values.
- Missing values are linear interpolated (faster).
If None equal weights=1 are used.
kwargs : dict, optional
Extra keyword arguments to pass to function, if any.
tol, rtol : float, optional
Iteration stops when (average) error between last two iterates is less than
`tol` OR the relative change is less than `rtol`.
maxiter : int, default 520, optional
Maximum order of quadrature.
Remember that the array of function values is of size iter**dim .
miniter : int, default 8, optional
Minimum order of quadrature.
Returns
-------
arrays values, error
Notes
-----
- Convergence of Clenshaw Curtis is about the same as Gauss-Legendre [1]_,[2]_.
- The iterative procedure reuses the previous calculated function values corresponding to F(n//2).
Therefore eCC is faster
- Error estimates are based on the difference between F(n) and F(n//2)
which is on the order of other more sophisticated estimates [2]_.
- Curse of dimension: The error for d-dim integrals is of order :math:`O(N^{-r/d})`
if the 1-dim integration method is :math:`O(N^{-r})` with N as number of evaluation points in d-dim space.
For Clenshaw-Curtis r is about 3 [2]_.
- For higher dimensions used Monte-Carlo Methods (e.g. with pseudo random numbers).
Examples
--------
The cuboid formfactor includes an orientational average over the unit sphere which is done by
integration over angles phi and theta which are our scalar integration variables.
The array of q values are our vector input as we want to integrate for all q.
The integrand `cuboid` contains patterns of scheme ``q*theta[:,None]`` to result in a 2-dim array with the last
dimension as vectorized return.
The first dimension goes along the points to evaluate as determined from the algorithm.
::
import jscatter as js
import numpy as np
pQACC = js.formel.pQACC # shortcut
pQFGxD = js.formel.pQFGxD
def cuboid(q, phi, theta, a, b, c):
# integrand
# scattering for orientations phi, theta as 1 dim arrays from 2dim integration
# q is array for vectorized integration in last dimension
# basically scheme as q*theta[:,None] results in array output of correct shape
pi2 = np.pi * 2
fa = (np.sinc(q * a * np.sin(theta[:,None]) * np.cos(phi[:,None]) / pi2) *
np.sinc(q * b * np.sin(theta[:,None]) * np.sin(phi[:,None]) / pi2) *
np.sinc(q * c * np.cos(theta[:,None]) / pi2))
# add volume, sin(theta) weight of integration, normalise for unit sphere
return fa**2*(a*b*c)**2*np.sin(theta[:,None])/np.pi/4
q=np.r_[0,0.1:11.1:0.1]
NN=20
a,b,c = 2,2,2
# quadrature: use one quadrant and multiply later by 8
FqCC,err = pQACC(cuboid,[0,0],[np.pi/2,np.pi/2],parnames=['phi','theta'],q=q,a=a,b=b,c=c)
FqGL=js.formel.pQFGxD(cuboid,[0,0],[np.pi/2,np.pi/2],parnames=['phi','theta'],q=q,a=c,b=b,c=c,n=NN)
p=js.grace()
p.multi(2,1)
p[0].title('Comparison adaptive Gauss and Clenshaw-Curtis integration',size=1.2)
p[0].subtitle('Cuboid formfactor integrated over unit sphere')
p[0].plot(q,FqCC*8,sy=1,le='CC')
p[0].plot(q,FqGL*8,sy=0,li=[1,2,4],le='GL')
p[1].plot(q,err,li=[1,2,2],sy=0,le='error estimate CC')
p[1].plot(q,np.abs(FqCC*8-FqGL*8),li=[3,2,4],sy=0,le='difference')
p[0].xaxis(label=r'', min=0, max=15,)
p[1].xaxis(label=r'q / nm\S-1', min=0, max=15)
p[0].yaxis(label='I(q)',scale='log',ticklabel='power')
p[1].yaxis(label='I(q)', scale='log',ticklabel='power', min=1e-16, max=1e-6)
p[1].legend(y=1e-12,x=8)
p[0].legend(y=1,x=12)
#p.save(js.examples.imagepath+'/Clenshaw-Curtis.jpg')
.. image:: ../../examples/images/Clenshaw-Curtis.jpg
:align: center
:width: 50 %
:alt: Clenshaw-Curtis
References
----------
.. [1] https://en.wikipedia.org/wiki/Clenshaw-Curtis_quadrature
.. [2] Error estimation in the Clenshaw-Curtis quadrature formula
H. O'Hara and Francis J. Smith
The Computer Journal, 11, 213–219 (1968), https://doi.org/10.1093/comjnl/11.2.213
.. [3] Monte Carlo theory, methods and examples, chapter 7 Other quadrature methods
Art B. Owen, 2019
https://statweb.stanford.edu/~owen/mc/
"""
if isinstance(parnames, str): parnames = [parnames]
if isinstance(lowlimit, numbers.Number): lowlimit = [lowlimit]
if isinstance(uplimit, numbers.Number): uplimit = [uplimit]
lenp = len(parnames)
if np.any(np.isinf(lowlimit)) or np.any(np.isinf(uplimit)):
raise ValueError("Clenshaw-Curtis quadrature is only available for finite limits.")
miniter = miniter + miniter % 2 # make it even returning n+1 points
maxiter = max(miniter + 1, maxiter)
vol = np.prod([(u - l) / 2 for u, l in zip(uplimit, lowlimit)])
def xw(n):
v, w = _CCKnotsWeights(n)
# get real xyz values from outside scope
xyz = [(u - l) * (v + 1) / 2.0 + l for u, l, p in zip(uplimit, lowlimit, parnames)]
# weightsi*w if present, same length as parnames
wxyz = [np.interp(x, ww[0], ww[1]) * w if ww is not None else w
for ww, x in zip([weights0, weights1, weights2], xyz)]
# points and weights as ndim ndarray for easy indexing
points = np.array(list(itertools.product(*xyz))).reshape(tuple([n + 1] * lenp + [-1]))
weights = np.prod(list(itertools.product(*wxyz)), axis=1).reshape(tuple([n + 1] * lenp))
return points, weights
n = miniter
fx = None
while True:
# calc points, weights for the result (n+1 points rule) and weights for error estimate (n//2+1 points rule)
points, weights = xw(n)
# calc values for all points reshape to matrix
if fx is None:
# first iteration
pointslist = points.reshape((-1, lenp))
fx = func(**dict(kwargs, **dict(zip(parnames, pointslist.T)))).reshape(tuple([n + 1] * lenp + [-1]))
# we need the (n//2+1 points rule) to calc previous step result for error determination
sel = (np.indices([n + 1] * lenp) % 2 == 0).prod(axis=0) == 1
_, weightsh = xw(n // 2)
prevresult = vol * np.sum(weightsh[..., None] * fx[sel].reshape((n // 2 + 1,) * lenp + (-1,)),
axis=tuple(range(lenp)))
else:
prevvalues = fx.copy()
prevresult = res.copy() # save prevresult not to calc it twice
fx = np.zeros((n + 1,) * lenp + (prevvalues.shape[-1],))
# select (odd,odd) indices to assign previous step values
sel = (np.indices([n + 1] * lenp) % 2 == 0).prod(axis=0) == 1
fx[sel] = prevvalues.reshape((-1, prevvalues.shape[-1]))
# calc new values and assign to missing
pointslist = points[~sel]
values = func(**dict(kwargs, **dict(zip(parnames, pointslist.T))))
fx[~sel] = values
# result
res = vol * np.sum(weights[..., None] * fx, axis=tuple(range(lenp)))
error = np.abs(res - prevresult)
if (np.sum(error) < rtol * np.sum(np.abs(res))) or (np.sum(error) < tol) or (n > maxiter):
break
else:
n = n * 2
return res, error
[docs]
def parQuadratureSimpson(funktion, lowlimit, uplimit, parname, weights=None, tol=1e-6, rtol=1e-6, dX=None, **kwargs):
"""
Vectorized quadrature over one parameter with weights using the adaptive Simpson rule. Shortcut pQS.
Integrate by adaptive Simpson integration for all .X values at once.
Only .Y values are integrated and checked for tol criterion.
Attributes and non .Y columns correspond to the weighted mean of parname.
Parameters
----------
funktion : function
Function returning dataArray or array
lowlimit,uplimit : float
Interval borders to integrate
parname : string
Parname to integrate
weights : ndarray shape(2,N),default=None
- Weights for integration along parname as a Gaussian with a<weights[0]<b and weights[1] contains weight values.
- Missing values are linear interpolated (faster). If None equal weights are used.
tol,rtol : float, default=1e-6
| Relative error or absolute error to stop integration. Stop if one is full filled.
| Tol is divided for each new interval that the sum of tol is kept.
| .IntegralErr_funktEvaluations in dataArray contains error and number of points in interval.
dX : float, default=None
Minimal distance between integration points to determine a minimal step for integration variable.
kwargs :
Additional parameters to pass to funktion.
If parname is in kwargs it is overwritten.
Returns
-------
dataArray or array
dataArrays have additional parameters as error and weights.
Notes
-----
What is the meaning of tol in Simpson method?
If the error in an interval exceeds tol, the algorithm subdivides the interval
in two equal parts with each :math:`tol/2` and applies the method to each subinterval in a recursive manner.
The condition in interval i is :math:`error=|f(ai,mi)+f(mi,bi)-f(ai,bi)|/15 < tol`.
The recursion stops in an interval if the improvement is smaller than tol.
Thus tol is the upper estimate for the total error.
Here we use a absolute (tol) and relative (rtol) criterion:
:math:`|f(ai,mi)+f(mi,bi)-f(ai,bi)|/15 < rtol*fnew`
with :math:`fnew= ( f(ai,mi)+f(mi,bi) + [f(ai,mi)+f(mi,bi)-f(ai,bi)]/15 )` as the next improved value
As this is tested for all .X the **worst** case is better than tol, rtol.
The algorithm is efficient as it memoizes function evaluation at each interval border and reuses the result.
This reduces computing time by about a factor 3-4.
Different distribution can be found in scipy.stats. But any distribution given explicitly can be used.
E.g. triangular np.c_[[-1,0,1],[0,1,0]].T
Examples
--------
Integrate Gaussian as test case.
As we integrate over .X the final .X will be the first integration point .X, here the lowlimit.
::
import jscatter as js
import numpy as np
import scipy
# testcase: integrate over x of a function
# area under normalized gaussian is 1
js.formel.parQuadratureSimpson(js.formel.gauss,-10,10,'x',mean=0,sigma=1)
Integrate a function over one parameter with a weighting function.
If weight is 1 the result is a simple integration.
Here the weight corresponds to a normal distribution and the result is a weighted average as implemented in
parDistributedAverage using fixedGaussian quadrature.
::
# normal distribtion of parameter D with width ds
t=np.r_[0:150:0.5]
D=0.3
ds=0.1
diff=js.dynamic.simpleDiffusion(t=t,q=0.5,D=D)
distrib =scipy.stats.norm(loc=D,scale=ds)
x=np.r_[D-5*ds:D+5*ds:30j]
pdf=np.c_[x,distrib.pdf(x)].T
diff_g=js.formel.parQuadratureSimpson(js.dynamic.simpleDiffusion,-3*ds+D,3*ds+D,parname='D',
weights=pdf,tol=0.01,q=0.5,t=t)
# compare it
p=js.grace()
p.plot(diff,le='monodisperse')
p.plot(diff_g,le='polydisperse')
p.xaxis(scale='l')
p.legend()
References
----------
.. [1] https://en.wikipedia.org/wiki/Adaptive_Simpson's_method
"""
# We have to deal with return values as arrays and dataArrays
if lowlimit == uplimit:
# return function with parname=a ; to be consistent
result = funktion(**dict(kwargs, **{parname: lowlimit}))
if isinstance(result, dA):
result.weightNormFactor = 1
return result
if lowlimit > uplimit:
lowlimit, uplimit = uplimit, lowlimit
def _memoize(f):
"""
avoid multiple calculations of same values at borders in each interation
saves factor 3-4 in time
"""
f.memo = {}
def _helper(x):
if x not in f.memo:
# this overwrites the kwargs[parname] with x
Y = f(**dict(kwargs, **{parname: x}))
if isinstance(Y, dA): # calc the function value
f.memo[x] = Y.Y
else:
f.memo[x] = Y
if weights is not None: # weight of value
f.memo[x] *= np.interp(x, weights[0], weights[1])
return f.memo[x]
return _helper
stack = [[lowlimit, uplimit, tol]]
if dX is None: dX = 2 * (uplimit - lowlimit)
funkt = _memoize(funktion)
Integral = 0
Err = 0
nn = 0
# do adaptive integration
while stack: # is not empty
[x1, x2, err] = stack.pop()
m = (x1 + x2) / 2.
I1 = (funkt(x1) + 4 * funkt(m) + funkt(x2)) * (x2 - x1) / 6. # Simpson rule.
mleft = (x1 + m) / 2.
Ileft = (funkt(x1) + 4 * funkt(mleft) + funkt(m)) * (m - x1) / 6. # Simpson rule.
mright = (m + x2) / 2.
Iright = (funkt(m) + 4 * funkt(mright) + funkt(x2)) * (x2 - m) / 6. # Simpson rule.
# does the new point improve better than interval err on relative scale
if (np.all(np.abs(Ileft + Iright - I1) < 15 * rtol * (Ileft + Iright + (Ileft + Iright - I1) / 15.)) or
np.all(np.abs((Ileft + Iright - I1)) < 15 * err)) and \
(x2 - x1) < dX:
# good enough in this interval
Integral += (Ileft + Iright + (Ileft + Iright - I1) / 15.)
Err += abs((Ileft + Iright - I1) / 15.)
nn += 1
else:
# split interval to improve with new points
stack.append([x1, m, err / 2])
stack.append([m, x2, err / 2])
# calc final result with normalized weights
if weights is not None:
normfactor = np.trapz(weights[1], weights[0])
parmean = np.trapz(weights[1] * weights[0], weights[0]) / normfactor
else:
normfactor = uplimit - lowlimit
parmean = (lowlimit + uplimit) / 2
result = funktion(**dict(kwargs, **{parname: parmean}))
if not isinstance(result, dA):
return Integral
result.Y = Integral
result.IntegralErr_funktEvaluations = max(Err), nn
result.weightNormFactor = normfactor
return result
[docs]
def simpleQuadratureSimpson(funktion, lowlimit, uplimit, parname, weights=None, tol=1e-6, rtol=1e-6, **kwargs):
"""
Integrate a scalar function over one of its parameters with weights using the adaptive Simpson rule.
Shortcut sQS.
Integrate by adaptive Simpson integration for scalar function.
Parameters
----------
funktion : function
function to integrate
lowlimit,uplimit : float
interval to integrate
parname : string
parname to integrate
weights : ndarray shape(2,N),default=None
- Weights for integration along parname as a Gaussian with a<weights[0]<b and weights[1] contains weight values.
- Missing values are linear interpolated (faster). If None equal weights are used.
tol,rtol : float, default=1e-6
| Relative error for intervals or absolute integral error to stop integration.
kwargs :
additional parameters to pass to funktion
if parname is in kwargs it is overwritten
Returns
-------
float
Notes
-----
What is the meaning of tol in Simpson method?
See parQuadratureSimpson.
Examples
--------
::
distrib =scipy.stats.norm(loc=1,scale=0.2)
x=np.linspace(0,1,1000)
pdf=np.c_[x,distrib.pdf(x)].T
# define function
f1=lambda x,p1,p2,p3:js.dA(np.c_[x,x*p1+x*x*p2+p3].T)
# calc the weighted integral
result=js.formel.parQuadratureSimpson(f1,0,1,parname='p2',weights=pdf,tol=0.01,p1=1,p3=1e-2,x=x)
# something simple should be 1
js.formel.simpleQuadratureSimpson(js.formel.gauss,-10,10,'x',mean=0,sigma=1)
References
----------
.. [1] https://en.wikipedia.org/wiki/Adaptive_Simpson's_method
"""
if lowlimit == uplimit:
# return function with parname=a ; to be consistent
result = funktion(**dict(kwargs, **{parname: lowlimit}))
return result
if lowlimit > uplimit:
lowlimit, uplimit = uplimit, lowlimit
def _memoize(f):
"""
avoid multiple calculations of same values at borders in each interation
saves factor 3-4 in time
"""
f.memo = {}
def _helper(x):
if x not in f.memo:
# this overwrites the kwargs[parname] with x
Y = f(**dict(kwargs, **{parname: x}))
if isinstance(Y, dA): Y = Y.Y
f.memo[x] = Y
if weights is not None:
f.memo[x] *= np.interp(x, weights[0], weights[1])
return f.memo[x]
return _helper
stack = [[lowlimit, uplimit, tol]]
funkt = _memoize(funktion)
Integral = 0
Err = 0
# do adaptive integration
while stack: # is not empty
[x1, x2, err] = stack.pop()
m = (x1 + x2) / 2.
I1 = (funkt(x1) + 4 * funkt(m) + funkt(x2)) * (x2 - x1) / 6. # Simpson rule.
mleft = (x1 + m) / 2.
Ileft = (funkt(x1) + 4 * funkt(mleft) + funkt(m)) * (m - x1) / 6. # Simpson rule.
mright = (m + x2) / 2.
Iright = (funkt(m) + 4 * funkt(mright) + funkt(x2)) * (x2 - m) / 6. # Simpson rule.
# does the new point improve better than interval err on relative scale
if np.all(np.abs(Ileft + Iright - I1) < 15 * rtol * (Ileft + Iright + (Ileft + Iright - I1) / 15.)) or \
np.all(np.abs((Ileft + Iright - I1)) < 15 * err):
# good enough in this interval
Integral += (Ileft + Iright + (Ileft + Iright - I1) / 15.)
Err += abs((Ileft + Iright - I1) / 15.)
else:
# split interval to improve with new points
stack.append([x1, m, err / 2])
stack.append([m, x2, err / 2])
return Integral
# noinspection PyIncorrectDocstring
[docs]
def parDistributedAverage(funktion, sig, parname, type='norm', nGauss=30, **kwargs):
"""
Vectorized average assuming a single parameter is distributed with width sig. Shortcut pDA.
Function average over a parameter with weights determined from probability distribution.
Gaussian quadrature over given distribution or summation with weights is used.
All columns are integrated except .X for dataArray.
Parameters
----------
funktion : function
Function to integrate with distribution weight.
Function needs to return dataArray. All columns except .X are integrated.
sig : float
Standard deviation from mean (root of variance) or location describing the width the distribution, see Notes.
parname : string
Name of the parameter of funktion which shows a distribution.
type : 'normal','lognorm','gamma','lorentz','uniform','poisson','schulz','duniform','truncnorm' default 'norm'
Type of the distribution
kwargs : parameters
Any additional keyword parameter to pass to function or distribution.
The value of parname will be the mean value of the distribution.
nGauss : float , default=30
Order of quadrature integration as number of intervals in Gauss–Legendre quadrature over distribution.
Distribution is integrated in probability interval [0.001..0.999].
ncpu : int, optional
Number of cpus in the pool.
Set this to 1 if the integrated function uses multiprocessing to avoid errors.
- 0 -> all cpus are used
- int>0 min (ncpu, mp.cpu_count)
- int<0 ncpu not to use
Returns
-------
out : dataArray
as returned from function with
- .parname_mean : mean of parname
- .parname_std : standard deviation of parname (root of variance, input parameter)
- .pdf : probability distribution within some range.
Notes
-----
The used distributions are from scipy.stats. Choose the distribution according to the problem.
- *mean* is the value in kwargs[parname]. *mean* is the expectation value of the distributed variable.
- *sig²* is the variance as the expectation of the squared deviation from the *mean*.
Distributions may be parametrized differently with additional parameters :
* norm :
| mean , sig
| stats.norm(loc=mean,scale=sig)
| here sig scales the position
* truncnorm
| mean , sig; a,b lower and upper cutoffs
| a,b are in scale of sig around mean;
| absolute cutoff c,d change to a, b = (c-mean)/sig, (d-mean)/sig
| stats.norm(a,b,loc=mean,scale=sig)
* lognorm :
| mean and sig evaluate to (see https://en.wikipedia.org/wiki/Log-normal_distribution)
| a = 1 + (sig / mean) ** 2
| s = np.sqrt(np.log(a))
| scale = mean / np.sqrt(a)
| stats.lognorm(s=s,scale=scale)
* gamma :
| mean and sig
| stats.gamma(a=mean**2/sig**2,scale=sig**2/mean)
| Same as SchulzZimm
* lorentz = cauchy :
| mean and sig are not defined. Use FWHM instead to describe width.
| sig=FWHM
| stats.cauchy(loc=mean,scale=sig))
* uniform :
| Continuous distribution.
| sig is width
| stats.uniform(loc=mean-sig/2.,scale=sig))
* schulz
| Same as gamma
* poisson:
stats.poisson(mu=mean,loc=sig)
* duniform:
| Uniform distribution integer values.
| sig>1
| stats.randint(low=mean-sig, high=mean+sig)
For more distribution look into this source code and use it appropriate with scipy.stats.
Examples
--------
::
import jscatter as js
p=js.grace()
q=js.loglist(0.1,5,500)
sp=js.ff.sphere(q=q,radius=5)
p.plot(sp,sy=[1,0.2],legend='single radius')
p.yaxis(scale='l',label='I(Q)')
p.xaxis(scale='n',label='Q / nm')
sig=0.2
p.title('radius distribution with width %.g' %(sig))
sp2=js.formel.pDA(js.ff.sphere,sig,'radius',type='norm',q=q,radius=5,nGauss=100)
p.plot(sp2,li=[1,2,2],sy=0,legend='normal 100 points Gauss ')
sp4=js.formel.pDA(js.ff.sphere,sig,'radius',type='normal',q=q,radius=5,nGauss=30)
p.plot(sp4,li=[1,2,3],sy=0,legend='normal 30 points Gauss ')
sp5=js.formel.pDA(js.ff.sphere,sig,'radius',type='normal',q=q,radius=5,nGauss=5)
p.plot(sp5,li=[1,2,5],sy=0,legend='normal 5 points Gauss ')
sp3=js.formel.pDA(js.ff.sphere,sig,'radius',type='lognorm',q=q,radius=5)
p.plot(sp3,li=[3,2,4],sy=0,legend='lognorm')
sp6=js.formel.pDA(js.ff.sphere,sig,'radius',type='gamma',q=q,radius=5)
p.plot(sp6,li=[2,2,6],sy=0,legend='gamma ')
sp9=js.formel.pDA(js.ff.sphere,sig,'radius',type='schulz',q=q,radius=5)
p.plot(sp9,li=[3,2,2],sy=0,legend='SchulzZimm ')
# an unrealistic example
sp7=js.formel.pDA(js.ff.sphere,1,'radius',type='poisson',q=q,radius=5)
p.plot(sp7,li=[1,2,6],sy=0,legend='poisson ')
sp8=js.formel.pDA(js.ff.sphere,1,'radius',type='duniform',q=q,radius=5)
p.plot(sp8,li=[1,2,6],sy=0,legend='duniform ')
p.legend()
"""
npoints = 1000
limit = 0.001
mean = kwargs[parname]
# define the distribution with parameters
if type == 'poisson':
distrib = stats.poisson(mu=mean, loc=sig)
elif type == 'duniform':
sigm = max(sig, 1)
distrib = stats.randint(low=mean - sigm, high=mean + sigm)
elif type == 'lognorm':
a = 1 + (sig / mean) ** 2
s = np.sqrt(np.log(a))
scale = mean / np.sqrt(a)
distrib = stats.lognorm(s=s, scale=scale)
elif type == 'gamma' or type == 'schulz':
distrib = stats.gamma(a=mean ** 2 / sig ** 2, scale=sig ** 2 / mean)
elif type == 'lorentz' or type == 'cauchy':
distrib = stats.cauchy(loc=mean, scale=sig)
elif type == 'uniform':
distrib = stats.uniform(loc=mean - sig / 2., scale=sig)
elif type == 'truncnorm':
aa = kwargs.pop('a', None)
bb = kwargs.pop('b', None)
distrib = stats.truncnorm(a=aa, b=bb, loc=mean, scale=sig)
else: # type=='norm' default
distrib = stats.norm(loc=mean, scale=sig)
# get starting and end values for integration
a = distrib.ppf(limit)
b = distrib.ppf(1 - limit)
if type in ['poisson', 'duniform']:
# discrete distributions
x = np.r_[int(a):int(b + 1)]
w = distrib.pmf(x)
pdf = np.c_[x, w].T
result = [funktion(**dict(kwargs, **{parname: xi})) for xi in x]
if isinstance(result[0], dA):
result[0][:, :] = np.sum([result[i] * wi for i, wi in enumerate(w)], axis=0) / w.sum()
result[0].X = result[1].X
else:
result[0] = np.sum([result[i] * wi for i, wi in enumerate(w)], axis=0) / w.sum()
result = result[0]
else:
if type == 'lognorm':
x = np.geomspace(a, b, npoints)
else:
x = np.linspace(a, b, npoints)
pdf = np.c_[x, distrib.pdf(x)].T
# calc the weighted integral using fixedGauss
result = parQuadratureFixedGauss(funktion, a, b, parname=parname, n=nGauss, weights=pdf, **kwargs)
normfactor = np.trapz(pdf[1], pdf[0])
if isinstance(result, dA):
result.Y = result.Y /normfactor
else:
result = result /normfactor
if isinstance(result, dA):
try:
delattr(result, parname)
except AttributeError:
pass
# calc mean and std and store in result
setattr(result, parname + '_mean', distrib.mean())
setattr(result, parname + '_std', distrib.std())
setattr(result, 'pdf', pdf)
if type == 'lorentz' or type == 'cauchy':
setattr(result, parname + '_FWHM', 2 * sig)
return result
# noinspection PyIncorrectDocstring
[docs]
def multiParDistributedAverage(funktion, sigs, parnames, types='normal', N=30, ncpu=1, **kwargs):
r"""
Vectorized average assuming multiple parameters are distributed in intervals. Shortcut mPDA.
Function average over multiple distributed parameters with weights determined from probability distribution.
The probabilities for the parameters are multiplied as weights and a weighted sum is calculated
by Monte-Carlo integration.
Parameters
----------
funktion : function
Function to integrate with distribution weight.
sigs : list of float
List of widths for parameters.
Sigs are the standard deviation from mean (or root of variance), see Notes.
parnames : string
List of names of the parameters which show a distribution.
types : list of 'normal', 'lognorm', 'gamma', 'lorentz', 'uniform', 'poisson', 'schulz', 'duniform', default 'normal'
List of types of the distributions.
If types list is shorter than parnames the last is repeated.
kwargs : parameters
Any additonal kword parameter to pass to function.
The value of parnames that are distributed will be the mean value of the distribution.
N : float , default=30
Number of points over distribution ranges.
Distributions are integrated in probability intervals :math:`[e^{-4} \ldots 1-e^{-4}]`.
ncpu : int, default=1, optional
Number of cpus in the pool for parallel excecution.
Set this to 1 if the integrated function uses multiprocessing to avoid errors.
- 0 -> all cpus are used
- int>0 min (ncpu, mp.cpu_count)
- int<0 ncpu not to use
Returns
-------
dataArray
as returned from function with
- .parname_mean = mean of parname
- .parname_std = standard deviation of parname
Notes
-----
Calculation of an average over D multiple distributed parameters by conventional integration requires
:math:`N^D` function evaluations which is quite time consuming. Monte-Carlo integration at N points
with random combinations of parameters requires only N evaluations.
The given function of fixed parameters :math:`q_j` and polydisperse parameters :math:`p_i`
with width :math:`s_i` related to the indicated distribution (types) is integrated as
.. math:: f_{mean}(q_j,p_i,s_i) = \frac{\sum_h{f(q_j,x^h_i)\prod_i{w_i(x^h_i)}}}{\sum_h \prod_i w_i(x^h_i)}
Each parameter :math:`p_i` is distributed along values :math:`x^h_i` with probability :math:`w_i(x^h_i)`
describing the probability distribution with mean :math:`p_i` and sigma :math:`s_i`.
Intervals for a parameter :math:`p_i` are choosen to represent the distribution
in the interval :math:`[w_i(x^0_i) = e^{-4} \ldots \sum_h w_i(x^h_i) = 1-e^{-4}]`
The distributed values :math:`x^h_i` are determined as pseudorandom numbers of N points with dimension
len(i) for Monte-Carlo integration.
- For a single polydisperse parameter use parDistributedAverage.
- During fitting it has to be accounted for the information content of the experimental data.
As in the example below it might be better to use a single width for all parameters to reduce
the number of redundant parameters.
The used distributions are from scipy.stats.
Choose the distribution according to the problem and check needed number of points N.
*mean* is the value in kwargs[parname]. mean is the expectation value of the distributed variable
and *sig²* are the variance as the expectation of the squared deviation from the mean.
Distributions may be parametrized differently :
* norm :
| mean , std
| stats.norm(loc=mean,scale=sig)
* lognorm :
| mean and sig evaluate to mean and std
| mu=math.log(mean**2/(sig+mean**2)**0.5)
| nu=(math.log(sig/mean**2+1))**0.5
| stats.lognorm(s=nu,scale=math.exp(mu))
* gamma :
| mean and sig evaluate to mean and std
| stats.gamma(a=mean**2/sig**2,scale=sig**2/mean)
| Same as SchulzZimm
* lorentz = cauchy:
| mean and std are not defined. Use FWHM instead to describe width.
| sig=FWHM
| stats.cauchy(loc=mean,scale=sig))
* uniform :
| Continuous distribution.
| sig is width
| stats.uniform(loc=mean-sig/2.,scale=sig))
* poisson:
stats.poisson(mu=mean,loc=sig)
* schulz
| same as gamma
* duniform:
| Uniform distribution integer values.
| sig>1
| stats.randint(low=mean-sig, high=mean+sig)
For more distribution look into this source code and use it appropriate with scipy.stats.
Examples
--------
The example of a cuboid with independent polydispersity on all edges.
To use the function in fitting please encapsulate it in a model function hiding the list parameters.
::
import jscatter as js
type=['norm','schulz']
p=js.grace()
q=js.loglist(0.1,5,500)
sp=js.ff.cuboid(q=q,a=4,b=4.1,c=4.3)
p.plot(sp,sy=[1,0.2],legend='single cube')
p.yaxis(scale='l',label='I(Q)')
p.xaxis(scale='n',label='Q / nm')
def cub(q,a,b,c):
a = js.ff.cuboid(q=q,a=a,b=b,c=c)
return a
p.title('Cuboid with independent polydispersity on all 3 edges')
p.subtitle('Using Monte Carlo integration; 30 points are enough here!')
sp1=js.formel.mPDA(cub,sigs=[0.2,0.3,0.1],parnames=['a','b','c'],types=type,q=q,a=4,b=4.1,c=4.2,N=10)
p.plot(sp1,li=[1,2,2],sy=0,legend='normal 10 points')
sp2=js.formel.mPDA(cub,sigs=[0.2,0.3,0.1],parnames=['a','b','c'],types=type,q=q,a=4,b=4.1,c=4.2,N=30)
p.plot(sp2,li=[1,2,3],sy=0,legend='normal 30 points')
sp3=js.formel.mPDA(cub,sigs=[0.2,0.3,0.1],parnames=['a','b','c'],types=type,q=q,a=4,b=4.1,c=4.2,N=90)
p.plot(sp3,li=[3,2,4],sy=0,legend='normal 100 points')
p.legend(x=2,y=1000)
# p.save(js.examples.imagepath+'/multiParDistributedAverage.jpg')
.. image:: ../../examples/images/multiParDistributedAverage.jpg
:align: center
:height: 300px
:alt: multiParDistributedAverage
During fitting encapsulation might be done like this ::
def polyCube(a,b,c,sig,N):
res = js.formel.mPDA(js.ff.cuboid,sigs=[sig,sig,sig],parnames=['a','b','c'],types='normal',q=q,a=a,b=b,c=c,N=N)
return res
"""
em4 = np.exp(-4)
# make lists
if isinstance(sigs, numbers.Number):
sigs = [sigs]
if isinstance(parnames, numbers.Number):
parnames = [parnames]
if isinstance(types, str):
types = [types]
dim = len(parnames)
if len(sigs) != len(parnames):
raise AttributeError('len of parnames and sigs is different!')
# extend missing types
types.extend(types[-1:] * dim)
# pseudorandom numbers in interval [0,1]
distribvalues = parallel.randomPointsInCube(N, 0, dim)
weights = np.zeros_like(distribvalues)
distribmeans = np.zeros(dim)
distribstds = np.zeros(dim)
# determine intervals and scale to it
for i, (parname, sig, type) in enumerate(zip(parnames, sigs, types)):
mean = kwargs[parname]
# define the distribution with parameters
if type == 'poisson':
distrib = stats.poisson(mu=mean, loc=sig)
elif type == 'duniform':
sigm = max(sig, 1)
distrib = stats.randint(low=mean - sigm, high=mean + sigm)
elif type == 'lognorm':
mu = math.log(mean ** 2 / (sig + mean ** 2) ** 0.5)
nu = (math.log(sig / mean ** 2 + 1)) ** 0.5
distrib = stats.lognorm(s=nu, scale=math.exp(mu))
elif type == 'gamma' or type == 'schulz':
distrib = stats.gamma(a=mean ** 2 / sig ** 2, scale=sig ** 2 / mean)
elif type == 'lorentz' or type == 'cauchy':
distrib = stats.cauchy(loc=mean, scale=sig)
elif type == 'uniform':
distrib = stats.uniform(loc=mean - sig / 2., scale=sig)
else: # type=='norm' default
distrib = stats.norm(loc=mean, scale=sig)
# get starting and end values for integration, then scale pseudorandom numbers to interval [0..1]
a = distrib.ppf(em4) # about 0.02
b = distrib.ppf(1 - em4)
distribvalues[:, i] = a + distribvalues[:, i] * (b - a)
try:
# continuous distributions
weights[:, i] = distrib.pdf(distribvalues[:, i])
except AttributeError:
# discrete distributions
weights[:, i] = distrib.pmf(distribvalues[:, i])
distribmeans[i] = distrib.mean()
distribstds[i] = distrib.std()
# prepare for pool
if ncpu < 0:
ncpu = max(mp.cpu_count() + ncpu, 1)
elif ncpu > 0:
ncpu = min(mp.cpu_count(), ncpu)
else:
ncpu = mp.cpu_count()
# calculate the values and calc weighted sum
if (ncpu == 1
or mp.current_process().name != 'MainProcess'
or 'spawn' not in mp.get_all_start_methods()):
result = [funktion(**dict(kwargs, **{p: d for p, d in zip(parnames, dv)})) for dv in distribvalues]
else:
with mp.get_context('spawn').Pool(ncpu) as pool:
jobs = [pool.apply_async(funktion, [], dict(kwargs, **{p: d for p, d in zip(parnames, dv)}))
for dv in distribvalues]
result = [job.get() for job in jobs]
w = weights.prod(axis=1)
if isinstance(result[0], dA):
result[0].Y = np.sum([result[i].Y * wi for i, wi in enumerate(w)], axis=0) / w.sum()
else:
result[0] = np.sum([result[i] * wi for i, wi in enumerate(w)], axis=0) / w.sum()
result = result[0]
if isinstance(result, dA):
# use mean and std and store in result
for parname, mean, std in zip(parnames, distribmeans, distribstds):
setattr(result, parname + '_mean', mean)
setattr(result, parname + '_std', std)
if type == 'lorentz' or type == 'cauchy':
setattr(result, parname + '_FWHM', 2 * sig)
return result
[docs]
def scatteringFromSizeDistribution(q, sizedistribution, size=None, func=None, weight=None, **kwargs):
r"""
Average function assuming one multimodal parameter like bimodal.
Distributions might be mixtures of small and large particles bi or multimodal.
For predefined distributions see formel.parDistributedAverage with examples.
The weighted average over given sizedistribution is calculated.
Parameters
----------
q : array of float;
Wavevectors to calculate scattering; unit = 1/unit(size distribution)
sizedistribution : dataArray or array
Explicit given distribution of sizes as [ [list size],[list probability]]
size : string
Name of the parameter describing the size (may be also something different than size).
func : lambda or function, default beaucage
Function that describes the form factor with first arguments (q,size,...)
and should return dataArray with .Y as result as e.g.func=js.ff.sphere.
kwargs :
Any additional keyword arguments passed to for func.
weight : function
Weight function dependent on size.
E.g. weight = lambda R:rho**2 * (4/3*np.pi*R**3)**2
with V= 4pi/3 R**3 for normalized form factors to account for
forward scattering of volume objects of dimension 3.
Returns
-------
dataArray
Columns [q,I(q)]
Notes
-----
We have to discriminate between formfactor normalized to 1 (e.g. beaucage) and
form factors returning the absolute scattering (e.g. sphere) including the contrast.
The later contains already :math:`\rho^2 V^2`, the first not.
We need for normalized formfactors P(q) :math:`I(q) = n \rho^2 V^2 P(q)` with :math:`n` as number density
:math:`\rho` as difference in average scattering length (contrast), V as volume of particle (~r³ ~ mass)
and use :math:`weight = \rho^2 V(R)^2`
.. math:: I(q)= \sum_{R_i} [ weight(R_i) * probability(R_i) * P(q, R_i , *kwargs).Y ]
For a gaussian chain with :math:`R_g^2=l^2 N^{2\nu}` and monomer number N (nearly 2D object)
we find :math:`N^2=(R_g/l)^{1/\nu}` and the forward scattering as weight :math:`I_0=b^2 N^2=b^2 (R_g/l)^{1/\nu}`
Examples
--------
The contribution of different simple sizes to Beaucage ::
import jscatter as js
q=js.loglist(0.01,6,100)
p=js.grace()
# bimodal with equal concentration
bimodal=[[12,70],[1,1]]
Iq=js.formel.scatteringFromSizeDistribution(q=q,sizedistribution=bimodal,
d=3,weight=lambda r:(r/12)**6,func=js.ff.beaucage)
p.plot(Iq,legend='bimodal 1:1 weight ~r\S6 ')
Iq=js.formel.scatteringFromSizeDistribution(q=q,sizedistribution=bimodal,d=3,func=js.ff.beaucage)
p.plot(Iq,legend='bimodal 1:1 weight equal')
# 2:1 concentration
bimodal=[[12,70],[1,5]]
Iq=js.formel.scatteringFromSizeDistribution(q=q,sizedistribution=bimodal,d=2.5,func=js.ff.beaucage)
p.plot(Iq,legend='bimodal 1:5 d=2.5')
p.yaxis(label='I(q)',scale='l')
p.xaxis(scale='l',label='q / nm\S-1')
p.title('Bimodal size distribution Beaucage particle')
p.legend(x=0.2,y=10000)
#p.save(js.examples.imagepath+'/scatteringFromSizeDistribution.jpg')
.. image:: ../../examples/images/scatteringFromSizeDistribution.jpg
:width: 50 %
:align: center
:alt: scatteringFromSizeDistribution
Three sphere sizes::
import jscatter as js
q=js.loglist(0.001,6,1000)
p=js.grace()
# trimodal with equal concentration
trimodal=[[10,50,500],[1,0.01,0.00001]]
Iq=js.formel.scatteringFromSizeDistribution(q=q,sizedistribution=trimodal,size='radius',func=js.ff.sphere)
p.plot(Iq,legend='with aggregates')
p.yaxis(label='I(q)',scale='l',max=1e13,min=1)
p.xaxis(scale='l',label='q / nm\S-1')
p.text(r'minimum \nlargest',x=0.002,y=1e10)
p.text(r'minimum \nmiddle',x=0.02,y=1e7)
p.text(r'minimum \nsmallest',x=0.1,y=1e5)
p.title('trimodal spheres')
p.subtitle('first minima indicated')
#p.save(js.examples.imagepath+'/scatteringFromSizeDistributiontrimodal.jpg')
.. image:: ../../examples/images/scatteringFromSizeDistributiontrimodal.jpg
:width: 50 %
:align: center
:alt: scatteringFromSizeDistribution
"""
if weight is None:
weight = lambda r: 1.
sizedistribution = np.array(sizedistribution)
result = []
if size is None:
for spr in sizedistribution.T:
result.append(weight(spr[0]) * spr[1] * func(q, spr[0], **kwargs).Y)
else:
for spr in sizedistribution.T:
kwargs.update({size: spr[0]})
result.append(weight(spr[0]) * spr[1] * func(q, **kwargs).Y)
result = dA(np.c_[q, np.r_[result].sum(axis=0)].T)
result.setColumnIndex(iey=None)
result.formfactor = str(func.__name__)
result.formfactorkwargs = str(kwargs)
result.modelname = inspect.currentframe().f_code.co_name
return result