source: sasmodels/sasmodels/kernelpy.py @ 40a87fa

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

lint and latex cleanup

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