source: sasmodels/sasmodels/kernelpy.py @ 3b32f8d

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

move python kernel load log message to a less noisy location

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