source: sasmodels/sasmodels/weights.py @ 737679d

core_shell_microgelscostrafo411magnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 737679d was 41e7f2e, checked in by ajj, 8 years ago

Merge branch 'master' into ticket651

  • Property mode set to 100644
File size: 7.9 KB
RevLine 
[ff7119b]1"""
2SAS distributions for polydispersity.
3"""
[ce27e21]4# TODO: include dispersion docs with the disperser models
[6cefbc9]5from __future__ import division, print_function
6
[7ae2b7f]7from math import sqrt  # type: ignore
[6cefbc9]8from collections import OrderedDict
9
[7ae2b7f]10import numpy as np  # type: ignore
11from scipy.special import gammaln  # type: ignore
[14de349]12
[733a3e1]13# TODO: include dispersion docs with the disperser models
14
[ce27e21]15class Dispersion(object):
16    """
17    Base dispersion object.
18
19    Subclasses should define *_weights(center, sigma, lb, ub)*
20    which returns the x points and their corresponding weights.
21    """
22    type = "base disperser"
23    default = dict(npts=35, width=0, nsigmas=3)
24    def __init__(self, npts=None, width=None, nsigmas=None):
25        self.npts = self.default['npts'] if npts is None else npts
26        self.width = self.default['width'] if width is None else width
27        self.nsigmas = self.default['nsigmas'] if nsigmas is None else nsigmas
[14de349]28
29    def get_pars(self):
[5c962df]30        """
31        Return the parameters to the disperser as a dictionary.
32        """
[ce27e21]33        pars = {'type': self.type}
34        pars.update(self.__dict__)
35        return pars
36
[3c56da87]37    # pylint: disable=no-self-use
[ce27e21]38    def set_weights(self, values, weights):
[5c962df]39        """
40        Set the weights on the disperser if it is :class:`ArrayDispersion`.
41        """
[ce27e21]42        raise RuntimeError("set_weights is only available for ArrayDispersion")
43
44    def get_weights(self, center, lb, ub, relative):
45        """
46        Return the weights for the distribution.
47
48        *center* is the center of the distribution
[14de349]49
[823e620]50        *lb*, *ub* are the min and max allowed values
[ce27e21]51
52        *relative* is True if the distribution width is proportional to the
53        center value instead of absolute.  For polydispersity use relative.
54        For orientation parameters use absolute.
55        """
56        sigma = self.width * center if relative else self.width
[5d4777d]57        if sigma == 0 or self.npts < 2:
58            if lb <= center <= ub:
59                return np.array([center], 'd'), np.array([1.], 'd')
60            else:
61                return np.array([], 'd'), np.array([], 'd')
[6cefbc9]62        x, px = self._weights(center, sigma, lb, ub)
63        return x, px
[ce27e21]64
65    def _weights(self, center, sigma, lb, ub):
66        """actual work of computing the weights"""
67        raise NotImplementedError
68
69    def _linspace(self, center, sigma, lb, ub):
70        """helper function to provide linear spaced weight points within range"""
71        npts, nsigmas = self.npts, self.nsigmas
72        x = center + np.linspace(-nsigmas*sigma, +nsigmas*sigma, npts)
73        x = x[(x >= lb) & (x <= ub)]
74        return x
75
76
77class GaussianDispersion(Dispersion):
[5c962df]78    r"""
79    Gaussian dispersion, with 1-\ $\sigma$ width.
80
81    .. math::
82
83        w = \exp\left(-\tfrac12 (x - c)^2/\sigma^2\right)
84    """
[ce27e21]85    type = "gaussian"
86    default = dict(npts=35, width=0, nsigmas=3)
87    def _weights(self, center, sigma, lb, ub):
[6cefbc9]88        # TODO: sample high probability regions more densely
89        # i.e., step uniformly in cumulative density rather than x value
90        # so weight = 1/Npts for all weights, but values are unevenly spaced
[ce27e21]91        x = self._linspace(center, sigma, lb, ub)
[14de349]92        px = np.exp((x-center)**2 / (-2.0 * sigma * sigma))
93        return x, px
94
[ce27e21]95
96class RectangleDispersion(Dispersion):
[5c962df]97    r"""
98    Uniform dispersion, with width $\sqrt{3}\sigma$.
99
100    .. math::
101
102        w = 1
103    """
[ce27e21]104    type = "rectangle"
105    default = dict(npts=35, width=0, nsigmas=1.70325)
106    def _weights(self, center, sigma, lb, ub):
[bb46723]107        x = self._linspace(center, sigma, lb, ub)
108        x = x[np.fabs(x-center) <= np.fabs(sigma)*sqrt(3.0)]
109        return x, np.ones_like(x)
[ce27e21]110
111
112class LogNormalDispersion(Dispersion):
[5c962df]113    r"""
114    log Gaussian dispersion, with 1-\ $\sigma$ width.
115
116    .. math::
117
118        w = \frac{\exp\left(-\tfrac12 (\ln x - c)^2/\sigma^2\right)}{x\sigma}
119    """
[ce27e21]120    type = "lognormal"
121    default = dict(npts=80, width=0, nsigmas=8)
122    def _weights(self, center, sigma, lb, ub):
[823e620]123        x = self._linspace(center, sigma, max(lb, 1e-8), max(ub, 1e-8))
[f1a8811]124        # sigma in the lognormal function is in ln(R) space, thus needs converting
125        sig = np.fabs(sigma/center)
126        px = np.exp(-0.5*((np.log(x)-np.log(center))/sig)**2)/(x*sig)
[ce27e21]127        return x, px
128
129
130class SchulzDispersion(Dispersion):
[5c962df]131    r"""
132    Schultz dispersion, with 1-\ $\sigma$ width.
133
134    .. math::
135
136        w = \frac{z^z\,R^{z-1}}{e^{Rz}\,c \Gamma(z)}
137
138    where $c$ is the center of the distribution, $R = x/c$ and $z=(c/\sigma)^2$.
139
140    This is evaluated using logarithms as
141
142    .. math::
143
144        w = \exp\left(z \ln z + (z-1)\ln R - Rz - \ln c - \ln \Gamma(z) \right)
145    """
[ce27e21]146    type = "schulz"
147    default = dict(npts=80, width=0, nsigmas=8)
148    def _weights(self, center, sigma, lb, ub):
[823e620]149        x = self._linspace(center, sigma, max(lb, 1e-8), max(ub, 1e-8))
150        R = x/center
[ce27e21]151        z = (center/sigma)**2
152        arg = z*np.log(z) + (z-1)*np.log(R) - R*z - np.log(center) - gammaln(z)
153        px = np.exp(arg)
154        return x, px
155
156
157class ArrayDispersion(Dispersion):
[5c962df]158    r"""
159    Empirical dispersion curve.
160
161    Use :meth:`set_weights` to set $w = f(x)$.
162    """
[ce27e21]163    type = "array"
164    default = dict(npts=35, width=0, nsigmas=1)
165    def __init__(self, npts=None, width=None, nsigmas=None):
166        Dispersion.__init__(self, npts, width, nsigmas)
167        self.values = np.array([0.], 'd')
168        self.weights = np.array([1.], 'd')
169
170    def set_weights(self, values, weights):
[5c962df]171        """
172        Set the weights for the given x values.
173        """
[ce27e21]174        self.values = np.ascontiguousarray(values, 'd')
175        self.weights = np.ascontiguousarray(weights, 'd')
176        self.npts = len(values)
177
178    def _weights(self, center, sigma, lb, ub):
[6cefbc9]179        # TODO: rebin the array dispersion using npts
180        # TODO: use a distribution that can be recentered and scaled
181        x = self.values
182        #x = center + self.values*sigma
[823e620]183        idx = (x >= lb) & (x <= ub)
[ce27e21]184        x = x[idx]
185        px = self.weights[idx]
186        return x, px
187
188
[ff7119b]189# dispersion name -> disperser lookup table.
[6cefbc9]190# Maintain order since this is used by sasview GUI to order the options in
191# the dispersion type combobox.
192MODELS = OrderedDict((d.type, d) for d in (
193    RectangleDispersion,
194    ArrayDispersion,
195    LogNormalDispersion,
196    GaussianDispersion,
197    SchulzDispersion,
[ce27e21]198))
199
[1780d59]200
201def get_weights(disperser, n, width, nsigmas, value, limits, relative):
[ff7119b]202    """
203    Return the set of values and weights for a polydisperse parameter.
204
205    *disperser* is the name of the disperser.
206
207    *n* is the number of points in the weight vector.
208
209    *width* is the width of the disperser distribution.
210
211    *nsigmas* is the number of sigmas to span for the dispersion convolution.
212
213    *value* is the value of the parameter in the model.
214
[6cefbc9]215    *limits* is [lb, ub], the lower and upper bound on the possible values.
[ff7119b]216
217    *relative* is true if *width* is defined in proportion to the value
218    of the parameter, and false if it is an absolute width.
219
[823e620]220    Returns *(value, weight)*, where *value* and *weight* are vectors.
[ff7119b]221    """
[6cefbc9]222    if disperser == "array":
223        raise NotImplementedError("Don't handle arrays through get_weights; use values and weights directly")
[fa5fd8d]224    cls = MODELS[disperser]
[1780d59]225    obj = cls(n, width, nsigmas)
[823e620]226    v, w = obj.get_weights(value, limits[0], limits[1], relative)
227    return v, w
[6cefbc9]228
229
230def plot_weights(model_info, pairs):
231    # type: (ModelInfo, List[Tuple[np.ndarray, np.ndarray]]) -> None
232    """
233    Plot the weights returned by :func:`get_weights`.
234
235    *model_info* is
236    :param model_info:
237    :param pairs:
238    :return:
239    """
240    import pylab
241
242    if any(len(values)>1 for values, weights in pairs):
243        labels = [p.name for p in model_info.parameters.call_parameters]
244        pylab.interactive(True)
245        pylab.figure()
246        for (v,w), s in zip(pairs, labels):
247            if len(v) > 1:
248                #print("weights for", s, v, w)
249                pylab.plot(v, w, '-o', label=s)
250        pylab.grid(True)
251        pylab.legend()
[f1a8811]252        #pylab.show()
Note: See TracBrowser for help on using the repository browser.