source: sasmodels/sasmodels/sasview_model.py @ ce896fd

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

improved handling of vector parameters; remove compile errors from onion.c

  • Property mode set to 100644
File size: 13.2 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 generate
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._kernel = 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        partype = model_info['partype']
78
79        for p in model_info['parameters']:
80            self.params[p.name] = p.default
81            self.details[p.name] = [p.units] + p.limits
82
83        for name in partype['pd-2d']:
84            self.dispersion[name] = {
85                'width': 0,
86                'npts': 35,
87                'nsigmas': 3,
88                'type': 'gaussian',
89            }
90
91        self.orientation_params = (
92            partype['orientation']
93            + [n + '.width' for n in partype['orientation']]
94            + partype['magnetic'])
95        self.magnetic_params = partype['magnetic']
96        self.fixed = [n + '.width' for n in partype['pd-2d']]
97        self.non_fittable = []
98
99        ## independent parameter name and unit [string]
100        self.input_name = model_info.get("input_name", "Q")
101        self.input_unit = model_info.get("input_unit", "A^{-1}")
102        self.output_name = model_info.get("output_name", "Intensity")
103        self.output_unit = model_info.get("output_unit", "cm^{-1}")
104
105        ## _persistency_dict is used by sas.perspectives.fitting.basepage
106        ## to store dispersity reference.
107        ## TODO: _persistency_dict to persistency_dict throughout sasview
108        self._persistency_dict = {}
109
110        ## New fields introduced for opencl rewrite
111        self.cutoff = 1e-5
112
113    def __get_state__(self):
114        state = self.__dict__.copy()
115        model_id = self._model_info['id']
116        state.pop('_kernel')
117        # May need to reload model info on set state since it has pointers
118        # to python implementations of Iq, etc.
119        #state.pop('_model_info')
120        return state
121
122    def __set_state__(self, state):
123        self.__dict__ = state
124        self._kernel = None
125
126    def __str__(self):
127        """
128        :return: string representation
129        """
130        return self.name
131
132    def is_fittable(self, par_name):
133        """
134        Check if a given parameter is fittable or not
135
136        :param par_name: the parameter name to check
137        """
138        return par_name.lower() in self.fixed
139        #For the future
140        #return self.params[str(par_name)].is_fittable()
141
142
143    # pylint: disable=no-self-use
144    def getProfile(self):
145        """
146        Get SLD profile
147
148        : return: (z, beta) where z is a list of depth of the transition points
149                beta is a list of the corresponding SLD values
150        """
151        return None, None
152
153    def setParam(self, name, value):
154        """
155        Set the value of a model parameter
156
157        :param name: name of the parameter
158        :param value: value of the parameter
159
160        """
161        # Look for dispersion parameters
162        toks = name.split('.')
163        if len(toks) == 2:
164            for item in self.dispersion.keys():
165                if item.lower() == toks[0].lower():
166                    for par in self.dispersion[item]:
167                        if par.lower() == toks[1].lower():
168                            self.dispersion[item][par] = value
169                            return
170        else:
171            # Look for standard parameter
172            for item in self.params.keys():
173                if item.lower() == name.lower():
174                    self.params[item] = value
175                    return
176
177        raise ValueError("Model does not contain parameter %s" % name)
178
179    def getParam(self, name):
180        """
181        Set the value of a model parameter
182
183        :param name: name of the parameter
184
185        """
186        # Look for dispersion parameters
187        toks = name.split('.')
188        if len(toks) == 2:
189            for item in self.dispersion.keys():
190                if item.lower() == toks[0].lower():
191                    for par in self.dispersion[item]:
192                        if par.lower() == toks[1].lower():
193                            return self.dispersion[item][par]
194        else:
195            # Look for standard parameter
196            for item in self.params.keys():
197                if item.lower() == name.lower():
198                    return self.params[item]
199
200        raise ValueError("Model does not contain parameter %s" % name)
201
202    def getParamList(self):
203        """
204        Return a list of all available parameters for the model
205        """
206        param_list = self.params.keys()
207        # WARNING: Extending the list with the dispersion parameters
208        param_list.extend(self.getDispParamList())
209        return param_list
210
211    def getDispParamList(self):
212        """
213        Return a list of all available parameters for the model
214        """
215        # TODO: fix test so that parameter order doesn't matter
216        ret = ['%s.%s' % (d.lower(), p)
217               for d in self._model_info['partype']['pd-2d']
218               for p in ('npts', 'nsigmas', 'width')]
219        #print(ret)
220        return ret
221
222    def clone(self):
223        """ Return a identical copy of self """
224        return deepcopy(self)
225
226    def run(self, x=0.0):
227        """
228        Evaluate the model
229
230        :param x: input q, or [q,phi]
231
232        :return: scattering function P(q)
233
234        **DEPRECATED**: use calculate_Iq instead
235        """
236        if isinstance(x, (list, tuple)):
237            # pylint: disable=unpacking-non-sequence
238            q, phi = x
239            return self.calculate_Iq([q * math.cos(phi)],
240                                     [q * math.sin(phi)])[0]
241        else:
242            return self.calculate_Iq([float(x)])[0]
243
244
245    def runXY(self, x=0.0):
246        """
247        Evaluate the model in cartesian coordinates
248
249        :param x: input q, or [qx, qy]
250
251        :return: scattering function P(q)
252
253        **DEPRECATED**: use calculate_Iq instead
254        """
255        if isinstance(x, (list, tuple)):
256            return self.calculate_Iq([float(x[0])], [float(x[1])])[0]
257        else:
258            return self.calculate_Iq([float(x)])[0]
259
260    def evalDistribution(self, qdist):
261        r"""
262        Evaluate a distribution of q-values.
263
264        :param qdist: array of q or a list of arrays [qx,qy]
265
266        * For 1D, a numpy array is expected as input
267
268        ::
269
270            evalDistribution(q)
271
272          where *q* is a numpy array.
273
274        * For 2D, a list of *[qx,qy]* is expected with 1D arrays as input
275
276        ::
277
278              qx = [ qx[0], qx[1], qx[2], ....]
279              qy = [ qy[0], qy[1], qy[2], ....]
280
281        If the model is 1D only, then
282
283        .. math::
284
285            q = \sqrt{q_x^2+q_y^2}
286
287        """
288        if isinstance(qdist, (list, tuple)):
289            # Check whether we have a list of ndarrays [qx,qy]
290            qx, qy = qdist
291            partype = self._model_info['partype']
292            if not partype['orientation'] and not partype['magnetic']:
293                return self.calculate_Iq(np.sqrt(qx ** 2 + qy ** 2))
294            else:
295                return self.calculate_Iq(qx, qy)
296
297        elif isinstance(qdist, np.ndarray):
298            # We have a simple 1D distribution of q-values
299            return self.calculate_Iq(qdist)
300
301        else:
302            raise TypeError("evalDistribution expects q or [qx, qy], not %r"
303                            % type(qdist))
304
305    def calculate_Iq(self, *args):
306        """
307        Calculate Iq for one set of q with the current parameters.
308
309        If the model is 1D, use *q*.  If 2D, use *qx*, *qy*.
310
311        This should NOT be used for fitting since it copies the *q* vectors
312        to the card for each evaluation.
313        """
314        if self._kernel is None:
315            self._kernel = core.build_model(self._model_info)
316        q_vectors = [np.asarray(q) for q in args]
317        fn = self._kernel(q_vectors)
318        pars = [self.params[v] for v in fn.fixed_pars]
319        pd_pars = [self._get_weights(p) for p in fn.pd_pars]
320        result = fn(pars, pd_pars, self.cutoff)
321        fn.q_input.release()
322        fn.release()
323        return result
324
325    def calculate_ER(self):
326        """
327        Calculate the effective radius for P(q)*S(q)
328
329        :return: the value of the effective radius
330        """
331        ER = self._model_info.get('ER', None)
332        if ER is None:
333            return 1.0
334        else:
335            values, weights = self._dispersion_mesh()
336            fv = ER(*values)
337            #print(values[0].shape, weights.shape, fv.shape)
338            return np.sum(weights * fv) / np.sum(weights)
339
340    def calculate_VR(self):
341        """
342        Calculate the volf ratio for P(q)*S(q)
343
344        :return: the value of the volf ratio
345        """
346        VR = self._model_info.get('VR', None)
347        if VR is None:
348            return 1.0
349        else:
350            values, weights = self._dispersion_mesh()
351            whole, part = VR(*values)
352            return np.sum(weights * part) / np.sum(weights * whole)
353
354    def set_dispersion(self, parameter, dispersion):
355        """
356        Set the dispersion object for a model parameter
357
358        :param parameter: name of the parameter [string]
359        :param dispersion: dispersion object of type Dispersion
360        """
361        if parameter.lower() in (s.lower() for s in self.params.keys()):
362            # TODO: Store the disperser object directly in the model.
363            # The current method of creating one on the fly whenever it is
364            # needed is kind of funky.
365            # Note: can't seem to get disperser parameters from sasview
366            # (1) Could create a sasview model that has not yet # been
367            # converted, assign the disperser to one of its polydisperse
368            # parameters, then retrieve the disperser parameters from the
369            # sasview model.  (2) Could write a disperser parameter retriever
370            # in sasview.  (3) Could modify sasview to use sasmodels.weights
371            # dispersers.
372            # For now, rely on the fact that the sasview only ever uses
373            # new dispersers in the set_dispersion call and create a new
374            # one instead of trying to assign parameters.
375            from . import weights
376            disperser = weights.dispersers[dispersion.__class__.__name__]
377            dispersion = weights.models[disperser]()
378            self.dispersion[parameter] = dispersion.get_pars()
379        else:
380            raise ValueError("%r is not a dispersity or orientation parameter")
381
382    def _dispersion_mesh(self):
383        """
384        Create a mesh grid of dispersion parameters and weights.
385
386        Returns [p1,p2,...],w where pj is a vector of values for parameter j
387        and w is a vector containing the products for weights for each
388        parameter set in the vector.
389        """
390        pars = self._model_info['partype']['volume']
391        return core.dispersion_mesh([self._get_weights(p) for p in pars])
392
393    def _get_weights(self, par):
394        """
395            Return dispersion weights
396            :param par parameter name
397        """
398        from . import weights
399
400        relative = self._model_info['partype']['pd-rel']
401        limits = self._model_info['limits']
402        dis = self.dispersion[par]
403        value, weight = weights.get_weights(
404            dis['type'], dis['npts'], dis['width'], dis['nsigmas'],
405            self.params[par], limits[par], par in relative)
406        return value, weight / np.sum(weight)
407
Note: See TracBrowser for help on using the repository browser.