source: sasmodels/explore/asymint.py @ 6e604f8

core_shell_microgelsmagnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 6e604f8 was 6e604f8, checked in by Paul Kienzle <pkienzle@…>, 7 years ago

fix broken explore/asymint.py (environment now set up with static methods rather than instance methods)

  • Property mode set to 100755
File size: 11.9 KB
Line 
1#!/usr/bin/env python
2"""
3Asymmetric shape integration
4"""
5
6from __future__ import print_function, division
7
8import os, sys
9sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
10
11import numpy as np
12import mpmath as mp
13from numpy import pi, sin, cos, sqrt, exp, expm1, degrees, log10
14from numpy.polynomial.legendre import leggauss
15from scipy.integrate import dblquad, simps, romb, romberg
16import pylab
17
18import sasmodels.special as sp
19
20# Need to parse shape early since it determines the kernel function
21# that will be used for the various integrators
22shape = 'parallelepiped' if len(sys.argv) < 2 else sys.argv[1]
23Qstr = '0.005' if len(sys.argv) < 3 else sys.argv[2]
24
25class MPenv:
26    sqrt = staticmethod(mp.sqrt)
27    exp = staticmethod(mp.exp)
28    expm1 = staticmethod(mp.expm1)
29    cos = staticmethod(mp.cos)
30    sin = staticmethod(mp.sin)
31    tan = staticmethod(mp.tan)
32    @staticmethod
33    def sas_3j1x_x(x):
34        return 3*(mp.sin(x)/x - mp.cos(x))/(x*x)
35    @staticmethod
36    def sas_2J1x_x(x):
37        return 2*mp.j1(x)/x
38    @staticmethod
39    def sas_sinx_x(x):
40        return mp.sin(x)/x
41    pi = mp.pi
42    mpf = staticmethod(mp.mpf)
43
44class NPenv:
45    sqrt = staticmethod(np.sqrt)
46    exp = staticmethod(np.exp)
47    expm1 = staticmethod(np.expm1)
48    cos = staticmethod(np.cos)
49    sin = staticmethod(np.sin)
50    tan = staticmethod(np.tan)
51    sas_3j1x_x = staticmethod(sp.sas_3j1x_x)
52    sas_2J1x_x = staticmethod(sp.sas_2J1x_x)
53    sas_sinx_x = staticmethod(sp.sas_sinx_x)
54    pi = np.pi
55    mpf = staticmethod(float)
56
57SLD = 3
58SLD_SOLVENT = 6
59CONTRAST = SLD - SLD_SOLVENT
60
61# Carefully code models so that mpmath will use full precision.  That means:
62#    * wrap inputs in env.mpf
63#    * don't use floating point constants, only integers
64#    * for division, make sure the numerator or denominator is env.mpf
65#    * use env.pi, env.sas_sinx_x, etc. for functions
66def make_parallelepiped(a, b, c, env=NPenv):
67    a, b, c = env.mpf(a), env.mpf(b), env.mpf(c)
68    def Fq(qa, qb, qc):
69        siA = env.sas_sinx_x(0.5*a*qa/2)
70        siB = env.sas_sinx_x(0.5*b*qb/2)
71        siC = env.sas_sinx_x(0.5*c*qc/2)
72        return siA * siB * siC
73    Fq.__doc__ = "parallelepiped a=%g, b=%g c=%g"%(a, b, c)
74    volume = a*b*c
75    norm = CONTRAST**2*volume/10000
76    return norm, Fq
77
78def make_triaxial_ellipsoid(a, b, c, env=NPenv):
79    a, b, c = env.mpf(a), env.mpf(b), env.mpf(c)
80    def Fq(qa, qb, qc):
81        qr = env.sqrt((a*qa)**2 + (b*qb)**2 + (c*qc)**2)
82        return env.sas_3j1x_x(qr)
83    Fq.__doc__ = "triaxial ellipse minor=%g, major=%g polar=%g"%(a, b, c)
84    volume = 4*env.pi*a*b*c/3
85    norm = CONTRAST**2*volume/10000
86    return norm, Fq
87
88def make_cylinder(radius, length, env=NPenv):
89    radius, length = env.mpf(radius), env.mpf(length)
90    def Fq(qa, qb, qc):
91        qab = env.sqrt(qa**2 + qb**2)
92        return env.sas_2J1x_x(qab*radius) * env.sas_sinx_x((qc*length)/2)
93    Fq.__doc__ = "cylinder radius=%g, length=%g"%(radius, length)
94    volume = env.pi*radius**2*length
95    norm = CONTRAST**2*volume/10000
96    return norm, Fq
97
98def make_sphere(radius, env=NPenv):
99    radius = env.mpf(radius)
100    def Fq(qa, qb, qc):
101        q = env.sqrt(qa**2 + qb**2 + qc**2)
102        return env.sas_3j1x_x(q*radius)
103    Fq.__doc__ = "sphere radius=%g"%(radius, )
104    volume = 4*pi*radius**3
105    norm = CONTRAST**2*volume/10000
106    return norm, Fq
107
108def make_paracrystal(radius, dnn, d_factor, lattice='bcc', env=NPenv):
109    radius, dnn, d_factor = env.mpf(radius), env.mpf(dnn), env.mpf(d_factor)
110    def sc(qa, qb, qc):
111        return qa, qb, qc
112    def bcc(qa, qb, qc):
113        a1 = (+qa + qb + qc)/2
114        a2 = (-qa - qb + qc)/2
115        a3 = (-qa + qb - qc)/2
116        return a1, a2, a3
117    def fcc(qa, qb, qc):
118        a1 = ( 0. + qb + qc)/2
119        a2 = (-qa + 0. + qc)/2
120        a3 = (-qa + qb + 0.)/2
121        return a1, a2, a3
122    lattice_fn = {'sc': sc, 'bcc': bcc, 'fcc': fcc}[lattice]
123    radius, dnn, d_factor = env.mpf(radius), env.mpf(dnn), env.mpf(d_factor)
124    def Fq(qa, qb, qc):
125        a1, a2, a3 = lattice_fn(qa, qb, qc)
126        # Note: paper says that different directions can have different
127        # distoration factors.  Easy enough to add to the code.
128        # This would definitely break 8-fold symmetry.
129        arg = -(dnn*d_factor)**2*(a1**2 + a2**2 + a3**2)/2
130        exp_arg = env.exp(arg)
131        den = [((exp_arg - 2*env.cos(dnn*a))*exp_arg + 1) for a in (a1, a2, a3)]
132        Sq = -env.expm1(2*arg)**3/(den[0]*den[1]*den[2])
133
134        q = env.sqrt(qa**2 + qb**2 + qc**2)
135        Fq = env.sas_3j1x_x(q*radius)
136        # the kernel computes F(q)**2, but we need S(q)*F(q)**2
137        return env.sqrt(Sq)*Fq
138    Fq.__doc__ = "%s paracrystal a=%g da=%g r=%g"%(lattice, dnn, d_factor, radius)
139    def sphere_volume(r): return 4*env.pi*r**3/3
140    Vf = {
141        'sc': sphere_volume(radius/dnn),
142        'bcc': 2*sphere_volume(env.sqrt(3)/2*radius/dnn),
143        'fcc': 4*sphere_volume(radius/dnn/env.sqrt(2)),
144    }[lattice]
145    volume = sphere_volume(radius)
146    norm = CONTRAST**2*volume*Vf/10000
147    return norm, Fq
148
149if shape == 'sphere':
150    RADIUS = 50  # integer for the sake of mpf
151    NORM, KERNEL = make_sphere(radius=RADIUS)
152    NORM_MP, KERNEL_MP = make_sphere(radius=RADIUS, env=MPenv)
153elif shape == 'cylinder':
154    #RADIUS, LENGTH = 10, 100000
155    RADIUS, LENGTH = 10, 300  # integer for the sake of mpf
156    NORM, KERNEL = make_cylinder(radius=RADIUS, length=LENGTH)
157    NORM_MP, KERNEL_MP = make_cylinder(radius=RADIUS, length=LENGTH, env=MPenv)
158elif shape == 'triaxial_ellipsoid':
159    #A, B, C = 4450, 14000, 47
160    A, B, C = 445, 140, 47  # integer for the sake of mpf
161    NORM, KERNEL = make_triellip(A, B, C)
162    NORM_MP, KERNEL_MP = make_triellip(A, B, C, env=MPenv)
163elif shape == 'parallelepiped':
164    #A, B, C = 4450, 14000, 47
165    A, B, C = 445, 140, 47  # integer for the sake of mpf
166    NORM, KERNEL = make_parallelepiped(A, B, C)
167    NORM_MP, KERNEL_MP = make_parallelepiped(A, B, C, env=MPenv)
168elif shape == 'paracrystal':
169    LATTICE = 'bcc'
170    #LATTICE = 'fcc'
171    #LATTICE = 'sc'
172    DNN, D_FACTOR = 220, '0.06'  # mpmath needs to initialize floats from string
173    RADIUS = 40  # integer for the sake of mpf
174    NORM, KERNEL = make_paracrystal(
175        radius=RADIUS, dnn=DNN, d_factor=D_FACTOR, lattice=LATTICE)
176    NORM_MP, KERNEL_MP = make_paracrystal(
177        radius=RADIUS, dnn=DNN, d_factor=D_FACTOR, lattice=LATTICE, env=MPenv)
178else:
179    raise ValueError("Unknown shape %r"%shape)
180
181# Note: hardcoded in mp_quad
182THETA_LOW, THETA_HIGH = 0, pi
183PHI_LOW, PHI_HIGH = 0, 2*pi
184SCALE = 1
185
186# mathematica code for triaxial_ellipsoid (untested)
187_ = """
188R[theta_, phi_, a_, b_, c_] := Sqrt[(a Sin[theta]Cos[phi])^2 + (b Sin[theta]Sin[phi])^2 + (c Cos[theta])^2]
189Sphere[q_, r_] := 3 SphericalBesselJ[q r]/(q r)
190V[a_, b_, c_] := 4/3 pi a b c
191Norm[sld_, solvent_, a_, b_, c_] := V[a, b, c] (solvent - sld)^2
192F[q_, theta_, phi_, a_, b_, c_] := Sphere[q, R[theta, phi, a, b, c]]
193I[q_, sld_, solvent_, a_, b_, c_] := Norm[sld, solvent, a, b, c]/(4 pi) Integrate[F[q, theta, phi, a, b, c]^2 Sin[theta], {phi, 0, 2 pi}, {theta, 0, pi}]
194I[6/10^3, 63/10, 3, 445, 140, 47]
195"""
196
197# 2D integration functions
198def mp_quad_2d(q, shape):
199    evals = [0]
200    def integrand(theta, phi):
201        evals[0] += 1
202        qab = q*mp.sin(theta)
203        qa = qab*mp.cos(phi)
204        qb = qab*mp.sin(phi)
205        qc = q*mp.cos(theta)
206        Zq = KERNEL_MP(qa, qb, qc)**2
207        return Zq*mp.sin(theta)
208    ans = mp.quad(integrand, (0, mp.pi), (0, 2*mp.pi))
209    Iq = NORM_MP*ans/(4*mp.pi)
210    return evals[0], Iq
211
212def kernel_2d(q, theta, phi):
213    """
214    S(q) kernel for paracrystal forms.
215    """
216    qab = q*sin(theta)
217    qa = qab*cos(phi)
218    qb = qab*sin(phi)
219    qc = q*cos(theta)
220    return NORM*KERNEL(qa, qb, qc)**2
221
222def scipy_dblquad_2d(q):
223    """
224    Compute the integral using scipy dblquad.  This gets the correct answer
225    eventually, but it is slow.
226    """
227    evals = [0]
228    def integrand(phi, theta):
229        evals[0] += 1
230        Zq = kernel_2d(q, theta=theta, phi=phi)
231        return Zq*sin(theta)
232    ans = dblquad(integrand, THETA_LOW, THETA_HIGH, lambda x: PHI_LOW, lambda x: PHI_HIGH)[0]
233    return evals[0], ans*SCALE/(4*pi)
234
235
236def scipy_romberg_2d(q):
237    """
238    Compute the integral using romberg integration.  This function does not
239    complete in a reasonable time.  No idea if it is accurate.
240    """
241    evals = [0]
242    def inner(phi, theta):
243        evals[0] += 1
244        return kernel_2d(q, theta=theta, phi=phi)
245    def outer(theta):
246        Zq = romberg(inner, PHI_LOW, PHI_HIGH, divmax=100, args=(theta,))
247        return Zq*sin(theta)
248    ans = romberg(outer, THETA_LOW, THETA_HIGH, divmax=100)
249    return evals[0], ans*SCALE/(4*pi)
250
251
252def semi_romberg_2d(q, n=100):
253    """
254    Use 1D romberg integration in phi and regular simpsons rule in theta.
255    """
256    evals = [0]
257    def inner(phi, theta):
258        evals[0] += 1
259        return kernel_2d(q, theta=theta, phi=phi)
260    theta = np.linspace(THETA_LOW, THETA_HIGH, n)
261    Zq = [romberg(inner, PHI_LOW, PHI_HIGH, divmax=100, args=(t,)) for t in theta]
262    ans = simps(np.array(Zq)*sin(theta), dx=theta[1]-theta[0])
263    return evals[0], ans*SCALE/(4*pi)
264
265def gauss_quad_2d(q, n=150):
266    """
267    Compute the integral using gaussian quadrature for n = 20, 76 or 150.
268    """
269    z, w = leggauss(n)
270    theta = (THETA_HIGH-THETA_LOW)*(z + 1)/2 + THETA_LOW
271    phi = (PHI_HIGH-PHI_LOW)*(z + 1)/2 + PHI_LOW
272    Atheta, Aphi = np.meshgrid(theta, phi)
273    Aw = w[None, :] * w[:, None]
274    sin_theta = abs(sin(Atheta))
275    Zq = kernel_2d(q=q, theta=Atheta, phi=Aphi)
276    # change from [-1,1] x [-1,1] range to [0, pi] x [0, 2 pi] range
277    dxdy_stretch = (THETA_HIGH-THETA_LOW)/2 * (PHI_HIGH-PHI_LOW)/2
278    Iq = np.sum(Zq*Aw*sin_theta)*SCALE/(4*pi) * dxdy_stretch
279    return n**2, Iq
280
281def gridded_2d(q, n=300):
282    """
283    Compute the integral on a regular grid using rectangular, trapezoidal,
284    simpsons, and romberg integration.  Romberg integration requires that
285    the grid be of size n = 2**k + 1.
286    """
287    theta = np.linspace(THETA_LOW, THETA_HIGH, n)
288    phi = np.linspace(PHI_LOW, PHI_HIGH, n)
289    Atheta, Aphi = np.meshgrid(theta, phi)
290    Zq = kernel_2d(q=q, theta=Atheta, phi=Aphi)
291    Zq *= abs(sin(Atheta))
292    dx, dy = theta[1]-theta[0], phi[1]-phi[0]
293    print("rect-%d"%n, n**2, np.sum(Zq)*dx*dy*SCALE/(4*pi))
294    print("trapz-%d"%n, n**2, np.trapz(np.trapz(Zq, dx=dx), dx=dy)*SCALE/(4*pi))
295    print("simpson-%d"%n, n**2, simps(simps(Zq, dx=dx), dx=dy)*SCALE/(4*pi))
296    print("romb-%d"%n, n**2, romb(romb(Zq, dx=dx), dx=dy)*SCALE/(4*pi))
297
298def plot_2d(q, n=300):
299    """
300    Plot the 2D surface that needs to be integrated in order to compute
301    the BCC S(q) at a particular q, dnn and d_factor.  *n* is the number
302    of points in the grid.
303    """
304    theta = np.linspace(THETA_LOW, THETA_HIGH, n)
305    phi = np.linspace(PHI_LOW, PHI_HIGH, n)
306    Atheta, Aphi = np.meshgrid(theta, phi)
307    Zq = kernel_2d(q=q, theta=Atheta, phi=Aphi)
308    #Zq *= abs(sin(Atheta))
309    pylab.pcolor(degrees(theta), degrees(phi), log10(np.fmax(Zq, 1.e-6)))
310    pylab.axis('tight')
311    pylab.title("%s I(q,t) sin(t) for q=%g" % (KERNEL.__doc__, q))
312    pylab.xlabel("theta (degrees)")
313    pylab.ylabel("phi (degrees)")
314    cbar = pylab.colorbar()
315    cbar.set_label('log10 S(q)')
316    pylab.show()
317
318def main(Qstr):
319    Q = float(Qstr)
320    if shape == 'sphere':
321        print("exact", NORM*sas_3j1x_x(Q*RADIUS)**2)
322    print("gauss-20", *gauss_quad_2d(Q, n=20))
323    print("gauss-76", *gauss_quad_2d(Q, n=76))
324    print("gauss-150", *gauss_quad_2d(Q, n=150))
325    print("gauss-500", *gauss_quad_2d(Q, n=500))
326    #gridded_2d(Q, n=2**8+1)
327    gridded_2d(Q, n=2**10+1)
328    #gridded_2d(Q, n=2**13+1)
329    #gridded_2d(Q, n=2**15+1)
330    if shape != 'paracrystal':  # adaptive forms are too slow!
331        print("dblquad", *scipy_dblquad_2d(Q))
332        print("semi-romberg-100", *semi_romberg_2d(Q, n=100))
333        print("romberg", *scipy_romberg_2d(Q))
334        with mp.workprec(100):
335            print("mpmath", *mp_quad_2d(mp.mpf(Qstr), shape))
336    plot_2d(Q, n=200)
337
338if __name__ == "__main__":
339    main(Qstr)
Note: See TracBrowser for help on using the repository browser.