source: sasmodels/explore/realspace.py @ 67cc0ff

Last change on this file since 67cc0ff was a1c32c2, checked in by Paul Kienzle <pkienzle@…>, 7 years ago

Use gauss-legendre integration for cross-checking against P(r)

  • Property mode set to 100644
File size: 15.8 KB
Line 
1from __future__ import division, print_function
2
3import time
4from copy import copy
5
6import numpy as np
7from numpy import pi, radians, sin, cos, sqrt
8from numpy.random import poisson, uniform
9from numpy.polynomial.legendre import leggauss
10from scipy.integrate import simps
11from scipy.special import j1 as J1
12
13# Definition of rotation matrices comes from wikipedia:
14#    https://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations
15def Rx(angle):
16    """Construct a matrix to rotate points about *x* by *angle* degrees."""
17    a = radians(angle)
18    R = [[1, 0, 0],
19         [0, +cos(a), -sin(a)],
20         [0, +sin(a), +cos(a)]]
21    return np.matrix(R)
22
23def Ry(angle):
24    """Construct a matrix to rotate points about *y* by *angle* degrees."""
25    a = radians(angle)
26    R = [[+cos(a), 0, +sin(a)],
27         [0, 1, 0],
28         [-sin(a), 0, +cos(a)]]
29    return np.matrix(R)
30
31def Rz(angle):
32    """Construct a matrix to rotate points about *z* by *angle* degrees."""
33    a = radians(angle)
34    R = [[+cos(a), -sin(a), 0],
35         [+sin(a), +cos(a), 0],
36         [0, 0, 1]]
37    return np.matrix(R)
38
39def rotation(theta, phi, psi):
40    """
41    Apply the jitter transform to a set of points.
42
43    Points are stored in a 3 x n numpy matrix, not a numpy array or tuple.
44    """
45    return Rx(phi)*Ry(theta)*Rz(psi)
46
47class Shape:
48    rotation = np.matrix([[1., 0, 0], [0, 1, 0], [0, 0, 1]])
49    center = np.array([0., 0., 0.])[:, None]
50    r_max = None
51
52    def volume(self):
53        # type: () -> float
54        raise NotImplementedError()
55
56    def sample(self, density):
57        # type: (float) -> np.ndarray[N], np.ndarray[N, 3]
58        raise NotImplementedError()
59
60    def rotate(self, theta, phi, psi):
61        self.rotation = rotation(theta, phi, psi) * self.rotation
62        return self
63
64    def shift(self, x, y, z):
65        self.center = self.center + np.array([x, y, z])[:, None]
66        return self
67
68    def _adjust(self, points):
69        points = np.asarray(self.rotation * np.matrix(points.T)) + self.center
70        return points.T
71
72    def r_bins(self, q, over_sampling=1, r_step=0.):
73        r_max = min(2 * pi / q[0], self.r_max)
74        if r_step == 0.:
75            r_step = 2 * pi / q[-1] / over_sampling
76        #r_step = 0.01
77        return np.arange(r_step, r_max, r_step)
78
79class Composite(Shape):
80    def __init__(self, shapes, center=(0, 0, 0), orientation=(0, 0, 0)):
81        self.shapes = shapes
82        self.rotate(*orientation)
83        self.shift(*center)
84
85        # Find the worst case distance between any two points amongst a set
86        # of shapes independent of orientation.  This could easily be a
87        # factor of two worse than necessary, e.g., a pair of thin rods
88        # end-to-end vs the same pair side-by-side.
89        distances = [((s1.r_max + s2.r_max)/2
90                      + sqrt(np.sum((s1.center - s2.center)**2)))
91                     for s1 in shapes
92                     for s2 in shapes]
93        self.r_max = max(distances + [s.r_max for s in shapes])
94
95    def volume(self):
96        return sum(shape.volume() for shape in self.shapes)
97
98    def sample(self, density):
99        values, points = zip(*(shape.sample(density) for shape in self.shapes))
100        return np.hstack(values), self._adjust(np.vstack(points))
101
102class Box(Shape):
103    def __init__(self, a, b, c,
104                 value, center=(0, 0, 0), orientation=(0, 0, 0)):
105        self.value = np.asarray(value)
106        self.rotate(*orientation)
107        self.shift(*center)
108        self.a, self.b, self.c = a, b, c
109        self._scale = np.array([a/2, b/2, c/2])[None, :]
110        self.r_max = sqrt(a**2 + b**2 + c**2)
111
112    def volume(self):
113        return self.a*self.b*self.c
114
115    def sample(self, density):
116        num_points = poisson(density*self.a*self.b*self.c)
117        points = self._scale*uniform(-1, 1, size=(num_points, 3))
118        values = self.value.repeat(points.shape[0])
119        return values, self._adjust(points)
120
121class EllipticalCylinder(Shape):
122    def __init__(self, ra, rb, length,
123                 value, center=(0, 0, 0), orientation=(0, 0, 0)):
124        self.value = np.asarray(value)
125        self.rotate(*orientation)
126        self.shift(*center)
127        self.ra, self.rb, self.length = ra, rb, length
128        self._scale = np.array([ra, rb, length/2])[None, :]
129        self.r_max = sqrt(4*max(ra, rb)**2 + length**2)
130
131    def volume(self):
132        return pi*self.ra*self.rb*self.length
133
134    def sample(self, density):
135        # density of the bounding box
136        num_points = poisson(density*4*self.ra*self.rb*self.length)
137        points = uniform(-1, 1, size=(num_points, 3))
138        radius = points[:, 0]**2 + points[:, 1]**2
139        points = self._scale*points[radius <= 1]
140        values = self.value.repeat(points.shape[0])
141        return values, self._adjust(points)
142
143class TriaxialEllipsoid(Shape):
144    def __init__(self, ra, rb, rc,
145                 value, center=(0, 0, 0), orientation=(0, 0, 0)):
146        self.value = np.asarray(value)
147        self.rotate(*orientation)
148        self.shift(*center)
149        self.ra, self.rb, self.rc = ra, rb, rc
150        self._scale = np.array([ra, rb, rc])[None, :]
151        self.r_max = 2*max(ra, rb, rc)
152
153    def volume(self):
154        return 4*pi/3 * self.ra * self.rb * self.rc
155
156    def sample(self, density):
157        # randomly sample from a box of side length 2*r, excluding anything
158        # not in the ellipsoid
159        num_points = poisson(density*8*self.ra*self.rb*self.rc)
160        points = uniform(-1, 1, size=(num_points, 3))
161        radius = np.sum(points**2, axis=1)
162        points = self._scale*points[radius <= 1]
163        values = self.value.repeat(points.shape[0])
164        return values, self._adjust(points)
165
166class Helix(Shape):
167    def __init__(self, helix_radius, helix_pitch, tube_radius, tube_length,
168                 value, center=(0, 0, 0), orientation=(0, 0, 0)):
169        self.value = np.asarray(value)
170        self.rotate(*orientation)
171        self.shift(*center)
172        self.helix_radius, self.helix_pitch = helix_radius, helix_pitch
173        self.tube_radius, self.tube_length = tube_radius, tube_length
174        helix_length = helix_pitch * tube_length/sqrt(helix_radius**2 + helix_pitch**2)
175        self.r_max = sqrt((2*helix_radius + 2*tube_radius)*2
176                          + (helix_length + 2*tube_radius)**2)
177
178    def volume(self):
179        # small tube radius approximation; for larger tubes need to account
180        # for the fact that the inner length is much shorter than the outer
181        # length
182        return pi*self.tube_radius**2*self.tube_length
183
184    def points(self, density):
185        num_points = poisson(density*4*self.tube_radius**2*self.tube_length)
186        points = uniform(-1, 1, size=(num_points, 3))
187        radius = points[:, 0]**2 + points[:, 1]**2
188        points = points[radius <= 1]
189
190        # Based on math stackexchange answer by Jyrki Lahtonen
191        #     https://math.stackexchange.com/a/461637
192        # with helix along z rather than x [so tuples in answer are (z, x, y)]
193        # and with random points in the cross section (p1, p2) rather than
194        # uniform points on the surface (cos u, sin u).
195        a, R = self.tube_radius, self.helix_radius
196        h = self.helix_pitch
197        scale = 1/sqrt(R**2 + h**2)
198        t = points[:, 3] * (self.tube_length * scale/2)
199        cos_t, sin_t = cos(t), sin(t)
200
201        # rx = R*cos_t
202        # ry = R*sin_t
203        # rz = h*t
204        # nx = -a * cos_t * points[:, 1]
205        # ny = -a * sin_t * points[:, 1]
206        # nz = 0
207        # bx = (a * h/scale) * sin_t * points[:, 2]
208        # by = (-a * h/scale) * cos_t * points[:, 2]
209        # bz = a*R/scale
210        # x = rx + nx + bx
211        # y = ry + ny + by
212        # z = rz + nz + bz
213        u, v = (R - a*points[:, 1]), (a * h/scale)*points[:, 2]
214        x = u * cos_t + v * sin_t
215        y = u * sin_t - v * cos_t
216        z = a*R/scale + h * t
217
218        points = np.hstack((x, y, z))
219        values = self.value.repeat(points.shape[0])
220        return values, self._adjust(points)
221
222def _calc_Pr_nonuniform(r, rho, points):
223    # Make Pr a little be bigger than necessary so that only distances
224    # min < d < max end up in Pr
225    n_max = len(r)+1
226    extended_Pr = np.zeros(n_max+1, 'd')
227    # r refers to bin centers; find corresponding bin edges
228    bins = bin_edges(r)
229    t_next = time.clock() + 3
230    for k, rho_k in enumerate(rho[:-1]):
231        distance = sqrt(np.sum((points[k] - points[k+1:])**2, axis=1))
232        weights = rho_k * rho[k+1:]
233        index = np.searchsorted(bins, distance)
234        # Note: indices may be duplicated, so "Pr[index] += w" will not work!!
235        extended_Pr += np.bincount(index, weights, n_max+1)
236        t = time.clock()
237        if t > t_next:
238            t_next = t + 3
239            print("processing %d of %d"%(k, len(rho)-1))
240    Pr = extended_Pr[1:-1]
241    return Pr / Pr.max()
242
243def calc_Pr(r, rho, points):
244    # P(r) with uniform steps in r is 3x faster; check if we are uniform
245    # before continuing
246    if np.max(np.abs(np.diff(r) - r[0])) > r[0]*0.01:
247        return _calc_Pr_nonuniform(r, rho, points)
248
249    # Make Pr a little be bigger than necessary so that only distances
250    # min < d < max end up in Pr
251    n_max = len(r)
252    extended_Pr = np.zeros(n_max+1, 'd')
253    t0 = time.clock()
254    t_next = t0 + 3
255    row_zero = np.zeros(len(rho), 'i')
256    for k, rho_k in enumerate(rho[:-1]):
257        distances = sqrt(np.sum((points[k] - points[k+1:])**2, axis=1))
258        weights = rho_k * rho[k+1:]
259        index = np.minimum(np.asarray(distances/r[0], 'i'), n_max)
260        # Note: indices may be duplicated, so "Pr[index] += w" will not work!!
261        extended_Pr += np.bincount(index, weights, n_max+1)
262        t = time.clock()
263        if t > t_next:
264            t_next = t + 3
265            print("processing %d of %d"%(k, len(rho)-1))
266    #print("time py:", time.clock() - t0)
267    Pr = extended_Pr[:-1]
268    # Make Pr independent of sampling density.  The factor of 2 comes because
269    # we are only accumulating the upper triangular distances.
270    #Pr = Pr * 2 / len(rho)**2
271    return Pr / Pr.max()
272
273    # Can get an additional 2x by going to C.  Cuda/OpenCL will allow even
274    # more speedup, though still bounded by the n^2 cose.
275    """
276void pdfcalc(int n, const double *pts, const double *rho,
277             int nPr, double *Pr, double rstep)
278{
279  int i,j;
280
281  for (i=0; i<n-2; i++) {
282    for (j=i+1; j<=n-1; j++) {
283      const double dxx=pts[3*i]-pts[3*j];
284      const double dyy=pts[3*i+1]-pts[3*j+1];
285      const double dzz=pts[3*i+2]-pts[3*j+2];
286      const double d=sqrt(dxx*dxx+dyy*dyy+dzz*dzz);
287      const int k=rint(d/rstep);
288      if (k < nPr) Pr[k]+=rho[i]*rho[j];
289    }
290  }
291}
292"""
293
294def j0(x):
295    return np.sinc(x/np.pi)
296
297def calc_Iq(q, r, Pr):
298    Iq = np.array([simps(Pr * j0(qk*r), r) for qk in q])
299    #Iq = np.array([np.trapz(Pr * j0(qk*r), r) for qk in q])
300    Iq /= Iq[0]
301    return Iq
302
303# NOTE: copied from sasmodels/resolution.py
304def bin_edges(x):
305    """
306    Determine bin edges from bin centers, assuming that edges are centered
307    between the bins.
308
309    Note: this uses the arithmetic mean, which may not be appropriate for
310    log-scaled data.
311    """
312    if len(x) < 2 or (np.diff(x) < 0).any():
313        raise ValueError("Expected bins to be an increasing set")
314    edges = np.hstack([
315        x[0]  - 0.5*(x[1]  - x[0]),  # first point minus half first interval
316        0.5*(x[1:] + x[:-1]),        # mid points of all central intervals
317        x[-1] + 0.5*(x[-1] - x[-2]), # last point plus half last interval
318        ])
319    return edges
320
321def plot_calc(r, Pr, q, Iq, theory=None):
322    import matplotlib.pyplot as plt
323    plt.subplot(211)
324    plt.plot(r, Pr, '-', label="Pr")
325    plt.xlabel('r (A)')
326    plt.ylabel('Pr (1/A^2)')
327    plt.subplot(212)
328    plt.loglog(q, Iq, '-', label='from Pr')
329    plt.xlabel('q (1/A')
330    plt.ylabel('Iq')
331    if theory is not None:
332        plt.loglog(theory[0], theory[1], '-', label='analytic')
333        plt.legend()
334
335def plot_points(rho, points):
336    import mpl_toolkits.mplot3d
337    import matplotlib.pyplot as plt
338
339    ax = plt.axes(projection='3d')
340    try:
341        ax.axis('square')
342    except Exception:
343        pass
344    n = len(points)
345    index = np.random.choice(n, size=1000) if n > 1000 else slice(None, None)
346    ax.scatter(points[index, 0], points[index, 1], points[index, 2], c=rho[index])
347    #low, high = points.min(axis=0), points.max(axis=0)
348    #ax.axis([low[0], high[0], low[1], high[1], low[2], high[2]])
349    ax.autoscale(True)
350
351def sas_2J1x_x(x):
352    with np.errstate(all='ignore'):
353        retvalue = 2*J1(x)/x
354    retvalue[x == 0] = 1.
355    return retvalue
356
357def sas_3j1x_x(x):
358    """return 3*j1(x)/x"""
359    with np.errstate(all='ignore'):
360        retvalue = 3*(sin(x) - x*cos(x))/x**3
361    retvalue[x == 0.] = 1.
362    return retvalue
363
364def cylinder_Iq(q, radius, length):
365    z, w = leggauss(76)
366    cos_alpha = (z+1)/2
367    sin_alpha = sqrt(1.0 - cos_alpha**2)
368    Iq = np.empty_like(q)
369    for k, qk in enumerate(q):
370        qab, qc = qk*sin_alpha, qk*cos_alpha
371        Fq = sas_2J1x_x(qab*radius) * j0(qc*length/2)
372        Iq[k] = np.sum(w*Fq**2)
373    Iq = Iq/Iq[0]
374    return Iq
375
376def sphere_Iq(q, radius):
377    Iq = sas_3j1x_x(q*radius)**2
378    return Iq/Iq[0]
379
380def csbox_Iq(q, a, b, c, da, db, dc, slda, sldb, sldc, sld_core):
381    z, w = leggauss(76)
382
383    sld_solvent = 0
384    overlapping = False
385    dr0 = sld_core - sld_solvent
386    drA, drB, drC = slda-sld_solvent, sldb-sld_solvent, sldc-sld_solvent
387    tA, tB, tC = a + 2*da, b + 2*db, c + 2*dc
388
389    outer_sum = np.zeros_like(q)
390    for cos_alpha, outer_w in zip((z+1)/2, w):
391        sin_alpha = sqrt(1.0-cos_alpha*cos_alpha)
392        qc = q*cos_alpha
393        siC = c*j0(c*qc/2)
394        siCt = tC*j0(tC*qc/2)
395        inner_sum = np.zeros_like(q)
396        for beta, inner_w in zip((z + 1)*pi/4, w):
397            qa, qb = q*sin_alpha*sin(beta), q*sin_alpha*cos(beta)
398            siA = a*j0(a*qa/2)
399            siB = b*j0(b*qb/2)
400            siAt = tA*j0(tA*qa/2)
401            siBt = tB*j0(tB*qb/2)
402            if overlapping:
403                Fq = (dr0*siA*siB*siC
404                      + drA*(siAt-siA)*siB*siC
405                      + drB*siAt*(siBt-siB)*siC
406                      + drC*siAt*siBt*(siCt-siC))
407            else:
408                Fq = (dr0*siA*siB*siC
409                      + drA*(siAt-siA)*siB*siC
410                      + drB*siA*(siBt-siB)*siC
411                      + drC*siA*siB*(siCt-siC))
412            inner_sum += inner_w * Fq**2
413        outer_sum += outer_w * inner_sum
414    Iq = outer_sum / 4  # = outer*um*zm*8.0/(4.0*M_PI)
415    return Iq/Iq[0]
416
417def check_shape(shape, fn=None):
418    rho_solvent = 0
419    q = np.logspace(-3, 0, 200)
420    r = shape.r_bins(q, r_step=0.01)
421    sampling_density = 15000 / shape.volume()
422    rho, points = shape.sample(sampling_density)
423    Pr = calc_Pr(r, rho-rho_solvent, points)
424    Iq = calc_Iq(q, r, Pr)
425    theory = (q, fn(q)) if fn is not None else None
426
427    import pylab
428    #plot_points(rho, points); pylab.figure()
429    plot_calc(r, Pr, q, Iq, theory=theory)
430    pylab.show()
431
432def check_cylinder(radius=25, length=125, rho=2.):
433    shape = EllipticalCylinder(radius, radius, length, rho)
434    fn = lambda q: cylinder_Iq(q, radius, length)
435    check_shape(shape, fn)
436
437def check_sphere(radius=125, rho=2):
438    shape = TriaxialEllipsoid(radius, radius, radius, rho)
439    fn = lambda q: sphere_Iq(q, radius)
440    check_shape(shape, fn)
441
442def check_csbox(a=10, b=20, c=30, da=1, db=2, dc=3, slda=1, sldb=2, sldc=3, sld_core=4):
443    core = Box(a, b, c, sld_core)
444    side_a = Box(da, b, c, slda, center=((a+da)/2, 0, 0))
445    side_b = Box(a, db, c, sldb, center=(0, (b+db)/2, 0))
446    side_c = Box(a, b, dc, sldc, center=(0, 0, (c+dc)/2))
447    side_a2 = copy(side_a).shift(-a-da, 0, 0)
448    side_b2 = copy(side_b).shift(0, -b-db, 0)
449    side_c2 = copy(side_c).shift(0, 0, -c-dc)
450    shape = Composite((core, side_a, side_b, side_c, side_a2, side_b2, side_c2))
451    fn = lambda q: csbox_Iq(q, a, b, c, da, db, dc, slda, sldb, sldc, sld_core)
452    check_shape(shape, fn)
453
454if __name__ == "__main__":
455    check_cylinder(radius=10, length=40)
456    #check_sphere()
457    #check_csbox()
458    #check_csbox(da=100, db=200, dc=300)
Note: See TracBrowser for help on using the repository browser.