source: sasmodels/sasmodels/direct_model.py @ 6d6508e

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

refactor model_info from dictionary to class

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