source: sasmodels/sasmodels/direct_model.py @ 0ff62d4

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

refactor: move dispersion_mesh alongside build_details in kernel.py

  • Property mode set to 100644
File size: 12.9 KB
Line 
1"""
2Class interface to the model calculator.
3
4Calling a model is somewhat non-trivial since the functions called depend
5on the data type.  For 1D data the *Iq* kernel needs to be called, for
62D data the *Iqxy* kernel needs to be called, and for SESANS data the
7*Iq* kernel needs to be called followed by a Hankel transform.  Before
8the kernel is called an appropriate *q* calculation vector needs to be
9constructed.  This is not the simple *q* vector where you have measured
10the data since the resolution calculation will require values beyond the
11range of the measured data.  After the calculation the resolution calculator
12must be called to return the predicted value for each measured data point.
13
14:class:`DirectModel` is a callable object that takes *parameter=value*
15keyword arguments and returns the appropriate theory values for the data.
16
17:class:`DataMixin` does the real work of interpreting the data and calling
18the model calculator.  This is used by :class:`DirectModel`, which uses
19direct parameter values and by :class:`bumps_model.Experiment` which wraps
20the parameter values in boxes so that the user can set fitting ranges, etc.
21on the individual parameters and send the model to the Bumps optimizers.
22"""
23from __future__ import print_function
24
25import numpy as np  # type: ignore
26
27# TODO: fix sesans module
28from . import sesans  # type: ignore
29from . import weights
30from . import resolution
31from . import resolution2d
32from . import kernel
33
34try:
35    from typing import Optional, Dict, Tuple
36except ImportError:
37    pass
38else:
39    from .data import Data
40    from .kernel import Kernel, KernelModel
41    from .modelinfo import Parameter, ParameterSet
42
43def call_kernel(calculator, pars, cutoff=0., mono=False):
44    # type: (Kernel, ParameterSet, float, bool) -> np.ndarray
45    """
46    Call *kernel* returned from *model.make_kernel* with parameters *pars*.
47
48    *cutoff* is the limiting value for the product of dispersion weights used
49    to perform the multidimensional dispersion calculation more quickly at a
50    slight cost to accuracy. The default value of *cutoff=0* integrates over
51    the entire dispersion cube.  Using *cutoff=1e-5* can be 50% faster, but
52    with an error of about 1%, which is usually less than the measurement
53    uncertainty.
54
55    *mono* is True if polydispersity should be set to none on all parameters.
56    """
57    parameters = calculator.info.parameters
58    if mono:
59        active = lambda name: False
60    elif calculator.dim == '1d':
61        active = lambda name: name in parameters.pd_1d
62    elif calculator.dim == '2d':
63        active = lambda name: name in parameters.pd_2d
64    else:
65        active = lambda name: True
66
67    vw_pairs = [(get_weights(p, pars) if active(p.name)
68                 else ([pars.get(p.name, p.default)], []))
69                for p in parameters.call_parameters]
70
71    call_details, weights, values = kernel.build_details(calculator, vw_pairs)
72    return calculator(call_details, weights, values, cutoff)
73
74def get_weights(parameter, values):
75    # type: (Parameter, Dict[str, float]) -> Tuple[np.ndarray, np.ndarray]
76    """
77    Generate the distribution for parameter *name* given the parameter values
78    in *pars*.
79
80    Uses "name", "name_pd", "name_pd_type", "name_pd_n", "name_pd_sigma"
81    from the *pars* dictionary for parameter value and parameter dispersion.
82    """
83    value = float(values.get(parameter.name, parameter.default))
84    relative = parameter.relative_pd
85    limits = parameter.limits
86    disperser = values.get(parameter.name+'_pd_type', 'gaussian')
87    npts = values.get(parameter.name+'_pd_n', 0)
88    width = values.get(parameter.name+'_pd', 0.0)
89    nsigma = values.get(parameter.name+'_pd_nsigma', 3.0)
90    if npts == 0 or width == 0:
91        return [value], []
92    value, weight = weights.get_weights(
93        disperser, npts, width, nsigma, value, limits, relative)
94    return value, weight / np.sum(weight)
95
96class DataMixin(object):
97    """
98    DataMixin captures the common aspects of evaluating a SAS model for a
99    particular data set, including calculating Iq and evaluating the
100    resolution function.  It is used in particular by :class:`DirectModel`,
101    which evaluates a SAS model parameters as key word arguments to the
102    calculator method, and by :class:`bumps_model.Experiment`, which wraps the
103    model and data for use with the Bumps fitting engine.  It is not
104    currently used by :class:`sasview_model.SasviewModel` since this will
105    require a number of changes to SasView before we can do it.
106
107    :meth:`_interpret_data` initializes the data structures necessary
108    to manage the calculations.  This sets attributes in the child class
109    such as *data_type* and *resolution*.
110
111    :meth:`_calc_theory` evaluates the model at the given control values.
112
113    :meth:`_set_data` sets the intensity data in the data object,
114    possibly with random noise added.  This is useful for simulating a
115    dataset with the results from :meth:`_calc_theory`.
116    """
117    def _interpret_data(self, data, model):
118        # type: (Data, KernelModel) -> None
119        # pylint: disable=attribute-defined-outside-init
120
121        self._data = data
122        self._model = model
123
124        # interpret data
125        if hasattr(data, 'lam'):
126            self.data_type = 'sesans'
127        elif hasattr(data, 'qx_data'):
128            self.data_type = 'Iqxy'
129        elif getattr(data, 'oriented', False):
130            self.data_type = 'Iq-oriented'
131        else:
132            self.data_type = 'Iq'
133
134        if self.data_type == 'sesans':
135           
136            q = sesans.make_q(data.sample.zacceptance, data.Rmax)
137            index = slice(None, None)
138            res = None
139            if data.y is not None:
140                Iq, dIq = data.y, data.dy
141            else:
142                Iq, dIq = None, None
143            #self._theory = np.zeros_like(q)
144            q_vectors = [q]           
145            q_mono = sesans.make_all_q(data)
146        elif self.data_type == 'Iqxy':
147            #if not model.info.parameters.has_2d:
148            #    raise ValueError("not 2D without orientation or magnetic parameters")
149            q = np.sqrt(data.qx_data**2 + data.qy_data**2)
150            qmin = getattr(data, 'qmin', 1e-16)
151            qmax = getattr(data, 'qmax', np.inf)
152            accuracy = getattr(data, 'accuracy', 'Low')
153            index = ~data.mask & (q >= qmin) & (q <= qmax)
154            if data.data is not None:
155                index &= ~np.isnan(data.data)
156                Iq = data.data[index]
157                dIq = data.err_data[index]
158            else:
159                Iq, dIq = None, None
160            res = resolution2d.Pinhole2D(data=data, index=index,
161                                         nsigma=3.0, accuracy=accuracy)
162            #self._theory = np.zeros_like(self.Iq)
163            q_vectors = res.q_calc
164            q_mono = []
165        elif self.data_type == 'Iq':
166            index = (data.x >= data.qmin) & (data.x <= data.qmax)
167            if data.y is not None:
168                index &= ~np.isnan(data.y)
169                Iq = data.y[index]
170                dIq = data.dy[index]
171            else:
172                Iq, dIq = None, None
173            if getattr(data, 'dx', None) is not None:
174                q, dq = data.x[index], data.dx[index]
175                if (dq > 0).any():
176                    res = resolution.Pinhole1D(q, dq)
177                else:
178                    res = resolution.Perfect1D(q)
179            elif (getattr(data, 'dxl', None) is not None
180                  and getattr(data, 'dxw', None) is not None):
181                res = resolution.Slit1D(data.x[index],
182                                        qx_width=data.dxw[index],
183                                        qy_width=data.dxl[index])
184            else:
185                res = resolution.Perfect1D(data.x[index])
186
187            #self._theory = np.zeros_like(self.Iq)
188            q_vectors = [res.q_calc]
189            q_mono = []
190        elif self.data_type == 'Iq-oriented':
191            index = (data.x >= data.qmin) & (data.x <= data.qmax)
192            if data.y is not None:
193                index &= ~np.isnan(data.y)
194                Iq = data.y[index]
195                dIq = data.dy[index]
196            else:
197                Iq, dIq = None, None
198            if (getattr(data, 'dxl', None) is None
199                or getattr(data, 'dxw', None) is None):
200                raise ValueError("oriented sample with 1D data needs slit resolution")
201
202            res = resolution2d.Slit2D(data.x[index],
203                                      qx_width=data.dxw[index],
204                                      qy_width=data.dxl[index])
205            q_vectors = res.q_calc
206            q_mono = []
207        else:
208            raise ValueError("Unknown data type") # never gets here
209
210        # Remember function inputs so we can delay loading the function and
211        # so we can save/restore state
212        self._kernel_inputs = q_vectors
213        self._kernel_mono_inputs = q_mono
214        self._kernel = None
215        self.Iq, self.dIq, self.index = Iq, dIq, index
216        self.resolution = res
217
218    def _set_data(self, Iq, noise=None):
219        # type: (np.ndarray, Optional[float]) -> None
220        # pylint: disable=attribute-defined-outside-init
221        if noise is not None:
222            self.dIq = Iq*noise*0.01
223        dy = self.dIq
224        y = Iq + np.random.randn(*dy.shape) * dy
225        self.Iq = y
226        if self.data_type in ('Iq', 'Iq-oriented'):
227            self._data.dy[self.index] = dy
228            self._data.y[self.index] = y
229        elif self.data_type == 'Iqxy':
230            self._data.data[self.index] = y
231        elif self.data_type == 'sesans':
232            self._data.y[self.index] = y
233        else:
234            raise ValueError("Unknown model")
235
236    def _calc_theory(self, pars, cutoff=0.0):
237        # type: (ParameterSet, float) -> np.ndarray
238        if self._kernel is None:
239            self._kernel = self._model.make_kernel(self._kernel_inputs)
240            self._kernel_mono = (self._model.make_kernel(self._kernel_mono_inputs)
241                                 if self._kernel_mono_inputs else None)
242
243        Iq_calc = call_kernel(self._kernel, pars, cutoff=cutoff)
244        # TODO: may want to plot the raw Iq for other than oriented usans
245        self.Iq_calc = None
246        if self.data_type == 'sesans':
247            Iq_mono = (call_kernel(self._kernel_mono, pars, mono=True)
248                       if self._kernel_mono_inputs else None)
249            result = sesans.transform(self._data,
250                                   self._kernel_inputs[0], Iq_calc, 
251                                   self._kernel_mono_inputs, Iq_mono)
252        else:
253            result = self.resolution.apply(Iq_calc)
254            if hasattr(self.resolution, 'nx'):
255                self.Iq_calc = (
256                    self.resolution.qx_calc, self.resolution.qy_calc,
257                    np.reshape(Iq_calc, (self.resolution.ny, self.resolution.nx))
258                )
259        return result       
260
261
262class DirectModel(DataMixin):
263    """
264    Create a calculator object for a model.
265
266    *data* is 1D SAS, 2D SAS or SESANS data
267
268    *model* is a model calculator return from :func:`generate.load_model`
269
270    *cutoff* is the polydispersity weight cutoff.
271    """
272    def __init__(self, data, model, cutoff=1e-5):
273        # type: (Data, KernelModel, float) -> None
274        self.model = model
275        self.cutoff = cutoff
276        # Note: _interpret_data defines the model attributes
277        self._interpret_data(data, model)
278
279    def __call__(self, **pars):
280        # type: (**float) -> np.ndarray
281        return self._calc_theory(pars, cutoff=self.cutoff)
282
283    def simulate_data(self, noise=None, **pars):
284        # type: (Optional[float], **float) -> None
285        """
286        Generate simulated data for the model.
287        """
288        Iq = self.__call__(**pars)
289        self._set_data(Iq, noise=noise)
290
291def main():
292    # type: () -> None
293    """
294    Program to evaluate a particular model at a set of q values.
295    """
296    import sys
297    from .data import empty_data1D, empty_data2D
298    from .core import load_model_info, build_model
299
300    if len(sys.argv) < 3:
301        print("usage: python -m sasmodels.direct_model modelname (q|qx,qy) par=val ...")
302        sys.exit(1)
303    model_name = sys.argv[1]
304    call = sys.argv[2].upper()
305    if call != "ER_VR":
306        try:
307            values = [float(v) for v in call.split(',')]
308        except Exception:
309            values = []
310        if len(values) == 1:
311            q, = values
312            data = empty_data1D([q])
313        elif len(values) == 2:
314            qx, qy = values
315            data = empty_data2D([qx], [qy])
316        else:
317            print("use q or qx,qy or ER or VR")
318            sys.exit(1)
319    else:
320        data = empty_data1D([0.001])  # Data not used in ER/VR
321
322    model_info = load_model_info(model_name)
323    model = build_model(model_info)
324    calculator = DirectModel(data, model)
325    pars = dict((k, float(v))
326                for pair in sys.argv[3:]
327                for k, v in [pair.split('=')])
328    if call == "ER_VR":
329        print(calculator.ER_VR(**pars))
330    else:
331        Iq = calculator(**pars)
332        print(Iq[0])
333
334if __name__ == "__main__":
335    main()
Note: See TracBrowser for help on using the repository browser.