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

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

reenable python models

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