diff --git a/.gitignore b/.gitignore
index ef35fc65..8979fceb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,6 +55,22 @@ docs/_build/
.docker-current
.eggs
+# tests
.pytest_cache
tests/*.idx
+tests/data
+major_basins_of_the_world_0_0_0*
+Major_Basins_of_the_World.*
+zzz_for_spectra.grib
+# diagnostics
+diag/data
+diag/mem_result.yaml
+diag/.benchmarks
+
+# vscode
+.vscode/
+.env
+
+# mac
+.DS_Store
diff --git a/.travis.yml b/.travis.yml
index 07f0ee15..3bdb389f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -38,12 +38,12 @@ before_install:
# auto-yes for conda
- conda config --set always_yes yes
# update conda
- #- conda update -n base -c conda-forge conda
+ - conda update -n base -c conda-forge conda
install:
# install deps
- - pip install netcdf4 xarray cfgrib pytest pytest-cov black coveralls
- - conda install -c conda-forge metview-batch
+ - pip install netcdf4 xarray cfgrib scipy pytest pytest-cov black coveralls PyYAML
+ - conda install -c conda-forge "metview-batch>=5.14.1"
#---------------------------------#
# build configuration #
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 51bde4b8..9268575e 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,7 +1,133 @@
Changelog for Metview's Python interface
+
========================================
+1.16.1
+-------------------
+- added support for numpy 2.0
+
+
+1.16.0
+-------------------
+- added helper functions to aid automatic conversion from Macro to Python
+- added support for reading PNG files
+- added string() function from Metview
+- fixed warning message in select() method with pandas 2.2
+
+
+1.15.0
+------------------
+- Added arguments() function to mimic the behaviour of the Macro function of the same name
+- Fixed case of mean(dim=...) where the input fieldset is not a complete hypercube
+
+
+1.14.0
+------------------
+- Fieldset function stdev() now accepts 'dim' argument to compute over given dimension
+- added new function plot_xs_avg() to provide high-level plotting interface for average cross sections
+- Fixed issue where functions that take numpy arrays as input did not work properly with multi-dimensional arrays
+
+
+1.13.1
+------------------
+- fixed memory leaks when passing strings, numbers and dates from the binary layer to the Python layer
+
+
+1.13.0
+------------------
+- Fieldset functions mean() and sum() now accept 'dim' argument to compute over given dimension
+- function gallery.load_dataset() now downloads from a new server
+- added new functions smooth_n_point() and smooth_gaussian() to perform spatial smoothing on fieldsets with lat-lon grids
+- added new function convolve() to perform spatial 2D convolution on fieldsets with lat-lon grids
+
+
+1.12.0
+------------------
+- add 'reverse' and/or operators between Fieldsets and bools (e.g. 'True & my_fieldset')
+- fixed issue where automatic indexing of a dataset fails
+
+
+1.11.0
+------------------
+- make fieldset parameter selector operator work in pure Python Fieldset
+- make vector data extraction operator work for 100m and 200m wind
+- select(): do not try to interpret shortName values as vector field names
+- select(): do not apply sorting on results
+- Fieldsets: add sort method based on indexed metadata
+- select(): fix issue where cannot filter by date when data date has no year component (e.g. monthly climatologies)
+- Requests: allow True/False as argument values when creating Requests
+
+
+1.10.5
+------------------
+- fix for automatic indexing of dataset
+
+
+1.10.0
+------------------
+- experimental pure Python implementation of Fieldset - for internal testing at the moment
+
+
+1.9.0
+------------------
+- the plot functions now automatically plot inline if running inside a Jupyter notebook
+ - it is no longer necessary to call setoutput('jupyter')
+ - call setoutput('screen') to force the interactive plot window to appear
+- inline plots in Jupyter notebooks will be automatically trimmed of surrounding whitespace if pillow is installed
+- new functions to build popup dialogs and read in user input. Available via the newly added ui module.
+ - ui.dialog()
+ - ui.any()
+ - ui.colour()
+ - ui.icon()
+ - ui.option_menu()
+ - ui.slider()
+ - ui.toggle()
+- added high-level plotting functions to be used with Datasets or in Jupyter notebooks
+ - plot_maps()
+ - plot_diff_maps()
+ - plot_xs()
+ - plot_rmse()
+- new object Track to represent a storm track
+- new function make_geoview() to generate a geoview object with predefined settings
+- new interface for Datasets
+ - a Dataset represents a collection of data files (GRIB and CSV) and a set of predefined styles to visualise the data. Ideal for training courses or case studies.
+ - see Jupyter notebook example at https://metview.readthedocs.io/en/latest/notebook_gallery.html
+- added new keyword argument called check_local to gallery.load_dataset(). If it is True and the data file exists locally it will not be downloaded.
+- fixed issue when describe() crashed when called with a paramId
+
+
+1.8.1
+------------------
+- fixed case where map_area_gallery() crashed
+- fixed case where map_style_gallery() crashed
+- fixed issue where plot_maps() could not plot wind data
+- fixed issue where a style could not be updated when verb argument is specified
+
+
+1.8.0
+------------------
+- new functions/methods on Fieldset to give an overview of contents:
+ - fs.describe()
+ - fs.describe("tp")
+ - fs.ls()
+ - see Jupyter notebook example at https://metview.readthedocs.io/en/latest/notebook_gallery.html
+- new GRIB filtering function, select(), offers different filtering options from read() and is faster
+ - see Jupyter notebook example at https://metview.readthedocs.io/en/latest/notebook_gallery.html
+- new shorthand way to select parameters from Fieldsets, e.g.
+ - g = fs["t"]
+ - g = fs["t500"]
+ - g = fs["t500hPa"]
+- the Fieldset constructor can now take a list of paths to GRIB files or a wildcard:
+ - e.g. a = mv.Fieldset(path=["/path1/to/data1.grib", "relpath/data2.grib"])
+ - e.g. a = mv.Fieldset(path="data/*.grib")
+- the result of a call to mcont() etc can now be modified, e.g.
+ - c = mv.mcont() ; c["contour_line_colour"] = "green" ; mv.plot(data, c)
+ - gv.update({"MAP_COASTLINE_land_SHADE_COLOUR": "green"}, sub="COASTlines")
+- improved the output of print(Fieldset):
+ - "Fieldset (6 fields)"
+
+
1.7.2
------------------
- new argument to setoutput(plot_widget=) - default True, set False to allow images to be saved into the notebook
diff --git a/MANIFEST.in b/MANIFEST.in
index e78cc396..ac67a476 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -4,6 +4,7 @@ include LICENSE
include Makefile
include *.rst
include tox.ini
+graft metview/etc
recursive-include docs *.gitkeep
recursive-include docs *.py
diff --git a/README.rst b/README.rst
index c66dbc67..64bb5316 100644
--- a/README.rst
+++ b/README.rst
@@ -3,7 +3,7 @@ Metview Python bindings
=======================
Python interface to Metview, a meteorological workstation and batch system for accessing, examining, manipulating and visualising meteorological data.
-See documentation at https://confluence.ecmwf.int/metview/Metview's+Python+Interface
+See documentation at https://metview.readthedocs.io/en/latest/index.html
Try the example notebooks on Binder!
@@ -18,8 +18,8 @@ Requirements
------------
- A working Metview 5 installation (at least version 5.0.3, ideally 5.3.0 or above), either from binaries or built from source.
- Binary installation packages are available for many Linux distributions.
- See https://confluence.ecmwf.int/metview/Releases
+ Conda packages are available for Linux, and native packages are available for many Linux distributions.
+ See https://metview.readthedocs.io/en/latest/install.html
- An alternative is to build from the Metview Source Bundle.
See https://confluence.ecmwf.int/metview/The+Metview+Source+Bundle
@@ -38,6 +38,11 @@ The package is installed from PyPI with::
$ pip install metview
+or from conda-forge with::
+
+ $ conda install metview-python -c conda-forge
+
+
Test
----
@@ -81,7 +86,7 @@ Code quality .. image:: https://travis-ci.com/ecmwf/metview-python.svg?branch=m
License
-------
-Copyright 2017-2020 European Centre for Medium-Range Weather Forecasts (ECMWF).
+Copyright 2017-2021 European Centre for Medium-Range Weather Forecasts (ECMWF).
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/docs/conf.py b/docs/conf.py
index 645dc2ce..5cda8703 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -52,8 +52,8 @@
master_doc = "index"
# General information about the project.
-project = u"metview"
-copyright = u"2017-2019, European Centre for Medium-Range Weather Forecasts (ECMWF)."
+project = "metview"
+copyright = "2017-2019, European Centre for Medium-Range Weather Forecasts (ECMWF)."
# The version info for the project you're documenting, acts as replacement
# for |version| and |release|, also used in various other places throughout
@@ -203,7 +203,7 @@
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
- ("index", "metview.tex", u"metview Documentation", u"Metview python API", "manual"),
+ ("index", "metview.tex", "metview Documentation", "Metview python API", "manual"),
]
# The name of an image file (relative to this directory) to place at
@@ -231,7 +231,7 @@
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
-man_pages = [("index", "metview", u"metview Documentation", [u"Metview python API"], 1)]
+man_pages = [("index", "metview", "metview Documentation", ["Metview python API"], 1)]
# If true, show URL addresses after external links.
# man_show_urls = False
@@ -246,8 +246,8 @@
(
"index",
"metview",
- u"metview Documentation",
- u"Metview python API",
+ "metview Documentation",
+ "Metview python API",
"metview",
"One line description of project.",
"Miscellaneous",
diff --git a/environment.yml b/environment.yml
index a097c38f..86271b55 100644
--- a/environment.yml
+++ b/environment.yml
@@ -1,7 +1,7 @@
channels:
- conda-forge
dependencies:
- - metview-batch
+ - metview-batch >=5.16.0
- pip
- pip:
- metview >=1.4.0
diff --git a/metview/__init__.py b/metview/__init__.py
index cc68a97f..4a02c67d 100644
--- a/metview/__init__.py
+++ b/metview/__init__.py
@@ -8,6 +8,7 @@
# nor does it submit to any jurisdiction.
# requires a Python 3 interpreter
+import os
import sys
if sys.version_info[0] < 3: # pragma: no cover
@@ -22,13 +23,22 @@
# catch errors differently
if len(sys.argv) != 2 or sys.argv[0] != "-m" or sys.argv[1] != "selfcheck":
+ try:
+ from . import bindings as _bindings
- from . import bindings as _bindings
+ _bindings.bind_functions(globals(), module_name=__name__)
- _bindings.bind_functions(globals(), module_name=__name__)
+ # Remove "_bindings" from the public API.
+ del _bindings
- # Remove "_bindings" from the public API.
- del _bindings
+ from . import gallery
+ from . import style
+ from metview.metviewpy import indexer
+ from . import title
+ from . import layout
+ from . import ui
+ from . import compat
-
-from . import gallery
+ except Exception as exp:
+ if "METVIEW_PYTHON_ONLY" not in os.environ:
+ raise exp
diff --git a/metview/bindings.py b/metview/bindings.py
index 3558a430..4f0c40af 100644
--- a/metview/bindings.py
+++ b/metview/bindings.py
@@ -7,21 +7,38 @@
# granted to it by virtue of its status as an intergovernmental organisation
# nor does it submit to any jurisdiction.
-
+import builtins
import datetime
+from enum import Enum
import keyword
+import logging
import os
import pkgutil
import signal
import tempfile
-import builtins
-from enum import Enum
import cffi
import numpy as np
+from metview.metviewpy.indexdb import FieldsetDb
+from metview.dataset import Dataset
+from metview.style import (
+ GeoView,
+ Style,
+ Visdef,
+ map_area_gallery,
+ map_style_gallery,
+ make_geoview,
+)
+from metview import plotting
+from metview.metviewpy.ipython import is_ipython_active, import_widgets
+from metview.metviewpy import utils
+
+__version__ = "1.16.1"
-__version__ = "1.7.2"
+
+# logging.basicConfig(level=logging.DEBUG, format="%(levelname)s - %(message)s")
+LOG = logging.getLogger(__name__)
def string_from_ffi(s):
@@ -85,11 +102,14 @@ def __init__(self):
try:
subprocess.Popen(metview_flags)
except Exception as exp: # pragma: no cover
- print(
- "Could not run the Metview executable ('" + metview_startup_cmd + "'); "
- "check that the binaries for Metview (version 5 at least) are installed "
- "and are in the PATH."
- )
+ if "METVIEW_PYTHON_ONLY" not in os.environ:
+ print(
+ "Could not run the Metview executable ('"
+ + metview_startup_cmd
+ + "'); "
+ "check that the binaries for Metview (version 5 at least) are installed "
+ "and are in the PATH."
+ )
raise exp
# wait for Metview to respond...
@@ -259,48 +279,55 @@ class Request(dict, Value):
verb = "UNKNOWN"
def __init__(self, req, myverb=None):
- self.val_pointer = None
- # initialise from Python object (dict/Request)
+ if isinstance(req, Request): # copy an existing Request
+ self.xx = _request(req.get_verb(), req) # to avoid deletion of Macro object
+ self.val_pointer = self.xx.val_pointer
+ self.verb = req.get_verb()
+ self.update(req) # copy into dict
+ return
+
+ if myverb:
+ self.verb = myverb
if isinstance(req, dict):
- self.update(req)
- self.to_metview_style()
- if isinstance(req, Request):
- self.verb = req.verb
- self.val_pointer = req.val_pointer
- else:
- if myverb is not None:
- self.set_verb(myverb)
+ self.create_new(self.verb, req)
+ return
# initialise from a Macro pointer
else:
Value.__init__(self, req)
self.verb = string_from_ffi(lib.p_get_req_verb(req))
- n = lib.p_get_req_num_params(req)
- for i in range(0, n):
- param = string_from_ffi(lib.p_get_req_param(req, i))
- self[param] = self[param]
- # self['_MACRO'] = 'BLANK'
- # self['_PATH'] = 'BLANK'
+ super().update(self.to_dict()) # update dict
def __str__(self):
return "VERB: " + self.verb + super().__str__()
- # translate Python classes into Metview ones where needed
- def to_metview_style(self):
- for k in list(self):
+ def to_dict(self):
+ keys = _keywords(self)
+ d = {}
+ for k in keys:
+ d[k] = self[k]
+ return d
+
+ # translate Python classes into Metview ones where needed - single parameter
+ def item_to_metview_style(self, key, value):
+ modified = False
+ delete_original_key = False
- # bool -> on/off
- v = self.get(k) # self[k] returns 1 for True
- if isinstance(v, bool):
- conversion_dict = {True: "on", False: "off"}
- self[k] = conversion_dict[v]
+ # bool -> on/off
+ if isinstance(value, bool):
+ conversion_dict = {True: "on", False: "off"}
+ value = conversion_dict[value]
+ modified = True
- # class_ -> class (because 'class' is a Python keyword and cannot be
- # used as a named parameter)
- elif k == "class_":
- self["class"] = v
- del self["class_"]
+ # class_ -> class (because 'class' is a Python keyword and cannot be
+ # used as a named parameter)
+ elif key == "class_":
+ key = "class"
+ delete_original_key = True
+ modified = True
+
+ return (key, value, modified, delete_original_key)
def set_verb(self, v):
self.verb = v
@@ -308,27 +335,52 @@ def set_verb(self, v):
def get_verb(self):
return self.verb
+ def create_new(self, rverb, rdict):
+ r = definition() # new, empty definition
+ self.val_pointer = r.val_pointer
+ self.xx = r # to avoid deletion of Macro object
+ self.update(rdict) # will set all parameters via __setitem__
+ return
+
def push(self):
# if we have a pointer to a Metview Value, then use that because it's more
# complete than the dict
if self.val_pointer:
Value.push(self)
else:
- r = lib.p_new_request(self.verb.encode("utf-8"))
-
- # to populate a request on the Macro side, we push each
- # value onto its stack, and then tell it to create a new
- # parameter with that name for the request. This allows us to
- # use Macro to handle the addition of complex data types to
- # a request
- for k, v in self.items():
- push_arg(v)
- lib.p_set_request_value_from_pop(r, k.encode("utf-8"))
-
+ self.create_new(self.verb, self)
lib.p_push_request(r)
+ def update(self, items, sub=""):
+ if sub:
+ if not isinstance(sub, str):
+ raise IndexError("sub argument should be a string")
+ subreq = self[sub.upper()]
+ if subreq:
+ subreq.update(items)
+ self[sub] = subreq
+ else:
+ raise IndexError("'" + sub + "' not a valid subrequest in " + str(self))
+ else:
+ for key in items:
+ self.__setitem__(key, items[key])
+
def __getitem__(self, index):
- return subset(self, index)
+ item = subset(self, index)
+ # subrequests can return '#' if not uppercase
+ if isinstance(item, str) and item == "#":
+ item = subset(self, index.upper())
+ return item
+
+ def __setitem__(self, index, value):
+ if (self.val_pointer) and (value is not None):
+ new_key, new_val, _, _ = self.item_to_metview_style(index, value)
+ push_arg(new_key)
+ push_arg(new_val)
+ lib.p_set_subvalue_from_arg_stack(self.val_pointer)
+ dict.__setitem__(self, new_key, new_val)
+ else:
+ dict.__setitem__(self, index, value)
def push_bytes(b):
@@ -374,18 +426,18 @@ def push_vector(npa):
dtype = npa.dtype
if dtype == np.float64: # can directly pass the data buffer
cffi_buffer = ffi.cast("double*", npa.ctypes.data)
- lib.p_push_vector_from_double_array(cffi_buffer, len(npa), np.nan)
+ lib.p_push_vector_from_double_array(cffi_buffer, npa.size, np.nan)
elif dtype == np.float32: # can directly pass the data buffer
cffi_buffer = ffi.cast("float*", npa.ctypes.data)
- lib.p_push_vector_from_float32_array(cffi_buffer, len(npa), np.nan)
+ lib.p_push_vector_from_float32_array(cffi_buffer, npa.size, np.nan)
elif dtype == bool: # convert first to float32
f32_array = npa.astype(np.float32)
cffi_buffer = ffi.cast("float*", f32_array.ctypes.data)
- lib.p_push_vector_from_float32_array(cffi_buffer, len(f32_array), np.nan)
+ lib.p_push_vector_from_float32_array(cffi_buffer, f32_array.size, np.nan)
elif dtype == int: # convert first to float64
f64_array = npa.astype(np.float64)
cffi_buffer = ffi.cast("double*", f64_array.ctypes.data)
- lib.p_push_vector_from_double_array(cffi_buffer, len(f64_array), np.nan)
+ lib.p_push_vector_from_double_array(cffi_buffer, f64_array.size, np.nan)
else:
raise TypeError(
"Only float32 and float64 numPy arrays can be passed to Metview, not ",
@@ -393,6 +445,39 @@ def push_vector(npa):
)
+def push_style_object(s):
+ r = s.to_request()
+ if isinstance(r, list):
+ push_list(r)
+ else:
+ r.push()
+
+
+def valid_date(*args, base=None, step=None, step_units=None):
+ if len(args) != 0:
+ return call("valid_date", *args)
+ else:
+ assert isinstance(base, datetime.datetime)
+ step = [] if step is None else step
+ step_units = datetime.timedelta(hours=1) if step_units is None else step_units
+ if not isinstance(step, list):
+ step = [step]
+ return [base + step_units * int(x) for x in step]
+
+
+def sort(*args, **kwargs):
+ if "_cpp_implementation" in kwargs:
+ return call("sort", *args)
+ elif len(args) != 0 and isinstance(args[0], Fieldset):
+ if len(args) == 1:
+ return args[0].sort(**kwargs)
+ else:
+ args = args[1:]
+ return args[0].sort(*args, **kwargs)
+ else:
+ return call("sort", *args)
+
+
class File(Value):
def __init__(self, val_pointer):
Value.__init__(self, val_pointer)
@@ -474,9 +559,15 @@ def __abs__(self):
def __and__(self, other):
return met_and(self, other)
+ def __rand__(self, other):
+ return met_and(other, self)
+
def __or__(self, other):
return met_or(self, other)
+ def __ror__(self, other):
+ return met_or(other, self)
+
def __invert__(self):
return met_not(self)
@@ -575,13 +666,24 @@ def __init__(self, val_pointer=None, path=None, fields=None):
element_types=Fieldset,
support_slicing=True,
)
+ self._db = None
+ self._ds_param_info = None
+ self._label = ""
if (path is not None) and (fields is not None):
raise ValueError("Fieldset cannot take both path and fields")
if path is not None:
- temp = read(path)
- self.steal_val_pointer(temp)
+ if isinstance(path, list):
+ v = []
+ for p in path:
+ v.extend(utils.get_file_list(p))
+ path = v
+ else:
+ path = utils.get_file_list(path)
+
+ # fill the 'fields' var - it will be used a few lines down
+ fields = [read(p) for p in path]
if fields is not None:
for f in fields:
@@ -592,6 +694,7 @@ def append(self, other):
if self.val_pointer is not None: # we will overwrite ourselves, so delete
lib.p_destroy_value(self.val_pointer)
self.steal_val_pointer(temp)
+ self._db = None
def to_dataset(self, **kwarg):
# soft dependency on cfgrib
@@ -603,6 +706,163 @@ def to_dataset(self, **kwarg):
dataset = xr.open_dataset(self.url(), engine="cfgrib", backend_kwargs=kwarg)
return dataset
+ def index(self, path=""):
+ pass
+
+ def load_index(self, path):
+ pass
+
+ def _scan(self):
+ if self._db is None:
+ self._db = FieldsetDb(fs=self)
+ self._db.scan()
+
+ def _get_db(self):
+ if self._db is None:
+ self._db = FieldsetDb(fs=self)
+ assert self._db is not None
+ return self._db
+
+ def select(self, *args, **kwargs):
+ if len(args) == 1 and isinstance(args[0], dict):
+ return self._get_db().select(**args[0])
+ else:
+ return self._get_db().select(**kwargs)
+
+ def describe(self, *args, **kwargs):
+ return self._get_db().describe(*args, **kwargs)
+
+ def ls(self, **kwargs):
+ return self._get_db().ls(**kwargs)
+
+ def sort(self, *args, **kwargs):
+ return self._get_db().sort(*args, **kwargs)
+
+ def apply_function_over_dim(self, dim, preserve_dims, func_to_run, name, **kwargs):
+ if len(self) == 0:
+ raise ValueError(f"Input to function {name} is an empty Fieldset")
+
+ if dim is None:
+ return func_to_run(self, **kwargs)
+
+ import itertools
+
+ if preserve_dims:
+ _preserve_dims = preserve_dims
+ else:
+ _preserve_dims = ["shortName", "level", "step", "number", "date", "time"]
+ if dim in _preserve_dims:
+ _preserve_dims.remove(dim)
+ dim_combos = {k: unique(self.grib_get_string(k)) for k in _preserve_dims}
+ keys, values = zip(*dim_combos.items())
+ perms = [dict(zip(keys, v)) for v in itertools.product(*values)]
+ # e.g. [{level=1000,shortName="t",date=20220101, time=6}, ...]
+ fieldsets_to_apply_function_to = [self.select(**x) for x in perms]
+ result = Fieldset(
+ fields=[
+ func_to_run(x, **kwargs)
+ for x in fieldsets_to_apply_function_to
+ if len(x) != 0
+ ]
+ )
+ return result
+
+ def mean(self, dim=None, preserve_dims=None, missing=False):
+ return self.apply_function_over_dim(
+ dim, preserve_dims, met_mean, "mean", missing=missing
+ )
+
+ def sum(self, dim=None, preserve_dims=None, missing=False):
+ return self.apply_function_over_dim(
+ dim, preserve_dims, met_sum, "sum", missing=missing
+ )
+
+ def stdev(self, dim=None, preserve_dims=None, missing=False):
+ return self.apply_function_over_dim(dim, preserve_dims, met_stdev, "stdev")
+
+ @property
+ def ds_param_info(self):
+ if self._ds_param_info is None:
+ self._ds_param_info = FieldsetDb.make_param_info(self)
+ return self._ds_param_info
+
+ @property
+ def label(self):
+ if self._label is not None and self._label:
+ return self._label
+ elif self._db is not None:
+ return self._db.label
+ else:
+ return str()
+
+ @label.setter
+ def label(self, value):
+ if self._db is not None:
+ if self._db.label:
+ print(
+ f"Warning: cannot set label! It is already set on index database as {self._db.label}!"
+ )
+ return
+ else:
+ self._db.label = value
+
+ self._label = value
+
+ def _unique_metadata(self, key):
+ return self._get_db().unique(key)
+
+ def ds_style(self, plot_type="map"):
+ from metview import style
+
+ return style.get_db().style(self, plot_type=plot_type)
+
+ def ds_style_list(self, plot_type="map"):
+ from metview import style
+
+ return style.get_db().style_list(self, plot_type=plot_type)
+
+ def speed(self, *args):
+ if len(args) == 0:
+ u = self[0::2]
+ v = self[1::2]
+ if len(u) != len(v):
+ raise Exception(
+ f"Fieldsets must contain an even number of fields for this operation! len={len(self)} is not even!"
+ )
+ sp = u.speed(v)
+ sp._init_db_from(self)
+ return sp
+ else:
+ return call("speed", self, *args)
+
+ def deacc(self, **kwargs):
+ r = utils.deacc(self, **kwargs)
+ r._init_db_from(self)
+ return r
+
+ def convolve(self, *args, **kwargs):
+ return utils.convolve(self, *args, **kwargs)
+
+ def smooth_n_point(self, *args, **kwargs):
+ return utils.smooth_n_point(self, *args, **kwargs)
+
+ def smooth_gaussian(self, *args, **kwargs):
+ return utils.smooth_gaussian(self, *args, **kwargs)
+
+ def _init_db_from(self, other):
+ if self._db is None and other._db is not None:
+ self._db = FieldsetDb(self, other.label)
+ self._db.load()
+
+ def __getitem__(self, key):
+ if isinstance(key, str):
+ self._scan()
+ if self._db is not None:
+ return self._db.select_with_name(key)
+ return None
+ else:
+ return super().__getitem__(key)
+
def __getstate__(self):
# used for pickling
# we cannot (and do not want to) directly pickle the Value pointer
@@ -619,6 +879,13 @@ def __setstate__(self, state):
self.__dict__.update(state)
self.__init__(val_pointer=None, path=state["url_path"])
+ def __str__(self):
+ n = int(self.count())
+ s = "s"
+ if n == 1:
+ s = ""
+ return "Fieldset (" + str(n) + " field" + s + ")"
+
class Bufr(FileBackedValue):
def __init__(self, val_pointer):
@@ -723,7 +990,7 @@ def dataset_to_fieldset(val, **kwarg):
# we try to import xarray as locally as possible to reduce startup time
# try to write the xarray as a GRIB file, then read into a fieldset
import xarray as xr
- import cfgrib
+ from cfgrib.xarray_to_grib import to_grib
if not isinstance(val, xr.core.dataset.Dataset):
raise TypeError(
@@ -737,7 +1004,7 @@ def dataset_to_fieldset(val, **kwarg):
try:
# could add keys, e.g. grib_keys={'centre': 'ecmf'})
- cfgrib.to_grib(val, tmp, **kwarg)
+ to_grib(val, tmp, **kwarg)
except:
print(
"Error trying to write xarray dataset to GRIB for conversion to Metview Fieldset"
@@ -789,6 +1056,9 @@ def __init__(self):
(datetime.date, lambda n: push_datetime_date(n)),
(np.ndarray, lambda n: push_vector(n)),
(File, lambda n: n.push()),
+ (GeoView, lambda n: push_style_object(n)),
+ (Style, lambda n: push_style_object(n)),
+ (Visdef, lambda n: push_style_object(n)),
)
def push_value(self, val):
@@ -851,6 +1121,7 @@ def datestring_from_metview(val):
mdate = string_from_ffi(lib.p_value_as_datestring(val))
dt = datetime.datetime.strptime(mdate, "%Y-%m-%dT%H:%M:%S")
+ lib.p_destroy_value(val)
return dt
@@ -888,7 +1159,15 @@ def handle_error(val):
def string_from_metview(val):
- return string_from_ffi(lib.p_value_as_string(val))
+ s = string_from_ffi(lib.p_value_as_string(val))
+ lib.p_destroy_value(val)
+ return s
+
+
+def number_from_metview(val):
+ n = lib.p_value_as_number(val)
+ lib.p_destroy_value(val)
+ return n
class MvRetVal(Enum):
@@ -916,7 +1195,7 @@ class ValueReturner:
def __init__(self):
self.funcs = {}
- self.funcs[MvRetVal.tnumber.value] = lambda val: lib.p_value_as_number(val)
+ self.funcs[MvRetVal.tnumber.value] = lambda val: number_from_metview(val)
self.funcs[MvRetVal.tstring.value] = lambda val: string_from_metview(val)
self.funcs[MvRetVal.tgrib.value] = lambda val: Fieldset(val)
self.funcs[MvRetVal.trequest.value] = lambda val: Request(val)
@@ -938,9 +1217,17 @@ def translate_return_val(self, val):
try:
return self.funcs[rt](val)
except Exception:
- raise Exception(
- "value_from_metview got an unhandled return type: " + str(rt)
- )
+ # if the type is unknown, it might be a type that is actually stored
+ # as a request rather than as a MARS type, e.g. PNG
+ try:
+ if rt == 99:
+ rt = MvRetVal.trequest.value
+ return self.funcs[rt](val)
+ except Exception:
+ raise Exception(
+ "value_from_metview got an unhandled return type and could not convert to a Request: "
+ + str(rt)
+ )
vr = ValueReturner()
@@ -953,6 +1240,19 @@ def value_from_metview(val):
return retval
+# -----------------------------------------------------------------------------
+#
+# -----------------------------------------------------------------------------
+
+
+def to_dataset(fs, *args, **kwargs):
+ return fs.to_dataset(args, kwargs)
+
+
+def to_dataset(fs, *args, **kwargs):
+ return fs.to_dataset(args, kwargs)
+
+
# -----------------------------------------------------------------------------
# Creating and calling Macro functions
# -----------------------------------------------------------------------------
@@ -979,7 +1279,7 @@ def make(mfname):
def wrapped(*args, **kwargs):
err = _call_function(mfname, *args, **kwargs)
if err:
- pass # throw Exceception
+ pass # throw Exception
val = lib.p_result_as_value()
return value_from_metview(val)
@@ -987,6 +1287,19 @@ def wrapped(*args, **kwargs):
return wrapped
+def _make_function_for_object(name):
+ """
+ Creates a function to invoke the method called name on obj. This will make it
+ possible to call some object methods as global functions. E.g.: if name="ls" and
+ f is a Fieldset we could invoke the ls() method as mv.ls(f) on top of f.ls()
+ """
+
+ def fn(obj, *args, **kwargs):
+ return getattr(obj, name)(*args, **kwargs)
+
+ return fn
+
+
def bind_functions(namespace, module_name=None):
"""Add to the module globals all metview functions except operators like: +, &, etc."""
for metview_name in make("dictionary")():
@@ -1005,11 +1318,13 @@ def bind_functions(namespace, module_name=None):
# else:
# print('metview function %r not bound to python' % metview_name)
- # HACK: some fuctions are missing from the 'dictionary' call.
+ # HACK: some functions are missing from the 'dictionary' call.
namespace["neg"] = make("neg")
namespace["nil"] = make("nil")
+ namespace["dialog"] = make("dialog")
namespace["div"] = div
namespace["mod"] = mod
+ namespace["string"] = make("string")
# override some functions that need special treatment
# FIXME: this needs to be more structured
namespace["plot"] = plot
@@ -1018,23 +1333,57 @@ def bind_functions(namespace, module_name=None):
namespace["version_info"] = version_info
namespace["merge"] = merge
namespace["dataset_to_fieldset"] = dataset_to_fieldset
-
+ namespace["valid_date"] = valid_date
+ namespace["sort"] = sort
+ namespace["load_dataset"] = Dataset.load_dataset
+ namespace["plot_maps"] = plotting.plot_maps
+ namespace["plot_diff_maps"] = plotting.plot_diff_maps
+ namespace["plot_xs"] = plotting.plot_xs
+ namespace["plot_xs_avg"] = plotting.plot_xs_avg
+ namespace["plot_stamp"] = plotting.plot_stamp
+ namespace["plot_rmse"] = plotting.plot_rmse
+ namespace["plot_cdf"] = plotting.plot_cdf
+ namespace["map_style_gallery"] = map_style_gallery
+ namespace["map_area_gallery"] = map_area_gallery
+ namespace["make_geoview"] = make_geoview
+ namespace["arguments"] = met_arguments
namespace["Fieldset"] = Fieldset
namespace["Request"] = Request
+ # some ui specific functions are prefixed with _. They will be exposed via the ui module!
+ for name in ["dialog", "any", "colour", "icon", "option_menu", "slider", "toggle"]:
+ namespace["_" + name] = namespace[name]
+ namespace.pop(name)
+
+ # add some object methods the to global namespace
+ for name in [
+ "to_dataset",
+ "to_dataframe",
+ "ls",
+ "describe",
+ "select",
+ "convolve",
+ "smooth_n_point",
+ "smooth_gaussian",
+ ]:
+ namespace[name] = _make_function_for_object(name)
+
# some explicit bindings are used here
add = make("+")
call = make("call")
count = make("count")
+definition = make("definition")
div = make("/")
download = make("download")
equal = make("=")
filter = make("filter")
greater_equal_than = make(">=")
greater_than = make(">")
+_keywords = make("keywords")
lower_equal_than = make("<=")
lower_than = make("<")
+met_mean = make("mean")
met_merge = make("&")
met_not_eq = make("<>")
met_plot = make("plot")
@@ -1049,10 +1398,14 @@ def bind_functions(namespace, module_name=None):
metzoom = make("metzoom")
sub = make("-")
subset = make("[]")
+met_stdev = make("stdev")
+met_sum = make("sum")
met_and = make("and")
met_or = make("or")
met_not = make("not")
met_version_info = make("version_info")
+_request = make("request")
+unique = make("unique")
write = make("write")
@@ -1078,12 +1431,24 @@ def merge(*args):
class Plot:
+ has_pillow = None
+ padding = np.array([x * 40 for x in [-1, -1, 1, 1]])
+
def __init__(self):
self.plot_to_jupyter = False
self.plot_widget = True
self.jupyter_args = {}
+ self.called_once = False
+ self.setoutput_called_once = False
def __call__(self, *args, **kwargs):
+ # first time called? If we are in Jupyter and user did not specify,
+ # then plot inline to Jupyter by default
+ if not (self.called_once or self.setoutput_called_once):
+ if is_ipython_active():
+ setoutput("jupyter")
+ self.called_once = True
+
if self.plot_to_jupyter: # pragma: no cover
if self.plot_widget:
return plot_to_notebook(args, **kwargs)
@@ -1104,6 +1469,42 @@ def __call__(self, *args, **kwargs):
# None is better for Python
return None
+ def crop_image(self, path):
+ if Plot.has_pillow is None:
+ try:
+ import PIL
+
+ Plot.has_pillow = True
+ except ImportError as e:
+ Plot.has_pillow = False
+ if Plot.has_pillow:
+ try:
+ from PIL import Image
+ from PIL import ImageOps
+
+ im = Image.open(path)
+ im.load()
+
+ # find inner part
+ im_invert = im.convert("RGB")
+ box = ImageOps.invert(im_invert).getbbox()
+
+ # crop to box
+ if box[2] - box[0] > 100 or box[3] - box[1] > 100:
+ box = list(np.asarray(box) + Plot.padding)
+ box = (
+ max(0, box[0]),
+ max(0, box[1]),
+ min(im.size[0], box[2]),
+ min(im.size[1], box[3]),
+ )
+ im_crop = im.crop(box)
+ im.close()
+ im_crop.save(path)
+ except Exception as e:
+ # print(f"ERROR={e}")
+ pass
+
plot = Plot()
@@ -1141,6 +1542,9 @@ def plot_to_notebook(*args, **kwargs): # pragma: no cover
files = [os.path.join(tempdirpath, f) for f in sorted(filenames)]
+ for f in files:
+ plot.crop_image(f)
+
if (animation_mode == True) or (animation_mode == "auto" and len(filenames) > 1):
frame_widget = widgets.IntSlider(
value=1,
@@ -1218,6 +1622,8 @@ def plot_to_notebook_return_image(*args, **kwargs): # pragma: no cover
plot.jupyter_args.update(output_name=base, output_name_first_page_number="off")
met_setoutput(png_output(plot.jupyter_args))
met_plot(*args)
+ plot.crop_image(tmp)
+
image = Image(tmp)
os.unlink(tmp)
return image
@@ -1228,19 +1634,16 @@ def plot_to_notebook_return_image(*args, **kwargs): # pragma: no cover
# functionality. Since this occurs within a function, we need a little trickery to
# get the IPython functions into the global namespace so that the plot object can use them
def setoutput(*args, **kwargs):
+ plot.setoutput_called_once = True
if "jupyter" in args: # pragma: no cover
- try:
- import IPython
-
- get_ipython = IPython.get_ipython
- except ImportError as imperr:
- print("Could not import IPython module - plotting to Jupyter will not work")
- raise imperr
-
- # test whether we're in the Jupyter environment
- if get_ipython() is not None:
+ if is_ipython_active():
+ global widgets
plot.plot_to_jupyter = True
plot.plot_widget = kwargs.get("plot_widget", True)
+ if plot.plot_widget:
+ widgets = import_widgets()
+ if not widgets:
+ plot.plot_widget = False
if "plot_widget" in kwargs:
del kwargs["plot_widget"]
plot.jupyter_args = kwargs
@@ -1250,15 +1653,20 @@ def setoutput(*args, **kwargs):
)
raise (Exception("Could not set output to jupyter"))
- try:
- global widgets
- widgets = __import__("ipywidgets", globals(), locals())
- except ImportError as imperr:
- print(
- "Could not import ipywidgets module - plotting to Jupyter will not work"
- )
- raise imperr
-
else:
plot.plot_to_jupyter = False
met_setoutput(*args)
+
+
+def met_arguments():
+ """Emulate the Macro arguments() function"""
+ import sys
+
+ args = sys.argv[1:]
+ # these will all come in as strings; but Macro does a little processing on them
+ # in order to intelligently decide whether their types or string or number;
+ # creating a Request with these values will simulate this behaviour
+ args_dict = {i: i for i in args}
+ modified_args_dict = Request(args_dict).to_dict()
+ modified_args = [modified_args_dict[j] for j in modified_args_dict]
+ return modified_args
diff --git a/metview/compat.py b/metview/compat.py
new file mode 100644
index 00000000..64ccd1c4
--- /dev/null
+++ b/metview/compat.py
@@ -0,0 +1,86 @@
+# (C) Copyright 2017- ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation
+# nor does it submit to any jurisdiction.
+
+import numpy as np
+
+import metview as mv
+
+
+def concat(*args):
+ if len(args) <= 1:
+ raise ValueError(f"concat requires at least 2 arguments, got only {len(args)}")
+
+ vals = list(args)
+ if vals[0] is None:
+ vals.pop(0)
+ if len(vals) == 1:
+ return vals[0]
+
+ # concatenate strings
+ if isinstance(vals[0], str):
+ return "".join([str(v) for v in vals])
+ elif isinstance(vals[0], np.ndarray):
+ return np.concatenate(vals)
+ elif isinstance(
+ vals[0],
+ (mv.Fieldset, mv.bindings.Geopoints, mv.bindings.GeopointSet, mv.bindings.Bufr),
+ ):
+ return mv.merge(*vals)
+ elif isinstance(vals[0], list):
+ first = []
+ for v in vals:
+ if isinstance(v, list):
+ first.extend(v)
+ else:
+ first.append(v)
+ return first
+
+ raise ValueError(f"concat failed to handle the specified arguments={args}")
+
+
+def index4(vals, start, stop, step, num):
+ """
+ Return a boolean index ndarray to select a subset of elements from ``vals``.
+
+ Parameters
+ ----------
+ vals: ndarray
+ Input array.
+ start: int
+ Start index.
+ stop: int
+ Stop index.
+ step: int
+ Step.
+ num: int
+ Number of elements to be selected for each step.
+
+ Returns
+ -------
+ ndarray
+ Boolean index array
+
+ Examples
+ --------
+
+ >>> import numpy as np
+ >>> import metview
+ >>> vals = np.array(list(range(12)))
+ >>> r = index4(vals, 0, 11, 4, 2)
+ [ True True True True True True False False False False False False]
+ a[]
+ >>> vals[r]
+ array([0, 1, 4, 5, 8, 9])
+
+ """
+ num = min(num, step)
+ m = np.zeros(len(vals), dtype=bool)
+ for i in range(start, stop, step):
+ m[i : i + num] = 1
+ return m
diff --git a/metview/dataset.py b/metview/dataset.py
new file mode 100644
index 00000000..0f905e87
--- /dev/null
+++ b/metview/dataset.py
@@ -0,0 +1,461 @@
+# (C) Copyright 2017- ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation
+# nor does it submit to any jurisdiction.
+#
+
+import logging
+import os
+from pathlib import Path
+import shutil
+
+import pandas as pd
+import requests
+import yaml
+
+import metview as mv
+from metview.metviewpy.indexer import ExperimentIndexer
+from metview.metviewpy.indexdb import IndexDb
+from metview.track import Track
+from metview.metviewpy.param import init_pandas_options
+from metview.metviewpy import utils
+
+
+ETC_PATH = os.path.join(os.path.dirname(__file__), "etc")
+
+# logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
+# logging.basicConfig(level=logging.DEBUG, format="%(levelname)s - %(message)s")
+LOG = logging.getLogger(__name__)
+
+
+class ExperimentDb(IndexDb):
+ def __init__(self, name, **kwargs):
+ super().__init__(name, **kwargs)
+ self.fs = {}
+ self.vector_loaded = True
+ self._indexer = None
+ self.fieldset_class = mv.Fieldset
+ LOG.debug(f"rootdir_placeholder_value={self.rootdir_placeholder_value}")
+
+ @staticmethod
+ def make_from_conf(name, conf, root_dir, db_root_dir, regrid_conf, dataset):
+ # LOG.debug(f"conf={conf}")
+ db = ExperimentDb(
+ name,
+ label=conf.get("label", ""),
+ desc=conf.get("desc", ""),
+ path=conf.get("dir", "").replace(
+ IndexDb.ROOTDIR_PLACEHOLDER_TOKEN, root_dir
+ ),
+ rootdir_placeholder_value=root_dir
+ if IndexDb.ROOTDIR_PLACEHOLDER_TOKEN in conf.get("dir", "")
+ or "merge" in conf
+ else "",
+ file_name_pattern=conf.get("fname", ""),
+ db_dir=os.path.join(db_root_dir, name),
+ merge_conf=conf.get("merge", []),
+ mapped_params={v: k for k, v in conf.get("mapped_params", {}).items()},
+ regrid_from=regrid_conf.get(name, []),
+ blocks={},
+ dataset=dataset,
+ )
+ return db
+
+ def _clone(self):
+ return ExperimentDb(
+ self.name,
+ label=self.label,
+ db_dir=self.db_dir,
+ mapped_params=self.mapped_params,
+ regrid_from=self.regrid_from,
+ dataset=self.dataset,
+ data_files=self.data_files,
+ rootdir_placeholder_value=self.rootdir_placeholder_value,
+ )
+
+ @property
+ def indexer(self):
+ if self._indexer is None:
+ self._indexer = ExperimentIndexer(self)
+ return self._indexer
+
+ def scan(self, vector=True):
+ print(f"Generate index for dataset component={self.name} ...")
+ self.data_files = []
+ # self.blocks = {}
+ self.indexer.scan()
+
+ def load(self, keys=None, vector=True):
+ keys = [] if keys is None else keys
+ ivk = [x for x in keys if x not in self.indexer.allowed_keys()]
+ if ivk:
+ raise Exception(
+ f"{self} keys={ivk} cannot be used! The allowed set of keys={self.indexer.allowed_keys()}"
+ )
+
+ self.load_data_file_list()
+ if len(self.data_files) == 0:
+ self.scan(vector=True)
+ self.load_data_file_list()
+
+ if len(self.blocks) == 0:
+ for key in ExperimentIndexer.get_storage_key_list(self.db_dir):
+ self.blocks[key] = ExperimentIndexer.read_dataframe(key, self.db_dir)
+
+ def load_data_file_list(self):
+ if len(self.data_files) == 0:
+ try:
+ file_path = os.path.join(self.db_dir, "datafiles.yaml")
+ with open(file_path, "rt") as f:
+ self.data_files = yaml.safe_load(f)
+ if self.rootdir_placeholder_value:
+ self.data_files = [
+ x.replace(
+ IndexDb.ROOTDIR_PLACEHOLDER_TOKEN,
+ self.rootdir_placeholder_value,
+ )
+ for x in self.data_files
+ ]
+ # LOG.debug(f"data_files={self.data_files}")
+ for f in self.data_files:
+ assert os.path.isfile(f)
+ except:
+ pass
+
+ def __getitem__(self, key):
+ if isinstance(key, str):
+ self.load()
+ return self.select_with_name(key)
+ return None
+
+ def _filter_blocks(self, dims):
+ self.load()
+ dfs = {}
+ # LOG.debug(f"data_files={self.data_files}")
+ # LOG.debug(f"dims={dims}")
+ cnt = 0
+ for key, df in self.blocks.items():
+ # LOG.debug(f"key={key}")
+ # df = self._load_block(key)
+ # LOG.debug(f"df={df}")
+ f_df = self._filter_df(df=df, dims=dims)
+ # LOG.debug(f"df={df}"
+ if f_df is not None and not f_df.empty:
+ cnt += len(f_df)
+ # LOG.debug(f" matching rows={len(df)}")
+ dfs[key] = f_df
+
+ # LOG.debug(f"total matching rows={cnt}")
+ return dfs
+
+ def _extract_fields(self, df, fs, max_count):
+ if df.empty:
+ return None
+
+ if "_fileIndex3" in df.columns:
+ comp_num = 3
+ elif "_fileIndex2" in df.columns:
+ comp_num = 2
+ elif "_fileIndex1" in df.columns:
+ comp_num = 1
+ else:
+ return None
+
+ idx = [[] for k in range(comp_num)]
+ comp_lst = list(range(comp_num))
+ for row in df.itertuples():
+ for comp in comp_lst:
+ idx_file = row[-1 - (comp_num - comp - 1) * 2]
+ idx_msg = row[-2 - (comp_num - comp - 1) * 2]
+ if not idx_file in self.fs:
+ self.fs[idx_file] = mv.read(self.data_files[idx_file])
+ fs.append(self.fs[idx_file][idx_msg])
+ idx[comp].append(len(fs) - 1)
+ # generate a new dataframe
+ df = df.copy()
+ for k, v in enumerate(idx):
+ df[f"_msgIndex{k+1}"] = v
+ df.drop([f"_fileIndex{x+1}" for x in range(comp_num)], axis=1, inplace=True)
+ return df
+
+ def to_fieldset(self):
+ db, fs = self._get_fields({})
+ fs._db = db
+ return fs
+
+ def get_longname_and_units(self, shortName, paramId):
+ return "", ""
+
+
+class TrackConf:
+ def __init__(self, name, conf, data_dir, dataset):
+ self.name = name
+ self.dataset = dataset
+ self.label = self.name
+ self.path = conf.get("dir", "").replace(
+ IndexDb.ROOTDIR_PLACEHOLDER_TOKEN, data_dir
+ )
+ self.file_name_pattern = conf.get("fname", "")
+ # self.conf_dir = os.path.join("_conf", self.name)
+ self.data_files = []
+ self.conf = conf
+
+ def load_data_file_list(self):
+ if len(self.data_files) == 0:
+ self.data_files = utils.get_file_list(
+ self.path, file_name_pattern=self.file_name_pattern
+ )
+
+ def select(self, name):
+ tr = self._make(name)
+ if tr is None:
+ raise Exception(f"No track is available with name={name}!")
+ return tr
+
+ def describe(self):
+ self.load_data_file_list()
+ init_pandas_options()
+ t = {"Name": [], "Suffix": []}
+ for f in self.data_files:
+ n, s = os.path.splitext(os.path.basename(f))
+ t["Name"].append(n)
+ t["Suffix"].append(s)
+ df = pd.DataFrame.from_dict(t)
+ df.set_index("Name", inplace=True)
+ return df
+
+ def _make(self, name):
+ self.load_data_file_list()
+ for f in self.data_files:
+ if name == os.path.basename(f).split(".")[0]:
+ c = {
+ x: self.conf.get(x, None)
+ for x in [
+ "skiprows",
+ "date_index",
+ "time_index",
+ "lon_index",
+ "lat_index",
+ ]
+ }
+ return Track(f, **c)
+ return None
+
+
+class Dataset:
+ """
+ Represents a Dataset
+ """
+
+ URL = "https://get.ecmwf.int/repository/test-data/metview/dataset"
+ LOCAL_ROOT = os.getenv(
+ "MPY_DATASET_ROOT", os.path.join(os.getenv("HOME", ""), "mpy_dataset")
+ )
+ COMPRESSION = "bz2"
+
+ def __init__(self, name_or_path, load_style=True):
+ self.field_conf = {}
+ self.track_conf = {}
+
+ self.path = name_or_path
+ if any(x in self.path for x in ["/", "\\", "..", "./"]):
+ self.name = os.path.basename(self.path)
+ else:
+ self.name = self.path
+ self.path = ""
+
+ assert self.name
+ # print(f"name={self.name}")
+
+ # set local path
+ if self.path == "":
+ self.path = os.path.join(self.LOCAL_ROOT, self.name)
+
+ # If the path does not exists, it must be a built-in dataset. Data will be
+ # downloaded into path.
+ if not os.path.isdir(self.path):
+ if self.check_remote():
+ self.fetch(forced=True)
+ else:
+ raise Exception(
+ f"Could not find dataset={self.name} either under path={self.path} or on data server"
+ )
+ # WARN: we do not store the dataset in the CACHE any more. Check code versions before
+ # 09082021 to see how the CACHE was used.
+
+ if load_style:
+ self.load_style()
+
+ self.load()
+
+ for _, c in self.field_conf.items():
+ LOG.debug(f"{c}")
+
+ @staticmethod
+ def load_dataset(*args, **kwargs):
+ return Dataset(*args, **kwargs)
+
+ def check_remote(self):
+ try:
+ return (
+ requests.head(
+ f"{self.URL}/{self.name}/conf.tar", allow_redirects=True
+ ).status_code
+ == 200
+ )
+ except:
+ return False
+
+ def load(self):
+ data_dir = os.path.join(self.path, "data")
+ index_dir = os.path.join(self.path, "index")
+ data_conf_file = os.path.join(self.path, "data.yaml")
+ with open(data_conf_file, "rt") as f:
+ d = yaml.safe_load(f)
+ regrid_conf = d.get("regrid", {})
+ for item in d["experiments"]:
+ ((name, conf),) = item.items()
+ if conf.get("type") == "track":
+ self.track_conf[name] = TrackConf(name, conf, data_dir, self)
+ else:
+ c = ExperimentDb.make_from_conf(
+ name, conf, data_dir, index_dir, regrid_conf, self
+ )
+ self.field_conf[c.name] = c
+
+ def scan(self, name=None):
+ # indexer = ExperimentIndexer()
+ if name:
+ if name in self.field_conf:
+ self.field_conf[name].scan()
+ # indexer.scan(self.field_conf[name], to_disk=True)
+ else:
+ for name, c in self.field_conf.items():
+ LOG.info("-" * 40)
+ c.scan()
+ # indexer.scan(c, to_disk=True)
+
+ def find(self, name, comp="field"):
+ if comp == "all":
+ f = self.field_conf.get(name, None)
+ if f is not None:
+ return f
+ else:
+ return self.track_conf.get(name, None)
+ elif comp == "field":
+ return self.field_conf.get(name, None)
+ elif comp == "track":
+ return self.track_conf.get(name, None)
+ else:
+ return None
+
+ def describe(self):
+ init_pandas_options()
+ print("Dataset components:")
+ t = {"Component": [], "Description": []}
+ for _, f in self.field_conf.items():
+ t["Component"].append(f.name)
+ t["Description"].append(f.desc)
+ for _, f in self.track_conf.items():
+ t["Component"].append(f.name)
+ t["Description"].append("Storm track data")
+ df = pd.DataFrame.from_dict(t)
+ df.set_index("Component", inplace=True)
+ return df
+
+ def fetch(self, forced=False):
+ if not os.path.isdir(self.path):
+ Path(self.path).mkdir(0o755, parents=True, exist_ok=True)
+
+ files = {
+ "conf.tar": ["data.yaml", "conf"],
+ f"index.tar.{self.COMPRESSION}": ["index"],
+ f"data.tar.{self.COMPRESSION}": ["data"],
+ }
+
+ checked = False
+ for src, targets in files.items():
+ # if forced or not utils.CACHE.all_exists(targets, self.path):
+ if forced:
+ if not checked and not self.check_remote():
+ raise Exception(
+ f"Could not find dataset={self.name} on data server"
+ )
+ else:
+ checked = True
+
+ remote_file = os.path.join(self.URL, self.name, src)
+ target_file = os.path.join(self.path, src)
+ # LOG.debug(f"target_file={target_file}")
+ try:
+ # print("Download data ...")
+ utils.download(remote_file, target_file)
+ print(f"Unpack data ... {src}")
+ utils.unpack(target_file, remove=True)
+ # TODO: we skip the reference creation to make things faster. Enable it when
+ # it is needed!
+ # utils.CACHE.make_reference(targets, self.path)
+ except:
+ # if os.exists(target_file):
+ # os.remove(target_file)
+ raise Exception(f"Failed to download file={remote_file}")
+
+ def __getitem__(self, key):
+ if isinstance(key, str):
+ r = self.find(key, comp="all")
+ if r is None:
+ raise Exception(f"No component={key} found in {self}")
+ return r
+ return None
+
+ def load_style(self):
+ conf_dir = os.path.join(self.path, "conf")
+ mv.style.load_custom_config(conf_dir, force=True)
+
+ def __str__(self):
+ return f"{self.__class__.__name__}[name={self.name}]"
+
+
+def create_dataset_template(name_or_path):
+ path = name_or_path
+ if any(x in path for x in ["/", "\\", "..", "./"]):
+ name = os.path.basename(path)
+ else:
+ name = path
+ path = ""
+
+ if path == "":
+ path = os.path.join(Dataset.LOCAL_ROOT, name)
+
+ if not os.path.exists(path):
+ os.mkdir(path)
+ else:
+ if not os.path.isdir(path):
+ raise Exception(f"path must be a directory!")
+ if os.path.exists(os.path.join("path", "data.yaml")):
+ raise Exception(
+ f"The specified dataset directory={path} already exists and is not empty!"
+ )
+
+ # create dirs
+ for dir_name in ["conf", "data", "index"]:
+ os.mkdir(os.path.join(path, dir_name))
+
+ # copy files
+ files = {
+ "params.yaml": ("conf", ""),
+ "param_styles.yaml": ("conf", ""),
+ "areas.yaml": ("conf", ""),
+ "map_styles_template.yaml": ("conf", "map_styles.yaml"),
+ "dataset_template.yaml": ("", "data.yaml"),
+ }
+ for src_name, target in files.items():
+ target_dir = os.path.join(path, target[0]) if target[0] else path
+ target_name = target[1] if target[1] else src_name
+ shutil.copyfile(
+ os.path.join(ETC_PATH, src_name), os.path.join(target_dir, target_name)
+ )
diff --git a/metview/etc/areas.yaml b/metview/etc/areas.yaml
new file mode 100644
index 00000000..e63fd1d1
--- /dev/null
+++ b/metview/etc/areas.yaml
@@ -0,0 +1,3 @@
+- base:
+ map_area_definition: "corners"
+ area: [-90,-180,90,180]
\ No newline at end of file
diff --git a/metview/etc/dataset_template.yaml b/metview/etc/dataset_template.yaml
new file mode 100644
index 00000000..530ec300
--- /dev/null
+++ b/metview/etc/dataset_template.yaml
@@ -0,0 +1,14 @@
+experiments:
+ - myexp:
+ label: "myexp"
+ dir: __ROOTDIR__/myexp
+ fname : "*.grib"
+ - track:
+ type: track
+ dir: __ROOTDIR__/track
+ fname: "*.csv"
+ skiprows: 10
+ date_index: 0
+ time_index: 1
+ lat_index: 3
+ lon_index: 4
\ No newline at end of file
diff --git a/metview/etc/map_styles.yaml b/metview/etc/map_styles.yaml
new file mode 100644
index 00000000..8734c129
--- /dev/null
+++ b/metview/etc/map_styles.yaml
@@ -0,0 +1,149 @@
+base:
+ map_coastline_land_shade: "off"
+ map_coastline_sea_shade: 'off'
+base_diff:
+ map_coastline_colour: CHARCOAL
+ map_coastline_land_shade: "on"
+ map_coastline_land_shade_colour: "RGB(0.8549,0.8549,0.8549)"
+ map_coastline_sea_shade: 'off'
+ map_coastline_land_shade_colour: WHITE
+grey_light_blue:
+ map_coastline_land_shade: "on"
+ map_coastline_land_shade_colour: "grey"
+ map_coastline_sea_shade: "on"
+ map_coastline_sea_shade_colour: "RGB(0.86,0.94,1)"
+ map_boundaries: "off"
+ map_boundaries_colour: "RGB(0.21,0.21,0.21)"
+ map_disputed_boundaries: "off"
+ map_administrative_boundaries: "off"
+ map_grid_colour: "RGB(0.294,0.294,0.2941)"
+ map_label_colour: "RGB(0.294,0.294,0.2941)"
+black_coast_only:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'off'
+ map_coastline_sea_shade: 'off'
+ # map_coastline_colour: BLACK
+black_grey:
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: black
+ map_coastline_sea_shade: 'on'
+ map_coastline_sea_shade_colour: RGB(0.7767,0.7932,0.8076)
+ map_coastline: 'on'
+ map_coastline_colour: BACKGROUND
+ map_coastline_style: solid
+ map_coastline_thickness: 1
+blue_sea_only:
+ map_coastline: 'on'
+ map_coastline_sea_shade: 'on'
+ map_coastline_sea_shade_colour: RGB(0.7373,0.9294,0.9608)
+brown:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: RGB(0.7216,0.6863,0.6471)
+ map_coastline_sea_shade: 'on'
+ map_coastline_sea_shade_colour: RGB(0.9059,0.9176,0.9373)
+ map_coastline_colour: TAN
+cream_blue:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: CREAM
+ map_coastline_sea_shade: 'on'
+ map_coastline_sea_shade_colour: SKY
+cream_land_only:
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: CREAM
+ map_coastline_sea_shade: 'off'
+ map_coastline_sea_shade_colour: WHITE
+ map_coastline_colour: BLACK
+ map_coastline_style: solid
+ map_coastline_thickness: 1
+dark_1:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: CHARCOAL
+ map_coastline_sea_shade: 'on'
+ map_coastline_sea_shade_colour: RGB(0.3677,0.3677,0.4009)
+ map_coastline_colour: WHITE
+dark_2:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: CHARCOAL
+ map_coastline_sea_shade: 'on'
+ map_coastline_sea_shade_colour: RGB(0.2471,0.2471,0.2471)
+ map_coastline_colour: WHITE
+dark_3:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: RGB(0.4431,0.4431,0.4431)
+ map_coastline_sea_shade: 'on'
+ map_coastline_sea_shade_colour: RGB(0.3176,0.3176,0.3176)
+green_blue:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: RGB(0.2233,0.7101,0.3369)
+ map_coastline_sea_shade: 'on'
+ map_coastline_sea_shade_colour: RGB(0.4923,0.6884,0.8489)
+ map_coastline_colour: BLACK
+green_land_only:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: RGB(0.1020,0.4000,0.0000)
+ map_coastline_sea_shade: 'off'
+green_navy:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: EVERGREEN
+ map_coastline_sea_shade: 'on'
+ map_coastline_sea_shade_colour: RGB(0.0000,0.0000,0.5020)
+grey_1:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: RGB(0.3725,0.4157,0.5451)
+ map_coastline_sea_shade: 'on'
+ map_coastline_sea_shade_colour: RGB(0.7767,0.7932,0.8076)
+ map_coastline_colour: BLACK
+ map_coastline_style: solid
+ map_coastline_thickness: 1
+grey_2:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: GREY
+ map_coastline_sea_shade: 'on'
+ map_coastline_sea_shade_colour: RGB(0.9059,0.9176,0.9373)
+ map_coastline_colour: CHARCOAL
+grey_3:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: RGB(0.5373,0.5373,0.5373)
+ map_coastline_sea_shade: 'on'
+ map_coastline_sea_shade_colour: RGB(0.8392,0.8510,0.8706)
+ map_coastline_colour: CHARCOAL
+grey_4:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: RGB(0.5569,0.5569,0.5569)
+ map_coastline_sea_shade: 'on'
+ map_coastline_sea_shade_colour: RGB(0.8000,0.8000,0.8000)
+ map_coastline_colour: CHARCOAL
+grey_blue:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: GREY
+ map_coastline_sea_shade: 'on'
+ map_coastline_sea_shade_colour: RGB(0.7922,0.8431,0.9412)
+ map_coastline_colour: CHARCOAL
+grey_darker_land_only:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: RGB(0.5020,0.5020,0.5020)
+ map_coastline_sea_shade: 'off'
+grey_land_only:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'on'
+ map_coastline_land_shade_colour: RGB(0.8549,0.8549,0.8549)
+ map_coastline_sea_shade: 'off'
+white_coast_only:
+ map_coastline: 'on'
+ map_coastline_land_shade: 'off'
+ map_coastline_sea_shade: 'off'
+ map_coastline_colour: WHITE
\ No newline at end of file
diff --git a/metview/etc/map_styles_template.yaml b/metview/etc/map_styles_template.yaml
new file mode 100644
index 00000000..ac36c89a
--- /dev/null
+++ b/metview/etc/map_styles_template.yaml
@@ -0,0 +1,29 @@
+base:
+ map_coastline_resolution: "low"
+ map_coastline_land_shade: "on"
+ map_coastline_land_shade_colour: "grey"
+ map_coastline_sea_shade: "on"
+ map_coastline_sea_shade_colour: "RGB(0.86,0.94,1)"
+ map_boundaries: "on"
+ map_boundaries_colour: "RGB(0.21,0.21,0.21)"
+ map_disputed_boundaries: "off"
+ map_administrative_boundaries: "off"
+ map_grid_latitude_increment: 10
+ map_grid_longitude_increment: 10
+ map_grid_colour: "RGB(0.294,0.294,0.2941)"
+ map_label_colour: "RGB(0.294,0.294,0.2941)"
+base_diff:
+ map_coastline_resolution: "low"
+ map_coastline_colour: CHARCOAL
+ map_coastline_land_shade: "on"
+ map_coastline_land_shade_colour: "RGB(0.8549,0.8549,0.8549)"
+ map_coastline_sea_shade: "off"
+ map_coastline_sea_shade_colour: "white"
+ map_boundaries: "on"
+ map_boundaries_colour: "RGB(0.21,0.21,0.21)"
+ map_disputed_boundaries: "off"
+ map_administrative_boundaries: "off"
+ map_grid_latitude_increment: 10
+ map_grid_longitude_increment: 10
+ map_grid_colour: "RGB(0.294,0.294,0.2941)"
+ map_label_colour: "RGB(0.294,0.294,0.2941)"
\ No newline at end of file
diff --git a/metview/etc/param_styles.yaml b/metview/etc/param_styles.yaml
new file mode 100644
index 00000000..74e76bf0
--- /dev/null
+++ b/metview/etc/param_styles.yaml
@@ -0,0 +1,579 @@
+# ------------------------
+# DEFAULT STYLES
+# ------------------------
+# this is the default style assigned to a scalar param!
+default_mcont:
+ mcont:
+ contour_automatic_setting: ecmwf
+ legend: "on"
+# this is the default style assigned to a vector param!
+default_mwind:
+ mwind:
+ wind_thinning_method: "density"
+ wind_density: 4
+ legend: "on"
+# this is the default style for difference fields
+default_diff:
+ mcont:
+ legend: "off"
+# ------------------------
+# SCALAR STYLES
+# ------------------------
+msl:
+ mcont:
+ legend: "off"
+ contour_highlight_colour: black
+ contour_highlight_thickness: 4
+ contour_interval: 5
+ contour_label: "on"
+ contour_label_frequency: 2
+ contour_label_height: 0.4
+ contour_level_selection_type: interval
+ contour_line_colour: black
+ contour_line_thickness: 2
+ contour_reference_level: 1010
+# style for potential temperature
+pt:
+ mcont:
+ legend: "on"
+ contour: "off"
+ contour_level_selection_type: "interval"
+ contour_max_level: 380
+ contour_min_level: 260
+ contour_interval: 5
+ contour_label: "off"
+ contour_shade: "on"
+ contour_shade_method: "area_fill"
+ contour_shade_colour_method: "palette"
+ contour_shade_palette_name: eccharts_rainbow_blue_purple_24
+ grib_scaling_of_retrieved_fields: "off"
+# style for pressure velocity
+w:
+ mcont:
+ contour_automatic_setting: style_name
+ contour_style_name: sh_viobrn_fM5t5lst
+ legend: "on"
+# style for relative and absolute vorticity
+vo:
+ mcont:
+ contour_automatic_setting: style_name
+ contour_style_name: sh_blured_fM50t50lst_less
+ legend: "on"
+ grib_scaling_of_derived_fields: "on"
+# style for potential vorticity on 320K
+pv:
+ mcont:
+ legend: "on"
+ contour: "off"
+ contour_level_selection_type: "level_list"
+ contour_level_list: [1,2,4,6,8,10,20,30]
+ contour_label: "off"
+ contour_shade: "on"
+ contour_shade_method: "area_fill"
+ contour_shade_colour_method: "palette"
+ contour_shade_palette_name: matplotlib_Plasma_7_r
+q_flux:
+ mcont:
+ legend: "on"
+ contour: "off"
+ contour_level_selection_type: "interval"
+ contour_level_selection_type: "level_list"
+ contour_level_list: [-0.04, -0.01, -0.008, -0.004, -0.002, -0.001, -0.0005, -0.0002, 0.0002, 0.0005, 0.001, 0.002, 0.004, 0.008, 0.01, 0.04]
+ contour_label: "off"
+ contour_shade: "on"
+ contour_shade_method: "area_fill"
+ contour_shade_colour_method: "palette"
+ contour_shade_palette_name: "colorbrewer_PRGn_15_r"
+t_flux:
+ mcont:
+ legend: "on"
+ contour: "off"
+ contour_level_selection_type: "interval"
+ contour_level_selection_type: "level_list"
+ contour_level_list: [-120,-50,-20,-10,-5,-2,-1,-0.5, 0.5,1,2, 5, 10, 20, 50, 120]
+ contour_label: "off"
+ contour_shade: "on"
+ contour_shade_method: "area_fill"
+ contour_shade_colour_method: "palette"
+ contour_shade_palette_name: "colorbrewer_PuOr_15"
+z:
+ mcont:
+ legend: "off"
+ contour_highlight_colour: black
+ contour_highlight_thickness: 4
+ contour_interval: 5
+ contour_label: "on"
+ contour_label_frequency: 2
+ contour_label_height: 0.4
+ contour_level_selection_type: interval
+ contour_line_colour: black
+ contour_line_thickness: 2
+ contour_reference_level: 1010
+u_10m:
+ mcont:
+ legend: "on"
+ contour: "off"
+ contour_level_selection_type: "level_list"
+ contour_level_list: [-80,-30,-20,-10, -5, -2,0,2,5,10,20,30,80]
+ contour_label: "off"
+ contour_shade: "on"
+ contour_shade_colour_method: "palette"
+ contour_shade_method: "area_fill"
+ contour_shade_palette_name: "colorbrewer_PuOr"
+ contour_shade_colour_list_policy: "dynamic"
+u_700:
+ mcont:
+ legend: "on"
+ contour: "off"
+ contour_level_selection_type: "level_list"
+ contour_level_list: [-90,-30,-25,-20,-15,-10,0,10,15,20,25,30,90]
+ contour_label: "off"
+ contour_shade: "on"
+ contour_shade_colour_method: "palette"
+ contour_shade_method: "area_fill"
+ contour_shade_palette_name: "colorbrewer_PuOr"
+ contour_shade_colour_list_policy: "dynamic"
+u_500:
+ mcont:
+ legend: "on"
+ contour: "off"
+ contour_level_selection_type: "level_list"
+ contour_level_list: [-90,-50,-40,-30,-20,-10,0,10,20,30,40,50,90]
+ contour_label: "off"
+ contour_shade: "on"
+ contour_shade_colour_method: "palette"
+ contour_shade_method: "area_fill"
+ contour_shade_palette_name: "colorbrewer_PuOr"
+ contour_shade_colour_list_policy: "dynamic"
+u_200:
+ mcont:
+ legend: "on"
+ contour: "off"
+ contour_level_selection_type: "level_list"
+ contour_level_list: [-120,-80,-60,-40,-20,-10,0,10,20,40,60,80,120]
+ contour_label: "off"
+ contour_shade: "on"
+ contour_shade_colour_method: "palette"
+ contour_shade_method: "area_fill"
+ contour_shade_palette_name: "colorbrewer_PuOr"
+ contour_shade_colour_list_policy: "dynamic"
+# ------------------------
+# VECTOR STYLES
+# ------------------------
+# black wind arrows
+# -------------------
+arrow_black:
+ mwind: &arrow_black
+ legend: "off"
+ wind_advanced_method: "off"
+ wind_thinning_method: "density"
+ wind_densisty: 2
+ wind_arrow_colour: "black"
+ wind_arrow_min_speed: 0
+# 10m
+arrow_black_10:
+ mwind:
+ <<: *arrow_black
+ wind_arrow_max_speed: 30
+ wind_arrow_unit_velocity: 20.0
+# 850 hPa
+arrow_black_850:
+ mwind:
+ <<: *arrow_black
+ wind_arrow_max_speed: 50
+ wind_arrow_unit_velocity: 30.0
+# 700 hPa
+arrow_black_700:
+ mwind:
+ <<: *arrow_black
+ wind_arrow_max_speed: 60
+ wind_arrow_unit_velocity: 30.0
+# 500 hPa
+arrow_black_500:
+ mwind:
+ <<: *arrow_black
+ wind_arrow_max_speed: 70
+ wind_arrow_unit_velocity: 50.0
+# 200 hPa
+arrow_black_200:
+ mwind:
+ <<: *arrow_black
+ wind_arrow_max_speed: 100
+ wind_arrow_unit_velocity: 70.0
+# 100 hPa
+arrow_black_100:
+ mwind:
+ <<: *arrow_black
+ wind_arrow_max_speed: 60
+ wind_arrow_unit_velocity: 30.0
+# ----------------------
+# Blue-red wind arrows
+# ----------------------
+# generic
+arrow_blue_red:
+ mwind: &arrow_blue_red
+ legend: "on"
+ wind_advanced_method: "on"
+ wind_advanced_colour_selection_type: "interval"
+ wind_advanced_colour_max_level_colour: "red"
+ wind_advanced_colour_min_level_colour: "blue"
+ wind_advanced_colour_direction: "anticlockwise"
+ wind_thinning_method: "density"
+ wind_density: 2
+ wind_advanced_colour_min_value: 0
+# 10m
+arrow_blue_red_10:
+ mwind:
+ <<: *arrow_blue_red
+ wind_advanced_colour_max_value: 30
+ wind_advanced_colour_level_interval: 5
+ wind_arrow_unit_velocity: 20.0
+# style for wind arrows
+arrow_blue_red:
+ mwind:
+ <<: *arrow_blue_red
+ wind_advanced_colour_max_value: 70
+ wind_advanced_colour_min_value: 0
+ wind_advanced_colour_level_interval: 10
+ wind_arrow_unit_velocity: 50.0
+# 850 hPa
+arrow_blue_red_850:
+ mwind:
+ <<: *arrow_blue_red
+ wind_advanced_colour_max_value: 50
+ wind_arrow_unit_velocity: 30.0
+# 700 hPa
+arrow_blue_red_700:
+ mwind:
+ <<: *arrow_blue_red
+ wind_advanced_colour_max_value: 60
+ wind_arrow_unit_velocity: 30.0
+# 500 hPa
+arrow_blue_red_500:
+ mwind:
+ <<: *arrow_blue_red
+ wind_advanced_colour_max_value: 70
+ wind_arrow_unit_velocity: 50.0
+# 200 hPa
+arrow_blue_red_200:
+ mwind:
+ <<: *arrow_blue_red
+ wind_advanced_colour_max_value: 100
+ wind_arrow_unit_velocity: 70.0
+# 200 hPa
+arrow_blue_red_100:
+ mwind:
+ <<: *arrow_blue_red
+ wind_advanced_colour_max_value: 60
+ wind_arrow_unit_velocity: 30.0
+# ----------------------
+# Black wind flags
+# ----------------------
+# 10m
+flag_black_10:
+ mwind:
+ wind_field_type: "flags"
+ wind_flag_calm_indicator: "off"
+ wind_flag_length: 0.7
+ wind_flag_origin_marker: "off"
+ wind_flag_colour: "black"
+ wind_thinning_method: "density"
+ wind_density: 1
+# upper levels
+flag_black_upper:
+ mwind:
+ wind_field_type: "flags"
+ wind_flag_calm_indicator: "off"
+ wind_flag_length: 0.5
+ wind_flag_origin_marker: "off"
+ wind_flag_colour: "black"
+ wind_thinning_method: "density"
+ wind_density: 1
+ wind_flag_colour: "black"
+# ------------------------
+# CROSS SECTION STYLES
+# ------------------------
+xs_q:
+ mcont:
+ contour_automatic_setting: style_name
+ contour_style_name: sh_spechum_option1
+ legend: "on"
+xs_r:
+ mcont:
+ contour_automatic_setting: style_name
+ contour_style_name: sh_grnblu_f65t100i15_light
+ legend: "on"
+xs_t:
+ mcont:
+ contour_automatic_setting: style_name
+ contour_style_name: sh_all_fM80t56i4_v2
+ legend: "on"
+# style for black wind arrows used in cross section
+xs_arrow_black:
+ mwind:
+ legend: "off"
+ wind_advanced_method: "off"
+ wind_arrow_colour: "black"
+ #wind_advanced_colour_min_value: 0
+ #wind_advanced_colour_level_interval: 10
+ wind_arrow_unit_velocity: 30.0
+ wind_thinning_factor: 1
+# ------------------------
+# SHAPES AND TRACKS
+# ------------------------
+box:
+ mcont:
+ contour: "off"
+ contour_level_selection_type: "level_list"
+ contour_level_list: [0.5,1.1]
+ contour_label: "off"
+ contour_shade: "on"
+ contour_shade_technique: grid_shading
+ contour_shade_colour_method: "list"
+ contour_shade_colour_list: "RGBA(1,0,0,128)"
+frame:
+ mcont:
+ contour: "off"
+ contour_level_selection_type: "level_list"
+ contour_level_list: [0.5,1.1]
+ contour_label: "off"
+ contour_shade: "on"
+ contour_shade_technique: grid_shading
+ contour_shade_colour_method: "list"
+ contour_shade_colour_list: "red"
+track:
+ - msymb:
+ symbol_type: "text"
+ symbol_table_mode: "advanced"
+ symbol_advanced_table_selection_type: "list"
+ symbol_advanced_table_text_font_colour: "black"
+ symbol_advanced_table_text_font_size: 0.5
+ symbol_advanced_table_text_font_style: "normal"
+ symbol_advanced_table_text_display_type: "right"
+ - mgraph:
+ graph_line_colour: red
+ graph_line_thickness: 4
+ graph_symbol: "on"
+ graph_symbol_colour: white
+ graph_symbol_height: 0.5
+ graph_symbol_marker_index: 15
+ graph_symbol_outline: "on"
+ graph_symbol_outline_colour: red
+# ------------------------
+# DIFFERENCE STYLES
+# ------------------------
+# core definition for the positive range
+plus_core:
+ - mcont: &plus
+ legend: "on"
+ contour: "on"
+ contour_highlight: "off"
+ contour_line_colour: "black"
+ contour_level_selection_type: "level_list"
+ #contour_max_level: 40
+ #contour_min_level: 0
+ #contour_interval: 5
+ contour_label: "off"
+ contour_shade: "on"
+ contour_shade_method: "area_fill"
+ #contour_shade_max_level_colour: red
+ contour_shade_max_level_colour: "RGB(110./255.,0,0)"
+ contour_shade_min_level_colour: "RGB(0.937,0.804,0.8041)"
+ contour_shade_colour_direction: anticlockwise
+ grib_scaling_of_derived_fields: "on"
+# core definition for the negative range
+minus_core:
+ - mcont: &minus
+ legend: "on"
+ contour: "on"
+ contour_highlight: "off"
+ contour_line_colour: black
+ contour_level_selection_type: level_list
+ #contour_max_level: 40
+ #contour_min_level: 0
+ #contour_interval: 5
+ contour_label: "off"
+ contour_shade: "on"
+ contour_shade_method: area_fill
+ contour_shade_max_level_colour: "RGB(0.798,0.798,0.9192)"
+ contour_shade_min_level_colour: "RGB(0,25./255,51./255)"
+ #contour_shade_min_level_colour: blue
+ contour_shade_colour_direction: anticlockwise
+ grib_scaling_of_derived_fields: "on"
+plus_core_shade_only:
+ - mcont: &plus_shade
+ <<: *plus
+ contour: "off"
+minus_core_shade_only:
+ - mcont: &minus_shade
+ <<: *minus
+ contour: "off"
+# diff_msl:
+# mcont:
+# <<: *diff_core
+# # contour_level_list: [-50,-20,-10,-5,-0.5,0.5,5,10,20,50]
+# contour_level_list: [-50,-30,-20,-10,-5,-0.5,0.5,5,10,20,30, 50]
+diff_msl:
+ - mcont:
+ # contour_level_list: [-50,-20,-10,-5,-0.5,0.5,5,10,20,50]
+ <<: *minus_shade
+ contour_level_list: [-50,-15,-10,-5,-2]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [2,5,10,15,50]
+# temperature (K)
+diff_t:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-30,-10,-8,-6,-4,-2]
+ grib_scaling_of_derived_fields: "off"
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [2,4,6,8,10,30]
+ grib_scaling_of_derived_fields: "off"
+# equivalent potential temperature (K)
+diff_pt:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-50,-25,-20,-15,-10,-5,-2]
+ grib_scaling_of_derived_fields: "off"
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [2,5,10,15,20,25,50]
+ grib_scaling_of_derived_fields: "off"
+# potential vorticity
+diff_pv:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-100,-20,-10,-5,-2]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [2,5,10,20,100]
+# geopotential (dkm)
+diff_z:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-50,-20,-15,-10,-5,-2]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [2,5,10,15,20,50]
+# precipitation (mm)
+diff_tp:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-200,-20,-10,-5,-0.5]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [0.5,5,10,20,200]
+# wind u/v component (m/s)
+diff_u_10:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-70,-10,-8,-5,-2,-1]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [1,2,5,8,10,70]
+# wind u/v component 700 hPa (m/s)
+diff_u_700:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-100,-20,-10,-5,-2]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [2,5,10,20,100]
+# wind u/v component 500 hPa (m/s)
+diff_u_500:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-100,-20,-10,-5,-2]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [2,5,10,20,100]
+# wind u/v component 200 hPa(m/s)
+diff_u_200:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-100,-30,-20,-10,-5]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [5,10,20,30,100]
+# wind speed (m/s)
+diff_speed_10:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-70,-10,-8,-5,-2,-1]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [1,2,5,8,10,70]
+# wind speed 700 hPa (m/s)
+diff_speed_700:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-100,-20,-10,-5,-2]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [2,5,10,20,100]
+# wind speed 200 hPa(m/s)
+diff_speed_200:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-100,-30,-20,-10,-5]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [5,10,20,30,100]
+# wind gust (m/s)
+diff_10fg3:
+ - mcont:
+ <<: *minus
+ contour_level_list: [-35,-20,-10,-5,-3]
+ - mcont:
+ <<: *plus
+ contour_level_list: [3,5,10,20,35]
+# relative humidity (0-100)
+diff_r:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-100,-60,-30,-10,-5]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [5,10,30,60,100]
+# specific humidity (0-28)
+diff_q:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-30,-10,-5,-2,-1,-0.5]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [0.5,1,2,5,10,30]
+# vorticity (1E-5 1/s)
+diff_vo:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-100,-20,-10,-5,-2]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [2,5,10,20,100]
+# vertical velocity (Pa/s)
+diff_w:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-100,-2,-1,-0.5,-0.2,-0.1]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [0.1,0.2,0.5,1,2,100]
+# t flux
+diff_q_flux:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-0.1,-0.01,-0.005,-0.001,-0.0005]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [0.0005, 0.001, 0.005, 0.01, 0.1]
+# t flux
+diff_t_flux:
+ - mcont:
+ <<: *minus_shade
+ contour_level_list: [-100,-20,-10,-5,-2,-1]
+ - mcont:
+ <<: *plus_shade
+ contour_level_list: [1,2,5,10,20,100]
diff --git a/metview/etc/params.yaml b/metview/etc/params.yaml
new file mode 100644
index 00000000..e0858b1d
--- /dev/null
+++ b/metview/etc/params.yaml
@@ -0,0 +1,668 @@
+# --------------------
+# SCALAR PARAMETERS
+# --------------------
+# 2m dewpoint temperature
+-
+ styles:
+ diff:
+ - diff_t
+ match:
+ -
+ info_name: 2d
+ -
+ shortName: 2d
+ paramId: 168
+# 2m temperature
+-
+ styles:
+ diff:
+ - diff_t
+ match:
+ -
+ info_name: 2t
+ -
+ shortName: 2t
+ paramId: 167
+# 10m wind gust 3h
+-
+ styles:
+ diff:
+ - diff_10fg3
+ match:
+ -
+ info_name: 10fg3
+ -
+ shortName: 10fg3
+ paramId: 228028
+# 10m wind speed
+-
+ styles:
+ diff:
+ - diff_speed_10
+ match:
+ -
+ info_name: 10si
+ -
+ shortName: 10si
+ paramId: 207
+# 10m u wind component
+-
+ styles:
+ basic:
+ - u_10m
+ diff:
+ - diff_u_10
+ match:
+ -
+ info_name: 10u
+ -
+ shortName: 10u
+ paramId: 165
+# 10m v wind component
+-
+ styles:
+ basic:
+ - u_10m
+ diff:
+ - diff_u_10
+ match:
+ -
+ info_name: 10v
+ -
+ shortName: 10v
+ paramId: 166
+# absolute vorticity
+-
+ styles:
+ basic:
+ - vo
+ diff:
+ - diff_vo
+ scaling:
+ - xs
+ match:
+ -
+ info_name: absv
+ -
+ shortName: absv
+ paramId: 3041
+# equivalent potential temperature
+-
+ styles:
+ basic:
+ - pt
+ diff:
+ - diff_pt
+ match:
+ -
+ info_name: eqpt
+ -
+ shortName: eqpt
+ paramId: 4
+# geopotential
+-
+ styles:
+ basic:
+ - z
+ diff:
+ - diff_z
+ match:
+ -
+ info_name: z
+ -
+ shortName: z
+ paramId: 129
+# mean_sea_level_pressure:
+-
+ styles:
+ basic:
+ - msl
+ diff:
+ - diff_msl
+ match:
+ -
+ info_name: msl
+ -
+ shortName: msl
+ paramId: 151
+# potential temperature
+-
+ styles:
+ basic:
+ - pt
+ diff:
+ - diff_pt
+ match:
+ -
+ info_name: pt
+ -
+ shortName: pt
+ paramId: 3
+# potential vorticity
+-
+ styles:
+ basic:
+ - pv
+ diff:
+ - diff_pv
+ scaling:
+ - xs
+ match:
+ -
+ info_name: pv
+# relative humidity
+-
+ styles:
+ diff:
+ - diff_r
+ xs:
+ - xs_r
+ match:
+ -
+ info_name: r
+ -
+ shortName: r
+ paramId: 157
+# relative vorticity
+-
+ styles:
+ basic:
+ - vo
+ diff:
+ - diff_vo
+ match:
+ -
+ info_name: vo
+ -
+ shortName: vo
+ paramId: 138
+ scaling:
+ - xs
+# specific humidity
+-
+ styles:
+ diff:
+ - diff_q
+ xs:
+ - xs_q
+ scaling:
+ - xs
+ match:
+ -
+ info_name: q
+ -
+ shortName: q
+ paramId: 133
+# sea surface temperature
+-
+ styles:
+ diff:
+ - diff_t
+ match:
+ -
+ info_name: sst
+ -
+ shortName: sst
+ paramId: 34
+# temperature
+-
+ styles:
+ diff:
+ - diff_t
+ xs:
+ - xs_t
+ scaling:
+ - xs
+ match:
+ -
+ info_name: t
+ -
+ shortName: t
+ paramId: 130
+# total precipitation
+-
+ styles:
+ diff:
+ - diff_tp
+ match:
+ -
+ info_name: tp
+ -
+ shortName: tp
+ paramId: 228
+# vertical (pressure) velocity
+-
+ styles:
+ basic:
+ - w
+ diff:
+ - diff_w
+ match:
+ -
+ info_name: w
+ -
+ shortName: w
+ paramId: 135
+# u wind component 1000hPa
+-
+ styles:
+ basic:
+ - u_10m
+ diff:
+ - diff_u_10
+ match:
+ -
+ info_name: u
+ levtype: pl
+ levelist: 1000
+ -
+ shortName: u
+ paramId: 131
+ levtype: pl
+ levelist: 1000
+# u wind component around 700 hPa
+-
+ styles:
+ basic:
+ - u_700
+ diff:
+ - diff_u_700
+ match:
+ -
+ info_name: u
+ levtype: pl
+ levelist: [925, 850, 700]
+ -
+ shortName: u
+ paramId: 131
+ levtype: pl
+ levelist: [925, 850, 700]
+# u wind component around 500 hPa + 100 hPa
+-
+ styles:
+ basic:
+ - u_500
+ diff:
+ - diff_u_500
+ match:
+ -
+ info_name: u
+ levtype: pl
+ levelist: [600, 500, 100]
+ -
+ shortName: u
+ paramId: 131
+ levtype: pl
+ levelist: [600, 500, 100]
+# u wind component around 200 hPa
+-
+ styles:
+ basic:
+ - u_200
+ diff:
+ - diff_u_200
+ match:
+ -
+ info_name: u
+ levtype: pl
+ levelist: [400, 300, 250, 200, 150]
+ -
+ shortName: u
+ paramId: 131
+ levtype: pl
+ levelist: [400, 300, 250, 200, 150]
+# v wind component 1000hPa
+-
+ styles:
+ basic:
+ - u_10m
+ diff:
+ - diff_u_10
+ match:
+ -
+ info_name: v
+ levtype: pl
+ levelist: 1000
+ -
+ shortName: v
+ paramId: 132
+ levtype: pl
+ levelist: 1000
+# v wind component around 700 hPa
+-
+ styles:
+ basic:
+ - u_700
+ diff:
+ - diff_u_700
+ match:
+ -
+ info_name: v
+ levtype: pl
+ levelist: [925, 850, 700]
+ -
+ shortName: v
+ paramId: 132
+ levtype: pl
+ levelist: [925, 850, 700]
+# v wind component around 500 hPa + 100 hPa
+-
+ styles:
+ basic:
+ - u_500
+ diff:
+ - diff_u_500
+ match:
+ -
+ info_name: v
+ levtype: pl
+ levelist: [600, 500, 100]
+ -
+ shortName: v
+ paramId: 132
+ levtype: pl
+ levelist: [600, 500, 100]
+# v wind component around 200 hPa
+-
+ styles:
+ basic:
+ - u_200
+ diff:
+ - diff_u_200
+ match:
+ -
+ info_name: v
+ levtype: pl
+ levelist: [400, 300, 250, 200, 150]
+ -
+ shortName: v
+ paramId: 132
+ levtype: pl
+ levelist: [400, 300, 250, 200, 150]
+# wind speed 1000hPa
+-
+ styles:
+ diff:
+ - diff_speed_10
+ match:
+ -
+ info_name: ws
+ levtype: pl
+ levelist: 1000
+ -
+ shortName: ws
+ paramId: 10
+ levtype: pl
+ levelist: 1000
+# wind speed around 700 hPa + 100 hPa
+-
+ styles:
+ diff:
+ - diff_speed_700
+ match:
+ -
+ info_name: ws
+ levtype: pl
+ levelist: [925, 850, 700, 600, 100]
+ -
+ shortName: ws
+ paramId: 10
+ levtype: pl
+ levelist: [925, 850, 700, 600, 100]
+# wind speed around 200 hPa
+-
+ styles:
+ diff:
+ - diff_speed_200
+ match:
+ -
+ info_name: ws
+ levtype: pl
+ levelist: [500, 400, 300, 250, 200, 150]
+ -
+ shortName: ws
+ paramId: 10
+ levtype: pl
+ levelist: [500, 400, 300, 250, 200, 150]
+# --------------------
+# VECTOR PARAMETERS
+# --------------------
+# wind (generic)
+-
+ type: vector
+ styles:
+ basic:
+ - arrow_blue_red
+ - arrow_black
+ match:
+ -
+ info_name: wind
+# wind 10 m
+-
+ type: vector
+ styles:
+ basic:
+ - arrow_blue_red_10
+ - arrow_black_10
+ - flag_black_10
+ match:
+ -
+ info_name: wind10m
+# wind 1000 hPa
+-
+ type: vector
+ styles:
+ basic:
+ - arrow_blue_red_10
+ - arrow_black_10
+ - flag_black_10
+ match:
+ -
+ info_name: wind
+ levtype: pl
+ levelist: 1000
+# wind 925-850 hPa
+-
+ type: vector
+ styles:
+ basic:
+ - arrow_blue_red_850
+ - arrow_black_850
+ - flag_black_upper
+ match:
+ -
+ info_name: wind
+ levtype: pl
+ levelist: [925,850]
+# wind 700-600 hPa
+-
+ type: vector
+ styles:
+ basic:
+ - arrow_blue_red_700
+ - arrow_black_700
+ - flag_black_upper
+ match:
+ -
+ info_name: wind
+ levtype: pl
+ levelist: [700, 600]
+# wind 500-400 hPa
+-
+ type: vector
+ styles:
+ basic:
+ - arrow_blue_red_500
+ - arrow_black_500
+ - flag_black_upper
+ match:
+ -
+ info_name: wind
+ levtype: pl
+ levelist: [500, 400]
+# wind 300-150 hPa
+-
+ type: vector
+ styles:
+ basic:
+ - arrow_blue_red_200
+ - arrow_black_200
+ - flag_black_upper
+ match:
+ -
+ info_name: wind
+ levtype: pl
+ levelist: [300, 250, 200, 150]
+# wind 100- hPa
+-
+ type: vector
+ styles:
+ basic:
+ - arrow_blue_red_100
+ - arrow_black_100
+ - flag_black_upper
+ match:
+ -
+ info_name: wind
+ levtype: pl
+ levelist: 100
+# wind3d (generic)
+-
+ type: vector
+ styles:
+ basic:
+ - xs_arrow_black
+ match:
+ -
+ info_name: wind3d
+# -------------------------------------------
+# PARAMETERS REPRESENTING GEOMETRIC SHAPES
+# -------------------------------------------
+-
+ styles:
+ basic:
+ - box
+ match:
+ -
+ info_name: box
+-
+ styles:
+ basic:
+ - frame
+ match:
+ -
+ info_name: frame
+
+# -------------------------------------------
+# TRACKS
+# -------------------------------------------
+-
+ styles:
+ basic:
+ - track
+ match:
+ -
+ info_name: track
+
+# ---------------------------------
+# PARAMETERS WITHOUT A SHORTNAME
+# ---------------------------------
+-
+ styles:
+ basic:
+ - q_flux
+ diff:
+ - diff_q_flux
+ match:
+ -
+ info_name: qcl
+ -
+ paramId: 110
+-
+ styles:
+ basic:
+ - q_flux
+ diff:
+ - diff_q_flux
+ match:
+ -
+ info_name: qcon
+ -
+ paramId: 106
+-
+ styles:
+ basic:
+ - q_flux
+ diff:
+ - diff_q_flux
+ match:
+ -
+ info_name: qdyn
+ -
+ paramId: 94
+-
+ styles:
+ basic:
+ - q_flux
+ diff:
+ - diff_q_flux
+ match:
+ -
+ info_name: qvdiff
+ -
+ paramId: 99
+-
+ styles:
+ basic:
+ - t_flux
+ diff:
+ - diff_t_flux
+ match:
+ -
+ info_name: tcl
+ -
+ paramId: 109
+-
+ styles:
+ basic:
+ - t_flux
+ diff:
+ - diff_t_flux
+ match:
+ -
+ info_name: tcon
+ -
+ paramId: 105
+-
+ styles:
+ basic:
+ - t_flux
+ diff:
+ - diff_t_flux
+ match:
+ -
+ info_name: tdyn
+ -
+ paramId: 93
+-
+ styles:
+ basic:
+ - t_flux
+ diff:
+ - diff_t_flux
+ match:
+ -
+ info_name: trad
+ -
+ paramId: 95
+-
+ styles:
+ basic:
+ - t_flux
+ diff:
+ - diff_t_flux
+ match:
+ -
+ info_name: tvdiff
+ -
+ paramId: 98
diff --git a/metview/etc/scaling_ecmwf.yaml b/metview/etc/scaling_ecmwf.yaml
new file mode 100644
index 00000000..c5fc9447
--- /dev/null
+++ b/metview/etc/scaling_ecmwf.yaml
@@ -0,0 +1,252 @@
+10**5/sec:
+ match: []
+ paramId:
+ - '138'
+ - '155'
+ - '3041'
+ shortName:
+ - vo
+ - absv
+ - d
+C:
+ match:
+ - centre: '7'
+ paramId: '11'
+ - centre: '46'
+ paramId: '128'
+ - centre: '46'
+ paramId: '129'
+ - centre: '46'
+ paramId: '158'
+ - centre: '46'
+ paramId: '175'
+ - centre: '46'
+ paramId: '187'
+ - centre: '46'
+ paramId: '188'
+ - centre: '46'
+ paramId: '189'
+ - centre: '46'
+ paramId: '190'
+ - centre: '46'
+ paramId: '191'
+ - centre: '46'
+ paramId: '232'
+ - centre: '46'
+ paramId: '234'
+ - centre: '46'
+ paramId: '243'
+ - centre: '46'
+ paramId: '251'
+ - centre: '46'
+ paramId: '253'
+ paramId:
+ - '34'
+ - '35'
+ - '36'
+ - '37'
+ - '38'
+ - '51'
+ - '52'
+ - '121'
+ - '122'
+ - '130'
+ - '139'
+ - '167'
+ - '168'
+ - '170'
+ - '183'
+ - '201'
+ - '202'
+ - '208'
+ - '235'
+ - '236'
+ - '238'
+ - '228004'
+ - '228008'
+ - '228010'
+ - '228011'
+ - '228013'
+ - '228026'
+ - '228027'
+ - '260510'
+ shortName:
+ - mn2t6
+ - ltlt
+ - lmlt
+ - mn2t24
+ - skt
+ - 2d
+ - mx2t24
+ - clbt
+ - lict
+ - mn2t
+ - mn2t3
+ - istl4
+ - lblt
+ - mx2t3
+ - istl3
+ - stl1
+ - istl2
+ - tsn
+ - mx2t6
+ - mean2t
+ - stl2
+ - mx2t
+ - sst
+ - stl4
+ - stl3
+ - 2t
+ - istl1
+ - t
+Dob:
+ match: []
+ paramId:
+ - '206'
+ shortName: []
+cm:
+ match: []
+ paramId:
+ - '44'
+ - '45'
+ - '141'
+ - '149'
+ - '160'
+ - '173'
+ shortName:
+ - smlt
+ - es
+dam:
+ match: []
+ paramId:
+ - '129'
+ - '156'
+ - '206'
+ - '171129'
+ shortName:
+ - gh
+ - za
+ - z
+g/kg:
+ match: []
+ paramId:
+ - '133'
+ - '203'
+ - '233'
+ - '246'
+ - '247'
+ shortName:
+ - q
+μg/kg:
+ match: []
+ paramId:
+ - '217004'
+ - '210123'
+ - '210203'
+ - '210124'
+ - '217006'
+ - '217028'
+ - '210121'
+ - '217027'
+ - '217030'
+ shortName:
+ - ch4_c
+ - co
+ - go3
+ - hcho
+ - hno3
+ - ho2
+ - no2
+ - no
+ - oh
+ - so2
+hPa:
+ match: []
+ paramId:
+ - '54'
+ - '134'
+ - '151'
+ shortName:
+ - msl
+ - sp
+kg m**-2 h**-1:
+ match: []
+ paramId:
+ - '3059'
+ - '3064'
+ - '174142'
+ - '174143'
+ - '228218'
+ - '228219'
+ - '228220'
+ - '228221'
+ - '228222'
+ - '228223'
+ - '228224'
+ - '228225'
+ - '228226'
+ - '228227'
+ - '260048'
+ shortName:
+ - mxtpr
+ - crr
+ - mxtpr3
+ - mntpr6
+ - srweq
+ - mntpr3
+ - lsrr
+ - mntpr
+ - tprate
+ - crfrate
+ - lsrrate
+ - lssfr
+ - prate
+ - mxtpr6
+ - csfr
+mm:
+ match: []
+ paramId:
+ - '8'
+ - '9'
+ - '140'
+ - '142'
+ - '143'
+ - '144'
+ - '171'
+ - '182'
+ - '184'
+ - '198'
+ - '205'
+ - '228'
+ - '237'
+ - '239'
+ - '240'
+ - '244'
+ - '174008'
+ - '174009'
+ - '228216'
+ shortName:
+ - ssro
+ - fzra
+ - lsp
+ - csf
+ - sf
+ - tp
+ - cp
+ - sro
+ - ro
+ - lsf
+ppbv:
+ match: []
+ paramId:
+ - '210062'
+ - '217004'
+ shortName:
+ - ch4_c
+ - ch4
+pv units:
+ match: []
+ paramId:
+ - '60'
+ shortName:
+ - pv
diff --git a/metview/etc/units-rules.json b/metview/etc/units-rules.json
new file mode 100644
index 00000000..98a59373
--- /dev/null
+++ b/metview/etc/units-rules.json
@@ -0,0 +1,137 @@
+{
+ "C": [
+ {
+ "from" : "K",
+ "to" : "C",
+ "offset" : -273.15,
+ "scaling" : 1
+ }
+ ],
+ "pv units" : [
+ {
+ "from" : "K m**2 kg**-1 s**-1",
+ "offset" : -0.0,
+ "to" : "pv units",
+ "scaling" : 1000000.0
+ }
+ ],
+ "10**5/sec" : [
+ {
+ "from" : "s**-1",
+ "to" : "10**5/sec",
+ "offset" : -0.0,
+ "scaling" : 100000.0
+ }
+ ],
+ "g/kg" : [
+ {
+ "from" : "kg kg**-1",
+ "to" : "g/kg",
+ "offset" : -0.0,
+ "scaling" : 1000.0
+ },
+ {
+ "from" : "1",
+ "to" : "gr kg-1",
+ "offset" : 0.0,
+ "scaling" : 1000.0
+ }
+ ],
+ "μg/kg" : [
+ {
+ "from" : "kg kg**-1",
+ "to" : "μg/kg",
+ "offset" : -0.0,
+ "scaling" : 1e9
+ },
+ {
+ "from" : "1",
+ "to" : "μgr kg-1",
+ "offset" : 0.0,
+ "scaling" : 1e9
+ }
+ ],
+ "mm" : [
+ {
+ "from" : "m",
+ "to" : "mm",
+ "offset" : -0.0,
+ "scaling" : 1000.0
+ },
+ {
+ "from" : "m of water equivalent",
+ "to" : "mm",
+ "offset" : -0.0,
+ "scaling" : 1000.0
+ },
+ {
+ "from" : "m of water",
+ "to" : "mm",
+ "offset" : -0.0,
+ "scaling" : 1000.0
+ }
+ ],
+ "cm" : [
+ {
+ "from" : "m",
+ "to" : "cm",
+ "offset" : -0.0,
+ "scaling" : 100.0
+ },
+ {
+ "from" : "m of water equivalent",
+ "to" : "cm",
+ "offset" : -0.0,
+ "scaling" : 100.0
+ }
+ ],
+ "dam" : [
+ {
+ "from" : "m**2 s**-2",
+ "offset" : -0.0,
+ "to" : "dam",
+ "scaling" : 0.0101971621297793
+ },
+ {
+ "from" : "m",
+ "offset" : -0.0,
+ "to" : "dam",
+ "scaling" : 10.0
+ },
+ {
+ "from" : "gpm",
+ "offset" : -0.0,
+ "to" : "dam",
+ "scaling" : 10.0
+ }
+ ],
+ "hPa" : [
+ {
+ "from" : "Pa",
+ "offset" : -0.0,
+ "to" : "hPa",
+ "scaling" : 0.01
+ }
+ ],
+ "kg m**-2 h**-1" : [
+ {
+ "from" : "kg m**-2 s**-1",
+ "offset" : -0.0,
+ "to" : "kg m**-2 h**-1",
+ "scaling" : 3600.0
+ }
+ ],
+ "Dob" : [
+ {
+ "from" : "kg m**-2",
+ "offset" : -0.0,
+ "to" : "Dob",
+ "scaling" : 46696.2409526033
+ }
+ ]
+}
+
+
+
+
+
diff --git a/metview/gallery.py b/metview/gallery.py
index 27f91c7d..bc0d4c11 100644
--- a/metview/gallery.py
+++ b/metview/gallery.py
@@ -7,20 +7,42 @@
# granted to it by virtue of its status as an intergovernmental organisation
# nor does it submit to any jurisdiction.
+import os
import zipfile
import metview as mv
-def load_dataset(filename):
- base_url = "http://download.ecmwf.org/test-data/metview/gallery/"
+def load_dataset(filename, check_local=False):
+ def _simple_download(url, target):
+ import requests
+
+ r = requests.get(url, allow_redirects=True)
+ r.raise_for_status()
+ open(target, "wb").write(r.content)
+
+ if check_local and os.path.exists(filename):
+ try:
+ return mv.read(filename)
+ except:
+ return None
+
+ base_url = "https://get.ecmwf.int/test-data/metview/gallery/"
try:
- d = mv.download(url=base_url + filename, target=filename)
- if filename.endswith(".zip"):
- with zipfile.ZipFile(filename, "r") as f:
- f.extractall()
- return d
- except:
+ # d = mv.download(url=base_url + filename, target=filename)
+ _simple_download(os.path.join(base_url, filename), filename)
+ except Exception as e:
raise Exception(
- "Could not download file " + filename + " from the download server"
+ f"Could not download file={filename} from the download server. {e}"
)
+
+ d = None
+ if filename.endswith(".zip"):
+ with zipfile.ZipFile(filename, "r") as f:
+ f.extractall()
+ else:
+ try:
+ d = mv.read(filename)
+ except:
+ pass
+ return d
diff --git a/metview/layout.py b/metview/layout.py
new file mode 100644
index 00000000..a22ff46f
--- /dev/null
+++ b/metview/layout.py
@@ -0,0 +1,306 @@
+#
+# (C) Copyright 2020- ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation
+# nor does it submit to any jurisdiction.
+#
+
+import logging
+import math
+
+import metview as mv
+
+LOG = logging.getLogger(__name__)
+
+
+class Layout:
+
+ GRID_DEF = {
+ 1: [1, 1],
+ 2: [2, 1],
+ 3: [3, 1],
+ 4: [2, 2],
+ 8: [3, 3],
+ 9: [3, 3],
+ 10: [4, 3],
+ }
+
+ def _grid_row_col(self, page_num=0, rows=None, columns=None, layout=None):
+ if rows is None and columns is None:
+ if layout is not None and layout != "":
+ v = layout.split("x")
+ if len(v) == 2:
+ try:
+ r = int(v[0])
+ c = int(v[1])
+ # LOG.debug(f"r{r} c={c}")
+ except:
+ raise Exception(f"Invalid layout specification ({layout}")
+ if page_num > 0 and r * c < page_num:
+ raise Exception(
+ f"Layout specification={layout} does not match number of scenes={page_num}"
+ )
+ else:
+ if page_num in self.GRID_DEF:
+ r, c = self.GRID_DEF[page_num]
+ elif page_num >= 1:
+ r, c = self._compute_row_col(page_num)
+ else:
+ raise Exception(f"Cannot create a layout for {page_num} pages!")
+ return r, c
+
+ def build_grid(self, page_num=0, rows=None, columns=None, layout=None, view=None):
+ r, c = self._grid_row_col(
+ page_num=page_num, rows=rows, columns=columns, layout=layout
+ )
+
+ if isinstance(view, mv.style.GeoView):
+ v = view.to_request()
+ else:
+ v = view
+
+ return self._build_grid(rows=r, columns=c, view=v)
+
+ def _build_grid(self, rows=1, columns=1, view=None):
+ assert rows >= 1 and columns >= 1
+ if rows == 1 and columns == 1:
+ return mv.plot_superpage(pages=[mv.plot_page(view=view)])
+ else:
+ return mv.plot_superpage(
+ pages=mv.mvl_regular_layout(
+ view, columns, rows, 1, 1, [5, 100, 15, 100]
+ )
+ )
+
+ def _compute_row_col(self, page_num):
+ # has been checked for 100
+ r = int(math.sqrt(page_num))
+ c = r
+ if c * r < page_num:
+ if (page_num - c * r) % c == 0:
+ c += (page_num - c * r) // c
+ else:
+ c += 1 + (page_num - c * r) // c
+ return (r, c)
+
+ def build_diff(self, view):
+ page_1 = mv.plot_page(top=5, bottom=50, left=25, right=75, view=view)
+
+ page_2 = mv.plot_page(top=52, bottom=97, right=50, view=view)
+
+ page_3 = mv.plot_page(top=52, bottom=97, left=50, right=100, view=view)
+
+ return mv.plot_superpage(pages=[page_1, page_2, page_3])
+
+ def build_xs(self, line, map_view):
+ xs_view = mv.mxsectview(
+ line=line,
+ bottom_level=1000,
+ top_level=150
+ # wind_perpendicular : "off",
+ # wind_parallel :"on",
+ # wind_intensity :"off"
+ )
+
+ page_1 = mv.plot_page(top=35 if map_view is not None else 5, view=xs_view)
+
+ if map_view is not None:
+ page = mv.plot_page(top=5, bottom=35, view=map_view)
+ return mv.plot_superpage(pages=[page_1, page])
+ else:
+ return mv.plot_superpage(pages=[page_1])
+
+ def build_xs_avg(
+ self,
+ area,
+ direction,
+ bottom_level,
+ top_level,
+ vertical_scaling,
+ axis_label_height=0.4,
+ ):
+ axis = mv.maxis(axis_tick_label_height=axis_label_height)
+
+ return mv.maverageview(
+ area=area,
+ direction=direction,
+ bottom_level=bottom_level,
+ top_level=top_level,
+ vertical_scaling=vertical_scaling,
+ horizontal_axis=axis,
+ vertical_axis=axis,
+ )
+
+ def build_stamp(self, page_num=0, layout="", view=None):
+
+ if True:
+ coast_empty = mv.mcoast(
+ map_coastline="off", map_grid="off", map_label="off"
+ )
+
+ empty_view = mv.geoview(
+ page_frame="off",
+ subpage_frame="off",
+ coastlines=coast_empty,
+ subpage_x_position=40,
+ subpage_x_length=10,
+ subpage_y_length=10,
+ )
+
+ title_page = mv.plot_page(
+ top=0, bottom=5, left=30, right=70, view=empty_view
+ )
+
+ r, c = self._grid_row_col(page_num=page_num, layout=layout)
+
+ pages = mv.mvl_regular_layout(view, c, r, 1, 1, [5, 100, 15, 100])
+ pages.append(title_page)
+ return mv.plot_superpage(pages=pages)
+
+ # g = self.build_grid(page_num=page_num, layout=layout, view=view)
+ # return g
+
+ def build_rmse(self, xmin, xmax, ymin, ymax, xtick, ytick, xtitle, ytitle):
+
+ horizontal_axis = mv.maxis(
+ axis_type="date",
+ axis_orientation="horizontal",
+ axis_position="bottom",
+ # axis_title : 'on',
+ # axis_title_height : 0.4,
+ axis_grid="on",
+ axis_grid_colour="grey",
+ # axis_title_text : xtitle,
+ # axis_title_quality : 'high',
+ axis_tick_interval=xtick,
+ axis_tick_label_height=0.6,
+ axis_date_type="days",
+ axis_years_label="off",
+ axis_months_label="off",
+ axis_days_label_height="0.3",
+ )
+
+ vertical_axis = mv.maxis(
+ axis_orientation="vertical",
+ axis_position="left",
+ axis_grid="on",
+ axis_grid_colour="grey",
+ axis_title="on",
+ axis_title_height=0.4,
+ axis_title_text=ytitle,
+ # axis_title_quality : 'high',
+ axis_tick_interval=ytick,
+ axis_tick_label_height=0.3,
+ )
+
+ cview = mv.cartesianview(
+ page_frame="off",
+ x_axis_type="date",
+ y_axis_type="regular",
+ y_automatic="off",
+ x_automatic="off",
+ y_min=ymin,
+ y_max=ymax,
+ x_date_min=xmin,
+ x_date_max=xmax,
+ horizontal_axis=horizontal_axis,
+ vertical_axis=vertical_axis,
+ )
+
+ return cview
+
+ def build_xy(self, xmin, xmax, ymin, ymax, xtick, ytick, xtitle, ytitle):
+
+ horizontal_axis = mv.maxis(
+ axis_orientation="horizontal",
+ axis_position="bottom",
+ axis_title="on",
+ axis_title_height=0.5,
+ axis_title_text=xtitle,
+ # axis_title : 'on',
+ # axis_title_height : 0.4,
+ axis_grid="on",
+ axis_grid_colour="grey",
+ # axis_title_text : xtitle,
+ # axis_title_quality : 'high',
+ axis_tick_interval=xtick,
+ axis_tick_label_height=0.6,
+ axis_date_type="days",
+ axis_years_label="off",
+ axis_months_label="off",
+ axis_days_label_height="0.3",
+ )
+
+ vertical_axis = mv.maxis(
+ axis_orientation="vertical",
+ axis_position="left",
+ axis_grid="on",
+ axis_grid_colour="grey",
+ axis_title="on",
+ axis_title_height=0.6,
+ axis_title_text=ytitle,
+ # axis_title_quality : 'high',
+ axis_tick_interval=ytick,
+ axis_tick_label_height=0.3,
+ )
+
+ cview = mv.cartesianview(
+ page_frame="off",
+ x_axis_type="regular",
+ y_axis_type="regular",
+ y_automatic="off",
+ x_automatic="off",
+ y_min=ymin,
+ y_max=ymax,
+ x_min=xmin,
+ x_max=xmax,
+ horizontal_axis=horizontal_axis,
+ vertical_axis=vertical_axis,
+ )
+
+ return cview
+
+ @staticmethod
+ def compute_axis_range(v_min, v_max):
+ count = 15
+ d = math.fabs(v_max - v_min)
+ if d > 0:
+ b = d / count
+ n = math.floor(math.log10(b))
+ v = b / math.pow(10, n)
+ # print("d={} b={} n={} v={}".format(d,b,n,v))
+
+ if v <= 1:
+ v = 1
+ elif v <= 2:
+ v = 2
+ elif v <= 5:
+ v = 5
+ else:
+ v = 10
+
+ bin_size = v * math.pow(10, n)
+ bin_start = math.ceil(v_min / bin_size) * bin_size
+ if bin_start >= v_min and math.fabs(bin_start - v_min) < bin_size / 10000:
+ bin_start = bin_start - bin_size / 100000
+ av = v_min
+ else:
+ bin_start = bin_start - bin_size
+ av = bin_start
+
+ max_iter = 100
+ act_iter = 0
+ while av < v_max:
+ av += bin_size
+ act_iter += 1
+ if act_iter > max_iter:
+ return (0, v_min, v_max)
+
+ bin_end = av # + bin_size / 100000
+ return bin_size, bin_start, bin_end
+ else:
+ return (0, v_min, v_max)
diff --git a/metview/metviewpy/__init__.py b/metview/metviewpy/__init__.py
new file mode 100644
index 00000000..ecc7c909
--- /dev/null
+++ b/metview/metviewpy/__init__.py
@@ -0,0 +1,29 @@
+# (C) Copyright 2017- ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation
+# nor does it submit to any jurisdiction.
+#
+
+import os
+
+
+# if we're running pytest, it will need the fieldset functionality, so detect if it
+# is running and if so, import the user-facing functions
+
+
+def running_from_pytest():
+ from inspect import stack
+
+ call_stack = [s.function for s in stack()]
+ return "pytest_collection" in call_stack
+
+
+if "METVIEW_PYTHON_ONLY" in os.environ or running_from_pytest():
+
+ from . import fieldset
+
+ fieldset.bind_functions(globals(), module_name=__name__)
diff --git a/metview/metviewpy/fieldset.py b/metview/metviewpy/fieldset.py
new file mode 100644
index 00000000..c280186f
--- /dev/null
+++ b/metview/metviewpy/fieldset.py
@@ -0,0 +1,890 @@
+# (C) Copyright 2017- ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation
+# nor does it submit to any jurisdiction.
+
+
+from inspect import signature
+import sys
+
+import numpy as np
+import eccodes
+
+from . import maths
+from .temporary import temp_file
+
+from . import indexdb as indexdb
+from . import utils
+
+
+BITS_PER_VALUE_FOR_WRITING = 24
+
+
+class CodesHandle:
+ """Wraps an ecCodes handle"""
+
+ MISSING_VALUE = 1e34
+
+ STRING = ("s", eccodes.codes_get_string, eccodes.codes_set_string)
+ LONG = ("l", eccodes.codes_get_long, eccodes.codes_set_long)
+ DOUBLE = ("d", eccodes.codes_get_double, eccodes.codes_set_double)
+ LONG_ARRAY = ("la", eccodes.codes_get_long_array, eccodes.codes_set_long_array)
+ DOUBLE_ARRAY = (
+ "da",
+ eccodes.codes_get_double_array,
+ eccodes.codes_set_double_array,
+ )
+ NATIVE = ("n", eccodes.codes_get, eccodes.codes_set)
+ NATIVE_ARRAY = ("na", eccodes.codes_get_array, eccodes.codes_set_array)
+
+ _STR_TYPES = {
+ v[0]: tuple(v[1:3])
+ for v in [STRING, LONG, DOUBLE, LONG_ARRAY, DOUBLE_ARRAY, NATIVE, NATIVE_ARRAY]
+ }
+
+ def __init__(self, handle, path, offset):
+ self.handle = handle
+ self.path = path
+ self.offset = offset
+ eccodes.codes_set(handle, "missingValue", CodesHandle.MISSING_VALUE)
+
+ def __del__(self):
+ # print("CodesHandle:release ", self)
+ eccodes.codes_release(self.handle)
+
+ def clone(self):
+ new_handle = eccodes.codes_clone(self.handle)
+ return CodesHandle(new_handle, None, None)
+
+ # def __str__(self):
+ # s = "CodesHandle("
+ # return s + str(self.handle) + "," + self.path + "," + str(self.offset) + ")"
+
+ def call_get_func(self, func, key, default=False):
+ try:
+ result = func(self.handle, key)
+ except Exception as exp:
+ if default != False:
+ return default
+ else:
+ raise exp
+ return result
+
+ def get_any(self, keys, key_type=None):
+ # single key with key_type specified
+ if key_type is not None:
+ assert isinstance(keys, str)
+ func = key_type[1]
+ return func(self.handle, keys)
+ # list of keys
+ else:
+ result = []
+ for key in keys:
+ key_type_str = "s"
+ parts = key.split(":")
+ if len(parts) == 2:
+ key, key_type_str = parts
+ if (
+ key_type_str == "n"
+ and eccodes.codes_get_size(self.handle, key) > 1
+ ):
+ key_type_str = "na"
+ func = CodesHandle._STR_TYPES.get(key_type_str, None)[0]
+ result.append(self.call_get_func(func, key, default=None))
+ return result
+
+ def get_string(self, key):
+ return eccodes.codes_get_string(self.handle, key)
+
+ def get_long(self, key):
+ return eccodes.codes_get_long(self.handle, key)
+
+ def get_double(self, key):
+ return eccodes.codes_get_double(self.handle, key)
+
+ def get_long_array(self, key):
+ return eccodes.codes_get_long_array(self.handle, key)
+
+ def get_double_array(self, key):
+ return eccodes.codes_get_double_array(self.handle, key)
+
+ def get_values(self):
+ vals = eccodes.codes_get_values(self.handle)
+ if self.get_long("bitmapPresent"):
+ vals[vals == CodesHandle.MISSING_VALUE] = np.nan
+ return vals
+
+ def set_any(self, keys_and_vals, key_type=None):
+ for key, v in zip(keys_and_vals[0::2], keys_and_vals[1::2]):
+ if key_type is not None:
+ func = key_type[2]
+ else:
+ key_type_str = "s" # default is str
+ parts = key.split(":")
+ if len(parts) == 2:
+ key, key_type_str = parts
+ func = CodesHandle._STR_TYPES.get(key_type_str, None)[1]
+ func(self.handle, key, v)
+
+ def set_string(self, key, value):
+ eccodes.codes_set_string(self.handle, key, value)
+
+ def set_long(self, key, value):
+ eccodes.codes_set_long(self.handle, key, value)
+
+ def set_double(self, key, value):
+ eccodes.codes_set_double(self.handle, key, value)
+
+ def set_double_array(self, key, value):
+ eccodes.codes_set_double_array(self.handle, key, value)
+
+ def set_values(self, value):
+ # replace nans with missing values
+ values_nans_replaced = np.nan_to_num(
+ value, copy=True, nan=CodesHandle.MISSING_VALUE
+ )
+ self.set_long("bitsPerValue", BITS_PER_VALUE_FOR_WRITING)
+ self.set_double_array("values", values_nans_replaced)
+
+ def write(self, fout, path):
+ self.offset = fout.tell()
+ eccodes.codes_write(self.handle, fout)
+ if path:
+ self.path = path
+
+
+class GribFile:
+ """Encapsulates a GRIB file, giving access to an iteration of CodesHandles"""
+
+ def __init__(self, path):
+ self.path = path
+ self.file = open(path, "rb")
+ self.num_messages = eccodes.codes_count_in_file(self.file)
+
+ def __del__(self):
+ try:
+ # print("GribFile:close")
+ self.file.close()
+ except Exception:
+ pass
+
+ def __len__(self):
+ return self.num_messages
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ handle = self._next_handle()
+ if handle is None:
+ raise StopIteration()
+ return handle
+
+ def _next_handle(self):
+ offset = self.file.tell()
+ handle = eccodes.codes_new_from_file(self.file, eccodes.CODES_PRODUCT_GRIB)
+ if not handle:
+ return None
+ return CodesHandle(handle, self.path, offset)
+
+
+class Field:
+ """Encapsulates single GRIB message"""
+
+ def __init__(self, handle, gribfile, keep_values_in_memory=False, temp=None):
+ self.handle = handle
+ self.gribfile = gribfile
+ self.temp = temp
+ self.vals = None
+ self.keep_values_in_memory = keep_values_in_memory
+
+ def grib_get(self, *args, **kwargs):
+ return self.handle.get_any(*args, **kwargs)
+
+ def values(self):
+ if self.vals is None:
+ vals = self.handle.get_values()
+ if self.keep_values_in_memory:
+ self.vals = vals
+ else:
+ vals = self.vals
+ return vals
+
+ def latitudes(self):
+ return self.handle.get_double_array("latitudes")
+
+ def longitudes(self):
+ return self.handle.get_double_array("longitudes")
+
+ def grib_set(self, *args, **kwargs):
+ result = self.clone()
+ result.handle.set_any(*args, **kwargs)
+ return result
+
+ def encode_values(self, value):
+ self.handle.set_long("bitmapPresent", 1)
+ self.handle.set_values(value)
+ if not self.keep_values_in_memory:
+ self.vals = None
+
+ def write(self, fout, path, temp=None):
+ self.temp = temp # store a reference to the temp file object for persistence
+ self.handle.write(fout, path)
+
+ def grib_index(self):
+ return (self.handle.path, self.handle.offset)
+ # return (self.handle.path, self.grib_get("offset", key_type=CodesHandle.LONG))
+
+ def clone(self):
+ c = Field(
+ self.handle.clone(),
+ self.gribfile,
+ self.keep_values_in_memory,
+ )
+ c.vals = None
+ return c
+
+ def field_func(self, func):
+ """Applies a function to all values, returning a new Field"""
+ result = self.clone()
+ result.vals = func(self.values())
+ result.encode_values(result.vals)
+ return result
+
+ def field_other_func(self, func, other, reverse_args=False):
+ """Applies a function with something to all values, returning a new Field"""
+ result = self.clone()
+ if isinstance(other, Field):
+ other = other.values()
+ if reverse_args:
+ result.vals = func(other, self.values())
+ else:
+ result.vals = func(self.values(), other)
+ result.encode_values(result.vals)
+ return result
+
+
+# decorator to implement math functions in Fieldset
+def wrap_maths(cls):
+ def wrap_single_method(fn):
+ def wrapper(self):
+ return self.field_func(fn)
+
+ return wrapper
+
+ def wrap_double_method(fn, **opt):
+ def wrapper(self, *args):
+ return self.fieldset_other_func(fn, *args, **opt)
+
+ return wrapper
+
+ for name, it in cls.WRAP_MATHS_ATTRS.items():
+ if not isinstance(it, tuple):
+ it = (it, {})
+ fn, opt = it
+ n = len(signature(fn).parameters)
+ if n == 1:
+ setattr(cls, name, wrap_single_method(fn))
+ elif n == 2:
+ setattr(cls, name, wrap_double_method(fn, **opt))
+ return cls
+
+
+@wrap_maths
+class Fieldset:
+ """A set of Fields, each of which can come from different GRIB files"""
+
+ WRAP_MATHS_ATTRS = {
+ "abs": maths.abs,
+ "acos": maths.acos,
+ "asin": maths.asin,
+ "atan": maths.atan,
+ "atan2": maths.atan2,
+ "cos": maths.cos,
+ "div": maths.floor_div,
+ "exp": maths.exp,
+ "log": maths.log,
+ "log10": maths.log10,
+ "mod": maths.mod,
+ "sgn": maths.sgn,
+ "sin": maths.sin,
+ "square": maths.square,
+ "sqrt": maths.sqrt,
+ "tan": maths.tan,
+ "__neg__": maths.neg,
+ "__pos__": maths.pos,
+ "__invert__": maths.not_func,
+ "__add__": maths.add,
+ "__radd__": (maths.add, {"reverse_args": True}),
+ "__sub__": maths.sub,
+ "__rsub__": (maths.sub, {"reverse_args": True}),
+ "__mul__": maths.mul,
+ "__rmul__": (maths.mul, {"reverse_args": True}),
+ "__truediv__": maths.div,
+ "__rtruediv__": (maths.div, {"reverse_args": True}),
+ "__pow__": maths.pow,
+ "__rpow__": (maths.pow, {"reverse_args": True}),
+ "__ge__": maths.ge,
+ "__gt__": maths.gt,
+ "__le__": maths.le,
+ "__lt__": maths.lt,
+ "__eq__": maths.eq,
+ "__ne__": maths.ne,
+ "__and__": maths.and_func,
+ "__or__": maths.or_func,
+ "bitmap": (maths.bitmap, {"use_first_from_other": True}),
+ "nobitmap": maths.nobitmap,
+ }
+
+ # QUALIFIER_MAP = {"float": "d"}
+
+ # INT_KEYS = ["Nx", "Ny", "number"]
+
+ def __init__(
+ self, path=None, fields=None, keep_values_in_memory=False, temporary=False
+ ):
+ self.fields = []
+ self.count = 0
+ self.temporary = None
+ self._db = None
+
+ if (path is not None) and (fields is not None):
+ raise ValueError("Fieldset cannot take both path and fields")
+
+ if path:
+ if isinstance(path, list):
+ v = []
+ for p in path:
+ v.extend(utils.get_file_list(p))
+ path = v
+ else:
+ path = utils.get_file_list(path)
+
+ for p in path:
+ g = GribFile(p)
+ self.count = len(g)
+ for handle in g:
+ self.fields.append(Field(handle, p, keep_values_in_memory))
+ if temporary:
+ self.temporary = temp_file()
+ if fields:
+ self.fields = fields
+
+ def __len__(self):
+ return len(self.fields)
+
+ def __str__(self):
+ n = len(self)
+ s = "s" if n > 1 else ""
+ return f"Fieldset ({n} field{s})"
+
+ def _grib_get(self, *args, as_list=False, **kwargs):
+ ret = [x.grib_get(*args, **kwargs) for x in self.fields]
+ return ret if as_list else Fieldset._list_or_single(ret)
+
+ def grib_get_string(self, key):
+ return self._grib_get(key, key_type=CodesHandle.STRING)
+
+ def grib_get_long(self, key):
+ return self._grib_get(key, key_type=CodesHandle.LONG)
+
+ def grib_get_double(self, key):
+ return self._grib_get(key, key_type=CodesHandle.DOUBLE)
+
+ def grib_get_long_array(self, key):
+ return self._grib_get(key, key_type=CodesHandle.LONG_ARRAY)
+
+ def grib_get_double_array(self, key):
+ return self._grib_get(key, key_type=CodesHandle.DOUBLE_ARRAY)
+
+ def grib_get(self, keys, grouping="field"):
+ if grouping not in ["field", "key"]:
+ raise ValueError(f"grib_get: grouping must be field or key, not {grouping}")
+ ret = self._grib_get(keys, as_list=True)
+ if grouping == "key":
+ ret = list(map(list, zip(*ret))) # transpose lists of lists
+ return ret
+
+ def _grib_set(self, *args, **kwargs):
+ result = Fieldset(temporary=True)
+ path = result.temporary.path
+ with open(path, "wb") as fout:
+ for f in self.fields:
+ result._append_field(f.grib_set(*args, **kwargs))
+ result.fields[-1].write(fout, path, temp=result.temporary)
+ return result
+
+ def grib_set_string(self, keys_and_vals):
+ return self._grib_set(keys_and_vals, key_type=CodesHandle.STRING)
+
+ def grib_set_long(self, keys_and_vals):
+ return self._grib_set(keys_and_vals, key_type=CodesHandle.LONG)
+
+ def grib_set_double(self, keys_and_vals):
+ return self._grib_set(keys_and_vals, key_type=CodesHandle.DOUBLE)
+
+ def grib_set(self, keys_and_vals):
+ return self._grib_set(keys_and_vals)
+
+ # def grib_set_double_array(self, key, value):
+ # return self._grib_set_any(key, value, "grib_set_double_array")
+
+ def values(self):
+ v = [x.values() for x in self.fields]
+ return self._make_2d_array(v)
+
+ def set_values(self, values):
+ if isinstance(values, list):
+ list_of_arrays = values
+ else:
+ if len(values.shape) > 1:
+ list_of_arrays = [a for a in values]
+ else:
+ list_of_arrays = [values]
+ if len(list_of_arrays) != len(self.fields):
+ msg = str(len(list_of_arrays)) + " instead of " + str(len(self.fields))
+ raise ValueError("set_values has the wrong number of arrays: " + msg)
+
+ return self.fieldset_other_func(
+ maths.set_from_other, list_of_arrays, index_other=True
+ )
+
+ def write(self, path):
+ with open(path, "wb") as fout:
+ for f in self.fields:
+ f.write(fout, path)
+
+ def grib_index(self):
+ return [i.grib_index() for i in self.fields]
+
+ def _append_field(self, field):
+ self.fields.append(field)
+ self._db = None
+
+ def __getitem__(self, index):
+ try:
+ if isinstance(index, np.ndarray):
+ return Fieldset(fields=[self.fields[i] for i in index])
+ # # GRIB key
+ # elif isinstance(index, str):
+ # keyname = index
+ # qualifier = "n"
+ # parts = keyname.split(":")
+ # if len(parts) > 1:
+ # keyname = parts[0]
+ # qualifier = Fieldset.QUALIFIER_MAP[parts[1]]
+
+ # native_key = keyname + ":" + qualifier
+ # # print("native_key", native_key)
+ # value = self.grib_get([native_key])[0][0]
+ # # print(value)
+ # if value is None:
+ # raise IndexError
+ # if index in Fieldset.INT_KEYS:
+ # # print("int value:", int(value))
+ # return int(value)
+ # # if isinstance(value, float):
+ # # return int(value)
+ # return value
+ elif isinstance(index, str):
+ return self._get_db().select_with_name(index)
+ else:
+ return Fieldset(fields=self._always_list(self.fields[index]))
+ except IndexError as ide:
+ # print("This Fieldset contains", len(self), "fields; index is", index)
+ raise ide
+
+ def append(self, other):
+ self.fields = self.fields + other.fields
+ self._db = None
+
+ def merge(self, other):
+ result = Fieldset(fields=self.fields, temporary=True)
+ result.append(other)
+ return result
+
+ # def items(self):
+ # its = []
+ # for i in range(len(self)):
+ # its.append((i, Fieldset(fields=[self.fields[i]])))
+ # return its
+
+ # def to_dataset(self, via_file=True, **kwarg):
+ # # soft dependency on cfgrib
+ # try:
+ # import xarray as xr
+ # except ImportError: # pragma: no cover
+ # print("Package xarray not found. Try running 'pip install xarray'.")
+ # raise
+ # # dataset = xr.open_dataset(self.url(), engine="cfgrib", backend_kwargs=kwarg)
+ # if via_file:
+ # dataset = xr.open_dataset(self.url(), engine="cfgrib", backend_kwargs=kwarg)
+ # else:
+ # print("Using experimental cfgrib interface to go directly from Fieldset")
+ # dataset = xr.open_dataset(self, engine="cfgrib", backend_kwargs=kwarg)
+ # return dataset
+
+ def field_func(self, func):
+ """Applies a function to all values in all fields"""
+ result = Fieldset(temporary=True)
+ with open(result.temporary.path, "wb") as fout:
+ for f in self.fields:
+ result._append_field(f.field_func(func))
+ result.fields[-1].write(
+ fout, result.temporary.path, temp=result.temporary
+ )
+ return result
+
+ def fieldset_other_func(
+ self,
+ func,
+ other,
+ reverse_args=False,
+ index_other=False,
+ use_first_from_other=False,
+ ):
+ """Applies a function to a fieldset and a scalar/fieldset, e.g. F+5"""
+ # print(
+ # f"Fieldset.fieldset_other_func() func={func}, other={other}, reverse_args={reverse_args}, index_other={index_other}, use_first_from_other={use_first_from_other}"
+ # )
+
+ def _process_one(f, g, result):
+ new_field = f.field_other_func(func, g, reverse_args=reverse_args)
+ result._append_field(new_field)
+ result.fields[-1].write(fout, result.temporary.path, temp=result.temporary)
+
+ result = Fieldset(temporary=True)
+ with open(result.temporary.path, "wb") as fout:
+ if isinstance(other, Fieldset):
+ if len(other) == len(self.fields):
+ for f, g in zip(self.fields, other.fields):
+ _process_one(f, g, result)
+ elif use_first_from_other:
+ for f in self.fields:
+ _process_one(f, other.fields[0], result)
+ else:
+ raise Exception(
+ f"Fieldsets must have the same number of fields for this operation! {len(self.fields)} != {len(other)}"
+ )
+ elif index_other:
+ for f, g in zip(self.fields, other):
+ _process_one(f, g, result)
+ else:
+ for f in self.fields:
+ _process_one(f, other, result)
+ return result
+
+ def base_date(self):
+ if len(self.fields) > 0:
+ result = []
+ for f in self.fields:
+ md = f.grib_get(["dataDate", "dataTime"])
+ result.append(
+ utils.date_from_ecc_keys(md[0], md[1]) if len(md) == 2 else None
+ )
+ return Fieldset._list_or_single(result)
+
+ def valid_date(self):
+ if len(self.fields) > 0:
+ result = []
+ for f in self.fields:
+ md = f.grib_get(["validityDate", "validityTime"])
+ result.append(
+ utils.date_from_ecc_keys(md[0], md[1]) if len(md) == 2 else None
+ )
+ return Fieldset._list_or_single(result)
+
+ def accumulate(self):
+ if len(self.fields) > 0:
+ result = [np.nan] * len(self.fields)
+ for i, f in enumerate(self.fields):
+ result[i] = np.sum(f.values())
+ return Fieldset._list_or_single(result)
+
+ def average(self):
+ if len(self.fields) > 0:
+ result = [np.nan] * len(self.fields)
+ for i, f in enumerate(self.fields):
+ result[i] = f.values().mean()
+ return Fieldset._list_or_single(result)
+
+ def maxvalue(self):
+ if len(self.fields) > 0:
+ result = np.array([np.nan] * len(self.fields))
+ for i, f in enumerate(self.fields):
+ result[i] = f.values().max()
+ return result.max()
+
+ def minvalue(self):
+ if len(self.fields) > 0:
+ result = np.array([np.nan] * len(self.fields))
+ for i, f in enumerate(self.fields):
+ result[i] = f.values().min()
+ return result.min()
+
+ def _make_single_result(self, v):
+ assert len(self) > 0
+ result = Fieldset(temporary=True)
+ with open(result.temporary.path, "wb") as fout:
+ f = self.fields[0].clone()
+ f.encode_values(v)
+ result._append_field(f)
+ result.fields[-1].write(fout, result.temporary.path, temp=result.temporary)
+ return result
+
+ def mean(self):
+ if len(self.fields) > 0:
+ v = self.fields[0].values()
+ for i in range(1, len(self.fields)):
+ v += self.fields[i].values()
+ v = v / len(self.fields)
+ return self._make_single_result(v)
+ else:
+ return None
+
+ def rms(self):
+ if len(self.fields) > 0:
+ v = np.square(self.fields[0].values())
+ for i in range(1, len(self.fields)):
+ v += np.square(self.fields[i].values())
+ v = np.sqrt(v / len(self.fields))
+ return self._make_single_result(v)
+ else:
+ return None
+
+ def stdev(self):
+ v = self._compute_var()
+ return self._make_single_result(np.sqrt(v)) if v is not None else None
+
+ def sum(self):
+ if len(self.fields) > 0:
+ v = self.fields[0].values()
+ for i in range(1, len(self.fields)):
+ v += self.fields[i].values()
+ return self._make_single_result(v)
+ else:
+ return None
+
+ def var(self):
+ v = self._compute_var()
+ return self._make_single_result(v) if v is not None else None
+
+ def _compute_var(self):
+ if len(self.fields) > 0:
+ v2 = self.fields[0].values()
+ v1 = np.square(v2)
+ for i in range(1, len(self.fields)):
+ v = self.fields[i].values()
+ v1 += np.square(v)
+ v2 += v
+ return v1 / len(self.fields) - np.square(v2 / len(self.fields))
+
+ def latitudes(self):
+ v = [x.latitudes() for x in self.fields]
+ return Fieldset._make_2d_array(v)
+
+ def longitudes(self):
+ v = [x.longitudes() for x in self.fields]
+ return Fieldset._make_2d_array(v)
+
+ def _lat_func(self, func, bitmap_poles=False):
+ result = Fieldset(temporary=True)
+ pole_limit = 90.0 - 1e-06
+ with open(result.temporary.path, "wb") as fout:
+ for f in self.fields:
+ lat = f.latitudes()
+ if bitmap_poles:
+ lat[np.fabs(lat) > pole_limit] = np.nan
+ v = func(np.deg2rad(lat))
+ c = f.clone()
+ c.encode_values(v)
+ result._append_field(c)
+ result.fields[-1].write(
+ fout, result.temporary.path, temp=result.temporary
+ )
+ return result
+
+ def coslat(self):
+ return self._lat_func(np.cos)
+
+ def sinlat(self):
+ return self._lat_func(np.sin)
+
+ def tanlat(self):
+ return self._lat_func(np.tan, bitmap_poles=True)
+
+ @staticmethod
+ def _list_or_single(lst):
+ return lst if len(lst) != 1 else lst[0]
+
+ @staticmethod
+ def _always_list(items):
+ return items if isinstance(items, list) else [items]
+
+ @staticmethod
+ def _make_2d_array(v):
+ """Forms a 2D ndarray from a list of 1D ndarrays"""
+ v = Fieldset._list_or_single(v)
+ return np.stack(v, axis=0) if isinstance(v, list) else v
+
+ # TODO: add all the field_func functions
+
+ # TODO: add all field_field_func functions
+
+ # TODO: add all field_scalar_func functions
+
+ # TODO: add tests for all fieldset-fieldset methods:
+ # **, ==, !=, &, |
+
+ # TODO: allow these methods to be called as functions (?)
+
+ # TODO: function to write to single file if fields from different files
+
+ # TODO: optimise write() to copy file if exists
+
+ # TODO: to_dataset()
+
+ # TODO: pickling
+
+ # TODO: gribsetbits, default=24
+
+ def _get_db(self):
+ if self._db is None:
+ self._db = indexdb.FieldsetDb(fs=self)
+ assert self._db is not None
+ return self._db
+
+ def _unique_metadata(self, key):
+ return self._get_db().unique(key)
+
+ def select(self, *args, **kwargs):
+ if len(args) == 1 and isinstance(args[0], dict):
+ return self._get_db().select(**args[0])
+ else:
+ return self._get_db().select(**kwargs)
+
+ def describe(self, *args, **kwargs):
+ return self._get_db().describe(*args, **kwargs)
+
+ def ls(self, **kwargs):
+ return self._get_db().ls(**kwargs)
+
+ def sort(self, *args, **kwargs):
+ return self._get_db().sort(*args, **kwargs)
+
+ def deacc(self, **kwargs):
+ return utils.deacc(self, **kwargs)
+
+ def speed(self, *args):
+ if len(args) == 0:
+ u = self[0::2]
+ v = self[1::2]
+ if len(u) != len(v):
+ raise Exception(
+ f"Fieldsets must contain an even number of fields for this operation! len={len(self.fields)} is not even!"
+ )
+ return u.speed(v)
+ elif len(args) == 1:
+ other = args[0]
+ result = Fieldset(temporary=True)
+ param_ids = {
+ 131: 10, # atmospheric wind
+ 165: 207, # 10m wind
+ 228246: 228249, # 100m wind
+ 228239: 228241, # 200m wind
+ }
+ if len(self.fields) != len(other):
+ raise Exception(
+ f"Fieldsets must have the same number of fields for this operation! {len(self.fields)} != {len(other)}"
+ )
+ with open(result.temporary.path, "wb") as fout:
+ for f, g in zip(self.fields, other.fields):
+ sp = np.sqrt(np.square(f.values()) + np.square(g.values()))
+ c = f.clone()
+ c.encode_values(sp)
+ param_id_u = f.grib_get("paramId", CodesHandle.LONG)
+ param_id_sp = param_ids.get(param_id_u, None)
+ if param_id_sp is not None:
+ c = c.grib_set(["paramId", param_id_sp], CodesHandle.LONG)
+ result._append_field(c)
+ result.fields[-1].write(
+ fout, result.temporary.path, temp=result.temporary
+ )
+ return result
+
+
+class FieldsetCF:
+ def __init__(self, fs):
+ self.fs = fs
+
+ def items(self):
+ its = []
+ for i in range(len(self.fs)):
+ its.append((i, FieldCF(self.fs[i])))
+ # print("FieldsetCF items: ", d)
+ return its
+
+ def __getitem__(self, index):
+ return FieldCF(self.fs[index])
+
+
+class FieldCF:
+
+ QUALIFIER_MAP = {"float": "d"}
+
+ INT_KEYS = ["Nx", "Ny", "number"]
+
+ def __init__(self, fs):
+ self.fs = fs
+
+ def __getitem__(self, key):
+ keyname = key
+ qualifier = "n"
+ parts = key.split(":")
+ if len(parts) > 1:
+ keyname = parts[0]
+ qualifier = FieldCF.QUALIFIER_MAP[parts[1]]
+
+ native_key = keyname + ":" + qualifier
+ # print("native_key", native_key)
+ value = self.fs.grib_get([native_key])[0][0]
+ # print(value)
+ if value is None:
+ raise IndexError
+ if key in FieldCF.INT_KEYS:
+ # print("int value:", int(value))
+ return int(value)
+ # if isinstance(value, float):
+ # return int(value)
+ return value
+
+
+def read(p):
+ return Fieldset(path=p)
+
+
+# expose all Fieldset functions as a module level function
+def _make_module_func(name):
+ def wrapped(fs, *args):
+ return getattr(fs, name)(*args)
+
+ return wrapped
+
+
+module_obj = sys.modules[__name__]
+for fn in dir(Fieldset):
+ if callable(getattr(Fieldset, fn)) and not fn.startswith("_"):
+ setattr(module_obj, fn, _make_module_func(fn))
+
+
+def bind_functions(namespace, module_name=None):
+ """Add to the module globals all metview functions except operators like: +, &, etc."""
+ namespace["read"] = read
+ for fn in dir(Fieldset):
+ if callable(getattr(Fieldset, fn)) and not fn.startswith("_"):
+ namespace[fn] = _make_module_func(fn)
+ namespace["Fieldset"] = Fieldset
diff --git a/metview/metviewpy/indexdb.py b/metview/metviewpy/indexdb.py
new file mode 100644
index 00000000..860102dc
--- /dev/null
+++ b/metview/metviewpy/indexdb.py
@@ -0,0 +1,581 @@
+# (C) Copyright 2017- ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation
+# nor does it submit to any jurisdiction.
+#
+
+import copy
+import logging
+import os
+
+import pandas as pd
+
+from .indexer import GribIndexer, FieldsetIndexer
+from .param import (
+ ParamInfo,
+ ParamNameDesc,
+ ParamIdDesc,
+ init_pandas_options,
+)
+from .ipython import is_ipython_active
+
+
+# logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
+# logging.basicConfig(level=logging.DEBUG, format="%(levelname)s - %(message)s")
+LOG = logging.getLogger(__name__)
+
+
+class IndexDb:
+ ROOTDIR_PLACEHOLDER_TOKEN = "__ROOTDIR__"
+
+ def __init__(
+ self,
+ name,
+ label="",
+ desc="",
+ path="",
+ rootdir_placeholder_value="",
+ file_name_pattern="",
+ db_dir="",
+ blocks=None,
+ data_files=None,
+ merge_conf=None,
+ mapped_params=None,
+ regrid_from=None,
+ dataset=None,
+ ):
+ self.name = name
+ self.dataset = dataset
+ self.label = label
+ if self.label is None or self.label == "":
+ self.label = self.name
+ self.desc = desc
+ self.path = path
+
+ self.rootdir_placeholder_value = rootdir_placeholder_value
+ self.file_name_pattern = file_name_pattern
+ if self.file_name_pattern == "":
+ self.path = os.path.dirname(self.path)
+ self.file_name_pattern = os.path.basename(self.path)
+
+ self.db_dir = db_dir
+ self.mapped_params = {} if mapped_params is None else mapped_params
+ self.regrid_from = [] if regrid_from is None else regrid_from
+ self.blocks = {} if blocks is None else blocks
+ self.vector_loaded = False
+ self._param_types = {}
+ self.data_files = [] if data_files is None else data_files
+ self.merge_conf = [] if merge_conf is None else merge_conf
+ self._params = {}
+
+ def select_with_name(self, name):
+ """
+ Perform a select operation where selection options are derived
+ from the specified name.
+ """
+ # print(f"select_with_name blocks: {self.blocks.keys()}")
+ # print(f"vector_loaded: {self.vector_loaded}")
+
+ if "wind" in name and not self.vector_loaded:
+ self.load(vector=True)
+ pnf = ParamInfo.build_from_name(name, param_level_types=self.param_types)
+ if pnf is not None:
+ fs = self._select_fs(
+ **pnf.make_filter(), _named_vector_param=(not pnf.scalar)
+ )
+ if fs is not None:
+ pnf.update_meta(fs._db._first_index_row())
+ fs._ds_param_info = pnf
+ return fs
+ return None
+
+ def select(self, **kwargs):
+ return self._select_fs(**kwargs)
+
+ def _select_fs(self, **kwargs):
+ """
+ Create a fieldset with the specified filter conditions. The resulting fieldset
+ will contain an index db.
+ """
+ LOG.debug(f"kwargs={kwargs}")
+
+ vector = kwargs.pop("_named_vector_param", False)
+ max_count = kwargs.pop("_max_count", -1)
+
+ # print(f"kwargs={kwargs}")
+ # LOG.debug(f"blocks={self.blocks}")
+ dims = self._make_dims(kwargs)
+
+ # We can only have a vector param when the fs["wind"]-like operator is
+ # invoked and we deduce the shortName from the specified name
+ if vector:
+ short_name = dims.get("shortName", [])
+ if isinstance(short_name, list):
+ assert len(short_name) == 1
+
+ # print(f"dims={dims}")
+ self.load(keys=list(dims.keys()), vector=vector)
+
+ db, fs = self._get_fields(dims, max_count=max_count, vector=vector)
+ fs._db = db
+ # LOG.debug(f"fs={fs}")
+ # print(f"blocks={fs._db.blocks}")
+ return fs
+
+ def _get_fields(self, dims, max_count=-1, vector=False):
+
+ res = self.fieldset_class()
+ dfs = {}
+ LOG.debug(f"dims={dims}")
+
+ name_filter = "shortName" in dims or "paramId" in dims
+ if not vector and name_filter:
+ # in this case filtering can only be done on the scalar block
+ if "scalar" in self.blocks.keys():
+ self._get_fields_for_block("scalar", dims, dfs, res, max_count)
+ else:
+ for key in self.blocks.keys():
+ self._get_fields_for_block(key, dims, dfs, res, max_count)
+ if max_count != -1 and len(res) >= max_count:
+ break
+
+ # LOG.debug(f"len_res={len(res)}")
+ # LOG.debug(f"dfs={dfs}")
+ # LOG.debug(f"res={res}")
+ c = FieldsetDb(
+ res,
+ name=self.name,
+ blocks=dfs,
+ label=self.label,
+ mapped_params=self.mapped_params,
+ regrid_from=self.regrid_from,
+ )
+ return c, res
+
+ def _get_meta(self, dims):
+ LOG.debug(f"dims={dims}")
+ key = "scalar"
+ if key in self.blocks:
+ if self.blocks[key] is None:
+ self._load_block(key)
+ df = self._filter_df(df=self.blocks[key], dims=dims)
+ # LOG.debug(f"df={df}")
+ return df
+ return None
+
+ def _build_query(self, dims, df):
+ q = ""
+ for column, v in dims.items():
+ # print(f"v={v}")
+ if v:
+ col_type = None
+ if q:
+ q += " and "
+
+ # datetime columns
+ if column in GribIndexer.DATETIME_KEYS:
+ name_date = GribIndexer.DATETIME_KEYS[column][0]
+ name_time = GribIndexer.DATETIME_KEYS[column][1]
+ # here we should simply say: name_date*10000 + name_time. However,
+ # pandas cannot handle it in the query because the column types are
+ # Int64. np.int64 would work but that cannot handle missing values. So
+ # we need to break down the condition into individual logical components!
+ s = []
+ for x in v:
+ # print(f"x={x}")
+ # print(" date=" + x.strftime("%Y%m%d"))
+ # print(" time=" + x.strftime("%H%M"))
+ s.append(
+ f"(`{name_date}` == "
+ + str(int(x.strftime("%Y%m%d")))
+ + " and "
+ + f"`{name_time}` == "
+ + str(int(x.strftime("%H%M")))
+ + ")"
+ )
+ q += "(" + " or ".join(s) + " ) "
+ else:
+ col_type = df.dtypes[column]
+ column = f"`{column}`"
+ if not isinstance(v, list):
+ q += f"{column} == {GribIndexer._convert_query_value(v, col_type)}"
+ else:
+ v = [GribIndexer._convert_query_value(x, col_type) for x in v]
+ q += f"{column} in {v}"
+ return q
+
+ def _filter_df(self, df=None, dims={}):
+ if len(dims) == 0:
+ return df
+ else:
+ df_res = None
+ if df is not None:
+ # print("types={}".format(df.dtypes))
+ q = self._build_query(dims, df)
+ # print("query={}".format(q))
+ if q != "":
+ df_res = df.query(q, engine="python")
+ df_res.reset_index(drop=True, inplace=True)
+ # print(f"df_res={df_res}")
+ # LOG.debug(f"df_res={df_res}")
+ else:
+ return df
+ return df_res
+
+ def _get_fields_for_block(self, key, dims, dfs, res, max_count):
+ # print(f"key={key} dims={dims}")
+ # LOG.debug(f"block={self.blocks[key]}")
+ if self.blocks[key] is None:
+ self._load_block(key)
+ df = self._filter_df(df=self.blocks[key], dims=dims)
+ # print(f"df={df}")
+ # LOG.debug(f"df={df}")
+ if df is not None and not df.empty:
+ df_fs = self._extract_fields(df, res, max_count)
+ # LOG.debug(f"len_res={len(res)}")
+ if df_fs is not None:
+ # LOG.debug(f"df_fs={df_fs}")
+ dfs[key] = df_fs
+
+ def _make_param_info(self):
+ m = self._first_index_row()
+ if m:
+ name = m["shortName"]
+ pnf = ParamInfo(
+ name, meta=dict(m), scalar=not name in ParamInfo.VECTOR_NAMES
+ )
+ return pnf
+ return None
+
+ def _first_index_row(self):
+ if self.blocks:
+ df = self.blocks[list(self.blocks.keys())[0]]
+ if df is not None and not df.empty:
+ row = df.iloc[0]
+ return dict(row)
+ return {}
+
+ def _make_dims(self, options):
+ dims = {}
+ GribIndexer._check_datetime_in_filter_input(options)
+ for k, v in options.items():
+ name = str(k)
+ vv = copy.deepcopy(v)
+ r = GribIndexer._convert_filter_value(name, self._to_list(vv))
+ for name, vv in r:
+ if len(r) > 1 or vv:
+ dims[name] = vv
+ return dims
+
+ def _to_list(self, v):
+ if not isinstance(v, list):
+ v = [v]
+ return v
+
+ @property
+ def param_types(self):
+ if len(self._param_types) == 0:
+ self.load()
+ for k, df in self.blocks.items():
+ df_u = df[["shortName", "typeOfLevel"]].drop_duplicates()
+ for row in df_u.itertuples(name=None):
+ if not row[1] in self._param_types:
+ self._param_types[row[1]] = [row[2]]
+ else:
+ self._param_types[row[1]].append(row[2])
+ # print(self._param_types)
+ return self._param_types
+
+ def unique(self, key):
+ self.load([key])
+ for _, v in self.blocks.items():
+ if key in v.columns:
+ return list(v[key].unique())
+ return []
+
+ @property
+ def param_meta(self):
+ if len(self._params) == 0:
+ self.load()
+ for par in sorted(self.unique("shortName")):
+ self._params[par] = ParamNameDesc(par)
+ self._params[par].load(self)
+ return self._params
+
+ def param_id_meta(self, param_id):
+ self.load()
+ p = ParamIdDesc(param_id)
+ p.load(self)
+ return p
+
+ def describe(self, *args, **kwargs):
+ param = args[0] if len(args) == 1 else None
+ if param is None:
+ param = kwargs.pop("param", None)
+ return ParamNameDesc.describe(self, param=param, **kwargs)
+
+ def to_df(self):
+ return pd.concat([p for _, p in self.blocks.items()])
+
+ def __str__(self):
+ return "{}[name={}]".format(self.__class__.__name__, self.name)
+
+
+class FieldsetDb(IndexDb):
+ def __init__(self, fs, name="", **kwargs):
+ super().__init__(name, **kwargs)
+ self.fs = fs
+ self.fieldset_class = fs.__class__
+ self._indexer = None
+
+ @property
+ def indexer(self):
+ if self._indexer is None:
+ self._indexer = FieldsetIndexer(self)
+ return self._indexer
+
+ def scan(self, vector=False):
+ self.indexer.scan(vector=vector)
+ self.vector_loaded = vector
+
+ def load(self, keys=[], vector=False):
+ # print(f"blocks={self.blocks}")
+ if self.indexer.update_keys(keys):
+ self.blocks = {}
+ self._param_types = {}
+ self.scan(vector=self.vector_loaded)
+ elif not self.blocks:
+ self._param_types = {}
+ self.scan(vector=vector)
+ self.vector_loaded = vector
+ elif vector and not self.vector_loaded:
+ self._param_types = {}
+ self.indexer._scan_vector()
+ self.vector_loaded = True
+
+ def _extract_fields(self, df, fs, max_count):
+ if df.empty:
+ return None
+
+ # print(f"cols={df.columns}")
+ if "_msgIndex3" in df.columns:
+ comp_num = 3
+ elif "_msgIndex2" in df.columns:
+ comp_num = 2
+ elif "_msgIndex1" in df.columns:
+ comp_num = 1
+ else:
+ return None
+
+ # print(f"comp_num={comp_num}")
+ idx = [[] for k in range(comp_num)]
+ comp_lst = list(range(comp_num))
+ cnt = 0
+ for row in df.itertuples():
+ # print(f"{row}")
+ if max_count == -1 or len(fs) < max_count:
+ for comp in comp_lst:
+ fs.append(self.fs[row[-1 - (comp_num - comp - 1)]])
+ idx[comp].append(len(fs) - 1)
+ cnt += 1
+ else:
+ break
+
+ # generate a new dataframe
+ if max_count == -1 or cnt == df.shape[0]:
+ df = df.copy()
+ else:
+ df = df.head(cnt).copy()
+
+ for k, v in enumerate(idx):
+ df[f"_msgIndex{k+1}"] = v
+ return df
+
+ def _extract_scalar_fields(self, df):
+ if df.empty:
+ return None, None
+
+ assert "_msgIndex1" in df.columns
+ assert "_msgIndex2" not in df.columns
+ assert "_msgIndex3" not in df.columns
+
+ fs = self.fieldset_class()
+ for row in df.itertuples():
+ fs.append(self.fs[row[-1]])
+
+ assert len(fs) == len(df.index)
+ # generate a new dataframe
+ df = df.copy()
+ df["_msgIndex1"] = list(range(len(df.index)))
+ return df, fs
+
+ def _clone(self):
+ db = FieldsetDb(self.name, label=self.label, regrid_from=self.regrid_from)
+
+ if self._indexer is not None:
+ db.indexer.update_keys(self._indexer.keys_ecc)
+ db.blocks = {k: v.copy() for k, v in self.blocks.items()}
+ db.vector_loaded = self.vector_loaded
+ return db
+
+ @staticmethod
+ def make_param_info(fs):
+ if fs._db is not None:
+ return fs._db._make_param_info()
+ else:
+ return ParamInfo.build_from_fieldset(fs)
+
+ def ls(self, extra_keys=None, filter=None, no_print=False):
+ default_keys = [
+ "centre",
+ "shortName",
+ "typeOfLevel",
+ "level",
+ "dataDate",
+ "dataTime",
+ "stepRange",
+ "dataType",
+ "number",
+ "gridType",
+ ]
+ ls_keys = default_keys
+ extra_keys = [] if extra_keys is None else extra_keys
+ if extra_keys is not None:
+ [ls_keys.append(x) for x in extra_keys if x not in ls_keys]
+ keys = list(ls_keys)
+
+ # add keys appearing in the filter to the full list of keys
+ dims = {} if filter is None else filter
+ dims = self._make_dims(dims)
+ [keys.append(k) for k, v in dims.items() if k not in keys]
+
+ # get metadata
+ self.load(keys=keys, vector=False)
+
+ # performs filter
+ df = self._get_meta(dims)
+
+ # extract results
+ keys = list(ls_keys)
+ keys.append("_msgIndex1")
+ df = df[keys]
+ df = df.sort_values("_msgIndex1")
+ df = df.rename(columns={"_msgIndex1": "Message"})
+ df = df.set_index("Message")
+
+ # only show the column for number in the default set of keys if
+ # there are any valid values in it
+ if "number" not in extra_keys:
+ r = df["number"].unique()
+ skip = False
+ if len(r) == 1:
+ skip = r[0] in ["0", None]
+ if skip:
+ df.drop("number", axis=1, inplace=True)
+
+ init_pandas_options()
+
+ # test whether we're in the Jupyter environment
+ if is_ipython_active():
+ return df
+ elif not no_print:
+ print(df)
+ return df
+
+ def sort(self, *args, **kwargs):
+ # handle arguments
+ keys = []
+ asc = None
+ if len(args) >= 1:
+ keys = args[0]
+ if not isinstance(keys, list):
+ keys = [keys]
+ # optional positional argument - we only implement it to provide
+ # backward compability for the sort() Macro function
+ if len(args) == 2:
+ asc = args[1]
+ if isinstance(asc, list):
+ if len(keys) != len(asc):
+ raise ValueError(
+ f"sort(): when order is specified as a list it must have the same number of elements as keys! {len(keys)} != {len(asc)}"
+ )
+ for i, v in enumerate(asc):
+ if v not in [">", "<"]:
+ raise ValueError(
+ f"sort(): invalid value={v} in order! Only "
+ > " and "
+ < " are allowed!"
+ )
+ asc[i] = True if v == "<" else False
+ else:
+ if asc not in [">", "<"]:
+ raise ValueError(
+ f"sort(): invalid value={asc} in order! Only "
+ > " and "
+ < " are allowed!"
+ )
+ asc = True if asc == "<" else False
+
+ if "ascending" in kwargs:
+ if asc is not None:
+ raise ValueError(
+ "sort(): cannot take both a second positional argument and the ascending keyword argument!"
+ )
+ asc = kwargs.pop("ascending")
+
+ if asc is None:
+ asc = True
+
+ if len(keys) == 0:
+ keys = self.indexer.DEFAULT_SORT_KEYS
+
+ # print(f"keys={keys} asc={asc}")
+
+ # get metadata
+ self.load(keys=keys, vector=False)
+
+ scalar_df = self.blocks.get("scalar")
+ if scalar_df is not None:
+ dfs = self.indexer._sort_dataframe(scalar_df, columns=keys, ascending=asc)
+ # print(f"dfs={dfs.iloc[0:5]}")
+ # print(dfs)
+
+ df, res = self._extract_scalar_fields(dfs)
+ # print(f"df={df.iloc[0:5]}")
+
+ # LOG.debug(f"len_res={len(res)}")
+ # LOG.debug(f"dfs={dfs}")
+ # LOG.debug(f"res={res}")
+ c = FieldsetDb(
+ res,
+ name=self.name,
+ blocks={"scalar": df},
+ label=self.label,
+ mapped_params=self.mapped_params,
+ regrid_from=self.regrid_from,
+ )
+
+ res._db = c
+ return res
+
+ def get_longname_and_units(self, short_name, param_id):
+ # The name and units keys are not included in the default set of keys for the
+ # indexer. When we need them (primarily in ParamDesc) we simply get the first
+ # grib message and extract them from it.
+ a = {}
+ if short_name:
+ a["shortName"] = short_name
+ if param_id:
+ a["paramId"] = param_id
+ if a:
+ a["_max_count"] = 1
+ r = self.select(**a)
+ if r is not None and len(r) > 0:
+ md = r[0].grib_get(["name", "units"])
+ if md and len(md[0]) == 2:
+ return md[0][0], md[0][1]
+ return "", ""
diff --git a/metview/metviewpy/indexer.py b/metview/metviewpy/indexer.py
new file mode 100644
index 00000000..e94ed72a
--- /dev/null
+++ b/metview/metviewpy/indexer.py
@@ -0,0 +1,655 @@
+#
+# (C) Copyright 2017- ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation
+# nor does it submit to any jurisdiction.
+#
+
+import copy
+import datetime
+import logging
+import os
+from pathlib import Path
+
+from . import utils
+import numpy as np
+import pandas as pd
+import yaml
+
+# logging.basicConfig(level=logging.DEBUG)
+# logging.basicConfig(level=logging.INFO)
+LOG = logging.getLogger(__name__)
+
+NEWER = True
+
+
+class GribIndexer:
+ VECTOR_PARAMS = {
+ "wind10m": ["10u", "10v"],
+ "wind100m": ["100u", "100v"],
+ "wind200m": ["200u", "200v"],
+ "wind": ["u", "v"],
+ "wind3d": ["u", "v", "w"],
+ }
+
+ # tuple-> 0: ecCodes type, 1: pandas type, 2: Python type 3: use in duplicate check
+ DEFAULT_KEYS = {
+ "shortName": ("s", str, str, False),
+ "paramId": ("l", "Int32", int, False),
+ "date": ("l", "Int64", int, True),
+ "time": ("l", "Int64", int, True),
+ "step": ("l", "Int32", int, True),
+ "level": ("l", "Int32", int, True),
+ "typeOfLevel": ("s", str, str, False),
+ "number": ("s", str, str, True),
+ "experimentVersionNumber": ("s", str, str, False),
+ "marsClass": ("s", str, str, False),
+ "marsStream": ("s", str, str, False),
+ "marsType": ("s", str, str, False),
+ }
+
+ DEFAULT_ECC_KEYS = [f"{k}:{v[0]}" for k, v in DEFAULT_KEYS.items()]
+ BLOCK_KEYS = ["shortName", "typeOfLevel"]
+
+ DEFAULT_SORT_KEYS = [
+ "date",
+ "time",
+ "step",
+ "number",
+ "level",
+ "paramId",
+ ]
+ DATE_KEYS = {
+ k: ("l", "Int64", int)
+ for k in ["date", "dataDate", "validityDate", "mars.date", "marsDate"]
+ }
+ TIME_KEYS = {
+ k: ("l", "Int64", int)
+ for k in ["time", "dataTime", "validityTime", "mars.time", "marsTime"]
+ }
+
+ DATETIME_KEYS = {
+ "_dateTime": ("date", "time"),
+ "_dataDateTime": ("dataDate", "dataTime"),
+ "_validityDateTime": ("validityDate", "validityTime"),
+ }
+
+ KEYS_TO_REPLACE = {
+ ("type", "mars.type"): "marsType",
+ ("stream", "mars.stream"): "marsStream",
+ ("class", "mars.class", "class_"): "marsClass",
+ ("perturbationNumber"): "number",
+ ("mars.date", "marsDate"): "date",
+ ("mars.time", "marsTime"): "time",
+ }
+
+ PREDEF_KEYS = copy.deepcopy(DEFAULT_KEYS)
+ PREDEF_KEYS.update(DATE_KEYS)
+ PREDEF_KEYS.update(TIME_KEYS)
+
+ PREDEF_PD_TYPES = {k: v[1] for k, v in PREDEF_KEYS.items()}
+ PREDEF_PT_TYPES = {k: v[2] for k, v in PREDEF_KEYS.items()}
+
+ def __init__(self, db):
+ self.db = db
+ assert self.db is not None
+ self.ref_column_count = None
+ self.keys = []
+ self.keys_ecc = []
+ for k, v in GribIndexer.DEFAULT_KEYS.items():
+ name = k
+ self.keys.append(name)
+ if v[0]:
+ name = f"{k}:{v[0]}"
+ self.keys_ecc.append(name)
+
+ self.keys_duplicate_check = [
+ k for k, v in GribIndexer.DEFAULT_KEYS.items() if v[3] == True
+ ]
+
+ self.shortname_index = self.keys.index("shortName")
+ self.levtype_index = self.keys.index("typeOfLevel")
+ self.type_index = self.keys.index("marsType")
+ self.number_index = self.keys.index("number")
+ self.param_id_index = self.keys.index("paramId")
+
+ self.wind_check_index = []
+ for v in [
+ "date",
+ "time",
+ "step",
+ "level",
+ "typeOfLevel",
+ "level",
+ "number",
+ "experimentVersionNumber",
+ "marsClass",
+ "marsStream",
+ "marsType",
+ ]:
+ self.wind_check_index.append(self.keys.index(v) + 1)
+
+ # self.block_key_index = [self.keys.index(v) for v in GribIndexer.BLOCK_KEYS]
+
+ self.pd_types = {k: v[1] for k, v in GribIndexer.DEFAULT_KEYS.items()}
+ self.pt_types = {k: v[2] for k, v in GribIndexer.DEFAULT_KEYS.items()}
+
+ def update_keys(self, keys):
+ ret = False
+ for k in keys:
+ name = k
+ # we do not add datetime keys (they are pseudo keys, and their value
+ # is always generated on the fly)
+ if name not in self.keys and name not in GribIndexer.DATETIME_KEYS:
+ self.keys.append(name)
+ p = GribIndexer.PREDEF_KEYS.get(name, ("", str, str))
+ ecc_name = name if p[0] == "" else name + ":" + p[0]
+ self.keys_ecc.append(ecc_name)
+ self.pd_types[name] = p[1]
+ self.pt_types[name] = p[2]
+ ret = True
+ return ret
+
+ def _check_duplicates(self, name, df):
+ dup = df.duplicated(subset=self.keys_duplicate_check)
+ first_dup = True
+ cnt = 0
+ for i, v in dup.items():
+ if v:
+ if first_dup:
+ LOG.error(
+ f"{name}: has duplicates for key group: {self.keys_duplicate_check}!"
+ )
+ first_dup = False
+ LOG.error(f" first duplicate: {df.iloc[i]}")
+ cnt += 1
+
+ if cnt > 1:
+ LOG.error(f" + {cnt-1} more duplicate(s)!")
+
+ def _build_vector_index(self, df, v_name, v_comp):
+ # LOG.debug(f"v_name={v_name} v_comp={v_comp}")
+ comp_num = len(v_comp)
+
+ # filter components belonging together
+ comp_df = []
+ for i, comp_name in enumerate(v_comp):
+ query = f"shortName == '{comp_name}'"
+ r = df.query(query, engine="python")
+ # if we do not use copy, the assignment below as:
+ # comp_df[0].loc[...
+ # generates the SettingWithCopyWarning warning!!!
+ if i == 0:
+ r = df.query(query, engine="python").copy()
+ else:
+ r = df.query(query, engine="python")
+ if r.empty:
+ return []
+ else:
+ comp_df.append(r)
+
+ assert comp_num == len(comp_df)
+
+ # pair up components within a 2D vector field. This
+ # version proved to be the fastest!
+ # LOG.debug(" pair up collected components:")
+
+ # print(f"v_name={v_name} {len(comp_df[1].index)}")
+ # print("view=", comp_df[0]._is_view)
+ r = []
+ used1 = np.full(len(comp_df[1].index), False, dtype="?")
+ comp_df[0].loc[:, "shortName"] = v_name
+ # 2D
+ if comp_num == 2:
+ for row0 in comp_df[0].itertuples(name=None):
+ i = 0
+ for row1 in comp_df[1].itertuples(name=None):
+ if not used1[i]:
+ b = True
+ for x in self.wind_check_index:
+ if row0[x] != row1[x]:
+ b = False
+ break
+ if b:
+ d = list(row0[1:])
+ d.extend(row1[-self.ref_column_count :])
+ r.append(d)
+ used1[i] = True
+ break
+ i += 1
+ # 3D
+ elif comp_num == 3:
+ used2 = np.full(len(comp_df[2].index), False, dtype="?")
+ for row0 in comp_df[0].itertuples(name=None):
+ i = 0
+ for row1 in comp_df[1].itertuples(name=None):
+ if not used1[i]:
+ b = True
+ for x in self.wind_check_index:
+ if row0[x] != row1[x]:
+ b = False
+ break
+ if b:
+ j = 0
+ for row2 in comp_df[2].itertuples(name=None):
+ if not used2[j]:
+ b = True
+ for x in self.wind_check_index:
+ if row0[x] != row2[x]:
+ b = False
+ break
+ if b:
+ d = list(row0[1:])
+ d.extend(row1[-self.ref_column_count :])
+ d.extend(row2[-self.ref_column_count :])
+ r.append(d)
+ used1[i] = True
+ used2[j] = True
+ j = -1
+ break
+ j += 1
+ if j == -1:
+ break
+ i += 1
+ return r
+
+ def _make_dataframe(self, data, sort=False, columns=None):
+ if columns is not None:
+ df = pd.DataFrame(data, columns=columns)
+ else:
+ df = pd.DataFrame(data)
+
+ for c in df.columns:
+ if self.pd_types.get(c, "") in ["Int32", "Int64"]:
+ df.fillna(value={c: np.nan}, inplace=True)
+ df = df.astype(self.pd_types)
+ if sort:
+ df = GribIndexer._sort_dataframe(df)
+
+ return df
+
+ @staticmethod
+ def _sort_dataframe(df, columns=None, ascending=True):
+ if columns is None:
+ columns = list(df.columns)
+ elif not isinstance(columns, list):
+ columns = [columns]
+
+ # mergesoft is a stable sorting algorithm
+ df = df.sort_values(by=columns, ascending=ascending, kind="mergesort")
+ df = df.reset_index(drop=True)
+ return df
+
+ def _write_dataframe(self, df, name, out_dir):
+ f_name = os.path.join(out_dir, f"{name}.csv.gz")
+ df.to_csv(path_or_buf=f_name, header=True, index=False, compression="gzip")
+
+ @staticmethod
+ def read_dataframe(key, dir_name):
+ # assert len(key) == len(GribIndexer.BLOCK_KEYS)
+ name = key
+ f_name = os.path.join(dir_name, f"{name}.csv.gz")
+ # LOG.debug("f_name={}".format(f_name))
+ return pd.read_csv(f_name, index_col=None, dtype=GribIndexer.PREDEF_PD_TYPES)
+
+ @staticmethod
+ def get_storage_key_list(dir_name):
+ r = []
+ # LOG.debug(f"dir_name={dir_name}")
+ suffix = ".csv.gz"
+ for f in utils.get_file_list(os.path.join(dir_name, f"*{suffix}")):
+ name = os.path.basename(f)
+ # LOG.debug(f"name={name}")
+ r.append(name[: -len(suffix)])
+ return r
+
+ @staticmethod
+ def is_key_wind(key):
+ return key in GribIndexer.VECTOR_PARAMS
+
+ @staticmethod
+ def _convert_query_value(v, col_type):
+ # print(f"v={v} {type(v)} {col_type}")
+ return v if col_type != "object" else str(v)
+
+ @staticmethod
+ def _check_datetime_in_filter_input(keys):
+ for k, v in GribIndexer.DATETIME_KEYS.items():
+ name = k[1:]
+ name_date = v[0]
+ name_time = v[1]
+ if keys.get(name, []) and (
+ keys.get(name_date, []) or keys.get(name_time, [])
+ ):
+ raise Exception(
+ f"Cannot specify {name} together with {name_date} and {name_time}!"
+ )
+
+ @staticmethod
+ def _convert_filter_value(name, val):
+ """
+ Analyse the filter key-value pairs and perform the necessary conversions
+ """
+ valid_name = name.split(":")[0] if ":" in name else name
+
+ # datetime keys are pseudo keys, they start with _. Their value is converted to
+ # datetime. The key itself is not added to the scan!
+ if ("_" + valid_name) in GribIndexer.DATETIME_KEYS:
+ valid_name = "_" + valid_name
+ name_date = GribIndexer.DATETIME_KEYS[valid_name][0]
+ name_time = GribIndexer.DATETIME_KEYS[valid_name][1]
+ for i, t in enumerate(val):
+ val[i] = GribIndexer._to_datetime(name, t)
+ # print(f"t={t} -> {val[i]}")
+ # We add the date and time components with an empty value. So they will be
+ # added to the scan, but they will be ignored by the query. Conversely,
+ # the datetime key itself will be ignored in the scan, but will be used
+ # in the query.
+ return [("_" + name, val), (name_date, []), (name_time, [])]
+ # we convert dates to int
+ elif valid_name in GribIndexer.DATE_KEYS:
+ for i, t in enumerate(val):
+ d = GribIndexer._to_date(name, t)
+ # for daily climatologies dates where the year is missing the
+ # the a tuple is returned
+ if not isinstance(d, tuple):
+ val[i] = int(d.strftime("%Y%m%d"))
+ else:
+ val[i] = d[0] * 100 + d[1]
+ # we convert times to int
+ elif valid_name in GribIndexer.TIME_KEYS:
+ for i, t in enumerate(val):
+ val[i] = int(GribIndexer._to_time(name, t).strftime("%H%M"))
+ # print(f"t={t} -> {val[i]}")
+ else:
+ pt_type = GribIndexer.PREDEF_PT_TYPES.get(name, None)
+ # print(f"name={name} {pt_type}")
+ if pt_type is not None:
+ for i, t in enumerate(val):
+ val[i] = pt_type(t)
+ # print(f" t={t} -> {val[i]}")
+
+ # remap some names to the ones already in the default set of indexer keys
+ for k, v in GribIndexer.KEYS_TO_REPLACE.items():
+ if name in k:
+ name = v
+
+ return [(name, val)]
+
+ @staticmethod
+ def _to_datetime(param, val):
+ try:
+ if isinstance(val, datetime.datetime):
+ return val
+ elif isinstance(val, str):
+ return utils.date_from_str(val)
+ elif isinstance(val, (int, float)):
+ return utils.date_from_str(str(val))
+ else:
+ raise
+ except:
+ raise Exception(f"Invalid datetime value={val} specified for key={param}")
+
+ @staticmethod
+ def _to_date(param, val):
+ try:
+ if isinstance(val, datetime.datetime):
+ return val.date()
+ elif isinstance(val, datetime.date):
+ return val
+ elif isinstance(val, str):
+ d = utils.date_from_str(val)
+ return d.date() if not isinstance(d, tuple) else d
+ elif isinstance(val, (int, float)):
+ d = utils.date_from_str(str(val))
+ return d.date() if not isinstance(d, tuple) else d
+ else:
+ raise
+ except:
+ raise Exception(f"Invalid date value={val} specified for key={param}")
+
+ @staticmethod
+ def _to_time(param, val):
+ try:
+ if isinstance(val, (datetime.datetime)):
+ return val.time()
+ elif isinstance(val, datetime.time):
+ return val
+ elif isinstance(val, str):
+ return utils.time_from_str(val)
+ elif isinstance(val, int):
+ return utils.time_from_str(str(val))
+ else:
+ raise
+ except:
+ raise Exception(f"Invalid time value={val} specified for key={param}")
+
+
+class FieldsetIndexer(GribIndexer):
+ def __init__(self, *args):
+ super().__init__(*args)
+ self.ref_column_count = 1
+
+ def scan(self, vector=False):
+ data = self._scan(self.db.fs, mapped_params=self.db.mapped_params)
+ if data:
+ df = self._make_dataframe(data, sort=False)
+ self.db.blocks["scalar"] = df
+ if vector:
+ self._scan_vector()
+
+ def _scan(self, fs, mapped_params={}):
+ LOG.info(f" scan fields ...")
+ data = {}
+ # print(f"fs_len={len(fs)}")
+ # print(f"keys_ecc={self.keys_ecc}")
+ if utils.is_fieldset_type(fs) and len(fs) > 0:
+ md_vals = fs.grib_get(self.keys_ecc, "key")
+ if mapped_params:
+ for i in range(len(fs)):
+ v = md_vals[self.param_id_index][i]
+ if v in mapped_params:
+ short_name = mapped_params[v]
+ md_vals[self.shortname_index][i] = short_name
+
+ assert len(self.keys) == len(self.keys_ecc)
+ data = {k: md_vals[i] for i, k in enumerate(self.keys)}
+ data["_msgIndex1"] = list(range(len(fs)))
+ LOG.info(f" {len(fs)} GRIB messages processed")
+ return data
+
+ def _scan_vector(self):
+ df = self.db.blocks["scalar"]
+ if df is not None and not df.empty:
+ for v_name, v_comp in GribIndexer.VECTOR_PARAMS.items():
+ r = self._build_vector_index(df, v_name, v_comp)
+ comp_num = len(v_comp)
+ if r:
+ cols = [*self.keys]
+ for i in range(comp_num):
+ cols.extend([f"_msgIndex{i+1}"])
+ w_df = self._make_dataframe(r, sort=False, columns=cols)
+ self.db.blocks[v_name] = w_df
+ # self._write_dataframe(w_df, v_name, out_dir)
+ else:
+ LOG.debug(" No paired fields found!")
+ continue
+
+
+class ExperimentIndexer(GribIndexer):
+ def __init__(self, *args):
+ super().__init__(*args)
+ self.ref_column_count = 2
+
+ def scan(self):
+ out_dir = self.db.db_dir
+ Path(out_dir).mkdir(exist_ok=True, parents=True)
+ LOG.info(f"scan {self.db} out_dir={out_dir} ...")
+
+ data = {k: [] for k in [*self.keys, "_msgIndex1", "_fileIndex1"]}
+ input_files = []
+
+ # print(f"out_dir={out_dir}")
+ # merge existing experiment objects
+ if self.db.merge_conf:
+ ds = []
+ # simple merge
+ if isinstance(self.db.merge_conf, list):
+ for c_name in self.db.merge_conf:
+ ds.append(
+ {"data": db.dataset.find(c_name), "name": c_name, "ens": {}}
+ )
+ # explicit ENS merge
+ else:
+ assert "pf" in self.db.merge_conf
+ # control forecast
+ c_name = self.db.merge_conf.get("cf", "")
+ if c_name != "":
+ ds.append(
+ {
+ "data": self.db.dataset.find(c_name, comp="field"),
+ "name": c_name,
+ "ens": {"type": "cf", "number": 0},
+ }
+ )
+ for i, c_name in enumerate(self.db.merge_conf.get("pf", [])):
+ ds.append(
+ {
+ "data": self.db.dataset.find(c_name, comp="field"),
+ "name": c_name,
+ "ens": {"type": "pf", "number": i + 1},
+ }
+ )
+
+ for c in ds:
+ if c["data"] is None:
+ c_name = d["name"]
+ raise Exception(
+ f"Cannot merge experiments as {self.db}! Experiment {c_name} is not found!"
+ )
+ else:
+ input_files = self._scan_one(
+ input_dir=c["data"].path,
+ file_name_pattern=c["data"].file_name_pattern,
+ input_files=input_files,
+ mapped_params=self.db.mapped_params,
+ ens=c["ens"],
+ data=data,
+ rootdir_placeholder_value=c["data"].rootdir_placeholder_value,
+ rootdir_placeholder_token=self.db.ROOTDIR_PLACEHOLDER_TOKEN,
+ )
+ # index a single experiment
+ else:
+ input_files = self._scan_one(
+ input_dir=self.db.path,
+ file_name_pattern=self.db.file_name_pattern,
+ input_files=[],
+ mapped_params=self.db.mapped_params,
+ ens={},
+ data=data,
+ rootdir_placeholder_value=self.db.rootdir_placeholder_value,
+ rootdir_placeholder_token=self.db.ROOTDIR_PLACEHOLDER_TOKEN,
+ )
+
+ # print(f"input_files={input_files}")
+ if len(input_files) > 0 and len(data["shortName"]) > 0:
+ # write config file for input file list
+ LOG.info(f"generate datafiles.yaml ...")
+ f_name = os.path.join(out_dir, "datafiles.yaml")
+ r = yaml.dump(input_files, default_flow_style=False)
+ with open(f_name, "w") as f:
+ f.write(r)
+ self.db.input_files = input_files
+
+ # scalar
+ LOG.info(f"generate scalar fields index ...")
+ df = self._make_dataframe(data, sort=True)
+ self.db.blocks["scalar"] = df
+ self._write_dataframe(df, "scalar", out_dir)
+
+ # vector (2D)
+ LOG.info(f"generate vector fields index ...")
+ for v_name, v_comp in GribIndexer.VECTOR_PARAMS.items():
+ r = self._build_vector_index(df, v_name, v_comp)
+ comp_num = len(v_comp)
+ if r:
+ cols = [*self.keys]
+ for i in range(comp_num):
+ cols.extend([f"_msgIndex{i+1}", f"_fileIndex{i+1}"])
+ w_df = self._make_dataframe(r, sort=True, columns=cols)
+ # print(f"wind_len={len(w_df.index)}")
+ self.db.blocks[v_name] = w_df
+ self._write_dataframe(w_df, v_name, out_dir)
+ else:
+ LOG.debug(" No paired fields found!")
+ continue
+
+ def _scan_one(
+ self,
+ input_dir="",
+ file_name_pattern="",
+ input_files=[],
+ mapped_params={},
+ ens={},
+ data={},
+ rootdir_placeholder_value="",
+ rootdir_placeholder_token=None,
+ ):
+ LOG.info("scan fields ...")
+ LOG.info(f" input_dir={input_dir} file_name_pattern={file_name_pattern}")
+ # print(f" input_dir={input_dir} file_name_pattern={file_name_pattern}")
+
+ # for f_path in glob.glob(f_pattern):
+ cnt = 0
+ input_files_tmp = []
+ for f_path in utils.get_file_list(
+ input_dir, file_name_pattern=file_name_pattern
+ ):
+ # LOG.debug(f" f_path={f_path}")
+ fs = self.db.fieldset_class(path=f_path)
+ if utils.is_fieldset_type(fs) and len(fs) > 0:
+ cnt += 1
+ input_files_tmp.append(f_path)
+ file_index = len(input_files) + len(input_files_tmp) - 1
+ md_vals = fs.grib_get(self.keys_ecc, "key")
+
+ if mapped_params:
+ for i in range(len(fs)):
+ v = md_vals[self.param_id_index][i]
+ if v in mapped_params:
+ short_name = mapped_params[v]
+ md_vals[self.shortname_index][i] = short_name
+ if ens:
+ for i in range(len(fs)):
+ md_vals[self.type_index][i] = ens["type"]
+ md_vals[self.number_index][i] = ens["number"]
+
+ assert len(self.keys) == len(self.keys_ecc)
+ for i, c in enumerate(self.keys):
+ data[c].extend(md_vals[i])
+ data["_msgIndex1"].extend(list(range(len(fs))))
+ data["_fileIndex1"].extend([file_index] * len(fs))
+
+ # print({k: len(v) for k, v in data.items()})
+
+ if rootdir_placeholder_value:
+ input_files_tmp = [
+ x.replace(rootdir_placeholder_value, rootdir_placeholder_token)
+ for x in input_files_tmp
+ ]
+
+ input_files.extend(input_files_tmp)
+
+ LOG.info(f" {cnt} GRIB files processed")
+ return input_files
+
+ def allowed_keys(self):
+ r = list(self.keys)
+ r.extend(GribIndexer.DATE_KEYS)
+ r.extend(GribIndexer.TIME_KEYS)
+ r.extend(list(GribIndexer.DATETIME_KEYS.keys()))
+ return set(r)
diff --git a/metview/metviewpy/ipython.py b/metview/metviewpy/ipython.py
new file mode 100644
index 00000000..feb360ea
--- /dev/null
+++ b/metview/metviewpy/ipython.py
@@ -0,0 +1,42 @@
+#
+# (C) Copyright 2017- ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation
+# nor does it submit to any jurisdiction.
+#
+
+"""
+ipython is not None when running a notebook
+"""
+
+import logging
+import sys
+
+
+ipython_active = None
+
+
+def is_ipython_active():
+ global ipython_active
+ if ipython_active is None:
+ try:
+ from IPython import get_ipython
+
+ ipython_active = get_ipython() is not None
+ except Exception:
+ ipython_active = False
+ return ipython_active
+
+
+def import_widgets():
+ try:
+ widgets = __import__("ipywidgets", globals(), locals())
+ return widgets
+ except ImportError as imperr:
+ print("Could not import ipywidgets module - animation widget will not appear")
+ print("Call setoutput('jupyter', plot_widget=False) to suppress this message")
+ return None
diff --git a/metview/metviewpy/maths.py b/metview/metviewpy/maths.py
new file mode 100644
index 00000000..ba46c392
--- /dev/null
+++ b/metview/metviewpy/maths.py
@@ -0,0 +1,168 @@
+# (C) Copyright 2017- ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation
+# nor does it submit to any jurisdiction.
+import numpy as np
+
+
+def neg(x):
+ return -x
+
+
+def pos(x):
+ return x
+
+
+# def abs(x):
+# return np.abs(x)
+
+
+def not_func(x):
+ return (~(x != 0)).astype(int)
+
+
+def add(x, y):
+ return x + y
+
+
+def sub(x, y):
+ return x - y
+
+
+def mul(x, y):
+ return x * y
+
+
+def div(x, y):
+ return x / y
+
+
+def pow(x, y):
+ return x**y
+
+
+def ge(x, y):
+ return (x >= y).astype(int)
+
+
+def gt(x, y):
+ return (x > y).astype(int)
+
+
+def le(x, y):
+ return (x <= y).astype(int)
+
+
+def lt(x, y):
+ return (x < y).astype(int)
+
+
+def eq(x, y):
+ return (x == y).astype(int)
+
+
+def ne(x, y):
+ return (x != y).astype(int)
+
+
+def and_func(x, y):
+ return ne(x, 0) * ne(y, 0)
+
+
+def or_func(x, y):
+ return np.clip(ne(x, 0) + ne(y, 0), 0, 1)
+
+
+def set_from_other(x, y):
+ return y
+
+
+# single argument functions
+
+
+def abs(x):
+ return np.fabs(x)
+
+
+def acos(x):
+ return np.arccos(x)
+
+
+def asin(x):
+ return np.arcsin(x)
+
+
+def atan(x):
+ return np.arctan(x)
+
+
+def cos(x):
+ return np.cos(x)
+
+
+def exp(x):
+ return np.exp(x)
+
+
+def log(x):
+ return np.log(x)
+
+
+def log10(x):
+ return np.log10(x)
+
+
+def sgn(x):
+ return np.sign(x)
+
+
+def square(x):
+ return np.square(x)
+
+
+def sqrt(x):
+ return np.sqrt(x)
+
+
+def sin(x):
+ return np.sin(x)
+
+
+def tan(x):
+ return np.tan(x)
+
+
+# double argument functions
+
+
+def atan2(x, y):
+ return np.arctan2(x, y)
+
+
+def floor_div(x, y):
+ return np.floor_divide(x, y)
+
+
+def mod(x, y):
+ return np.mod(x, y)
+
+
+# bitmapping
+
+
+def bitmap(x, y):
+ if isinstance(y, (int, float)):
+ x[x == y] = np.nan
+ return x
+ elif isinstance(y, np.ndarray):
+ x[np.isnan(y)] = np.nan
+ return x
+
+
+def nobitmap(x, y):
+ x[np.isnan(x)] = y
+ return x
diff --git a/metview/metviewpy/param.py b/metview/metviewpy/param.py
new file mode 100644
index 00000000..7ba5033b
--- /dev/null
+++ b/metview/metviewpy/param.py
@@ -0,0 +1,582 @@
+# (C) Copyright 2017- ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation
+# nor does it submit to any jurisdiction.
+#
+
+import logging
+import re
+import pandas as pd
+
+from .indexer import GribIndexer
+from .ipython import is_ipython_active
+from .utils import is_fieldset_type
+
+# logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
+# logging.basicConfig(level=logging.DEBUG, format="%(levelname)s - %(message)s")
+LOG = logging.getLogger(__name__)
+
+PANDAS_ORI_OPTIONS = {}
+
+
+def init_pandas_options():
+ global PANDAS_ORI_OPTIONS
+ if len(PANDAS_ORI_OPTIONS) == 0:
+ opt = {
+ "display.max_colwidth": 300,
+ "display.colheader_justify": "center",
+ "display.max_columns": 100,
+ "display.max_rows": 500,
+ "display.width": None,
+ }
+ for k, _ in opt.items():
+ PANDAS_ORI_OPTIONS[k] = pd.get_option(k)
+ for k, v in opt.items():
+ pd.set_option(k, v)
+
+
+def reset_pandas_options():
+ global PANDAS_ORI_OPTIONS
+ if len(PANDAS_ORI_OPTIONS) > 0:
+ for k, v in PANDAS_ORI_OPTIONS.items():
+ pd.set_option(k, v)
+ PANDAS_ORI_OPTIONS = {}
+
+
+class ParamInfo:
+ SUFFIXES = {
+ "hPa": "isobaricInhPa",
+ "hpa": "isobaricInhPa",
+ "K": "theta",
+ "ml": "hybrid",
+ }
+ LEVEL_TYPES = {"sfc": "surface", "pl": "isobaricInhPa", "ml": "hybrid"}
+ LEVEL_RE = re.compile(r"(\d+)")
+ NUM_RE = re.compile(r"[0-9]+")
+ SURF_RE = re.compile(r"^\d+\w+")
+ # SURF_NAME_MAPPER = {"t2": "2t", "q2": "2q", "u10": "10u", "v10": "10v"}
+ KNOWN_SURF_NAMES = [
+ "2t",
+ "2q",
+ "10u",
+ "10v",
+ "100u",
+ "100v",
+ "200u",
+ "200v",
+ "msl",
+ "wind10m",
+ "wind100m",
+ "wind200m",
+ ]
+ VECTOR_NAMES = [
+ "wind100m",
+ "wind200m",
+ "wind10m",
+ "wind3d",
+ "wind",
+ ] # the longest ones first
+
+ def __init__(self, name, meta=None, scalar=None):
+ self.name = name
+ self.scalar = scalar if scalar is not None else True
+ self.meta = {} if meta is None else meta
+ if len(self.meta) == 0:
+ self.meta["shortName"] = name
+
+ def make_filter(self):
+ dims = {}
+ if self.name:
+ dims["shortName"] = [self.name]
+ for n in ["level", "typeOfLevel"]:
+ v = self.meta.get(n, None)
+ if v is not None:
+ dims[n] = [v]
+ return dims
+
+ @staticmethod
+ def build_from_name(full_name, param_level_types=None):
+ full_name = full_name
+ name = full_name
+ level = None
+ level_type = ""
+
+ # the name is a known param name
+ if param_level_types:
+ if name in param_level_types:
+ lev_t = param_level_types.get(name, [])
+ meta = {}
+ if len(lev_t) == 1:
+ meta = {"typeOfLevel": lev_t[0], "level": None}
+ scalar = not name in ParamInfo.VECTOR_NAMES
+ return ParamInfo(name, meta=meta, scalar=scalar)
+
+ t = full_name
+ # surface fields
+ if t in ParamInfo.KNOWN_SURF_NAMES or ParamInfo.SURF_RE.match(t) is not None:
+ level_type = "surface"
+
+ else:
+ # guess the level type from the suffix
+ for k, v in ParamInfo.SUFFIXES.items():
+ if full_name.endswith(k):
+ level_type = v
+ t = full_name[: -(len(k))]
+ break
+
+ # recognise vector params
+ for v in ParamInfo.VECTOR_NAMES:
+ if t.startswith(v):
+ name = v
+ t = t[len(v) :]
+ break
+
+ # determine level value
+ m = ParamInfo.LEVEL_RE.search(t)
+ if m and m.groups() and len(m.groups()) == 1:
+ level = int(m.group(1))
+ if level_type == "" and level > 10:
+ level_type = "isobaricInhPa"
+ if name == full_name:
+ name = ParamInfo.NUM_RE.sub("", t)
+
+ # check param name in the conf
+ if param_level_types:
+ if not name in param_level_types:
+ raise Exception(
+ f"Param={name} (guessed from name={full_name}) is not found in dataset!"
+ )
+
+ lev_t = param_level_types.get(name, [])
+ if lev_t:
+ if not level_type and len(lev_t) == 1:
+ level_type = lev_t[0]
+ elif level_type and level_type not in lev_t:
+ raise Exception(
+ f"Level type cannot be guessed from param name={full_name}!"
+ )
+
+ if level_type == "":
+ level = None
+ scalar = not name in ParamInfo.VECTOR_NAMES
+
+ LOG.debug(f"scalar={scalar}")
+ meta = {"level": level, "typeOfLevel": level_type}
+ return ParamInfo(name, meta=meta, scalar=scalar)
+
+ @staticmethod
+ def build_from_fieldset(fs):
+ assert is_fieldset_type(fs)
+ f = fs[0:3] if len(fs) >= 3 else fs
+ m = ParamInfo._grib_get(f, GribIndexer.DEFAULT_ECC_KEYS)
+ name = level = lev_type = ""
+ scalar = True
+
+ meta_same = True
+ for x in m.keys():
+ if x != "shortName" and m[x].count(m[x][0]) != len(m[x]):
+ same = False
+ break
+ if meta_same:
+ if len(m["shortName"]) == 3 and m["shortName"] == ["u", "v", "w"]:
+ name = "wind3d"
+ scalar = False
+ elif len(m["shortName"]) >= 2:
+ if m["shortName"][0:2] == ["u", "v"]:
+ name = "wind"
+ scalar = False
+ elif m["shortName"][0:2] == ["10u", "10v"]:
+ name = "wind10m"
+ m["level"][0] = 0
+ m["typeOfLevel"][0] = "sfc"
+ scalar = False
+ if not name:
+ name = m["shortName"][0]
+
+ if name:
+ return ParamInfo(name, meta={k: v[0] for k, v in m.items()}, scalar=scalar)
+ else:
+ return None
+
+ def _meta_match(self, meta, key):
+ local_key = key if key != "levelist" else "level"
+ if (
+ key in meta
+ and meta[key] is not None
+ and meta[key]
+ and local_key in self.meta
+ ):
+ # print(f"local={self.meta[local_key]} other={meta[key]}")
+ if isinstance(meta[key], list):
+ return str(self.meta[local_key]) in meta[key]
+ else:
+ return meta[key] == str(self.meta[local_key])
+ else:
+ return False
+
+ def match(self, name, meta):
+ # print(f"{self}, name={name}, meta={meta}")
+ r = 0
+ if self.name == name:
+ r += 3
+ for n in ["shortName", "paramId"]:
+ if self._meta_match(meta, n):
+ r += 1
+ # we only check the rest if the param is ok
+ if r > 0:
+ if self._meta_match(meta, "typeOfLevel"):
+ r += 1
+ if self._meta_match(meta, "levelist"):
+ r += 1
+
+ return r
+
+ def update_meta(self, meta):
+ self.meta = {**meta, **self.meta}
+
+ @staticmethod
+ def _grib_get(f, keys, single_value_as_list=True):
+ md = f.grib_get(keys, "key")
+ m = {}
+ for k, v in zip(keys, md):
+ key_val = k.split(":")[0]
+ val = v
+ if k.endswith(":l"):
+ val = []
+ for x in v:
+ try:
+ val.append(int(x))
+ except:
+ val.append(None)
+ if not single_value_as_list and len(val) == 1:
+ val = val[0]
+ m[key_val] = val
+ return m
+
+ def __str__(self):
+ return "{}[name={}, scalar={}, meta={}]".format(
+ self.__class__.__name__, self.name, self.scalar, self.meta
+ )
+
+
+class ParamDesc:
+ def __init__(self, name):
+ self.db = None
+ # self.name = name
+ self.md = {}
+ self.levels = {}
+ self._short_name = None
+ self._param_id = None
+ self._long_name = None
+ self._units = None
+
+ def load(self, db):
+ raise NotImplementedError
+
+ def _parse(self, md):
+ if "level" in md and len(md["level"]) > 0:
+ df = pd.DataFrame(md)
+ md.pop("typeOfLevel")
+ md.pop("level")
+
+ for md_key in list(md.keys()):
+ d = list(df[md_key].unique())
+ self.md[md_key] = d
+
+ lev_types = list(df["typeOfLevel"].unique())
+ for t in lev_types:
+ # print(f" t={t}")
+ self.levels[t] = []
+ q = f"typeOfLevel == '{t}'"
+ # print(q)
+ dft = df.query(q)
+ if dft is not None:
+ self.levels[t] = list(dft["level"].unique())
+
+ for k, v in self.md.items():
+ self.md[k] = sorted(v)
+
+ for k, v in self.levels.items():
+ self.levels[k] = sorted(v)
+
+ @property
+ def short_name(self):
+ if self._short_name is None:
+ self._short_name = ""
+ if self.md["shortName"]:
+ self._short_name = self.md["shortName"][0]
+ return self._short_name
+
+ @property
+ def param_id(self):
+ if self._param_id is None:
+ self._param_id = ""
+ if self.md["paramId"]:
+ self._param_id = self.md["paramId"][0]
+ return self._param_id
+
+ @property
+ def long_name(self):
+ if self._long_name is None:
+ self._long_name = ""
+ if self.db is not None:
+ self._long_name, self._units = self.db.get_longname_and_units(
+ self.short_name, self.param_id
+ )
+ return self._long_name
+
+ @property
+ def units(self):
+ if self._units is None:
+ self._units = ""
+ if self.db:
+ self._long_name, self._units = self.db.get_longname_and_units(
+ self.short_name, self.param_id
+ )
+ return self._units
+
+ @staticmethod
+ def describe(db, param=None, no_print=False):
+ labels = {"marsClass": "class", "marsStream": "stream", "marsType": "type"}
+ in_jupyter = is_ipython_active()
+
+ # describe all the params
+ if param is None:
+ t = {"parameter": [], "typeOfLevel": [], "level": []}
+ need_number = False
+ for k, v in db.param_meta.items():
+ if not v.md.get("number", None) in [["0"], [None]]:
+ need_number = True
+ break
+
+ for k, v in db.param_meta.items():
+ t["parameter"].append(k)
+ if len(v.levels) > 1:
+ lev_type = ""
+ level = ""
+ cnt = 0
+ for md_k, md in v.levels.items():
+ if in_jupyter:
+ lev_type += md_k + "
"
+ level += str(ParamDesc.format_list(md)) + "
"
+ else:
+ prefix = " " if cnt > 0 else ""
+ lev_type += prefix + f"[{cnt+1}]:" + md_k
+ level += (
+ prefix + f"[{cnt+1}]:" + str(ParamDesc.format_list(md))
+ )
+ cnt += 1
+ t["typeOfLevel"].append(lev_type)
+ t["level"].append(level)
+ else:
+ for md_k, md in v.levels.items():
+ t["typeOfLevel"].append(md_k)
+ t["level"].append(ParamDesc.format_list(md))
+
+ for md_k, md in v.md.items():
+ if md_k != "number" or need_number:
+ md_k = labels.get(md_k, md_k)
+ if not md_k in t:
+ t[md_k] = []
+ t[md_k].append(ParamDesc.format_list(md))
+
+ if in_jupyter:
+ txt = ParamDesc._make_html_table(t)
+ from IPython.display import HTML
+
+ return HTML(txt)
+ else:
+ df = pd.DataFrame.from_dict(t)
+ df = df.set_index(["parameter"])
+ init_pandas_options()
+ if not no_print:
+ print(df)
+ return df
+
+ # specific param
+ else:
+ v = None
+ if isinstance(param, str):
+ v = db.param_meta.get(param, None)
+ elif isinstance(param, int):
+ v = db.param_id_meta(param)
+
+ if v is None or len(v.md) == 0:
+ print(f"No shortName/paramId={param} found in data!")
+ return
+
+ # if v is not None:
+ t = {
+ "key": ["shortName"],
+ "val": [v.short_name],
+ }
+
+ if v.long_name != "" or v.units != "":
+ t["key"].append("name")
+ t["val"].append(v.long_name)
+
+ t["key"].append("paramId")
+ t["val"].append(v.param_id)
+ # ParamDesc.format_list(v.md["shortName"], full=True),
+
+ if v.long_name != "" or v.units != "":
+ t["key"].append("units")
+ t["val"].append(v.units)
+
+ add_cnt = len(v.levels) > 1
+ cnt = 0
+ for md_k, md in v.levels.items():
+ t["key"].append("typeOfLevel" + (f"[{cnt+1}]" if add_cnt else ""))
+ t["val"].append(md_k)
+ t["key"].append("level" + (f"[{cnt+1}]" if add_cnt else ""))
+ t["val"].append(ParamDesc.format_list(md, full=True))
+ cnt += 1
+
+ for kk, md_v in v.md.items():
+ if kk == "number" and md_v == ["0"]:
+ continue
+ if not kk in ["shortName", "paramId"]:
+ t["key"].append(labels.get(kk, kk))
+ t["val"].append(ParamDesc.format_list(md_v, full=True))
+
+ if in_jupyter:
+ from IPython.display import HTML
+
+ txt = ParamDesc._make_html_table(t, header=False)
+ return HTML(txt)
+ else:
+ df = pd.DataFrame.from_dict(t)
+ df = df.set_index("key")
+ init_pandas_options()
+ if not no_print:
+ print(df)
+ return df
+
+ @staticmethod
+ def _make_html_table(d, header=True):
+ if len(d) > 1:
+ first_column_name = list(d.keys())[0]
+ txt = """
+