Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

pycama / src / pycama / AnalysisAndPlot.py @ 834:d989df597b80

History | View | Annotate | Download (11 KB)

1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3

    
4
# Copyright 2016-2017 Maarten Sneep, KNMI
5
#
6
# Redistribution and use in source and binary forms, with or without
7
# modification, are permitted provided that the following conditions are met:
8
#
9
# 1. Redistributions of source code must retain the above copyright notice,
10
#    this list of conditions and the following disclaimer.
11
#
12
# 2. Redistributions in binary form must reproduce the above copyright notice,
13
#    this list of conditions and the following disclaimer in the documentation
14
#    and/or other materials provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
20
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
23
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26

    
27
## \file AnalysisAndPlot.py
28
#  Base class for an analysis class. All 'actions' in PyCAMA are represented by a sub-class of the class defined in this file.
29
# @author Maarten Sneep
30

    
31
import math
32
import logging
33
from collections import OrderedDict
34

    
35
import numpy as np
36

    
37
from .utilities import *
38

    
39
## Base class for an analysis class.
40
#
41
# All 'actions' in PyCAMA are represented by a subclass of the pycama.AnalysisAndPlot.AnalysisAndPlot class.
42
#
43
# This class (or rather its sub-classes) are responsible for extracting the summarizing
44
# data after the data and meta-data have been read by the Reader class.
45
# After extraction they write the data to the netCDF4 output file. This is the operational part of PyCAMA.
46
#
47
# Another responsibility is to be able to restore the object based on the data in the netCDF4 output file.
48
# This is for plotting in phase 2 of a PyCAMA run, the report generation.
49
#
50
class AnalysisAndPlot(object):
51

    
52
    ## The Constructor
53
    #
54
    #  @param reader_data a pycama.Reader.Reader object that has ingested data from S5P L2 files.
55
    #  @param filename a netCDF file with data for this plot type.
56
    #  @param time_index time index to read from filename
57
    #  @param kwargs Other arguments specific to the subclass, these are passed on to the setup() method.
58
    #
59
    #  @Note Either `reader_data` or both `filename` and `time_index` must be supplied.
60
    #
61
    def __init__(self, reader_data=None, filename=None, time_index=None, exclude=None, **kwargs):
62
        ## logging.Logger instance for messaging
63
        self.logger = logging.getLogger('PyCAMA')
64
        self.product = None
65
        self.processing_mode = None
66

    
67
        if reader_data is None:
68
            if time_index is not None:
69
                self.logger.info("Ingesting '%s' data for index %d", self.__class__.__name__.replace("Plot", "").lower(), time_index)
70
            else:
71
                self.logger.info("Ingesting '%s' data", self.__class__.__name__.replace("Plot", "").lower())
72
        
73
        ## The list of variables (indexed by variable name)
74
        self.variables = OrderedDict()
75

    
76
        ## Basic metadata for the variables
77
        self.variables_meta = OrderedDict()
78

    
79
        ## Link to the pycama.Reader.Reader instance with the input data (or None, in case we are plotting).
80
        self.input_variables = reader_data
81

    
82
        if hasattr(self, 'setup'):
83
            self.setup(**kwargs)
84
        else:
85
            self.logger.debug("The class %s does not have a setup() method", self.__class__.__name__)
86

    
87
        self.success = False
88
        if self.input_variables is not None:
89
            ## Current time index
90
            self.time_index_in_output = self.input_variables.time_index_in_output
91
            self.add_raw_variables()
92
            self.product = self.input_variables.product
93
            self.processing_mode = self.input_variables.mode
94
        else:
95
            self.time_index_in_output = None
96
            if filename is not None and time_index is not None:
97
                self.success = self.ingest(filename, time_index, exclude=exclude)
98
                self.product, self.processing_mode = read_product_and_mode_from_file(filename)
99
                if not self.success:
100
                    self.logger.warning("Ingestion not successful for class %s", self.__class__.__name__)
101

    
102

    
103
    ## Print a progress message
104
    #
105
    #  @param pct The percentage of completion (for the current phase).
106
    def progress(self, pct):
107
        self.logger.progress("{0}:{1:5.1f}%".format(self.__class__.__name__.replace("Plot", "").lower(), float(pct)))
108

    
109
    ## The name of the group in the output file where this class should write its extracted data.
110
    #
111
    # Subclasses should use this property to obtain a unique and consistent name for the groupd in which they write to the output.
112
    @property
113
    def storage_group_name(self):
114
        return self.__class__.__name__.lower() + '_data'
115

    
116
    ## Crude mode finder
117
    #
118
    #  @param x the x-axis (bin centers) of the histogram
119
    #  @param h the histogram
120
    def mode(self, x, h, bounds=None):
121
        return x[np.argmax(h)]
122

    
123
    ## Include variable in output?
124
    #
125
    #  Determine if a variable should be included in the output.
126
    #
127
    #  Part of this class and not of the pycama.Variable.Variable class because subclasses may want to override this method.
128
    #
129
    #  @param variable a pycama.Variable.Variable instance.
130
    #
131
    def include_var(self, variable):
132
        return variable.show
133

    
134
    ## Add input variables when they should be plotted for this class.
135
    def add_raw_variables(self):
136
        for var in self.input_variables.variables.values():
137
            if not self.include_var(var):
138
                continue
139
            self.add_variable(var)
140

    
141
    ## A an input variable.
142
    #
143
    # @param var a pycama.Variable.Variable instance.
144
    #
145
    # This method copies metadata. Subclasses should reserve storage,
146
    # and make sure the method in the superclass (i.e. this method) gets called.
147
    #
148
    def add_variable(self, var):
149
        self.variables_meta[var.name] = dict(units=var.units,
150
                                             title=var.title,
151
                                             data_range=var.data_range,
152
                                             color_scale=var.color_scale,
153
                                             log_range=var.log_range)
154

    
155
    ## Extract the required information from the input data.
156
    #
157
    # Expected to be overridden in subclasses.
158
    def process(self):
159
        raise NotImplementedError("Method must be overridden in a subclass.")
160

    
161
    ## Read processed data from the input file, for specified time index.
162
    #
163
    #  @param fname      The pycama data file (netCDF4).
164
    #  @param time_index The time-index to read.
165
    #
166
    #  Expected to be overridden in subclasses.
167
    #
168
    def ingest(self, fname, time_index, exclude=None):
169
        raise NotImplementedError("Method must be overridden in a subclass.")
170

    
171
    ## Write processed data to output netcdf file.
172
    #
173
    #  @param fname File to write to
174
    #  @param mode  Writing mode, defaults to append.
175
    #
176
    #  Expected to be overridden in subclasses.
177
    #
178
    def dump(self, fname, mode='a'):
179
        raise NotImplementedError("Method must be overridden in a subclass.")
180

    
181
    ## Merge data into a combined dataset.
182
    #
183
    #  @param other The object to be added to self.
184
    #
185
    #  Expected to be overridden in subclasses.
186
    #
187
    def __iadd__(self, other):
188
        raise NotImplementedError("Method must be overridden in a subclass.")
189

    
190
    ## A list of variable names available for plotting
191
    @property
192
    def variable_names(self):
193
        return list(self.variables.keys())
194

    
195
    ## Make a plot of a specified variable
196
    #
197
    #  @param varname The name of the variable to plot.
198
    #  @param figure  The matplotlib.figure.Figure instance to plot to.
199
    #  @param ax      The matplotlib.axes.Axes object to use. Default is None, in that case `ax = matplotlib.pyplot.gca()` shall be used.
200
    #  @param kwargs  Other keyword parameters (sub-class dependent)
201
    #
202
    #  Expected to be overridden in subclasses.
203
    #
204
    #  The figure, and possibly the axes into which the result should
205
    #  be plotted must be passed in. Figure creation and closure must be
206
    #  done by the caller, the caller is also responsible for grouping (if required).
207
    #  If the axes are not supplied, the current graphics context is obtained by calling `matplotlib.pyplot.gca()`.
208
    #
209
    #  @return A boolean. When `True` the plotting was succesful, and the result
210
    #  can be included in the output. When `False` the plot was not succesful and the figure will be discarded.
211
    def plot(self, varname, figure, ax=None, **kwargs):
212
        raise NotImplementedError("Method must be overridden in a subclass.")
213

    
214
    ## What is our plot like (for messages).
215
    @property
216
    def plot_type(self):
217
        return self.__class__.__name__.lower().replace('plot', '')
218

    
219
    ## Pretty-print single numbers
220
    #
221
    #  The result is \f$v\f$ when no exponents are needed, or
222
    #  \f$v \times 10^{n}\f$ otherwise.
223
    #
224
    # @param v value
225
    # @return A LaTeX formatted string
226
    #
227
    def number_pretty_printer(self, v, limits=None):
228
        if v is None or not np.isfinite(float(v)):
229
            return "-"
230

    
231
        if limits is None:
232
            limits = [-1, 3]
233

    
234
        try:
235
            l = math.log10(math.fabs(float(v)))
236
        except ValueError:
237
            return "{0:.1f}".format(float(v))
238

    
239
        if limits[0] <= l <= limits[1]:
240
            fmt = "{{0:.{0:d}f}}".format(int(limits[1]-l))
241
            return fmt.format(float(v))
242
        else:
243
            w = "{0:.4e}".format(float(v)).split('e')
244
            return r"{0:.3f}\times10^{{{1}}}".format(float(w[0]), int(w[1]))
245

    
246
    ## Pretty-print numbers, \f$v \pm e\f$.
247
    #
248
    #  The result is \f$v \pm e\f$ when no exponents are needed, or
249
    #  \f$(v \pm e) \times 10^{3}\f$ (or whatever the required common exponent is) otherwise.
250
    #
251
    # @param v value
252
    # @param e precision
253
    # @return A LaTeX formatted string
254
    #
255
    def number_pretty_printer_pair(self, v, e, limits=None):
256
        if v is None or e is None or not np.isfinite(float(v)) or not np.isfinite(float(e)):
257
            return "-"
258
        if limits is None:
259
            limits = [-1, 3]
260

    
261
        try:
262
            l = math.log10(math.fabs(float(v)))
263
        except ValueError:
264
            return r"{0:.1f}\pm{1:.2f}".format(float(v), float(e))
265

    
266
        if limits[0] <= l <= limits[1]:
267
            fmt = "{{0:.{0:d}f}}\pm{{1:.{0:d}f}}".format(int(limits[1]-l))
268
            return fmt.format(float(v), float(e))
269
        else:
270
            scale_exp = int(math.floor(l) if l < 0 else math.ceil(l))
271
            return r"({0:.3f}\pm{1:.3f})\times10^{{{2}}}".format(float(v)/10**scale_exp, float(e)/10**scale_exp, scale_exp)