source: sasmodels/sasmodels/kernelpy.py @ 119bd3d

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 119bd3d was 119bd3d, checked in by wojciech, 8 years ago

kernelpy.py clean-up

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