Changeset 20fe0cd in sasmodels for explore/asymint.py
- Timestamp:
- Oct 23, 2017 11:17:40 PM (7 years ago)
- Branches:
- master, core_shell_microgels, magnetic_model, ticket-1257-vesicle-product, ticket_1156, ticket_1265_superball, ticket_822_more_unit_tests
- Children:
- 6e604f8
- Parents:
- 36b3154
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
explore/asymint.py
- Property mode changed from 100644 to 100755
r31eea1f r20fe0cd 1 #!/usr/bin/env python 1 2 """ 2 3 Asymmetric shape integration … … 6 7 7 8 import os, sys 8 sys.path.insert(0, os.path.dirname(os.path.dirname( __file__)))9 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) 9 10 10 11 import numpy as np … … 15 16 import pylab 16 17 17 from sasmodels.special import square 18 from sasmodels.special import Gauss20Wt, Gauss20Z 19 from sasmodels.special import Gauss76Wt, Gauss76Z 20 from sasmodels.special import Gauss150Wt, Gauss150Z 21 from sasmodels.special import sas_2J1x_x, sas_sinx_x, sas_3j1x_x 22 23 def mp_3j1x_x(x): 24 return 3*(mp.sin(x)/x - mp.cos(x))/(x*x) 25 def mp_2J1x_x(x): 26 return 2*mp.j1(x)/x 27 def mp_sinx_x(x): 28 return mp.sin(x)/x 18 import 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 22 shape = 'parallelepiped' if len(sys.argv) < 2 else sys.argv[1] 23 Qstr = '0.005' if len(sys.argv) < 3 else sys.argv[2] 24 25 class MPenv: 26 sqrt = mp.sqrt 27 exp = mp.exp 28 expm1 = mp.expm1 29 cos = mp.cos 30 sin = mp.sin 31 tan = mp.tan 32 def sas_3j1x_x(x): 33 return 3*(mp.sin(x)/x - mp.cos(x))/(x*x) 34 def sas_2J1x_x(x): 35 return 2*mp.j1(x)/x 36 def sas_sinx_x(x): 37 return mp.sin(x)/x 38 pi = mp.pi 39 mpf = mp.mpf 40 41 class NPenv: 42 sqrt = np.sqrt 43 exp = np.exp 44 expm1 = np.expm1 45 cos = np.cos 46 sin = np.sin 47 tan = np.tan 48 sas_3j1x_x = sp.sas_3j1x_x 49 sas_2J1x_x = sp.sas_2J1x_x 50 sas_sinx_x = sp.sas_sinx_x 51 pi = np.pi 52 mpf = float 29 53 30 54 SLD = 3 … … 32 56 CONTRAST = SLD - SLD_SOLVENT 33 57 34 def make_parallelepiped(a, b, c): 35 def Fq(qa, qb, qc): 36 siA = sas_sinx_x(0.5*a*qa) 37 siB = sas_sinx_x(0.5*b*qb) 38 siC = sas_sinx_x(0.5*c*qc) 58 # Carefully code models so that mpmath will use full precision. That means: 59 # * wrap inputs in env.mpf 60 # * don't use floating point constants, only integers 61 # * for division, make sure the numerator or denominator is env.mpf 62 # * use env.pi, env.sas_sinx_x, etc. for functions 63 def make_parallelepiped(a, b, c, env=NPenv): 64 a, b, c = env.mpf(a), env.mpf(b), env.mpf(c) 65 def Fq(qa, qb, qc): 66 siA = env.sas_sinx_x(0.5*a*qa/2) 67 siB = env.sas_sinx_x(0.5*b*qb/2) 68 siC = env.sas_sinx_x(0.5*c*qc/2) 39 69 return siA * siB * siC 70 Fq.__doc__ = "parallelepiped a=%g, b=%g c=%g"%(a, b, c) 40 71 volume = a*b*c 41 norm = volume*CONTRAST**2/10**4 42 return norm, Fq 43 44 def make_parallelepiped_mp(a, b, c): 45 a, b, c = mp.mpf(a), mp.mpf(b), mp.mpf(c) 46 def Fq(qa, qb, qc): 47 siA = mp_sinx_x(a*qa/2) 48 siB = mp_sinx_x(b*qb/2) 49 siC = mp_sinx_x(c*qc/2) 50 return siA * siB * siC 51 volume = a*b*c 52 norm = (volume*CONTRAST**2)/10000 # mpf since volume=a*b*c is mpf 53 return norm, Fq 54 55 def make_triellip(a, b, c): 56 def Fq(qa, qb, qc): 57 qr = sqrt((a*qa)**2 + (b*qb)**2 + (c*qc)**2) 58 return sas_3j1x_x(qr) 59 volume = 4*pi*a*b*c/3 60 norm = volume*CONTRAST**2/10**4 61 return norm, Fq 62 63 def make_triellip_mp(a, b, c): 64 a, b, c = mp.mpf(a), mp.mpf(b), mp.mpf(c) 65 def Fq(qa, qb, qc): 66 qr = mp.sqrt((a*qa)**2 + (b*qb)**2 + (c*qc)**2) 67 return mp_3j1x_x(qr) 68 volume = (4*mp.pi*a*b*c)/3 69 norm = (volume*CONTRAST**2)/10000 # mpf since mp.pi is mpf 70 return norm, Fq 71 72 def make_cylinder(radius, length): 73 def Fq(qa, qb, qc): 74 qab = sqrt(qa**2 + qb**2) 75 return sas_2J1x_x(qab*radius) * sas_sinx_x(qc*0.5*length) 76 volume = pi*radius**2*length 77 norm = volume*CONTRAST**2/10**4 78 return norm, Fq 79 80 def make_cylinder_mp(radius, length): 81 radius, length = mp.mpf(radius), mp.mpf(length) 82 def Fq(qa, qb, qc): 83 qab = mp.sqrt(qa**2 + qb**2) 84 return mp_2J1x_x(qab*radius) * mp_sinx_x((qc*length)/2) 85 volume = mp.pi*radius**2*length 86 norm = (volume*CONTRAST**2)/10000 # mpf since mp.pi is mpf 87 return norm, Fq 88 89 def make_sphere(radius): 90 def Fq(qa, qb, qc): 91 q = sqrt(qa**2 + qb**2 + qc**2) 92 return sas_3j1x_x(q*radius) 93 volume = 4*pi*radius**3/3 94 norm = volume*CONTRAST**2/10**4 95 return norm, Fq 96 97 def make_sphere_mp(radius): 98 radius = mp.mpf(radius) 99 def Fq(qa, qb, qc): 100 q = mp.sqrt(qa**2 + qb**2 + qc**2) 101 return mp_3j1x_x(q*radius) 102 volume = (4*mp.pi*radius**3)/3 103 norm = (volume*CONTRAST**2)/10000 # mpf since mp.pi is mpf 104 return norm, Fq 105 106 shape = 'parallelepiped' 107 #shape = 'triellip' 108 #shape = 'sphere' 109 #shape = 'cylinder' 110 if shape == 'cylinder': 72 norm = CONTRAST**2*volume/10000 73 return norm, Fq 74 75 def make_triaxial_ellipsoid(a, b, c, env=NPenv): 76 a, b, c = env.mpf(a), env.mpf(b), env.mpf(c) 77 def Fq(qa, qb, qc): 78 qr = env.sqrt((a*qa)**2 + (b*qb)**2 + (c*qc)**2) 79 return env.sas_3j1x_x(qr) 80 Fq.__doc__ = "triaxial ellipse minor=%g, major=%g polar=%g"%(a, b, c) 81 volume = 4*env.pi*a*b*c/3 82 norm = CONTRAST**2*volume/10000 83 return norm, Fq 84 85 def make_cylinder(radius, length, env=NPenv): 86 radius, length = env.mpf(radius), env.mpf(length) 87 def Fq(qa, qb, qc): 88 qab = env.sqrt(qa**2 + qb**2) 89 return env.sas_2J1x_x(qab*radius) * env.sas_sinx_x((qc*length)/2) 90 Fq.__doc__ = "cylinder radius=%g, length=%g"%(radius, length) 91 volume = env.pi*radius**2*length 92 norm = CONTRAST**2*volume/10000 93 return norm, Fq 94 95 def make_sphere(radius, env=NPenv): 96 radius = env.mpf(radius) 97 def Fq(qa, qb, qc): 98 q = env.sqrt(qa**2 + qb**2 + qc**2) 99 return env.sas_3j1x_x(q*radius) 100 Fq.__doc__ = "sphere radius=%g"%(radius, ) 101 volume = 4*pi*radius**3 102 norm = CONTRAST**2*volume/10000 103 return norm, Fq 104 105 def make_paracrystal(radius, dnn, d_factor, lattice='bcc', env=NPenv): 106 radius, dnn, d_factor = env.mpf(radius), env.mpf(dnn), env.mpf(d_factor) 107 def sc(qa, qb, qc): 108 return qa, qb, qc 109 def bcc(qa, qb, qc): 110 a1 = (+qa + qb + qc)/2 111 a2 = (-qa - qb + qc)/2 112 a3 = (-qa + qb - qc)/2 113 return a1, a2, a3 114 def fcc(qa, qb, qc): 115 a1 = ( 0. + qb + qc)/2 116 a2 = (-qa + 0. + qc)/2 117 a3 = (-qa + qb + 0.)/2 118 return a1, a2, a3 119 lattice_fn = {'sc': sc, 'bcc': bcc, 'fcc': fcc}[lattice] 120 radius, dnn, d_factor = env.mpf(radius), env.mpf(dnn), env.mpf(d_factor) 121 def Fq(qa, qb, qc): 122 a1, a2, a3 = lattice_fn(qa, qb, qc) 123 # Note: paper says that different directions can have different 124 # distoration factors. Easy enough to add to the code. 125 # This would definitely break 8-fold symmetry. 126 arg = -(dnn*d_factor)**2*(a1**2 + a2**2 + a3**2)/2 127 exp_arg = env.exp(arg) 128 den = [((exp_arg - 2*env.cos(dnn*a))*exp_arg + 1) for a in (a1, a2, a3)] 129 Sq = -env.expm1(2*arg)**3/(den[0]*den[1]*den[2]) 130 131 q = env.sqrt(qa**2 + qb**2 + qc**2) 132 Fq = env.sas_3j1x_x(q*radius) 133 # the kernel computes F(q)**2, but we need S(q)*F(q)**2 134 return env.sqrt(Sq)*Fq 135 Fq.__doc__ = "%s paracrystal a=%g da=%g r=%g"%(lattice, dnn, d_factor, radius) 136 def sphere_volume(r): return 4*env.pi*r**3/3 137 Vf = { 138 'sc': sphere_volume(radius/dnn), 139 'bcc': 2*sphere_volume(env.sqrt(3)/2*radius/dnn), 140 'fcc': 4*sphere_volume(radius/dnn/env.sqrt(2)), 141 }[lattice] 142 volume = sphere_volume(radius) 143 norm = CONTRAST**2*volume*Vf/10000 144 return norm, Fq 145 146 if shape == 'sphere': 147 RADIUS = 50 # integer for the sake of mpf 148 NORM, KERNEL = make_sphere(radius=RADIUS) 149 NORM_MP, KERNEL_MP = make_sphere(radius=RADIUS, env=MPenv) 150 elif shape == 'cylinder': 111 151 #RADIUS, LENGTH = 10, 100000 112 152 RADIUS, LENGTH = 10, 300 # integer for the sake of mpf 113 153 NORM, KERNEL = make_cylinder(radius=RADIUS, length=LENGTH) 114 NORM_MP, KERNEL_MP = make_cylinder _mp(radius=RADIUS, length=LENGTH)115 elif shape == 'tri ellip':154 NORM_MP, KERNEL_MP = make_cylinder(radius=RADIUS, length=LENGTH, env=MPenv) 155 elif shape == 'triaxial_ellipsoid': 116 156 #A, B, C = 4450, 14000, 47 117 157 A, B, C = 445, 140, 47 # integer for the sake of mpf 118 158 NORM, KERNEL = make_triellip(A, B, C) 119 NORM_MP, KERNEL_MP = make_triellip _mp(A, B, C)159 NORM_MP, KERNEL_MP = make_triellip(A, B, C, env=MPenv) 120 160 elif shape == 'parallelepiped': 121 161 #A, B, C = 4450, 14000, 47 122 162 A, B, C = 445, 140, 47 # integer for the sake of mpf 123 163 NORM, KERNEL = make_parallelepiped(A, B, C) 124 NORM_MP, KERNEL_MP = make_parallelepiped_mp(A, B, C) 125 elif shape == 'sphere': 126 RADIUS = 50 # integer for the sake of mpf 127 NORM, KERNEL = make_sphere(radius=RADIUS) 128 NORM_MP, KERNEL_MP = make_sphere_mp(radius=RADIUS) 164 NORM_MP, KERNEL_MP = make_parallelepiped(A, B, C, env=MPenv) 165 elif shape == 'paracrystal': 166 LATTICE = 'bcc' 167 #LATTICE = 'fcc' 168 #LATTICE = 'sc' 169 DNN, D_FACTOR = 220, '0.06' # mpmath needs to initialize floats from string 170 RADIUS = 40 # integer for the sake of mpf 171 NORM, KERNEL = make_paracrystal( 172 radius=RADIUS, dnn=DNN, d_factor=D_FACTOR, lattice=LATTICE) 173 NORM_MP, KERNEL_MP = make_paracrystal( 174 radius=RADIUS, dnn=DNN, d_factor=D_FACTOR, lattice=LATTICE, env=MPenv) 129 175 else: 130 176 raise ValueError("Unknown shape %r"%shape) 131 177 178 # Note: hardcoded in mp_quad 132 179 THETA_LOW, THETA_HIGH = 0, pi 133 180 PHI_LOW, PHI_HIGH = 0, 2*pi … … 145 192 """ 146 193 147 148 def mp_quad (q, shape):194 # 2D integration functions 195 def mp_quad_2d(q, shape): 149 196 evals = [0] 150 197 def integrand(theta, phi): … … 160 207 return evals[0], Iq 161 208 162 def kernel (q, theta, phi):209 def kernel_2d(q, theta, phi): 163 210 """ 164 211 S(q) kernel for paracrystal forms. … … 170 217 return NORM*KERNEL(qa, qb, qc)**2 171 218 172 def scipy_dblquad (q):219 def scipy_dblquad_2d(q): 173 220 """ 174 221 Compute the integral using scipy dblquad. This gets the correct answer … … 178 225 def integrand(phi, theta): 179 226 evals[0] += 1 180 Zq = kernel (q, theta=theta, phi=phi)227 Zq = kernel_2d(q, theta=theta, phi=phi) 181 228 return Zq*sin(theta) 182 229 ans = dblquad(integrand, THETA_LOW, THETA_HIGH, lambda x: PHI_LOW, lambda x: PHI_HIGH)[0] … … 192 239 def inner(phi, theta): 193 240 evals[0] += 1 194 return kernel (q, theta=theta, phi=phi)241 return kernel_2d(q, theta=theta, phi=phi) 195 242 def outer(theta): 196 243 Zq = romberg(inner, PHI_LOW, PHI_HIGH, divmax=100, args=(theta,)) … … 200 247 201 248 202 def semi_romberg (q, n=100):249 def semi_romberg_2d(q, n=100): 203 250 """ 204 251 Use 1D romberg integration in phi and regular simpsons rule in theta. … … 207 254 def inner(phi, theta): 208 255 evals[0] += 1 209 return kernel (q, theta=theta, phi=phi)256 return kernel_2d(q, theta=theta, phi=phi) 210 257 theta = np.linspace(THETA_LOW, THETA_HIGH, n) 211 258 Zq = [romberg(inner, PHI_LOW, PHI_HIGH, divmax=100, args=(t,)) for t in theta] … … 213 260 return evals[0], ans*SCALE/(4*pi) 214 261 215 def gauss_quad (q, n=150):262 def gauss_quad_2d(q, n=150): 216 263 """ 217 264 Compute the integral using gaussian quadrature for n = 20, 76 or 150. 218 265 """ 219 if n == 20: 220 z, w = Gauss20Z, Gauss20Wt 221 elif n == 76: 222 z, w = Gauss76Z, Gauss76Wt 223 elif n == 150: 224 z, w = Gauss150Z, Gauss150Wt 225 else: 226 z, w = leggauss(n) 266 z, w = leggauss(n) 227 267 theta = (THETA_HIGH-THETA_LOW)*(z + 1)/2 + THETA_LOW 228 268 phi = (PHI_HIGH-PHI_LOW)*(z + 1)/2 + PHI_LOW … … 230 270 Aw = w[None, :] * w[:, None] 231 271 sin_theta = abs(sin(Atheta)) 232 Zq = kernel (q=q, theta=Atheta, phi=Aphi)272 Zq = kernel_2d(q=q, theta=Atheta, phi=Aphi) 233 273 # change from [-1,1] x [-1,1] range to [0, pi] x [0, 2 pi] range 234 274 dxdy_stretch = (THETA_HIGH-THETA_LOW)/2 * (PHI_HIGH-PHI_LOW)/2 … … 236 276 return n**2, Iq 237 277 238 239 def gridded_integrals(q, n=300): 278 def gridded_2d(q, n=300): 240 279 """ 241 280 Compute the integral on a regular grid using rectangular, trapezoidal, … … 246 285 phi = np.linspace(PHI_LOW, PHI_HIGH, n) 247 286 Atheta, Aphi = np.meshgrid(theta, phi) 248 Zq = kernel (q=q, theta=Atheta, phi=Aphi)287 Zq = kernel_2d(q=q, theta=Atheta, phi=Aphi) 249 288 Zq *= abs(sin(Atheta)) 250 289 dx, dy = theta[1]-theta[0], phi[1]-phi[0] … … 254 293 print("romb-%d"%n, n**2, romb(romb(Zq, dx=dx), dx=dy)*SCALE/(4*pi)) 255 294 256 def plot (q, n=300):295 def plot_2d(q, n=300): 257 296 """ 258 297 Plot the 2D surface that needs to be integrated in order to compute … … 263 302 phi = np.linspace(PHI_LOW, PHI_HIGH, n) 264 303 Atheta, Aphi = np.meshgrid(theta, phi) 265 Zq = kernel (q=q, theta=Atheta, phi=Aphi)304 Zq = kernel_2d(q=q, theta=Atheta, phi=Aphi) 266 305 #Zq *= abs(sin(Atheta)) 267 306 pylab.pcolor(degrees(theta), degrees(phi), log10(np.fmax(Zq, 1.e-6))) 268 307 pylab.axis('tight') 269 pylab.title("%s Z(q) for q=%g" % (KERNEL.__name__, q))308 pylab.title("%s I(q,t) sin(t) for q=%g" % (KERNEL.__doc__, q)) 270 309 pylab.xlabel("theta (degrees)") 271 310 pylab.ylabel("phi (degrees)") … … 274 313 pylab.show() 275 314 276 if __name__ == "__main__": 277 Qstr = '0.005' 278 #Qstr = '0.8' 279 #Qstr = '0.0003' 315 def main(Qstr): 280 316 Q = float(Qstr) 281 317 if shape == 'sphere': 282 318 print("exact", NORM*sas_3j1x_x(Q*RADIUS)**2) 283 print("gauss-20", *gauss_quad(Q, n=20)) 284 print("gauss-76", *gauss_quad(Q, n=76)) 285 print("gauss-150", *gauss_quad(Q, n=150)) 286 print("gauss-500", *gauss_quad(Q, n=500)) 287 print("dblquad", *scipy_dblquad(Q)) 288 print("semi-romberg-100", *semi_romberg(Q, n=100)) 289 print("romberg", *scipy_romberg_2d(Q)) 290 #gridded_integrals(Q, n=2**8+1) 291 gridded_integrals(Q, n=2**10+1) 292 #gridded_integrals(Q, n=2**13+1) 293 #gridded_integrals(Q, n=2**15+1) 294 with mp.workprec(100): 295 print("mpmath", *mp_quad(mp.mpf(Qstr), shape)) 296 #plot(Q, n=200) 319 print("gauss-20", *gauss_quad_2d(Q, n=20)) 320 print("gauss-76", *gauss_quad_2d(Q, n=76)) 321 print("gauss-150", *gauss_quad_2d(Q, n=150)) 322 print("gauss-500", *gauss_quad_2d(Q, n=500)) 323 #gridded_2d(Q, n=2**8+1) 324 gridded_2d(Q, n=2**10+1) 325 #gridded_2d(Q, n=2**13+1) 326 #gridded_2d(Q, n=2**15+1) 327 if shape != 'paracrystal': # adaptive forms are too slow! 328 print("dblquad", *scipy_dblquad_2d(Q)) 329 print("semi-romberg-100", *semi_romberg_2d(Q, n=100)) 330 print("romberg", *scipy_romberg_2d(Q)) 331 with mp.workprec(100): 332 print("mpmath", *mp_quad_2d(mp.mpf(Qstr), shape)) 333 plot_2d(Q, n=200) 334 335 if __name__ == "__main__": 336 main(Qstr)
Note: See TracChangeset
for help on using the changeset viewer.