source: sasmodels/sasmodels/kernelpy.py @ 1e2a1ba

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

Merge remote-tracking branch 'origin/master' into polydisp

Conflicts:

sasmodels/core.py
sasmodels/custom/init.py
sasmodels/direct_model.py
sasmodels/generate.py
sasmodels/kernelpy.py
sasmodels/sasview_model.py

  • Property mode set to 100644
File size: 10.3 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 make_kernel(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        if self.is_2d:
56            self.q = np.empty((self.nq, 2), dtype=dtype)
57            self.q[:, 0] = q_vectors[0]
58            self.q[:, 1] = q_vectors[1]
59        else:
60            self.q = np.empty(self.nq, dtype=dtype)
61            self.q[:self.nq] = q_vectors[0]
62
63    def release(self):
64        """
65        Free resources associated with the model inputs.
66        """
67        self.q = None
68
69class PyKernel(object):
70    """
71    Callable SAS kernel.
72
73    *kernel* is the DllKernel object to call.
74
75    *model_info* is the module information
76
77    *q_input* is the DllInput q vectors at which the kernel should be
78    evaluated.
79
80    The resulting call method takes the *pars*, a list of values for
81    the fixed parameters to the kernel, and *pd_pars*, a list of (value,weight)
82    vectors for the polydisperse parameters.  *cutoff* determines the
83    integration limits: any points with combined weight less than *cutoff*
84    will not be calculated.
85
86    Call :meth:`release` when done with the kernel instance.
87    """
88    def __init__(self, kernel, model_info, q_input):
89        self.info = model_info
90        self.q_input = q_input
91        self.res = np.empty(q_input.nq, q_input.dtype)
92        dim = '2d' if q_input.is_2d else '1d'
93        # Loop over q unless user promises that the kernel is vectorized by
94        # taggining it with vectorized=True
95        if not getattr(kernel, 'vectorized', False):
96            if dim == '2d':
97                def vector_kernel(qx, qy, *args):
98                    """
99                    Vectorized 2D kernel.
100                    """
101                    return np.array([kernel(qxi, qyi, *args)
102                                     for qxi, qyi in zip(qx, qy)])
103            else:
104                def vector_kernel(q, *args):
105                    """
106                    Vectorized 1D kernel.
107                    """
108                    return np.array([kernel(qi, *args)
109                                     for qi in q])
110            self.kernel = vector_kernel
111        else:
112            self.kernel = kernel
113        fixed_pars = model_info['partype']['fixed-' + dim]
114        pd_pars = model_info['partype']['pd-' + dim]
115        vol_pars = model_info['partype']['volume']
116
117        # First two fixed pars are scale and background
118        pars = [p.name for p in model_info['parameters'][2:]]
119        offset = len(self.q_input.q_vectors)
120        self.args = self.q_input.q_vectors + [None] * len(pars)
121        self.fixed_index = np.array([pars.index(p) + offset
122                                     for p in fixed_pars[2:]])
123        self.pd_index = np.array([pars.index(p) + offset
124                                  for p in pd_pars])
125        self.vol_index = np.array([pars.index(p) + offset
126                                   for p in vol_pars])
127        try: self.theta_index = pars.index('theta') + offset
128        except ValueError: self.theta_index = -1
129
130        # Caller needs fixed_pars and pd_pars
131        self.fixed_pars = fixed_pars
132        self.pd_pars = pd_pars
133
134    def __call__(self, fixed, pd, cutoff=1e-5):
135        #print("fixed",fixed)
136        #print("pd", pd)
137        args = self.args[:]  # grab a copy of the args
138        form, form_volume = self.kernel, self.info['form_volume']
139        # First two fixed
140        scale, background = fixed[:2]
141        for index, value in zip(self.fixed_index, fixed[2:]):
142            args[index] = float(value)
143        res = _loops(form, form_volume, cutoff, scale, background, args,
144                     pd, self.pd_index, self.vol_index, self.theta_index)
145
146        return res
147
148    def release(self):
149        """
150        Free resources associated with the kernel.
151        """
152        self.q_input = None
153
154def _loops(form, form_volume, cutoff, scale, background,
155           args, pd, pd_index, vol_index, theta_index):
156    """
157    *form* is the name of the form function, which should be vectorized over
158    q, but otherwise have an interface like the opencl kernels, with the
159    q parameters first followed by the individual parameters in the order
160    given in model.parameters (see :mod:`sasmodels.generate`).
161
162    *form_volume* calculates the volume of the shape.  *vol_index* gives
163    the list of volume parameters
164
165    *cutoff* ignores the corners of the dispersion hypercube
166
167    *scale*, *background* multiplies the resulting form and adds an offset
168
169    *args* is the prepopulated set of arguments to the form function, starting
170    with the q vectors, and including slots for all the parameters.  The
171    values for the parameters will be substituted with values from the
172    dispersion functions.
173
174    *pd* is the list of dispersion parameters
175
176    *pd_index* are the indices of the dispersion parameters in *args*
177
178    *vol_index* are the indices of the volume parameters in *args*
179
180    *theta_index* is the index of the theta parameter for the sasview
181    spherical correction, or -1 if there is no angular dispersion
182    """
183
184    ################################################################
185    #                                                              #
186    #   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!   #
187    #   !!                                                    !!   #
188    #   !!  KEEP THIS CODE CONSISTENT WITH KERNEL_TEMPLATE.C  !!   #
189    #   !!                                                    !!   #
190    #   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!   #
191    #                                                              #
192    ################################################################
193
194    weight = np.empty(len(pd), 'd')
195    if weight.size > 0:
196        # weight vector, to be populated by polydispersity loops
197
198        # identify which pd parameters are volume parameters
199        vol_weight_index = np.array([(index in vol_index) for index in pd_index])
200
201        # Sort parameters in decreasing order of pd length
202        Npd = np.array([len(pdi[0]) for pdi in pd], 'i')
203        order = np.argsort(Npd)[::-1]
204        stride = np.cumprod(Npd[order])
205        pd = [pd[index] for index in order]
206        pd_index = pd_index[order]
207        vol_weight_index = vol_weight_index[order]
208
209        fast_value = pd[0][0]
210        fast_weight = pd[0][1]
211    else:
212        stride = np.array([1])
213        vol_weight_index = slice(None, None)
214        # keep lint happy
215        fast_value = [None]
216        fast_weight = [None]
217
218    ret = np.zeros_like(args[0])
219    norm = np.zeros_like(ret)
220    vol = np.zeros_like(ret)
221    vol_norm = np.zeros_like(ret)
222    for k in range(stride[-1]):
223        # update polydispersity parameter values
224        fast_index = k % stride[0]
225        if fast_index == 0:  # bottom loop complete ... check all other loops
226            if weight.size > 0:
227                for i, index, in enumerate(k % stride):
228                    args[pd_index[i]] = pd[i][0][index]
229                    weight[i] = pd[i][1][index]
230        else:
231            args[pd_index[0]] = fast_value[fast_index]
232            weight[0] = fast_weight[fast_index]
233
234        # Computes the weight, and if it is not sufficient then ignore this
235        # parameter set.
236        # Note: could precompute w1*...*wn so we only need to multiply by w0
237        w = np.prod(weight)
238        if w > cutoff:
239            # Note: can precompute spherical correction if theta_index is not
240            # the fast index. Correction factor for spherical integration
241            #spherical_correction = abs(cos(pi*args[phi_index])) if phi_index>=0 else 1.0
242            spherical_correction = (abs(cos(pi * args[theta_index])) * pi / 2
243                                    if theta_index >= 0 else 1.0)
244            #spherical_correction = 1.0
245
246            # Call the scattering function and adds it to the total.
247            I = form(*args)
248            if np.isnan(I).any(): continue
249            ret += w * I * spherical_correction
250            norm += w
251
252            # Volume normalization.
253            # If there are "volume" polydispersity parameters, then these
254            # will be used to call the form_volume function from the user
255            # supplied kernel, and accumulate a normalized weight.
256            # Note: can precompute volume norm if fast index is not a volume
257            if form_volume:
258                vol_args = [args[index] for index in vol_index]
259                vol_weight = np.prod(weight[vol_weight_index])
260                vol += vol_weight * form_volume(*vol_args)
261                vol_norm += vol_weight
262
263    positive = (vol * vol_norm != 0.0)
264    ret[positive] *= vol_norm[positive] / vol[positive]
265    result = scale * ret / norm + background
266    return result
Note: See TracBrowser for help on using the repository browser.