source: sasmodels/sasmodels/details.py @ baa79c2

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

lint

  • Property mode set to 100644
File size: 10.3 KB
Line 
1"""
2Kernel Call Details
3===================
4
5When calling sas computational kernels with polydispersity there are a
6number of details that need to be sent to the caller.  This includes the
7list of polydisperse parameters, the number of points in the polydispersity
8weight distribution, and which parameter is the "theta" parameter for
9polar coordinate integration.  The :class:`CallDetails` object maintains
10this data.  Use :func:`build_details` to build a *details* object which
11can be passed to one of the computational kernels.
12"""
13
14from __future__ import print_function
15
16import numpy as np  # type: ignore
17from numpy import pi, cos, sin
18
19try:
20    np.meshgrid([])
21    meshgrid = np.meshgrid
22except ValueError:
23    # CRUFT: np.meshgrid requires multiple vectors
24    def meshgrid(*args):
25        if len(args) > 1:
26            return np.meshgrid(*args)
27        else:
28            return [np.asarray(v) for v in args]
29
30try:
31    from typing import List
32except ImportError:
33    pass
34else:
35    from .modelinfo import ModelInfo
36
37
38class CallDetails(object):
39    """
40    Manage the polydispersity information for the kernel call.
41
42    Conceptually, a polydispersity calculation is an integral over a mesh
43    in n-D space where n is the number of polydisperse parameters.  In order
44    to keep the program responsive, and not crash the GPU, only a portion
45    of the mesh is computed at a time.  Meshes with a large number of points
46    will therefore require many calls to the polydispersity loop.  Restarting
47    a nested loop in the middle requires that the indices of the individual
48    mesh dimensions can be computed for the current loop location.  This
49    is handled by the *pd_stride* vector, with n//stride giving the loop
50    index and n%stride giving the position in the sub loops.
51
52    One of the parameters may be the latitude.  When integrating in polar
53    coordinates, the total circumference decreases as latitude varies from
54    pi r^2 at the equator to 0 at the pole, and the weight associated
55    with a range of phi values needs to be scaled by this circumference.
56    This scale factor needs to be updated each time the theta value
57    changes.  *theta_par* indicates which of the values in the parameter
58    vector is the latitude parameter, or -1 if there is no latitude
59    parameter in the model.  In practice, the normalization term cancels
60    if the latitude is not a polydisperse parameter.
61    """
62    parts = None  # type: List["CallDetails"]
63    def __init__(self, model_info):
64        # type: (ModelInfo) -> None
65        parameters = model_info.parameters
66        max_pd = parameters.max_pd
67
68        # Structure of the call details buffer:
69        #   pd_par[max_pd]     pd params in order of length
70        #   pd_length[max_pd]  length of each pd param
71        #   pd_offset[max_pd]  offset of pd values in parameter array
72        #   pd_stride[max_pd]  index of pd value in loop = n//stride[k]
73        #   pd_prod            total length of pd loop
74        #   pd_sum             total length of the weight vector
75        #   num_active         number of pd params
76        #   theta_par          parameter number for theta parameter
77        self.buffer = np.zeros(4*max_pd + 4, 'i4')
78
79        # generate views on different parts of the array
80        self._pd_par = self.buffer[0 * max_pd:1 * max_pd]
81        self._pd_length = self.buffer[1 * max_pd:2 * max_pd]
82        self._pd_offset = self.buffer[2 * max_pd:3 * max_pd]
83        self._pd_stride = self.buffer[3 * max_pd:4 * max_pd]
84
85        # theta_par is fixed
86        self.theta_par = parameters.theta_offset
87
88    @property
89    def pd_par(self):
90        """List of polydisperse parameters"""
91        return self._pd_par
92
93    @property
94    def pd_length(self):
95        """Number of weights for each polydisperse parameter"""
96        return self._pd_length
97
98    @property
99    def pd_offset(self):
100        """Offsets for the individual weight vectors in the set of weights"""
101        return self._pd_offset
102
103    @property
104    def pd_stride(self):
105        """Stride in the pd mesh for each pd dimension"""
106        return self._pd_stride
107
108    @property
109    def pd_prod(self):
110        """Total size of the pd mesh"""
111        return self.buffer[-4]
112
113    @pd_prod.setter
114    def pd_prod(self, v):
115        self.buffer[-4] = v
116
117    @property
118    def pd_sum(self):
119        """Total length of all the weight vectors"""
120        return self.buffer[-3]
121
122    @pd_sum.setter
123    def pd_sum(self, v):
124        self.buffer[-3] = v
125
126    @property
127    def num_active(self):
128        """Number of active polydispersity loops"""
129        return self.buffer[-2]
130
131    @num_active.setter
132    def num_active(self, v):
133        self.buffer[-2] = v
134
135    @property
136    def theta_par(self):
137        """Location of the theta parameter in the parameter vector"""
138        return self.buffer[-1]
139
140    @theta_par.setter
141    def theta_par(self, v):
142        self.buffer[-1] = v
143
144    def show(self):
145        """Print the polydispersity call details to the console"""
146        print("num_active", self.num_active)
147        print("pd_prod", self.pd_prod)
148        print("pd_sum", self.pd_sum)
149        print("theta par", self.theta_par)
150        print("pd_par", self.pd_par)
151        print("pd_length", self.pd_length)
152        print("pd_offset", self.pd_offset)
153        print("pd_stride", self.pd_stride)
154
155
156def mono_details(model_info):
157    # type: (ModelInfo) -> CallDetails
158    """
159    Return a :class:`CallDetails` object for a monodisperse calculation
160    of the model defined by *model_info*.
161    """
162    call_details = CallDetails(model_info)
163    call_details.pd_prod = 1
164    call_details.pd_sum = model_info.parameters.nvalues
165    call_details.pd_par[:] = np.arange(0, model_info.parameters.max_pd)
166    call_details.pd_length[:] = 1
167    call_details.pd_offset[:] = np.arange(0, model_info.parameters.max_pd)
168    call_details.pd_stride[:] = 1
169    return call_details
170
171
172def poly_details(model_info, weights):
173    """
174    Return a :class:`CallDetails` object for a polydisperse calculation
175    of the model defined by *model_info* for the given set of *weights*.
176    *weights* is a list of vectors, one for each parameter.  Monodisperse
177    parameters should provide a weight vector containing [1.0].
178    """
179    # type: (ModelInfo) -> CallDetails
180    #print("weights",weights)
181    #weights = weights[2:] # Skip scale and background
182
183    # Decreasing list of polydispersity lengths
184    #print([p.id for p in model_info.parameters.call_parameters])
185    pd_length = np.array([len(w)
186                          for w in weights[2:2+model_info.parameters.npars]])
187    num_active = np.sum(pd_length>1)
188    max_pd = model_info.parameters.max_pd
189    if num_active > max_pd:
190        raise ValueError("Too many polydisperse parameters")
191
192    pd_offset = np.cumsum(np.hstack((0, pd_length)))
193    #print(", ".join(str(i)+"-"+p.id for i,p in enumerate(model_info.parameters.call_parameters)))
194    #print("len:",pd_length)
195    #print("off:",pd_offset)
196    # Note: the reversing view, x[::-1], does not require a copy
197    idx = np.argsort(pd_length)[::-1][:max_pd]
198    pd_stride = np.cumprod(np.hstack((1, pd_length[idx])))
199
200    call_details = CallDetails(model_info)
201    call_details.pd_par[:max_pd] = idx
202    call_details.pd_length[:max_pd] = pd_length[idx]
203    call_details.pd_offset[:max_pd] = pd_offset[idx]
204    call_details.pd_stride[:max_pd] = pd_stride[:-1]
205    call_details.pd_prod = pd_stride[-1]
206    call_details.pd_sum = sum(len(w) for w in weights)
207    call_details.num_active = num_active
208    #call_details.show()
209    return call_details
210
211
212def dispersion_mesh(model_info, pars):
213    # type: (ModelInfo) -> Tuple[List[np.ndarray], List[np.ndarray]]
214    """
215    Create a mesh grid of dispersion parameters and weights.
216
217    Returns [p1,p2,...],w where pj is a vector of values for parameter j
218    and w is a vector containing the products for weights for each
219    parameter set in the vector.
220    """
221    value, weight = zip(*pars)
222    weight = [w if w else [1.] for w in weight]
223    weight = np.vstack([v.flatten() for v in meshgrid(*weight)])
224    weight = np.prod(weight, axis=0)
225    value = [v.flatten() for v in meshgrid(*value)]
226    lengths = [par.length for par in model_info.parameters.kernel_parameters
227               if par.type == 'volume']
228    if any(n > 1 for n in lengths):
229        pars = []
230        offset = 0
231        for n in lengths:
232            pars.append(np.vstack(value[offset:offset+n])
233                        if n > 1 else value[offset])
234            offset += n
235        value = pars
236    return value, weight
237
238
239
240def build_details(kernel, pairs):
241    # type: (Kernel, Tuple[List[np.ndarray], List[np.ndarray]]) -> Tuple[CallDetails, np.ndarray, bool]
242    """
243    Converts (value, weight) pairs into parameters for the kernel call.
244
245    Returns a CallDetails object indicating the polydispersity, a data object
246    containing the different values, and the magnetic flag indicating whether
247    any magnetic magnitudes are non-zero. Magnetic vectors (M0, phi, theta) are
248    converted to rectangular coordinates (mx, my, mz).
249    """
250    values, weights = zip(*pairs)
251    scalars = [v[0] for v in values]
252    if all(len(w)==1 for w in weights):
253        call_details = mono_details(kernel.info)
254        # Pad value array to a 32 value boundary
255        data_len = 3*len(scalars)
256        extra = ((data_len+31)//32)*32 - data_len
257        data = np.array(scalars+scalars+[1.]*len(scalars)+[0.]*extra,
258                        dtype=kernel.dtype)
259    else:
260        call_details = poly_details(kernel.info, weights)
261        # Pad value array to a 32 value boundary
262        data_len = len(scalars) + 2*sum(len(v) for v in values)
263        extra = ((data_len+31)//32)*32 - data_len
264        data = np.hstack(scalars+list(values)+list(weights)+[0.]*extra)
265        data = data.astype(kernel.dtype)
266    is_magnetic = convert_magnetism(kernel.info.parameters, data)
267    #call_details.show()
268    return call_details, data, is_magnetic
269
270def convert_magnetism(parameters, values):
271    """
272    Convert magnetism values from polar to rectangular coordinates.
273
274    Returns True if any magnetism is present.
275    """
276    mag = values[parameters.nvalues-3*parameters.nmagnetic:parameters.nvalues]
277    mag = mag.reshape(-1, 3)
278    M0 = mag[:,0]
279    if np.any(M0):
280        theta, phi = mag[:,1]*pi/180., mag[:,2]*pi/180.
281        cos_theta = cos(theta)
282        mx = M0*cos_theta*cos(phi)
283        my = M0*sin(theta)
284        mz = -M0*cos_theta*sin(phi)
285        mag[:,0], mag[:,1], mag[:,2] = mx, my, mz
286        return True
287    else:
288        return False
Note: See TracBrowser for help on using the repository browser.