source: sasmodels/sasmodels/kernelpy.py @ e62a134

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

add type hinting to model_test

  • Property mode set to 100644
File size: 11.6 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 . import details
13from .generate import F64
14
15try:
16    from typing import Union
17except:
18    pass
19else:
20    DType = Union[None, str, np.dtype]
21
22class PyModel(object):
23    """
24    Wrapper for pure python models.
25    """
26    def __init__(self, model_info):
27        # Make sure Iq and Iqxy are available and vectorized
28        _create_default_functions(model_info)
29        self.info = model_info
30
31    def make_kernel(self, q_vectors):
32        q_input = PyInput(q_vectors, dtype=F64)
33        kernel = self.info.Iqxy if q_input.is_2d else self.info.Iq
34        return PyKernel(kernel, self.info, q_input)
35
36    def release(self):
37        """
38        Free resources associated with the model.
39        """
40        pass
41
42class PyInput(object):
43    """
44    Make q data available to the gpu.
45
46    *q_vectors* is a list of q vectors, which will be *[q]* for 1-D data,
47    and *[qx, qy]* for 2-D data.  Internally, the vectors will be reallocated
48    to get the best performance on OpenCL, which may involve shifting and
49    stretching the array to better match the memory architecture.  Additional
50    points will be evaluated with *q=1e-3*.
51
52    *dtype* is the data type for the q vectors. The data type should be
53    set to match that of the kernel, which is an attribute of
54    :class:`GpuProgram`.  Note that not all kernels support double
55    precision, so even if the program was created for double precision,
56    the *GpuProgram.dtype* may be single precision.
57
58    Call :meth:`release` when complete.  Even if not called directly, the
59    buffer will be released when the data object is freed.
60    """
61    def __init__(self, q_vectors, dtype):
62        self.nq = q_vectors[0].size
63        self.dtype = dtype
64        self.is_2d = (len(q_vectors) == 2)
65        if self.is_2d:
66            self.q = np.empty((self.nq, 2), dtype=dtype)
67            self.q[:, 0] = q_vectors[0]
68            self.q[:, 1] = q_vectors[1]
69        else:
70            self.q = np.empty(self.nq, dtype=dtype)
71            self.q[:self.nq] = q_vectors[0]
72
73    def release(self):
74        """
75        Free resources associated with the model inputs.
76        """
77        self.q = None
78
79class PyKernel(object):
80    """
81    Callable SAS kernel.
82
83    *kernel* is the DllKernel object to call.
84
85    *model_info* is the module information
86
87    *q_input* is the DllInput q vectors at which the kernel should be
88    evaluated.
89
90    The resulting call method takes the *pars*, a list of values for
91    the fixed parameters to the kernel, and *pd_pars*, a list of (value,weight)
92    vectors for the polydisperse parameters.  *cutoff* determines the
93    integration limits: any points with combined weight less than *cutoff*
94    will not be calculated.
95
96    Call :meth:`release` when done with the kernel instance.
97    """
98    def __init__(self, kernel, model_info, q_input):
99        self.dtype = np.dtype('d')
100        self.info = model_info
101        self.q_input = q_input
102        self.res = np.empty(q_input.nq, q_input.dtype)
103        self.kernel = kernel
104        self.dim = '2d' if q_input.is_2d else '1d'
105
106        partable = model_info.parameters
107        kernel_parameters = (partable.iqxy_parameters if q_input.is_2d
108                             else partable.iq_parameters)
109        volume_parameters = partable.form_volume_parameters
110
111        # Create an array to hold the parameter values.  There will be a
112        # single array whose values are updated as the calculator goes
113        # through the loop.  Arguments to the kernel and volume functions
114        # will use views into this vector, relying on the fact that a
115        # an array of no dimensions acts like a scalar.
116        parameter_vector = np.empty(len(partable.call_parameters)-2, 'd')
117
118        # Create views into the array to hold the arguments
119        offset = 0
120        kernel_args, volume_args = [], []
121        for p in partable.kernel_parameters:
122            if p.length == 1:
123                # Scalar values are length 1 vectors with no dimensions.
124                v = parameter_vector[offset:offset+1].reshape(())
125            else:
126                # Vector values are simple views.
127                v = parameter_vector[offset:offset+p.length]
128            offset += p.length
129            if p in kernel_parameters:
130                kernel_args.append(v)
131            if p in volume_parameters:
132                volume_args.append(v)
133
134        # Hold on to the parameter vector so we can use it to call kernel later.
135        # This may also be required to preserve the views into the vector.
136        self._parameter_vector = parameter_vector
137
138        # Generate a closure which calls the kernel with the views into the
139        # parameter array.
140        if q_input.is_2d:
141            form = model_info.Iqxy
142            qx, qy = q_input.q[:,0], q_input.q[:,1]
143            self._form = lambda: form(qx, qy, *kernel_args)
144        else:
145            form = model_info.Iq
146            q = q_input.q
147            self._form = lambda: form(q, *kernel_args)
148
149        # Generate a closure which calls the form_volume if it exists.
150        form_volume = model_info.form_volume
151        self._volume = ((lambda: form_volume(*volume_args)) if form_volume
152                        else (lambda: 1.0))
153
154    def __call__(self, call_details, weights, values, cutoff):
155        assert isinstance(call_details, details.CallDetails)
156        res = _loops(self._parameter_vector, self._form, self._volume,
157                     self.q_input.nq, call_details, weights, values, cutoff)
158        return res
159
160    def release(self):
161        """
162        Free resources associated with the kernel.
163        """
164        self.q_input = None
165
166def _loops(parameters,  # type: np.ndarray
167           form,        # type: Callable[[], np.ndarray]
168           form_volume, # type: Callable[[], float]
169           nq,          # type: int
170           call_details,# type: details.CallDetails
171           weights,     # type: np.ndarray
172           values,      # type: np.ndarray
173           cutoff,      # type: float
174           ):           # type: (...) -> None
175    ################################################################
176    #                                                              #
177    #   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!   #
178    #   !!                                                    !!   #
179    #   !!  KEEP THIS CODE CONSISTENT WITH KERNEL_TEMPLATE.C  !!   #
180    #   !!                                                    !!   #
181    #   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!   #
182    #                                                              #
183    ################################################################
184    parameters[:] = values[call_details.par_offset]
185    scale, background = values[0], values[1]
186    if call_details.num_active == 0:
187        norm = float(form_volume())
188        if norm > 0.0:
189            return (scale/norm)*form() + background
190        else:
191            return np.ones(nq, 'd')*background
192
193    partial_weight = np.NaN
194    spherical_correction = 1.0
195    pd_stride = call_details.pd_stride[:call_details.num_active]
196    pd_length = call_details.pd_length[:call_details.num_active]
197    pd_offset = call_details.pd_offset[:call_details.num_active]
198    pd_index = np.empty_like(pd_offset)
199    offset = np.empty_like(call_details.par_offset)
200    theta = call_details.theta_par
201    fast_length = pd_length[0]
202    pd_index[0] = fast_length
203    total = np.zeros(nq, 'd')
204    norm = 0.0
205    for loop_index in range(call_details.total_pd):
206        # update polydispersity parameter values
207        if pd_index[0] == fast_length:
208            pd_index[:] = (loop_index/pd_stride)%pd_length
209            partial_weight = np.prod(weights[pd_offset+pd_index][1:])
210            for k in range(call_details.num_coord):
211                par = call_details.par_coord[k]
212                coord = call_details.pd_coord[k]
213                this_offset = call_details.par_offset[par]
214                block_size = 1
215                for bit in range(len(pd_offset)):
216                    if coord&1:
217                        this_offset += block_size * pd_index[bit]
218                        block_size *= pd_length[bit]
219                    coord >>= 1
220                    if coord == 0: break
221                offset[par] = this_offset
222                parameters[par] = values[this_offset]
223                if par == theta and not (call_details.par_coord[k]&1):
224                    spherical_correction = max(abs(cos(pi/180 * parameters[theta])), 1e-6)
225        for k in range(call_details.num_coord):
226            if call_details.pd_coord[k]&1:
227                #par = call_details.par_coord[k]
228                parameters[par] = values[offset[par]]
229                #print "par",par,offset[par],parameters[par+2]
230                offset[par] += 1
231                if par == theta:
232                    spherical_correction = max(abs(cos(pi/180 * parameters[theta])), 1e-6)
233
234        weight = partial_weight * weights[pd_offset[0] + pd_index[0]]
235        pd_index[0] += 1
236        if weight > cutoff:
237            # Call the scattering function
238            # Assume that NaNs are only generated if the parameters are bad;
239            # exclude all q for that NaN.  Even better would be to have an
240            # INVALID expression like the C models, but that is too expensive.
241            I = form()
242            if np.isnan(I).any(): continue
243
244            # update value and norm
245            weight *= spherical_correction
246            total += weight * I
247            norm += weight * form_volume()
248
249    if norm > 0.0:
250        return (scale/norm)*total + background
251    else:
252        return np.ones(nq, 'd')*background
253
254
255def _create_default_functions(model_info):
256    """
257    Autogenerate missing functions, such as Iqxy from Iq.
258
259    This only works for Iqxy when Iq is written in python. :func:`make_source`
260    performs a similar role for Iq written in C.  This also vectorizes
261    any functions that are not already marked as vectorized.
262    """
263    _create_vector_Iq(model_info)
264    _create_vector_Iqxy(model_info)  # call create_vector_Iq() first
265
266
267def _create_vector_Iq(model_info):
268    """
269    Define Iq as a vector function if it exists.
270    """
271    Iq = model_info.Iq
272    if callable(Iq) and not getattr(Iq, 'vectorized', False):
273        #print("vectorizing Iq")
274        def vector_Iq(q, *args):
275            """
276            Vectorized 1D kernel.
277            """
278            return np.array([Iq(qi, *args) for qi in q])
279        vector_Iq.vectorized = True
280        model_info.Iq = vector_Iq
281
282def _create_vector_Iqxy(model_info):
283    """
284    Define Iqxy as a vector function if it exists, or default it from Iq().
285    """
286    Iq, Iqxy = model_info.Iq, model_info.Iqxy
287    if callable(Iqxy):
288        if not getattr(Iqxy, 'vectorized', False):
289            #print("vectorizing Iqxy")
290            def vector_Iqxy(qx, qy, *args):
291                """
292                Vectorized 2D kernel.
293                """
294                return np.array([Iqxy(qxi, qyi, *args) for qxi, qyi in zip(qx, qy)])
295            vector_Iqxy.vectorized = True
296            model_info.Iqxy = vector_Iqxy
297    elif callable(Iq):
298        #print("defaulting Iqxy")
299        # Iq is vectorized because create_vector_Iq was already called.
300        def default_Iqxy(qx, qy, *args):
301            """
302            Default 2D kernel.
303            """
304            return Iq(np.sqrt(qx**2 + qy**2), *args)
305        default_Iqxy.vectorized = True
306        model_info.Iqxy = default_Iqxy
307
Note: See TracBrowser for help on using the repository browser.