source: sasmodels/sasmodels/kernelpy.py @ b139ee6

core_shell_microgelscostrafo411magnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since b139ee6 was b139ee6, checked in by Paul Kienzle <pkienzle@…>, 7 years ago

create log entry when python model is loaded

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