source: sasmodels/sasmodels/kernelpy.py @ 6d6508e

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

refactor model_info from dictionary to class

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