source: sasmodels/sasmodels/data.py @ 56b2687

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

Merge branch 'master' into polydisp

Conflicts:

README.rst
sasmodels/core.py
sasmodels/data.py
sasmodels/generate.py
sasmodels/kernelcl.py
sasmodels/kerneldll.py
sasmodels/sasview_model.py

  • Property mode set to 100644
File size: 20.3 KB
Line 
1"""
2SAS data representations.
3
4Plotting functions for data sets:
5
6    :func:`plot_data` plots the data file.
7
8    :func:`plot_theory` plots a calculated result from the model.
9
10Wrappers for the sasview data loader and data manipulations:
11
12    :func:`load_data` loads a sasview data file.
13
14    :func:`set_beam_stop` masks the beam stop from the data.
15
16    :func:`set_half` selects the right or left half of the data, which can
17    be useful for shear measurements which have not been properly corrected
18    for path length and reflections.
19
20    :func:`set_top` cuts the top part off the data.
21
22
23Empty data sets for evaluating models without data:
24
25    :func:`empty_data1D` creates an empty dataset, which is useful for plotting
26    a theory function before the data is measured.
27
28    :func:`empty_data2D` creates an empty 2D dataset.
29
30Note that the empty datasets use a minimal representation of the SasView
31objects so that models can be run without SasView on the path.  You could
32also use these for your own data loader.
33
34"""
35import traceback
36
37import numpy as np  # type: ignore
38
39try:
40    from typing import Union, Dict, List, Optional
41except ImportError:
42    pass
43else:
44    Data = Union["Data1D", "Data2D", "SesansData"]
45
46def load_data(filename):
47    # type: (str) -> Data
48    """
49    Load data using a sasview loader.
50    """
51    from sas.sascalc.dataloader.loader import Loader  # type: ignore
52    loader = Loader()
53    data = loader.load(filename)
54    if data is None:
55        raise IOError("Data %r could not be loaded" % filename)
56    return data
57
58
59def set_beam_stop(data, radius, outer=None):
60    # type: (Data, float, Optional[float]) -> None
61    """
62    Add a beam stop of the given *radius*.  If *outer*, make an annulus.
63    """
64    from sas.sascalc.dataloader.manipulations import Ringcut
65    if hasattr(data, 'qx_data'):
66        data.mask = Ringcut(0, radius)(data)
67        if outer is not None:
68            data.mask += Ringcut(outer, np.inf)(data)
69    else:
70        data.mask = (data.x < radius)
71        if outer is not None:
72            data.mask |= (data.x >= outer)
73
74
75def set_half(data, half):
76    # type: (Data, str) -> None
77    """
78    Select half of the data, either "right" or "left".
79    """
80    from sas.sascalc.dataloader.manipulations import Boxcut
81    if half == 'right':
82        data.mask += \
83            Boxcut(x_min=-np.inf, x_max=0.0, y_min=-np.inf, y_max=np.inf)(data)
84    if half == 'left':
85        data.mask += \
86            Boxcut(x_min=0.0, x_max=np.inf, y_min=-np.inf, y_max=np.inf)(data)
87
88
89def set_top(data, cutoff):
90    # type: (Data, float) -> None
91    """
92    Chop the top off the data, above *cutoff*.
93    """
94    from sas.sascalc.dataloader.manipulations import Boxcut
95    data.mask += \
96        Boxcut(x_min=-np.inf, x_max=np.inf, y_min=-np.inf, y_max=cutoff)(data)
97
98
99class Data1D(object):
100    """
101    1D data object.
102
103    Note that this definition matches the attributes from sasview, with
104    some generic 1D data vectors and some SAS specific definitions.  Some
105    refactoring to allow consistent naming conventions between 1D, 2D and
106    SESANS data would be helpful.
107
108    **Attributes**
109
110    *x*, *dx*: $q$ vector and gaussian resolution
111
112    *y*, *dy*: $I(q)$ vector and measurement uncertainty
113
114    *mask*: values to include in plotting/analysis
115
116    *dxl*: slit widths for slit smeared data, with *dx* ignored
117
118    *qmin*, *qmax*: range of $q$ values in *x*
119
120    *filename*: label for the data line
121
122    *_xaxis*, *_xunit*: label and units for the *x* axis
123
124    *_yaxis*, *_yunit*: label and units for the *y* axis
125    """
126    def __init__(self, x=None, y=None, dx=None, dy=None):
127        # type: (Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]) -> None
128        self.x, self.y, self.dx, self.dy = x, y, dx, dy
129        self.dxl = None
130        self.filename = None
131        self.qmin = x.min() if x is not None else np.NaN
132        self.qmax = x.max() if x is not None else np.NaN
133        # TODO: why is 1D mask False and 2D mask True?
134        self.mask = (np.isnan(y) if y is not None
135                     else np.zeros_like(x, 'b') if x is not None
136                     else None)
137        self._xaxis, self._xunit = "x", ""
138        self._yaxis, self._yunit = "y", ""
139
140    def xaxis(self, label, unit):
141        # type: (str, str) -> None
142        """
143        set the x axis label and unit
144        """
145        self._xaxis = label
146        self._xunit = unit
147
148    def yaxis(self, label, unit):
149        # type: (str, str) -> None
150        """
151        set the y axis label and unit
152        """
153        self._yaxis = label
154        self._yunit = unit
155
156class SesansData(Data1D):
157    def __init__(self, **kw):
158        Data1D.__init__(self, **kw)
159        self.lam = None # type: Optional[np.ndarray]
160
161class Data2D(object):
162    """
163    2D data object.
164
165    Note that this definition matches the attributes from sasview. Some
166    refactoring to allow consistent naming conventions between 1D, 2D and
167    SESANS data would be helpful.
168
169    **Attributes**
170
171    *qx_data*, *dqx_data*: $q_x$ matrix and gaussian resolution
172
173    *qy_data*, *dqy_data*: $q_y$ matrix and gaussian resolution
174
175    *data*, *err_data*: $I(q)$ matrix and measurement uncertainty
176
177    *mask*: values to exclude from plotting/analysis
178
179    *qmin*, *qmax*: range of $q$ values in *x*
180
181    *filename*: label for the data line
182
183    *_xaxis*, *_xunit*: label and units for the *x* axis
184
185    *_yaxis*, *_yunit*: label and units for the *y* axis
186
187    *_zaxis*, *_zunit*: label and units for the *y* axis
188
189    *Q_unit*, *I_unit*: units for Q and intensity
190
191    *x_bins*, *y_bins*: grid steps in *x* and *y* directions
192    """
193    def __init__(self, x=None, y=None, z=None, dx=None, dy=None, dz=None):
194        # type: (Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]) -> None
195        self.qx_data, self.dqx_data = x, dx
196        self.qy_data, self.dqy_data = y, dy
197        self.data, self.err_data = z, dz
198        self.mask = (np.isnan(z) if z is not None
199                     else np.zeros_like(x, dtype='bool') if x is not None
200                     else None)
201        self.q_data = np.sqrt(x**2 + y**2)
202        self.qmin = 1e-16
203        self.qmax = np.inf
204        self.detector = []
205        self.source = Source()
206        self.Q_unit = "1/A"
207        self.I_unit = "1/cm"
208        self.xaxis("Q_x", "1/A")
209        self.yaxis("Q_y", "1/A")
210        self.zaxis("Intensity", "1/cm")
211        self._xaxis, self._xunit = "x", ""
212        self._yaxis, self._yunit = "y", ""
213        self._zaxis, self._zunit = "z", ""
214        self.x_bins, self.y_bins = None, None
215
216    def xaxis(self, label, unit):
217        # type: (str, str) -> None
218        """
219        set the x axis label and unit
220        """
221        self._xaxis = label
222        self._xunit = unit
223
224    def yaxis(self, label, unit):
225        # type: (str, str) -> None
226        """
227        set the y axis label and unit
228        """
229        self._yaxis = label
230        self._yunit = unit
231
232    def zaxis(self, label, unit):
233        # type: (str, str) -> None
234        """
235        set the y axis label and unit
236        """
237        self._zaxis = label
238        self._zunit = unit
239
240
241class Vector(object):
242    """
243    3-space vector of *x*, *y*, *z*
244    """
245    def __init__(self, x=None, y=None, z=None):
246        # type: (float, float, Optional[float]) -> None
247        self.x, self.y, self.z = x, y, z
248
249class Detector(object):
250    """
251    Detector attributes.
252    """
253    def __init__(self, pixel_size=(None, None), distance=None):
254        # type: (Tuple[float, float], float) -> None
255        self.pixel_size = Vector(*pixel_size)
256        self.distance = distance
257
258class Source(object):
259    """
260    Beam attributes.
261    """
262    def __init__(self):
263        # type: () -> None
264        self.wavelength = np.NaN
265        self.wavelength_unit = "A"
266
267
268def empty_data1D(q, resolution=0.0):
269    # type: (np.ndarray, float) -> Data1D
270    """
271    Create empty 1D data using the given *q* as the x value.
272
273    *resolution* dq/q defaults to 5%.
274    """
275
276    #Iq = 100 * np.ones_like(q)
277    #dIq = np.sqrt(Iq)
278    Iq, dIq = None, None
279    q = np.asarray(q)
280    data = Data1D(q, Iq, dx=resolution * q, dy=dIq)
281    data.filename = "fake data"
282    return data
283
284
285def empty_data2D(qx, qy=None, resolution=0.0):
286    # type: (np.ndarray, Optional[np.ndarray], float) -> Data2D
287    """
288    Create empty 2D data using the given mesh.
289
290    If *qy* is missing, create a square mesh with *qy=qx*.
291
292    *resolution* dq/q defaults to 5%.
293    """
294    if qy is None:
295        qy = qx
296    qx, qy = np.asarray(qx), np.asarray(qy)
297    # 5% dQ/Q resolution
298    Qx, Qy = np.meshgrid(qx, qy)
299    Qx, Qy = Qx.flatten(), Qy.flatten()
300    Iq = 100 * np.ones_like(Qx)  # type: np.ndarray
301    dIq = np.sqrt(Iq)
302    if resolution != 0:
303        # https://www.ncnr.nist.gov/staff/hammouda/distance_learning/chapter_15.pdf
304        # Should have an additional constant which depends on distances and
305        # radii of the aperture, pixel dimensions and wavelength spread
306        # Instead, assume radial dQ/Q is constant, and perpendicular matches
307        # radial (which instead it should be inverse).
308        Q = np.sqrt(Qx**2 + Qy**2)
309        dqx = resolution * Q
310        dqy = resolution * Q
311    else:
312        dqx = dqy = None
313
314    data = Data2D(x=Qx, y=Qy, z=Iq, dx=dqx, dy=dqy, dz=dIq)
315    data.x_bins = qx
316    data.y_bins = qy
317    data.filename = "fake data"
318
319    # pixel_size in mm, distance in m
320    detector = Detector(pixel_size=(5, 5), distance=4)
321    data.detector.append(detector)
322    data.source.wavelength = 5 # angstroms
323    data.source.wavelength_unit = "A"
324    return data
325
326
327def plot_data(data, view='log', limits=None):
328    # type: (Data, str, Optional[Tuple[float, float]]) -> None
329    """
330    Plot data loaded by the sasview loader.
331
332    *data* is a sasview data object, either 1D, 2D or SESANS.
333
334    *view* is log or linear.
335
336    *limits* sets the intensity limits on the plot; if None then the limits
337    are inferred from the data.
338    """
339    # Note: kind of weird using the plot result functions to plot just the
340    # data, but they already handle the masking and graph markup already, so
341    # do not repeat.
342    if hasattr(data, 'lam'):
343        _plot_result_sesans(data, None, None, use_data=True, limits=limits)
344    elif hasattr(data, 'qx_data'):
345        _plot_result2D(data, None, None, view, use_data=True, limits=limits)
346    else:
347        _plot_result1D(data, None, None, view, use_data=True, limits=limits)
348
349
350def plot_theory(data, theory, resid=None, view='log',
351                use_data=True, limits=None, Iq_calc=None):
352    # type: (Data, Optional[np.ndarray], Optional[np.ndarray], str, bool, Optional[Tuple[float,float]], Optional[np.ndarray]) -> None
353    """
354    Plot theory calculation.
355
356    *data* is needed to define the graph properties such as labels and
357    units, and to define the data mask.
358
359    *theory* is a matrix of the same shape as the data.
360
361    *view* is log or linear
362
363    *use_data* is True if the data should be plotted as well as the theory.
364
365    *limits* sets the intensity limits on the plot; if None then the limits
366    are inferred from the data.
367
368    *Iq_calc* is the raw theory values without resolution smearing
369    """
370    if hasattr(data, 'lam'):
371        _plot_result_sesans(data, theory, resid, use_data=True, limits=limits)
372    elif hasattr(data, 'qx_data'):
373        _plot_result2D(data, theory, resid, view, use_data, limits=limits)
374    else:
375        _plot_result1D(data, theory, resid, view, use_data,
376                       limits=limits, Iq_calc=Iq_calc)
377
378
379def protect(fn):
380    # type: (Callable) -> Callable
381    """
382    Decorator to wrap calls in an exception trapper which prints the
383    exception and continues.  Keyboard interrupts are ignored.
384    """
385    def wrapper(*args, **kw):
386        """
387        Trap and print errors from function.
388        """
389        try:
390            return fn(*args, **kw)
391        except Exception:
392            traceback.print_exc()
393
394    return wrapper
395
396
397@protect
398def _plot_result1D(data, theory, resid, view, use_data,
399                   limits=None, Iq_calc=None):
400    # type: (Data1D, Optional[np.ndarray], Optional[np.ndarray], str, bool, Optional[Tuple[float, float]], Optional[np.ndarray]) -> None
401    """
402    Plot the data and residuals for 1D data.
403    """
404    import matplotlib.pyplot as plt  # type: ignore
405    from numpy.ma import masked_array, masked  # type: ignore
406
407    use_data = use_data and data.y is not None
408    use_theory = theory is not None
409    use_resid = resid is not None
410    use_calc = use_theory and Iq_calc is not None
411    num_plots = (use_data or use_theory) + use_calc + use_resid
412    non_positive_x = (data.x<=0.0).any()
413
414    scale = data.x**4 if view == 'q4' else 1.0
415
416    if use_data or use_theory:
417        if num_plots > 1:
418            plt.subplot(1, num_plots, 1)
419
420        #print(vmin, vmax)
421        all_positive = True
422        some_present = False
423        if use_data:
424            mdata = masked_array(data.y, data.mask.copy())
425            mdata[~np.isfinite(mdata)] = masked
426            if view is 'log':
427                mdata[mdata <= 0] = masked
428            plt.errorbar(data.x, scale*mdata, yerr=data.dy, fmt='.')
429            all_positive = all_positive and (mdata > 0).all()
430            some_present = some_present or (mdata.count() > 0)
431
432
433        if use_theory:
434            # Note: masks merge, so any masked theory points will stay masked,
435            # and the data mask will be added to it.
436            mtheory = masked_array(theory, data.mask.copy())
437            mtheory[~np.isfinite(mtheory)] = masked
438            if view is 'log':
439                mtheory[mtheory <= 0] = masked
440            plt.plot(data.x, scale*mtheory, '-', hold=True)
441            all_positive = all_positive and (mtheory > 0).all()
442            some_present = some_present or (mtheory.count() > 0)
443
444        if limits is not None:
445            plt.ylim(*limits)
446
447        plt.xscale('linear' if not some_present or non_positive_x  else view)
448        plt.yscale('linear'
449                   if view == 'q4' or not some_present or not all_positive
450                   else view)
451        plt.xlabel("$q$/A$^{-1}$")
452        plt.ylabel('$I(q)$')
453
454    if use_calc:
455        # Only have use_calc if have use_theory
456        plt.subplot(1, num_plots, 2)
457        qx, qy, Iqxy = Iq_calc
458        plt.pcolormesh(qx, qy[qy>0], np.log10(Iqxy[qy>0,:]))
459        plt.xlabel("$q_x$/A$^{-1}$")
460        plt.xlabel("$q_y$/A$^{-1}$")
461        plt.xscale('log')
462        plt.yscale('log')
463        #plt.axis('equal')
464
465    if use_resid:
466        mresid = masked_array(resid, data.mask.copy())
467        mresid[~np.isfinite(mresid)] = masked
468        some_present = (mresid.count() > 0)
469
470        if num_plots > 1:
471            plt.subplot(1, num_plots, use_calc + 2)
472        plt.plot(data.x, mresid, '-')
473        plt.xlabel("$q$/A$^{-1}$")
474        plt.ylabel('residuals')
475        plt.xscale('linear' if not some_present or non_positive_x else view)
476
477
478@protect
479def _plot_result_sesans(data, theory, resid, use_data, limits=None):
480    # type: (SesansData, Optional[np.ndarray], Optional[np.ndarray], bool, Optional[Tuple[float, float]]) -> None
481    """
482    Plot SESANS results.
483    """
484    import matplotlib.pyplot as plt  # type: ignore
485    use_data = use_data and data.y is not None
486    use_theory = theory is not None
487    use_resid = resid is not None
488    num_plots = (use_data or use_theory) + use_resid
489
490    if use_data or use_theory:
491        is_tof = (data.lam != data.lam[0]).any()
492        if num_plots > 1:
493            plt.subplot(1, num_plots, 1)
494        if use_data:
495            if is_tof:
496                plt.errorbar(data.x, np.log(data.y)/(data.lam*data.lam),
497                             yerr=data.dy/data.y/(data.lam*data.lam))
498            else:
499                plt.errorbar(data.x, data.y, yerr=data.dy)
500        if theory is not None:
501            if is_tof:
502                plt.plot(data.x, np.log(theory)/(data.lam*data.lam), '-', hold=True)
503            else:
504                plt.plot(data.x, theory, '-', hold=True)
505        if limits is not None:
506            plt.ylim(*limits)
507
508        plt.xlabel('spin echo length ({})'.format(data._xunit))
509        if is_tof:
510            plt.ylabel('(Log (P/P$_0$))/$\lambda^2$')
511        else:
512            plt.ylabel('polarization (P/P0)')
513
514
515    if resid is not None:
516        if num_plots > 1:
517            plt.subplot(1, num_plots, (use_data or use_theory) + 1)
518        plt.plot(data.x, resid, 'x')
519        plt.xlabel('spin echo length ({})'.format(data._xunit))
520        plt.ylabel('residuals (P/P0)')
521
522
523@protect
524def _plot_result2D(data, theory, resid, view, use_data, limits=None):
525    # type: (Data2D, Optional[np.ndarray], Optional[np.ndarray], str, bool, Optional[Tuple[float,float]]) -> None
526    """
527    Plot the data and residuals for 2D data.
528    """
529    import matplotlib.pyplot as plt  # type: ignore
530    use_data = use_data and data.data is not None
531    use_theory = theory is not None
532    use_resid = resid is not None
533    num_plots = use_data + use_theory + use_resid
534
535    # Put theory and data on a common colormap scale
536    vmin, vmax = np.inf, -np.inf
537    target = None # type: Optional[np.ndarray]
538    if use_data:
539        target = data.data[~data.mask]
540        datamin = target[target > 0].min() if view == 'log' else target.min()
541        datamax = target.max()
542        vmin = min(vmin, datamin)
543        vmax = max(vmax, datamax)
544    if use_theory:
545        theorymin = theory[theory > 0].min() if view == 'log' else theory.min()
546        theorymax = theory.max()
547        vmin = min(vmin, theorymin)
548        vmax = max(vmax, theorymax)
549
550    # Override data limits from the caller
551    if limits is not None:
552        vmin, vmax = limits
553
554    # Plot data
555    if use_data:
556        if num_plots > 1:
557            plt.subplot(1, num_plots, 1)
558        _plot_2d_signal(data, target, view=view, vmin=vmin, vmax=vmax)
559        plt.title('data')
560        h = plt.colorbar()
561        h.set_label('$I(q)$')
562
563    # plot theory
564    if use_theory:
565        if num_plots > 1:
566            plt.subplot(1, num_plots, use_data+1)
567        _plot_2d_signal(data, theory, view=view, vmin=vmin, vmax=vmax)
568        plt.title('theory')
569        h = plt.colorbar()
570        h.set_label(r'$\log_{10}I(q)$' if view == 'log'
571                    else r'$q^4 I(q)$' if view == 'q4'
572                    else '$I(q)$')
573
574    # plot resid
575    if use_resid:
576        if num_plots > 1:
577            plt.subplot(1, num_plots, use_data+use_theory+1)
578        _plot_2d_signal(data, resid, view='linear')
579        plt.title('residuals')
580        h = plt.colorbar()
581        h.set_label(r'$\Delta I(q)$')
582
583
584@protect
585def _plot_2d_signal(data, signal, vmin=None, vmax=None, view='log'):
586    # type: (Data2D, np.ndarray, Optional[float], Optional[float], str) -> Tuple[float, float]
587    """
588    Plot the target value for the data.  This could be the data itself,
589    the theory calculation, or the residuals.
590
591    *scale* can be 'log' for log scale data, or 'linear'.
592    """
593    import matplotlib.pyplot as plt  # type: ignore
594    from numpy.ma import masked_array  # type: ignore
595
596    image = np.zeros_like(data.qx_data)
597    image[~data.mask] = signal
598    valid = np.isfinite(image)
599    if view == 'log':
600        valid[valid] = (image[valid] > 0)
601        if vmin is None: vmin = image[valid & ~data.mask].min()
602        if vmax is None: vmax = image[valid & ~data.mask].max()
603        image[valid] = np.log10(image[valid])
604    elif view == 'q4':
605        image[valid] *= (data.qx_data[valid]**2+data.qy_data[valid]**2)**2
606        if vmin is None: vmin = image[valid & ~data.mask].min()
607        if vmax is None: vmax = image[valid & ~data.mask].max()
608    else:
609        if vmin is None: vmin = image[valid & ~data.mask].min()
610        if vmax is None: vmax = image[valid & ~data.mask].max()
611
612    image[~valid | data.mask] = 0
613    #plottable = Iq
614    plottable = masked_array(image, ~valid | data.mask)
615    # Divide range by 10 to convert from angstroms to nanometers
616    xmin, xmax = min(data.qx_data), max(data.qx_data)
617    ymin, ymax = min(data.qy_data), max(data.qy_data)
618    if view == 'log':
619        vmin, vmax = np.log10(vmin), np.log10(vmax)
620    plt.imshow(plottable.reshape(len(data.x_bins), len(data.y_bins)),
621               interpolation='nearest', aspect=1, origin='lower',
622               extent=[xmin, xmax, ymin, ymax], vmin=vmin, vmax=vmax)
623    plt.xlabel("$q_x$/A$^{-1}$")
624    plt.ylabel("$q_y$/A$^{-1}$")
625    return vmin, vmax
626
627def demo():
628    # type: () -> None
629    """
630    Load and plot a SAS dataset.
631    """
632    data = load_data('DEC07086.DAT')
633    set_beam_stop(data, 0.004)
634    plot_data(data)
635    import matplotlib.pyplot as plt  # type: ignore
636    plt.show()
637
638
639if __name__ == "__main__":
640    demo()
Note: See TracBrowser for help on using the repository browser.