source: sasmodels/sasmodels/sasview_model.py @ 63b32bb

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

lint cleaning

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