source: sasmodels/sasmodels/direct_model.py @ d18582e

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

default to double precision if single=False is set in model file

  • Property mode set to 100644
File size: 9.1 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            self._kernel = make_kernel(self._model, self._kernel_inputs)  # pylint: disable=attribute-defined-outside-init
152
153        Iq_calc = call_kernel(self._kernel, pars, cutoff=cutoff)
154        if self.data_type == 'sesans':
155            result = sesans.hankel(self._data.x, self._data.lam * 1e-9,
156                                   self._data.sample.thickness / 10,
157                                   self._kernel_inputs[0], Iq_calc)
158        else:
159            result = self.resolution.apply(Iq_calc)
160        return result
161
162
163class DirectModel(DataMixin):
164    """
165    Create a calculator object for a model.
166
167    *data* is 1D SAS, 2D SAS or SESANS data
168
169    *model* is a model calculator return from :func:`generate.load_model`
170
171    *cutoff* is the polydispersity weight cutoff.
172    """
173    def __init__(self, data, model, cutoff=1e-5):
174        self.model = model
175        self.cutoff = cutoff
176        # Note: _interpret_data defines the model attributes
177        self._interpret_data(data, model)
178
179    def __call__(self, **pars):
180        return self._calc_theory(pars, cutoff=self.cutoff)
181
182    def ER(self, **pars):
183        """
184        Compute the equivalent radius for the model.
185
186        Return 0. if not defined.
187        """
188        return call_ER(self.model.info, pars)
189
190    def VR(self, **pars):
191        """
192        Compute the equivalent volume for the model, including polydispersity
193        effects.
194
195        Return 1. if not defined.
196        """
197        return call_VR(self.model.info, pars)
198
199    def simulate_data(self, noise=None, **pars):
200        """
201        Generate simulated data for the model.
202        """
203        Iq = self.__call__(**pars)
204        self._set_data(Iq, noise=noise)
205
206def main():
207    """
208    Program to evaluate a particular model at a set of q values.
209    """
210    import sys
211    from .data import empty_data1D, empty_data2D
212
213    if len(sys.argv) < 3:
214        print("usage: python -m sasmodels.direct_model modelname (q|qx,qy) par=val ...")
215        sys.exit(1)
216    model_name = sys.argv[1]
217    call = sys.argv[2].upper()
218    if call not in ("ER", "VR"):
219        try:
220            values = [float(v) for v in call.split(',')]
221        except:
222            values = []
223        if len(values) == 1:
224            q, = values
225            data = empty_data1D([q])
226        elif len(values) == 2:
227            qx, qy = values
228            data = empty_data2D([qx], [qy])
229        else:
230            print("use q or qx,qy or ER or VR")
231            sys.exit(1)
232    else:
233        data = empty_data1D([0.001])  # Data not used in ER/VR
234
235    model_definition = load_model_definition(model_name)
236    model = load_model(model_definition)
237    calculator = DirectModel(data, model)
238    pars = dict((k, float(v))
239                for pair in sys.argv[3:]
240                for k, v in [pair.split('=')])
241    if call == "ER":
242        print(calculator.ER(**pars))
243    elif call == "VR":
244        print(calculator.VR(**pars))
245    else:
246        Iq = calculator(**pars)
247        print(Iq[0])
248
249if __name__ == "__main__":
250    main()
Note: See TracBrowser for help on using the repository browser.