source: sasmodels/explore/asymint.py @ a189283

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

add overlap support for core-shell-parallelepiped to asymint

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