source: sasmodels/sasmodels/data.py @ a769b54

core_shell_microgelscostrafo411magnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since a769b54 was a769b54, checked in by Paul Kienzle <pkienzle@…>, 7 years ago

allow sascomp with -data=filename to set q,dq from file

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