import math
from datetime import datetime
from typing import Tuple
import netCDF4
#: The current version of the data cube's configuration and data model.
#: The model version is incremented on every change of the cube's data model.
CUBE_MODEL_VERSION = '2.0.2'
CUBE_CHANGELOG = """
version 2.0.2
---------------
* added new parameter to cube_config: lon0, lat0, lon1, lat1 to specify upper left corner of
the upper left grid cell and the lower right corner of the lower right grid cell.
version 2.0.1
---------------
* Switched to zarr data format
version 1.0.2
-------------
* Introduced new configuration parameter comp_level (0...9, default 5)
version 1.0.1
-------------
* Configuration parameter chunk_size now used
version 1.0.0
-------------
* First official release to public
version 0.2.4
-------------
* Changed the data type of Ozone from double to float https://github.com/CAB-LAB/cablab-core/issues/54
* Fixed the inconsistencies on the values on the first time step of the second year onwards of the MPI data https://github.com/CAB-LAB/cablab-core/issues/55
* Fixed flipped aerosols data https://github.com/CAB-LAB/cablab-core/issues/57
version 0.2.3
-------------
* Fixed flipped and shifted issue in ozone data https://github.com/CAB-LAB/cablab-core/issues/52
* Fixed the missing value issue in ozone data https://github.com/CAB-LAB/cablab-core/issues/51
version 0.2.2
-------------
* Fixed lon- and pixel- shifted issues in air temperature data
* Changed the downsampling method for country mask variable to MODE
version 0.2.1
-------------
* Remove add_offset and scale_factor from cube data: https://github.com/CAB-LAB/cablab-core/issues/43
version 0.2
-----------
The netCDF file schema has been updated according to the following issues:
* CF-compliant time information: https://github.com/CAB-LAB/cablab-core/issues/30
* CF-compliant variable names (ongoing): https://github.com/CAB-LAB/cablab-core/issues/32
* CF-compliant geospatial information: https://github.com/CAB-LAB/cablab-core/issues/35
version 0.1
-----------
* initial version
"""
[docs]class CubeConfig:
"""
A data cube's static configuration information.
:param spatial_res: The spatial image resolution in degree.
:param lon0: Left border of the most left grid cell
:param lon1: Right border of the most right grid cell
:param lat0: Upper border of the uppermost grid cell
:param lat1: Lower border of the lowermost grid cell
:param grid_width: The fixed grid width in pixels (longitude direction).
:param grid_height: The fixed grid height in pixels (latitude direction).
:param temporal_res: The temporal resolution in days.
:param ref_time: A datetime value which defines the units in which time values are given, namely
'days since *ref_time*'.
:param start_time: The inclusive start time of the first image of any variable in the cube given as datetime value.
``None`` means unlimited.
:param end_time: The exclusive end time of the last image of any variable in the cube given as datetime value.
``None`` means unlimited.
:param variables: A list of variable names to be included in the cube.
:param file_format: The file format used. Must be one of 'NETCDF4', 'NETCDF4_CLASSIC', 'NETCDF3_CLASSIC'
or 'NETCDF3_64BIT'.
:param chunk_sizes: A mapping of dimension names to chunk size for encoding.
Default is None.
:param compression: Whether gzip compression is used for encoding.
Default is False.
:param comp_level: Integer between 1 and 9 describing the level of compression desired for encoding.
Default is 5. Ignored if *compression* is False.
"""
def __init__(self,
spatial_res=0.25,
grid_x0=0,
grid_y0=0,
lon0=None,
lon1=None,
lat0=None,
lat1=None,
grid_width=1440,
grid_height=720,
temporal_res=8,
calendar='gregorian',
ref_time=datetime(2001, 1, 1),
start_time=datetime(2001, 1, 1),
end_time=datetime(2012, 1, 1),
variables=None,
file_format='NETCDF4_CLASSIC',
chunk_sizes=None,
compression=False,
comp_level=5,
static_data=False,
model_version=CUBE_MODEL_VERSION):
self.model_version = model_version
if (not lon0) | (not lon1) | (not lat0) | (not lat1):
lon0 = -180 + grid_x0 * spatial_res
lat0 = 90 - grid_y0 * spatial_res
lon1 = -180 + (grid_x0 + grid_width) * spatial_res
lat1 = 90 - (grid_y0 + grid_height) * spatial_res
self.lon0 = lon0
self.lon1 = lon1
self.lat0 = lat0
self.lat1 = lat1
self.grid_width = grid_width
self.grid_height = grid_height
self.temporal_res = temporal_res
self.calendar = calendar
self.ref_time = ref_time
self.start_time = start_time
self.end_time = end_time
self.variables = variables
self.file_format = file_format
self.static_data = static_data
self.chunk_sizes = chunk_sizes
self.compression = compression
self.comp_level = comp_level
self._validate()
def __repr__(self):
return 'CubeConfig(grid_width=%d, grid_height=%d, ' \
'temporal_res=%d, ref_time=%s)' % (
self.grid_width, self.grid_height,
self.temporal_res,
repr(self.ref_time))
@property
def northing(self) -> float:
"""
The longitude position of the upper-left-most corner of the upper-left-most grid cell
given by (grid_x0, grid_y0).
"""
return self.lat0
@property
def easting(self) -> float:
"""
The latitude position of the upper-left-most corner of the upper-left-most grid cell
given by (grid_x0, grid_y0).
"""
return self.lon0
@property
def geo_bounds(self) -> Tuple[Tuple[float, float], Tuple[float, float]]:
"""
The geographical boundary given as ((LL-lon, LL-lat), (UR-lon, UR-lat)).
"""
return ((self.lon0, self.lat1),
(self.lon1, self.lat0))
@property
def time_units(self) -> str:
"""
Return the time units used by the data cube as string using the format 'days since *ref_time*'.
"""
ref_time = self.ref_time
return 'days since %4d-%02d-%02d %02d:%02d' % \
(ref_time.year, ref_time.month, ref_time.day, ref_time.hour, ref_time.minute)
@property
def num_periods_per_year(self) -> float:
"""
Return the integer number of target periods per year.
"""
return math.ceil(365.0 / self.temporal_res)
[docs] def date2num(self, date) -> float:
"""
Return the number of days for the given *date* as a number in the time units
given by the ``time_units`` property.
:param date: The date as a datetime.datetime value
"""
return netCDF4.date2num(date, self.time_units, calendar=self.calendar)
[docs] @staticmethod
def load(path) -> object:
"""
Load a CubeConfig from a text file.
:param path: The file's path name.
:return: A new CubeConfig instance
"""
config = dict()
with open(path) as fp:
code = fp.read()
exec(code, {'datetime': __import__('datetime')}, config)
CubeConfig._ensure_compatible_config(config)
return CubeConfig(**config)
@staticmethod
def _ensure_compatible_config(config_dict):
model_version = config_dict.get('model_version', None)
# Here: check for compatibility with Cube.MODEL_VERSION, convert if possible, otherwise raise error.
if model_version is None or model_version < CUBE_MODEL_VERSION:
print('WARNING: outdated cube model version, current version is %s' % CUBE_MODEL_VERSION)
def _validate(self):
if (self.lon0 < -180) | (self.lon0 > 180.0):
raise ValueError('illegal lon0 value')
if (self.lon1 < -180) | (self.lon1 > 180.0):
raise ValueError('illegal lon1 value')
if (self.lat0 < -90) | (self.lat0 > 90):
raise ValueError('illegal lat0 value')
if (self.lat1 < -90) | (self.lat1 > 90.0):
raise ValueError('illegal lat1 value')
if self.lat0 <= self.lat1:
raise ValueError(
'illegal combination of grid_y0, grid_height, spatial_res values. Latitude must be given in descending orders')
if self.lon0 >= self.lon1:
raise ValueError('illegal combination of grid_x0, grid_width, spatial_res values')
if self.chunk_sizes is not None and len(self.chunk_sizes) != 3:
raise ValueError(
'chunk_sizes must be a sequence of three integers: <time-size>, <lat-size>, <lon-size>')
if self.comp_level is not None and (self.comp_level < 1 or self.comp_level > 9):
raise ValueError('comp_level must be an integer in the range 1 to 9')