source: sasmodels/sasmodels/direct_model.py @ 803f835

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

delint

  • Property mode set to 100644
File size: 9.3 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 load_model_definition, load_model, make_kernel
28from .core import call_kernel, call_ER, call_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        else:
66            self.data_type = 'Iq'
67
68        partype = model.info['partype']
69
70        if self.data_type == 'sesans':
71            q = sesans.make_q(data.sample.zacceptance, data.Rmax)
72            index = slice(None, None)
73            res = None
74            if data.y is not None:
75                Iq, dIq = data.y, data.dy
76            else:
77                Iq, dIq = None, None
78            #self._theory = np.zeros_like(q)
79            q_vectors = [q]
80        elif self.data_type == 'Iqxy':
81            if not partype['orientation'] and not partype['magnetic']:
82                raise ValueError("not 2D without orientation or magnetic parameters")
83            q = np.sqrt(data.qx_data**2 + data.qy_data**2)
84            qmin = getattr(data, 'qmin', 1e-16)
85            qmax = getattr(data, 'qmax', np.inf)
86            accuracy = getattr(data, 'accuracy', 'Low')
87            index = ~data.mask & (q >= qmin) & (q <= qmax)
88            if data.data is not None:
89                index &= ~np.isnan(data.data)
90                Iq = data.data[index]
91                dIq = data.err_data[index]
92            else:
93                Iq, dIq = None, None
94            res = resolution2d.Pinhole2D(data=data, index=index,
95                                         nsigma=3.0, accuracy=accuracy)
96            #self._theory = np.zeros_like(self.Iq)
97            q_vectors = res.q_calc
98        elif self.data_type == 'Iq':
99            index = (data.x >= data.qmin) & (data.x <= data.qmax)
100            if data.y is not None:
101                index &= ~np.isnan(data.y)
102                Iq = data.y[index]
103                dIq = data.dy[index]
104            else:
105                Iq, dIq = None, None
106            if getattr(data, 'dx', None) is not None:
107                q, dq = data.x[index], data.dx[index]
108                if (dq > 0).any():
109                    res = resolution.Pinhole1D(q, dq)
110                else:
111                    res = resolution.Perfect1D(q)
112            elif (getattr(data, 'dxl', None) is not None
113                  and getattr(data, 'dxw', None) is not None):
114                res = resolution.Slit1D(data.x[index],
115                                        width=data.dxh[index],
116                                        height=data.dxw[index])
117            else:
118                res = resolution.Perfect1D(data.x[index])
119
120            #self._theory = np.zeros_like(self.Iq)
121            q_vectors = [res.q_calc]
122        else:
123            raise ValueError("Unknown data type") # never gets here
124
125        # Remember function inputs so we can delay loading the function and
126        # so we can save/restore state
127        self._kernel_inputs = [v for v in q_vectors]
128        self._kernel = None
129        self.Iq, self.dIq, self.index = Iq, dIq, index
130        self.resolution = res
131
132    def _set_data(self, Iq, noise=None):
133        # pylint: disable=attribute-defined-outside-init
134        if noise is not None:
135            self.dIq = Iq*noise*0.01
136        dy = self.dIq
137        y = Iq + np.random.randn(*dy.shape) * dy
138        self.Iq = y
139        if self.data_type == 'Iq':
140            self._data.dy[self.index] = dy
141            self._data.y[self.index] = y
142        elif self.data_type == 'Iqxy':
143            self._data.data[self.index] = y
144        elif self.data_type == 'sesans':
145            self._data.y[self.index] = y
146        else:
147            raise ValueError("Unknown model")
148
149    def _calc_theory(self, pars, cutoff=0.0):
150        if self._kernel is None:
151            q_input = self._model.make_input(self._kernel_inputs)
152            self._kernel = self._model(q_input)  # pylint: disable=attribute-defined-outside-init
153
154        Iq_calc = call_kernel(self._kernel, pars, cutoff=cutoff)
155        if self.data_type == 'sesans':
156            result = sesans.hankel(self._data.x, self._data.lam * 1e-9,
157                                   self._data.sample.thickness / 10,
158                                   self._kernel_inputs[0], Iq_calc)
159        else:
160            result = self.resolution.apply(Iq_calc)
161        return result
162
163
164class DirectModel(DataMixin):
165    """
166    Create a calculator object for a model.
167
168    *data* is 1D SAS, 2D SAS or SESANS data
169
170    *model* is a model calculator return from :func:`generate.load_model`
171
172    *cutoff* is the polydispersity weight cutoff.
173    """
174    def __init__(self, data, model, cutoff=1e-5):
175        self.model = model
176        self.cutoff = cutoff
177        # Note: _interpret_data defines the model attributes
178        self._interpret_data(data, model)
179        self.kernel = make_kernel(self.model, self._kernel_inputs)
180
181    def __call__(self, **pars):
182        return self._calc_theory(pars, cutoff=self.cutoff)
183
184    def ER(self, **pars):
185        """
186        Compute the equivalent radius for the model.
187
188        Return 0. if not defined.
189        """
190        return call_ER(self.model.info, pars)
191
192    def VR(self, **pars):
193        """
194        Compute the equivalent volume for the model, including polydispersity
195        effects.
196
197        Return 1. if not defined.
198        """
199        return call_VR(self.model.info, pars)
200
201    def simulate_data(self, noise=None, **pars):
202        """
203        Generate simulated data for the model.
204        """
205        Iq = self.__call__(**pars)
206        self._set_data(Iq, noise=noise)
207
208def main():
209    """
210    Program to evaluate a particular model at a set of q values.
211    """
212    import sys
213    from .data import empty_data1D, empty_data2D
214
215    if len(sys.argv) < 3:
216        print("usage: python -m sasmodels.direct_model modelname (q|qx,qy) par=val ...")
217        sys.exit(1)
218    model_name = sys.argv[1]
219    call = sys.argv[2].upper()
220    if call not in ("ER", "VR"):
221        try:
222            values = [float(v) for v in call.split(',')]
223        except:
224            values = []
225        if len(values) == 1:
226            q, = values
227            data = empty_data1D([q])
228        elif len(values) == 2:
229            qx, qy = values
230            data = empty_data2D([qx], [qy])
231        else:
232            print("use q or qx,qy or ER or VR")
233            sys.exit(1)
234    else:
235        data = empty_data1D([0.001])  # Data not used in ER/VR
236
237    model_definition = load_model_definition(model_name)
238    model = load_model(model_definition, dtype='single')
239    calculator = DirectModel(data, model)
240    pars = dict((k, float(v))
241                for pair in sys.argv[3:]
242                for k, v in [pair.split('=')])
243    if call == "ER":
244        print(calculator.ER(**pars))
245    elif call == "VR":
246        print(calculator.VR(**pars))
247    else:
248        Iq = calculator(**pars)
249        print(Iq[0])
250
251if __name__ == "__main__":
252    main()
Note: See TracBrowser for help on using the repository browser.