source: sasmodels/sasmodels/kernelpy.py @ 46d9f48

core_shell_microgelscostrafo411magnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 46d9f48 was 46d9f48, checked in by jhbakker, 7 years ago

Hankel trafo optimized. Proper garbage collection for Linux users in
kernelpy. Hankel matrix constructor moved from
sasview/qsmearing to sasmodels/sesans. Sesans code cleaned up.

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