source: sasmodels/sasmodels/kernelpy.py @ f734e7d

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since f734e7d was f734e7d, checked in by pkienzle, 9 years ago

restructure c code generation for maintainability; extend test harness to allow opencl and ctypes tests

  • Property mode set to 100644
File size: 9.2 KB
Line 
1import numpy as np
2from numpy import pi, sin, cos, sqrt
3
4from .generate import F32, F64
5
6class PyModel(object):
7    def __init__(self, info):
8        self.info = info
9    def __call__(self, input):
10        kernel = self.info['Iqxy'] if input.is_2D else self.info['Iq']
11        return PyKernel(kernel, self.info, input)
12    def make_input(self, q_vectors):
13        return PyInput(q_vectors, dtype=F64)
14    def release(self):
15        pass
16
17class PyInput(object):
18    """
19    Make q data available to the gpu.
20
21    *q_vectors* is a list of q vectors, which will be *[q]* for 1-D data,
22    and *[qx, qy]* for 2-D data.  Internally, the vectors will be reallocated
23    to get the best performance on OpenCL, which may involve shifting and
24    stretching the array to better match the memory architecture.  Additional
25    points will be evaluated with *q=1e-3*.
26
27    *dtype* is the data type for the q vectors. The data type should be
28    set to match that of the kernel, which is an attribute of
29    :class:`GpuProgram`.  Note that not all kernels support double
30    precision, so even if the program was created for double precision,
31    the *GpuProgram.dtype* may be single precision.
32
33    Call :meth:`release` when complete.  Even if not called directly, the
34    buffer will be released when the data object is freed.
35    """
36    def __init__(self, q_vectors, dtype):
37        self.nq = q_vectors[0].size
38        self.dtype = dtype
39        self.is_2D = (len(q_vectors) == 2)
40        self.q_vectors = [np.ascontiguousarray(q,self.dtype) for q in q_vectors]
41        self.q_pointers = [q.ctypes.data for q in q_vectors]
42
43    def release(self):
44        self.q_vectors = []
45
46class PyKernel(object):
47    """
48    Callable SAS kernel.
49
50    *kernel* is the DllKernel object to call.
51
52    *info* is the module information
53
54    *input* is the DllInput q vectors at which the kernel should be
55    evaluated.
56
57    The resulting call method takes the *pars*, a list of values for
58    the fixed parameters to the kernel, and *pd_pars*, a list of (value,weight)
59    vectors for the polydisperse parameters.  *cutoff* determines the
60    integration limits: any points with combined weight less than *cutoff*
61    will not be calculated.
62
63    Call :meth:`release` when done with the kernel instance.
64    """
65    def __init__(self, kernel, info, input):
66        self.info = info
67        self.input = input
68        self.res = np.empty(input.nq, input.dtype)
69        dim = '2d' if input.is_2D else '1d'
70        # Loop over q unless user promises that the kernel is vectorized by
71        # taggining it with vectorized=True
72        if not getattr(kernel, 'vectorized', False):
73            if dim == '2d':
74                def vector_kernel(qx, qy, *args):
75                    return np.array([kernel(qxi,qyi,*args) for qxi,qyi in zip(qx,qy)])
76            else:
77                def vector_kernel(q, *args):
78                    return np.array([kernel(qi,*args) for qi in q])
79            self.kernel = vector_kernel
80        else:
81            self.kernel = kernel
82        fixed_pars = info['partype']['fixed-'+dim]
83        pd_pars = info['partype']['pd-'+dim]
84        vol_pars = info['partype']['volume']
85
86        # First two fixed pars are scale and background
87        pars = [p[0] for p in info['parameters'][2:]]
88        offset = len(self.input.q_vectors)
89        self.args = self.input.q_vectors + [None]*len(pars)
90        self.fixed_index = np.array([pars.index(p)+offset for p in fixed_pars[2:] ])
91        self.pd_index = np.array([pars.index(p)+offset for p in pd_pars])
92        self.vol_index = np.array([pars.index(p)+offset for p in vol_pars])
93        try: self.theta_index = pars.index('theta')+offset
94        except ValueError: self.theta_index = -1
95
96        # Caller needs fixed_pars and pd_pars
97        self.fixed_pars = fixed_pars
98        self.pd_pars = pd_pars
99
100    def __call__(self, fixed, pd, cutoff=1e-5):
101        #print "fixed",fixed
102        #print "pd", pd
103        args = self.args[:]  # grab a copy of the args
104        form, form_volume = self.kernel, self.info['form_volume']
105        # First two fixed
106        scale, background = fixed[:2]
107        for index,value in zip(self.fixed_index, fixed[2:]):
108            args[index] = float(value)
109        res = _loops(form, form_volume, cutoff, scale, background,  args,
110                     pd, self.pd_index, self.vol_index, self.theta_index)
111
112        return res
113
114    def release(self):
115        self.input = None
116
117def _loops(form, form_volume, cutoff, scale, background,
118           args, pd, pd_index, vol_index, theta_index):
119    """
120    *form* is the name of the form function, which should be vectorized over
121    q, but otherwise have an interface like the opencl kernels, with the
122    q parameters first followed by the individual parameters in the order
123    given in model.parameters (see :mod:`sasmodels.generate`).
124
125    *form_volume* calculates the volume of the shape.  *vol_index* gives
126    the list of volume parameters
127
128    *cutoff* ignores the corners of the dispersion hypercube
129
130    *scale*, *background* multiplies the resulting form and adds an offset
131
132    *args* is the prepopulated set of arguments to the form function, starting
133    with the q vectors, and including slots for all the parameters.  The
134    values for the parameters will be substituted with values from the
135    dispersion functions.
136
137    *pd* is the list of dispersion parameters
138
139    *pd_index* are the indices of the dispersion parameters in *args*
140
141    *vol_index* are the indices of the volume parameters in *args*
142
143    *theta_index* is the index of the theta parameter for the sasview
144    spherical correction, or -1 if there is no angular dispersion
145    """
146
147    ################################################################
148    #                                                              #
149    #   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!   #
150    #   !!                                                    !!   #
151    #   !!  KEEP THIS CODE CONSISTENT WITH KERNEL_TEMPLATE.C  !!   #
152    #   !!                                                    !!   #
153    #   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!   #
154    #                                                              #
155    ################################################################
156
157    weight = np.empty(len(pd), 'd')
158    if weight.size > 0:
159        # weight vector, to be populated by polydispersity loops
160
161        # identify which pd parameters are volume parameters
162        vol_weight_index = np.array([(index in vol_index) for index in pd_index])
163
164        # Sort parameters in decreasing order of pd length
165        Npd = np.array([len(pdi[0]) for pdi in pd], 'i')
166        order = np.argsort(Npd)[::-1]
167        stride = np.cumprod(Npd[order])
168        pd = [pd[index] for index in order]
169        pd_index = pd_index[order]
170        vol_weight_index = vol_weight_index[order]
171
172        fast_value = pd[0][0]
173        fast_weight = pd[0][1]
174    else:
175        stride = np.array([1])
176        vol_weight_index = slice(None, None)
177        # keep lint happy
178        fast_value = [None]
179        fast_weight = [None]
180
181    ret = np.zeros_like(args[0])
182    norm = np.zeros_like(ret)
183    vol = np.zeros_like(ret)
184    vol_norm = np.zeros_like(ret)
185    for k in range(stride[-1]):
186        # update polydispersity parameter values
187        fast_index = k%stride[0]
188        if fast_index == 0:  # bottom loop complete ... check all other loops
189            if weight.size > 0:
190                for i,index, in enumerate(k%stride):
191                    args[pd_index[i]] = pd[i][0][index]
192                    weight[i] = pd[i][1][index]
193        else:
194            args[pd_index[0]] = fast_value[fast_index]
195            weight[0] = fast_weight[fast_index]
196        # This computes the weight, and if it is sufficient, calls the scattering
197        # function and adds it to the total.  If there is a volume normalization,
198        # it will also be added here.
199        # Note: make sure this is consistent with the code in PY_LOOP_BODY!!
200        # Note: can precompute w1*w2*...*wn
201        w = np.prod(weight)
202        if w > cutoff:
203            I = form(*args)
204            positive = (I>=0.0)
205
206            # Note: can precompute spherical correction if theta_index is not the fast index
207            # Correction factor for spherical integration p(theta) I(q) sin(theta) dtheta
208            #spherical_correction = abs(sin(pi*args[theta_index])) if theta_index>=0 else 1.0
209            spherical_correction = abs(cos(pi*args[theta_index]))*pi/2 if theta_index>=0 else 1.0
210            #spherical_correction = 1.0
211            ret += w*I*spherical_correction*positive
212            norm += w*positive
213
214            # Volume normalization.
215            # If there are "volume" polydispersity parameters, then these will be used
216            # to call the form_volume function from the user supplied kernel, and accumulate
217            # a normalized weight.
218            # Note: can precompute volume norm if the fast index is not a volume index
219            if form_volume:
220                vol_args = [args[index] for index in vol_index]
221                vol_weight = np.prod(weight[vol_weight_index])
222                vol += vol_weight*form_volume(*vol_args)*positive
223                vol_norm += vol_weight*positive
224
225    positive = (vol*vol_norm != 0.0)
226    ret[positive] *= vol_norm[positive] / vol[positive]
227    result = scale*ret/norm+background
228    return result
Note: See TracBrowser for help on using the repository browser.