source: sasmodels/sasmodels/direct_model.py @ 4d76711

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

adjust interface to sasview

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