source: sasmodels/sasmodels/kernelpy.py @ 062c56d

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 062c56d was 062c56d, checked in by Paul Kienzle <pkienzle@…>, 8 years ago

allow negative I(q) in models

  • Property mode set to 100644
File size: 9.5 KB
Line 
1import numpy as np
2from numpy import pi, cos
3
4from .generate import F64
5
6class PyModel(object):
7    def __init__(self, info):
8        self.info = info
9
10    def __call__(self, q_input):
11        kernel = self.info['Iqxy'] if q_input.is_2D else self.info['Iq']
12        return PyKernel(kernel, self.info, q_input)
13
14    # pylint: disable=no-self-use
15    def make_input(self, q_vectors):
16        return PyInput(q_vectors, dtype=F64)
17
18    def release(self):
19        pass
20
21class PyInput(object):
22    """
23    Make q data available to the gpu.
24
25    *q_vectors* is a list of q vectors, which will be *[q]* for 1-D data,
26    and *[qx, qy]* for 2-D data.  Internally, the vectors will be reallocated
27    to get the best performance on OpenCL, which may involve shifting and
28    stretching the array to better match the memory architecture.  Additional
29    points will be evaluated with *q=1e-3*.
30
31    *dtype* is the data type for the q vectors. The data type should be
32    set to match that of the kernel, which is an attribute of
33    :class:`GpuProgram`.  Note that not all kernels support double
34    precision, so even if the program was created for double precision,
35    the *GpuProgram.dtype* may be single precision.
36
37    Call :meth:`release` when complete.  Even if not called directly, the
38    buffer will be released when the data object is freed.
39    """
40    def __init__(self, q_vectors, dtype):
41        self.nq = q_vectors[0].size
42        self.dtype = dtype
43        self.is_2D = (len(q_vectors) == 2)
44        self.q_vectors = [np.ascontiguousarray(q, self.dtype) for q in q_vectors]
45        self.q_pointers = [q.ctypes.data for q in self.q_vectors]
46
47    def release(self):
48        self.q_vectors = []
49
50class PyKernel(object):
51    """
52    Callable SAS kernel.
53
54    *kernel* is the DllKernel object to call.
55
56    *info* is the module information
57
58    *q_input* is the DllInput q vectors at which the kernel should be
59    evaluated.
60
61    The resulting call method takes the *pars*, a list of values for
62    the fixed parameters to the kernel, and *pd_pars*, a list of (value,weight)
63    vectors for the polydisperse parameters.  *cutoff* determines the
64    integration limits: any points with combined weight less than *cutoff*
65    will not be calculated.
66
67    Call :meth:`release` when done with the kernel instance.
68    """
69    def __init__(self, kernel, info, q_input):
70        self.info = info
71        self.q_input = q_input
72        self.res = np.empty(q_input.nq, q_input.dtype)
73        dim = '2d' if q_input.is_2D else '1d'
74        # Loop over q unless user promises that the kernel is vectorized by
75        # taggining it with vectorized=True
76        if not getattr(kernel, 'vectorized', False):
77            if dim == '2d':
78                def vector_kernel(qx, qy, *args):
79                    return np.array([kernel(qxi, qyi, *args)
80                                     for qxi, qyi in zip(qx, qy)])
81            else:
82                def vector_kernel(q, *args):
83                    return np.array([kernel(qi, *args)
84                                     for qi in q])
85            self.kernel = vector_kernel
86        else:
87            self.kernel = kernel
88        fixed_pars = info['partype']['fixed-' + dim]
89        pd_pars = info['partype']['pd-' + dim]
90        vol_pars = info['partype']['volume']
91
92        # First two fixed pars are scale and background
93        pars = [p[0] for p in info['parameters'][2:]]
94        offset = len(self.q_input.q_vectors)
95        self.args = self.q_input.q_vectors + [None] * len(pars)
96        self.fixed_index = np.array([pars.index(p) + offset
97                                     for p in fixed_pars[2:]])
98        self.pd_index = np.array([pars.index(p) + offset
99                                  for p in pd_pars])
100        self.vol_index = np.array([pars.index(p) + offset
101                                   for p in vol_pars])
102        try: self.theta_index = pars.index('theta') + offset
103        except ValueError: self.theta_index = -1
104
105        # Caller needs fixed_pars and pd_pars
106        self.fixed_pars = fixed_pars
107        self.pd_pars = pd_pars
108
109    def __call__(self, fixed, pd, cutoff=1e-5):
110        #print "fixed",fixed
111        #print "pd", pd
112        args = self.args[:]  # grab a copy of the args
113        form, form_volume = self.kernel, self.info['form_volume']
114        # First two fixed
115        scale, background = fixed[:2]
116        for index, value in zip(self.fixed_index, fixed[2:]):
117            args[index] = float(value)
118        res = _loops(form, form_volume, cutoff, scale, background, args,
119                     pd, self.pd_index, self.vol_index, self.theta_index)
120
121        return res
122
123    def release(self):
124        self.q_input = None
125
126def _loops(form, form_volume, cutoff, scale, background,
127           args, pd, pd_index, vol_index, theta_index):
128    """
129    *form* is the name of the form function, which should be vectorized over
130    q, but otherwise have an interface like the opencl kernels, with the
131    q parameters first followed by the individual parameters in the order
132    given in model.parameters (see :mod:`sasmodels.generate`).
133
134    *form_volume* calculates the volume of the shape.  *vol_index* gives
135    the list of volume parameters
136
137    *cutoff* ignores the corners of the dispersion hypercube
138
139    *scale*, *background* multiplies the resulting form and adds an offset
140
141    *args* is the prepopulated set of arguments to the form function, starting
142    with the q vectors, and including slots for all the parameters.  The
143    values for the parameters will be substituted with values from the
144    dispersion functions.
145
146    *pd* is the list of dispersion parameters
147
148    *pd_index* are the indices of the dispersion parameters in *args*
149
150    *vol_index* are the indices of the volume parameters in *args*
151
152    *theta_index* is the index of the theta parameter for the sasview
153    spherical correction, or -1 if there is no angular dispersion
154    """
155
156    ################################################################
157    #                                                              #
158    #   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!   #
159    #   !!                                                    !!   #
160    #   !!  KEEP THIS CODE CONSISTENT WITH KERNEL_TEMPLATE.C  !!   #
161    #   !!                                                    !!   #
162    #   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!   #
163    #                                                              #
164    ################################################################
165
166    weight = np.empty(len(pd), 'd')
167    if weight.size > 0:
168        # weight vector, to be populated by polydispersity loops
169
170        # identify which pd parameters are volume parameters
171        vol_weight_index = np.array([(index in vol_index) for index in pd_index])
172
173        # Sort parameters in decreasing order of pd length
174        Npd = np.array([len(pdi[0]) for pdi in pd], 'i')
175        order = np.argsort(Npd)[::-1]
176        stride = np.cumprod(Npd[order])
177        pd = [pd[index] for index in order]
178        pd_index = pd_index[order]
179        vol_weight_index = vol_weight_index[order]
180
181        fast_value = pd[0][0]
182        fast_weight = pd[0][1]
183    else:
184        stride = np.array([1])
185        vol_weight_index = slice(None, None)
186        # keep lint happy
187        fast_value = [None]
188        fast_weight = [None]
189
190    ret = np.zeros_like(args[0])
191    norm = np.zeros_like(ret)
192    vol = np.zeros_like(ret)
193    vol_norm = np.zeros_like(ret)
194    for k in range(stride[-1]):
195        # update polydispersity parameter values
196        fast_index = k % stride[0]
197        if fast_index == 0:  # bottom loop complete ... check all other loops
198            if weight.size > 0:
199                for i, index, in enumerate(k % stride):
200                    args[pd_index[i]] = pd[i][0][index]
201                    weight[i] = pd[i][1][index]
202        else:
203            args[pd_index[0]] = fast_value[fast_index]
204            weight[0] = fast_weight[fast_index]
205        # This computes the weight, and if it is sufficient, calls the
206        # scattering function and adds it to the total.  If there is a volume
207        # normalization, it will also be added here.
208        # Note: make sure this is consistent with the code in PY_LOOP_BODY!!
209        # Note: can precompute w1*w2*...*wn
210        w = np.prod(weight)
211        if w > cutoff:
212            I = form(*args)
213            #positive = (I >= 0.0)
214
215            # Note: can precompute spherical correction if theta_index is not
216            # the fast index. Correction factor for spherical integration
217            #spherical_correction = abs(cos(pi*args[phi_index])) if phi_index>=0 else 1.0
218            spherical_correction = (abs(cos(pi * args[theta_index])) * pi / 2
219                                    if theta_index >= 0 else 1.0)
220            #spherical_correction = 1.0
221            ret += w * I * spherical_correction #* positive
222            norm += w #* positive
223
224            # Volume normalization.
225            # If there are "volume" polydispersity parameters, then these
226            # will be used to call the form_volume function from the user
227            # supplied kernel, and accumulate a normalized weight.
228            # Note: can precompute volume norm if fast index is not a volume
229            if form_volume:
230                vol_args = [args[index] for index in vol_index]
231                vol_weight = np.prod(weight[vol_weight_index])
232                vol += vol_weight * form_volume(*vol_args) #* positive
233                vol_norm += vol_weight #* positive
234
235    positive = (vol * vol_norm != 0.0)
236    ret[positive] *= vol_norm[positive] / vol[positive]
237    result = scale * ret / norm + background
238    return result
Note: See TracBrowser for help on using the repository browser.