source: sasmodels/sasmodels/direct_model.py @ ea75043

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

add support for oriented usans using direct 2d resolution integral

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