• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Open-MSS / MSS / 26680079893

30 May 2026 09:11AM UTC coverage: 70.128% (+0.4%) from 69.766%
26680079893

Pull #3065

github

web-flow
Merge 952b452c6 into 3ad049700
Pull Request #3065: subprocess instead of multiprocessing, refactoring testsetup, enable windows tests

371 of 460 new or added lines in 22 files covered. (80.65%)

63 existing lines in 8 files now uncovered.

14553 of 20752 relevant lines covered (70.13%)

2.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.95
/mslib/mswms/mss_plot_driver.py
1
# -*- coding: utf-8 -*-
2
"""
3

4
    mslib.mswms.mss_plot_driver
5
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~
6

7
    Driver classes to create plots from ECMWF NetCDF data.
8

9
    This file is part of MSS.
10

11
    :copyright: Copyright 2008-2014 Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
12
    :copyright: Copyright 2011-2014 Marc Rautenhaus (mr)
13
    :copyright: Copyright 2016-2026 by the MSS team, see AUTHORS.
14
    :license: APACHE-2.0, see LICENSE for details.
15

16
    Licensed under the Apache License, Version 2.0 (the "License");
17
    you may not use this file except in compliance with the License.
18
    You may obtain a copy of the License at
19

20
       http://www.apache.org/licenses/LICENSE-2.0
21

22
    Unless required by applicable law or agreed to in writing, software
23
    distributed under the License is distributed on an "AS IS" BASIS,
24
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25
    See the License for the specific language governing permissions and
26
    limitations under the License.
27
"""
28

29
from datetime import datetime
4✔
30

31
import logging
4✔
32
import os
4✔
33
from abc import ABCMeta, abstractmethod
4✔
34

35
import numpy as np
4✔
36

37
from mslib.utils import netCDF4tools
4✔
38
import mslib.utils.coordinate as coordinate
4✔
39
from mslib.utils.units import convert_to, units
4✔
40

41

42
class MSSPlotDriver(metaclass=ABCMeta):
4✔
43
    """
44
    Abstract super class for implementing driver classes that provide
45
    access to the MSS data server.
46

47
    The idea of a driver class is to encapsulate all methods related to
48
    loading data fields into memory. A driver can control objects from
49
    plotting classes that provide (a) a list of required variables and
50
    (b) a plotting method that only accepts data fields already loaded into
51
    memory.
52

53
    MSSPlotDriver implements methods that determine, given a list of required
54
    variables from a plotting instance <plot_object> and a forecast time
55
    specified by initialisation and valid time, the corresponding data files.
56
    The files are opened and the NetCDF variable objects are determined.
57

58
    Classes that derive from this class need to implement the two methods
59
    set_plot_parameters() and plot().
60
    """
61

62
    def __init__(self, data_access_object):
4✔
63
        """
64
        Requires an instance of a data access object from the MSS
65
        configuration (i.e. an NWPDataAccess instance).
66
        """
67
        self.data_access = data_access_object
4✔
68
        self.dataset = None
4✔
69
        self.plot_object = None
4✔
70
        self.filenames = []
4✔
71

72
    def close(self):
4✔
73
        """
74
        Close the open NetCDF dataset, if existing, and release its file handle.
75

76
        On Windows an open dataset keeps a lock on the underlying file, so this
77
        should be called explicitly when the driver is no longer needed rather
78
        than relying on __del__ / garbage collection.
79
        """
80
        if self.dataset is not None:
4✔
81
            self.dataset.close()
4✔
82
            self.dataset = None
4✔
83

84
    def __del__(self):
4✔
85
        """
86
        Closes the open NetCDF dataset, if existing.
87
        """
NEW
88
        self.close()
×
89

90
    def _set_time(self, init_time, fc_time):
4✔
91
        """
92
        Open the dataset that corresponds to a forecast field specified
93
        by an initialisation and a valid time.
94

95
        This method
96
          determines the files that correspond to an init time and forecast step
97
          checks if an open NetCDF dataset exists
98
            if yes, checks whether it contains the requested valid time
99
              if not, closes the dataset and opens the corresponding one
100
          loads dimension data if required.
101
        """
102
        if len(self.plot_object.required_datafields) == 0:
4✔
103
            logging.debug("no datasets required.")
×
104
            self.dataset = None
×
105
            self.filenames = []
×
106
            self.init_time = None
×
107
            self.fc_time = None
×
108
            self.times = np.array([])
×
109
            self.lat_data = np.array([])
×
110
            self.lon_data = np.array([])
×
111
            self.lat_order = 1
×
112
            self.vert_data = None
×
113
            self.vert_order = None
×
114
            self.vert_units = None
×
115
            return
×
116

117
        if self.uses_inittime_dimension():
4✔
118
            logging.debug("\trequested initialisation time %s", init_time)
4✔
119
            if fc_time < init_time:
4✔
120
                msg = "Forecast valid time cannot be earlier than " \
×
121
                      "initialisation time."
122
                logging.error(msg)
×
123
                raise ValueError(msg)
×
124
        self.fc_time = fc_time
4✔
125
        logging.debug("\trequested forecast valid time %s", fc_time)
4✔
126

127
        # Check if a dataset is open and if it contains the requested times.
128
        # (a dataset will only be open if the used layer has not changed,
129
        # i.e. the required variables have not changed as well).
130
        if (self.dataset is not None) and (self.init_time == init_time) and (fc_time in self.times):
4✔
131
            logging.debug("\tinit time correct and forecast valid time contained (%s).", fc_time)
4✔
132
            if not self.data_access.is_reload_required(self.filenames):
4✔
133
                return
4✔
134
            logging.debug("need to re-open input files.")
×
135
            self.dataset.close()
×
136
            self.dataset = None
×
137

138
        # Determine the input files from the required variables and the
139
        # requested time:
140

141
        # Create the names of the files containing the required parameters.
142
        self.filenames = []
4✔
143
        for vartype, var, _ in self.plot_object.required_datafields:
4✔
144
            filename = self.data_access.get_filename(
4✔
145
                var, vartype, init_time, fc_time, fullpath=True)
146
            if filename not in self.filenames:
4✔
147
                self.filenames.append(filename)
4✔
148
            logging.debug("\tvariable '%s' requires input file '%s'",
4✔
149
                          var, os.path.basename(filename))
150

151
        if len(self.filenames) == 0:
4✔
152
            raise ValueError("no files found that correspond to the specified "
×
153
                             "datafields. Aborting..")
154

155
        self.init_time = init_time
4✔
156

157
        # Open NetCDF files as one dataset with common dimensions.
158
        logging.debug("opening datasets.")
4✔
159
        dsKWargs = self.data_access.mfDatasetArgs()
4✔
160
        dataset = netCDF4tools.MFDatasetCommonDims(self.filenames, **dsKWargs)
4✔
161

162
        # Load and check time dimension. self.dataset will remain None
163
        # if an Exception is raised here.
164
        timename, timevar = netCDF4tools.identify_CF_time(dataset)
4✔
165

166
        times = netCDF4tools.num2date(timevar[:], timevar.units)
4✔
167
        # removed after discussion, see
168
        # https://mss-devel.slack.com/archives/emerge/p1486658769000007
169
        # if init_time != netCDF4tools.num2date(0, timevar.units):
170
        #     dataset.close()
171
        #     raise ValueError("wrong initialisation time in input")
172

173
        if fc_time not in times:
4✔
174
            msg = f"Forecast valid time '{fc_time}' is not available."
×
175
            logging.error(msg)
×
176
            dataset.close()
×
177
            raise ValueError(msg)
×
178

179
        # Load lat/lon dimensions.
180
        try:
4✔
181
            lat_data, lon_data, lat_order = netCDF4tools.get_latlon_data(dataset)
4✔
182
        except Exception as ex:
×
183
            logging.error("ERROR: %s %s", type(ex), ex)
×
184
            dataset.close()
×
185
            raise
×
186

187
        _, vert_data, vert_orientation, vert_units, _ = netCDF4tools.identify_vertical_axis(dataset)
4✔
188
        self.vert_data = vert_data[:] if vert_data is not None else None
4✔
189
        self.vert_order = vert_orientation
4✔
190
        self.vert_units = vert_units
4✔
191

192
        self.dataset = dataset
4✔
193
        self.times = times
4✔
194
        self.lat_data = lat_data
4✔
195
        self.lon_data = lon_data
4✔
196
        self.lat_order = lat_order
4✔
197

198
        # Identify the variable objects from the NetCDF file that correspond
199
        # to the data fields required by the plot object.
200
        self._find_data_vars()
4✔
201

202
    def _find_data_vars(self):
4✔
203
        """
204
        Find NetCDF variables of required data fields.
205

206
        A dictionary data_vars is created. Its keys are the CF standard names
207
        of the variables provided by the plot object. The values are pointers
208
        to the NetCDF variable objects.
209

210
        <data_vars> can be accessed as <self.data_vars>.
211
        """
212
        self.data_vars = {}
4✔
213
        self.data_units = {}
4✔
214
        for df_type, df_name, _ in self.plot_object.required_datafields:
4✔
215
            varname, var = netCDF4tools.identify_variable(self.dataset, df_name, check=True)
4✔
216
            logging.debug("\tidentified variable <%s> for field <%s>", varname, df_name)
4✔
217
            self.data_vars[df_name] = var
4✔
218
            self.data_units[df_name] = getattr(var, "units", None)
4✔
219

220
    def have_data(self, plot_object, init_time, valid_time):
4✔
221
        """
222
        Checks if this driver has the required data to do the plot
223

224
        This inquires the contained data access class if data is available for
225
        all required data fields for the specified times.
226
        """
227
        return all(
×
228
            self.data_access.have_data(var, vartype, init_time, valid_time)
229
            for vartype, var in plot_object.required_datafields)
230

231
    @abstractmethod
4✔
232
    def set_plot_parameters(self, plot_object, init_time=None, valid_time=None,
4✔
233
                            style=None, bbox=None, figsize=(800, 600),
234
                            noframe=False, require_reload=False, transparent=False,
235
                            mime_type="image/png"):
236
        """
237
        Set parameters controlling the plot.
238

239
        Parameters not passed as arguments are reset to standard values.
240

241
        THIS METHOD NEEDS TO BE REIMPLEMENTED IN ANY CLASS DERIVING FROM
242
        MSSPlotDriver!
243

244
        Derived methods need to call the super method before all other
245
        statements.
246
        """
247
        logging.debug("using plot object '%s'", plot_object.name)
4✔
248
        logging.debug("\tfigure size %s in pixels", figsize)
4✔
249

250
        # If the plot object has been changed, the dataset needs to be reloaded
251
        # (the required variables could have changed).
252
        if self.plot_object is not None:
4✔
253
            require_reload = require_reload or (self.plot_object != plot_object)
4✔
254
        if require_reload and self.dataset is not None:
4✔
255
            self.dataset.close()
4✔
256
            self.dataset = None
4✔
257

258
        self.plot_object = plot_object
4✔
259
        self.figsize = figsize
4✔
260
        self.noframe = noframe
4✔
261
        self.style = style
4✔
262
        self.bbox = bbox
4✔
263
        self.transparent = transparent
4✔
264
        self.mime_type = mime_type
4✔
265

266
        self._set_time(init_time, valid_time)
4✔
267

268
    @abstractmethod
4✔
269
    def update_plot_parameters(self, plot_object=None, figsize=None, style=None,
4✔
270
                               bbox=None, init_time=None, valid_time=None,
271
                               noframe=None, transparent=None, mime_type=None):
272
        """
273
        Update parameters controlling the plot.
274

275
        Similar to set_plot_parameters(), but keeps all parameters already
276
        set except the ones that are specified.
277

278
        THIS METHOD NEEDS TO BE REIMPLEMENTED IN ANY CLASS DERIVING FROM
279
        MSSPlotDriver!
280

281
        Derived methods need to call the super method before all other
282
        statements.
283
        """
284
        plot_object = plot_object if plot_object is not None else self.plot_object
×
285
        figsize = figsize if figsize is not None else self.figsize
×
286
        noframe = noframe if noframe is not None else self.noframe
×
287
        init_time = init_time if init_time is not None else self.init_time
×
288
        valid_time = valid_time if valid_time is not None else self.fc_time
×
289
        style = style if style is not None else self.style
×
290
        bbox = bbox if bbox is not None else self.bbox
×
291
        transparent = transparent if transparent is not None else self.transparent
×
292
        mime_type = mime_type if mime_type is not None else self.mime_type
×
293
        # Explicitly call MSSPlotDriver's set_plot_parameters(). A "self.--"
294
        # call would call the derived class's method and thus reset
295
        # parameters specific to the derived class.
296
        MSSPlotDriver.set_plot_parameters(self, plot_object,
×
297
                                          init_time=init_time,
298
                                          valid_time=valid_time,
299
                                          figsize=figsize,
300
                                          style=style,
301
                                          bbox=bbox,
302
                                          noframe=noframe,
303
                                          transparent=transparent,
304
                                          mime_type=mime_type)
305

306
    @abstractmethod
4✔
307
    def plot(self):
4✔
308
        """
309
        Plot the figure (i.e. load the data fields and call the
310
        corresponding plotting routines of the plot object).
311

312
        THIS METHOD NEEDS TO BE REIMPLEMENTED IN ANY CLASS DERIVING FROM
313
        MSSPlotDriver!
314
        """
315
        pass
×
316

317
    def get_init_times(self):
4✔
318
        """
319
        Returns a list of available forecast init times (base times).
320
        """
321
        return self.data_access.get_init_times()
4✔
322

323
    def get_elevations(self, vert_type):
4✔
324
        """
325
        See ECMWFDataAccess.get_elevations().
326
        """
327
        return self.data_access.get_elevations(vert_type)
4✔
328

329
    def get_elevation_units(self, vert_type):
4✔
330
        """
331
        See ECMWFDataAccess.get_elevation().
332
        """
333
        return self.data_access.get_elevation_units(vert_type)
4✔
334

335
    def get_all_valid_times(self, variable, vartype):
4✔
336
        """
337
        See ECMWFDataAccess.get_all_valid_times().
338
        """
339
        return self.data_access.get_all_valid_times(variable, vartype)
4✔
340

341
    def get_valid_times(self, variable, vartype, init_time):
4✔
342
        """
343
        See ECMWFDataAccess.get_valid_times().
344
        """
345
        return self.data_access.get_valid_times(variable, vartype, init_time)
×
346

347
    def uses_inittime_dimension(self):
4✔
348
        """
349
        Returns whether this driver uses the WMS inittime dimensions.
350
        """
351
        return self.data_access.uses_inittime_dimension()
4✔
352

353
    def uses_validtime_dimension(self):
4✔
354
        """
355
        Returns whether this layer uses the WMS time dimensions.
356
        """
357
        return self.data_access.uses_validtime_dimension()
4✔
358

359

360
class VerticalSectionDriver(MSSPlotDriver):
4✔
361
    """
362
    The vertical section driver is responsible for loading the data that
363
    is to be plotted and for calling the plotting routines (that have
364
    to be registered).
365
    """
366

367
    def set_plot_parameters(self, plot_object=None, vsec_path=None,
4✔
368
                            vsec_numpoints=101, vsec_path_connection='linear',
369
                            vsec_numlabels=10,
370
                            init_time=None, valid_time=None, style=None,
371
                            bbox=None, figsize=(800, 600), noframe=False, draw_verticals=False,
372
                            show=False, transparent=False,
373
                            mime_type="image/png"):
374
        """
375
        """
376
        MSSPlotDriver.set_plot_parameters(self, plot_object,
4✔
377
                                          init_time=init_time,
378
                                          valid_time=valid_time,
379
                                          style=style,
380
                                          bbox=bbox,
381
                                          figsize=figsize, noframe=noframe,
382
                                          transparent=transparent,
383
                                          mime_type=mime_type)
384
        self._set_vertical_section_path(vsec_path, vsec_numpoints,
4✔
385
                                        vsec_path_connection)
386
        self.show = show
4✔
387
        self.vsec_numlabels = vsec_numlabels
4✔
388
        self.draw_verticals = draw_verticals
4✔
389

390
    def update_plot_parameters(self, plot_object=None, vsec_path=None,
4✔
391
                               vsec_numpoints=None, vsec_path_connection=None,
392
                               vsec_numlabels=None,
393
                               init_time=None, valid_time=None, style=None,
394
                               bbox=None, figsize=None, noframe=None, draw_verticals=None, show=None,
395
                               transparent=None, mime_type=None):
396
        """
397
        """
398
        plot_object = plot_object if plot_object is not None else self.plot_object
×
399
        figsize = figsize if figsize is not None else self.figsize
×
400
        noframe = noframe if noframe is not None else self.noframe
×
401
        draw_verticals = draw_verticals if draw_verticals else self.draw_verticals
×
402
        init_time = init_time if init_time is not None else self.init_time
×
403
        valid_time = valid_time if valid_time is not None else self.fc_time
×
404
        style = style if style is not None else self.style
×
405
        bbox = bbox if bbox is not None else self.bbox
×
406
        vsec_path = vsec_path if vsec_path is not None else self.vsec_path
×
407
        vsec_numpoints = vsec_numpoints if vsec_numpoints is not None else self.vsec_numpoints
×
408
        vsec_numlabels = vsec_numlabels if vsec_numlabels is not None else self.vsec_numlabels
×
409
        if vsec_path_connection is None:
×
410
            vsec_path_connection = self.vsec_path_connection
×
411
        show = show if show else self.show
×
412
        transparent = transparent if transparent is not None else self.transparent
×
413
        mime_type = mime_type if mime_type is not None else self.mime_type
×
414
        self.set_plot_parameters(plot_object=plot_object,
×
415
                                 vsec_path=vsec_path,
416
                                 vsec_numpoints=vsec_numpoints,
417
                                 vsec_path_connection=vsec_path_connection,
418
                                 vsec_numlabels=vsec_numlabels,
419
                                 init_time=init_time,
420
                                 valid_time=valid_time,
421
                                 style=style,
422
                                 bbox=bbox,
423
                                 figsize=figsize,
424
                                 noframe=noframe,
425
                                 draw_verticals=draw_verticals,
426
                                 show=show,
427
                                 transparent=transparent,
428
                                 mime_type=mime_type)
429

430
    def _set_vertical_section_path(self, vsec_path, vsec_numpoints=101,
4✔
431
                                   vsec_path_connection='linear'):
432
        """
433
        """
434
        logging.debug("computing %i interpolation points, connection: %s",
4✔
435
                      vsec_numpoints, vsec_path_connection)
436
        self.lats, self.lons = coordinate.path_points(
4✔
437
            [_x[0] for _x in vsec_path],
438
            [_x[1] for _x in vsec_path],
439
            numpoints=vsec_numpoints, connection=vsec_path_connection)
440
        self.lats, self.lons = np.asarray(self.lats), np.asarray(self.lons)
4✔
441
        self.vsec_path = vsec_path
4✔
442
        self.vsec_numpoints = vsec_numpoints
4✔
443
        self.vsec_path_connection = vsec_path_connection
4✔
444

445
    def _load_interpolate_timestep(self):
4✔
446
        """
447
        Load and interpolate the data fields as required by the vertical
448
        section style instance. Only data of time <fc_time> is processed.
449

450
        Shifts the data fields such that the longitudes are in the range
451
        left_longitude .. left_longitude+360, where left_longitude is the
452
        westmost longitude appearing in the list of waypoints minus one
453
        gridpoint (to include all waypoint longitudes).
454

455
        Necessary to prevent data cut-offs in situations where the requested
456
        cross section crosses the data longitude boundaries (e.g. data is
457
        stored on a 0..360 grid, but the path is in the range -10..+20).
458
        """
459
        if self.dataset is None:
4✔
460
            return {}
×
461
        data = {}
4✔
462

463
        timestep = self.times.searchsorted(self.fc_time)
4✔
464
        logging.debug("loading data for time step %s (%s)", timestep, self.fc_time)
4✔
465

466
        # Determine the westmost longitude in the cross-section path. Subtract
467
        # one gridbox size to obtain "left_longitude".
468
        dlon = self.lon_data[1] - self.lon_data[0]
4✔
469
        left_longitude = np.unwrap(self.lons, period=360).min() - dlon
4✔
470
        logging.debug("shifting data grid to gridpoint west of westmost "
4✔
471
                      "longitude in path: %.2f (path %.2f).",
472
                      left_longitude, self.lons.min())
473

474
        # Shift the longitude field such that the data is in the range
475
        # left_longitude .. left_longitude+360.
476
        # NOTE: This does not overwrite self.lon_data (which is required
477
        # in its original form in case other data is loaded while this
478
        # file is open).
479
        lon_data = ((self.lon_data - left_longitude) % 360) + left_longitude
4✔
480
        lon_indices = lon_data.argsort()
4✔
481
        lon_data = lon_data[lon_indices]
4✔
482
        # Identify jump in longitudes due to non-global dataset
483
        dlon_data = np.diff(lon_data)
4✔
484
        jump = np.where(dlon_data > 2 * dlon)[0]
4✔
485

486
        lons = ((self.lons - left_longitude) % 360) + left_longitude
4✔
487

488
        for name, var in self.data_vars.items():
4✔
489
            if len(var.shape) == 4:
4✔
490
                var_data = var[timestep, ::-self.vert_order, ::self.lat_order, :]
4✔
491
            else:
492
                var_data = var[:][timestep, np.newaxis, ::self.lat_order, :]
×
493
            logging.debug("\tLoaded %.2f Mbytes from data field <%s> at timestep %s.",
4✔
494
                          var_data.nbytes / 1048576., name, timestep)
495
            logging.debug("\tVertical dimension direction is %s.",
4✔
496
                          "up" if self.vert_order == 1 else "down")
497
            logging.debug("\tInterpolating to cross-section path.")
4✔
498
            # Re-arange longitude dimension in the data field.
499
            var_data = var_data[:, :, lon_indices]
4✔
500
            if jump:
4✔
501
                logging.debug("\tsetting jump data to NaN at %s", jump)
4✔
502
                var_data = var_data.copy()
4✔
503
                var_data[:, :, jump] = np.nan
4✔
504
            data[name] = coordinate.interpolate_vertsec(var_data, self.lat_data, lon_data, self.lats, lons)
4✔
505
            # Free memory.
506
            del var_data
4✔
507

508
        return data
4✔
509

510
    def shift_data(self):
4✔
511
        """
512
        Shift the data fields such that the longitudes are in the range
513
        left_longitude .. left_longitude+360, where left_longitude is the
514
        westmost longitude appearing in the list of waypoints minus one
515
        gridpoint (to include all waypoint longitudes).
516

517
        Necessary to prevent data cut-offs in situations where the requested
518
        cross section crosses the data longitude boundaries (e.g. data is
519
        stored on a 0..360 grid, but the path is in the range -10..+20).
520
        """
521
        # Determine the leftmost longitude in the plot.
522
        left_longitude = self.lons.min()
×
523
        logging.debug("shifting data grid to leftmost longitude in path "
×
524
                      "(%.2f)..", left_longitude)
525

526
        # Shift the longitude field such that the data is in the range
527
        # left_longitude .. left_longitude+360.
528
        self.lons = ((self.lons - left_longitude) % 360) + left_longitude
×
529
        lon_indices = self.lons.argsort()
×
530
        self.lons = self.lons[lon_indices]
×
531

532
        # Shift data fields correspondingly.
533
        for key in self.data:
×
534
            self.data[key] = self.data[key][:, lon_indices]
×
535

536
    def plot(self):
4✔
537
        """
538
        """
539
        d1 = datetime.now()
4✔
540

541
        # Load and interpolate the data fields as required by the vertical
542
        # section style instance. <data> is a dictionary containing the
543
        # interpolated curtains of the variables identified through CF
544
        # standard names as specified by <self.vsec_style_instance>.
545
        data = self._load_interpolate_timestep()
4✔
546

547
        d2 = datetime.now()
4✔
548
        logging.debug("Loaded and interpolated data (required time %s).", d2 - d1)
4✔
549
        logging.debug("Plotting interpolated curtain.")
4✔
550

551
        if len(self.lat_data) > 1 and len(self.lon_data) > 1:
4✔
552
            resolution = (self.lon_data[1] - self.lon_data[0],
4✔
553
                          self.lat_data[1] - self.lat_data[0])
554
        else:
555
            resolution = (-1, -1)
×
556

557
        if self.mime_type not in ("image/png", "text/xml"):
4✔
558
            raise RuntimeError(f"Unexpected format for vertical sections '{self.mime_type}'.")
4✔
559

560
        # Call the plotting method of the vertical section style instance.
561
        image = self.plot_object.plot_vsection(data, self.lats, self.lons,
4✔
562
                                               valid_time=self.fc_time,
563
                                               init_time=self.init_time,
564
                                               resolution=resolution,
565
                                               bbox=self.bbox,
566
                                               style=self.style,
567
                                               show=self.show,
568
                                               highlight=self.vsec_path,
569
                                               noframe=self.noframe,
570
                                               figsize=self.figsize,
571
                                               draw_verticals=self.draw_verticals,
572
                                               transparent=self.transparent,
573
                                               numlabels=self.vsec_numlabels,
574
                                               mime_type=self.mime_type)
575
        # Free memory.
576
        del data
4✔
577

578
        d3 = datetime.now()
4✔
579
        logging.debug("Finished plotting (required time %s; total "
4✔
580
                      "time %s).\n", d3 - d2, d3 - d1)
581

582
        return image
4✔
583

584

585
class HorizontalSectionDriver(MSSPlotDriver):
4✔
586
    """
587
    The horizontal section driver is responsible for loading the data that
588
    is to be plotted and for calling the plotting routines (that have
589
    to be registered).
590
    """
591

592
    def set_plot_parameters(self, plot_object=None, bbox=None, level=None, crs=None, init_time=None, valid_time=None,
4✔
593
                            style=None, figsize=(800, 600), noframe=False, show=False, transparent=False,
594
                            mime_type="image/png"):
595
        """
596
        """
597
        MSSPlotDriver.set_plot_parameters(self, plot_object,
4✔
598
                                          init_time=init_time,
599
                                          valid_time=valid_time,
600
                                          style=style,
601
                                          bbox=bbox,
602
                                          figsize=figsize, noframe=noframe,
603
                                          transparent=transparent,
604
                                          mime_type=mime_type)
605
        self.level = level
4✔
606
        self.actual_level = None
4✔
607
        self.crs = crs
4✔
608
        self.show = show
4✔
609

610
    def update_plot_parameters(self, plot_object=None, bbox=None, level=None, crs=None, init_time=None, valid_time=None,
4✔
611
                               style=None, figsize=None, noframe=None, show=None, transparent=None, mime_type=None):
612
        """
613
        """
614
        plot_object = plot_object if plot_object is not None else self.plot_object
×
615
        figsize = figsize if figsize is not None else self.figsize
×
616
        noframe = noframe if noframe is not None else self.noframe
×
617
        init_time = init_time if init_time is not None else self.init_time
×
618
        valid_time = valid_time if valid_time is not None else self.fc_time
×
619
        style = style if style is not None else self.style
×
620
        bbox = bbox if bbox is not None else self.bbox
×
621
        level = level if level is not None else self.level
×
622
        crs = crs if crs is not None else self.crs
×
623
        show = show if show is not None else self.show
×
624
        transparent = transparent if transparent is not None else self.transparent
×
625
        mime_type = mime_type if mime_type is not None else self.mime_type
×
626
        self.set_plot_parameters(plot_object=plot_object, bbox=bbox, level=level, crs=crs, init_time=init_time,
×
627
                                 valid_time=valid_time, style=style, figsize=figsize, noframe=noframe, show=show,
628
                                 transparent=transparent, mime_type=mime_type)
629

630
    def _load_timestep(self):
4✔
631
        """
632
        Load the data fields as required by the horizontal section style
633
        instance at the current timestep.
634
        """
635
        if self.dataset is None:
4✔
636
            return {}
×
637
        data = {}
4✔
638
        timestep = self.times.searchsorted(self.fc_time)
4✔
639
        level = None
4✔
640
        if self.level is not None:
4✔
641
            # select the nearest level available
642
            level = np.abs(self.vert_data - self.level).argmin()
4✔
643
            if abs(self.vert_data[level] - self.level) > 1e-3 * np.abs(np.diff(self.vert_data).mean()):
4✔
644
                raise ValueError("Requested elevation not available.")
4✔
645
            self.actual_level = self.vert_data[level]
4✔
646
        logging.debug("loading data for time step %s (%s), level index %s (level %s)",
4✔
647
                      timestep, self.fc_time, level, self.actual_level)
648
        for name, var in self.data_vars.items():
4✔
649
            if level is None or len(var.shape) == 3:
4✔
650
                # 2D fields: time, lat, lon.
651
                var_data = var[timestep, ::self.lat_order, :]
4✔
652
            else:
653
                # 3D fields: time, level, lat, lon.
654
                var_data = var[timestep, level, ::self.lat_order, :]
4✔
655
            logging.debug("\tLoaded %.2f Mbytes from data field <%s>.",
4✔
656
                          var_data.nbytes / 1048576., name)
657
            data[name] = var_data
4✔
658
            # Free memory.
659
            del var_data
4✔
660

661
        return data
4✔
662

663
    def plot(self):
4✔
664
        """
665
        """
666
        d1 = datetime.now()
4✔
667

668
        # Load and interpolate the data fields as required by the horizontal
669
        # section style instance. <data> is a dictionary containing the
670
        # horizontal sections of the variables identified through CF
671
        # standard names as specified by <self.hsec_style_instance>.
672
        data = self._load_timestep()
4✔
673

674
        d2 = datetime.now()
4✔
675
        logging.debug("Loaded data (required time %s).", (d2 - d1))
4✔
676
        logging.debug("Plotting horizontal section.")
4✔
677

678
        if len(self.lat_data) > 1:
4✔
679
            resolution = (self.lat_data[1] - self.lat_data[0])
4✔
680
        else:
681
            resolution = 0
×
682

683
        if self.mime_type != "image/png":
4✔
684
            raise RuntimeError(f"Unexpected format for horizontal sections '{self.mime_type}'.")
4✔
685

686
        # Call the plotting method of the horizontal section style instance.
687
        image = self.plot_object.plot_hsection(data,
4✔
688
                                               self.lat_data,
689
                                               self.lon_data,
690
                                               self.bbox,
691
                                               level=self.actual_level,
692
                                               valid_time=self.fc_time,
693
                                               init_time=self.init_time,
694
                                               resolution=resolution,
695
                                               show=self.show,
696
                                               crs=self.crs,
697
                                               style=self.style,
698
                                               noframe=self.noframe,
699
                                               figsize=self.figsize,
700
                                               transparent=self.transparent)
701
        # Free memory.
702
        del data
4✔
703

704
        d3 = datetime.now()
4✔
705
        logging.debug("Finished plotting (required time %s; total "
4✔
706
                      "time %s).\n", d3 - d2, d3 - d1)
707

708
        return image
4✔
709

710

711
class LinearSectionDriver(VerticalSectionDriver):
4✔
712
    """
713
        The linear plot driver is responsible for loading the data that
714
        is to be plotted and for calling the plotting routines (that have
715
        to be registered).
716
        """
717

718
    def set_plot_parameters(self, plot_object=None, lsec_path=None,
4✔
719
                            lsec_numpoints=101, lsec_path_connection='linear',
720
                            init_time=None, valid_time=None, bbox=None, mime_type=None):
721
        """
722
        """
723
        MSSPlotDriver.set_plot_parameters(self, plot_object,
4✔
724
                                          init_time=init_time,
725
                                          valid_time=valid_time,
726
                                          bbox=bbox, mime_type=mime_type)
727
        self._set_linear_section_path(lsec_path, lsec_numpoints, lsec_path_connection)
4✔
728

729
    def update_plot_parameters(self, plot_object=None, lsec_path=None,
4✔
730
                               lsec_numpoints=None, lsec_path_connection=None,
731
                               init_time=None, valid_time=None, bbox=None, mime_type=None):
732
        """
733
        """
734
        plot_object = plot_object if plot_object is not None else self.plot_object
×
735
        init_time = init_time if init_time is not None else self.init_time
×
736
        valid_time = valid_time if valid_time is not None else self.fc_time
×
737
        bbox = bbox if bbox is not None else self.bbox
×
738
        lsec_path = lsec_path if lsec_path is not None else self.lsec_path
×
739
        lsec_numpoints = lsec_numpoints if lsec_numpoints is not None else self.lsec_numpoints
×
740
        if lsec_path_connection is None:
×
741
            lsec_path_connection = self.lsec_path_connection
×
742
        mime_type = mime_type if mime_type is not None else self.mime_type
×
743
        self.set_plot_parameters(plot_object=plot_object,
×
744
                                 lsec_path=lsec_path,
745
                                 lsec_numpoints=lsec_numpoints,
746
                                 lsec_path_connection=lsec_path_connection,
747
                                 init_time=init_time,
748
                                 valid_time=valid_time,
749
                                 bbox=bbox,
750
                                 mime_type=mime_type)
751

752
    def _set_linear_section_path(self, lsec_path, lsec_numpoints=101, lsec_path_connection='linear'):
4✔
753
        """
754
        """
755
        logging.debug("computing %i interpolation points, connection: %s",
4✔
756
                      lsec_numpoints, lsec_path_connection)
757
        self.lats, self.lons, self.alts = coordinate.path_points(
4✔
758
            [_x[0] for _x in lsec_path],
759
            [_x[1] for _x in lsec_path],
760
            alts=[_x[2] for _x in lsec_path],
761
            numpoints=lsec_numpoints, connection=lsec_path_connection)
762
        self.lats, self.lons, self.alts = [
4✔
763
            np.asarray(_x) for _x in (self.lats, self.lons, self.alts)]
764
        self.lsec_path = lsec_path
4✔
765
        self.lsec_numpoints = lsec_numpoints
4✔
766
        self.lsec_path_connection = lsec_path_connection
4✔
767

768
    def _load_interpolate_timestep(self):
4✔
769
        """
770
        Load and interpolate the data fields as required by the linear
771
        section style instance. Only data of time <fc_time> is processed.
772

773
        Shifts the data fields such that the longitudes are in the range
774
        left_longitude .. left_longitude+360, where left_longitude is the
775
        westmost longitude appearing in the list of waypoints minus one
776
        gridpoint (to include all waypoint longitudes).
777

778
        Necessary to prevent data cut-offs in situations where the requested
779
        cross section crosses the data longitude boundaries (e.g. data is
780
        stored on a 0..360 grid, but the path is in the range -10..+20).
781
        """
782
        if self.dataset is None:
4✔
783
            return {}
×
784
        data = {}
4✔
785

786
        timestep = self.times.searchsorted(self.fc_time)
4✔
787
        logging.debug("loading data for time step %s (%s)", timestep, self.fc_time)
4✔
788

789
        # Determine the westmost longitude in the cross-section path. Subtract
790
        # one gridbox size to obtain "left_longitude".
791
        dlon = self.lon_data[1] - self.lon_data[0]
4✔
792
        left_longitude = np.unwrap(self.lons, period=360).min() - dlon
4✔
793
        logging.debug("shifting data grid to gridpoint west of westmost "
4✔
794
                      "longitude in path: %.2f (path %.2f).",
795
                      left_longitude, self.lons.min())
796

797
        # Shift the longitude field such that the data is in the range
798
        # left_longitude .. left_longitude+360.
799
        # NOTE: This does not overwrite self.lon_data (which is required
800
        # in its original form in case other data is loaded while this
801
        # file is open).
802
        lon_data = ((self.lon_data - left_longitude) % 360) + left_longitude
4✔
803
        lon_indices = lon_data.argsort()
4✔
804
        lon_data = lon_data[lon_indices]
4✔
805
        # Identify jump in longitudes due to non-global dataset
806
        dlon_data = np.diff(lon_data)
4✔
807
        jump = np.where(dlon_data > 2 * dlon)[0]
4✔
808

809
        lons = ((self.lons - left_longitude) % 360) + left_longitude
4✔
810
        factors = []
4✔
811

812
        pressures = None
4✔
813
        if "air_pressure" not in self.data_vars:
4✔
814
            if units(self.vert_units).check("[pressure]"):
4✔
815
                pressures = np.log(convert_to(
4✔
816
                    self.vert_data[::-self.vert_order, np.newaxis],
817
                    self.vert_units, "Pa").repeat(len(self.lats), axis=1))
818
            else:
819
                raise ValueError(
×
820
                    "air_pressure must be available for linear plotting layers "
821
                    "with non-pressure axis. Please add to required_datafields.")
822

823
        # Make sure air_pressure is the first to be evaluated if needed
824
        variables = list(self.data_vars)
4✔
825
        if "air_pressure" in self.data_vars:
4✔
826
            if variables[0] != "air_pressure":
4✔
827
                variables.insert(0, variables.pop(variables.index("air_pressure")))
×
828

829
        for name in variables:
4✔
830
            var = self.data_vars[name]
4✔
831
            data[name] = []
4✔
832
            if len(var.shape) == 4:
4✔
833
                var_data = var[:][timestep, ::-self.vert_order, ::self.lat_order, :]
4✔
834
            else:
835
                var_data = var[:][timestep, np.newaxis, ::self.lat_order, :]
×
836
            logging.debug("\tLoaded %.2f Mbytes from data field <%s> at timestep %s.",
4✔
837
                          var_data.nbytes / 1048576., name, timestep)
838
            logging.debug("\tVertical dimension direction is %s.",
4✔
839
                          "up" if self.vert_order == 1 else "down")
840
            logging.debug("\tInterpolating to cross-section path.")
4✔
841
            # Re-arange longitude dimension in the data field.
842
            var_data = var_data[:, :, lon_indices]
4✔
843
            if jump:
4✔
844
                logging.debug("\tsetting jump data to NaN at %s", jump)
4✔
845
                var_data = var_data.copy()
4✔
846
                var_data[:, :, jump] = np.nan
4✔
847

848
            cross_section = coordinate.interpolate_vertsec(var_data, self.lat_data, lon_data, self.lats, lons)
4✔
849
            # Create vertical interpolation factors and indices for subsequent variables
850
            # TODO: Improve performance for this interpolation in general
851
            if len(factors) == 0:
4✔
852
                if name == "air_pressure":
4✔
853
                    pressures = np.log(convert_to(cross_section, self.data_units[name], "Pa"))
4✔
854
                for index_lonlat, alt in enumerate(np.log(self.alts)):
4✔
855
                    pressure = pressures[:, index_lonlat]
4✔
856
                    idx0 = None
4✔
857
                    for index_altitude in range(len(pressures) - 1):
4✔
858
                        if (pressure[index_altitude] <= alt <= pressure[index_altitude + 1]) or \
4✔
859
                           (pressure[index_altitude] >= alt >= pressure[index_altitude + 1]):
860
                            idx0 = index_altitude
4✔
861
                            break
4✔
862
                    if idx0 is None:
4✔
863
                        factors.append(((0, np.nan), (0, np.nan)))
×
864
                        continue
865

866
                    idx1 = idx0 + 1
4✔
867
                    fac1 = (pressure[idx0] - alt) / (pressure[idx0] - pressure[idx1])
4✔
868
                    fac0 = 1 - fac1
4✔
869
                    assert 0 <= fac0 <= 1, fac0
4✔
870
                    factors.append(((idx0, fac0), (idx1, fac1)))
4✔
871

872
            # Interpolate with the previously calculated pressure indices and factors
873
            for index, ((idx0, w0), (idx1, w1)) in enumerate(factors):
4✔
874
                value = cross_section[idx0, index] * w0 + cross_section[idx1, index] * w1
4✔
875
                data[name].append(value)
4✔
876

877
            # Free memory.
878
            del var_data
4✔
879
            data[name] = np.array(data[name])
4✔
880

881
        return data
4✔
882

883
    def plot(self):
4✔
884
        """
885
        """
886
        d1 = datetime.now()
4✔
887

888
        # Load and interpolate the data fields as required by the linear
889
        # section style instance. <data> is a dictionary containing the
890
        # interpolated curtains of the variables identified through CF
891
        # standard names as specified by <self.lsec_style_instance>.
892
        data = self._load_interpolate_timestep()
4✔
893
        d2 = datetime.now()
4✔
894

895
        if self.mime_type != "text/xml":
4✔
896
            raise RuntimeError(f"Unexpected format for linear sections '{self.mime_type}'.")
4✔
897

898
        # Call the plotting method of the linear section style instance.
899
        image = self.plot_object.plot_lsection(data, self.lats, self.lons,
4✔
900
                                               valid_time=self.fc_time,
901
                                               init_time=self.init_time)
902
        # Free memory.
903
        del data
4✔
904

905
        d3 = datetime.now()
4✔
906
        logging.debug("Finished plotting (required time %s; total "
4✔
907
                      "time %s).\n", d3 - d2, d3 - d1)
908

909
        return image
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc