source: sasmodels/sasmodels/data.py @ a1c5758

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

Merge branch 'master' into ticket-776-orientation

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