source: sasmodels/sasmodels/sasview_model.py @ fb5914f

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

bind sasview to new parameter table definition

  • Property mode set to 100644
File size: 13.6 KB
Line 
1"""
2Sasview model constructor.
3
4Given a module defining an OpenCL kernel such as sasmodels.models.cylinder,
5create a sasview model class to run that kernel as follows::
6
7    from sasmodels.sasview_model import make_class
8    from sasmodels.models import cylinder
9    CylinderModel = make_class(cylinder, dtype='single')
10
11The model parameters for sasmodels are different from those in sasview.
12When reloading previously saved models, the parameters should be converted
13using :func:`sasmodels.convert.convert`.
14"""
15
16import math
17from copy import deepcopy
18import collections
19
20import numpy as np
21
22from . import core
23from . import weights
24
25def standard_models():
26    return [make_class(model_name) for model_name in core.list_models()]
27
28# TODO: rename to make_class_from_name and update sasview
29def make_class(model_name):
30    """
31    Load the sasview model defined in *kernel_module*.
32
33    Returns a class that can be used directly as a sasview model.t
34    """
35    model_info = core.load_model_info(model_name)
36    return make_class_from_info(model_info)
37
38def make_class_from_file(path):
39    model_info = core.load_model_info_from_path(path)
40    return make_class_from_info(model_info)
41
42def make_class_from_info(model_info):
43    def __init__(self, multfactor=1):
44        SasviewModel.__init__(self)
45    attrs = dict(__init__=__init__, _model_info=model_info)
46    ConstructedModel = type(model_info['name'], (SasviewModel,), attrs)
47    return ConstructedModel
48
49class SasviewModel(object):
50    """
51    Sasview wrapper for opencl/ctypes model.
52    """
53    def __init__(self):
54        self._model = None
55        model_info = self._model_info
56        parameters = model_info['parameters']
57
58        self.name = model_info['name']
59        self.description = model_info['description']
60        self.category = None
61        #self.is_multifunc = False
62        for p in parameters.kernel_parameters:
63            if p.is_control:
64                profile_axes = model_info['profile_axes']
65                self.multiplicity_info = [
66                    p.limits[1], p.name, p.choices, profile_axes[0]
67                    ]
68                break
69        else:
70            self.multiplicity_info = []
71
72        ## interpret the parameters
73        ## TODO: reorganize parameter handling
74        self.details = dict()
75        self.params = collections.OrderedDict()
76        self.dispersion = dict()
77
78        self.orientation_params = []
79        self.magnetic_params = []
80        self.fixed = []
81        for p in parameters.user_parameters():
82            self.params[p.name] = p.default
83            self.details[p.name] = [p.units] + p.limits
84            if p.polydisperse:
85                self.dispersion[p.name] = {
86                    'width': 0,
87                    'npts': 35,
88                    'nsigmas': 3,
89                    'type': 'gaussian',
90                }
91            if p.type == 'orientation':
92                self.orientation_params.append(p.name)
93                self.orientation_params.append(p.name+".width")
94                self.fixed.append(p.name+".width")
95            if p.type == 'magnetic':
96                self.orientation_params.append(p.name)
97                self.magnetic_params.append(p.name)
98                self.fixed.append(p.name+".width")
99
100        self.non_fittable = []
101
102        ## independent parameter name and unit [string]
103        self.input_name = model_info.get("input_name", "Q")
104        self.input_unit = model_info.get("input_unit", "A^{-1}")
105        self.output_name = model_info.get("output_name", "Intensity")
106        self.output_unit = model_info.get("output_unit", "cm^{-1}")
107
108        ## _persistency_dict is used by sas.perspectives.fitting.basepage
109        ## to store dispersity reference.
110        ## TODO: _persistency_dict to persistency_dict throughout sasview
111        self._persistency_dict = {}
112
113        ## New fields introduced for opencl rewrite
114        self.cutoff = 1e-5
115
116    def __get_state__(self):
117        state = self.__dict__.copy()
118        state.pop('_kernel')
119        # May need to reload model info on set state since it has pointers
120        # to python implementations of Iq, etc.
121        #state.pop('_model_info')
122        return state
123
124    def __set_state__(self, state):
125        self.__dict__ = state
126        self._model = None
127
128    def __str__(self):
129        """
130        :return: string representation
131        """
132        return self.name
133
134    def is_fittable(self, par_name):
135        """
136        Check if a given parameter is fittable or not
137
138        :param par_name: the parameter name to check
139        """
140        return par_name.lower() in self.fixed
141        #For the future
142        #return self.params[str(par_name)].is_fittable()
143
144
145    # pylint: disable=no-self-use
146    def getProfile(self):
147        """
148        Get SLD profile
149
150        : return: (z, beta) where z is a list of depth of the transition points
151                beta is a list of the corresponding SLD values
152        """
153        return None, None
154
155    def setParam(self, name, value):
156        """
157        Set the value of a model parameter
158
159        :param name: name of the parameter
160        :param value: value of the parameter
161
162        """
163        # Look for dispersion parameters
164        toks = name.split('.')
165        if len(toks) == 2:
166            for item in self.dispersion.keys():
167                if item.lower() == toks[0].lower():
168                    for par in self.dispersion[item]:
169                        if par.lower() == toks[1].lower():
170                            self.dispersion[item][par] = value
171                            return
172        else:
173            # Look for standard parameter
174            for item in self.params.keys():
175                if item.lower() == name.lower():
176                    self.params[item] = value
177                    return
178
179        raise ValueError("Model does not contain parameter %s" % name)
180
181    def getParam(self, name):
182        """
183        Set the value of a model parameter
184
185        :param name: name of the parameter
186
187        """
188        # Look for dispersion parameters
189        toks = name.split('.')
190        if len(toks) == 2:
191            for item in self.dispersion.keys():
192                if item.lower() == toks[0].lower():
193                    for par in self.dispersion[item]:
194                        if par.lower() == toks[1].lower():
195                            return self.dispersion[item][par]
196        else:
197            # Look for standard parameter
198            for item in self.params.keys():
199                if item.lower() == name.lower():
200                    return self.params[item]
201
202        raise ValueError("Model does not contain parameter %s" % name)
203
204    def getParamList(self):
205        """
206        Return a list of all available parameters for the model
207        """
208        param_list = self.params.keys()
209        # WARNING: Extending the list with the dispersion parameters
210        param_list.extend(self.getDispParamList())
211        return param_list
212
213    def getDispParamList(self):
214        """
215        Return a list of polydispersity parameters for the model
216        """
217        # TODO: fix test so that parameter order doesn't matter
218        ret = ['%s.%s' % (p.name.lower(), ext)
219               for p in self._model_info['parameters'].user_parameters()
220               for ext in ('npts', 'nsigmas', 'width')
221               if p.polydisperse]
222        #print(ret)
223        return ret
224
225    def clone(self):
226        """ Return a identical copy of self """
227        return deepcopy(self)
228
229    def run(self, x=0.0):
230        """
231        Evaluate the model
232
233        :param x: input q, or [q,phi]
234
235        :return: scattering function P(q)
236
237        **DEPRECATED**: use calculate_Iq instead
238        """
239        if isinstance(x, (list, tuple)):
240            # pylint: disable=unpacking-non-sequence
241            q, phi = x
242            return self.calculate_Iq([q * math.cos(phi)],
243                                     [q * math.sin(phi)])[0]
244        else:
245            return self.calculate_Iq([float(x)])[0]
246
247
248    def runXY(self, x=0.0):
249        """
250        Evaluate the model in cartesian coordinates
251
252        :param x: input q, or [qx, qy]
253
254        :return: scattering function P(q)
255
256        **DEPRECATED**: use calculate_Iq instead
257        """
258        if isinstance(x, (list, tuple)):
259            return self.calculate_Iq([float(x[0])], [float(x[1])])[0]
260        else:
261            return self.calculate_Iq([float(x)])[0]
262
263    def evalDistribution(self, qdist):
264        r"""
265        Evaluate a distribution of q-values.
266
267        :param qdist: array of q or a list of arrays [qx,qy]
268
269        * For 1D, a numpy array is expected as input
270
271        ::
272
273            evalDistribution(q)
274
275          where *q* is a numpy array.
276
277        * For 2D, a list of *[qx,qy]* is expected with 1D arrays as input
278
279        ::
280
281              qx = [ qx[0], qx[1], qx[2], ....]
282              qy = [ qy[0], qy[1], qy[2], ....]
283
284        If the model is 1D only, then
285
286        .. math::
287
288            q = \sqrt{q_x^2+q_y^2}
289
290        """
291        if isinstance(qdist, (list, tuple)):
292            # Check whether we have a list of ndarrays [qx,qy]
293            qx, qy = qdist
294            if not self._model_info['parameters'].has_2d:
295                return self.calculate_Iq(np.sqrt(qx ** 2 + qy ** 2))
296            else:
297                return self.calculate_Iq(qx, qy)
298
299        elif isinstance(qdist, np.ndarray):
300            # We have a simple 1D distribution of q-values
301            return self.calculate_Iq(qdist)
302
303        else:
304            raise TypeError("evalDistribution expects q or [qx, qy], not %r"
305                            % type(qdist))
306
307    def calculate_Iq(self, *args):
308        """
309        Calculate Iq for one set of q with the current parameters.
310
311        If the model is 1D, use *q*.  If 2D, use *qx*, *qy*.
312
313        This should NOT be used for fitting since it copies the *q* vectors
314        to the card for each evaluation.
315        """
316        if self._model is None:
317            self._model = core.build_model(self._model_info, platform='dll')
318        q_vectors = [np.asarray(q) for q in args]
319        kernel = self._model.make_kernel(q_vectors)
320        pairs = [self._get_weights(p)
321                 for p in self._model_info['parameters'].call_parameters]
322        details, weights, values = core.build_details(kernel, pairs)
323        return kernel(details, weights, values, cutoff=self.cutoff)
324        kernel.q_input.release()
325        kernel.release()
326        return result
327
328    def calculate_ER(self):
329        """
330        Calculate the effective radius for P(q)*S(q)
331
332        :return: the value of the effective radius
333        """
334        ER = self._model_info.get('ER', None)
335        if ER is None:
336            return 1.0
337        else:
338            values, weights = self._dispersion_mesh()
339            fv = ER(*values)
340            #print(values[0].shape, weights.shape, fv.shape)
341            return np.sum(weights * fv) / np.sum(weights)
342
343    def calculate_VR(self):
344        """
345        Calculate the volf ratio for P(q)*S(q)
346
347        :return: the value of the volf ratio
348        """
349        VR = self._model_info.get('VR', None)
350        if VR is None:
351            return 1.0
352        else:
353            values, weights = self._dispersion_mesh()
354            whole, part = VR(*values)
355            return np.sum(weights * part) / np.sum(weights * whole)
356
357    def set_dispersion(self, parameter, dispersion):
358        """
359        Set the dispersion object for a model parameter
360
361        :param parameter: name of the parameter [string]
362        :param dispersion: dispersion object of type Dispersion
363        """
364        if parameter.lower() in (s.lower() for s in self.params.keys()):
365            # TODO: Store the disperser object directly in the model.
366            # The current method of creating one on the fly whenever it is
367            # needed is kind of funky.
368            # Note: can't seem to get disperser parameters from sasview
369            # (1) Could create a sasview model that has not yet # been
370            # converted, assign the disperser to one of its polydisperse
371            # parameters, then retrieve the disperser parameters from the
372            # sasview model.  (2) Could write a disperser parameter retriever
373            # in sasview.  (3) Could modify sasview to use sasmodels.weights
374            # dispersers.
375            # For now, rely on the fact that the sasview only ever uses
376            # new dispersers in the set_dispersion call and create a new
377            # one instead of trying to assign parameters.
378            from . import weights
379            disperser = weights.dispersers[dispersion.__class__.__name__]
380            dispersion = weights.models[disperser]()
381            self.dispersion[parameter] = dispersion.get_pars()
382        else:
383            raise ValueError("%r is not a dispersity or orientation parameter")
384
385    def _dispersion_mesh(self):
386        """
387        Create a mesh grid of dispersion parameters and weights.
388
389        Returns [p1,p2,...],w where pj is a vector of values for parameter j
390        and w is a vector containing the products for weights for each
391        parameter set in the vector.
392        """
393        pars = self._model_info['partype']['volume']
394        return core.dispersion_mesh([self._get_weights(p) for p in pars])
395
396    def _get_weights(self, par):
397        """
398        Return dispersion weights for parameter
399        """
400        if par.polydisperse:
401            dis = self.dispersion[par.name]
402            value, weight = weights.get_weights(
403                dis['type'], dis['npts'], dis['width'], dis['nsigmas'],
404                self.params[par.name], par.limits, par.relative_pd)
405            return value, weight / np.sum(weight)
406        else:
407            return [self.params[par.name]], []
408
409def test_model():
410    Cylinder = make_class('cylinder')
411    cylinder = Cylinder()
412    return cylinder.evalDistribution([0.1,0.1])
413
414if __name__ == "__main__":
415    print("cylinder(0.1,0.1)=%g"%test_model())
Note: See TracBrowser for help on using the repository browser.