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

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

support for pure python models without polydispersity

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