source: sasmodels/sasmodels/direct_model.py @ 7ae2b7f

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

still more linting; ignore numpy types

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