source: sasmodels/sasmodels/multiscat.py @ 802c412

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

lint reduction

  • Property mode set to 100644
File size: 25.5 KB
Line 
1#!/usr/bin/env python
2r"""
3Multiple scattering calculator
4
5Calculate multiple scattering using 2D FFT convolution.
6
7Usage:
8
9    -p, --probability: the scattering probability
10    -q, --qmax: that max q that you care about
11    -w, --window: the extension window (q is calculated for qmax*window)
12    -n, --nq: the number of mesh points (dq = qmax*window/nq)
13    -r, --random: generate a random parameter set
14    -2, --2d: perform the calculation for an oriented pattern
15    model_name
16    model_par=value ...
17
18Assume the probability of scattering is $p$. After each scattering event,
19$1-p$ neutrons will leave the system and go to the detector, and the remaining
20$p$ will scatter again.
21
22Let the scattering probability for $n$ scattering event at $q$ be $f_n(q)$,
23where
24.. math:: f_1(q) = \frac{I_1(q)}{\int I_1(q) dq}
25for $I_1(q)$, the single scattering from the system. After two scattering
26events, the scattering probability will be the convolution of the first
27scattering and itself, or $f_2(q) = (f_1*f_1)(q)$.  After $n$ events it will be
28$f_n(q) = (f_1 * \cdots * f_1)(q)$.  The total scattering is calculated
29as the weighted sum of $f_k$, with weights following the Poisson distribution
30.. math:: P(k; \lambda) = \frac{\lambda^k e^{-\lambda}}{k!}
31for $\lambda$ determined by the total thickness divided by the mean
32free path between scattering, giving
33.. math::
34    I(q) = \sum_{k=0}^\infty P(k; \lambda) f_k(q)
35The $k=0$ term is ignored since it involves no scattering.
36We cut the series when cumulative probability is less than cutoff $C=99\%$,
37which is $\max n$ such that
38.. math::
39    \frac{\sum_{k=1}^n \frac{P(k; \lambda)}{1 - P(0; \lambda)} < C
40
41Using the convolution theorem, where
42$F = \mathcal{F}(f)$ is the Fourier transform,
43.. math:: f * g = \mathcal{F}^{-1}\{\mathcal{F}\{f\} \cdot \mathcal{F}\{g\}\}
44so
45.. math:: f * \ldots * f = \mathcal{F}^{-1}\{ F^n \}
46Since the Fourier transform is a linear operator, we can move the polynomial
47expression for the convolution into the transform, giving
48.. math::
49    I(q) = \mathcal{F}^{-1}\left\{ \sum_{k=1}^{n} P(k; \lambda) F^k \right\}
50In the dilute limit $L \rightarrow 0$ only the $k=1$ term is active,
51and so
52.. math::
53    P(1; \lambda) = \lambda e{-\lambda} = \int I_1(q) dq
54therefore we compute
55.. math::
56    I(q) = \int I_1(q) dq \mathcal{F}^{-1}\left\{
57        \sum_{l=1}^{n} \frac{P(k; \lambda)}{P(1; \lambda))} F^k \right\}
58
59For speed we may use the fast fourier transform with a power of two.
60The resulting $I(q)$ will be linearly spaced and likely heavily oversampled.
61The usual pinhole or slit resolution calculation can performed from these
62calculated values.
63
64By default single precision OpenCL is used for the calculation.  Set the
65environment variable *SAS_OPENCL=none* to use double precision numpy FFT
66instead.  The OpenCL versions is about 10x faster on an elderly Mac with
67Intel HD 4000 graphics.  The single precision numerical artifacts don't
68seem to seriously impact overall accuracy, though they look pretty bad.
69"""
70
71from __future__ import print_function, division
72
73import argparse
74import time
75import os.path
76
77import numpy as np
78from numpy import pi
79from scipy.special import gamma
80
81from sasmodels import core
82from sasmodels import compare
83from sasmodels import resolution2d
84from sasmodels.resolution import Resolution, bin_edges
85from sasmodels.data import empty_data1D, empty_data2D, plot_data
86from sasmodels.direct_model import call_kernel
87import sasmodels.kernelcl
88
89# TODO: select fast and accurate fft library
90# clFFT: https://github.com/clMathLibraries/clFFT (AMD's OpenCL)
91# - gpyfft: https://github.com/geggo/gpyfft (wraps clFFT)
92# - arrayfire: https://github.com/arrayfire (wraps clFFT and much more; +cuda)
93# pyFFT: https://github.com/fjarri-attic/pyfft (based on Apple's OpenCL; +cuda)
94# - Reikna: https://github.com/fjarri/reikna (evolved from pyfft)
95# genFFT: https://software.intel.com/en-us/articles/genFFT (Intel's OpenCL)
96# VexCL: https://vexcl.readthedocs.io/en/latest/ (c++ library)
97# TODO: switch to fftw when opencl is not available
98
99try:
100    import pyfft.cl
101    import pyopencl.array as cl_array
102    HAVE_OPENCL = sasmodels.kernelcl.use_opencl()
103except ImportError:
104    HAVE_OPENCL = False
105PRECISION = np.dtype('f' if HAVE_OPENCL else 'd')  # 'f' or 'd'
106USE_FAST = True  # OpenCL faster, less accurate math
107
108class NumpyCalculator:
109    def __init__(self, dims=None, dtype=PRECISION):
110        self.dtype = dtype
111        self.complex_dtype = np.dtype('F') if dtype == np.dtype('f') else np.dtype('D')
112        pass
113
114    def fft(self, Iq):
115        #t0 = time.time()
116        Iq = np.asarray(Iq, self.dtype)
117        result = np.fft.fft2(Iq)
118        #print("fft time", time.time()-t0)
119        return result
120
121    def ifft(self, fourier_frame):
122        #t0 = time.time()
123        fourier_frame = np.asarray(fourier_frame, self.complex_dtype)
124        result = np.fft.ifft2(fourier_frame)
125        #print("ifft time", time.time()-t0)
126        return result
127
128    def multiple_scattering(self, Iq, p, coverage=0.99):
129        r"""
130        Compute multiple scattering for I(q) given scattering probability p.
131
132        Given a probability p of scattering with the thickness, the expected
133        number of scattering events, $\lambda$ is $-\log(1 - p)$, giving a
134        Poisson weighted sum of single, double, triple, etc. scattering patterns.
135        The number of patterns used is based on coverage (default 99%).
136        """
137        #t0 = time.time()
138        coeffs = scattering_coeffs(p, coverage)
139        poly = np.asarray(coeffs[::-1], dtype=self.dtype)
140        scale = np.sum(Iq)
141        frame = _forward_shift(Iq/scale, dtype=self.dtype)
142        F = np.fft.fft2(frame)
143        F_convolved = F * np.polyval(poly, F)
144        frame = np.fft.ifft2(F_convolved)
145        result = scale * _inverse_shift(frame.real, dtype=self.dtype)
146        #print("numpy multiscat time", time.time()-t0)
147        return result
148
149# polyval1(c, x) computes (...((c0 x + c1) x + c2) x ... + cn) x
150# where c is an array of length *degree* and x is an array of
151# complex values (type double2) of length *n*. 2-D arrays can of
152# course be treated as 1-D arrays of length *nx* X *ny*.
153# When compiling with sasmodels.kernelcl.compile_model the double precision
154# types are converted to single precision as needed.  See the code in
155# sasmodels.generate._convert_type for details.
156POLYVAL1_KERNEL = """
157kernel void polyval1(
158    const int degree,
159    global const double *coeff,
160    const int n,
161    global double2 *array)
162{
163    int index = get_global_id(0);
164    if (index < n) {
165        const double2 x = array[index];
166        double2 total = coeff[0];
167        for (int k=1; k < degree; k++) {
168            total = fma(total, x, coeff[k]);
169        }
170        array[index] = total * x;
171    }
172}
173"""
174
175class OpenclCalculator(NumpyCalculator):
176    polyval1f = None
177    polyval1d = None
178    def __init__(self, dims, dtype=PRECISION):
179        env = sasmodels.kernelcl.environment()
180        context = env.get_context(dtype)
181        if dtype == np.dtype('f'):
182            if self.polyval1f is None:
183                program = sasmodels.kernelcl.compile_model(
184                    context, POLYVAL1_KERNEL, dtype, fast=USE_FAST)
185                # Assume context is always the same for a given dtype
186                OpenclCalculator.polyval1f = program.polyval1
187            self.dtype = dtype
188            self.complex_dtype = np.dtype('F')
189            self.polyval1 = self.polyval1f
190        else:
191            if self.polyval1d is None:
192                program = sasmodels.kernelcl.compile_model(
193                    context, POLYVAL1_KERNEL, dtype, fast=False)
194                # Assume context is always the same for a given dtype
195                OpenclCalculator.polyval1d = program.polyval1
196            self.dtype = dtype
197            self.complex_type = np.dtype('D')
198            self.polyval1 = self.polyval1d
199        self.queue = env.get_queue(dtype)
200        self.plan = pyfft.cl.Plan(dims, queue=self.queue)
201
202    def fft(self, Iq):
203        # forward transform
204        #t0 = time.time()
205        data = np.asarray(Iq, self.complex_dtype)
206        gpu_data = cl_array.to_device(self.queue, data)
207        self.plan.execute(gpu_data.data)
208        result = gpu_data.get()
209        #print("fft time", time.time()-t0)
210        return result
211
212    def ifft(self, fourier_frame):
213        # inverse transform
214        #t0 = time.time()
215        data = np.asarray(fourier_frame, self.complex_dtype)
216        gpu_data = cl_array.to_device(self.queue, data)
217        self.plan.execute(gpu_data.data, inverse=True)
218        result = gpu_data.get()
219        #print("ifft time", time.time()-t0)
220        return result
221
222    def multiple_scattering(self, Iq, p, coverage=0.99):
223        #t0 = time.time()
224        coeffs = scattering_coeffs(p, coverage)
225        scale = np.sum(Iq)
226        poly = np.asarray(coeffs[::-1], self.dtype)
227        frame = _forward_shift(Iq/scale, dtype=self.complex_dtype)
228        gpu_data = cl_array.to_device(self.queue, frame)
229        gpu_poly = cl_array.to_device(self.queue, poly)
230        self.plan.execute(gpu_data.data)
231        degree, n = poly.shape[0], frame.shape[0]*frame.shape[1]
232        self.polyval1(
233            self.queue, [n], None,
234            np.int32(degree), gpu_poly.data, np.int32(n), gpu_data.data)
235        self.plan.execute(gpu_data.data, inverse=True)
236        frame = gpu_data.get()
237        #result = scale * _inverse_shift(frame.real, dtype=self.dtype)
238        result = scale * _inverse_shift(frame.real, dtype=self.dtype)
239        #print("OpenCL multiscat time", time.time()-t0)
240        return result
241
242Calculator = OpenclCalculator if HAVE_OPENCL else NumpyCalculator
243
244def scattering_powers(Iq, n, dtype='f', transform=None):
245    r"""
246    Calculate the scattering powers up to n.
247
248    This includes 1 even though it should just be Iq itself
249
250    The frames are unweighted; to weight scale by $\lambda^k e^{-\lambda}/k!$.
251    """
252    if transform is None:
253        nx, ny = Iq.shape
254        transform = Calculator(dims=(nx*2, ny*2), dtype=dtype)
255    scale = np.sum(Iq)
256    frame = _forward_shift(Iq/scale, dtype=dtype)
257    F = transform.fft(frame)
258    powers = [scale * _inverse_shift(transform.ifft(F**(k+1)).real, dtype=dtype)
259              for k in range(n)]
260    return powers
261
262def scattering_coeffs(p, coverage=0.99):
263    r"""
264    Return the coefficients of the scattering powers for transmission
265    probability *p*.  This is just the corresponding values for the
266    Poisson distribution for $\lambda = -\ln(1-p)$ such that
267    $\sum_{k = 0 \ldots n} P(k; \lambda)$ is larger than *coverage*.
268    """
269    L = -np.log(1-p)
270    num_scatter = truncated_poisson_invcdf(coverage, L)
271    coeffs = [L**k/gamma(k+2) for k in range(num_scatter)]
272    return coeffs
273
274def truncated_poisson_invcdf(coverage, L):
275    r"""
276    Return smallest k such that cdf(k; L) > coverage from the truncated Poisson
277    probability excluding k=0
278    """
279    # pmf(k; L) = L**k * exp(-L) / (k! * (1-exp(-L))
280    cdf = 0
281    pmf = -np.exp(-L) / np.expm1(-L)
282    k = 0
283    while cdf < coverage:
284        k += 1
285        pmf *= L/k
286        cdf += pmf
287    return k
288
289def _forward_shift(Iq, dtype=PRECISION):
290    # Prepare padded array and forward transform
291    nq = Iq.shape[0]
292    half_nq = nq//2
293    frame = np.zeros((2*nq, 2*nq), dtype=dtype)
294    frame[:half_nq, :half_nq] = Iq[half_nq:, half_nq:]
295    frame[-half_nq:, :half_nq] = Iq[:half_nq, half_nq:]
296    frame[:half_nq, -half_nq:] = Iq[half_nq:, :half_nq]
297    frame[-half_nq:, -half_nq:] = Iq[:half_nq, :half_nq]
298    return frame
299
300def _inverse_shift(frame, dtype=PRECISION):
301    # Invert the transform and recover the transformed data
302    nq = frame.shape[0]//2
303    half_nq = nq//2
304    Iq = np.empty((nq, nq), dtype=dtype)
305    Iq[half_nq:, half_nq:] = frame[:half_nq, :half_nq]
306    Iq[:half_nq, half_nq:] = frame[-half_nq:, :half_nq]
307    Iq[half_nq:, :half_nq] = frame[:half_nq, -half_nq:]
308    Iq[:half_nq, :half_nq] = frame[-half_nq:, -half_nq:]
309    return Iq
310
311
312class MultipleScattering(Resolution):
313    r"""
314    Compute multiple scattering using Fourier convolution.
315
316    The fourier steps are determined by *qmax*, the maximum $q$ value
317    desired, *nq* the number of $q$ steps and *window*, the amount
318    of padding around the circular convolution.  The $q$ spacing
319    will be $\Delta q = 2 q_\mathrm{max} w / n_q$.  If *nq* is not
320    given it will use $n_q = 2^k$ such that $\Delta q < q_\mathrm{min}$.
321
322    *probability* is related to the expected number of scattering
323    events in the sample $\lambda$ as $p = 1 = e^{-\lambda}$.  As a
324    hack to allow probability to be a fitted parameter, the "value"
325    can be a function that takes no parameters and returns the current
326    value of the probability.  *coverage* determines how many scattering
327    steps to consider.  The default is 0.99, which sets $n$ such that
328    $1 \ldots n$ covers 99% of the Poisson probability mass function.
329
330    *is2d* is True then 2D scattering is used, otherwise it accepts
331    and returns 1D scattering.
332
333    *resolution* is the resolution function to apply after multiple
334    scattering.  If present, then the resolution $q$ vectors will provide
335    default values for *qmin*, *qmax* and *nq*.
336    """
337    def __init__(self, qmin=None, qmax=None, nq=None, window=2,
338                 probability=None, coverage=0.99,
339                 is2d=False, resolution=None,
340                 dtype=PRECISION):
341        # Infer qmin, qmax from instrument resolution calculator, if present
342        if resolution is not None:
343            is2d = hasattr(resolution, 'qx_data')
344            if is2d:
345                # 2D data
346                if qmax is None:
347                    qx_calc, qy_calc = resolution.q_calc
348                    qmax = np.sqrt(np.max(qx_calc**2 + qy_calc**2))
349                if qmin is None and nq is None:
350                    qx, qy = resolution.data.x_bins, resolution.data.y_bins
351                    if qx and qy:
352                        dx = (np.max(qx) - np.min(qx)) / len(qx)
353                        dy = (np.max(qy) - np.min(qy)) / len(qy)
354                    else:
355                        qx, qy = resolution.data.qx_data, resolution.data.qy_data
356                        steps = np.sqrt(len(qx))
357                        dx = (np.max(qx) - np.min(qx)) / steps
358                        dy = (np.max(qy) - np.min(qy)) / steps
359                    qmin = min(dx, dy)
360            else:
361                # 1D data
362                if qmax is None:
363                    qmax = np.max(resolution.q_calc)
364                if qmin is None and nq is None:
365                    qmin = np.min(np.abs(resolution.q_calc))
366
367        # estimate nq from qmin, qmax if not given explicitly
368        q_range = qmax * window
369        if nq is None:
370            nq = 2**np.ceil(np.log2(q_range/qmin))
371        nq = int(nq)
372        # Compute available qmin based on nq
373        qmin = 2*q_range / nq
374        #print(nq)
375
376        # remember input parameters
377        self.qmax = qmax
378        self.qmin = qmin
379        self.nq = nq
380        self.probability = probability
381        self.coverage = coverage
382        self.is2d = is2d
383        self.window = window
384        self.resolution = resolution
385
386        # Determine the q values to calculate
387        q = np.linspace(-q_range, q_range, nq)
388        qx, qy = np.meshgrid(q, q)
389        if is2d:
390            q_calc = (qx.flatten(), qy.flatten())
391        else:
392            # For 1-D patterns, compute q from the center to the corners and
393            # interpolate from there into the individual pixels.  Given that
394            # nq represents the points in [-qmax*windows, qmax*window],
395            # then using 2*sqrt(2)*nq/2 will oversample the points in q by
396            # a factor of two relative to the pixels.
397            q_range_to_corner = np.sqrt(2.) * q_range
398            nq_to_corner = 10*int(np.ceil(np.sqrt(2.) * nq))
399            q_to_corner = np.linspace(0, q_range_to_corner, nq_to_corner+1)[1:]
400            q_calc = (q_to_corner,)
401            # Remember the q radii of the calculated points
402            self._radius = np.sqrt(qx**2 + qy**2)
403            #self._q = q_to_corner
404        self._q_steps = q
405        self.q_calc = q_calc
406
407        # TODO: use cleaner data representation than that from sasview
408        # Resolution function forwards underlying q data (for plotting, etc?)
409        if is2d:
410            if resolution is not None:
411                # forward resolution function info to multiscattering
412                self.qx_data = resolution.qx_data
413                self.qy_data = resolution.qy_data
414            else:
415                # no underlying resolution function, but make it look like there is
416                self.qx_data, self.qy_data = q_calc
417        else:
418            # 1-D radial profile is determined by the q values we need to
419            # compute, either for the calculated q values for the resolution
420            # function (if any) or for the raw q values desired
421            self._q = np.linspace(qmin, qmax, nq//(2*window))
422            self._edges = bin_edges(self._q)
423            self._norm, _ = np.histogram(self._radius, bins=self._edges)
424            if resolution is not None:
425                self.q = resolution.q
426            else:
427                # no underlying resolution function, but make it look like there is
428                self.q = self._q
429
430        # Prepare the multiple scattering calculator (either numpy or OpenCL)
431        self.transform = Calculator((2*nq, 2*nq), dtype=dtype)
432
433        # Iq and Iqxy will be set during apply
434        self.Iq = None # type: np.ndarray
435        self.Iqxy = None # type: np.ndarray
436
437    def apply(self, theory):
438        if self.is2d:
439            Iq_calc = theory
440        else:
441            Iq_calc = np.interp(self._radius, self.q_calc[0], theory)
442        Iq_calc = Iq_calc.reshape(self.nq, self.nq)
443
444        probability = self.probability() if callable(self.probability) else self.probability
445        coverage = self.coverage
446        #t0 = time.time()
447        Iqxy = self.transform.multiple_scattering(Iq_calc, probability, coverage)
448        #print("multiple scattering calc time", time.time()-t0)
449
450        # remember the intermediate result in case we want to see it later
451        self.Iqxy = Iqxy
452
453        if self.is2d:
454            if self.resolution is not None:
455                Iqxy = self.resolution.apply(Iqxy)
456            return Iqxy
457        else:
458            # remember the intermediate result in case we want to see it later
459            Iq = self.radial_profile(Iqxy)
460            self.Iq = Iq
461            if self.resolution is not None:
462                q = self._q
463                Iq_res = np.interp(np.abs(self.resolution.q_calc), q, self.Iq)
464                _ = """
465                k = 6
466                print("q theory", q[:k])
467                print("Iq theory", theory[:k])
468                print("interp NaN?", np.any(np.isnan(Iq_calc)))
469                print("convolved NaN?", np.any(np.isnan(Iqxy)))
470                print("Iq intgrated", self.Iq[:k])
471                print("radius", self._radius[self.nq/2,self.nq/2:self.nq/2+k])
472                print("q edges", self._edges[:k+1])
473                print("Iq norm", self._norm[:k])
474                print("res q", self.resolution.q_calc[:k])
475                print("Iq res", Iq_res[:k])
476                #print(Iq)
477                #print(Iq_res)
478                """
479                Iq = self.resolution.apply(Iq_res)
480            return Iq
481
482    def radial_profile(self, Iqxy):
483        """
484        Compute that radial profile for the given Iqxy grid.  The grid should
485        be defined as for
486        """
487        # circular average, no anti-aliasing
488        Iq = np.histogram(self._radius, bins=self._edges, weights=Iqxy)[0]/self._norm
489        return Iq
490
491
492def annular_average(qxy, Iqxy, qbins):
493    """
494    Compute annular average of points in *Iqxy* at *qbins*.  The $q_x$, $q_y$
495    coordinates for *Iqxy* are given in *qxy*.
496    """
497    qxy, Iqxy = qxy.flatten(), Iqxy.flatten()
498    index = np.argsort(qxy)
499    qxy, Iqxy = qxy[index], Iqxy[index]
500    print(qxy.shape, Iqxy.shape, index.shape, qbins.shape)
501    #values = rebin(np.vstack((0., qxy)), Iqxy, qbins)
502    integral = np.cumsum(Iqxy)
503    Io = np.diff(np.interp(qbins, qxy, integral, left=0.))
504    # normalize by area of annulus
505    # TODO: doesn't properly account for box.
506    # Need to chop off chords that are greater than the box edges
507    # (left, right, top and bottom), then add back in the corners
508    # chopped off by both. https://en.wikipedia.org/wiki/Circular_segment
509    norms = np.diff(pi*qbins**2)
510    return Io/norms
511
512def rebin(x, I, xo):
513    """
514    Rebin from edges *x*, bins I into edges *xo*.
515
516    *x* and *xo* should be monotonically increasing.
517
518    If *x* has duplicate values, then all corresponding values at I(x) will
519    be effectively summed into the same bin.  If *xo* has duplicate values,
520    the first bin will contain the entire contents and subsequent bins will
521    contain zeros.
522    """
523    integral = np.cumsum(I)
524    Io = np.diff(np.interp(xo, x[1:], integral, left=0.))
525    return Io
526
527
528def parse_pars(model, opts):
529    # type: (ModelInfo, argparse.Namespace) -> Dict[str, float]
530
531    seed = np.random.randint(1000000) if opts.random and opts.seed < 0 else opts.seed
532    compare_opts = {
533        'info': (model.info, model.info),
534        'use_demo': False,
535        'seed': seed,
536        'mono': True,
537        'magnetic': False,
538        'values': opts.pars,
539        #'show_pars': True,
540        'show_pars': False,
541        'is2d': opts.is2d,
542    }
543    pars, pars2 = compare.parse_pars(compare_opts)
544    return pars
545
546
547def main():
548    parser = argparse.ArgumentParser(
549        description="Compute multiple scattering",
550        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
551        )
552    parser.add_argument('-p', '--probability', type=float, default=0.1, help="scattering probability")
553    parser.add_argument('-n', '--nq', type=int, default=1024, help='number of mesh points')
554    parser.add_argument('-q', '--qmax', type=float, default=0.5, help='max q')
555    parser.add_argument('-w', '--window', type=float, default=2.0, help='q calc = q max * window')
556    parser.add_argument('-2', '--2d', dest='is2d', action='store_true', help='oriented sample')
557    parser.add_argument('-s', '--seed', default=-1, help='random pars with given seed')
558    parser.add_argument('-r', '--random', action='store_true', help='random pars with random seed')
559    parser.add_argument('-o', '--outfile', type=str, default="", help='random pars with random seed')
560    parser.add_argument('model', type=str, help='sas model name such as cylinder')
561    parser.add_argument('pars', type=str, nargs='*', help='model parameters such as radius=30')
562    opts = parser.parse_args()
563    assert opts.nq%2 == 0, "require even # points"
564
565    model = core.load_model(opts.model)
566    pars = parse_pars(model, opts)
567    res = MultipleScattering(qmax=opts.qmax, nq=opts.nq, window=opts.window,
568                             probability=opts.probability, is2d=opts.is2d)
569    kernel = model.make_kernel(res.q_calc)
570    #print(pars)
571    bg = pars.get('background', 0.0)
572    pars['background'] = 0.0
573    theory = call_kernel(kernel, pars)
574    Iq = res.apply(theory) + bg
575    plot_and_save_powers(res, theory, Iq, outfile=opts.outfile, background=bg)
576
577def plot_and_save_powers(res, theory, result, plot=True, outfile="", background=0.):
578    import pylab
579    probability, coverage = res.probability, res.coverage
580    weights = scattering_coeffs(probability, coverage)
581
582    # cribbed from MultipleScattering.apply
583    if res.is2d:
584        Iq_calc = theory
585    else:
586        Iq_calc = np.interp(res._radius, res.q_calc[0], theory)
587    Iq_calc = Iq_calc.reshape(res.nq, res.nq)
588
589    # Compute the scattering powers for 1, 2, ... n scattering events
590    powers = scattering_powers(Iq_calc, len(weights))
591
592    #plotxy(Iqxy); import pylab; pylab.figure()
593    if res.is2d:
594        if outfile:
595            data = np.vstack([Ipower.flatten() for Ipower in powers]).T
596            np.savetxt(outfile + "_powers.txt", data)
597            data = np.vstack(Iq_calc).T
598            np.savetxt(outfile + ".txt", data)
599        if plot:
600            plotxy((res._q_steps, res._q_steps), Iq_calc)
601            pylab.title("single scattering F")
602            for k, v in enumerate(powers[1:]):
603                pylab.figure()
604                plotxy((res._q_steps, res._q_steps), v+background)
605                pylab.title("multiple scattering F^%d" % (k+2))
606            pylab.figure()
607            plotxy((res._q_steps, res._q_steps), res.Iqxy+background)
608            pylab.title("total scattering for p=%g" % probability)
609    else:
610        q = res._q
611        Iq_powers = [res.radial_profile(Iqxy) for Iqxy in powers]
612        if outfile:
613            data = np.vstack([q, theory, res.Iq]).T
614            np.savetxt(outfile + ".txt", data)
615            # circular average, no anti-aliasing for individual powers
616            data = np.vstack([q] + Iq_powers).T
617            np.savetxt(outfile + "_powers.txt", data)
618        if plot:
619            # Plot 2D pattern for total scattering
620            plotxy((res._q_steps, res._q_steps), res.Iqxy+background)
621            pylab.title("total scattering for p=%g" % probability)
622            pylab.figure()
623
624            # Plot 1D pattern for partial scattering
625            pylab.loglog(q, res.Iq+background, label="total for p=%g"%probability)
626            #new_annulus = annular_average(res._radius, res.Iqxy, res._edges)
627            #pylab.loglog(q, new_annulus+background, label="new total for p=%g"%probability)
628            for n, (w, Ipower) in enumerate(zip(weights, Iq_powers)):
629                pylab.loglog(q, w*Ipower+background, label="scattering^%d"%(n+1))
630            pylab.legend()
631            pylab.title('total scattering for p=%g' % probability)
632    pylab.show()
633
634def plotxy(q, Iq):
635    import pylab
636    # q is a tuple of (q,) or (qx, qy)
637    if len(q) == 1:
638        pylab.loglog(q[0], Iq)
639    else:
640        data = Iq.copy()
641        data[Iq <= 0] = np.min(Iq[Iq > 0])/2
642        pylab.imshow(np.log10(data))
643
644if __name__ == "__main__":
645    main()
Note: See TracBrowser for help on using the repository browser.