diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a382275c7e71456a109eb39e26214bfb56c537c3..bcbcb43310f344c172a1c7b2ce9e09fc43a543ff 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,15 @@ CHANGELOG .. start-here +1.1.5 +============ + +* Release date: 2024/09/20 +* Changes and new features: + + * to_netcdf function changes the type argument to nc_type + * Bugfixes + 1.1.4 ============ diff --git a/environment.yml b/environment.yml index 334f31af1a0b093980d8f186ab5af9a13fc0b90e..fe29057c4d083ad8350a8ec6bff5f322d69866ba 100755 --- a/environment.yml +++ b/environment.yml @@ -1,6 +1,3 @@ ---- - -name: NES_v1.1.4 channels: - conda-forge @@ -16,4 +13,7 @@ dependencies: - eccodes - python-eccodes - filelock - - configargparse \ No newline at end of file + - configargparse + - openpyxl + - jupyter + - ipykernel \ No newline at end of file diff --git a/nes/__init__.py b/nes/__init__.py index ed8dfd3a6366187180a6a53b10fb7c5e638f010b..e315033b0671c7afe45ff0ac35e95d9cef3c9bf8 100644 --- a/nes/__init__.py +++ b/nes/__init__.py @@ -1,8 +1,14 @@ -__date__ = "2024-05-31" -__version__ = "1.1.4" +__date__ = "2024-09-20" +__version__ = "1.1.5" from .load_nes import open_netcdf, concatenate_netcdfs # from .load_nes import open_raster from .create_nes import create_nes, from_shapefile -from .nc_projections import * from .methods.cell_measures import calculate_geometry_area +from .nc_projections import (Nes, LatLonNes, LCCNes, RotatedNes, RotatedNestedNes, MercatorNes, PointsNesProvidentia, + PointsNes, PointsNesGHOST) + +__all__ = [ + 'open_netcdf', 'concatenate_netcdfs', 'create_nes', 'from_shapefile', 'calculate_geometry_area', 'Nes', 'LatLonNes', + 'LCCNes', 'RotatedNes', 'RotatedNestedNes', 'MercatorNes', 'PointsNesProvidentia', 'PointsNesGHOST', 'PointsNes' +] diff --git a/nes/create_nes.py b/nes/create_nes.py index 98f81f07a0eed533b945c88ac86db85e6e6fe752..ce8b619fa7d6b231cb2dcb53121e58016c8e2fc5 100644 --- a/nes/create_nes.py +++ b/nes/create_nes.py @@ -4,11 +4,10 @@ import warnings import sys from netCDF4 import num2date from mpi4py import MPI -import geopandas as gpd -from .nc_projections import * +from .nc_projections import PointsNes, LatLonNes, RotatedNes, RotatedNestedNes, LCCNes, MercatorNes -def create_nes(comm=None, info=False, projection=None, parallel_method='Y', balanced=False, +def create_nes(comm=None, info=False, projection=None, parallel_method="Y", balanced=False, times=None, avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, **kwargs): """ @@ -16,24 +15,58 @@ def create_nes(comm=None, info=False, projection=None, parallel_method='Y', bala Parameters ---------- - comm: MPI.Communicator - MPI Communicator. - info: bool - Indicates if you want to get reading/writing info. - parallel_method : str - Indicates the parallelization method that you want. Default: 'Y'. - accepted values: ['X', 'Y', 'T']. - balanced : bool - Indicates if you want a balanced parallelization or not. - Balanced dataset cannot be written in chunking mode. - avoid_first_hours : int - Number of hours to remove from first time steps. - avoid_last_hours : int - Number of hours to remove from last time steps. - first_level : int - Index of the first level to use. - last_level : int, None - Index of the last level to use. None if it is the last. + comm : MPI.Comm, optional + MPI Communicator. If None, uses MPI.COMM_WORLD. + info : bool, optional + Indicates if reading/writing info should be provided. Default is False. + projection : str, optional + The projection type. Accepted values are None, "regular", "global", "rotated", "rotated-nested", "lcc", + "mercator". + parallel_method : str, optional + The parallelization method to use. Default is "Y". Accepted values are ["X", "Y", "T"]. + balanced : bool, optional + Indicates if balanced parallelization is desired. Balanced datasets cannot be written in chunking mode. + Default is False. + times : list of datetime, optional + List of datetime objects representing the time dimension. If None, a default time array is created. + avoid_first_hours : int, optional + Number of hours to remove from the start of the time steps. Default is 0. + avoid_last_hours : int, optional + Number of hours to remove from the end of the time steps. Default is 0. + first_level : int, optional + Index of the first level to use. Default is 0. + last_level : int or None, optional + Index of the last level to use. If None, the last level is used. Default is None. + **kwargs : additional arguments + Additional parameters required for specific projections. + + Returns + ------- + nes : Nes + An instance of the Nes class based on the specified parameters and projection. + + Raises + ------ + ValueError + If any required projection-specific parameters are missing or if invalid parameters are provided. + NotImplementedError + If an unsupported parallel method or projection type is specified. + + Notes + ----- + The function dynamically creates an instance of a specific Nes subclass based on the provided projection. + The required parameters for each projection type are: + - None: ["lat", "lon"] + - "regular": ["lat_orig", "lon_orig", "inc_lat", "inc_lon", "n_lat", "n_lon"] + - "global": ["inc_lat", "inc_lon"] + - "rotated": ["centre_lat", "centre_lon", "west_boundary", "south_boundary", "inc_rlat", "inc_rlon"] + - "rotated-nested": ["parent_grid_path", "parent_ratio", "i_parent_start", "j_parent_start", "n_rlat", "n_rlon"] + - "lcc": ["lat_1", "lat_2", "lon_0", "lat_0", "nx", "ny", "inc_x", "inc_y", "x_0", "y_0"] + - "mercator": ["lat_ts", "lon_0", "nx", "ny", "inc_x", "inc_y", "x_0", "y_0"] + + Example + ------- + >>> nes = create_nes(projection="regular", lat_orig=0, lon_orig=0, inc_lat=1, inc_lon=1, n_lat=180, n_lon=360) """ if comm is None: @@ -43,13 +76,13 @@ def create_nes(comm=None, info=False, projection=None, parallel_method='Y', bala # Create time array if times is None: - units = 'days since 1996-12-31 00:00:00' - calendar = 'standard' + units = "days since 1996-12-31 00:00:00" + calendar = "standard" times = num2date([0], units=units, calendar=calendar) times = [aux.replace(second=0, microsecond=0) for aux in times] else: if not isinstance(times, list): - times = times.tolist() + times = list(times) # Check if the parameters that are required to create the object have been defined in kwargs kwargs_list = [] @@ -57,66 +90,66 @@ def create_nes(comm=None, info=False, projection=None, parallel_method='Y', bala kwargs_list.append(name) if projection is None: - required_vars = ['lat', 'lon'] - elif projection == 'regular': - required_vars = ['lat_orig', 'lon_orig', 'inc_lat', 'inc_lon', 'n_lat', 'n_lon'] - elif projection == 'global': - required_vars = ['inc_lat', 'inc_lon'] - elif projection == 'rotated': - required_vars = ['centre_lat', 'centre_lon', 'west_boundary', 'south_boundary', 'inc_rlat', 'inc_rlon'] - elif projection == 'rotated-nested': - required_vars = ['parent_grid_path', 'parent_ratio', 'i_parent_start', 'j_parent_start', 'n_rlat', 'n_rlon'] - elif projection == 'lcc': - required_vars = ['lat_1', 'lat_2', 'lon_0', 'lat_0', 'nx', 'ny', 'inc_x', 'inc_y', 'x_0', 'y_0'] - elif projection == 'mercator': - required_vars = ['lat_ts', 'lon_0', 'nx', 'ny', 'inc_x', 'inc_y', 'x_0', 'y_0'] + required_vars = ["lat", "lon"] + elif projection == "regular": + required_vars = ["lat_orig", "lon_orig", "inc_lat", "inc_lon", "n_lat", "n_lon"] + elif projection == "global": + required_vars = ["inc_lat", "inc_lon"] + elif projection == "rotated": + required_vars = ["centre_lat", "centre_lon", "west_boundary", "south_boundary", "inc_rlat", "inc_rlon"] + elif projection == "rotated-nested": + required_vars = ["parent_grid_path", "parent_ratio", "i_parent_start", "j_parent_start", "n_rlat", "n_rlon"] + elif projection == "lcc": + required_vars = ["lat_1", "lat_2", "lon_0", "lat_0", "nx", "ny", "inc_x", "inc_y", "x_0", "y_0"] + elif projection == "mercator": + required_vars = ["lat_ts", "lon_0", "nx", "ny", "inc_x", "inc_y", "x_0", "y_0"] else: raise ValueError("Unknown projection: {0}".format(projection)) for var in required_vars: if var not in kwargs_list: - msg = 'Variable {0} has not been defined. '.format(var) - msg += 'For a {} projection, it is necessary to define {}'.format(projection, required_vars) + msg = "Variable {0} has not been defined. ".format(var) + msg += "For a {} projection, it is necessary to define {}".format(projection, required_vars) raise ValueError(msg) for var in kwargs_list: if var not in required_vars: - msg = 'Variable {0} has been defined. '.format(var) - msg += 'For a {} projection, you can only define {}'.format(projection, required_vars) + msg = "Variable {0} has been defined. ".format(var) + msg += "For a {} projection, you can only define {}".format(projection, required_vars) raise ValueError(msg) if projection is None: - if parallel_method == 'Y': + if parallel_method == "Y": warnings.warn("Parallel method cannot be 'Y' to create points NES. Setting it to 'X'") sys.stderr.flush() - parallel_method = 'X' - elif parallel_method == 'T': + parallel_method = "X" + elif parallel_method == "T": raise NotImplementedError("Parallel method T not implemented yet") nessy = PointsNes(comm=comm, dataset=None, info=info, parallel_method=parallel_method, avoid_first_hours=avoid_first_hours, avoid_last_hours=avoid_last_hours, first_level=first_level, last_level=last_level, balanced=balanced, create_nes=True, times=times, **kwargs) - elif projection in ['regular', 'global']: + elif projection in ["regular", "global"]: nessy = LatLonNes(comm=comm, dataset=None, info=info, parallel_method=parallel_method, avoid_first_hours=avoid_first_hours, avoid_last_hours=avoid_last_hours, first_level=first_level, last_level=last_level, balanced=balanced, create_nes=True, times=times, **kwargs) - elif projection == 'rotated': + elif projection == "rotated": nessy = RotatedNes(comm=comm, dataset=None, info=info, parallel_method=parallel_method, avoid_first_hours=avoid_first_hours, avoid_last_hours=avoid_last_hours, first_level=first_level, last_level=last_level, balanced=balanced, create_nes=True, times=times, **kwargs) - elif projection == 'rotated-nested': + elif projection == "rotated-nested": nessy = RotatedNestedNes(comm=comm, dataset=None, info=info, parallel_method=parallel_method, avoid_first_hours=avoid_first_hours, avoid_last_hours=avoid_last_hours, first_level=first_level, last_level=last_level, balanced=balanced, create_nes=True, times=times, **kwargs) - elif projection == 'lcc': + elif projection == "lcc": nessy = LCCNes(comm=comm, dataset=None, info=info, parallel_method=parallel_method, avoid_first_hours=avoid_first_hours, avoid_last_hours=avoid_last_hours, first_level=first_level, last_level=last_level, balanced=balanced, create_nes=True, times=times, **kwargs) - elif projection == 'mercator': + elif projection == "mercator": nessy = MercatorNes(comm=comm, dataset=None, info=info, parallel_method=parallel_method, avoid_first_hours=avoid_first_hours, avoid_last_hours=avoid_last_hours, first_level=first_level, last_level=last_level, balanced=balanced, @@ -127,7 +160,7 @@ def create_nes(comm=None, info=False, projection=None, parallel_method='Y', bala return nessy -def from_shapefile(path, method=None, parallel_method='Y', **kwargs): +def from_shapefile(path, method=None, parallel_method="Y", **kwargs): """ Create NES from shapefile data. @@ -140,10 +173,10 @@ def from_shapefile(path, method=None, parallel_method='Y', **kwargs): path : str Path to shapefile. method : str - Overlay method. Accepted values: ['nearest', 'intersection', None]. + Overlay method. Accepted values: ["nearest", "intersection", None]. parallel_method : str - Indicates the parallelization method that you want. Default: 'Y'. - accepted values: ['X', 'Y', 'T']. + Indicates the parallelization method that you want. Default: "Y". + accepted values: ["X", "Y", "T"]. """ # Create NES diff --git a/nes/load_nes.py b/nes/load_nes.py index b30086940f5d1af7f68fc66b0d9c790e10f4ad83..542b5837c7b28f0697a3658f4b743e59f24fe69f 100644 --- a/nes/load_nes.py +++ b/nes/load_nes.py @@ -2,17 +2,17 @@ import os import sys +from numpy import empty from mpi4py import MPI from netCDF4 import Dataset -import warnings -import numpy as np -from .nc_projections import * +from warnings import warn +from .nc_projections import RotatedNes, PointsNes, PointsNesGHOST, PointsNesProvidentia, LCCNes, LatLonNes, MercatorNes -DIM_VAR_NAMES = ['lat', 'latitude', 'lat_bnds', 'lon', 'longitude', 'lon_bnds', 'time', 'time_bnds', 'lev', 'level', - 'cell_area', 'crs', 'rotated_pole', 'x', 'y', 'rlat', 'rlon', 'Lambert_conformal', 'mercator'] +DIM_VAR_NAMES = ["lat", "latitude", "lat_bnds", "lon", "longitude", "lon_bnds", "time", "time_bnds", "lev", "level", + "cell_area", "crs", "rotated_pole", "x", "y", "rlat", "rlon", "Lambert_conformal", "mercator"] -def open_netcdf(path, comm=None, info=False, parallel_method='Y', avoid_first_hours=0, avoid_last_hours=0, +def open_netcdf(path, comm=None, info=False, parallel_method="Y", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, balanced=False): """ Open a netCDF file. @@ -30,8 +30,8 @@ def open_netcdf(path, comm=None, info=False, parallel_method='Y', avoid_first_ho avoid_last_hours : int Number of hours to remove from last time steps. parallel_method : str - Indicates the parallelization method that you want. Default: 'Y'. - Accepted values: ['X', 'Y', 'T'] + Indicates the parallelization method that you want. Default: "Y". + Accepted values: ["X", "Y", "T"] balanced : bool Indicates if you want a balanced parallelization or not. Balanced dataset cannot be written in chunking mode. first_level : int @@ -42,7 +42,7 @@ def open_netcdf(path, comm=None, info=False, parallel_method='Y', avoid_first_ho Returns ------- Nes - Nes object. Variables read in lazy mode (only metadata). + A Nes object. Variables read in lazy mode (only metadata). """ if comm is None: @@ -53,12 +53,12 @@ def open_netcdf(path, comm=None, info=False, parallel_method='Y', avoid_first_ho if not os.path.exists(path): raise FileNotFoundError(path) - dataset = Dataset(path, format="NETCDF4", mode='r', parallel=False) + dataset = Dataset(path, format="NETCDF4", mode="r", parallel=False) # Parallel is not needed for reading # if comm.Get_size() == 1: - # dataset = Dataset(path, format="NETCDF4", mode='r', parallel=False) + # dataset = Dataset(path, format="NETCDF4", mode="r", parallel=False) # else: - # dataset = Dataset(path, format="NETCDF4", mode='r', parallel=True, comm=comm, info=MPI.Info()) + # dataset = Dataset(path, format="NETCDF4", mode="r", parallel=True, comm=comm, info=MPI.Info()) if __is_rotated(dataset): # Rotated grids @@ -66,10 +66,10 @@ def open_netcdf(path, comm=None, info=False, parallel_method='Y', avoid_first_ho avoid_first_hours=avoid_first_hours, avoid_last_hours=avoid_last_hours, first_level=first_level, last_level=last_level, create_nes=False, balanced=balanced,) elif __is_points(dataset): - if parallel_method == 'Y': - warnings.warn("Parallel method cannot be 'Y' to create points NES. Setting it to 'X'") + if parallel_method == "Y": + warn("Parallel method cannot be 'Y' to create points NES. Setting it to 'X'") sys.stderr.flush() - parallel_method = 'X' + parallel_method = "X" if __is_points_ghost(dataset): # Points - GHOST nessy = PointsNesGHOST(comm=comm, dataset=dataset, info=info, @@ -122,9 +122,9 @@ def __is_rotated(dataset): Indicated if the netCDF is a rotated one. """ - if 'rotated_pole' in dataset.variables.keys(): + if "rotated_pole" in dataset.variables.keys(): return True - elif ('rlat' in dataset.dimensions) and ('rlon' in dataset.dimensions): + elif ("rlat" in dataset.dimensions) and ("rlon" in dataset.dimensions): return True else: return False @@ -145,7 +145,7 @@ def __is_points(dataset): Indicated if the netCDF is a points non-GHOST one. """ - if 'station' in dataset.dimensions: + if "station" in dataset.dimensions: return True else: return False @@ -166,7 +166,7 @@ def __is_points_ghost(dataset): Indicated if the netCDF is a points GHOST one. """ - if 'N_flag_codes' in dataset.dimensions and 'N_qa_codes' in dataset.dimensions: + if "N_flag_codes" in dataset.dimensions and "N_qa_codes" in dataset.dimensions: return True else: return False @@ -187,8 +187,8 @@ def __is_points_providentia(dataset): Indicated if the netCDF is a points Providentia one. """ - if (('grid_edge' in dataset.dimensions) and ('model_latitude' in dataset.dimensions) - and ('model_longitude' in dataset.dimensions)): + if (("grid_edge" in dataset.dimensions) and ("model_latitude" in dataset.dimensions) and + ("model_longitude" in dataset.dimensions)): return True else: return False @@ -206,10 +206,10 @@ def __is_lcc(dataset): Returns ------- value : bool - Indicated if the netCDF is a LCC one. + Indicated if the netCDF is an LCC one. """ - if 'Lambert_Conformal' in dataset.variables.keys() or 'Lambert_conformal' in dataset.variables.keys(): + if "Lambert_Conformal" in dataset.variables.keys() or "Lambert_conformal" in dataset.variables.keys(): return True else: return False @@ -230,13 +230,13 @@ def __is_mercator(dataset): Indicated if the netCDF is a Mercator one. """ - if 'mercator' in dataset.variables.keys(): + if "mercator" in dataset.variables.keys(): return True else: return False -def concatenate_netcdfs(nessy_list, comm=None, info=False, parallel_method='Y', avoid_first_hours=0, avoid_last_hours=0, +def concatenate_netcdfs(nessy_list, comm=None, info=False, parallel_method="Y", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, balanced=False): """ Concatenate variables form different sources. @@ -244,14 +244,30 @@ def concatenate_netcdfs(nessy_list, comm=None, info=False, parallel_method='Y', Parameters ---------- nessy_list : list - List of Nes objects or list of paths to concatenate. - comm : MPI.Communicator + A List of Nes objects or list of paths to concatenate. + comm : MPI.Comm MPI Communicator. + info: bool + Indicates if you want to get reading/writing info. + parallel_method : str + Indicates the parallelization method that you want. Default: "Y". + accepted values: ["X", "Y", "T"]. + balanced : bool + Indicates if you want a balanced parallelization or not. + Balanced dataset cannot be written in chunking mode. + avoid_first_hours : int + Number of hours to remove from first time steps. + avoid_last_hours : int + Number of hours to remove from last time steps. + first_level : int + Index of the first level to use. + last_level : int, None + Index of the last level to use. None if it is the last. Returns ------- Nes - Nes object with all the variables. + A Nes object with all the variables. """ if not isinstance(nessy_list, list): raise AttributeError("You must pass a list of NES objects or paths.") @@ -272,7 +288,7 @@ def concatenate_netcdfs(nessy_list, comm=None, info=False, parallel_method='Y', nessy_first = nessy_list[0] for i, aux_nessy in enumerate(nessy_list[1:]): if isinstance(aux_nessy, str): - nc_add = Dataset(filename=aux_nessy, mode='r') + nc_add = Dataset(filename=aux_nessy, mode="r") for var_name, var_info in nc_add.variables.items(): if var_name not in DIM_VAR_NAMES: nessy_first.variables[var_name] = {} @@ -281,40 +297,40 @@ def concatenate_netcdfs(nessy_list, comm=None, info=False, parallel_method='Y', if len(var_dims) < 2: data = var_info[:] elif len(var_dims) == 2: - data = var_info[nessy_first.read_axis_limits['y_min']:nessy_first.read_axis_limits['y_max'], - nessy_first.read_axis_limits['x_min']:nessy_first.read_axis_limits['x_max']] + data = var_info[nessy_first.read_axis_limits["y_min"]:nessy_first.read_axis_limits["y_max"], + nessy_first.read_axis_limits["x_min"]:nessy_first.read_axis_limits["x_max"]] data = data.reshape(1, 1, data.shape[-2], data.shape[-1]) elif len(var_dims) == 3: - if 'strlen' in var_dims: - data = var_info[nessy_first.read_axis_limits['y_min']:nessy_first.read_axis_limits['y_max'], - nessy_first.read_axis_limits['x_min']:nessy_first.read_axis_limits['x_max'], + if "strlen" in var_dims: + data = var_info[nessy_first.read_axis_limits["y_min"]:nessy_first.read_axis_limits["y_max"], + nessy_first.read_axis_limits["x_min"]:nessy_first.read_axis_limits["x_max"], :] - data_aux = np.empty(shape=(data.shape[0], data.shape[1]), dtype=object) + data_aux = empty(shape=(data.shape[0], data.shape[1]), dtype=object) for lat_n in range(data.shape[0]): for lon_n in range(data.shape[1]): - data_aux[lat_n, lon_n] = ''.join( - data[lat_n, lon_n].tobytes().decode('ascii').replace('\x00', '')) + data_aux[lat_n, lon_n] = "".join( + data[lat_n, lon_n].tobytes().decode("ascii").replace("\x00", "")) data = data_aux.reshape((1, 1, data_aux.shape[-2], data_aux.shape[-1])) else: - data = var_info[nessy_first.read_axis_limits['t_min']:nessy_first.read_axis_limits['t_max'], - nessy_first.read_axis_limits['y_min']:nessy_first.read_axis_limits['y_max'], - nessy_first.read_axis_limits['x_min']:nessy_first.read_axis_limits['x_max']] + data = var_info[nessy_first.read_axis_limits["t_min"]:nessy_first.read_axis_limits["t_max"], + nessy_first.read_axis_limits["y_min"]:nessy_first.read_axis_limits["y_max"], + nessy_first.read_axis_limits["x_min"]:nessy_first.read_axis_limits["x_max"]] data = data.reshape(data.shape[-3], 1, data.shape[-2], data.shape[-1]) elif len(var_dims) == 4: - data = var_info[nessy_first.read_axis_limits['t_min']:nessy_first.read_axis_limits['t_max'], - nessy_first.read_axis_limits['z_min']:nessy_first.read_axis_limits['z_max'], - nessy_first.read_axis_limits['y_min']:nessy_first.read_axis_limits['y_max'], - nessy_first.read_axis_limits['x_min']:nessy_first.read_axis_limits['x_max']] + data = var_info[nessy_first.read_axis_limits["t_min"]:nessy_first.read_axis_limits["t_max"], + nessy_first.read_axis_limits["z_min"]:nessy_first.read_axis_limits["z_max"], + nessy_first.read_axis_limits["y_min"]:nessy_first.read_axis_limits["y_max"], + nessy_first.read_axis_limits["x_min"]:nessy_first.read_axis_limits["x_max"]] else: raise TypeError("{} data shape is nto accepted".format(var_dims)) - nessy_first.variables[var_name]['data'] = data + nessy_first.variables[var_name]["data"] = data # Avoid some attributes for attrname in var_info.ncattrs(): - if attrname not in ['missing_value', '_FillValue']: + if attrname not in ["missing_value", "_FillValue"]: value = getattr(var_info, attrname) - if value in ['unitless', '-']: - value = '' + if value in ["unitless", "-"]: + value = "" nessy_first.variables[var_name][attrname] = value nc_add.close() diff --git a/nes/methods/__init__.py b/nes/methods/__init__.py index 772adacfae71525d99d30e987a427decf012c3f5..35b63462d5b404acdb58af6b19d83d92cf1ac7ba 100644 --- a/nes/methods/__init__.py +++ b/nes/methods/__init__.py @@ -2,3 +2,7 @@ from .vertical_interpolation import add_4d_vertical_info from .vertical_interpolation import interpolate_vertical from .horizontal_interpolation import interpolate_horizontal from .spatial_join import spatial_join + +__all__ = [ + 'add_4d_vertical_info', 'interpolate_vertical', 'interpolate_horizontal', 'spatial_join' +] diff --git a/nes/methods/cell_measures.py b/nes/methods/cell_measures.py index 5288a02ae50a533306f5bec7ca6b20c64b667ea9..185d0331506668b336a367052ec7eb7c60b8aaf0 100644 --- a/nes/methods/cell_measures.py +++ b/nes/methods/cell_measures.py @@ -1,6 +1,5 @@ #!/usr/bin/env python - -import numpy as np +from numpy import empty, newaxis, array, arcsin, tan, fabs, arctan, sqrt, radians, cos, sin, column_stack from copy import deepcopy @@ -15,37 +14,37 @@ def calculate_grid_area(self): """ # Create bounds if they do not exist - if self._lat_bnds is None or self._lon_bnds is None: + if self.lat_bnds is None or self.lon_bnds is None: self.create_spatial_bounds() # Get spatial number of vertices - spatial_nv = self.lat_bnds['data'].shape[-1] + spatial_nv = self.lat_bnds["data"].shape[-1] # Reshape bounds if spatial_nv == 2: - aux_shape = (self.lat_bnds['data'].shape[0], self.lon_bnds['data'].shape[0], 4) - lon_bnds_aux = np.empty(aux_shape) - lon_bnds_aux[:, :, 0] = self.lon_bnds['data'][np.newaxis, :, 0] - lon_bnds_aux[:, :, 1] = self.lon_bnds['data'][np.newaxis, :, 1] - lon_bnds_aux[:, :, 2] = self.lon_bnds['data'][np.newaxis, :, 1] - lon_bnds_aux[:, :, 3] = self.lon_bnds['data'][np.newaxis, :, 0] + aux_shape = (self.lat_bnds["data"].shape[0], self.lon_bnds["data"].shape[0], 4) + lon_bnds_aux = empty(aux_shape) + lon_bnds_aux[:, :, 0] = self.lon_bnds["data"][newaxis, :, 0] + lon_bnds_aux[:, :, 1] = self.lon_bnds["data"][newaxis, :, 1] + lon_bnds_aux[:, :, 2] = self.lon_bnds["data"][newaxis, :, 1] + lon_bnds_aux[:, :, 3] = self.lon_bnds["data"][newaxis, :, 0] lon_bnds = lon_bnds_aux del lon_bnds_aux - lat_bnds_aux = np.empty(aux_shape) - lat_bnds_aux[:, :, 0] = self.lat_bnds['data'][:, np.newaxis, 0] - lat_bnds_aux[:, :, 1] = self.lat_bnds['data'][:, np.newaxis, 0] - lat_bnds_aux[:, :, 2] = self.lat_bnds['data'][:, np.newaxis, 1] - lat_bnds_aux[:, :, 3] = self.lat_bnds['data'][:, np.newaxis, 1] + lat_bnds_aux = empty(aux_shape) + lat_bnds_aux[:, :, 0] = self.lat_bnds["data"][:, newaxis, 0] + lat_bnds_aux[:, :, 1] = self.lat_bnds["data"][:, newaxis, 0] + lat_bnds_aux[:, :, 2] = self.lat_bnds["data"][:, newaxis, 1] + lat_bnds_aux[:, :, 3] = self.lat_bnds["data"][:, newaxis, 1] lat_bnds = lat_bnds_aux del lat_bnds_aux else: - lon_bnds = self.lon_bnds['data'] - lat_bnds = self.lat_bnds['data'] + lon_bnds = self.lon_bnds["data"] + lat_bnds = self.lat_bnds["data"] # Reshape bounds and assign as grid corner coordinates grid_corner_lon = deepcopy(lon_bnds).reshape(lon_bnds.shape[0]*lon_bnds.shape[1], @@ -69,53 +68,53 @@ def calculate_geometry_area(geometry_list, earth_radius_minor_axis=6356752.3142, Parameters ---------- geometry_list : List - List with polygon geometries. + A List with polygon geometries. earth_radius_minor_axis : float Radius of the minor axis of the Earth. earth_radius_major_axis : float Radius of the major axis of the Earth. """ - geometry_area = np.empty(shape=(len(geometry_list,))) + geometry_area = empty(shape=(len(geometry_list,))) for geom_ind in range(0, len(geometry_list)): # Calculate the area of each geometry in multipolygon and collection objects - if geometry_list[geom_ind].geom_type in ['MultiPolygon', 'GeometryCollection']: + if geometry_list[geom_ind].geom_type in ["MultiPolygon", "GeometryCollection"]: multi_geom_area = 0 for multi_geom_ind in range(0, len(geometry_list[geom_ind].geoms)): - if geometry_list[geom_ind].geoms[multi_geom_ind].geom_type == 'Point': + if geometry_list[geom_ind].geoms[multi_geom_ind].geom_type == "Point": continue - geometry_corner_lon, geometry_corner_lat = geometry_list[geom_ind].geoms[multi_geom_ind].exterior.coords.xy - geometry_corner_lon = np.array(geometry_corner_lon) - geometry_corner_lat = np.array(geometry_corner_lat) - geom_area = mod_huiliers_area(geometry_corner_lon, geometry_corner_lat) + geometry_corner_lon, geometry_corner_lat = ( + geometry_list[geom_ind].geoms[multi_geom_ind].exterior.coords.xy) + geometry_corner_lon = array(geometry_corner_lon) + geometry_corner_lat = array(geometry_corner_lat) + geom_area = __mod_huiliers_area(geometry_corner_lon, geometry_corner_lat) multi_geom_area += geom_area geometry_area[geom_ind] = multi_geom_area * earth_radius_minor_axis * earth_radius_major_axis # Calculate the area of each geometry else: geometry_corner_lon, geometry_corner_lat = geometry_list[geom_ind].exterior.coords.xy - geometry_corner_lon = np.array(geometry_corner_lon) - geometry_corner_lat = np.array(geometry_corner_lat) - geom_area = mod_huiliers_area(geometry_corner_lon, geometry_corner_lat) + geometry_corner_lon = array(geometry_corner_lon) + geometry_corner_lat = array(geometry_corner_lat) + geom_area = __mod_huiliers_area(geometry_corner_lon, geometry_corner_lat) geometry_area[geom_ind] = geom_area * earth_radius_minor_axis * earth_radius_major_axis return geometry_area -def calculate_cell_area(grid_corner_lon, grid_corner_lat, - earth_radius_minor_axis=6356752.3142, - earth_radius_major_axis=6378137.0): +def calculate_cell_area(grid_corner_lon, grid_corner_lat, + earth_radius_minor_axis=6356752.3142, earth_radius_major_axis=6378137.0): """ Calculate the area of each cell of a grid. Parameters ---------- - grid_corner_lon : np.array - Array with longitude bounds of grid. - grid_corner_lat : np.array - Array with longitude bounds of grid. + grid_corner_lon : array + An Array with longitude bounds of grid. + grid_corner_lat : array + An Array with longitude bounds of grid. earth_radius_minor_axis : float Radius of the minor axis of the Earth. earth_radius_major_axis : float @@ -124,31 +123,31 @@ def calculate_cell_area(grid_corner_lon, grid_corner_lat, # Calculate area for each grid cell n_cells = grid_corner_lon.shape[0] - area = np.empty(shape=(n_cells,)) + area = empty(shape=(n_cells,)) for i in range(0, n_cells): - area[i] = mod_huiliers_area(grid_corner_lon[i], grid_corner_lat[i]) + area[i] = __mod_huiliers_area(grid_corner_lon[i], grid_corner_lat[i]) return area*earth_radius_minor_axis*earth_radius_major_axis -def mod_huiliers_area(cell_corner_lon, cell_corner_lat): +def __mod_huiliers_area(cell_corner_lon, cell_corner_lat): """ Calculate the area of each cell according to Huilier's theorem. Reference: CDO (https://earth.bsc.es/gitlab/ces/cdo/). Parameters ---------- - cell_corner_lon : np.array + cell_corner_lon : array Longitude boundaries of each cell. - cell_corner_lat : np.array + cell_corner_lat : array Latitude boundaries of each cell. """ - sum = 0 + my_sum = 0 # Get points 0 (bottom left) and 1 (bottom right) in Earth coordinates - point_0 = lon_lat_to_cartesian(cell_corner_lon[0], cell_corner_lat[0], earth_radius_major_axis=1) - point_1 = lon_lat_to_cartesian(cell_corner_lon[1], cell_corner_lat[1], earth_radius_major_axis=1) + point_0 = __lon_lat_to_cartesian(cell_corner_lon[0], cell_corner_lat[0], earth_radius_major_axis=1) + point_1 = __lon_lat_to_cartesian(cell_corner_lon[1], cell_corner_lat[1], earth_radius_major_axis=1) point_0, point_1 = point_0[0], point_1[0] # Get number of vertices @@ -160,67 +159,67 @@ def mod_huiliers_area(cell_corner_lon, cell_corner_lat): for i in range(2, spatial_nv): # Get point 2 (top right) in Earth coordinates - point_2 = lon_lat_to_cartesian(cell_corner_lon[i], cell_corner_lat[i], earth_radius_major_axis=1) + point_2 = __lon_lat_to_cartesian(cell_corner_lon[i], cell_corner_lat[i], earth_radius_major_axis=1) point_2 = point_2[0] # Calculate area of triangle between points 0, 1 and 2 - sum += tri_area(point_0, point_1, point_2) + my_sum += __tri_area(point_0, point_1, point_2) # Copy to calculate area of next triangle if i == (spatial_nv - 1): point_1 = deepcopy(point_2) - return sum + return my_sum -def tri_area(point_0, point_1, point_2): +def __tri_area(point_0, point_1, point_2): """ Calculate area between three points that form a triangle. Reference: CDO (https://earth.bsc.es/gitlab/ces/cdo/). Parameters ---------- - point_0 : np.array + point_0 : array Position of first point in cartesian coordinates. - point_1 : np.array + point_1 : array Position of second point in cartesian coordinates. - point_2 : np.array + point_2 : array Position of third point in cartesian coordinates. """ # Get length of side a (between point 0 and 1) - tmp_vec = cross_product(point_0, point_1) - sina = norm(tmp_vec) - a = np.arcsin(sina) + tmp_vec = __cross_product(point_0, point_1) + sin_a = __norm(tmp_vec) + a = arcsin(sin_a) # Get length of side b (between point 0 and 2) - tmp_vec = cross_product(point_0, point_2) - sinb = norm(tmp_vec) - b = np.arcsin(sinb) + tmp_vec = __cross_product(point_0, point_2) + sin_b = __norm(tmp_vec) + b = arcsin(sin_b) # Get length of side c (between point 1 and 2) - tmp_vec = cross_product(point_2, point_1) - sinc = norm(tmp_vec) - c = np.arcsin(sinc) + tmp_vec = __cross_product(point_2, point_1) + sin_c = __norm(tmp_vec) + c = arcsin(sin_c) # Calculate area s = 0.5*(a+b+c) - t = np.tan(s*0.5) * np.tan((s - a)*0.5) * np.tan((s - b)*0.5) * np.tan((s - c)*0.5) - area = np.fabs(4.0 * np.arctan(np.sqrt(np.fabs(t)))) + t = tan(s*0.5) * tan((s - a)*0.5) * tan((s - b)*0.5) * tan((s - c)*0.5) + area = fabs(4.0 * arctan(sqrt(fabs(t)))) return area -def cross_product(a, b): +def __cross_product(a, b): """ Calculate cross product between two points. Parameters ---------- - a : np.array - Position of point a in cartesian coordinates. - b : np.array - Position of point b in cartesian coordinates. + a : array + Position of point A in cartesian coordinates. + b : array + Position of point B in cartesian coordinates. """ return [a[1]*b[2] - a[2]*b[1], @@ -228,38 +227,39 @@ def cross_product(a, b): a[0]*b[1] - a[1]*b[0]] -def norm(cp): +def __norm(cp): """ Normalize the result of the cross product operation. Parameters ---------- - cp : np.array + cp : array Cross product between two points. """ - return np.sqrt(cp[0]*cp[0] + cp[1]*cp[1] + cp[2]*cp[2]) + return sqrt(cp[0]*cp[0] + cp[1]*cp[1] + cp[2]*cp[2]) -def lon_lat_to_cartesian(lon, lat, earth_radius_major_axis=6378137.0): +# noinspection DuplicatedCode +def __lon_lat_to_cartesian(lon, lat, earth_radius_major_axis=6378137.0): """ Calculate lon, lat coordinates of a point on a sphere. Parameters ---------- - lon : np.array + lon : array Longitude values. - lat : np.array + lat : array Latitude values. earth_radius_major_axis : float Radius of the major axis of the Earth. """ - lon_r = np.radians(lon) - lat_r = np.radians(lat) + lon_r = radians(lon) + lat_r = radians(lat) - x = earth_radius_major_axis * np.cos(lat_r) * np.cos(lon_r) - y = earth_radius_major_axis * np.cos(lat_r) * np.sin(lon_r) - z = earth_radius_major_axis * np.sin(lat_r) + x = earth_radius_major_axis * cos(lat_r) * cos(lon_r) + y = earth_radius_major_axis * cos(lat_r) * sin(lon_r) + z = earth_radius_major_axis * sin(lat_r) - return np.column_stack([x, y, z]) + return column_stack([x, y, z]) diff --git a/nes/methods/horizontal_interpolation.py b/nes/methods/horizontal_interpolation.py index 006f5ffa780ee4330ed58c47199c73a72fecfe6e..25efef619990c478d55226474f09a6165ae06537 100644 --- a/nes/methods/horizontal_interpolation.py +++ b/nes/methods/horizontal_interpolation.py @@ -1,27 +1,26 @@ #!/usr/bin/env python import sys -import warnings -import numpy as np -import pandas as pd -from geopandas import GeoSeries import os import nes +from warnings import warn, filterwarnings +from numpy import (ma, empty, nansum, concatenate, pad, nan, array, float64, int64, float32, meshgrid, expand_dims, + reciprocal, arange, uint32, array_split, radians, cos, sin, column_stack, zeros) +from pandas import concat, DataFrame from mpi4py import MPI from scipy import spatial from filelock import FileLock from datetime import datetime -from warnings import warn -import copy -import pyproj +from copy import deepcopy +from pyproj import Proj, Transformer, CRS import gc # CONSTANTS -NEAREST_OPTS = ['NearestNeighbour', 'NearestNeighbours', 'nn', 'NN'] -CONSERVATIVE_OPTS = ['Conservative', 'Area_Conservative', 'cons', 'conservative', 'area'] +NEAREST_OPTS = ["NearestNeighbour", "NearestNeighbours", "nn", "NN"] +CONSERVATIVE_OPTS = ["Conservative", "Area_Conservative", "cons", "conservative", "area"] -def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind='NearestNeighbour', n_neighbours=4, +def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind="NearestNeighbour", n_neighbours=4, info=False, to_providentia=False, only_create_wm=False, wm=None, flux=False): """ Horizontal methods from one grid to another one. @@ -35,7 +34,7 @@ def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind='Neares weight_matrix_path : str, None Path to the weight matrix to read/create. kind : str - Kind of horizontal interpolation. Accepted values: ['NearestNeighbour', 'Conservative']. + Kind of horizontal interpolation. Accepted values: ["NearestNeighbour", "Conservative"]. n_neighbours : int Used if kind == NearestNeighbour. Number of nearest neighbours to interpolate. Default: 4. info : bool @@ -49,36 +48,39 @@ def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind='Neares flux : bool Indicates if you want to calculate the weight matrix for flux variables. """ - if info and self.master: print("Creating Weight Matrix") + # Obtain weight matrix - if self.parallel_method == 'T': - weights, idx = get_weights_idx_t_axis(self, dst_grid, weight_matrix_path, kind, n_neighbours, - only_create_wm, wm, flux) - elif self.parallel_method in ['Y', 'X']: - weights, idx = get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighbours, - only_create_wm, wm, flux) + if self.parallel_method == "T": + weights, idx = __get_weights_idx_t_axis(self, dst_grid, weight_matrix_path, kind, n_neighbours, + only_create_wm, wm, flux) + elif self.parallel_method in ["Y", "X"]: + weights, idx = __get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighbours, + only_create_wm, wm, flux) else: - raise NotImplemented("Parallel method {0} is not implemented yet for horizontal interpolations. Use 'T'".format( - self.parallel_method)) + raise NotImplementedError("Parallel method {0} is not implemented yet for horizontal interpolations.".format( + self.parallel_method) + "Use 'T'") + if info and self.master: print("Weight Matrix done!") if only_create_wm: # weights for only_create is the WM NES object return weights - # idx[idx < 0] = np.nan - idx = np.ma.masked_array(idx, mask=idx == -999) - # idx = np.array(idx, dtype=float) - # idx[idx < 0] = np.nan - # weights[weights < 0] = np.nan - weights = np.ma.masked_array(weights, mask=weights == -999) - # weights = np.array(weights, dtype=float) - # weights[weights < 0] = np.nan + # idx[idx < 0] = nan + idx = ma.masked_array(idx, mask=idx == -999) + # idx = array(idx, dtype=float) + # idx[idx < 0] = nan + # weights[weights < 0] = nan + weights = ma.masked_array(weights, mask=weights == -999) + # weights = array(weights, dtype=float) + # weights[weights < 0] = nan # Copy NES final_dst = dst_grid.copy() + + sys.stdout.flush() final_dst.set_communicator(dst_grid.comm) # Remove original file information @@ -88,9 +90,9 @@ def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind='Neares # Return final_dst final_dst.lev = self.lev - final_dst._lev = self._lev + final_dst.set_full_levels(self.get_full_levels()) final_dst.time = self.time - final_dst._time = self._time + final_dst.set_full_times(self.get_full_times()) final_dst.hours_start = self.hours_start final_dst.hours_end = self.hours_end @@ -101,28 +103,28 @@ def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind='Neares if info and self.master: print("\t{var} horizontal interpolation".format(var=var_name)) sys.stdout.flush() - src_shape = var_info['data'].shape + src_shape = var_info["data"].shape if isinstance(dst_grid, nes.PointsNes): dst_shape = (src_shape[0], src_shape[1], idx.shape[-1]) else: dst_shape = (src_shape[0], src_shape[1], idx.shape[-2], idx.shape[-1]) # Creating new variable without data final_dst.variables[var_name] = {attr_name: attr_value for attr_name, attr_value in var_info.items() - if attr_name != 'data'} + if attr_name != "data"} # Creating empty data - final_dst.variables[var_name]['data'] = np.empty(dst_shape) + final_dst.variables[var_name]["data"] = empty(dst_shape) - # src_data = var_info['data'].reshape((src_shape[0], src_shape[1], src_shape[2] * src_shape[3])) + # src_data = var_info["data"].reshape((src_shape[0], src_shape[1], src_shape[2] * src_shape[3])) for time in range(dst_shape[0]): for lev in range(dst_shape[1]): - src_aux = get_src_data(self.comm, var_info['data'][time, lev], idx, self.parallel_method) - final_dst.variables[var_name]['data'][time, lev] = np.nansum(weights * src_aux, axis=1) + src_aux = __get_src_data(self.comm, var_info["data"][time, lev], idx, self.parallel_method) + final_dst.variables[var_name]["data"][time, lev] = nansum(weights * src_aux, axis=1) if isinstance(dst_grid, nes.PointsNes): # Removing level axis if src_shape[1] != 1: raise IndexError("Data with vertical levels cannot be interpolated to points") - final_dst.variables[var_name]['data'] = final_dst.variables[var_name]['data'].reshape( + final_dst.variables[var_name]["data"] = final_dst.variables[var_name]["data"].reshape( (src_shape[0], idx.shape[-1])) if isinstance(dst_grid, nes.PointsNesGHOST) and not to_providentia: final_dst = final_dst.to_points() @@ -144,64 +146,63 @@ def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind='Neares grid_edge_lat=grid_edge_lat) else: msg = "The final projection must be points to interpolate an experiment and get it in Providentia format." - warnings.warn(msg) + warn(msg) sys.stderr.flush() else: # Convert dimensions (time, lev, lat, lon) or (time, lat, lon) to (time, station) for interpolated variables # and reshape data if isinstance(final_dst, nes.PointsNes): for var_name, var_info in final_dst.variables.items(): - if len(var_info['dimensions']) != len(var_info['data'].shape): - final_dst.variables[var_name]['dimensions'] = ('time', 'station') + if len(var_info["dimensions"]) != len(var_info["data"].shape): + final_dst.variables[var_name]["dimensions"] = ("time", "station") return final_dst -def get_src_data(comm, var_data, idx, parallel_method): +def __get_src_data(comm, var_data, idx, parallel_method): """ To obtain the needed src data to interpolate. Parameters ---------- - comm : MPI.Communicator. + comm : MPI.Comm. MPI communicator. - var_data : np.array + var_data : array Rank source data. - idx : np.array + idx : array Index of the needed data in a 2D flatten way. parallel_method: str Source parallel method. Returns ------- - np.array + array Flatten source needed data. """ - if parallel_method == 'T': + if parallel_method == "T": var_data = var_data.flatten() else: var_data = comm.gather(var_data, root=0) if comm.Get_rank() == 0: - if parallel_method == 'Y': + if parallel_method == "Y": axis = 0 - elif parallel_method == 'X': + elif parallel_method == "X": axis = 1 else: raise NotImplementedError(parallel_method) - var_data = np.concatenate(var_data, axis=axis) + var_data = concatenate(var_data, axis=axis) var_data = var_data.flatten() var_data = comm.bcast(var_data) - var_data = np.pad(var_data, [1, 1], 'constant', constant_values=np.nan).take(idx + 1, mode='clip') - #var_data = np.take(var_data, idx) + var_data = pad(var_data, [1, 1], "constant", constant_values=nan).take(idx + 1, mode="clip") return var_data # noinspection DuplicatedCode -def get_weights_idx_t_axis(self, dst_grid, weight_matrix_path, kind, n_neighbours, only_create, wm, flux): +def __get_weights_idx_t_axis(self, dst_grid, weight_matrix_path, kind, n_neighbours, only_create, wm, flux): """ To obtain the weights and source data index through the T axis. @@ -214,7 +215,7 @@ def get_weights_idx_t_axis(self, dst_grid, weight_matrix_path, kind, n_neighbour weight_matrix_path : str, None Path to the weight matrix to read/create. kind : str - Kind of horizontal interpolation. Accepted values: ['NearestNeighbour', 'Conservative']. + Kind of horizontal interpolation. Accepted values: ["NearestNeighbour", "Conservative"]. n_neighbours : int Used if kind == NearestNeighbour. Number of nearest neighbours to interpolate. Default: 4. only_create : bool @@ -229,6 +230,7 @@ def get_weights_idx_t_axis(self, dst_grid, weight_matrix_path, kind, n_neighbour tuple Weights and source data index. """ + weight_matrix = None if wm is not None: weight_matrix = wm @@ -237,26 +239,26 @@ def get_weights_idx_t_axis(self, dst_grid, weight_matrix_path, kind, n_neighbour with FileLock(weight_matrix_path + "{0:03d}.lock".format(self.rank)): if os.path.isfile(weight_matrix_path): if self.master: - weight_matrix = read_weight_matrix(weight_matrix_path, comm=MPI.COMM_SELF) + weight_matrix = __read_weight_matrix(weight_matrix_path, comm=MPI.COMM_SELF) else: weight_matrix = True if kind in NEAREST_OPTS: if self.master: - if len(weight_matrix.lev['data']) != n_neighbours: + if len(weight_matrix.lev["data"]) != n_neighbours: warn("The selected weight matrix does not have the same number of nearest neighbours." + "Re-calculating again but not saving it.") sys.stderr.flush() - weight_matrix = create_nn_weight_matrix(self, dst_grid, n_neighbours=n_neighbours) + weight_matrix = __create_nn_weight_matrix(self, dst_grid, n_neighbours=n_neighbours) else: weight_matrix = True else: if self.master: if kind in NEAREST_OPTS: - weight_matrix = create_nn_weight_matrix(self, dst_grid, n_neighbours=n_neighbours, - wm_path=weight_matrix_path) + weight_matrix = __create_nn_weight_matrix(self, dst_grid, n_neighbours=n_neighbours, + wm_path=weight_matrix_path) elif kind in CONSERVATIVE_OPTS: - weight_matrix = create_area_conservative_weight_matrix( + weight_matrix = __create_area_conservative_weight_matrix( self, dst_grid, wm_path=weight_matrix_path, flux=flux) else: raise NotImplementedError(kind) @@ -268,9 +270,9 @@ def get_weights_idx_t_axis(self, dst_grid, weight_matrix_path, kind, n_neighbour else: if self.master: if kind in NEAREST_OPTS: - weight_matrix = create_nn_weight_matrix(self, dst_grid, n_neighbours=n_neighbours) + weight_matrix = __create_nn_weight_matrix(self, dst_grid, n_neighbours=n_neighbours) elif kind in CONSERVATIVE_OPTS: - weight_matrix = create_area_conservative_weight_matrix(self, dst_grid, flux=flux) + weight_matrix = __create_area_conservative_weight_matrix(self, dst_grid, flux=flux) else: raise NotImplementedError(kind) else: @@ -282,12 +284,12 @@ def get_weights_idx_t_axis(self, dst_grid, weight_matrix_path, kind, n_neighbour if self.master: if kind in NEAREST_OPTS: # Normalize to 1 - weights = np.array(np.array(weight_matrix.variables['weight']['data'], dtype=np.float64) / - np.array(weight_matrix.variables['weight']['data'], dtype=np.float64).sum(axis=1), - dtype=np.float64) + weights = array(array(weight_matrix.variables["weight"]["data"], dtype=float64) / + array(weight_matrix.variables["weight"]["data"], dtype=float64).sum(axis=1), + dtype=float64) else: - weights = np.array(weight_matrix.variables['weight']['data'], dtype=np.float64) - idx = np.array(weight_matrix.variables['idx']['data'][0], dtype=int) + weights = array(weight_matrix.variables["weight"]["data"], dtype=float64) + idx = array(weight_matrix.variables["idx"]["data"][0], dtype=int) else: weights = None idx = None @@ -299,7 +301,7 @@ def get_weights_idx_t_axis(self, dst_grid, weight_matrix_path, kind, n_neighbour # noinspection DuplicatedCode -def get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighbours, only_create, wm, flux): +def __get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighbours, only_create, wm, flux): """ To obtain the weights and source data index through the X or Y axis. @@ -312,7 +314,7 @@ def get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighbou weight_matrix_path : str, None Path to the weight matrix to read/create. kind : str - Kind of horizontal interpolation. Accepted values: ['NearestNeighbour', 'Conservative']. + Kind of horizontal interpolation. Accepted values: ["NearestNeighbour", "Conservative"]. n_neighbours : int Used if kind == NearestNeighbour. Number of nearest neighbours to interpolate. Default: 4. only_create : bool @@ -327,6 +329,7 @@ def get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighbou tuple Weights and source data index. """ + weight_matrix = None if isinstance(dst_grid, nes.PointsNes) and weight_matrix_path is not None: if self.master: @@ -335,33 +338,33 @@ def get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighbou weight_matrix_path = None if wm is not None: - weight_matrix = wm + weight_matrix = wm elif weight_matrix_path is not None: with FileLock(weight_matrix_path + "{0:03d}.lock".format(self.rank)): if os.path.isfile(weight_matrix_path): if self.master: - weight_matrix = read_weight_matrix(weight_matrix_path, comm=MPI.COMM_SELF) + weight_matrix = __read_weight_matrix(weight_matrix_path, comm=MPI.COMM_SELF) else: weight_matrix = True if kind in NEAREST_OPTS: if self.master: - if len(weight_matrix.lev['data']) != n_neighbours: + if len(weight_matrix.lev["data"]) != n_neighbours: warn("The selected weight matrix does not have the same number of nearest neighbours." + "Re-calculating again but not saving it.") sys.stderr.flush() - weight_matrix = create_nn_weight_matrix(self, dst_grid, n_neighbours=n_neighbours) + weight_matrix = __create_nn_weight_matrix(self, dst_grid, n_neighbours=n_neighbours) else: weight_matrix = True else: if kind in NEAREST_OPTS: if self.master: - weight_matrix = create_nn_weight_matrix(self, dst_grid, n_neighbours=n_neighbours, - wm_path=weight_matrix_path) + weight_matrix = __create_nn_weight_matrix(self, dst_grid, n_neighbours=n_neighbours, + wm_path=weight_matrix_path) else: weight_matrix = True elif kind in CONSERVATIVE_OPTS: - weight_matrix = create_area_conservative_weight_matrix( + weight_matrix = __create_area_conservative_weight_matrix( self, dst_grid, wm_path=weight_matrix_path, flux=flux) else: raise NotImplementedError(kind) @@ -370,9 +373,12 @@ def get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighbou os.remove(weight_matrix_path + "{0:03d}.lock".format(self.rank)) else: if kind in NEAREST_OPTS: - weight_matrix = create_nn_weight_matrix(self, dst_grid, n_neighbours=n_neighbours) + if self.master: + weight_matrix = __create_nn_weight_matrix(self, dst_grid, n_neighbours=n_neighbours) + else: + weight_matrix = True elif kind in CONSERVATIVE_OPTS: - weight_matrix = create_area_conservative_weight_matrix(self, dst_grid, flux=flux) + weight_matrix = __create_area_conservative_weight_matrix(self, dst_grid, flux=flux) else: raise NotImplementedError(kind) @@ -382,12 +388,12 @@ def get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighbou # Normalize to 1 if self.master: if kind in NEAREST_OPTS: - weights = np.array(np.array(weight_matrix.variables['weight']['data'], dtype=np.float64) / - np.array(weight_matrix.variables['weight']['data'], dtype=np.float64).sum(axis=1), - dtype=np.float64) + weights = array(array(weight_matrix.variables["weight"]["data"], dtype=float64) / + array(weight_matrix.variables["weight"]["data"], dtype=float64).sum(axis=1), + dtype=float64) else: - weights = np.array(weight_matrix.variables['weight']['data'], dtype=np.float64) - idx = np.array(weight_matrix.variables['idx']['data'][0], dtype=np.int64) + weights = array(weight_matrix.variables["weight"]["data"], dtype=float64) + idx = array(weight_matrix.variables["idx"]["data"][0], dtype=int64) else: weights = None idx = None @@ -398,20 +404,20 @@ def get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighbou # if isinstance(dst_grid, nes.PointsNes): # print("weights 1 ->", weights.shape) # print("idx 1 ->", idx.shape) - # weights = weights[:, dst_grid.write_axis_limits['x_min']:dst_grid.write_axis_limits['x_max']] - # idx = idx[dst_grid.write_axis_limits['x_min']:dst_grid.write_axis_limits['x_max']] + # weights = weights[:, dst_grid.write_axis_limits["x_min"]:dst_grid.write_axis_limits["x_max"]] + # idx = idx[dst_grid.write_axis_limits["x_min"]:dst_grid.write_axis_limits["x_max"]] # else: - weights = weights[:, :, dst_grid.write_axis_limits['y_min']:dst_grid.write_axis_limits['y_max'], - dst_grid.write_axis_limits['x_min']:dst_grid.write_axis_limits['x_max']] - idx = idx[:, dst_grid.write_axis_limits['y_min']:dst_grid.write_axis_limits['y_max'], - dst_grid.write_axis_limits['x_min']:dst_grid.write_axis_limits['x_max']] + weights = weights[:, :, dst_grid.write_axis_limits["y_min"]:dst_grid.write_axis_limits["y_max"], + dst_grid.write_axis_limits["x_min"]:dst_grid.write_axis_limits["x_max"]] + idx = idx[:, dst_grid.write_axis_limits["y_min"]:dst_grid.write_axis_limits["y_max"], + dst_grid.write_axis_limits["x_min"]:dst_grid.write_axis_limits["x_max"]] # print("weights 2 ->", weights.shape) # print("idx 2 ->", idx.shape) return weights, idx -def read_weight_matrix(weight_matrix_path, comm=None, parallel_method='T'): +def __read_weight_matrix(weight_matrix_path, comm=None, parallel_method="T"): """ Read weight matrix. @@ -419,8 +425,8 @@ def read_weight_matrix(weight_matrix_path, comm=None, parallel_method='T'): ---------- weight_matrix_path : str Path of the weight matrix. - comm : MPI.Communicator - Communicator to read the weight matrix. + comm : MPI.Comm + A Communicator to read the weight matrix. parallel_method : str Nes parallel method to read the weight matrix. @@ -434,16 +440,17 @@ def read_weight_matrix(weight_matrix_path, comm=None, parallel_method='T'): weight_matrix.load() # In previous versions of NES weight was called inverse_dists - if 'inverse_dists' in weight_matrix.variables.keys(): - weight_matrix.variables['weight'] = weight_matrix.variables['inverse_dists'] + if "inverse_dists" in weight_matrix.variables.keys(): + weight_matrix.variables["weight"] = weight_matrix.variables["inverse_dists"] - weight_matrix.variables['weight']['data'][weight_matrix.variables['weight']['data'] <= 0] = np.nan - weight_matrix.variables['weight']['data'][weight_matrix.variables['idx']['data'] <= 0] = np.nan + weight_matrix.variables["weight"]["data"][weight_matrix.variables["weight"]["data"] <= 0] = nan + weight_matrix.variables["weight"]["data"][weight_matrix.variables["idx"]["data"] <= 0] = nan return weight_matrix -def create_nn_weight_matrix(self, dst_grid, n_neighbours=4, wm_path=None, info=False): +# noinspection DuplicatedCode,PyProtectedMember +def __create_nn_weight_matrix(self, dst_grid, n_neighbours=4, wm_path=None, info=False): """ To create the weight matrix with the nearest neighbours method. @@ -465,32 +472,32 @@ def create_nn_weight_matrix(self, dst_grid, n_neighbours=4, wm_path=None, info=F nes.Nes Weight matrix. """ - + # Only master is here. if info and self.master: print("\tCreating Nearest Neighbour Weight Matrix with {0} neighbours".format(n_neighbours)) sys.stdout.flush() # Source - src_lat = np.array(self._lat['data'], dtype=np.float32) - src_lon = np.array(self._lon['data'], dtype=np.float32) + src_lat = array(self._full_lat["data"], dtype=float32) + src_lon = array(self._full_lon["data"], dtype=float32) # 1D to 2D coordinates if len(src_lon.shape) == 1: - src_lon, src_lat = np.meshgrid(src_lon, src_lat) + src_lon, src_lat = meshgrid(src_lon, src_lat) # Destination - dst_lat = np.array(dst_grid._lat['data'], dtype=np.float32) - dst_lon = np.array(dst_grid._lon['data'], dtype=np.float32) + dst_lat = array(dst_grid._full_lat["data"], dtype=float32) + dst_lon = array(dst_grid._full_lon["data"], dtype=float32) if isinstance(dst_grid, nes.PointsNes): - dst_lat = np.expand_dims(dst_grid._lat['data'], axis=0) - dst_lon = np.expand_dims(dst_grid._lon['data'], axis=0) + dst_lat = expand_dims(dst_grid._full_lat["data"], axis=0) + dst_lon = expand_dims(dst_grid._full_lon["data"], axis=0) else: # 1D to 2D coordinates if len(dst_lon.shape) == 1: - dst_lon, dst_lat = np.meshgrid(dst_lon, dst_lat) + dst_lon, dst_lat = meshgrid(dst_lon, dst_lat) # calculate N nearest neighbour inverse distance weights (and indices) - # from gridcells centres of model 1 to each gridcell centre of model 2 + # from gridcells centres of model 1 to each grid cell centre of model 2 # model geographic longitude/latitude coordinates are first converted # to cartesian ECEF (Earth Centred, Earth Fixed) coordinates, before # calculating distances. @@ -498,24 +505,24 @@ def create_nn_weight_matrix(self, dst_grid, n_neighbours=4, wm_path=None, info=F # src_mod_xy = lon_lat_to_cartesian(src_lon.flatten(), src_lat.flatten()) # dst_mod_xy = lon_lat_to_cartesian(dst_lon.flatten(), dst_lat.flatten()) - src_mod_xy = lon_lat_to_cartesian_ecef(src_lon.flatten(), src_lat.flatten()) - dst_mod_xy = lon_lat_to_cartesian_ecef(dst_lon.flatten(), dst_lat.flatten()) + src_mod_xy = __lon_lat_to_cartesian_ecef(src_lon.flatten(), src_lat.flatten()) + dst_mod_xy = __lon_lat_to_cartesian_ecef(dst_lon.flatten(), dst_lat.flatten()) # generate KDtree using model 1 coordinates (i.e. the model grid you are # interpolating from) src_tree = spatial.cKDTree(src_mod_xy) - # get n-neighbour nearest distances/indices (ravel form) of model 1 gridcell - # centres from each model 2 gridcell centre + # get n-neighbour nearest distances/indices (ravel form) of model 1 grid cell + # centres from each model 2 grid cell centre dists, idx = src_tree.query(dst_mod_xy, k=n_neighbours) # self.nearest_neighbour_inds = \ - # np.column_stack(np.unravel_index(idx, lon.shape)) + # column_stack(unravel_index(idx, lon.shape)) weight_matrix = dst_grid.copy() weight_matrix.time = [datetime(year=2000, month=1, day=1, hour=0, second=0, microsecond=0)] - weight_matrix._time = [datetime(year=2000, month=1, day=1, hour=0, second=0, microsecond=0)] - weight_matrix._time_bnds = None + weight_matrix._full_time = [datetime(year=2000, month=1, day=1, hour=0, second=0, microsecond=0)] + weight_matrix._full_time_bnds = None weight_matrix.time_bnds = None weight_matrix.last_level = None weight_matrix.first_level = 0 @@ -525,21 +532,22 @@ def create_nn_weight_matrix(self, dst_grid, n_neighbours=4, wm_path=None, info=F weight_matrix.set_communicator(MPI.COMM_SELF) # take the reciprocals of the nearest neighbours distances dists[dists < 1] = 1 - inverse_dists = np.reciprocal(dists) + inverse_dists = reciprocal(dists) inverse_dists_transf = inverse_dists.T.reshape((1, n_neighbours, dst_lon.shape[0], dst_lon.shape[1])) - weight_matrix.variables['weight'] = {'data': inverse_dists_transf, 'units': 'm'} + weight_matrix.variables["weight"] = {"data": inverse_dists_transf, "units": "m"} idx_transf = idx.T.reshape((1, n_neighbours, dst_lon.shape[0], dst_lon.shape[1])) - weight_matrix.variables['idx'] = {'data': idx_transf, 'units': ''} - weight_matrix.lev = {'data': np.arange(inverse_dists_transf.shape[1]), 'units': ''} - weight_matrix._lev = {'data': np.arange(inverse_dists_transf.shape[1]), 'units': ''} + weight_matrix.variables["idx"] = {"data": idx_transf, "units": ""} + weight_matrix.lev = {"data": arange(inverse_dists_transf.shape[1]), "units": ""} + weight_matrix._full_lev = {"data": arange(inverse_dists_transf.shape[1]), "units": ""} if wm_path is not None: weight_matrix.to_netcdf(wm_path) return weight_matrix -def create_area_conservative_weight_matrix(self, dst_nes, wm_path=None, flux=False, info=False): +# noinspection DuplicatedCode +def __create_area_conservative_weight_matrix(self, dst_nes, wm_path=None, flux=False, info=False): """ To create the weight matrix with the area conservative method. @@ -566,49 +574,49 @@ def create_area_conservative_weight_matrix(self, dst_nes, wm_path=None, flux=Fal print("\tCreating area conservative Weight Matrix") sys.stdout.flush() - my_crs = pyproj.CRS.from_proj4("+proj=latlon") # Common projection for both shapefiles + my_crs = CRS.from_proj4("+proj=latlon") # Common projection for both shapefiles # Get a portion of the destiny grid if dst_nes.shapefile is None: dst_nes.create_shapefile() - dst_grid = copy.deepcopy(dst_nes.shapefile) + dst_grid = deepcopy(dst_nes.shapefile) # Formatting Destination grid dst_grid.to_crs(crs=my_crs, inplace=True) - dst_grid['FID_dst'] = dst_grid.index + dst_grid["FID_dst"] = dst_grid.index # Preparing Source grid if self.shapefile is None: self.create_shapefile() - src_grid = copy.deepcopy(self.shapefile) + src_grid = deepcopy(self.shapefile) # Formatting Source grid src_grid.to_crs(crs=my_crs, inplace=True) # Serialize index intersection function to avoid memory problems - if self.size > 1 and self.parallel_method != 'T': + if self.size > 1 and self.parallel_method != "T": src_grid = self.comm.gather(src_grid, root=0) dst_grid = self.comm.gather(dst_grid, root=0) if self.master: - src_grid = pd.concat(src_grid) - dst_grid = pd.concat(dst_grid) + src_grid = concat(src_grid) + dst_grid = concat(dst_grid) if self.master: - src_grid['FID_src'] = src_grid.index + src_grid["FID_src"] = src_grid.index src_grid = src_grid.reset_index() dst_grid = dst_grid.reset_index() - fid_src, fid_dst = dst_grid.sindex.query(src_grid.geometry, predicate='intersects') + fid_src, fid_dst = dst_grid.sindex.query(src_grid.geometry, predicate="intersects") # Calculate intersected areas and fractions - intersection_df = pd.DataFrame(columns=["FID_src", "FID_dst"]) + intersection_df = DataFrame(columns=["FID_src", "FID_dst"]) - intersection_df['FID_src'] = np.array(src_grid.loc[fid_src, 'FID_src'], dtype=np.uint32) - intersection_df['FID_dst'] = np.array(dst_grid.loc[fid_dst, 'FID_dst'], dtype=np.uint32) + intersection_df["FID_src"] = array(src_grid.loc[fid_src, "FID_src"], dtype=uint32) + intersection_df["FID_dst"] = array(dst_grid.loc[fid_dst, "FID_dst"], dtype=uint32) - intersection_df['geometry_src'] = src_grid.loc[fid_src, 'geometry'].values - intersection_df['geometry_dst'] = dst_grid.loc[fid_dst, 'geometry'].values + intersection_df["geometry_src"] = src_grid.loc[fid_src, "geometry"].values + intersection_df["geometry_dst"] = dst_grid.loc[fid_dst, "geometry"].values del src_grid, dst_grid, fid_src, fid_dst # Split the array into smaller arrays in order to scatter the data among the processes - intersection_df = np.array_split(intersection_df, self.size) + intersection_df = array_split(intersection_df, self.size) else: intersection_df = None @@ -619,25 +627,24 @@ def create_area_conservative_weight_matrix(self, dst_nes, wm_path=None, flux=Fal sys.stdout.flush() if True: # No Warnings Zone - warnings.filterwarnings('ignore') - # intersection_df['weight'] = np.array(intersection_df.apply( - # lambda x: x['geometry_src'].intersection(x['geometry_dst']).buffer(0).area / x['geometry_src'].area, - # axis=1), dtype=np.float64) + filterwarnings("ignore") + # intersection_df["weight"] = array(intersection_df.apply( + # lambda x: x["geometry_src"].intersection(x["geometry_dst"]).buffer(0).area / x["geometry_src"].area, + # axis=1), dtype=float64) if flux: - intersection_df['weight'] = np.array(intersection_df.apply( - lambda x: (x['geometry_src'].intersection(x['geometry_dst']).buffer(0).area / x['geometry_src'].area) * - (nes.Nes.calculate_geometry_area([x['geometry_src']])[0] / - nes.Nes.calculate_geometry_area([x['geometry_dst']])[0]), - axis=1), dtype=np.float64) + intersection_df["weight"] = array(intersection_df.apply( + lambda x: (x["geometry_src"].intersection(x["geometry_dst"]).buffer(0).area / x["geometry_src"].area) * + (nes.Nes.calculate_geometry_area([x["geometry_src"]])[0] / + nes.Nes.calculate_geometry_area([x["geometry_dst"]])[0]), + axis=1), dtype=float64) else: - intersection_df['weight'] = np.array(intersection_df.apply( - lambda x: x['geometry_src'].intersection(x['geometry_dst']).buffer(0).area / x['geometry_src'].area, - axis=1), dtype=np.float64) - + intersection_df["weight"] = array(intersection_df.apply( + lambda x: x["geometry_src"].intersection(x["geometry_dst"]).buffer(0).area / x["geometry_src"].area, + axis=1), dtype=float64) intersection_df.drop(columns=["geometry_src", "geometry_dst"], inplace=True) gc.collect() - warnings.filterwarnings('default') + filterwarnings("default") # Format & Clean if info and self.master: @@ -645,18 +652,18 @@ def create_area_conservative_weight_matrix(self, dst_nes, wm_path=None, flux=Fal sys.stdout.flush() # Initialising weight matrix - if self.parallel_method != 'T': + if self.parallel_method != "T": intersection_df = self.comm.gather(intersection_df, root=0) if self.master: - if self.parallel_method != 'T': - intersection_df = pd.concat(intersection_df) - intersection_df = intersection_df.set_index(['FID_dst', intersection_df.groupby('FID_dst').cumcount()]).rename_axis( - ('FID', 'level')).sort_index() + if self.parallel_method != "T": + intersection_df = concat(intersection_df) + intersection_df = intersection_df.set_index( + ["FID_dst", intersection_df.groupby("FID_dst").cumcount()]).rename_axis(("FID", "level")).sort_index() intersection_df.rename(columns={"FID_src": "idx"}, inplace=True) weight_matrix = dst_nes.copy() weight_matrix.time = [datetime(year=2000, month=1, day=1, hour=0, second=0, microsecond=0)] - weight_matrix._time = [datetime(year=2000, month=1, day=1, hour=0, second=0, microsecond=0)] - weight_matrix._time_bnds = None + weight_matrix._full_time = [datetime(year=2000, month=1, day=1, hour=0, second=0, microsecond=0)] + weight_matrix._full_time_bnds = None weight_matrix.time_bnds = None weight_matrix.last_level = None weight_matrix.first_level = 0 @@ -665,36 +672,29 @@ def create_area_conservative_weight_matrix(self, dst_nes, wm_path=None, flux=Fal weight_matrix.set_communicator(MPI.COMM_SELF) - weight_matrix.set_levels({'data': np.arange(intersection_df.index.get_level_values('level').max() + 1), - 'dimensions': ('lev',), - 'units': '', - 'positive': 'up'}) + weight_matrix.set_levels({"data": arange(intersection_df.index.get_level_values("level").max() + 1), + "dimensions": ("lev",), + "units": "", + "positive": "up"}) # Creating Weight matrix empty variables - if len(weight_matrix._lat['data'].shape) == 1: - shape = (1, len(weight_matrix.lev['data']), - weight_matrix._lat['data'].shape[0], weight_matrix._lon['data'].shape[0],) - shape_flat = (1, len(weight_matrix.lev['data']), - weight_matrix._lat['data'].shape[0] * weight_matrix._lon['data'].shape[0],) - else: - shape = (1, len(weight_matrix.lev['data']), - weight_matrix._lat['data'].shape[0], weight_matrix._lat['data'].shape[1],) - shape_flat = (1, len(weight_matrix.lev['data']), - weight_matrix._lat['data'].shape[0] * weight_matrix._lat['data'].shape[1],) + wm_shape = weight_matrix.get_full_shape() + shape = (1, len(weight_matrix.lev["data"]), wm_shape[0], wm_shape[1],) + shape_flat = (1, len(weight_matrix.lev["data"]), wm_shape[0] * wm_shape[1],) - weight_matrix.variables['weight'] = {'data': np.empty(shape_flat), 'units': '-'} - weight_matrix.variables['weight']['data'][:] = -999 - weight_matrix.variables['idx'] = {'data': np.empty(shape_flat), 'units': '-'} - weight_matrix.variables['idx']['data'][:] = -999 + weight_matrix.variables["weight"] = {"data": empty(shape_flat), "units": "-"} + weight_matrix.variables["weight"]["data"][:] = -999 + weight_matrix.variables["idx"] = {"data": empty(shape_flat), "units": "-"} + weight_matrix.variables["idx"]["data"][:] = -999 # Filling Weight matrix variables - for aux_lev in weight_matrix.lev['data']: - aux_data = intersection_df.xs(level='level', key=aux_lev) - weight_matrix.variables['weight']['data'][0, aux_lev, aux_data.index] = aux_data.loc[:, 'weight'].values - weight_matrix.variables['idx']['data'][0, aux_lev, aux_data.index] = aux_data.loc[:, 'idx'].values + for aux_lev in weight_matrix.lev["data"]: + aux_data = intersection_df.xs(level="level", key=aux_lev) + weight_matrix.variables["weight"]["data"][0, aux_lev, aux_data.index] = aux_data.loc[:, "weight"].values + weight_matrix.variables["idx"]["data"][0, aux_lev, aux_data.index] = aux_data.loc[:, "idx"].values # Re-shaping - weight_matrix.variables['weight']['data'] = weight_matrix.variables['weight']['data'].reshape(shape) - weight_matrix.variables['idx']['data'] = weight_matrix.variables['idx']['data'].reshape(shape) + weight_matrix.variables["weight"]["data"] = weight_matrix.variables["weight"]["data"].reshape(shape) + weight_matrix.variables["idx"]["data"] = weight_matrix.variables["idx"]["data"].reshape(shape) if wm_path is not None: if info and self.master: print("\t\tWeight matrix saved at {0}".format(wm_path)) @@ -705,7 +705,8 @@ def create_area_conservative_weight_matrix(self, dst_nes, wm_path=None, flux=Fal return weight_matrix -def lon_lat_to_cartesian(lon, lat, radius=6378137.0): +# noinspection DuplicatedCode +def __lon_lat_to_cartesian(lon, lat, radius=6378137.0): """ Calculate lon, lat coordinates of a point on a sphere. @@ -713,49 +714,49 @@ def lon_lat_to_cartesian(lon, lat, radius=6378137.0): Parameters ---------- - lon : np.array + lon : array Longitude values. - lat : np.array + lat : array Latitude values. radius : float Radius of the sphere to get the distances. """ - lon_r = np.radians(lon) - lat_r = np.radians(lat) + lon_r = radians(lon) + lat_r = radians(lat) - x = radius * np.cos(lat_r) * np.cos(lon_r) - y = radius * np.cos(lat_r) * np.sin(lon_r) - z = radius * np.sin(lat_r) + x = radius * cos(lat_r) * cos(lon_r) + y = radius * cos(lat_r) * sin(lon_r) + z = radius * sin(lat_r) - return np.column_stack([x, y, z]) + return column_stack([x, y, z]) -def lon_lat_to_cartesian_ecef(lon, lat): +def __lon_lat_to_cartesian_ecef(lon, lat): """ Convert observational/model geographic longitude/latitude coordinates to cartesian ECEF (Earth Centred, Earth Fixed) coordinates, assuming WGS84 datum and ellipsoid, and that all heights = 0. - ECEF coordiantes represent positions (in meters) as X, Y, Z coordinates, approximating the earth surface + ECEF coordinates represent positions (in meters) as X, Y, Z coordinates, approximating the earth surface as an ellipsoid of revolution. - This conversion is for the subsequent calculation of euclidean distances of the model gridcell centres + This conversion is for the subsequent calculation of Euclidean distances of the model grid cell centres from each observational station. - Defining the distance between two points on the earth's surface as simply the euclidean distance + Defining the distance between two points on the earth's surface as simply the Euclidean distance between the two lat/lon pairs could lead to inaccurate results depending on the distance between two points (i.e. 1 deg. of longitude varies with latitude). Parameters ---------- - lon : np.array + lon : array Longitude values. - lat : np.array + lat : array Latitude values. """ - lla = pyproj.Proj(proj='latlong', ellps='WGS84', datum='WGS84') - ecef = pyproj.Proj(proj='geocent', ellps='WGS84', datum='WGS84') + lla = Proj(proj="latlong", ellps="WGS84", datum="WGS84") + ecef = Proj(proj="geocent", ellps="WGS84", datum="WGS84") - # x, y, z = pyproj.transform(lla, ecef, lon, lat, np.zeros(lon.shape), radians=False) + # x, y, z = pyproj.transform(lla, ecef, lon, lat, zeros(lon.shape), radians=False) # Deprecated: https://pyproj4.github.io/pyproj/stable/gotchas.html#upgrading-to-pyproj-2-from-pyproj-1 - transformer = pyproj.Transformer.from_proj(lla, ecef) - x, y, z = transformer.transform(lon, lat, np.zeros(lon.shape), radians=False) - return np.column_stack([x, y, z]) + transformer = Transformer.from_proj(lla, ecef) + x, y, z = transformer.transform(lon, lat, zeros(lon.shape), radians=False) + return column_stack([x, y, z]) diff --git a/nes/methods/spatial_join.py b/nes/methods/spatial_join.py index d647ebb66a6753fd1ac9b1b7c6400dd4c375c343..eb35864b4fb5e3398e6ef6a3b9f34edea59cd661 100644 --- a/nes/methods/spatial_join.py +++ b/nes/methods/spatial_join.py @@ -1,12 +1,10 @@ #!/usr/bin/env python import sys -import warnings -import geopandas as gpd -from geopandas import GeoDataFrame -import nes -import numpy as np -import pandas as pd +from warnings import warn, filterwarnings +from geopandas import sjoin_nearest, sjoin, read_file +from pandas import DataFrame +from numpy import array, uint32, nan from shapely.errors import TopologicalError @@ -17,11 +15,11 @@ def spatial_join(self, ext_shp, method=None, var_list=None, info=False, apply_bb Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. ext_shp : GeoPandasDataFrame or str File or path from where the data will be obtained on the intersection. method : str - Overlay method. Accepted values: ['nearest', 'intersection', 'centroid']. + Overlay method. Accepted values: ["nearest", "intersection", "centroid"]. var_list : List or None or str Variables that will be included in the resulting shapefile. info : bool @@ -43,27 +41,27 @@ def spatial_join(self, ext_shp, method=None, var_list=None, info=False, apply_bb sys.stdout.flush() self.create_shapefile() - ext_shp = prepare_external_shapefile(self, ext_shp=ext_shp, var_list=var_list, info=info, - apply_bbox=apply_bbox) + ext_shp = __prepare_external_shapefile(self, ext_shp=ext_shp, var_list=var_list, info=info, + apply_bbox=apply_bbox) - if method == 'nearest': + if method == "nearest": # Nearest centroids to the shapefile polygons - spatial_join_nearest(self, ext_shp=ext_shp, info=info) - elif method == 'intersection': + __spatial_join_nearest(self, ext_shp=ext_shp, info=info) + elif method == "intersection": # Intersect the areas of the shapefile polygons, outside the shapefile there will be NaN - spatial_join_intersection(self, ext_shp=ext_shp, info=info) - elif method == 'centroid': + __spatial_join_intersection(self, ext_shp=ext_shp, info=info) + elif method == "centroid": # Centroids that fall on the shapefile polygons, outside the shapefile there will be NaN - spatial_join_centroid(self, ext_shp=ext_shp, info=info) + __spatial_join_centroid(self, ext_shp=ext_shp, info=info) else: - accepted_values = ['nearest', 'intersection', 'centroid'] - raise NotImplementedError('{0} is not implemented. Choose from: {1}'.format(method, accepted_values)) + accepted_values = ["nearest", "intersection", "centroid"] + raise NotImplementedError("{0} is not implemented. Choose from: {1}".format(method, accepted_values)) return None -def prepare_external_shapefile(self, ext_shp, var_list, info=False, apply_bbox=True): +def __prepare_external_shapefile(self, ext_shp, var_list, info=False, apply_bbox=True): """ Prepare the external shapefile. @@ -76,8 +74,8 @@ def prepare_external_shapefile(self, ext_shp, var_list, info=False, apply_bbox=T Parameters ---------- self : nes.Nes - Nes Object. - ext_shp : GeoDataFrame or str + A Nes Object. + ext_shp : geopandas.GeoDataFrame or str External shapefile or path to it. var_list : List[str] or None External shapefile variables to be computed. @@ -96,20 +94,20 @@ def prepare_external_shapefile(self, ext_shp, var_list, info=False, apply_bbox=T # Reading external shapefile if self.master and info: print("\tReading external shapefile") - # ext_shp = gpd.read_file(ext_shp, include_fields=var_list, mask=self.shapefile.geometry) + # ext_shp = read_file(ext_shp, include_fields=var_list, mask=self.shapefile.geometry) if apply_bbox: - ext_shp = gpd.read_file(ext_shp, include_fields=var_list, bbox=get_bbox(self)) + ext_shp = read_file(ext_shp, include_fields=var_list, bbox=__get_bbox(self)) else: - ext_shp = gpd.read_file(ext_shp, include_fields=var_list) + ext_shp = read_file(ext_shp, include_fields=var_list) else: msg = "WARNING!!! " msg += "External shapefile already read. If you pass the path to the shapefile instead of the opened shapefile " msg += "a best usage of memory is performed because the external shape will be clipped while reading." - warnings.warn(msg) + warn(msg) sys.stderr.flush() ext_shp.reset_index(inplace=True) if var_list is not None: - ext_shp = ext_shp.loc[:, var_list + ['geometry']] + ext_shp = ext_shp.loc[:, var_list + ["geometry"]] self.comm.Barrier() if self.master and info: @@ -121,14 +119,14 @@ def prepare_external_shapefile(self, ext_shp, var_list, info=False, apply_bbox=T return ext_shp -def get_bbox(self): +def __get_bbox(self): """ Obtain the bounding box of the rank data (lon_min, lat_min, lon_max, lat_max). Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. Returns ------- @@ -136,20 +134,21 @@ def get_bbox(self): Bounding box """ - bbox = (self.lon_bnds['data'].min(), self.lat_bnds['data'].min(), - self.lon_bnds['data'].max(), self.lat_bnds['data'].max(), ) + bbox = (self.lon_bnds["data"].min(), self.lat_bnds["data"].min(), + self.lon_bnds["data"].max(), self.lat_bnds["data"].max(), ) return bbox -def spatial_join_nearest(self, ext_shp, info=False): +# noinspection DuplicatedCode +def __spatial_join_nearest(self, ext_shp, info=False): """ Perform the spatial join using the nearest method. Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. ext_shp : GeoDataFrame External shapefile. info : bool @@ -165,32 +164,33 @@ def spatial_join_nearest(self, ext_shp, info=False): # TODO: Check if the projection 4328 does not distort the coordinates too much # https://gis.stackexchange.com/questions/372564/ # userwarning-when-trying-to-get-centroid-from-a-polygon-geopandas - # ext_shp = ext_shp.to_crs('EPSG:4328') - # grid_shp = grid_shp.to_crs('EPSG:4328') + # ext_shp = ext_shp.to_crs("EPSG:4328") + # grid_shp = grid_shp.to_crs("EPSG:4328") # Calculate spatial joint by distance - aux_grid = gpd.sjoin_nearest(grid_shp, ext_shp, distance_col='distance') + aux_grid = sjoin_nearest(grid_shp, ext_shp, distance_col="distance") # Get data from closest shapes to centroids - del aux_grid['geometry'], aux_grid['index_right'] + del aux_grid["geometry"], aux_grid["index_right"] self.shapefile.loc[aux_grid.index, aux_grid.columns] = aux_grid var_list = list(ext_shp.columns) - var_list.remove('geometry') + var_list.remove("geometry") for var_name in var_list: - self.shapefile.loc[:, var_name] = np.array(self.shapefile.loc[:, var_name], dtype=ext_shp[var_name].dtype) + self.shapefile.loc[:, var_name] = array(self.shapefile.loc[:, var_name], dtype=ext_shp[var_name].dtype) return None -def spatial_join_centroid(self, ext_shp, info=False): +# noinspection DuplicatedCode +def __spatial_join_centroid(self, ext_shp, info=False): """ Perform the spatial join using the centroid method. Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. ext_shp : GeoDataFrame External shapefile. info : bool @@ -211,28 +211,28 @@ def spatial_join_centroid(self, ext_shp, info=False): if info and self.master: print("\t\tCalculating centroid spatial join") sys.stdout.flush() - aux_grid = gpd.sjoin(grid_shp, ext_shp, predicate='within') + aux_grid = sjoin(grid_shp, ext_shp, predicate="within") # Get data from shapes where there are centroids, rest will be NaN - del aux_grid['geometry'], aux_grid['index_right'] + del aux_grid["geometry"], aux_grid["index_right"] self.shapefile.loc[aux_grid.index, aux_grid.columns] = aux_grid var_list = list(ext_shp.columns) - var_list.remove('geometry') + var_list.remove("geometry") for var_name in var_list: - self.shapefile.loc[:, var_name] = np.array(self.shapefile.loc[:, var_name], dtype=ext_shp[var_name].dtype) + self.shapefile.loc[:, var_name] = array(self.shapefile.loc[:, var_name], dtype=ext_shp[var_name].dtype) return None -def spatial_join_intersection(self, ext_shp, info=False): +def __spatial_join_intersection(self, ext_shp, info=False): """ Perform the spatial join using the intersection method. Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. ext_shp : GeoDataFrame External shapefile. info : bool @@ -240,66 +240,66 @@ def spatial_join_intersection(self, ext_shp, info=False): """ var_list = list(ext_shp.columns) - var_list.remove('geometry') + var_list.remove("geometry") grid_shp = self.shapefile - grid_shp['FID_grid'] = grid_shp.index + grid_shp["FID_grid"] = grid_shp.index grid_shp = grid_shp.reset_index() # Get intersected areas - # inp, res = ext_shp.sindex.query(grid_shp.geometry, predicate='intersects') - inp, res = grid_shp.sindex.query(ext_shp.geometry, predicate='intersects') + # inp, res = ext_shp.sindex.query(grid_shp.geometry, predicate="intersects") + inp, res = grid_shp.sindex.query(ext_shp.geometry, predicate="intersects") if info: - print('\t\tRank {0:03d}: {1} intersected areas found'.format(self.rank, len(inp))) + print("\t\tRank {0:03d}: {1} intersected areas found".format(self.rank, len(inp))) sys.stdout.flush() # Calculate intersected areas and fractions - intersection = pd.DataFrame(columns=['FID', 'ext_shp_id', 'weight']) - intersection['FID'] = np.array(grid_shp.loc[res, 'FID_grid'], dtype=np.uint32) - intersection['ext_shp_id'] = np.array(inp, dtype=np.uint32) + intersection = DataFrame(columns=["FID", "ext_shp_id", "weight"]) + intersection["FID"] = array(grid_shp.loc[res, "FID_grid"], dtype=uint32) + intersection["ext_shp_id"] = array(inp, dtype=uint32) if len(intersection) > 0: if True: # No Warnings Zone - counts = intersection['FID'].value_counts() - warnings.filterwarnings('ignore') - intersection.loc[:, 'weight'] = 1. + counts = intersection["FID"].value_counts() + filterwarnings("ignore") + intersection.loc[:, "weight"] = 1. for i, row in intersection.iterrows(): if isinstance(i, int) and i % 1000 == 0 and info: - print('\t\t\tRank {0:03d}: {1:.3f} %'.format(self.rank, i * 100 / len(intersection))) + print("\t\t\tRank {0:03d}: {1:.3f} %".format(self.rank, i * 100 / len(intersection))) sys.stdout.flush() # Filter to do not calculate percentages over 100% grid cells spatial joint - if counts[row['FID']] > 1: + if counts[row["FID"]] > 1: try: - intersection.loc[i, 'weight'] = grid_shp.loc[res[i], 'geometry'].intersection( - ext_shp.loc[inp[i], 'geometry']).area / grid_shp.loc[res[i], 'geometry'].area + intersection.loc[i, "weight"] = grid_shp.loc[res[i], "geometry"].intersection( + ext_shp.loc[inp[i], "geometry"]).area / grid_shp.loc[res[i], "geometry"].area except TopologicalError: # If for some reason the geometry is corrupted it should work with the buffer function - ext_shp.loc[[inp[i]], 'geometry'] = ext_shp.loc[[inp[i]], 'geometry'].buffer(0) - intersection.loc[i, 'weight'] = grid_shp.loc[res[i], 'geometry'].intersection( - ext_shp.loc[inp[i], 'geometry']).area / grid_shp.loc[res[i], 'geometry'].area - # intersection['intersect_area'] = intersection.apply( - # lambda x: x['geometry_grid'].intersection(x['geometry_ext']).area, axis=1) - intersection.drop(intersection[intersection['weight'] <= 0].index, inplace=True) + ext_shp.loc[[inp[i]], "geometry"] = ext_shp.loc[[inp[i]], "geometry"].buffer(0) + intersection.loc[i, "weight"] = grid_shp.loc[res[i], "geometry"].intersection( + ext_shp.loc[inp[i], "geometry"]).area / grid_shp.loc[res[i], "geometry"].area + # intersection["intersect_area"] = intersection.apply( + # lambda x: x["geometry_grid"].intersection(x["geometry_ext"]).area, axis=1) + intersection.drop(intersection[intersection["weight"] <= 0].index, inplace=True) - warnings.filterwarnings('default') + filterwarnings("default") # Choose the biggest area from intersected areas with multiple options - intersection.sort_values('weight', ascending=False, inplace=True) - intersection = intersection.drop_duplicates(subset='FID', keep="first") - intersection = intersection.sort_values('FID').set_index('FID') + intersection.sort_values("weight", ascending=False, inplace=True) + intersection = intersection.drop_duplicates(subset="FID", keep="first") + intersection = intersection.sort_values("FID").set_index("FID") for var_name in var_list: - self.shapefile.loc[intersection.index, var_name] = np.array( - ext_shp.loc[intersection['ext_shp_id'], var_name]) + self.shapefile.loc[intersection.index, var_name] = array( + ext_shp.loc[intersection["ext_shp_id"], var_name]) else: for var_name in var_list: - self.shapefile.loc[:, var_name] = np.nan + self.shapefile.loc[:, var_name] = nan for var_name in var_list: - self.shapefile.loc[:, var_name] = np.array(self.shapefile.loc[:, var_name], dtype=ext_shp[var_name].dtype) + self.shapefile.loc[:, var_name] = array(self.shapefile.loc[:, var_name], dtype=ext_shp[var_name].dtype) return None diff --git a/nes/methods/vertical_interpolation.py b/nes/methods/vertical_interpolation.py index d1868e413043a90d566db81ea2eb3dba68565621..23ca71260678eadbedf31fa6d246801ae82dbe2a 100644 --- a/nes/methods/vertical_interpolation.py +++ b/nes/methods/vertical_interpolation.py @@ -1,9 +1,8 @@ #!/usr/bin/env python import sys -import nes +from numpy import nan, flip, cumsum, nanmean, empty, ndarray, ma, float64, array, interp, where from scipy.interpolate import interp1d -import numpy as np from copy import copy @@ -26,24 +25,24 @@ def add_4d_vertical_info(self, info_to_add): return None -def parse_extrapolate(extrapolate) -> tuple: +def __parse_extrapolate(extrapolate) -> tuple: """ - Parses the 'extrapolate' parameter and returns a tuple representing the extrapolation options. + Parses the "extrapolate" parameter and returns a tuple representing the extrapolation options. Parameters ---------- extrapolate : bool or tuple or None or number or NaN If bool: - - If True, both extrapolation options are set to 'extrapolate'. - - If False, extrapolation options are set to ('bottom', 'top'). + - If True, both extrapolation options are set to "extrapolate". + - If False, extrapolation options are set to ("bottom", "top"). If tuple: - The first element represents the extrapolation option for the lower bound. - The second element represents the extrapolation option for the upper bound. - If any element is bool: - - If True, it represents 'extrapolate'. + - If True, it represents "extrapolate". - If False: - - If it's the first element, it represents 'bottom'. - - If it's the second element, it represents 'top'. + - If it"s the first element, it represents "bottom". + - If it"s the second element, it represents "top". - If any element is None, it is replaced with numpy.nan. - Other numeric values are kept as they are. - If any element is NaN, it is kept as NaN. @@ -58,38 +57,38 @@ def parse_extrapolate(extrapolate) -> tuple: ------- tuple A tuple representing the extrapolation options. If the input is invalid, it returns - ('extrapolate', 'extrapolate'). + ("extrapolate", "extrapolate"). """ if isinstance(extrapolate, bool): if extrapolate: - extrapolate_options = ('extrapolate', 'extrapolate') + extrapolate_options = ("extrapolate", "extrapolate") else: - extrapolate_options = ('bottom', 'top') + extrapolate_options = ("bottom", "top") elif isinstance(extrapolate, tuple): extrapolate_options = [None, None] for i in range(len(extrapolate)): if isinstance(extrapolate[i], bool): if extrapolate[i]: - extrapolate_options[i] = 'extrapolate' + extrapolate_options[i] = "extrapolate" else: if i == 0: - extrapolate_options[i] = 'bottom' + extrapolate_options[i] = "bottom" else: - extrapolate_options[i] = 'top' + extrapolate_options[i] = "top" elif extrapolate[i] is None: - extrapolate_options[i] = np.nan + extrapolate_options[i] = nan else: extrapolate_options[i] = extrapolate[i] extrapolate_options = tuple(extrapolate_options) elif extrapolate is None: - extrapolate_options = ('bottom', 'top') + extrapolate_options = ("bottom", "top") else: extrapolate_options = (extrapolate, extrapolate) return extrapolate_options -def interpolate_vertical(self, new_levels, new_src_vertical=None, kind='linear', extrapolate_options=False, info=None, +def interpolate_vertical(self, new_levels, new_src_vertical=None, kind="linear", extrapolate_options=False, info=None, overwrite=False): """ Vertical interpolation. @@ -99,7 +98,7 @@ def interpolate_vertical(self, new_levels, new_src_vertical=None, kind='linear', self : Nes Source Nes object. new_levels : List - List of new vertical levels. + A List of new vertical levels. new_src_vertical : nes.Nes, str Nes object with the vertical information as variable or str with the path to the NetCDF file that contains the vertical data. @@ -107,16 +106,16 @@ def interpolate_vertical(self, new_levels, new_src_vertical=None, kind='linear', Vertical methods type. extrapolate_options : bool or tuple or None or number or NaN If bool: - - If True, both extrapolation options are set to 'extrapolate'. - - If False, extrapolation options are set to ('bottom', 'top'). + - If True, both extrapolation options are set to "extrapolate". + - If False, extrapolation options are set to ("bottom", "top"). If tuple: - The first element represents the extrapolation option for the lower bound. - The second element represents the extrapolation option for the upper bound. - If any element is bool: - - If True, it represents 'extrapolate'. + - If True, it represents "extrapolate". - If False: - - If it's the first element, it represents 'bottom'. - - If it's the second element, it represents 'top'. + - If it"s the first element, it represents "bottom". + - If it"s the second element, it represents "top". - If any element is None, it is replaced with numpy.nan. - Other numeric values are kept as they are. - If any element is NaN, it is kept as NaN. @@ -131,8 +130,11 @@ def interpolate_vertical(self, new_levels, new_src_vertical=None, kind='linear', overwrite: bool Indicates if you want to compute the vertical interpolation in the same object or not. """ - extrapolate_options = parse_extrapolate(extrapolate_options) - do_extrapolation = 'extrapolate' in extrapolate_options + src_levels_aux = None + fill_value = None + + extrapolate_options = __parse_extrapolate(extrapolate_options) + do_extrapolation = "extrapolate" in extrapolate_options if len(self.lev) == 1: raise RuntimeError("1D data cannot be vertically interpolated.") @@ -154,59 +156,59 @@ def interpolate_vertical(self, new_levels, new_src_vertical=None, kind='linear', # To use current level data current_level = True # Checking old order - src_levels = self.lev['data'] + src_levels = self.lev["data"] if src_levels[0] > src_levels[-1]: if not ascendant: - flip = False + do_flip = False else: - flip = True - src_levels = np.flip(src_levels) + do_flip = True + src_levels = flip(src_levels) else: if ascendant: - flip = False + do_flip = False else: - flip = True - src_levels = np.flip(src_levels) + do_flip = True + src_levels = flip(src_levels) else: current_level = False - src_levels = self.variables[self.vertical_var_name]['data'] - if self.vertical_var_name == 'layer_thickness': - src_levels = np.flip(np.cumsum(np.flip(src_levels, axis=1), axis=1)) + src_levels = self.variables[self.vertical_var_name]["data"] + if self.vertical_var_name == "layer_thickness": + src_levels = flip(cumsum(flip(src_levels, axis=1), axis=1)) else: - # src_levels = np.flip(src_levels, axis=1) + # src_levels = flip(src_levels, axis=1) pass # Checking old order - if np.nanmean(src_levels[:, 0, :, :]) > np.nanmean(src_levels[:, -1, :, :]): + if nanmean(src_levels[:, 0, :, :]) > nanmean(src_levels[:, -1, :, :]): if not ascendant: - flip = False + do_flip = False else: - flip = True - src_levels = np.flip(src_levels, axis=1) + do_flip = True + src_levels = flip(src_levels, axis=1) else: if ascendant: - flip = False + do_flip = False else: - flip = True - src_levels = np.flip(src_levels, axis=1) + do_flip = True + src_levels = flip(src_levels, axis=1) # Loop over variables for var_name in self.variables.keys(): - if self.variables[var_name]['data'] is None: + if self.variables[var_name]["data"] is None: # Load data if it is not loaded yet self.load(var_name) if var_name != self.vertical_var_name: - if flip: - self.variables[var_name]['data'] = np.flip(self.variables[var_name]['data'], axis=1) + if do_flip: + self.variables[var_name]["data"] = flip(self.variables[var_name]["data"], axis=1) if info and self.master: print("\t{var} vertical methods".format(var=var_name)) sys.stdout.flush() - nt, nz, ny, nx = self.variables[var_name]['data'].shape - dst_data = np.empty((nt, nz_new, ny, nx), dtype=self.variables[var_name]['data'].dtype) + nt, nz, ny, nx = self.variables[var_name]["data"].shape + dst_data = empty((nt, nz_new, ny, nx), dtype=self.variables[var_name]["data"].dtype) for t in range(nt): # if info and self.rank == self.size - 1: if self.info and self.master: - print('\t\t{3} time step {0} ({1}/{2}))'.format(self.time[t], t + 1, nt, var_name)) + print("\t\t{3} time step {0} ({1}/{2}))".format(self.time[t], t + 1, nt, var_name)) sys.stdout.flush() for j in range(ny): for i in range(nx): @@ -218,34 +220,34 @@ def interpolate_vertical(self, new_levels, new_src_vertical=None, kind='linear', curr_level_values = src_levels[t, :, j, i] try: # Check if all values are identical or masked - if ((isinstance(curr_level_values, np.ndarray) and + if ((isinstance(curr_level_values, ndarray) and (curr_level_values == curr_level_values[0]).all()) or - (isinstance(curr_level_values, np.ma.core.MaskedArray) and + (isinstance(curr_level_values, ma.core.MaskedArray) and curr_level_values.mask.all())): - kind = 'slinear' + kind = "slinear" else: - kind = kind # 'cubic' + kind = kind # "cubic" # Filtering filling values to extrapolation - fill_value = [np.nan, np.nan] - if 'bottom' in extrapolate_options: + fill_value = [nan, nan] + if "bottom" in extrapolate_options: if ascendant: - fill_value[0] = np.float64(self.variables[var_name]['data'][t, 0, j, i]) + fill_value[0] = float64(self.variables[var_name]["data"][t, 0, j, i]) else: - fill_value[0] = np.float64(self.variables[var_name]['data'][t, -1, j, i]) + fill_value[0] = float64(self.variables[var_name]["data"][t, -1, j, i]) else: fill_value[0] = extrapolate_options[0] - if 'top' in extrapolate_options: + if "top" in extrapolate_options: if ascendant: - fill_value[1] = np.float64(self.variables[var_name]['data'][t, -1, j, i]) + fill_value[1] = float64(self.variables[var_name]["data"][t, -1, j, i]) else: - fill_value[1] = np.float64(self.variables[var_name]['data'][t, 0, j, i]) + fill_value[1] = float64(self.variables[var_name]["data"][t, 0, j, i]) else: fill_value[1] = extrapolate_options[1] fill_value = tuple(fill_value) # We force the methods with float64 to avoid negative values - # We don't know why the negatives appears with float34 + # We don"t know why the negatives appears with float34 if current_level: # 1D vertical component src_levels_aux = src_levels @@ -253,72 +255,72 @@ def interpolate_vertical(self, new_levels, new_src_vertical=None, kind='linear', # 4D vertical component src_levels_aux = src_levels[t, :, j, i] - if kind == 'linear' and ascendant and not do_extrapolation: - dst_data[t, :, j, i] = np.array( - np.interp(new_levels, - np.array(src_levels_aux, dtype=np.float64), - np.array(self.variables[var_name]['data'][t, :, j, i], dtype=np.float64), - left=fill_value[0], right=fill_value[1]), - dtype=self.variables[var_name]['data'].dtype) + if kind == "linear" and ascendant and not do_extrapolation: + dst_data[t, :, j, i] = array( + interp(new_levels, + array(src_levels_aux, dtype=float64), + array(self.variables[var_name]["data"][t, :, j, i], dtype=float64), + left=fill_value[0], right=fill_value[1]), + dtype=self.variables[var_name]["data"].dtype) else: if not do_extrapolation: - dst_data[t, :, j, i] = np.array( - interp1d(np.array(src_levels_aux, dtype=np.float64), - np.array(self.variables[var_name]['data'][t, :, j, i], dtype=np.float64), + dst_data[t, :, j, i] = array( + interp1d(array(src_levels_aux, dtype=float64), + array(self.variables[var_name]["data"][t, :, j, i], dtype=float64), kind=kind, bounds_error=False, fill_value=fill_value)(new_levels), - dtype=self.variables[var_name]['data'].dtype) + dtype=self.variables[var_name]["data"].dtype) else: # If extrapolation first we need to extrapolate all (below & above) - dst_data[t, :, j, i] = np.array( - interp1d(np.array(src_levels_aux, dtype=np.float64), - np.array(self.variables[var_name]['data'][t, :, j, i], - dtype=np.float64), + dst_data[t, :, j, i] = array( + interp1d(array(src_levels_aux, dtype=float64), + array(self.variables[var_name]["data"][t, :, j, i], + dtype=float64), kind=kind, bounds_error=False, - fill_value='extrapolate')(new_levels), - dtype=self.variables[var_name]['data'].dtype) + fill_value="extrapolate")(new_levels), + dtype=self.variables[var_name]["data"].dtype) # Check values below the lower vertical level - if fill_value[0] != 'extrapolate': + if fill_value[0] != "extrapolate": if ascendant: - idx_bellow = np.where(new_levels < src_levels_aux[0]) + idx_bellow = where(new_levels < src_levels_aux[0]) else: - idx_bellow = np.where(new_levels > src_levels_aux[0]) + idx_bellow = where(new_levels > src_levels_aux[0]) dst_data[t, idx_bellow, j, i] = fill_value[0] # Check values above the upper vertical level - if fill_value[1] != 'extrapolate': + if fill_value[1] != "extrapolate": if ascendant: - idx_above = np.where(new_levels > src_levels_aux[-1]) + idx_above = where(new_levels > src_levels_aux[-1]) else: - idx_above = np.where(new_levels < src_levels_aux[-1]) + idx_above = where(new_levels < src_levels_aux[-1]) dst_data[t, idx_above, j, i] = fill_value[1] except Exception as e: print("time lat lon", t, j, i) print("***********************") print("LEVELS", src_levels_aux) - print("DATA", np.array(self.variables[var_name]['data'][t, :, j, i], dtype=np.float64)) + print("DATA", array(self.variables[var_name]["data"][t, :, j, i], dtype=float64)) print("METHOD", kind) print("FILL_VALUE", fill_value) print("+++++++++++++++++++++++") raise Exception(str(e)) # if level_array is not None: - # dst_data[t, :, j, i] = np.array(f(level_array), dtype=np.float32) + # dst_data[t, :, j, i] = array(f(level_array), dtype=float32) - self.variables[var_name]['data'] = copy(dst_data) - # print(self.variables[var_name]['data']) + self.variables[var_name]["data"] = copy(dst_data) + # print(self.variables[var_name]["data"]) # Update level information - new_lev_info = {'data': np.array(new_levels)} - if 'positive' in self._lev.keys(): + new_lev_info = {"data": array(new_levels)} + if "positive" in self.lev.keys(): # Vertical level direction if flip: self.reverse_level_direction() - new_lev_info['positive'] = self._lev['positive'] + new_lev_info["positive"] = self.lev["positive"] if self.vertical_var_name is not None: for var_attr, attr_info in self.variables[self.vertical_var_name].items(): - if var_attr not in ['data', 'dimensions', 'crs', 'grid_mapping']: + if var_attr not in ["data", "dimensions", "crs", "grid_mapping"]: new_lev_info[var_attr] = copy(attr_info) self.free_vars(self.vertical_var_name) self.vertical_var_name = None diff --git a/nes/nc_projections/__init__.py b/nes/nc_projections/__init__.py index fa530f9f0e1985c67066d46225d4f06f96545697..4839ec52dbf1b050f4f70db7b4c0e963459fbaf3 100644 --- a/nes/nc_projections/__init__.py +++ b/nes/nc_projections/__init__.py @@ -8,3 +8,8 @@ from .points_nes_providentia import PointsNesProvidentia from .lcc_nes import LCCNes from .mercator_nes import MercatorNes # from .raster_nes import RasterNes + +__all__ = [ + 'MercatorNes', 'Nes', 'LatLonNes', 'RotatedNes', 'RotatedNestedNes', 'PointsNes', 'PointsNesGHOST', + 'PointsNesProvidentia', 'LCCNes', +] diff --git a/nes/nc_projections/default_nes.py b/nes/nc_projections/default_nes.py index 243386379d9117fb6313710836051e7d9454a9ea..399968f0a0b0ecd40b6f9663c3ab13b1a7e12c6a 100644 --- a/nes/nc_projections/default_nes.py +++ b/nes/nc_projections/default_nes.py @@ -1,21 +1,21 @@ #!/usr/bin/env python import sys -import gc -import warnings -import numpy as np -import pandas as pd -from datetime import timedelta +from gc import collect +from warnings import warn +from numpy import (array, ndarray, abs, mean, diff, dstack, append, tile, empty, unique, stack, vstack, full, isnan, + flipud, nan, float32, float64, ma, generic, character, issubdtype, arange, newaxis, concatenate, + split, cumsum, zeros, column_stack) +from pandas import Index, concat +from geopandas import GeoDataFrame +from datetime import timedelta, datetime from netCDF4 import Dataset, num2date, date2num, stringtochar from mpi4py import MPI -from shapely.errors import TopologicalError -import geopandas as gpd from shapely.geometry import Polygon, Point from copy import deepcopy, copy -import datetime from dateutil.relativedelta import relativedelta -from typing import Union, List -import pyproj +from typing import Union, List, Dict, Any +from pyproj import Proj, Transformer from ..methods import vertical_interpolation, horizontal_interpolation, cell_measures, spatial_join from ..nes_formats import to_netcdf_cams_ra, to_netcdf_monarch, to_monarch_units, to_netcdf_cmaq, to_cmaq_units, \ to_netcdf_wrf_chem, to_wrf_chem_units @@ -23,10 +23,11 @@ from ..nes_formats import to_netcdf_cams_ra, to_netcdf_monarch, to_monarch_units class Nes(object): """ + A class to handle netCDF data with parallel processing capabilities using MPI. Attributes ---------- - comm : MPI.Communicator. + comm : MPI.Comm MPI communicator. rank : int MPI rank. @@ -44,61 +45,140 @@ class Nes(object): Number of hours to avoid from the last original values. dataset : Dataset netcdf4-python Dataset. - variables : dict - Variables information. - The variables are stored in a dictionary with the var_name as key and another dictionary with the information. - The information dictionary contains the 'data' key with None (if the variable is not loaded) or the array values - and the other keys are the variable attributes or description. - _time : List + variables : Dict[str, Dict[str, Any]] + Variables information. The dictionary structure is: + { + var_name: { + "data": ndarray or None, # Array values or None if the variable is not loaded. + attr_name: attr_value, # Variable attributes. + ... + }, + ... + } + _full_time : List[datetime] Complete list of original time step values. - _lev : dict - Vertical level dictionary with the complete 'data' key for all the values and the rest of the attributes. - _lat : dict - Latitudes dictionary with the complete 'data' key for all the values and the rest of the attributes. - _lon : dict - Longitudes dictionary with the complete 'data' key for all the values and the rest of the attributes. - _lat_bnds : None or dict - Latitude bounds dictionary with the complete 'data' key for the latitudinal boundaries of each grid and the rest of the attributes. - _lon_bnds : None or dict - Longitude bounds dictionary with the complete 'data' key for the longitudinal boundaries of each grid and the rest of the attributes. + _full_lev : Dict[str, array] + Vertical level dictionary with the complete "data" key for all the values and the rest of the attributes. + { + "data": ndarray, # Array of vertical level values. + attr_name: attr_value, # Vertical level attributes. + ... + } + _full_lat : dict + Latitudes dictionary with the complete "data" key for all the values and the rest of the attributes. + { + "data": ndarray, # Array of latitude values. + attr_name: attr_value, # Latitude attributes. + ... + } + _full_lon : dict + Longitudes dictionary with the complete "data" key for all the values and the rest of the attributes. + { + "data": ndarray, # Array of longitude values. + attr_name: attr_value, # Longitude attributes. + ... + } + _full_lat_bnds : dict + Latitude bounds dictionary with the complete "data" key for the latitudinal boundaries of each grid and the + rest of the attributes. + { + "data": ndarray, # Array of latitude bounds. + attr_name: attr_value, # Latitude bounds attributes. + ... + } + _full_lon_bnds : dict + Longitude bounds dictionary with the complete "data" key for the longitudinal boundaries of each grid and the + rest of the attributes. + { + "data": ndarray, # Array of longitude bounds. + attr_name: attr_value, # Longitude bounds attributes. + ... + } parallel_method : str - Parallel method to read/write. - Can be chosen any of the following axis to parallelize: 'T', 'Y' or 'X'. + Parallel method to read/write. Can be chosen from any of the following axes to parallelize: "T", "Y", or "X". read_axis_limits : dict - Dictionary with the 4D limits of the rank data to read. - t_min, t_max, z_min, z_max, y_min, y_max, x_min and x_max. + Dictionary with the 4D limits of the rank data to read. Structure: + { + "t_min": int, "t_max": int, # Time axis limits. + "z_min": int, "z_max": int, # Vertical axis limits. + "y_min": int, "y_max": int, # Latitudinal axis limits. + "x_min": int, "x_max": int, # Longitudinal axis limits. + } write_axis_limits : dict - Dictionary with the 4D limits of the rank data to write. - t_min, t_max, z_min, z_max, y_min, y_max, x_min and x_max. + Dictionary with the 4D limits of the rank data to write. Structure: + { + "t_min": int, "t_max": int, # Time axis limits. + "z_min": int, "z_max": int, # Vertical axis limits. + "y_min": int, "y_max": int, # Latitudinal axis limits. + "x_min": int, "x_max": int, # Longitudinal axis limits. + } time : List[datetime] List of time steps of the rank data. lev : dict - Vertical levels dictionary with the portion of 'data' corresponding to the rank values. + Vertical levels dictionary with the portion of "data" corresponding to the rank values. Structure: + { + "data": ndarray, # Array of vertical level values for the rank. + attr_name: attr_value, # Vertical level attributes. + ... + } lat : dict - Latitudes dictionary with the portion of 'data' corresponding to the rank values. + Latitudes dictionary with the portion of "data" corresponding to the rank values. Structure: + { + "data": ndarray, # Array of latitude values for the rank. + attr_name: attr_value, # Latitude attributes. + ... + } lon : dict - Longitudes dictionary with the portion of 'data' corresponding to the rank values. - lat_bnds : None or dict - Latitude bounds dictionary with the portion of 'data' for the latitudinal boundaries corresponding to the rank values. - lon_bnds : None or dict - Longitude bounds dictionary with the portion of 'data' for the longitudinal boundaries corresponding to the rank values. + Longitudes dictionary with the portion of "data" corresponding to the rank values. Structure: + { + "data": ndarray, # Array of longitude values for the rank. + attr_name: attr_value, # Longitude attributes. + ... + } + lat_bnds : dict + Latitude bounds dictionary with the portion of "data" for the latitudinal boundaries corresponding to the rank + values. + Structure: + { + "data": ndarray, # Array of latitude bounds for the rank. + attr_name: attr_value, # Latitude bounds attributes. + ... + } + lon_bnds : dict + Longitude bounds dictionary with the portion of "data" for the longitudinal boundaries corresponding to the + rank values. + Structure: + { + "data": ndarray, # Array of longitude bounds for the rank. + attr_name: attr_value, # Longitude bounds attributes. + ... + } global_attrs : dict - Global attributes with the attribute name as key and data as values. - _var_dim : None or tuple + Global attributes with the attribute name as key and data as values. Structure: + { + attr_name: attr_value, # Global attribute name and value. + ... + } + _var_dim : tuple Name of the Y and X dimensions for the variables. - _lat_dim : None or tuple + _lat_dim : tuple Name of the dimensions of the Latitude values. - _lon_dim : None or tuple + _lon_dim : tuple Name of the dimensions of the Longitude values. - projection : pyproj.Proj + projection : Proj Grid projection. projection_data : dict - Dictionary with the projection information. - + Dictionary with the projection information. Structure: + { + proj_param: proj_value, # Projection parameters. + ... + } """ - def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method='Y', - avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, - balanced=False, times=None, **kwargs): + def __init__(self, comm: Union[MPI.Comm, None] = None, path: Union[str, None] = None, info: bool = False, + dataset: Union[Dataset, None] = None, parallel_method: str = "Y", avoid_first_hours: int = 0, + avoid_last_hours: int = 0, first_level: int = 0, last_level: Union[int, None] = None, + create_nes: bool = False, balanced: bool = False, times: Union[List[datetime], None] = None, + **kwargs) -> None: """ Initialize the Nes class @@ -114,7 +194,7 @@ class Nes(object): NetCDF4-python Dataset to initialize the class. parallel_method : str Indicates the parallelization method that you want. Default over Y axis - accepted values: ['X', 'Y', 'T']. + accepted values: ["X", "Y", "T"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -162,12 +242,12 @@ class Nes(object): self.serial_nc = None # Place to store temporally the serial Nes instance # Get minor and major axes of Earth - self.earth_radius = self.get_earth_radius('WGS84') + self.earth_radius = self.get_earth_radius("WGS84") # Time resolution and climatology will be modified, if needed, during the time variable reading - self._time_resolution = 'hours' + self._time_resolution = "hours" self._climatology = False - self._climatology_var_name = 'climatology_bounds' # Default var_name but can be changed if the input is dif + self._climatology_var_name = "climatology_bounds" # Default var_name but can be changed if the input is dif # NetCDF object if create_nes: @@ -176,31 +256,33 @@ class Nes(object): # Set string length self.strlen = None - + # Initialize variables self.variables = {} - # Set projection - self._create_projection(**kwargs) + # Projection data This is duplicated due to if it is needed to create the object NES needs that info to + # create coordinates data. + self.projection_data = self._get_projection_data(create_nes, **kwargs) + self.projection = self._get_pyproj_projection() # Complete dimensions - self._time = times + self._full_time = times - self._time_bnds = self.__get_time_bnds(create_nes) - self._lat_bnds, self._lon_bnds = self.__get_coordinates_bnds(create_nes) - self._lev = {'data': np.array([0]), - 'units': '', - 'positive': 'up'} - self._lat, self._lon = self._create_centre_coordinates(**kwargs) + self._full_time_bnds = self.__get_time_bnds(create_nes) + self._full_lat_bnds, self._full_lon_bnds = self.__get_coordinates_bnds(create_nes) + self._full_lev = {"data": array([0]), "units": "", "positive": "up"} + self._full_lat, self._full_lon = self._create_centre_coordinates(**kwargs) # Set axis limits for parallel reading - self.read_axis_limits = self.get_read_axis_limits() + self.read_axis_limits = self._get_read_axis_limits() + self.write_axis_limits = self._get_write_axis_limits() # Dimensions screening - self.time = self._time[self.read_axis_limits['t_min']:self.read_axis_limits['t_max']] - self.time_bnds = self._time_bnds - self.lev = deepcopy(self._lev) - self.lat_bnds, self.lon_bnds = self._lat_bnds, self._lon_bnds + self.time = self.get_full_times()[self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"]] + self.time_bnds = self.get_full_time_bnds() + self.lev = self.get_full_levels() + self.lat_bnds = self.get_full_latitudes_boundaries() + self.lon_bnds = self.get_full_longitudes_boundaries() # Cell measures screening self.cell_measures = self.__get_cell_measures(create_nes) @@ -212,49 +294,49 @@ class Nes(object): if dataset is not None: self.dataset = dataset elif self.__ini_path is not None: - self.open() + self._open() # Get string length self.strlen = self._get_strlen() # Lazy variables self.variables = self._get_lazy_variables() - - # Get projection - self._get_projection() # Complete dimensions - self._time = self.__get_time() - self._time_bnds = self.__get_time_bnds() - self._lev = self._get_coordinate_dimension(['lev', 'level', 'lm', 'plev']) - self._lat = self._get_coordinate_dimension(['lat', 'latitude', 'latitudes']) - self._lon = self._get_coordinate_dimension(['lon', 'longitude', 'longitudes']) - self._lat_bnds, self._lon_bnds = self.__get_coordinates_bnds() + self._full_time = self.__get_time() + self._full_time_bnds = self.__get_time_bnds() + self._full_lev = self._get_coordinate_dimension(["lev", "level", "lm", "plev"]) + self._full_lat = self._get_coordinate_dimension(["lat", "latitude", "latitudes"]) + self._full_lon = self._get_coordinate_dimension(["lon", "longitude", "longitudes"]) + self._full_lat_bnds, self._full_lon_bnds = self.__get_coordinates_bnds() # Complete cell measures self._cell_measures = self.__get_cell_measures() # Set axis limits for parallel reading - self.read_axis_limits = self.get_read_axis_limits() + self.read_axis_limits = self._get_read_axis_limits() + # Set axis limits for parallel writing + self.write_axis_limits = self._get_write_axis_limits() # Dimensions screening - self.time = self._time[self.read_axis_limits['t_min']:self.read_axis_limits['t_max']] - self.time_bnds = self._time_bnds - self.lev = self._get_coordinate_values(self._lev, 'Z') - self.lat = self._get_coordinate_values(self._lat, 'Y') - self.lon = self._get_coordinate_values(self._lon, 'X') - self.lat_bnds = self._get_coordinate_values(self._lat_bnds, 'Y', bounds=True) - self.lon_bnds = self._get_coordinate_values(self._lon_bnds, 'X', bounds=True) + self.time = self.get_full_times()[self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"]] + self.time_bnds = self.get_full_time_bnds() + self.lev = self._get_coordinate_values(self.get_full_levels(), "Z") + self.lat = self._get_coordinate_values(self.get_full_latitudes(), "Y") + self.lon = self._get_coordinate_values(self.get_full_longitudes(), "X") + self.lat_bnds = self._get_coordinate_values(self.get_full_latitudes_boundaries(), "Y", bounds=True) + self.lon_bnds = self._get_coordinate_values(self.get_full_longitudes_boundaries(), "X", bounds=True) # Cell measures screening self.cell_measures = self._get_cell_measures_values(self._cell_measures) - # Set axis limits for parallel writing - self.write_axis_limits = self.get_write_axis_limits() - # Set NetCDF attributes self.global_attrs = self.__get_global_attributes() + # Projection data + self.projection_data = self._get_projection_data(create_nes, **kwargs) + self.projection = self._get_pyproj_projection() + # Writing options self.zip_lvl = 0 @@ -266,17 +348,31 @@ class Nes(object): self.vertical_var_name = None # Filtering (portion of the filter coordinates function) - idx = self.get_idx_intervals() - self._time = self._time[idx['idx_t_min']:idx['idx_t_max']] - self._lev['data'] = self._lev['data'][idx['idx_z_min']:idx['idx_z_max']] + idx = self._get_idx_intervals() + if self.master: + self.set_full_times(self._full_time[idx["idx_t_min"]:idx["idx_t_max"]]) + self._full_lev["data"] = self._full_lev["data"][idx["idx_z_min"]:idx["idx_z_max"]] self.hours_start = 0 self.hours_end = 0 self.last_level = None self.first_level = None + def __test_mpi__(self, num_test=None): + print(f"{self.rank} Barrier {num_test}") + sys.stdout.flush() + self.comm.Barrier() + if self.master: + data = 1 + else: + data = 0 + data = self.comm.bcast(data, root=0) + print(f"{self.rank} data {data}") + sys.stdout.flush() + return None + @staticmethod - def new(comm=None, path=None, info=False, dataset=None, parallel_method='Y', + def new(comm=None, path=None, info=False, dataset=None, parallel_method="Y", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, balanced=False, times=None, **kwargs): """ @@ -294,7 +390,7 @@ class Nes(object): NetCDF4-python Dataset to initialize the class. parallel_method : str Indicates the parallelization method that you want. Default over Y axis - accepted values: ['X', 'Y', 'T']. + accepted values: ["X", "Y", "T"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -328,13 +424,13 @@ class Nes(object): Max length of the string data """ - if 'strlen' in self.dataset.dimensions: - strlen = self.dataset.dimensions['strlen'].size + if "strlen" in self.dataset.dimensions: + strlen = self.dataset.dimensions["strlen"].size else: return None return strlen - + def set_strlen(self, strlen=75): """ Set the strlen @@ -361,30 +457,30 @@ class Nes(object): self.free_vars(list(self.variables.keys())) del self.variables del self.time - del self._time + del self._full_time del self.time_bnds - del self._time_bnds + del self._full_time_bnds del self.lev - del self._lev + del self._full_lev del self.lat - del self._lat + del self._full_lat del self.lon - del self._lon - del self._lat_bnds + del self._full_lon + del self._full_lat_bnds del self.lat_bnds - del self._lon_bnds + del self._full_lon_bnds del self.lon_bnds del self.strlen del self.shapefile for cell_measure in self.cell_measures.keys(): - if self.cell_measures[cell_measure]['data'] is not None: - del self.cell_measures[cell_measure]['data'] + if self.cell_measures[cell_measure]["data"] is not None: + del self.cell_measures[cell_measure]["data"] del self.cell_measures except (AttributeError, KeyError): pass del self - gc.collect() + collect() return None @@ -399,7 +495,7 @@ class Nes(object): """ d = self.__dict__ - state = {k: d[k] for k in d if k not in ['comm', 'variables', 'dataset', 'cell_measures']} + state = {k: d[k] for k in d if k not in ["comm", "variables", "dataset", "cell_measures"]} return state @@ -424,7 +520,7 @@ class Nes(object): Parameters ---------- other : Nes - Nes to be summed + A Nes to be summed Returns ------- @@ -437,7 +533,7 @@ class Nes(object): # Create New variable nessy.variables[var_name] = deepcopy(other.variables[var_name]) else: - nessy.variables[var_name]['data'] += other.variables[var_name]['data'] + nessy.variables[var_name]["data"] += other.variables[var_name]["data"] return nessy def __radd__(self, other): @@ -446,7 +542,7 @@ class Nes(object): else: return self.__add__(other) - def __getitem__(self, key: str) -> Union[np.array, None]: + def __getitem__(self, key: str) -> Union[array, None]: """ Retrieve the data associated with the specified key. @@ -457,17 +553,17 @@ class Nes(object): Returns ------- - Union[np.array, None] + Union[array, None] The data associated with the specified key, or None if the key does not exist. Notes ----- This method allows accessing data in the variables dictionary using - dictionary-like syntax, e.g., obj[key]['data']. + dictionary-like syntax, e.g., obj[key]["data"]. """ - return self.variables[key]['data'] + return self.variables[key]["data"] def copy(self, copy_vars: bool = False): """ @@ -497,28 +593,342 @@ class Nes(object): return nessy - def get_full_times(self): - return self._time + def get_full_times(self) -> List[datetime]: + """ + Retrieve the complete list of original time step values. + + Returns + ------- + List[datetime] + The complete list of original time step values from the netCDF data. + """ + if self.master: + data = self._full_time + else: + data = None + data = self.comm.bcast(data, root=0) + + if not isinstance(data, list): + data = list(data) + return data + + def get_full_time_bnds(self) -> List[datetime]: + """ + Retrieve the complete list of original time step boundaries. + + Returns + ------- + List[datetime] + The complete list of original time step boundary values from the netCDF data. + """ + data = self.comm.bcast(self._full_time_bnds) + return data + + def get_full_levels(self) -> Dict[str, Any]: + """ + Retrieve the complete vertical level information. + + Returns + ------- + Dict[str, Any] + A dictionary containing the complete vertical level data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of vertical level values. + attr_name: attr_value, # Vertical level attributes. + ... + } + """ + data = self.comm.bcast(self._full_lev) + return data + + def get_full_latitudes(self) -> Dict[str, Any]: + """ + Retrieve the complete latitude information. + + Returns + ------- + Dict[str, Any] + A dictionary containing the complete latitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of latitude values. + attr_name: attr_value, # Latitude attributes. + ... + } + """ + data = self.comm.bcast(self._full_lat) + + return data + + def get_full_longitudes(self) -> Dict[str, Any]: + """ + Retrieve the complete longitude information. + + Returns + ------- + Dict[str, Any] + A dictionary containing the complete longitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of longitude values. + attr_name: attr_value, # Longitude attributes. + ... + } + """ + data = self.comm.bcast(self._full_lon) + return data + + def get_full_latitudes_boundaries(self) -> Dict[str, Any]: + """ + Retrieve the complete latitude boundaries information. + + Returns + ------- + Dict[str, Any] + A dictionary containing the complete latitude boundaries data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of latitude boundaries values. + attr_name: attr_value, # Latitude boundaries attributes. + ... + } + """ + data = self.comm.bcast(self._full_lat_bnds) + return data + + def get_full_longitudes_boundaries(self) -> Dict[str, Any]: + """ + Retrieve the complete longitude boundaries information. + + Returns + ------- + Dict[str, Any] + A dictionary containing the complete longitude boundaries data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of longitude boundaries values. + attr_name: attr_value, # Longitude boundaries attributes. + ... + } + """ + data = self.comm.bcast(self._full_lon_bnds) + return data + + def set_full_times(self, data: List[datetime]) -> None: + """ + Set the complete list of original time step values. + + Parameters + ---------- + data : List[datetime] + The complete list of original time step values to set. + """ + if self.master: + self._full_time = data + return None + + def set_full_time_bnds(self, data: List[datetime]) -> None: + """ + Set the complete list of original time step boundaries. + + Parameters + ---------- + data : List[datetime] + The complete list of original time step boundary values to set. + """ + if self.master: + self._full_time_bnds = data + return None + + def set_full_levels(self, data: Dict[str, Any]) -> None: + """ + Set the complete vertical level information. + + Parameters + ---------- + data : Dict[str, Any] + A dictionary containing the complete vertical level data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of vertical level values. + attr_name: attr_value, # Vertical level attributes. + ... + } + """ + if self.master: + self._full_lev = data + return None + + def set_full_latitudes(self, data: Dict[str, Any]) -> None: + """ + Set the complete latitude information. + + Parameters + ---------- + data : Dict[str, Any] + A dictionary containing the complete latitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of latitude values. + attr_name: attr_value, # Latitude attributes. + ... + } + """ + if self.master: + self._full_lat = data + return None + + def set_full_longitudes(self, data: Dict[str, Any]) -> None: + """ + Set the complete longitude information. + + Parameters + ---------- + data : Dict[str, Any] + A dictionary containing the complete longitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of longitude values. + attr_name: attr_value, # Longitude attributes. + ... + } + """ + if self.master: + self._full_lon = data + return None + + def set_full_latitudes_boundaries(self, data: Dict[str, Any]) -> None: + """ + Set the complete latitude boundaries information. + + Parameters + ---------- + data : Dict[str, Any] + A dictionary containing the complete latitude boundaries data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of latitude boundaries values. + attr_name: attr_value, # Latitude boundaries attributes. + ... + } + """ + if self.master: + self._full_lat_bnds = data + return None + + def set_full_longitudes_boundaries(self, data: Dict[str, Any]) -> None: + """ + Set the complete longitude boundaries information. + + Parameters + ---------- + data : Dict[str, Any] + A dictionary containing the complete longitude boundaries data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of longitude boundaries values. + attr_name: attr_value, # Longitude boundaries attributes. + ... + } + """ + if self.master: + self._full_lon_bnds = data + + return None - def get_full_levels(self): - return self._lev + def get_fids(self, use_read=False): + """ + Obtain the FIDs in a 2D format. + + Parameters + ---------- + use_read : bool + Indicate if you want to use the read_axis_limits + + Returns + ------- + array + 2D array with the FID data. + """ + if self.master: + fids = arange(self._full_lat["data"].shape[0] * self._full_lon["data"].shape[-1]) + fids = fids.reshape((self._full_lat["data"].shape[0], self._full_lon["data"].shape[-1])) + else: + fids = None + fids = self.comm.bcast(fids) + + if use_read: + fids = fids[self.read_axis_limits["y_min"]:self.read_axis_limits["y_max"], + self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] + else: + fids = fids[self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] + + return fids + + def get_full_shape(self): + """ + Obtain the Full 2D shape of tha data + + Returns + ------- + tuple + 2D shape of tha data. + """ + if self.master: + shape = (self._full_lat["data"].shape[0], self._full_lon["data"].shape[-1]) + else: + shape = None + shape = self.comm.bcast(shape) + + return shape def set_level_direction(self, new_direction): - if new_direction not in ['up', 'down']: - raise ValueError("Level direction mus be up or down. '{0}' is not a valid option".format(new_direction)) - self._lev['positive'] = new_direction - self.lev['positive'] = new_direction + """ + Set the direction of the vertical level values. + + Parameters + ---------- + new_direction : str + The new direction for the vertical levels. Must be either "up" or "down". + + Returns + ------- + bool + True if the direction was set successfully. + + Raises + ------ + ValueError + If `new_direction` is not "up" or "down". + """ + if new_direction not in ["up", "down"]: + raise ValueError(f"Level direction mus be up or down. '{new_direction}' is not a valid option") + if self.master: + self._full_lev["positive"] = new_direction + self.lev["positive"] = new_direction return True def reverse_level_direction(self): - if 'positive' in self._lev.keys(): - if self._lev['positive'] == 'up': - self._lev['positive'] = 'down' - self.lev['positive'] = 'down' + """ + Reverse the current direction of the vertical level values. + + Returns + ------- + bool + True if the direction was reversed successfully. + """ + if "positive" in self.lev.keys(): + if self.lev["positive"] == "up": + if self.master: + self._full_lev["positive"] = "down" + self.lev["positive"] = "down" else: - self._lev['positive'] = 'up' - self.lev['positive'] = 'up' + if self.master: + self._full_lev["positive"] = "up" + self.lev["positive"] = "up" return True def clear_communicator(self): @@ -548,18 +958,43 @@ class Nes(object): self.master = self.rank == 0 self.size = self.comm.Get_size() - self.read_axis_limits = self.get_read_axis_limits() - self.write_axis_limits = self.get_write_axis_limits() + self.read_axis_limits = self._get_read_axis_limits() + self.write_axis_limits = self._get_write_axis_limits() return None def set_climatology(self, is_climatology): + """ + Set whether the dataset represents climatological data. + + Parameters + ---------- + is_climatology : bool + A boolean indicating if the dataset represents climatological data. + + Returns + ------- + None + + Raises + ------ + TypeError + If `is_climatology` is not a boolean. + """ if not isinstance(is_climatology, bool): raise TypeError("Only boolean values are accepted") self._climatology = is_climatology return None def get_climatology(self): + """ + Get whether the dataset represents climatological data. + + Returns + ------- + bool + True if the dataset represents climatological data, False otherwise. + """ return self._climatology def set_levels(self, levels): @@ -571,8 +1006,7 @@ class Nes(object): levels : dict Dictionary with the new level information to be set. """ - - self._lev = deepcopy(levels) + self.set_full_levels(deepcopy(levels)) self.lev = deepcopy(levels) return None @@ -586,9 +1020,9 @@ class Nes(object): time_list : List[datetime] List of time steps """ - if self.parallel_method == 'T': + if self.parallel_method == "T": raise TypeError("Cannot set time on a 'T' parallel method") - self._time = deepcopy(time_list) + self.set_full_times(deepcopy(time_list)) self.time = deepcopy(time_list) return None @@ -600,51 +1034,69 @@ class Nes(object): Parameters ---------- time_bnds : List - List with the new time bounds information to be set. + AList with the new time bounds information to be set. """ correct_format = True - for time_bnd in np.array(time_bnds).flatten(): - if not isinstance(time_bnd, datetime.datetime): + for time_bnd in array(time_bnds).flatten(): + if not isinstance(time_bnd, datetime): print("{0} is not a datetime object".format(time_bnd)) correct_format = False if correct_format: - if len(self._time) == len(time_bnds): - self._time_bnds = deepcopy(time_bnds) + if len(self.get_full_times()) == len(time_bnds): + self.set_full_time_bnds(deepcopy(time_bnds)) self.time_bnds = deepcopy(time_bnds) else: msg = "WARNING!!! " msg += "The given time bounds list has a different length than the time array. " - msg += "(time:{0}, bnds:{1}). Time bounds will not be set.".format(len(self._time), len(time_bnds)) - warnings.warn(msg) + msg += "(time:{0}, bnds:{1}). Time bounds will not be set.".format(len(self.time), len(time_bnds)) + warn(msg) sys.stderr.flush() else: - msg = 'WARNING!!! ' - msg += 'There is at least one element in the time bounds to be set that is not a datetime object. ' - msg += 'Time bounds will not be set.' - warnings.warn(msg) + msg = "WARNING!!! " + msg += "There is at least one element in the time bounds to be set that is not a datetime object. " + msg += "Time bounds will not be set." + warn(msg) sys.stderr.flush() return None def set_time_resolution(self, new_resolution): - accepted_resolutions = ['second', 'seconds', 'minute', 'minutes', 'hour', 'hours', 'day', 'days'] + """ + Set the time resolution for the dataset. + + Parameters + ---------- + new_resolution : str + The new time resolution. Accepted values are "second", "seconds", "minute", "minutes", + "hour", "hours", "day", "days". + + Returns + ------- + bool + True if the time resolution was set successfully. + + Raises + ------ + ValueError + If `new_resolution` is not one of the accepted values. + """ + accepted_resolutions = ["second", "seconds", "minute", "minutes", "hour", "hours", "day", "days"] if new_resolution in accepted_resolutions: self._time_resolution = new_resolution else: - raise ValueError("Time resolution '{0}' is not accepted. Use one of this: {1}".format( - new_resolution, accepted_resolutions)) + raise ValueError(f"Time resolution '{new_resolution}' is not accepted. " + + f"Use one of this: {accepted_resolutions}") return True - @staticmethod - def create_single_spatial_bounds(coordinates, inc, spatial_nv=2, inverse=False): + def _create_single_spatial_bounds(coordinates, inc, spatial_nv=2, inverse=False): """ Calculate the vertices coordinates. Parameters ---------- - coordinates : np.array + coordinates : array Coordinates in degrees (latitude or longitude). inc : float Increment between centre values. @@ -655,8 +1107,8 @@ class Nes(object): Returns ---------- - bounds : np.array - Array with as many elements as vertices for each value of coords. + bounds : array + An Array with as many elements as vertices for each value of coords. """ # Create new arrays moving the centres half increment less and more. @@ -666,17 +1118,17 @@ class Nes(object): # Defining the number of corners needed. 2 to regular grids and 4 for irregular ones. if spatial_nv == 2: # Create an array of N arrays of 2 elements to store the floor and the ceil values for each cell - bounds = np.dstack((coords_left, coords_right)) + bounds = dstack((coords_left, coords_right)) bounds = bounds.reshape((len(coordinates), spatial_nv)) elif spatial_nv == 4: # Create an array of N arrays of 4 elements to store the corner values for each cell # It can be stored in clockwise starting form the left-top element, or in inverse mode. if inverse: - bounds = np.dstack((coords_left, coords_left, coords_right, coords_right)) + bounds = dstack((coords_left, coords_left, coords_right, coords_right)) else: - bounds = np.dstack((coords_left, coords_right, coords_right, coords_left)) + bounds = dstack((coords_left, coords_right, coords_right, coords_left)) else: - raise ValueError('The number of vertices of the boundaries must be 2 or 4.') + raise ValueError("The number of vertices of the boundaries must be 2 or 4.") return bounds @@ -684,18 +1136,21 @@ class Nes(object): """ Calculate longitude and latitude bounds and set them. """ + # Latitudes + full_lat = self.get_full_latitudes() + inc_lat = abs(mean(diff(full_lat["data"]))) + lat_bnds = self._create_single_spatial_bounds(full_lat["data"], inc_lat, spatial_nv=2) - inc_lat = np.abs(np.mean(np.diff(self._lat['data']))) - lat_bnds = self.create_single_spatial_bounds(self._lat['data'], inc_lat, spatial_nv=2) + self.set_full_latitudes_boundaries({"data": deepcopy(lat_bnds)}) + self.lat_bnds = {"data": lat_bnds[self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], :]} - self._lat_bnds = {'data': deepcopy(lat_bnds)} - self.lat_bnds = {'data': lat_bnds[self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], :]} - - inc_lon = np.abs(np.mean(np.diff(self._lon['data']))) - lon_bnds = self.create_single_spatial_bounds(self._lon['data'], inc_lon, spatial_nv=2) + # Longitudes + full_lon = self.get_full_longitudes() + inc_lon = abs(mean(diff(full_lon["data"]))) + lon_bnds = self._create_single_spatial_bounds(full_lon["data"], inc_lon, spatial_nv=2) - self._lon_bnds = {'data': deepcopy(lon_bnds)} - self.lon_bnds = {'data': lon_bnds[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], :]} + self.set_full_longitudes_boundaries({"data": deepcopy(lon_bnds)}) + self.lon_bnds = {"data": lon_bnds[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], :]} return None @@ -717,29 +1172,29 @@ class Nes(object): if self.lat_bnds is None: self.create_spatial_bounds() - if self.lat_bnds['data'].shape[-1] == 2: + if self.lat_bnds["data"].shape[-1] == 2: # get the lat_b and lon_b first rows - lat_b_0 = np.append(self.lat_bnds['data'][:, 0], self.lat_bnds['data'][-1, -1]) - lon_b_0 = np.append(self.lon_bnds['data'][:, 0], self.lon_bnds['data'][-1, -1]) + lat_b_0 = append(self.lat_bnds["data"][:, 0], self.lat_bnds["data"][-1, -1]) + lon_b_0 = append(self.lon_bnds["data"][:, 0], self.lon_bnds["data"][-1, -1]) # expand lat_band lon_b in 2D - lat_bnds_mesh = np.tile(lat_b_0, (len(self.lon['data']) + 1, 1)).transpose() - lon_bnds_mesh = np.tile(lon_b_0, (len(self.lat['data']) + 1, 1)) + lat_bnds_mesh = tile(lat_b_0, (len(self.lon["data"]) + 1, 1)).transpose() + lon_bnds_mesh = tile(lon_b_0, (len(self.lat["data"]) + 1, 1)) - elif self.lat_bnds['data'].shape[-1] == 4: + elif self.lat_bnds["data"].shape[-1] == 4: # Irregular quadrilateral polygon cell definition - lat_bnds_mesh = np.empty((self.lat['data'].shape[0] + 1, self.lat['data'].shape[1] + 1)) - lat_bnds_mesh[:-1, :-1] = self.lat_bnds['data'][:, :, 0] - lat_bnds_mesh[:-1, 1:] = self.lat_bnds['data'][:, :, 1] - lat_bnds_mesh[1:, 1:] = self.lat_bnds['data'][:, :, 2] - lat_bnds_mesh[1:, :-1] = self.lat_bnds['data'][:, :, 3] - - lon_bnds_mesh = np.empty((self.lat['data'].shape[0] + 1, self.lat['data'].shape[1] + 1)) - lon_bnds_mesh[:-1, :-1] = self.lon_bnds['data'][:, :, 0] - lon_bnds_mesh[:-1, 1:] = self.lon_bnds['data'][:, :, 1] - lon_bnds_mesh[1:, 1:] = self.lon_bnds['data'][:, :, 2] - lon_bnds_mesh[1:, :-1] = self.lon_bnds['data'][:, :, 3] + lat_bnds_mesh = empty((self.lat["data"].shape[0] + 1, self.lat["data"].shape[1] + 1)) + lat_bnds_mesh[:-1, :-1] = self.lat_bnds["data"][:, :, 0] + lat_bnds_mesh[:-1, 1:] = self.lat_bnds["data"][:, :, 1] + lat_bnds_mesh[1:, 1:] = self.lat_bnds["data"][:, :, 2] + lat_bnds_mesh[1:, :-1] = self.lat_bnds["data"][:, :, 3] + + lon_bnds_mesh = empty((self.lat["data"].shape[0] + 1, self.lat["data"].shape[1] + 1)) + lon_bnds_mesh[:-1, :-1] = self.lon_bnds["data"][:, :, 0] + lon_bnds_mesh[:-1, 1:] = self.lon_bnds["data"][:, :, 1] + lon_bnds_mesh[1:, 1:] = self.lon_bnds["data"][:, :, 2] + lon_bnds_mesh[1:, :-1] = self.lon_bnds["data"][:, :, 3] else: - raise RuntimeError("Invalid number of vertices: {0}".format(self.lat_bnds['data'].shape[-1])) + raise RuntimeError("Invalid number of vertices: {0}".format(self.lat_bnds["data"].shape[-1])) return lon_bnds_mesh, lat_bnds_mesh @@ -759,10 +1214,10 @@ class Nes(object): if self.variables is not None: for var_name in var_list: if var_name in self.variables: - if 'data' in self.variables[var_name].keys(): - del self.variables[var_name]['data'] + if "data" in self.variables[var_name].keys(): + del self.variables[var_name]["data"] del self.variables[var_name] - gc.collect() + collect() return None @@ -785,6 +1240,7 @@ class Nes(object): return None + @property def get_time_interval(self): """ Calculate the interrval of hours between time steps. @@ -794,30 +1250,32 @@ class Nes(object): int Number of hours between time steps. """ + if self.master: + time_interval = self._full_time[1] - self._full_time[0] + time_interval = int(time_interval.seconds // 3600) + else: + time_interval = None - time_interval = self._time[1] - self._time[0] - time_interval = int(time_interval.seconds // 3600) - - return time_interval + return self.comm.bcast(time_interval) - def sel_time(self, time, copy=False): + def sel_time(self, time, inplace=True): """ To select only one time step. Parameters ---------- - time : datetime.datetime + time : datetime Time stamp to select. - copy : bool - Indicates if you want a copy with the selected time step (True) or to modify te existing one (False). + inplace : bool + Indicates if you want a copy with the selected time step (False) or to modify te existing one (True). Returns ------- Nes - Nes object with the data (and metadata) of the selected time step. + A Nes object with the data (and metadata) of the selected time step. """ - if copy: + if not inplace: aux_nessy = self.copy(copy_vars=False) aux_nessy.comm = self.comm else: @@ -829,33 +1287,69 @@ class Nes(object): idx_time = aux_nessy.time.index(time) aux_nessy.time = [self.time[idx_time]] - aux_nessy._time = aux_nessy.time + aux_nessy._full_time = aux_nessy.time for var_name, var_info in self.variables.items(): if copy: aux_nessy.variables[var_name] = {} for att_name, att_value in var_info.items(): - if att_name == 'data': + if att_name == "data": if att_value is None: raise ValueError("{} data not loaded".format(var_name)) aux_nessy.variables[var_name][att_name] = att_value[[idx_time]] else: aux_nessy.variables[var_name][att_name] = att_value else: - aux_nessy.variables[var_name]['data'] = aux_nessy.variables[var_name]['data'][[idx_time]] + aux_nessy.variables[var_name]["data"] = aux_nessy.variables[var_name]["data"][[idx_time]] return aux_nessy def sel(self, hours_start=None, time_min=None, hours_end=None, time_max=None, lev_min=None, lev_max=None, lat_min=None, lat_max=None, lon_min=None, lon_max=None): """ - Select a slice of time, lev, lat or lon given a minimum and maximum limits. - """ + Select a slice of time, vertical level, latitude, or longitude given minimum and maximum limits. + Parameters + ---------- + hours_start : int, optional + The number of hours from the start to begin the selection. + time_min : datetime, optional + The minimum datetime for the time selection. Mutually exclusive with `hours_start`. + hours_end : int, optional + The number of hours from the end to end the selection. + time_max : datetime, optional + The maximum datetime for the time selection. Mutually exclusive with `hours_end`. + lev_min : int, optional + The minimum vertical level index for the selection. + lev_max : int, optional + The maximum vertical level index for the selection. + lat_min : float, optional + The minimum latitude for the selection. + lat_max : float, optional + The maximum latitude for the selection. + lon_min : float, optional + The minimum longitude for the selection. + lon_max : float, optional + The maximum longitude for the selection. + + Returns + ------- + None + + Raises + ------ + ValueError + If any variables are already loaded or if mutually exclusive parameters are both provided. + + Notes + ----- + This method updates the selection criteria for the dataset and recalculates the read and write axis limits + accordingly. It also updates the time, level, latitude, and longitude slices based on the new criteria. + """ + full_time = self.get_full_times() loaded_vars = False for var_info in self.variables.values(): - if var_info['data'] is not None: + if var_info["data"] is not None: loaded_vars = True - # var_info['data'] = None if loaded_vars: raise ValueError("Some variables have been loaded. Use select function before load.") @@ -865,10 +1359,10 @@ class Nes(object): raise ValueError("Choose to select by hours_start or time_min but not both") self.hours_start = hours_start elif time_min is not None: - if time_min <= self._time[0]: + if time_min <= full_time[0]: self.hours_start = 0 else: - self.hours_start = int((time_min - self._time[0]).total_seconds() // 3600) + self.hours_start = int((time_min - full_time[0]).total_seconds() // 3600) # Last time filter if hours_end is not None: @@ -876,10 +1370,10 @@ class Nes(object): raise ValueError("Choose to select by hours_end or time_max but not both") self.hours_end = hours_end elif time_max is not None: - if time_max >= self._time[-1]: + if time_max >= full_time[-1]: self.hours_end = 0 else: - self.hours_end = int((self._time[-1] - time_max).total_seconds() // 3600) + self.hours_end = int((full_time[-1] - time_max).total_seconds() // 3600) # Level filter self.first_level = lev_min @@ -892,56 +1386,59 @@ class Nes(object): self.lon_max = lon_max # New axis limits - self.read_axis_limits = self.get_read_axis_limits() + self.read_axis_limits = self._get_read_axis_limits() # Dimensions screening - self.time = self._time[self.read_axis_limits['t_min']:self.read_axis_limits['t_max']] - self.time_bnds = self._time_bnds - self.lev = self._get_coordinate_values(self._lev, 'Z') - self.lat = self._get_coordinate_values(self._lat, 'Y') - self.lon = self._get_coordinate_values(self._lon, 'X') + self.time = self.get_full_times()[self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"]] + self.time_bnds = self.get_full_time_bnds() + self.lev = self._get_coordinate_values(self.get_full_levels(), "Z") + self.lat = self._get_coordinate_values(self.get_full_latitudes(), "Y") + self.lon = self._get_coordinate_values(self.get_full_longitudes(), "X") - self.lat_bnds = self._get_coordinate_values(self._lat_bnds, 'Y', bounds=True) - self.lon_bnds = self._get_coordinate_values(self._lon_bnds, 'X', bounds=True) + self.lat_bnds = self._get_coordinate_values(self.get_full_latitudes_boundaries(), "Y", bounds=True) + self.lon_bnds = self._get_coordinate_values(self.get_full_longitudes_boundaries(), "X", bounds=True) # Filter dimensions - self.filter_coordinates_selection() + self._filter_coordinates_selection() # Removing complete coordinates - self.write_axis_limits = self.get_write_axis_limits() + self.write_axis_limits = self._get_write_axis_limits() return None - def filter_coordinates_selection(self): + def _filter_coordinates_selection(self): """ Use the selection limits to filter time, lev, lat, lon, lon_bnds and lat_bnds. """ - idx = self.get_idx_intervals() - - self._time = self._time[idx['idx_t_min']:idx['idx_t_max']] - self._lev['data'] = self._lev['data'][idx['idx_z_min']:idx['idx_z_max']] - - if len(self._lat['data'].shape) == 1: - # Regular projection - self._lat['data'] = self._lat['data'][idx['idx_y_min']:idx['idx_y_max']] - self._lon['data'] = self._lon['data'][idx['idx_x_min']:idx['idx_x_max']] + idx = self._get_idx_intervals() - if self._lat_bnds is not None: - self._lat_bnds['data'] = self._lat_bnds['data'][idx['idx_y_min']:idx['idx_y_max'], :] - if self._lon_bnds is not None: - self._lon_bnds['data'] = self._lon_bnds['data'][idx['idx_x_min']:idx['idx_x_max'], :] - else: - # Irregular projections - self._lat['data'] = self._lat['data'][idx['idx_y_min']:idx['idx_y_max'], idx['idx_x_min']:idx['idx_x_max']] - self._lon['data'] = self._lon['data'][idx['idx_y_min']:idx['idx_y_max'], idx['idx_x_min']:idx['idx_x_max']] - - if self._lat_bnds is not None: - self._lat_bnds['data'] = self._lat_bnds['data'][idx['idx_y_min']:idx['idx_y_max'], - idx['idx_x_min']:idx['idx_x_max'], :] - if self._lon_bnds is not None: - self._lon_bnds['data'] = self._lon_bnds['data'][idx['idx_y_min']:idx['idx_y_max'], - idx['idx_x_min']:idx['idx_x_max'], :] + if self.master: + self._full_time = self._full_time[idx["idx_t_min"]:idx["idx_t_max"]] + self._full_lev["data"] = self._full_lev["data"][idx["idx_z_min"]:idx["idx_z_max"]] + + if len(self._full_lat["data"].shape) == 1: + # Regular projection + self._full_lat["data"] = self._full_lat["data"][idx["idx_y_min"]:idx["idx_y_max"]] + self._full_lon["data"] = self._full_lon["data"][idx["idx_x_min"]:idx["idx_x_max"]] + + if self._full_lat_bnds is not None: + self._full_lat_bnds["data"] = self._full_lat_bnds["data"][idx["idx_y_min"]:idx["idx_y_max"], :] + if self._full_lon_bnds is not None: + self._full_lon_bnds["data"] = self._full_lon_bnds["data"][idx["idx_x_min"]:idx["idx_x_max"], :] + else: + # Irregular projections + self._full_lat["data"] = self._full_lat["data"][idx["idx_y_min"]:idx["idx_y_max"], + idx["idx_x_min"]:idx["idx_x_max"]] + self._full_lon["data"] = self._full_lon["data"][idx["idx_y_min"]:idx["idx_y_max"], + idx["idx_x_min"]:idx["idx_x_max"]] + + if self._full_lat_bnds is not None: + self._full_lat_bnds["data"] = self._full_lat_bnds["data"][idx["idx_y_min"]:idx["idx_y_max"], + idx["idx_x_min"]:idx["idx_x_max"], :] + if self._full_lon_bnds is not None: + self._full_lon_bnds["data"] = self._full_lon_bnds["data"][idx["idx_y_min"]:idx["idx_y_max"], + idx["idx_x_min"]:idx["idx_x_max"], :] self.hours_start = 0 self.hours_end = 0 @@ -954,21 +1451,30 @@ class Nes(object): return None - def _get_projection(self): + def _get_projection_data(self, create_nes, **kwargs): """ - Must be implemented on inner class. + Retrieves projection data based on grid details. + + Parameters + ---------- + create_nes : bool + Flag indicating whether to create new object (True) or use existing (False). + **kwargs : dict + Additional keyword arguments for specifying projection details. """ - return None - - def _create_projection(self, **kwargs): + raise NotImplementedError("Must be implemented on inner class.") + + @staticmethod + def _get_pyproj_projection(): """ - Must be implemented on inner class. + Retrieves Pyproj projection data based on grid details. + """ - return None + raise NotImplementedError("Must be implemented on inner class.") - def get_idx_intervals(self): + def _get_idx_intervals(self): """ Calculate the index intervals @@ -977,49 +1483,51 @@ class Nes(object): dict Dictionary with the index intervals """ - idx = {'idx_t_min': self.get_time_id(self.hours_start, first=True), - 'idx_t_max': self.get_time_id(self.hours_end, first=False), - 'idx_z_min': self.first_level, - 'idx_z_max': self.last_level} + full_lat = self.get_full_latitudes() + full_lon = self.get_full_longitudes() + idx = {"idx_t_min": self._get_time_id(self.hours_start, first=True), + "idx_t_max": self._get_time_id(self.hours_end, first=False), + "idx_z_min": self.first_level, + "idx_z_max": self.last_level} # Axis Y if self.lat_min is None: - idx['idx_y_min'] = 0 + idx["idx_y_min"] = 0 else: - idx['idx_y_min'] = self.get_coordinate_id(self._lat['data'], self.lat_min, axis=0) + idx["idx_y_min"] = self._get_coordinate_id(full_lat["data"], self.lat_min, axis=0) if self.lat_max is None: - idx['idx_y_max'] = self._lat['data'].shape[0] + idx["idx_y_max"] = full_lat["data"].shape[0] else: - idx['idx_y_max'] = self.get_coordinate_id(self._lat['data'], self.lat_max, axis=0) + 1 + idx["idx_y_max"] = self._get_coordinate_id(full_lat["data"], self.lat_max, axis=0) + 1 - if idx['idx_y_min'] > idx['idx_y_max']: - idx_aux = copy(idx['idx_y_min']) - idx['idx_y_min'] = idx['idx_y_max'] - idx['idx_y_max'] = idx_aux + if idx["idx_y_min"] > idx["idx_y_max"]: + idx_aux = copy(idx["idx_y_min"]) + idx["idx_y_min"] = idx["idx_y_max"] + idx["idx_y_max"] = idx_aux # Axis X if self.lon_min is None: - idx['idx_x_min'] = 0 + idx["idx_x_min"] = 0 else: - if len(self._lon['data'].shape) == 1: + if len(full_lon["data"].shape) == 1: axis = 0 else: axis = 1 - idx['idx_x_min'] = self.get_coordinate_id(self._lon['data'], self.lon_min, axis=axis) + idx["idx_x_min"] = self._get_coordinate_id(full_lon["data"], self.lon_min, axis=axis) if self.lon_max is None: - idx['idx_x_max'] = self._lon['data'].shape[-1] + idx["idx_x_max"] = full_lon["data"].shape[-1] else: - if len(self._lon['data'].shape) == 1: + if len(full_lon["data"].shape) == 1: axis = 0 else: axis = 1 - idx['idx_x_max'] = self.get_coordinate_id(self._lon['data'], self.lon_max, axis=axis) + 1 + idx["idx_x_max"] = self._get_coordinate_id(full_lon["data"], self.lon_max, axis=axis) + 1 - if idx['idx_x_min'] > idx['idx_x_max']: - idx_aux = copy(idx['idx_x_min']) - idx['idx_x_min'] = idx['idx_x_max'] - idx['idx_x_max'] = idx_aux + if idx["idx_x_min"] > idx["idx_x_max"]: + idx_aux = copy(idx["idx_x_min"]) + idx["idx_x_min"] = idx["idx_x_max"] + idx["idx_x_max"] = idx_aux return idx # ================================================================================================================== @@ -1031,25 +1539,25 @@ class Nes(object): Modify variables to keep only the last time step. """ - if self.parallel_method == 'T': - raise NotImplementedError("Statistics are not implemented on time axis paralelitation method.") - aux_time = self._time[0].replace(hour=0, minute=0, second=0, microsecond=0) - self._time = [aux_time] + if self.parallel_method == "T": + raise NotImplementedError("Statistics are not implemented on time axis parallelization method.") + aux_time = self.get_full_times()[0].replace(hour=0, minute=0, second=0, microsecond=0) + self.set_full_times([aux_time]) self.time = [aux_time] for var_name, var_info in self.variables.items(): - if var_info['data'] is None: + if var_info["data"] is None: self.load(var_name) - aux_data = var_info['data'][-1, :] + aux_data = var_info["data"][-1, :] if len(aux_data.shape) == 3: aux_data = aux_data.reshape((1, aux_data.shape[0], aux_data.shape[1], aux_data.shape[2])) - self.variables[var_name]['data'] = aux_data + self.variables[var_name]["data"] = aux_data self.hours_start = 0 self.hours_end = 0 return None - def daily_statistic(self, op, type_op='calendar'): + def daily_statistic(self, op, type_op="calendar"): """ Calculate daily statistic. @@ -1059,45 +1567,46 @@ class Nes(object): Statistic to perform. Accepted values: "max", "mean" and "min". type_op : str Type of statistic to perform. Accepted values: "calendar", "alltsteps", and "withoutt0". - - "calendar": Calculate the statistic using the time metadata. It will avoid single time step by day calculations + - "calendar": Calculate the statistic using the time metadata. It will avoid single time step by day + calculations - "alltsteps": Calculate a single time statistic with all the time steps. - "withoutt0": Calculate a single time statistic with all the time steps avoiding the first one. """ - if self.parallel_method == 'T': + if self.parallel_method == "T": raise NotImplementedError("Statistics are not implemented on time axis parallel method.") - time_interval = self.get_time_interval() - if type_op == 'calendar': + time_interval = self.get_time_interval + if type_op == "calendar": aux_time_bounds = [] aux_time = [] day_list = [date_aux.day for date_aux in self.time] for var_name, var_info in self.variables.items(): - if var_info['data'] is None: + if var_info["data"] is None: self.load(var_name) stat_data = None - for day in np.unique(day_list): + for day in unique(day_list): idx_first = next(i for i, val in enumerate(day_list, 0) if val == day) idx_last = len(day_list) - next(i for i, val in enumerate(reversed(day_list), 1) if val == day) if idx_first != idx_last: # To avoid single time step statistic if idx_last != len(day_list): - if op == 'mean': - data_aux = var_info['data'][idx_first:idx_last + 1, :, :, :].mean(axis=0) - elif op == 'max': - data_aux = var_info['data'][idx_first:idx_last + 1, :, :, :].max(axis=0) - elif op == 'min': - data_aux = var_info['data'][idx_first:idx_last + 1, :, :, :].min(axis=0) + if op == "mean": + data_aux = var_info["data"][idx_first:idx_last + 1, :, :, :].mean(axis=0) + elif op == "max": + data_aux = var_info["data"][idx_first:idx_last + 1, :, :, :].max(axis=0) + elif op == "min": + data_aux = var_info["data"][idx_first:idx_last + 1, :, :, :].min(axis=0) else: - raise NotImplementedError("Statistic operation '{0}' is not implemented.".format(op)) + raise NotImplementedError(f"Statistic operation '{op}' is not implemented.") aux_time_bounds.append([self.time[idx_first], self.time[idx_last]]) else: - if op == 'mean': - data_aux = var_info['data'][idx_first:, :, :, :].mean(axis=0) - elif op == 'max': - data_aux = var_info['data'][idx_first:, :, :, :].max(axis=0) - elif op == 'min': - data_aux = var_info['data'][idx_first:, :, :, :].min(axis=0) + if op == "mean": + data_aux = var_info["data"][idx_first:, :, :, :].mean(axis=0) + elif op == "max": + data_aux = var_info["data"][idx_first:, :, :, :].max(axis=0) + elif op == "min": + data_aux = var_info["data"][idx_first:, :, :, :].min(axis=0) else: - raise NotImplementedError("Statistic operation '{0}' is not implemented.".format(op)) + raise NotImplementedError(f"Statistic operation '{op}' is not implemented.") aux_time_bounds.append([self.time[idx_first], self.time[-1]]) data_aux = data_aux.reshape((1, data_aux.shape[0], data_aux.shape[1], data_aux.shape[2])) @@ -1106,62 +1615,63 @@ class Nes(object): if stat_data is None: stat_data = data_aux.copy() else: - stat_data = np.vstack([stat_data, data_aux]) - self.variables[var_name]['data'] = stat_data - self.variables[var_name]['cell_methods'] = "time: {0} (interval: {1}hr)".format(op, time_interval) + stat_data = vstack([stat_data, data_aux]) + self.variables[var_name]["data"] = stat_data + self.variables[var_name]["cell_methods"] = "time: {0} (interval: {1}hr)".format(op, time_interval) self.time = aux_time - self._time = self.time + self.set_full_times(self.time) self.set_time_bnds(aux_time_bounds) - elif type_op == 'alltsteps': + elif type_op == "alltsteps": for var_name, var_info in self.variables.items(): - if var_info['data'] is None: + if var_info["data"] is None: self.load(var_name) - if op == 'mean': - aux_data = var_info['data'].mean(axis=0) - elif op == 'max': - aux_data = var_info['data'].max(axis=0) - elif op == 'min': - aux_data = var_info['data'].min(axis=0) + if op == "mean": + aux_data = var_info["data"].mean(axis=0) + elif op == "max": + aux_data = var_info["data"].max(axis=0) + elif op == "min": + aux_data = var_info["data"].min(axis=0) else: - raise NotImplementedError("Statistic operation '{0}' is not implemented.".format(op)) + raise NotImplementedError(f"Statistic operation '{op}' is not implemented.") if len(aux_data.shape) == 3: aux_data = aux_data.reshape((1, aux_data.shape[0], aux_data.shape[1], aux_data.shape[2])) - self.variables[var_name]['data'] = aux_data - self.variables[var_name]['cell_methods'] = "time: {0} (interval: {1}hr)".format(op, time_interval) + self.variables[var_name]["data"] = aux_data + self.variables[var_name]["cell_methods"] = "time: {0} (interval: {1}hr)".format(op, time_interval) aux_time = self.time[0].replace(hour=0, minute=0, second=0, microsecond=0) aux_time_bounds = [[self.time[0], self.time[-1]]] self.time = [aux_time] - self._time = self.time + self.set_full_times(self.time) self.set_time_bnds(aux_time_bounds) - elif type_op == 'withoutt0': + elif type_op == "withoutt0": for var_name, var_info in self.variables.items(): - if var_info['data'] is None: + if var_info["data"] is None: self.load(var_name) - if op == 'mean': - aux_data = var_info['data'][1:, :].mean(axis=0) - elif op == 'max': - aux_data = var_info['data'][1:, :].max(axis=0) - elif op == 'min': - aux_data = var_info['data'][1:, :].min(axis=0) + if op == "mean": + aux_data = var_info["data"][1:, :].mean(axis=0) + elif op == "max": + aux_data = var_info["data"][1:, :].max(axis=0) + elif op == "min": + aux_data = var_info["data"][1:, :].min(axis=0) else: - raise NotImplementedError("Statistic operation '{0}' is not implemented.".format(op)) + raise NotImplementedError(f"Statistic operation '{op}' is not implemented.") if len(aux_data.shape) == 3: aux_data = aux_data.reshape((1, aux_data.shape[0], aux_data.shape[1], aux_data.shape[2])) - self.variables[var_name]['data'] = aux_data - self.variables[var_name]['cell_methods'] = "time: {0} (interval: {1}hr)".format(op, time_interval) - aux_time = self._time[1].replace(hour=0, minute=0, second=0, microsecond=0) - aux_time_bounds = [[self._time[1], self._time[-1]]] + self.variables[var_name]["data"] = aux_data + self.variables[var_name]["cell_methods"] = "time: {0} (interval: {1}hr)".format(op, time_interval) + full_time = self.get_full_times() + aux_time = full_time[1].replace(hour=0, minute=0, second=0, microsecond=0) + aux_time_bounds = [[full_time[1], full_time[-1]]] self.time = [aux_time] - self._time = self.time + self.set_full_times(self.time) self.set_time_bnds(aux_time_bounds) else: - raise NotImplementedError("Statistic operation type '{0}' is not implemented.".format(type_op)) + raise NotImplementedError(f"Statistic operation type '{type_op}' is not implemented.") self.hours_start = 0 self.hours_end = 0 @@ -1170,40 +1680,40 @@ class Nes(object): @staticmethod def _get_axis_index_(axis): - if axis == 'T': + if axis == "T": value = 0 - elif axis == 'Z': + elif axis == "Z": value = 1 - elif axis == 'Y': + elif axis == "Y": value = 2 - elif axis == 'X': + elif axis == "X": value = 3 else: raise ValueError("Unknown axis: {0}".format(axis)) return value - def sum_axis(self, axis='Z'): + def sum_axis(self, axis="Z"): if self.parallel_method == axis: - raise NotImplementedError("It is not possible to sum the axis with it is parallelized '{0}'".format( - self.parallel_method)) + raise NotImplementedError( + f"It is not possible to sum the axis with it is parallelized '{self.parallel_method}'") for var_name, var_info in self.variables.items(): - if var_info['data'] is not None: - self.variables[var_name]['data'] = self.variables[var_name]['data'].sum( + if var_info["data"] is not None: + self.variables[var_name]["data"] = self.variables[var_name]["data"].sum( axis=self._get_axis_index_(axis), keepdims=True) - if axis == 'T': - self.variables[var_name]['cell_methods'] = "time: sum (interval: {0}hr)".format( + if axis == "T": + self.variables[var_name]["cell_methods"] = "time: sum (interval: {0}hr)".format( (self.time[-1] - self.time[0]).total_seconds() // 3600) - if axis == 'T': + if axis == "T": self.set_time_bnds([self.time[0], self.time[-1]]) self.time = [self.time[0]] - self._time = [self._time[0]] - if axis == 'Z': - self.lev['data'] = np.array([self.lev['data'][0]]) - self._lev['data'] = np.array([self._lev['data'][0]]) + self.set_full_times([self.time[0]]) + if axis == "Z": + self.lev["data"] = array([self.lev["data"][0]]) + self.set_full_levels(self.lev) return None @@ -1213,7 +1723,7 @@ class Nes(object): Parameters ---------- - time : datetime.datetime + time : datetime Time element. Returns @@ -1239,10 +1749,10 @@ class Nes(object): Returns ------- Nes - Nes object + A Nes object """ - if self.parallel_method == 'T': + if self.parallel_method == "T": raise NotImplementedError("The rolling mean cannot be calculated using the time axis parallel method.") aux_nessy = self.copy(copy_vars=False) @@ -1255,16 +1765,16 @@ class Nes(object): for var_name in var_list: # Load variables if they have not been loaded previously - if self.variables[var_name]['data'] is None: + if self.variables[var_name]["data"] is None: self.load(var_name) # Get original file shape - nessy_shape = self.variables[var_name]['data'].shape + nessy_shape = self.variables[var_name]["data"].shape # Initialise array aux_nessy.variables[var_name] = {} - aux_nessy.variables[var_name]['data'] = np.empty(shape=nessy_shape) - aux_nessy.variables[var_name]['dimensions'] = deepcopy(self.variables[var_name]['dimensions']) + aux_nessy.variables[var_name]["data"] = empty(shape=nessy_shape) + aux_nessy.variables[var_name]["dimensions"] = deepcopy(self.variables[var_name]["dimensions"]) for curr_time in self.time: # Get previous time given a set of hours @@ -1277,17 +1787,17 @@ class Nes(object): # Get mean if previous time is available if prev_time_id is not None: if self.info: - print(f'Calculating mean between {prev_time} and {curr_time}.') - aux_nessy.variables[var_name]['data'][curr_time_id, :, :, :] = self.variables[var_name]['data'][ + print(f"Calculating mean between {prev_time} and {curr_time}.") + aux_nessy.variables[var_name]["data"][curr_time_id, :, :, :] = self.variables[var_name]["data"][ prev_time_id:curr_time_id, :, :, :].mean(axis=0, keepdims=True) # Fill with nan if previous time is not available else: if self.info: - msg = f'Mean between {prev_time} and {curr_time} cannot be calculated ' - msg += f'because data for {prev_time} is not available.' + msg = f"Mean between {prev_time} and {curr_time} cannot be calculated " + msg += f"because data for {prev_time} is not available." print(msg) - aux_nessy.variables[var_name]['data'][curr_time_id, :, :, :] = np.full(shape= - (1, nessy_shape[1], nessy_shape[2], nessy_shape[3]), fill_value=np.nan) + aux_nessy.variables[var_name]["data"][curr_time_id, :, :, :] = full( + shape=(1, nessy_shape[1], nessy_shape[2], nessy_shape[3]), fill_value=nan) return aux_nessy @@ -1295,7 +1805,7 @@ class Nes(object): # Reading # ================================================================================================================== - def get_read_axis_limits(self): + def _get_read_axis_limits(self): """ Calculate the 4D reading axis limits depending on if them have to balanced or not. @@ -1307,11 +1817,11 @@ class Nes(object): """ if self.balanced: - return self.get_read_axis_limits_balanced() + return self._get_read_axis_limits_balanced() else: - return self.get_read_axis_limits_unbalanced() + return self._get_read_axis_limits_unbalanced() - def get_read_axis_limits_unbalanced(self): + def _get_read_axis_limits_unbalanced(self): """ Calculate the 4D reading axis limits. @@ -1322,81 +1832,81 @@ class Nes(object): t_min, t_max, z_min, z_max, y_min, y_max, x_min and x_max. """ - axis_limits = {'x_min': None, 'x_max': None, - 'y_min': None, 'y_max': None, - 'z_min': None, 'z_max': None, - 't_min': None, 't_max': None} + axis_limits = {"x_min": None, "x_max": None, + "y_min": None, "y_max": None, + "z_min": None, "z_max": None, + "t_min": None, "t_max": None} - idx = self.get_idx_intervals() - if self.parallel_method == 'Y': - y_len = idx['idx_y_max'] - idx['idx_y_min'] + idx = self._get_idx_intervals() + if self.parallel_method == "Y": + y_len = idx["idx_y_max"] - idx["idx_y_min"] if y_len < self.size: - raise IndexError('More processors (size={0}) selected than Y elements (size={1})'.format( + raise IndexError("More processors (size={0}) selected than Y elements (size={1})".format( self.size, y_len)) - axis_limits['y_min'] = ((y_len // self.size) * self.rank) + idx['idx_y_min'] + axis_limits["y_min"] = ((y_len // self.size) * self.rank) + idx["idx_y_min"] if self.rank + 1 < self.size: - axis_limits['y_max'] = ((y_len // self.size) * (self.rank + 1)) + idx['idx_y_min'] + axis_limits["y_max"] = ((y_len // self.size) * (self.rank + 1)) + idx["idx_y_min"] else: - axis_limits['y_max'] = idx['idx_y_max'] + axis_limits["y_max"] = idx["idx_y_max"] # Non parallel filters - axis_limits['x_min'] = idx['idx_x_min'] - axis_limits['x_max'] = idx['idx_x_max'] + axis_limits["x_min"] = idx["idx_x_min"] + axis_limits["x_max"] = idx["idx_x_max"] - axis_limits['t_min'] = idx['idx_t_min'] - axis_limits['t_max'] = idx['idx_t_max'] + axis_limits["t_min"] = idx["idx_t_min"] + axis_limits["t_max"] = idx["idx_t_max"] - elif self.parallel_method == 'X': - x_len = idx['idx_x_max'] - idx['idx_x_min'] + elif self.parallel_method == "X": + x_len = idx["idx_x_max"] - idx["idx_x_min"] if x_len < self.size: - raise IndexError('More processors (size={0}) selected than X elements (size={1})'.format( + raise IndexError("More processors (size={0}) selected than X elements (size={1})".format( self.size, x_len)) - axis_limits['x_min'] = ((x_len // self.size) * self.rank) + idx['idx_x_min'] + axis_limits["x_min"] = ((x_len // self.size) * self.rank) + idx["idx_x_min"] if self.rank + 1 < self.size: - axis_limits['x_max'] = ((x_len // self.size) * (self.rank + 1)) + idx['idx_x_min'] + axis_limits["x_max"] = ((x_len // self.size) * (self.rank + 1)) + idx["idx_x_min"] else: - axis_limits['x_max'] = idx['idx_x_max'] + axis_limits["x_max"] = idx["idx_x_max"] # Non parallel filters - axis_limits['y_min'] = idx['idx_y_min'] - axis_limits['y_max'] = idx['idx_y_max'] + axis_limits["y_min"] = idx["idx_y_min"] + axis_limits["y_max"] = idx["idx_y_max"] - axis_limits['t_min'] = idx['idx_t_min'] - axis_limits['t_max'] = idx['idx_t_max'] + axis_limits["t_min"] = idx["idx_t_min"] + axis_limits["t_max"] = idx["idx_t_max"] - elif self.parallel_method == 'T': - t_len = idx['idx_t_max'] - idx['idx_t_min'] + elif self.parallel_method == "T": + t_len = idx["idx_t_max"] - idx["idx_t_min"] if t_len < self.size: - raise IndexError('More processors (size={0}) selected than T elements (size={1})'.format( + raise IndexError("More processors (size={0}) selected than T elements (size={1})".format( self.size, t_len)) - axis_limits['t_min'] = ((t_len // self.size) * self.rank) + idx['idx_t_min'] + axis_limits["t_min"] = ((t_len // self.size) * self.rank) + idx["idx_t_min"] if self.rank + 1 < self.size: - axis_limits['t_max'] = ((t_len // self.size) * (self.rank + 1)) + idx['idx_t_min'] + axis_limits["t_max"] = ((t_len // self.size) * (self.rank + 1)) + idx["idx_t_min"] # Non parallel filters - axis_limits['y_min'] = idx['idx_y_min'] - axis_limits['y_max'] = idx['idx_y_max'] + axis_limits["y_min"] = idx["idx_y_min"] + axis_limits["y_max"] = idx["idx_y_max"] - axis_limits['x_min'] = idx['idx_x_min'] - axis_limits['x_max'] = idx['idx_x_max'] + axis_limits["x_min"] = idx["idx_x_min"] + axis_limits["x_max"] = idx["idx_x_max"] else: raise NotImplementedError("Parallel method '{meth}' is not implemented. Use one of these: {accept}".format( - meth=self.parallel_method, accept=['X', 'Y', 'T'])) + meth=self.parallel_method, accept=["X", "Y", "T"])) # Vertical levels selection: - axis_limits['z_min'] = self.first_level + axis_limits["z_min"] = self.first_level if self.last_level == -1 or self.last_level is None: self.last_level = None - elif self.last_level + 1 == len(self._lev['data']): + elif self.last_level + 1 == len(self.get_full_levels()["data"]): self.last_level = None else: self.last_level += 1 - axis_limits['z_max'] = self.last_level + axis_limits["z_max"] = self.last_level return axis_limits - def get_read_axis_limits_balanced(self): + def _get_read_axis_limits_balanced(self): """ Calculate the 4D reading balanced axis limits. @@ -1406,47 +1916,46 @@ class Nes(object): Dictionary with the 4D limits of the rank data to read. t_min, t_max, z_min, z_max, y_min, y_max, x_min and x_max. """ - idx = self.get_idx_intervals() + idx = self._get_idx_intervals() fid_dist = {} - if self.parallel_method == 'Y': - len_to_split = idx['idx_y_max'] - idx['idx_y_min'] + if self.parallel_method == "Y": + len_to_split = idx["idx_y_max"] - idx["idx_y_min"] if len_to_split < self.size: - raise IndexError('More processors (size={0}) selected than Y elements (size={1})'.format( + raise IndexError("More processors (size={0}) selected than Y elements (size={1})".format( self.size, len_to_split)) - min_axis = 'y_min' - max_axis = 'y_max' - to_add = idx['idx_y_min'] + min_axis = "y_min" + max_axis = "y_max" + to_add = idx["idx_y_min"] - elif self.parallel_method == 'X': - len_to_split = idx['idx_x_max'] - idx['idx_x_min'] + elif self.parallel_method == "X": + len_to_split = idx["idx_x_max"] - idx["idx_x_min"] if len_to_split < self.size: - raise IndexError('More processors (size={0}) selected than X elements (size={1})'.format( + raise IndexError("More processors (size={0}) selected than X elements (size={1})".format( self.size, len_to_split)) - min_axis = 'x_min' - max_axis = 'x_max' - to_add = idx['idx_x_min'] - elif self.parallel_method == 'T': - len_to_split = idx['idx_t_max'] - idx['idx_t_min'] + min_axis = "x_min" + max_axis = "x_max" + to_add = idx["idx_x_min"] + elif self.parallel_method == "T": + len_to_split = idx["idx_t_max"] - idx["idx_t_min"] if len_to_split < self.size: - raise IndexError('More processors (size={0}) selected than T elements (size={1})'.format( - self.size, len_to_split)) - min_axis = 't_min' - max_axis = 't_max' - to_add = idx['idx_t_min'] + raise IndexError(f"More processors (size={self.size}) selected than T elements (size={len_to_split})") + min_axis = "t_min" + max_axis = "t_max" + to_add = idx["idx_t_min"] else: raise NotImplementedError("Parallel method '{meth}' is not implemented. Use one of these: {accept}".format( - meth=self.parallel_method, accept=['X', 'Y', 'T'])) + meth=self.parallel_method, accept=["X", "Y", "T"])) procs_len = len_to_split // self.size procs_rows_extended = len_to_split - (procs_len * self.size) rows_sum = 0 for proc in range(self.size): - fid_dist[proc] = {'x_min': 0, 'x_max': None, - 'y_min': 0, 'y_max': None, - 'z_min': 0, 'z_max': None, - 't_min': 0, 't_max': None} + fid_dist[proc] = {"x_min": 0, "x_max": None, + "y_min": 0, "y_max": None, + "z_min": 0, "z_max": None, + "t_min": 0, "t_max": None} if proc < procs_rows_extended: aux_rows = procs_len + 1 else: @@ -1474,29 +1983,29 @@ class Nes(object): axis_limits = fid_dist[self.rank] # Non parallel filters - if self.parallel_method != 'T': - axis_limits['t_min'] = idx['idx_t_min'] - axis_limits['t_max'] = idx['idx_t_max'] - if self.parallel_method != 'X': - axis_limits['x_min'] = idx['idx_x_min'] - axis_limits['x_max'] = idx['idx_x_max'] - if self.parallel_method != 'Y': - axis_limits['y_min'] = idx['idx_y_min'] - axis_limits['y_max'] = idx['idx_y_max'] + if self.parallel_method != "T": + axis_limits["t_min"] = idx["idx_t_min"] + axis_limits["t_max"] = idx["idx_t_max"] + if self.parallel_method != "X": + axis_limits["x_min"] = idx["idx_x_min"] + axis_limits["x_max"] = idx["idx_x_max"] + if self.parallel_method != "Y": + axis_limits["y_min"] = idx["idx_y_min"] + axis_limits["y_max"] = idx["idx_y_max"] # Vertical levels selection: - axis_limits['z_min'] = self.first_level + axis_limits["z_min"] = self.first_level if self.last_level == -1 or self.last_level is None: self.last_level = None - elif self.last_level + 1 == len(self._lev['data']): + elif self.last_level + 1 == len(self.get_full_levels()["data"]): self.last_level = None else: self.last_level += 1 - axis_limits['z_max'] = self.last_level + axis_limits["z_max"] = self.last_level return axis_limits - def get_time_id(self, hours, first=True): + def _get_time_id(self, hours, first=True): """ Get the index of the corresponding time value. @@ -1513,23 +2022,24 @@ class Nes(object): int Index of the time array. """ + full_time = self.get_full_times() if first: - idx = self._time.index(self._time[0] + timedelta(hours=hours)) + idx = full_time.index(full_time[0] + timedelta(hours=hours)) else: - idx = self._time.index(self._time[-1] - timedelta(hours=hours)) + 1 + idx = full_time.index(full_time[-1] - timedelta(hours=hours)) + 1 return idx @staticmethod - def get_coordinate_id(array, value, axis=0): + def _get_coordinate_id(my_array, value, axis=0): """ Get the index of the corresponding coordinate value. Parameters ---------- - array : np.array - Array with the coordinate data + my_array : array + An Array with the coordinate data value : float Coordinate value to search. axis : int @@ -1541,11 +2051,11 @@ class Nes(object): int Index of the coordinate array. """ - idx = (np.abs(array - value)).argmin(axis=axis).min() + idx = (abs(my_array - value)).argmin(axis=axis).min() return idx - def open(self): + def _open(self): """ Open the NetCDF. """ @@ -1554,7 +2064,7 @@ class Nes(object): return None - def __open_netcdf4(self, mode='r'): + def __open_netcdf4(self, mode="r"): """ Open the NetCDF with netcdf4-python. @@ -1562,7 +2072,7 @@ class Nes(object): ---------- mode : str Inheritance from mode parameter from https://unidata.github.io/netcdf4-python/#Dataset.__init__ - Default: 'r' (read-only). + Default: "r" (read-only). Returns ------- netcdf : Dataset @@ -1582,31 +2092,28 @@ class Nes(object): """ Close the NetCDF with netcdf4-python. """ - if (hasattr(self, 'serial_nc')) and (self.serial_nc is not None): + if (hasattr(self, "serial_nc")) and (self.serial_nc is not None): if self.master: self.serial_nc.close() self.serial_nc = None - if (hasattr(self, 'dataset')) and (self.dataset is not None): + if (hasattr(self, "dataset")) and (self.dataset is not None): self.dataset.close() self.dataset = None return None - def __get_dates_from_months(self, time, units, calendar): + @staticmethod + def __get_dates_from_months(time): """ Calculates the number of days since the first date - in the 'time' list and store in new list: - This is useful when the units are 'months since', - which cannot be transformed to dates using num2date. + in the "time" list and store in new list: + This is useful when the units are "months since", + which cannot be transformed to dates using "num2date". Parameter --------- - time: List + time: List[datetime] Original time. - units: str - CF compliant time units. - calendar: str - Original calendar. Returns ------- @@ -1614,10 +2121,8 @@ class Nes(object): CF compliant time. """ - start_date_str = time.units.split('since')[1].lstrip() - start_date = datetime.datetime(int(start_date_str[0:4]), - int(start_date_str[5:7]), - int(start_date_str[8:10])) + start_date_str = time.units.split("since")[1].lstrip() + start_date = datetime(int(start_date_str[0:4]), int(start_date_str[5:7]), int(start_date_str[8:10])) new_time_deltas = [] @@ -1650,18 +2155,18 @@ class Nes(object): units = self.__parse_time_unit(time.units) - if not hasattr(time, 'calendar'): - calendar = 'standard' + if not hasattr(time, "calendar"): + calendar = "standard" else: calendar = time.calendar - if 'months since' in time.units: - units = 'days since ' + time.units.split('since')[1].lstrip() - time = self.__get_dates_from_months(time, units, calendar) + if "months since" in time.units: + units = "days since " + time.units.split("since")[1].lstrip() + time = self.__get_dates_from_months(time) time_data = time[:] - if len(time_data) == 1 and np.isnan(time_data[0]): + if len(time_data) == 1 and isnan(time_data[0]): time_data[0] = 0 return time_data, units, calendar @@ -1682,8 +2187,8 @@ class Nes(object): CF compliant time units. """ - if 'h @' in t_units: - t_units = 'hours since {0}-{1}-{2} {3}:{4}:{5} UTC'.format( + if "h @" in t_units: + t_units = "hours since {0}-{1}-{2} {3}:{4}:{5} UTC".format( t_units[4:8], t_units[8:10], t_units[10:12], t_units[13:15], t_units[15:17], t_units[17:-4]) return t_units @@ -1703,17 +2208,17 @@ class Nes(object): str Time variable resolution """ - if 'day' in units or 'days' in units: - resolution = 'days' - elif 'hour' in units or 'hours' in units: - resolution = 'hours' - elif 'minute' in units or 'minutes' in units: - resolution = 'minutes' - elif 'second' in units or 'seconds' in units: - resolution = 'seconds' + if "day" in units or "days" in units: + resolution = "days" + elif "hour" in units or "hours" in units: + resolution = "hours" + elif "minute" in units or "minutes" in units: + resolution = "minutes" + elif "second" in units or "seconds" in units: + resolution = "seconds" else: - # Default resolution is 'hours' - resolution = 'hours' + # Default resolution is "hours" + resolution = "hours" return resolution def __get_time(self): @@ -1722,26 +2227,23 @@ class Nes(object): Returns ------- - time : List - List of times (datetime.datetime) of the NetCDF data. + time : List[datetime] + List of times (datetime) of the NetCDF data. """ - if self.master: - nc_var = self.dataset.variables['time'] + nc_var = self.dataset.variables["time"] time_data, units, calendar = self.__parse_time(nc_var) # Extracting time resolution depending on the units self._time_resolution = self.__get_time_resolution_from_units(units) # Checking if it is a climatology dataset - if hasattr(nc_var, 'climatology'): + if hasattr(nc_var, "climatology"): self._climatology = True self._climatology_var_name = nc_var.climatology time = num2date(time_data, units, calendar=calendar) - time = [datetime.datetime(year=dt.year, month=dt.month, day=dt.day, hour=dt.hour, - minute=dt.minute) for dt in time] + time = [datetime(year=dt.year, month=dt.month, day=dt.day, hour=dt.hour, minute=dt.minute) for dt in time] else: time = None - time = self.comm.bcast(time, root=0) - self.free_vars('time') + self.free_vars("time") return time @@ -1757,17 +2259,17 @@ class Nes(object): Returns ------- time_bnds : List - List of time bounds (datetime) of the NetCDF data. + A List of time bounds (datetime) of the NetCDF data. """ - if self.master: - if not create_nes: - if 'time_bnds' in self.dataset.variables.keys() or self._climatology: - time = self.dataset.variables['time'] + if not create_nes: + if self.master: + if "time_bnds" in self.dataset.variables.keys() or self._climatology: + time = self.dataset.variables["time"] if self._climatology: nc_var = self.dataset.variables[self._climatology_var_name] else: - nc_var = self.dataset.variables['time_bnds'] + nc_var = self.dataset.variables["time_bnds"] time_bnds = num2date(nc_var[:], self.__parse_time_unit(time.units), calendar=time.calendar).tolist() # Iterate over each inner list @@ -1777,8 +2279,7 @@ class Nes(object): # Iterate over datetime objects within each inner list for dt in inner_list: # Access year, month, day, hour, and minute attributes of datetime objects - new_dt = datetime.datetime(year=dt.year, month=dt.month, day=dt.day, hour=dt.hour, - minute=dt.minute) + new_dt = datetime(year=dt.year, month=dt.month, day=dt.day, hour=dt.hour, minute=dt.minute) # Append the new datetime object to the new inner list new_inner_list.append(new_dt) # Replace the old inner list with the new one @@ -1790,9 +2291,7 @@ class Nes(object): else: time_bnds = None - time_bnds = self.comm.bcast(time_bnds, root=0) - - self.free_vars('time_bnds') + self.free_vars("time_bnds") return time_bnds @@ -1807,20 +2306,21 @@ class Nes(object): Returns ------- - lat_bnds : List + lat_bnds : dict Latitude bounds of the NetCDF data. - lon_bnds : List + lon_bnds : dict Longitude bounds of the NetCDF data. """ - if self.master: - if not create_nes: - if 'lat_bnds' in self.dataset.variables.keys(): - lat_bnds = {'data': self._unmask_array(self.dataset.variables['lat_bnds'][:])} + if not create_nes: + if self.master: + if "lat_bnds" in self.dataset.variables.keys(): + lat_bnds = {"data": self._unmask_array(self.dataset.variables["lat_bnds"][:])} else: lat_bnds = None - if 'lon_bnds' in self.dataset.variables.keys(): - lon_bnds = {'data': self._unmask_array(self.dataset.variables['lon_bnds'][:])} + + if "lon_bnds" in self.dataset.variables.keys(): + lon_bnds = {"data": self._unmask_array(self.dataset.variables["lon_bnds"][:])} else: lon_bnds = None else: @@ -1829,10 +2329,8 @@ class Nes(object): else: lat_bnds = None lon_bnds = None - lat_bnds = self.comm.bcast(lat_bnds, root=0) - lon_bnds = self.comm.bcast(lon_bnds, root=0) - self.free_vars(['lat_bnds', 'lon_bnds']) + self.free_vars(["lat_bnds", "lon_bnds"]) return lat_bnds, lon_bnds @@ -1854,12 +2352,12 @@ class Nes(object): c_measures = {} if self.master: if not create_nes: - if 'cell_area' in self.dataset.variables.keys(): - c_measures['cell_area'] = {} - c_measures['cell_area']['data'] = self._unmask_array(self.dataset.variables['cell_area'][:]) + if "cell_area" in self.dataset.variables.keys(): + c_measures["cell_area"] = {} + c_measures["cell_area"]["data"] = self._unmask_array(self.dataset.variables["cell_area"][:]) c_measures = self.comm.bcast(c_measures, root=0) - self.free_vars(['cell_area']) + self.free_vars(["cell_area"]) return c_measures @@ -1872,12 +2370,12 @@ class Nes(object): Parameters ---------- possible_names: List, str, list - List (or single string) of the possible names of the coordinate (e.g. ['lat', 'latitude']). + A List (or single string) of the possible names of the coordinate (e.g. ["lat", "latitude"]). Returns ------- nc_var : dict - Dictionary with the 'data' key with the coordinate variable values. and the attributes as other keys. + Dictionary with the "data" key with the coordinate variable values. and the attributes as other keys. """ if isinstance(possible_names, str): @@ -1885,16 +2383,21 @@ class Nes(object): try: dimension_name = set(possible_names).intersection(set(self.variables.keys())).pop() - - nc_var = self.variables[dimension_name].copy() - nc_var['data'] = self.dataset.variables[dimension_name][:] - if hasattr(nc_var, 'units'): - if nc_var['units'] in ['unitless', '-']: - nc_var['units'] = '' + if self.master: + nc_var = self.variables[dimension_name].copy() + nc_var["data"] = self.dataset.variables[dimension_name][:] + if hasattr(nc_var, "units"): + if nc_var["units"] in ["unitless", "-"]: + nc_var["units"] = "" + else: + nc_var = None self.free_vars(dimension_name) except KeyError: - nc_var = {'data': np.array([0]), - 'units': ''} + if self.master: + nc_var = {"data": array([0]), + "units": ""} + else: + nc_var = None return nc_var @@ -1905,9 +2408,9 @@ class Nes(object): Parameters ---------- coordinate_info : dict, list - Dictionary with the 'data' key with the coordinate variable values. and the attributes as other keys. + Dictionary with the "data" key with the coordinate variable values. and the attributes as other keys. coordinate_axis : str - Name of the coordinate to extract. Accepted values: ['Z', 'Y', 'X']. + Name of the coordinate to extract. Accepted values: ["Z", "Y", "X"]. bounds : bool Boolean variable to know if there are coordinate bounds. Returns @@ -1920,38 +2423,38 @@ class Nes(object): return None if not isinstance(coordinate_info, dict): - values = {'data': deepcopy(coordinate_info)} + values = {"data": deepcopy(coordinate_info)} else: values = deepcopy(coordinate_info) - coordinate_len = len(values['data'].shape) + coordinate_len = len(values["data"].shape) if bounds: coordinate_len -= 1 - if coordinate_axis == 'Y': + if coordinate_axis == "Y": if coordinate_len == 1: - values['data'] = values['data'][self.read_axis_limits['y_min']:self.read_axis_limits['y_max']] + values["data"] = values["data"][self.read_axis_limits["y_min"]:self.read_axis_limits["y_max"]] elif coordinate_len == 2: - values['data'] = values['data'][self.read_axis_limits['y_min']:self.read_axis_limits['y_max'], - self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + values["data"] = values["data"][self.read_axis_limits["y_min"]:self.read_axis_limits["y_max"], + self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] else: raise NotImplementedError("The coordinate has wrong dimensions: {dim}".format( - dim=values['data'].shape)) - elif coordinate_axis == 'X': + dim=values["data"].shape)) + elif coordinate_axis == "X": if coordinate_len == 1: - values['data'] = values['data'][self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + values["data"] = values["data"][self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] elif coordinate_len == 2: - values['data'] = values['data'][self.read_axis_limits['y_min']:self.read_axis_limits['y_max'], - self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + values["data"] = values["data"][self.read_axis_limits["y_min"]:self.read_axis_limits["y_max"], + self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] else: raise NotImplementedError("The coordinate has wrong dimensions: {dim}".format( - dim=values['data'].shape)) - elif coordinate_axis == 'Z': + dim=values["data"].shape)) + elif coordinate_axis == "Z": if coordinate_len == 1: - values['data'] = values['data'][self.read_axis_limits['z_min']:self.read_axis_limits['z_max']] + values["data"] = values["data"][self.read_axis_limits["z_min"]:self.read_axis_limits["z_max"]] else: raise NotImplementedError("The coordinate has wrong dimensions: {dim}".format( - dim=values['data'].shape)) + dim=values["data"].shape)) return values @@ -1962,7 +2465,7 @@ class Nes(object): Parameters ---------- cell_measures_info : dict, list - Dictionary with the 'data' key with the cell measures variable values. and the attributes as other keys. + Dictionary with the "data" key with the cell measures variable values. and the attributes as other keys. Returns ------- @@ -1978,16 +2481,16 @@ class Nes(object): for cell_measures_var in cell_measures_info.keys(): values = deepcopy(cell_measures_info[cell_measures_var]) - coordinate_len = len(values['data'].shape) + coordinate_len = len(values["data"].shape) if coordinate_len == 1: - values['data'] = values['data'][self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + values["data"] = values["data"][self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] elif coordinate_len == 2: - values['data'] = values['data'][self.read_axis_limits['y_min']:self.read_axis_limits['y_max'], - self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + values["data"] = values["data"][self.read_axis_limits["y_min"]:self.read_axis_limits["y_max"], + self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] else: raise NotImplementedError("The coordinate has wrong dimensions: {dim}".format( - dim=values['data'].shape)) + dim=values["data"].shape)) cell_measures_values[cell_measures_var] = values @@ -2001,11 +2504,11 @@ class Nes(object): ------- variables : dict Dictionary with the variable name as key and another dictionary as value. - De value dictionary will have the 'data' key with None as value and all the variable attributes as the + De value dictionary will have the "data" key with None as value and all the variable attributes as the other keys. e.g. - {'var_name_1': {'data': None, 'attr_1': value_1_1, 'attr_2': value_1_2, ...}, - 'var_name_2': {'data': None, 'attr_1': value_2_1, 'attr_2': value_2_2, ...}, + {"var_name_1": {"data": None, "attr_1": value_1_1, "attr_2": value_1_2, ...}, + "var_name_2": {"data": None, "attr_1": value_2_1, "attr_2": value_2_2, ...}, ...} """ @@ -2014,20 +2517,20 @@ class Nes(object): # Initialise data for var_name, var_info in self.dataset.variables.items(): variables[var_name] = {} - variables[var_name]['data'] = None - variables[var_name]['dimensions'] = var_info.dimensions - variables[var_name]['dtype'] = var_info.dtype - if variables[var_name]['dtype'] in [str, object]: + variables[var_name]["data"] = None + variables[var_name]["dimensions"] = var_info.dimensions + variables[var_name]["dtype"] = var_info.dtype + if variables[var_name]["dtype"] in [str, object]: if self.strlen is None: self.set_strlen() - variables[var_name]['dtype'] = str + variables[var_name]["dtype"] = str # Avoid some attributes for attrname in var_info.ncattrs(): - if attrname not in ['missing_value', '_FillValue']: + if attrname not in ["missing_value", "_FillValue", "add_offset", "scale_factor"]: value = getattr(var_info, attrname) - if str(value) in ['unitless', '-']: - value = '' + if str(value) in ["unitless", "-"]: + value = "" variables[var_name][attrname] = value else: variables = None @@ -2046,7 +2549,7 @@ class Nes(object): Returns ------- - data: np.array + data: array Portion of the variable data corresponding to the rank. """ @@ -2057,55 +2560,50 @@ class Nes(object): if len(var_dims) < 2: data = nc_var[:] elif len(var_dims) == 2: - data = nc_var[self.read_axis_limits['y_min']:self.read_axis_limits['y_max'], - self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + data = nc_var[self.read_axis_limits["y_min"]:self.read_axis_limits["y_max"], + self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] data = data.reshape(1, 1, data.shape[-2], data.shape[-1]) elif len(var_dims) == 3: - if 'strlen' in var_dims: - data = nc_var[self.read_axis_limits['y_min']:self.read_axis_limits['y_max'], - self.read_axis_limits['x_min']:self.read_axis_limits['x_max'], + if "strlen" in var_dims: + data = nc_var[self.read_axis_limits["y_min"]:self.read_axis_limits["y_max"], + self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"], :] - data_aux = np.empty(shape=(data.shape[0], data.shape[1]), dtype=object) + data_aux = empty(shape=(data.shape[0], data.shape[1]), dtype=object) for lat_n in range(data.shape[0]): for lon_n in range(data.shape[1]): - data_aux[lat_n, lon_n] = ''.join( - data[lat_n, lon_n].tobytes().decode('ascii').replace('\x00', '')) + data_aux[lat_n, lon_n] = "".join( + data[lat_n, lon_n].tobytes().decode("ascii").replace("\x00", "")) data = data_aux.reshape((1, 1, data_aux.shape[-2], data_aux.shape[-1])) else: - data = nc_var[self.read_axis_limits['t_min']:self.read_axis_limits['t_max'], - self.read_axis_limits['y_min']:self.read_axis_limits['y_max'], - self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + data = nc_var[self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"], + self.read_axis_limits["y_min"]:self.read_axis_limits["y_max"], + self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] data = data.reshape(data.shape[-3], 1, data.shape[-2], data.shape[-1]) elif len(var_dims) == 4: - data = nc_var[self.read_axis_limits['t_min']:self.read_axis_limits['t_max'], - self.read_axis_limits['z_min']:self.read_axis_limits['z_max'], - self.read_axis_limits['y_min']:self.read_axis_limits['y_max'], - self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + data = nc_var[self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"], + self.read_axis_limits["z_min"]:self.read_axis_limits["z_max"], + self.read_axis_limits["y_min"]:self.read_axis_limits["y_max"], + self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] elif len(var_dims) == 5: - if 'strlen' in var_dims: - data = nc_var[self.read_axis_limits['t_min']:self.read_axis_limits['t_max'], - self.read_axis_limits['z_min']:self.read_axis_limits['z_max'], - self.read_axis_limits['y_min']:self.read_axis_limits['y_max'], - self.read_axis_limits['x_min']:self.read_axis_limits['x_max'], + if "strlen" in var_dims: + data = nc_var[self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"], + self.read_axis_limits["z_min"]:self.read_axis_limits["z_max"], + self.read_axis_limits["y_min"]:self.read_axis_limits["y_max"], + self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"], :] - data_aux = np.empty(shape=(data.shape[0], data.shape[1], data.shape[2], data.shape[3]), dtype=object) + data_aux = empty(shape=(data.shape[0], data.shape[1], data.shape[2], data.shape[3]), dtype=object) for time_n in range(data.shape[0]): for lev_n in range(data.shape[1]): for lat_n in range(data.shape[2]): for lon_n in range(data.shape[3]): - data_aux[time_n, lev_n, lat_n, lon_n] = ''.join( - data[time_n, lev_n, lat_n, lon_n].tobytes().decode('ascii').replace('\x00', '')) + data_aux[time_n, lev_n, lat_n, lon_n] = "".join( + data[time_n, lev_n, lat_n, lon_n].tobytes().decode("ascii").replace("\x00", "")) data = data_aux else: - # data = nc_var[self.read_axis_limits['t_min']:self.read_axis_limits['t_max'], - # :, - # self.read_axis_limits['z_min']:self.read_axis_limits['z_max'], - # self.read_axis_limits['y_min']:self.read_axis_limits['y_max'], - # self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] - raise NotImplementedError('Error with {0}. Only can be read netCDF with 4 dimensions or less'.format( + raise NotImplementedError("Error with {0}. Only can be read netCDF with 4 dimensions or less".format( var_name)) else: - raise NotImplementedError('Error with {0}. Only can be read netCDF with 4 dimensions or less'.format( + raise NotImplementedError("Error with {0}. Only can be read netCDF with 4 dimensions or less".format( var_name)) # Unmask array @@ -2117,7 +2615,7 @@ class Nes(object): """ Load of the selected variables. - That function will fill the variable 'data' key with the corresponding values. + That function will fill the variable "data" key with the corresponding values. Parameters ---------- @@ -2126,10 +2624,10 @@ class Nes(object): """ if (self.__ini_path is None) and (self.dataset is None): - raise RuntimeError('Only data from existing files can be loaded.') + raise RuntimeError("Only data from existing files can be loaded.") if self.dataset is None: - self.__open_dataset() + self.__open_netcdf4() close = True else: close = False @@ -2142,21 +2640,21 @@ class Nes(object): for i, var_name in enumerate(var_list): if self.info: print("Rank {0:03d}: Loading {1} var ({2}/{3})".format(self.rank, var_name, i + 1, len(var_list))) - if self.variables[var_name]['data'] is None: - self.variables[var_name]['data'] = self._read_variable(var_name) + if self.variables[var_name]["data"] is None: + self.variables[var_name]["data"] = self._read_variable(var_name) # Data type changes when joining characters in read_variable (S1 to S+strlen) - if 'strlen' in self.variables[var_name]['dimensions']: + if "strlen" in self.variables[var_name]["dimensions"]: if self.strlen is None: self.set_strlen() - self.variables[var_name]['dtype'] = str - self.variables[var_name]['dimensions'] = tuple([x for x in self.variables[var_name]['dimensions'] + self.variables[var_name]["dtype"] = str + self.variables[var_name]["dimensions"] = tuple([x for x in self.variables[var_name]["dimensions"] if x != "strlen"]) else: if self.master: print("Data for {0} was previously loaded. Skipping variable.".format(var_name)) if self.info: print("Rank {0:03d}: Loaded {1} var ({2})".format( - self.rank, var_name, self.variables[var_name]['data'].shape)) + self.rank, var_name, self.variables[var_name]["data"].shape)) if close: self.close() @@ -2170,38 +2668,38 @@ class Nes(object): Parameters ---------- - data : np.array + data : array Masked array to unmask. Returns ------- - np.array + array Unmasked array. """ - if isinstance(data, np.ma.MaskedArray): + if isinstance(data, ma.MaskedArray): try: - data = data.filled(np.nan) + data = data.filled(nan) except TypeError: - msg = 'Data missing values cannot be converted to np.nan.' - warnings.warn(msg) + msg = "Data missing values cannot be converted to nan." + warn(msg) sys.stderr.flush() return data - def to_dtype(self, data_type='float32'): + def to_dtype(self, data_type="float32"): """ Cast variables data into selected data type. Parameters ---------- data_type : str or Type - Data type, by default 'float32' + Data type, by default "float32" """ for var_name, var_info in self.variables.items(): - if isinstance(var_info['data'], np.ndarray): - self.variables[var_name]['data'] = self.variables[var_name]['data'].astype(data_type) - self.variables[var_name]['dtype'] = data_type + if isinstance(var_info["data"], ndarray): + self.variables[var_name]["data"] = self.variables[var_name]["data"].astype(data_type) + self.variables[var_name]["dtype"] = data_type return None @@ -2217,7 +2715,7 @@ class Nes(object): Returns ------- list - List of var names added. + A List of var names added. """ if isinstance(aux_nessy, str): @@ -2228,7 +2726,7 @@ class Nes(object): else: new = False for var_name, var_info in aux_nessy.variables.items(): - if var_info['data'] is None: + if var_info["data"] is None: aux_nessy.read_axis_limits = self.read_axis_limits aux_nessy.load(var_name) @@ -2270,7 +2768,7 @@ class Nes(object): # Writing # ================================================================================================================== - def get_write_axis_limits(self): + def _get_write_axis_limits(self): """ Calculate the 4D writing axis limits depending on if them have to balanced or not. @@ -2282,11 +2780,11 @@ class Nes(object): """ if self.balanced: - return self.get_write_axis_limits_balanced() + return self._get_write_axis_limits_balanced() else: - return self.get_write_axis_limits_unbalanced() + return self._get_write_axis_limits_unbalanced() - def get_write_axis_limits_unbalanced(self): + def _get_write_axis_limits_unbalanced(self): """ Calculate the 4D writing axis limits. @@ -2297,33 +2795,33 @@ class Nes(object): t_min, t_max, z_min, z_max, y_min, y_max, x_min and x_max. """ - axis_limits = {'x_min': None, 'x_max': None, - 'y_min': None, 'y_max': None, - 'z_min': None, 'z_max': None, - 't_min': None, 't_max': None} - - if self.parallel_method == 'Y': - y_len = self._lat['data'].shape[0] - axis_limits['y_min'] = (y_len // self.size) * self.rank + axis_limits = {"x_min": None, "x_max": None, + "y_min": None, "y_max": None, + "z_min": None, "z_max": None, + "t_min": None, "t_max": None} + my_shape = self.get_full_shape() + if self.parallel_method == "Y": + y_len = my_shape[0] + axis_limits["y_min"] = (y_len // self.size) * self.rank if self.rank + 1 < self.size: - axis_limits['y_max'] = (y_len // self.size) * (self.rank + 1) - elif self.parallel_method == 'X': - x_len = self._lon['data'].shape[-1] - axis_limits['x_min'] = (x_len // self.size) * self.rank + axis_limits["y_max"] = (y_len // self.size) * (self.rank + 1) + elif self.parallel_method == "X": + x_len = my_shape[-1] + axis_limits["x_min"] = (x_len // self.size) * self.rank if self.rank + 1 < self.size: - axis_limits['x_max'] = (x_len // self.size) * (self.rank + 1) - elif self.parallel_method == 'T': - t_len = len(self._time) - axis_limits['t_min'] = ((t_len // self.size) * self.rank) + axis_limits["x_max"] = (x_len // self.size) * (self.rank + 1) + elif self.parallel_method == "T": + t_len = len(self.get_full_times()) + axis_limits["t_min"] = ((t_len // self.size) * self.rank) if self.rank + 1 < self.size: - axis_limits['t_max'] = (t_len // self.size) * (self.rank + 1) + axis_limits["t_max"] = (t_len // self.size) * (self.rank + 1) else: raise NotImplementedError("Parallel method '{meth}' is not implemented. Use one of these: {accept}".format( - meth=self.parallel_method, accept=['X', 'Y', 'T'])) + meth=self.parallel_method, accept=["X", "Y", "T"])) return axis_limits - def get_write_axis_limits_balanced(self): + def _get_write_axis_limits_balanced(self): """ Calculate the 4D reading balanced axis limits. @@ -2333,33 +2831,33 @@ class Nes(object): Dictionary with the 4D limits of the rank data to read. t_min, t_max, z_min, z_max, y_min, y_max, x_min and x_max. """ - + my_shape = self.get_full_shape() fid_dist = {} - if self.parallel_method == 'Y': - len_to_split = self._lat['data'].shape[0] - min_axis = 'y_min' - max_axis = 'y_max' - elif self.parallel_method == 'X': - len_to_split = self._lon['data'].shape[-1] - min_axis = 'x_min' - max_axis = 'x_max' - elif self.parallel_method == 'T': - len_to_split = len(self._time) - min_axis = 't_min' - max_axis = 't_max' + if self.parallel_method == "Y": + len_to_split = my_shape[0] + min_axis = "y_min" + max_axis = "y_max" + elif self.parallel_method == "X": + len_to_split = my_shape[-1] + min_axis = "x_min" + max_axis = "x_max" + elif self.parallel_method == "T": + len_to_split = len(self.get_full_times()) + min_axis = "t_min" + max_axis = "t_max" else: raise NotImplementedError("Parallel method '{meth}' is not implemented. Use one of these: {accept}".format( - meth=self.parallel_method, accept=['X', 'Y', 'T'])) + meth=self.parallel_method, accept=["X", "Y", "T"])) procs_len = len_to_split // self.size procs_rows_extended = len_to_split - (procs_len * self.size) rows_sum = 0 for proc in range(self.size): - fid_dist[proc] = {'x_min': 0, 'x_max': None, - 'y_min': 0, 'y_max': None, - 'z_min': 0, 'z_max': None, - 't_min': 0, 't_max': None} + fid_dist[proc] = {"x_min": 0, "x_max": None, + "y_min": 0, "y_max": None, + "z_min": 0, "z_max": None, + "t_min": 0, "t_max": None} if proc < procs_rows_extended: aux_rows = procs_len + 1 else: @@ -2386,7 +2884,7 @@ class Nes(object): def _create_dimensions(self, netcdf): """ - Create 'time', 'time_bnds', 'lev', 'lon' and 'lat' dimensions. + Create "time", "time_bnds", "lev", "lon" and "lat" dimensions. Parameters ---------- @@ -2395,24 +2893,25 @@ class Nes(object): """ # Create time dimension - netcdf.createDimension('time', None) + netcdf.createDimension("time", None) # Create time_nv (number of vertices) dimension - if self._time_bnds is not None: - netcdf.createDimension('time_nv', 2) + full_time_bnds = self.get_full_time_bnds() + if full_time_bnds is not None: + netcdf.createDimension("time_nv", 2) # Create lev, lon and lat dimensions - netcdf.createDimension('lev', len(self.lev['data'])) + netcdf.createDimension("lev", len(self.lev["data"])) # Create string length dimension if self.strlen is not None: - netcdf.createDimension('strlen', self.strlen) + netcdf.createDimension("strlen", self.strlen) return None def _create_dimension_variables(self, netcdf): """ - Create the 'time', 'time_bnds', 'lev', 'lat', 'lat_bnds', 'lon' and 'lon_bnds' variables. + Create the "time", "time_bnds", "lev", "lat", "lat_bnds", "lon" and "lon_bnds" variables. Parameters ---------- @@ -2426,7 +2925,7 @@ class Nes(object): def _create_dimension_variables_32(self, netcdf): """ - Create the 'time', 'time_bnds', 'lev', 'lat', 'lat_bnds', 'lon' and 'lon_bnds' variables. + Create the "time", "time_bnds", "lev", "lat", "lat_bnds", "lon" and "lon_bnds" variables. Parameters ---------- @@ -2435,95 +2934,102 @@ class Nes(object): """ # TIMES - time_var = netcdf.createVariable('time', np.float32, ('time',), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - time_var.units = '{0} since {1}'.format(self._time_resolution, self._time[0].strftime('%Y-%m-%d %H:%M:%S')) - time_var.standard_name = 'time' - time_var.calendar = 'standard' - time_var.long_name = 'time' - if self._time_bnds is not None: + full_time = self.get_full_times() + full_time_bnds = self.get_full_time_bnds() + time_var = netcdf.createVariable("time", float32, ("time",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) + time_var.units = "{0} since {1}".format(self._time_resolution, full_time[0].strftime("%Y-%m-%d %H:%M:%S")) + time_var.standard_name = "time" + time_var.calendar = "standard" + time_var.long_name = "time" + if full_time_bnds is not None: if self._climatology: time_var.climatology = self._climatology_var_name else: - time_var.bounds = 'time_bnds' + time_var.bounds = "time_bnds" if self.size > 1: time_var.set_collective(True) - time_var[:] = date2num(self._time[:], time_var.units, time_var.calendar) + time_var[:] = date2num(full_time[:], time_var.units, time_var.calendar) # TIME BOUNDS - if self._time_bnds is not None: + if full_time_bnds is not None: if self._climatology: - time_bnds_var = netcdf.createVariable(self._climatology_var_name, np.float64, ('time', 'time_nv',), + time_bnds_var = netcdf.createVariable(self._climatology_var_name, float64, ("time", "time_nv",), zlib=self.zip_lvl, complevel=self.zip_lvl) else: - time_bnds_var = netcdf.createVariable('time_bnds', np.float64, ('time', 'time_nv',), + time_bnds_var = netcdf.createVariable("time_bnds", float64, ("time", "time_nv",), zlib=self.zip_lvl, complevel=self.zip_lvl) if self.size > 1: time_bnds_var.set_collective(True) - time_bnds_var[:] = date2num(self._time_bnds, time_var.units, calendar='standard') + time_bnds_var[:] = date2num(full_time_bnds, time_var.units, calendar="standard") # LEVELS - lev = netcdf.createVariable('lev', np.float32, ('lev',), + full_lev = self.get_full_levels() + lev = netcdf.createVariable("lev", float32, ("lev",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - if 'units' in self._lev.keys(): - lev.units = self._lev['units'] + if "units" in full_lev.keys(): + lev.units = full_lev["units"] else: - lev.units = '' - if 'positive' in self._lev.keys(): - lev.positive = self._lev['positive'] + lev.units = "" + if "positive" in full_lev.keys(): + lev.positive = full_lev["positive"] if self.size > 1: lev.set_collective(True) - lev[:] = np.array(self._lev['data'], dtype=np.float32) + lev[:] = array(full_lev["data"], dtype=float32) # LATITUDES - lat = netcdf.createVariable('lat', np.float32, self._lat_dim, + full_lat = self.get_full_latitudes() + full_lat_bnds = self.get_full_latitudes_boundaries() + lat = netcdf.createVariable("lat", float32, self._lat_dim, zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - lat.units = 'degrees_north' - lat.axis = 'Y' - lat.long_name = 'latitude coordinate' - lat.standard_name = 'latitude' - if self._lat_bnds is not None: - lat.bounds = 'lat_bnds' + lat.units = "degrees_north" + lat.axis = "Y" + lat.long_name = "latitude coordinate" + lat.standard_name = "latitude" + if full_lat_bnds is not None: + lat.bounds = "lat_bnds" if self.size > 1: lat.set_collective(True) - lat[:] = np.array(self._lat['data'], dtype=np.float32) + lat[:] = array(full_lat["data"], dtype=float32) # LATITUDES BOUNDS - if self._lat_bnds is not None: - lat_bnds_var = netcdf.createVariable('lat_bnds', np.float32, - self._lat_dim + ('spatial_nv',), + if full_lat_bnds is not None: + lat_bnds_var = netcdf.createVariable("lat_bnds", float32, + self._lat_dim + ("spatial_nv",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) if self.size > 1: lat_bnds_var.set_collective(True) - lat_bnds_var[:] = np.array(self._lat_bnds['data'], dtype=np.float32) + lat_bnds_var[:] = array(full_lat_bnds["data"], dtype=float32) # LONGITUDES - lon = netcdf.createVariable('lon', np.float32, self._lon_dim, + full_lon = self.get_full_longitudes() + full_lon_bnds = self.get_full_longitudes_boundaries() + lon = netcdf.createVariable("lon", float32, self._lon_dim, zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - lon.units = 'degrees_east' - lon.axis = 'X' - lon.long_name = 'longitude coordinate' - lon.standard_name = 'longitude' - if self._lon_bnds is not None: - lon.bounds = 'lon_bnds' + lon.units = "degrees_east" + lon.axis = "X" + lon.long_name = "longitude coordinate" + lon.standard_name = "longitude" + if full_lon_bnds is not None: + lon.bounds = "lon_bnds" if self.size > 1: lon.set_collective(True) - lon[:] = np.array(self._lon['data'], dtype=np.float32) + lon[:] = array(full_lon["data"], dtype=float32) # LONGITUDES BOUNDS - if self._lon_bnds is not None: - lon_bnds_var = netcdf.createVariable('lon_bnds', np.float32, - self._lon_dim + ('spatial_nv',), + if full_lon_bnds is not None: + lon_bnds_var = netcdf.createVariable("lon_bnds", float32, + self._lon_dim + ("spatial_nv",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) if self.size > 1: lon_bnds_var.set_collective(True) - lon_bnds_var[:] = np.array(self._lon_bnds['data'], dtype=np.float32) + lon_bnds_var[:] = array(full_lon_bnds["data"], dtype=float32) return None def _create_dimension_variables_64(self, netcdf): """ - Create the 'time', 'time_bnds', 'lev', 'lat', 'lat_bnds', 'lon' and 'lon_bnds' variables. + Create the "time", "time_bnds", "lev", "lat", "lat_bnds", "lon" and "lon_bnds" variables. Parameters ---------- @@ -2532,120 +3038,126 @@ class Nes(object): """ # TIMES - time_var = netcdf.createVariable('time', np.float64, ('time',), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - time_var.units = '{0} since {1}'.format(self._time_resolution, self._time[0].strftime('%Y-%m-%d %H:%M:%S')) - time_var.standard_name = 'time' - time_var.calendar = 'standard' - time_var.long_name = 'time' - if self._time_bnds is not None: + full_time = self.get_full_times() + full_time_bnds = self.get_full_time_bnds() + time_var = netcdf.createVariable("time", float64, ("time",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) + time_var.units = "{0} since {1}".format(self._time_resolution, full_time[0].strftime("%Y-%m-%d %H:%M:%S")) + time_var.standard_name = "time" + time_var.calendar = "standard" + time_var.long_name = "time" + if full_time_bnds is not None: if self._climatology: time_var.climatology = self._climatology_var_name else: - time_var.bounds = 'time_bnds' + time_var.bounds = "time_bnds" if self.size > 1: time_var.set_collective(True) - time_var[:] = date2num(self._time[:], time_var.units, time_var.calendar) + time_var[:] = date2num(full_time[:], time_var.units, time_var.calendar) # TIME BOUNDS - if self._time_bnds is not None: + if full_time_bnds is not None: if self._climatology: - time_bnds_var = netcdf.createVariable(self._climatology_var_name, np.float64, ('time', 'time_nv',), + time_bnds_var = netcdf.createVariable(self._climatology_var_name, float64, ("time", "time_nv",), zlib=self.zip_lvl, complevel=self.zip_lvl) else: - time_bnds_var = netcdf.createVariable('time_bnds', np.float64, ('time', 'time_nv',), + time_bnds_var = netcdf.createVariable("time_bnds", float64, ("time", "time_nv",), zlib=self.zip_lvl, complevel=self.zip_lvl) if self.size > 1: time_bnds_var.set_collective(True) - time_bnds_var[:] = date2num(self._time_bnds, time_var.units, calendar='standard') + time_bnds_var[:] = date2num(full_time_bnds, time_var.units, calendar="standard") # LEVELS - lev = netcdf.createVariable('lev', self._lev['data'].dtype, ('lev',), + full_lev = self.get_full_levels() + lev = netcdf.createVariable("lev", full_lev["data"].dtype, ("lev",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - if 'units' in self._lev.keys(): - lev.units = self._lev['units'] + if "units" in full_lev.keys(): + lev.units = full_lev["units"] else: - lev.units = '' - if 'positive' in self._lev.keys(): - lev.positive = self._lev['positive'] + lev.units = "" + if "positive" in full_lev.keys(): + lev.positive = full_lev["positive"] if self.size > 1: lev.set_collective(True) - lev[:] = self._lev['data'] + lev[:] = full_lev["data"] # LATITUDES - lat = netcdf.createVariable('lat', self._lat['data'].dtype, self._lat_dim, + full_lat = self.get_full_latitudes() + full_lat_bnds = self.get_full_latitudes_boundaries() + lat = netcdf.createVariable("lat", full_lat["data"].dtype, self._lat_dim, zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - lat.units = 'degrees_north' - lat.axis = 'Y' - lat.long_name = 'latitude coordinate' - lat.standard_name = 'latitude' - if self._lat_bnds is not None: - lat.bounds = 'lat_bnds' + lat.units = "degrees_north" + lat.axis = "Y" + lat.long_name = "latitude coordinate" + lat.standard_name = "latitude" + if full_lat_bnds is not None: + lat.bounds = "lat_bnds" if self.size > 1: lat.set_collective(True) - lat[:] = self._lat['data'] + lat[:] = full_lat["data"] # LATITUDES BOUNDS - if self._lat_bnds is not None: - lat_bnds_var = netcdf.createVariable('lat_bnds', self._lat_bnds['data'].dtype, - self._lat_dim + ('spatial_nv',), + if full_lat_bnds is not None: + lat_bnds_var = netcdf.createVariable("lat_bnds", full_lat_bnds["data"].dtype, + self._lat_dim + ("spatial_nv",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) if self.size > 1: lat_bnds_var.set_collective(True) - lat_bnds_var[:] = self._lat_bnds['data'] + lat_bnds_var[:] = full_lat_bnds["data"] # LONGITUDES - lon = netcdf.createVariable('lon', self._lon['data'].dtype, self._lon_dim, + full_lon = self.get_full_longitudes() + full_lon_bnds = self.get_full_longitudes_boundaries() + lon = netcdf.createVariable("lon", full_lon["data"].dtype, self._lon_dim, zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - lon.units = 'degrees_east' - lon.axis = 'X' - lon.long_name = 'longitude coordinate' - lon.standard_name = 'longitude' - if self._lon_bnds is not None: - lon.bounds = 'lon_bnds' + lon.units = "degrees_east" + lon.axis = "X" + lon.long_name = "longitude coordinate" + lon.standard_name = "longitude" + if full_lon_bnds is not None: + lon.bounds = "lon_bnds" if self.size > 1: lon.set_collective(True) - lon[:] = self._lon['data'] + lon[:] = full_lon["data"] # LONGITUDES BOUNDS - if self._lon_bnds is not None: - lon_bnds_var = netcdf.createVariable('lon_bnds', self._lon_bnds['data'].dtype, - self._lon_dim + ('spatial_nv',), + if full_lon_bnds is not None: + lon_bnds_var = netcdf.createVariable("lon_bnds", full_lon_bnds["data"].dtype, + self._lon_dim + ("spatial_nv",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) if self.size > 1: lon_bnds_var.set_collective(True) - lon_bnds_var[:] = self._lon_bnds['data'] + lon_bnds_var[:] = full_lon_bnds["data"] return None - def _create_cell_measures(self, netcdf): # CELL AREA - if 'cell_area' in self.cell_measures.keys(): - cell_area = netcdf.createVariable('cell_area', self.cell_measures['cell_area']['data'].dtype, self._var_dim, + if "cell_area" in self.cell_measures.keys(): + cell_area = netcdf.createVariable("cell_area", self.cell_measures["cell_area"]["data"].dtype, self._var_dim, zlib=self.zip_lvl > 0, complevel=self.zip_lvl) if self.size > 1: cell_area.set_collective(True) - cell_area[self.read_axis_limits['y_min']:self.read_axis_limits['y_max'], - self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] = \ - self.cell_measures['cell_area']['data'] + cell_area[self.read_axis_limits["y_min"]:self.read_axis_limits["y_max"], + self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] = \ + self.cell_measures["cell_area"]["data"] - cell_area.long_name = 'area of grid cell' - cell_area.standard_name = 'cell_area' - cell_area.units = 'm2' + cell_area.long_name = "area of grid cell" + cell_area.standard_name = "cell_area" + cell_area.units = "m2" for var_name in self.variables.keys(): - self.variables[var_name]['cell_measures'] = 'area: cell_area' + self.variables[var_name]["cell_measures"] = "area: cell_area" if self.info: print("Rank {0:03d}: Cell measures done".format(self.rank)) return None - def str2char(self, data): + def _str2char(self, data): if self.strlen is None: - msg = 'String data could not be converted into chars while writing.' + msg = "String data could not be converted into chars while writing." msg += " Please, set the maximum string length (set_strlen) before writing." raise RuntimeError(msg) @@ -2656,7 +3168,7 @@ class Nes(object): data = data.flatten() # Split strings into chars (S1) - data_aux = stringtochar(np.array([v.encode('ascii', 'ignore') for v in data]).astype('S' + str(self.strlen))) + data_aux = stringtochar(array([v.encode("ascii", "ignore") for v in data]).astype("S" + str(self.strlen))) data_aux = data_aux.reshape(data_new_shape) return data_aux @@ -2674,49 +3186,49 @@ class Nes(object): """ for i, (var_name, var_dict) in enumerate(self.variables.items()): - if isinstance(var_dict['data'], int) and var_dict['data'] == 0: - var_dims = ('time', 'lev',) + self._var_dim - var_dtype = np.float32 + if isinstance(var_dict["data"], int) and var_dict["data"] == 0: + var_dims = ("time", "lev",) + self._var_dim + var_dtype = float32 else: # Get dimensions - if (var_dict['data'] is None) or (len(var_dict['data'].shape) == 4): - var_dims = ('time', 'lev',) + self._var_dim + if (var_dict["data"] is None) or (len(var_dict["data"].shape) == 4): + var_dims = ("time", "lev",) + self._var_dim else: var_dims = self._var_dim # Get data type - if 'dtype' in var_dict.keys(): - var_dtype = var_dict['dtype'] - if (var_dict['data'] is not None) and (var_dtype != var_dict['data'].dtype): + if "dtype" in var_dict.keys(): + var_dtype = var_dict["dtype"] + if (var_dict["data"] is not None) and (var_dtype != var_dict["data"].dtype): msg = "WARNING!!! " msg += "Different data types for variable {0}. ".format(var_name) - msg += "Input dtype={0}. Data dtype={1}.".format(var_dtype, var_dict['data'].dtype) - warnings.warn(msg) + msg += "Input dtype={0}. Data dtype={1}.".format(var_dtype, var_dict["data"].dtype) + warn(msg) sys.stderr.flush() try: - var_dict['data'] = var_dict['data'].astype(var_dtype) + var_dict["data"] = var_dict["data"].astype(var_dtype) except Exception as e: # TODO: Detect exception print(e) raise TypeError("It was not possible to cast the data to the input dtype.") else: - var_dtype = var_dict['data'].dtype + var_dtype = var_dict["data"].dtype if var_dtype is object: raise TypeError("Data dtype is object. Define dtype explicitly as dictionary key 'dtype'") - if var_dict['data'] is not None: + if var_dict["data"] is not None: # Ensure data is of type numpy array (to create NES) - if not isinstance(var_dict['data'], (np.ndarray, np.generic)): + if not isinstance(var_dict["data"], (ndarray, generic)): try: - var_dict['data'] = np.array(var_dict['data']) + var_dict["data"] = array(var_dict["data"]) except AttributeError: raise AttributeError("Data for variable {0} must be a numpy array.".format(var_name)) # Convert list of strings to chars for parallelization - if np.issubdtype(var_dtype, np.character): - var_dict['data_aux'] = self.str2char(var_dict['data']) - var_dims += ('strlen',) - var_dtype = 'S1' + if issubdtype(var_dtype, character): + var_dict["data_aux"] = self._str2char(var_dict["data"]) + var_dims += ("strlen",) + var_dtype = "S1" if self.info: print("Rank {0:03d}: Writing {1} var ({2}/{3})".format( @@ -2729,7 +3241,7 @@ class Nes(object): if self.balanced: raise NotImplementedError("A balanced data cannot be chunked.") if self.master: - chunk_size = var_dict['data'].shape + chunk_size = var_dict["data"].shape else: chunk_size = None chunk_size = self.comm.bcast(chunk_size, root=0) @@ -2746,51 +3258,51 @@ class Nes(object): self.rank, var_name, i + 1, len(self.variables))) for att_name, att_value in var_dict.items(): - if att_name == 'data': + if att_name == "data": if att_value is not None: if self.info: - print("Rank {0:03d}: Filling {1})".format(self.rank, var_name)) - if 'data_aux' in var_dict.keys(): - att_value = var_dict['data_aux'] + print("Rank {0:03d}: Filling {1}".format(self.rank, var_name)) + if "data_aux" in var_dict.keys(): + att_value = var_dict["data_aux"] if isinstance(att_value, int) and att_value == 0: - var[self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], - self.write_axis_limits['z_min']:self.write_axis_limits['z_max'], - self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max']] = 0 + var[self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], + self.write_axis_limits["z_min"]:self.write_axis_limits["z_max"], + self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] = 0 elif len(att_value.shape) == 5: - if 'strlen' in var_dims: - var[self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], - self.write_axis_limits['z_min']:self.write_axis_limits['z_max'], - self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], + if "strlen" in var_dims: + var[self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], + self.write_axis_limits["z_min"]:self.write_axis_limits["z_max"], + self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], :] = att_value else: - raise NotImplementedError('It is not possible to write 5D variables.') + raise NotImplementedError("It is not possible to write 5D variables.") elif len(att_value.shape) == 4: - var[self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], - self.write_axis_limits['z_min']:self.write_axis_limits['z_max'], - self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max']] = att_value + var[self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], + self.write_axis_limits["z_min"]:self.write_axis_limits["z_max"], + self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] = att_value elif len(att_value.shape) == 3: - if 'strlen' in var_dims: - var[self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], + if "strlen" in var_dims: + var[self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], :] = att_value else: - raise NotImplementedError('It is not possible to write 3D variables.') + raise NotImplementedError("It is not possible to write 3D variables.") if self.info: print("Rank {0:03d}: Var {1} data ({2}/{3})".format( self.rank, var_name, i + 1, len(self.variables))) - elif att_name not in ['chunk_size', 'var_dims', 'dimensions', 'dtype', 'data_aux']: + elif att_name not in ["chunk_size", "var_dims", "dimensions", "dtype", "data_aux"]: var.setncattr(att_name, att_value) - if 'data_aux' in var_dict.keys(): - del var_dict['data_aux'] + if "data_aux" in var_dict.keys(): + del var_dict["data_aux"] self._set_var_crs(var) if self.info: @@ -2799,7 +3311,7 @@ class Nes(object): return None - def append_time_step_data(self, i_time, out_format='DEFAULT'): + def append_time_step_data(self, i_time, out_format="DEFAULT"): """ Fill the netCDF data for the indicated index time. @@ -2821,33 +3333,33 @@ class Nes(object): self.serial_nc.append_time_step_data(i_time, out_format=out_format) self.comm.Barrier() else: - if out_format == 'MONARCH': + if out_format == "MONARCH": self.variables = to_monarch_units(self) - elif out_format == 'CMAQ': + elif out_format == "CMAQ": self.variables = to_cmaq_units(self) - elif out_format == 'WRF_CHEM': + elif out_format == "WRF_CHEM": self.variables = to_wrf_chem_units(self) for i, (var_name, var_dict) in enumerate(self.variables.items()): for att_name, att_value in var_dict.items(): - if att_name == 'data': + if att_name == "data": if att_value is not None: if self.info: - print("Rank {0:03d}: Filling {1})".format(self.rank, var_name)) + print("Rank {0:03d}: Filling {1}".format(self.rank, var_name)) var = self.dataset.variables[var_name] if isinstance(att_value, int) and att_value == 0: var[i_time, - self.write_axis_limits['z_min']:self.write_axis_limits['z_max'], - self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max']] = 0 + self.write_axis_limits["z_min"]:self.write_axis_limits["z_max"], + self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] = 0 elif len(att_value.shape) == 4: var[i_time, - self.write_axis_limits['z_min']:self.write_axis_limits['z_max'], - self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max']] = att_value + self.write_axis_limits["z_min"]:self.write_axis_limits["z_max"], + self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] = att_value elif len(att_value.shape) == 3: - raise NotImplementedError('It is not possible to write 3D variables.') + raise NotImplementedError("It is not possible to write 3D variables.") else: raise NotImplementedError("SHAPE APPEND ERROR: {0}".format(att_value.shape)) if self.info: @@ -2915,9 +3427,9 @@ class Nes(object): if self.info: print("Rank {0:03d}: Creating {1}".format(self.rank, path)) if self.size > 1: - netcdf = Dataset(path, format="NETCDF4", mode='w', parallel=True, comm=self.comm, info=MPI.Info()) + netcdf = Dataset(path, format="NETCDF4", mode="w", parallel=True, comm=self.comm, info=MPI.Info()) else: - netcdf = Dataset(path, format="NETCDF4", mode='w', parallel=False) + netcdf = Dataset(path, format="NETCDF4", mode="w", parallel=False) if self.info: print("Rank {0:03d}: NetCDF ready to write".format(self.rank)) @@ -2942,7 +3454,7 @@ class Nes(object): if self.global_attrs is not None: for att_name, att_value in self.global_attrs.items(): netcdf.setncattr(att_name, att_value) - netcdf.setncattr('Conventions', 'CF-1.7') + netcdf.setncattr("Conventions", "CF-1.7") if keep_open: self.dataset = netcdf @@ -2954,7 +3466,7 @@ class Nes(object): def __to_netcdf_cams_ra(self, path): return to_netcdf_cams_ra(self, path) - def to_netcdf(self, path, compression_level=0, serial=False, info=False, chunking=False, type='NES', + def to_netcdf(self, path, compression_level=0, serial=False, info=False, chunking=False, nc_type="NES", keep_open=False): """ Write the netCDF output file. @@ -2971,12 +3483,12 @@ class Nes(object): Indicates if you want to print the information of each writing step by stdout Default: False. chunking : bool Indicates if you want a chunked netCDF output. Only available with non-serial writes. Default: False. - type : str - Type to NetCDf to write. 'CAMS_RA' or 'NES' + nc_type : str + Type to NetCDf to write. "CAMS_RA" or "NES" keep_open : bool Indicates if you want to keep open the NetCDH to fill the data by time-step """ - nc_type = type + nc_type = nc_type old_info = self.info self.info = info self.serial_nc = None @@ -2997,36 +3509,36 @@ class Nes(object): new_nc.set_communicator(MPI.COMM_SELF) new_nc.variables = data new_nc.cell_measures = c_measures - if type in ['NES', 'DEFAULT']: + if nc_type in ["NES", "DEFAULT"]: new_nc.__to_netcdf_py(path, keep_open=keep_open) - elif type == 'CAMS_RA': + elif nc_type == "CAMS_RA": new_nc.__to_netcdf_cams_ra(path) - elif type == 'MONARCH': + elif nc_type == "MONARCH": to_netcdf_monarch(new_nc, path, chunking=chunking, keep_open=keep_open) - elif type == 'CMAQ': + elif nc_type == "CMAQ": to_netcdf_cmaq(new_nc, path, keep_open=keep_open) - elif type == 'WRF_CHEM': + elif nc_type == "WRF_CHEM": to_netcdf_wrf_chem(new_nc, path, keep_open=keep_open) else: - msg = "Unknown NetCDF type '{0}'. ".format(nc_type) + msg = f"Unknown NetCDF type '{nc_type}'. " msg += "Use CAMS_RA, MONARCH or NES (or DEFAULT)" raise ValueError(msg) self.serial_nc = new_nc else: self.serial_nc = True else: - if type in ['NES', 'DEFAULT']: + if nc_type in ["NES", "DEFAULT"]: self.__to_netcdf_py(path, chunking=chunking, keep_open=keep_open) - elif nc_type == 'CAMS_RA': + elif nc_type == "CAMS_RA": self.__to_netcdf_cams_ra(path) - elif nc_type == 'MONARCH': + elif nc_type == "MONARCH": to_netcdf_monarch(self, path, chunking=chunking, keep_open=keep_open) - elif nc_type == 'CMAQ': + elif nc_type == "CMAQ": to_netcdf_cmaq(self, path, keep_open=keep_open) - elif nc_type == 'WRF_CHEM': + elif nc_type == "WRF_CHEM": to_netcdf_wrf_chem(self, path, keep_open=keep_open) else: - msg = "Unknown NetCDF type '{0}'. ".format(nc_type) + msg = f"Unknown NetCDF type '{nc_type}''. " msg += "Use CAMS_RA, MONARCH or NES (or DEFAULT)" raise ValueError(msg) @@ -3062,16 +3574,16 @@ class Nes(object): from eccodes import codes_write from eccodes import codes_release - fout = open(path, 'wb') + fout = open(path, "wb") # read template - fin = open(grib_template_path, 'rb') + fin = open(grib_template_path, "rb") gid = codes_grib_new_from_file(fin) if gid is None: sys.exit(1) - iterid = codes_keys_iterator_new(gid, 'ls') + iterid = codes_keys_iterator_new(gid, "ls") while codes_keys_iterator_next(iterid): keyname = codes_keys_iterator_get_name(iterid) keyval = codes_get_string(gid, keyname) @@ -3081,49 +3593,48 @@ class Nes(object): codes_keys_iterator_delete(iterid) for var_name, var_info in self.variables.items(): for i_time, time in enumerate(self.time): - for i_lev, lev in enumerate(self.lev['data']): + for i_lev, lev in enumerate(self.lev["data"]): clone_id = codes_clone(gid) # Adding grib2 keys to file for key, value in grib_keys.items(): - if value not in ['', 'None', None, np.nan]: + if value not in ["", "None", None, nan]: try: codes_set(clone_id, key, value) except Exception as e: - print("Something went wrong while writing the Grib key '{0}': {1}".format(key, value)) + print(f"Something went wrong while writing the Grib key '{key}': {value}") raise e # Time dependent keys - if 'dataTime' in grib_keys.keys() and grib_keys['dataTime'] in ['', 'None', None, np.nan]: - codes_set(clone_id, 'dataTime', int(i_time * 100)) - if 'stepRange' in grib_keys.keys() and grib_keys['stepRange'] in ['', 'None', None, np.nan]: - n_secs = (time - self._time[0]).total_seconds() - codes_set(clone_id, 'stepRange', int(n_secs // 3600)) - if 'forecastTime' in grib_keys.keys() and grib_keys['forecastTime'] in ['', 'None', None, np.nan]: - n_secs = (time - self._time[0]).total_seconds() - codes_set(clone_id, 'forecastTime', int(n_secs)) + if "dataTime" in grib_keys.keys() and grib_keys["dataTime"] in ["", "None", None, nan]: + codes_set(clone_id, "dataTime", int(i_time * 100)) + if "stepRange" in grib_keys.keys() and grib_keys["stepRange"] in ["", "None", None, nan]: + n_secs = (time - self.get_full_times()[0]).total_seconds() + codes_set(clone_id, "stepRange", int(n_secs // 3600)) + if "forecastTime" in grib_keys.keys() and grib_keys["forecastTime"] in ["", "None", None, nan]: + n_secs = (time - self.get_full_times()[0]).total_seconds() + codes_set(clone_id, "forecastTime", int(n_secs)) # Level dependent keys - if 'typeOfFirstFixedSurface' in grib_keys.keys() and \ - grib_keys['typeOfFirstFixedSurface'] in ['', 'None', None, np.nan]: + if "typeOfFirstFixedSurface" in grib_keys.keys() and \ + grib_keys["typeOfFirstFixedSurface"] in ["", "None", None, nan]: if float(lev) == 0: - codes_set(clone_id, 'typeOfFirstFixedSurface', 1) - # grib_keys['typeOfFirstFixedSurface'] = 1 + codes_set(clone_id, "typeOfFirstFixedSurface", 1) + # grib_keys["typeOfFirstFixedSurface"] = 1 else: - codes_set(clone_id, 'typeOfFirstFixedSurface', 103) - # grib_keys['typeOfFirstFixedSurface'] = 103 - if 'level' in grib_keys.keys() and grib_keys['level'] in ['', 'None', None, np.nan]: - codes_set(clone_id, 'level', float(lev)) + codes_set(clone_id, "typeOfFirstFixedSurface", 103) + # grib_keys["typeOfFirstFixedSurface"] = 103 + if "level" in grib_keys.keys() and grib_keys["level"] in ["", "None", None, nan]: + codes_set(clone_id, "level", float(lev)) - newval = var_info['data'][i_time, i_lev, :, :] + newval = var_info["data"][i_time, i_lev, :, :] if lat_flip: - newval = np.flipud(newval) + newval = flipud(newval) # TODO Check default NaN Value - newval[np.isnan(newval)] = 0. + newval[isnan(newval)] = 0. - codes_set_values(clone_id, np.array(newval.ravel(), dtype='float64')) - # codes_set_values(clone_id, newval.ravel()) + codes_set_values(clone_id, array(newval.ravel(), dtype="float64")) codes_write(clone_id, fout) del newval codes_release(gid) @@ -3151,7 +3662,7 @@ class Nes(object): """ # if serial: - if self.parallel_method in ['X', 'Y'] and self.size > 1: + if self.parallel_method in ["X", "Y"] and self.size > 1: try: data = self._gather_data(self.variables) except KeyError: @@ -3173,7 +3684,7 @@ class Nes(object): def create_shapefile(self): """ - Create spatial geodataframe (shapefile). + Create spatial GeoDataFrame (shapefile). Returns ------- @@ -3183,25 +3694,25 @@ class Nes(object): if self.shapefile is None: - if self._lat_bnds is None or self._lon_bnds is None: + if self.lat_bnds is None or self.lon_bnds is None: self.create_spatial_bounds() # Reshape arrays to create geometry - aux_shape = (self.lat_bnds['data'].shape[0], self.lon_bnds['data'].shape[0], 4) - lon_bnds_aux = np.empty(aux_shape) - lon_bnds_aux[:, :, 0] = self.lon_bnds['data'][np.newaxis, :, 0] - lon_bnds_aux[:, :, 1] = self.lon_bnds['data'][np.newaxis, :, 1] - lon_bnds_aux[:, :, 2] = self.lon_bnds['data'][np.newaxis, :, 1] - lon_bnds_aux[:, :, 3] = self.lon_bnds['data'][np.newaxis, :, 0] + aux_shape = (self.lat_bnds["data"].shape[0], self.lon_bnds["data"].shape[0], 4) + lon_bnds_aux = empty(aux_shape) + lon_bnds_aux[:, :, 0] = self.lon_bnds["data"][newaxis, :, 0] + lon_bnds_aux[:, :, 1] = self.lon_bnds["data"][newaxis, :, 1] + lon_bnds_aux[:, :, 2] = self.lon_bnds["data"][newaxis, :, 1] + lon_bnds_aux[:, :, 3] = self.lon_bnds["data"][newaxis, :, 0] lon_bnds = lon_bnds_aux del lon_bnds_aux - lat_bnds_aux = np.empty(aux_shape) - lat_bnds_aux[:, :, 0] = self.lat_bnds['data'][:, np.newaxis, 0] - lat_bnds_aux[:, :, 1] = self.lat_bnds['data'][:, np.newaxis, 0] - lat_bnds_aux[:, :, 2] = self.lat_bnds['data'][:, np.newaxis, 1] - lat_bnds_aux[:, :, 3] = self.lat_bnds['data'][:, np.newaxis, 1] + lat_bnds_aux = empty(aux_shape) + lat_bnds_aux[:, :, 0] = self.lat_bnds["data"][:, newaxis, 0] + lat_bnds_aux[:, :, 1] = self.lat_bnds["data"][:, newaxis, 0] + lat_bnds_aux[:, :, 2] = self.lat_bnds["data"][:, newaxis, 1] + lat_bnds_aux[:, :, 3] = self.lat_bnds["data"][:, newaxis, 1] lat_bnds = lat_bnds_aux del lat_bnds_aux @@ -3217,13 +3728,9 @@ class Nes(object): (aux_b_lons[i, 2], aux_b_lats[i, 2]), (aux_b_lons[i, 3], aux_b_lats[i, 3]), (aux_b_lons[i, 0], aux_b_lats[i, 0])])) - fids = np.arange(len(self._lat['data']) * len(self._lon['data'])) - fids = fids.reshape((len(self._lat['data']), len(self._lon['data']))) - fids = fids[self.read_axis_limits['y_min']:self.read_axis_limits['y_max'], - self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] - gdf = gpd.GeoDataFrame(index=pd.Index(name='FID', data=fids.ravel()), - geometry=geometry, - crs="EPSG:4326") + + fids = self.get_fids() + gdf = GeoDataFrame(index=Index(name="FID", data=fids.ravel()), geometry=geometry, crs="EPSG:4326") self.shapefile = gdf else: @@ -3233,7 +3740,7 @@ class Nes(object): def write_shapefile(self, path): """ - Save spatial geodataframe (shapefile). + Save spatial GeoDataFrame (shapefile). Parameters ---------- @@ -3242,7 +3749,7 @@ class Nes(object): """ if self.shapefile is None: - raise ValueError('Shapefile was not created.') + raise ValueError("Shapefile was not created.") if self.size == 1: # In serial, avoid gather @@ -3251,7 +3758,7 @@ class Nes(object): # In parallel data = self.comm.gather(self.shapefile, root=0) if self.master: - data = pd.concat(data) + data = concat(data) data.to_file(path) return None @@ -3268,7 +3775,7 @@ class Nes(object): ---------- path : str Path to the output file. - time : datetime.datetime + time : datetime Time stamp to select. lev : int Vertical level to select. @@ -3286,32 +3793,32 @@ class Nes(object): # Add warning for unloaded variables unloaded_vars = [] for var_name in var_list: - if self.variables[var_name]['data'] is None: + if self.variables[var_name]["data"] is None: unloaded_vars.append(var_name) if len(unloaded_vars) > 0: - raise ValueError('The variables {0} need to be loaded/created before using to_shapefile.'.format( + raise ValueError("The variables {0} need to be loaded/created before using to_shapefile.".format( unloaded_vars)) # Select first vertical level (if needed) if lev is None: - msg = 'No vertical level has been specified. The first one will be selected.' - warnings.warn(msg) + msg = "No vertical level has been specified. The first one will be selected." + warn(msg) sys.stderr.flush() idx_lev = 0 else: - if lev not in self.lev['data']: - raise ValueError('Level {} is not available. Choose from {}'.format(lev, self.lev['data'])) + if lev not in self.lev["data"]: + raise ValueError("Level {} is not available. Choose from {}".format(lev, self.lev["data"])) idx_lev = lev # Select first time (if needed) if time is None: - msg = 'No time has been specified. The first one will be selected.' - warnings.warn(msg) + msg = "No time has been specified. The first one will be selected." + warn(msg) sys.stderr.flush() idx_time = 0 else: if time not in self.time: - raise ValueError('Time {} is not available. Choose from {}'.format(time, self.time)) + raise ValueError("Time {} is not available. Choose from {}".format(time, self.time)) idx_time = self.time.index(time) # Create shapefile @@ -3338,7 +3845,7 @@ class Nes(object): """ for var_name in var_list: - self.shapefile[var_name] = self.variables[var_name]['data'][idx_time, idx_lev, :].ravel() + self.shapefile[var_name] = self.variables[var_name]["data"][idx_time, idx_lev, :].ravel() return None @@ -3354,19 +3861,14 @@ class Nes(object): # Get centroids from coordinates centroids = [] - for lat_ind in range(0, len(self.lat['data'])): - for lon_ind in range(0, len(self.lon['data'])): - centroids.append(Point(self.lon['data'][lon_ind], - self.lat['data'][lat_ind])) + for lat_ind in range(0, len(self.lat["data"])): + for lon_ind in range(0, len(self.lon["data"])): + centroids.append(Point(self.lon["data"][lon_ind], + self.lat["data"][lat_ind])) - # Create dataframe cointaining all points - fids = np.arange(len(self._lat['data']) * len(self._lon['data'])) - fids = fids.reshape((len(self._lat['data']), len(self._lon['data']))) - fids = fids[self.read_axis_limits['y_min']:self.read_axis_limits['y_max'], - self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] - centroids_gdf = gpd.GeoDataFrame(index=pd.Index(name='FID', data=fids.ravel()), - geometry=centroids, - crs="EPSG:4326") + # Create dataframe containing all points + fids = self.get_fids() + centroids_gdf = GeoDataFrame(index=Index(name="FID", data=fids.ravel()), geometry=centroids, crs="EPSG:4326") return centroids_gdf @@ -3384,11 +3886,11 @@ class Nes(object): for var_name in data_list.keys(): try: # noinspection PyArgumentList - data_aux = self.comm.gather(data_list[var_name]['data'], root=0) + data_aux = self.comm.gather(data_list[var_name]["data"], root=0) if self.rank == 0: - shp_len = len(data_list[var_name]['data'].shape) + shp_len = len(data_list[var_name]["data"].shape) add_dimension = False # to Add a dimension - if self.parallel_method == 'Y': + if self.parallel_method == "Y": if shp_len == 2: # if is a 2D concatenate over first axis axis = 0 @@ -3398,7 +3900,7 @@ class Nes(object): else: # if is a 4D concatenate over third axis axis = 2 - elif self.parallel_method == 'X': + elif self.parallel_method == "X": if shp_len == 2: # if is a 2D concatenate over second axis axis = 1 @@ -3408,7 +3910,7 @@ class Nes(object): else: # if is a 4D concatenate over forth axis axis = 3 - elif self.parallel_method == 'T': + elif self.parallel_method == "T": if shp_len == 2: # if is a 2D add dimension add_dimension = True @@ -3422,21 +3924,19 @@ class Nes(object): else: raise NotImplementedError( "Parallel method '{meth}' is not implemented. Use one of these: {accept}".format( - meth=self.parallel_method, accept=['X', 'Y', 'T'])) + meth=self.parallel_method, accept=["X", "Y", "T"])) if add_dimension: - data_list[var_name]['data'] = np.stack(data_aux) + data_list[var_name]["data"] = stack(data_aux) else: - data_list[var_name]['data'] = np.concatenate(data_aux, axis=axis) + data_list[var_name]["data"] = concatenate(data_aux, axis=axis) except Exception as e: - print("**ERROR** an error has occurred while gathering the '{0}' variable.\n".format(var_name)) - sys.stderr.write("**ERROR** an error has occurred while gathering the '{0}' variable.\n".format( - var_name)) + msg = f"**ERROR** an error has occurred while gathering the '{var_name}' variable.\n" + print(msg) + sys.stderr.write(msg) print(e) sys.stderr.write(str(e)) - # print(e, file=sys.stderr) sys.stderr.flush() self.comm.Abort(1) - raise e return data_list @@ -3454,32 +3954,31 @@ class Nes(object): for var_name in data_list.keys(): if self.info and self.master: print("Gathering {0}".format(var_name)) - if data_list[var_name]['data'] is None: - data_list[var_name]['data'] = None - elif isinstance(data_list[var_name]['data'], int) and data_list[var_name]['data'] == 0: - data_list[var_name]['data'] = 0 + if data_list[var_name]["data"] is None: + data_list[var_name]["data"] = None + elif isinstance(data_list[var_name]["data"], int) and data_list[var_name]["data"] == 0: + data_list[var_name]["data"] = 0 else: - shp_len = len(data_list[var_name]['data'].shape) + shp_len = len(data_list[var_name]["data"].shape) # Collect local array sizes using the gather communication pattern - rank_shapes = np.array(self.comm.gather(data_list[var_name]['data'].shape, root=0)) - sendbuf = data_list[var_name]['data'].flatten() - sendcounts = np.array(self.comm.gather(len(sendbuf), root=0)) + rank_shapes = array(self.comm.gather(data_list[var_name]["data"].shape, root=0)) + sendbuf = data_list[var_name]["data"].flatten() + sendcounts = array(self.comm.gather(len(sendbuf), root=0)) if self.master: - # recvbuf = np.empty(sum(sendcounts), dtype=type(sendbuf[0])) - recvbuf = np.empty(sum(sendcounts), dtype=type(sendbuf.max())) + recvbuf = empty(sum(sendcounts), dtype=type(sendbuf.max())) else: recvbuf = None self.comm.Gatherv(sendbuf=sendbuf, recvbuf=(recvbuf, sendcounts), root=0) if self.master: - recvbuf = np.split(recvbuf, np.cumsum(sendcounts)) + recvbuf = split(recvbuf, cumsum(sendcounts)) # TODO ask - # I don't understand why it is giving one more split + # I don"t understand why it is giving one more split if len(recvbuf) > len(sendcounts): recvbuf = recvbuf[:-1] for i, shape in enumerate(rank_shapes): recvbuf[i] = recvbuf[i].reshape(shape) add_dimension = False # to Add a dimension - if self.parallel_method == 'Y': + if self.parallel_method == "Y": if shp_len == 2: # if is a 2D concatenate over first axis axis = 0 @@ -3489,7 +3988,7 @@ class Nes(object): else: # if is a 4D concatenate over third axis axis = 2 - elif self.parallel_method == 'X': + elif self.parallel_method == "X": if shp_len == 2: # if is a 2D concatenate over second axis axis = 1 @@ -3499,7 +3998,7 @@ class Nes(object): else: # if is a 4D concatenate over forth axis axis = 3 - elif self.parallel_method == 'T': + elif self.parallel_method == "T": if shp_len == 2: # if is a 2D add dimension add_dimension = True @@ -3513,11 +4012,11 @@ class Nes(object): else: raise NotImplementedError( "Parallel method '{meth}' is not implemented. Use one of these: {accept}".format( - meth=self.parallel_method, accept=['X', 'Y', 'T'])) + meth=self.parallel_method, accept=["X", "Y", "T"])) if add_dimension: - data_list[var_name]['data'] = np.stack(recvbuf) + data_list[var_name]["data"] = stack(recvbuf) else: - data_list[var_name]['data'] = np.concatenate(recvbuf, axis=axis) + data_list[var_name]["data"] = concatenate(recvbuf, axis=axis) return data_list @@ -3529,29 +4028,29 @@ class Nes(object): """ # Convert observational/model geographic longitude/latitude coordinates to cartesian ECEF (Earth Centred, # Earth Fixed) coordinates, assuming WGS84 datum and ellipsoid, and that all heights = 0. - # ECEF coordiantes represent positions (in meters) as X, Y, Z coordinates, approximating the earth surface + # ECEF coordinates represent positions (in meters) as X, Y, Z coordinates, approximating the earth surface # as an ellipsoid of revolution. - # This conversion is for the subsequent calculation of euclidean distances of the model gridcell centres + # This conversion is for the subsequent calculation of Euclidean distances of the model grid cell centres # from each observational station. - # Defining the distance between two points on the earth's surface as simply the euclidean distance + # Defining the distance between two points on the earth's surface as simply the Euclidean distance # between the two lat/lon pairs could lead to inaccurate results depending on the distance # between two points (i.e. 1 deg. of longitude varies with latitude). Parameters ---------- - lon : np.array + lon : array Longitude values. - lat : np.array + lat : array Latitude values. """ - lla = pyproj.Proj(proj='latlong', ellps='WGS84', datum='WGS84') - ecef = pyproj.Proj(proj='geocent', ellps='WGS84', datum='WGS84') - # x, y, z = pyproj.transform(lla, ecef, lon, lat, np.zeros(lon.shape), radians=False) + lla = Proj(proj="latlong", ellps="WGS84", datum="WGS84") + ecef = Proj(proj="geocent", ellps="WGS84", datum="WGS84") + # x, y, z = pyproj.transform(lla, ecef, lon, lat, zeros(lon.shape), radians=False) # Deprecated: https://pyproj4.github.io/pyproj/stable/gotchas.html#upgrading-to-pyproj-2-from-pyproj-1 - transformer = pyproj.Transformer.from_proj(lla, ecef) - x, y, z = transformer.transform(lon, lat, np.zeros(lon.shape), radians=False) - return np.column_stack([x, y, z]) + transformer = Transformer.from_proj(lla, ecef) + x, y, z = transformer.transform(lon, lat, zeros(lon.shape), radians=False) + return column_stack([x, y, z]) def add_4d_vertical_info(self, info_to_add): """ @@ -3566,7 +4065,7 @@ class Nes(object): return vertical_interpolation.add_4d_vertical_info(self, info_to_add) - def interpolate_vertical(self, new_levels, new_src_vertical=None, kind='linear', extrapolate=None, info=None, + def interpolate_vertical(self, new_levels, new_src_vertical=None, kind="linear", extrapolate=None, info=None, overwrite=False): """ Vertical interpolation function. @@ -3576,7 +4075,7 @@ class Nes(object): self : Nes Source Nes object. new_levels : List - List of new vertical levels. + A List of new vertical levels. new_src_vertical : nes.Nes, str Nes object with the vertical information as variable or str with the path to the NetCDF file that contains the vertical data. @@ -3584,16 +4083,16 @@ class Nes(object): Vertical methods type. extrapolate : bool or tuple or None or number or NaN If bool: - - If True, both extrapolation options are set to 'extrapolate'. - - If False, extrapolation options are set to ('bottom', 'top'). + - If True, both extrapolation options are set to "extrapolate". + - If False, extrapolation options are set to ("bottom", "top"). If tuple: - The first element represents the extrapolation option for the lower bound. - The second element represents the extrapolation option for the upper bound. - If any element is bool: - - If True, it represents 'extrapolate'. + - If True, it represents "extrapolate". - If False: - - If it's the first element, it represents 'bottom'. - - If it's the second element, it represents 'top'. + - If it"s the first element, it represents "bottom". + - If it"s the second element, it represents "top". - If any element is None, it is replaced with numpy.nan. - Other numeric values are kept as they are. - If any element is NaN, it is kept as NaN. @@ -3613,7 +4112,7 @@ class Nes(object): self, new_levels, new_src_vertical=new_src_vertical, kind=kind, extrapolate_options=extrapolate, info=info, overwrite=overwrite) - def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind='NearestNeighbour', n_neighbours=4, + def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind="NearestNeighbour", n_neighbours=4, info=False, to_providentia=False, only_create_wm=False, wm=None, flux=False): """ Horizontal methods from the current grid to another one. @@ -3625,7 +4124,7 @@ class Nes(object): weight_matrix_path : str, None Path to the weight matrix to read/create. kind : str - Kind of horizontal methods. choices = ['NearestNeighbour', 'Conservative']. + Kind of horizontal methods. choices = ["NearestNeighbour", "Conservative"]. n_neighbours: int Used if kind == NearestNeighbour. Number of nearest neighbours to interpolate. Default: 4. info: bool @@ -3653,7 +4152,7 @@ class Nes(object): ext_shp : GeoPandasDataFrame or str File or path from where the data will be obtained on the intersection. method : str - Overlay method. Accepted values: ['nearest', 'intersection', 'centroid']. + Overlay method. Accepted values: ["nearest", "intersection", "centroid"]. var_list : List or None Variables that will be included in the resulting shapefile. info : bool @@ -3677,12 +4176,12 @@ class Nes(object): Indicates if we want to overwrite the grid area. """ - if ('cell_area' not in self.cell_measures.keys()) or (overwrite): + if ("cell_area" not in self.cell_measures.keys()) or overwrite: grid_area = cell_measures.calculate_grid_area(self) - grid_area = grid_area.reshape([self.lat['data'].shape[0], self.lon['data'].shape[-1]]) - self.cell_measures['cell_area'] = {'data': grid_area} + grid_area = grid_area.reshape([self.lat["data"].shape[0], self.lon["data"].shape[-1]]) + self.cell_measures["cell_area"] = {"data": grid_area} else: - grid_area = self.cell_measures['cell_area']['data'] + grid_area = self.cell_measures["cell_area"]["data"] return grid_area @@ -3695,7 +4194,7 @@ class Nes(object): Parameters ---------- geometry_list : List - List with polygon geometries. + A List with polygon geometries. earth_radius_minor_axis : float Radius of the minor axis of the Earth. earth_radius_major_axis : float @@ -3717,23 +4216,34 @@ class Nes(object): """ # WGS84 with radius defined in Cartopy source code - earth_radius_dict = {'WGS84': [6356752.3142, 6378137.0]} + earth_radius_dict = {"WGS84": [6356752.3142, 6378137.0]} return earth_radius_dict[ellps] - def get_fids(self): + def create_providentia_exp_centre_coordinates(self): """ - Obtain the FIDs in a 2D format. + Calculate centre latitudes and longitudes from original coordinates and store as 2D arrays. Returns - ------- - np.array - 2D array with the FID data. + ---------- + model_centre_lat : dict + Dictionary with data of centre coordinates for latitude in 2D (latitude, longitude). + model_centre_lon : dict + Dictionary with data of centre coordinates for longitude in 2D (latitude, longitude). """ - fids = np.arange(self._lat['data'].shape[0] * self._lon['data'].shape[-1]) - fids = fids.reshape((self._lat['data'].shape[0], self._lon['data'].shape[-1])) - fids = fids[self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max']] + raise NotImplementedError("create_providentia_exp_centre_coordinates function is not implemented by default") - return fids + # noinspection DuplicatedCode + def create_providentia_exp_grid_edge_coordinates(self): + """ + Calculate grid edge latitudes and longitudes and get model grid outline. + + Returns + ---------- + grid_edge_lat : dict + Dictionary with data of grid edge latitudes. + grid_edge_lon : dict + Dictionary with data of grid edge longitudes. + """ + raise NotImplementedError("create_providentia_exp_grid_edge_coordinates function is not implemented by default") diff --git a/nes/nc_projections/latlon_nes.py b/nes/nc_projections/latlon_nes.py index 4ecad9320de88d02ec716a88d56eaf8feae7c3c0..35d68c8ee3e75b9b3483f953a882c021c2cb368f 100644 --- a/nes/nc_projections/latlon_nes.py +++ b/nes/nc_projections/latlon_nes.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -import numpy as np +from numpy import float64, linspace, meshgrid, mean, diff, append, flip, repeat, concatenate, vstack from pyproj import Proj from .default_nes import Nes @@ -11,16 +11,16 @@ class LatLonNes(Nes): Attributes ---------- _var_dim : tuple - Tuple with the name of the Y and X dimensions for the variables. - ('lat', 'lon') for a regular latitude-longitude projection. + A Tuple with the name of the Y and X dimensions for the variables. + ("lat", "lon") for a regular latitude-longitude projection. _lat_dim : tuple - Tuple with the name of the dimensions of the Latitude values. - ('lat',) for a regular latitude-longitude projection. + A Tuple with the name of the dimensions of the Latitude values. + ("lat", ) for a regular latitude-longitude projection. _lon_dim : tuple - Tuple with the name of the dimensions of the Longitude values. - ('lon',) for a regular latitude-longitude projection. + A Tuple with the name of the dimensions of the Longitude values. + ("lon", ) for a regular latitude-longitude projection. """ - def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method='Y', + def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method="Y", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, balanced=False, times=None, **kwargs): """ @@ -37,8 +37,8 @@ class LatLonNes(Nes): dataset: Dataset NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'Y'. - Accepted values: ['X', 'Y', 'T']. + Indicates the parallelization method that you want. Default: "Y". + Accepted values: ["X", "Y", "T"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -64,20 +64,20 @@ class LatLonNes(Nes): if create_nes: # Dimensions screening - self.lat = self._get_coordinate_values(self._lat, 'Y') - self.lon = self._get_coordinate_values(self._lon, 'X') + self.lat = self._get_coordinate_values(self.get_full_latitudes(), "Y") + self.lon = self._get_coordinate_values(self.get_full_longitudes(), "X") # Set axis limits for parallel writing - self.write_axis_limits = self.get_write_axis_limits() + self.write_axis_limits = self._get_write_axis_limits() - self._var_dim = ('lat', 'lon') - self._lat_dim = ('lat',) - self._lon_dim = ('lon',) + self._var_dim = ("lat", "lon") + self._lat_dim = ("lat",) + self._lon_dim = ("lon",) - self.free_vars('crs') + self.free_vars("crs") @staticmethod - def new(comm=None, path=None, info=False, dataset=None, parallel_method='Y', + def new(comm=None, path=None, info=False, dataset=None, parallel_method="Y", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, balanced=False, times=None, **kwargs): """ @@ -94,8 +94,8 @@ class LatLonNes(Nes): dataset: Dataset NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'Y'. - Accepted values: ['X', 'Y', 'T']. + Indicates the parallelization method that you want. Default: "Y". + Accepted values: ["X", "Y", "T"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -120,7 +120,8 @@ class LatLonNes(Nes): return new - def _get_pyproj_projection(self): + @staticmethod + def _get_pyproj_projection(): """ Get projection data as in Pyproj library. @@ -130,71 +131,99 @@ class LatLonNes(Nes): Grid projection. """ - projection = Proj(proj='latlong', - ellps='WGS84',) + projection = Proj(proj="latlong", ellps="WGS84",) return projection - - def _get_projection(self): - """ - Get 'projection' and 'projection_data' from grid details. + + # noinspection DuplicatedCode + def _get_projection_data(self, create_nes, **kwargs): """ + Retrieves projection data based on grid details. - if 'crs' in self.variables.keys(): - projection_data = self.variables['crs'] - self.free_vars('crs') - else: - projection_data = {'grid_mapping_name': 'latitude_longitude', - 'semi_major_axis': self.earth_radius[1], - 'inverse_flattening': 0, - } + Parameters + ---------- + create_nes : bool + Flag indicating whether to create new object (True) or use existing (False). + **kwargs : dict + Additional keyword arguments for specifying projection details. - if 'dtype' in projection_data.keys(): - del projection_data['dtype'] - - if 'data' in projection_data.keys(): - del projection_data['data'] - - if 'dimensions' in projection_data.keys(): - del projection_data['dimensions'] + Returns + ------- + Dict[str, Any] + A dictionary containing projection data with the following keys: + - "grid_mapping_name" : str + Type of grid mapping (e.g., "latitude_longitude"). + - "semi_major_axis" : float + Semi-major axis of the Earth's ellipsoid. + - "inverse_flattening" : int + Inverse flattening parameter. + - "inc_lat" : float + Increment in latitude. + - "inc_lon" : float + Increment in longitude. + - "lat_orig" : float + Origin latitude of the grid. + - "lon_orig" : float + Origin longitude of the grid. + - "n_lat" : int + Number of grid points along latitude. + - "n_lon" : int + Number of grid points along longitude. + + Notes + ----- + Depending on the `create_nes` flag and input `kwargs`, the method constructs + or retrieves projection data. If `create_nes` is True, the method initializes + projection details based on provided arguments such as increments (`inc_lat`, `inc_lon`), + and if additional keyword arguments (`lat_orig`, `lon_orig`, `n_lat`, `n_lon`) are not provided, + defaults for the global domain are used. If `create_nes` is False, the method checks for + an existing "crs" variable in `self.variables` and retrieves its data, freeing the "crs" variable + afterward to optimize memory usage. - self.projection_data = projection_data - self.projection = self._get_pyproj_projection() + """ + if create_nes: + projection_data = {"grid_mapping_name": "latitude_longitude", + "semi_major_axis": self.earth_radius[1], + "inverse_flattening": 0, + "inc_lat": kwargs["inc_lat"], + "inc_lon": kwargs["inc_lon"], + } + # Global domain + if len(kwargs) == 2: + projection_data["lat_orig"] = -90 + projection_data["lon_orig"] = -180 + projection_data["n_lat"] = int(180 // float64(projection_data["inc_lat"])) + projection_data["n_lon"] = int(360 // float64(projection_data["inc_lon"])) + # Other domains + else: + projection_data["lat_orig"] = kwargs["lat_orig"] + projection_data["lon_orig"] = kwargs["lon_orig"] + projection_data["n_lat"] = kwargs["n_lat"] + projection_data["n_lon"] = kwargs["n_lon"] + else: + if "crs" in self.variables.keys(): + projection_data = self.variables["crs"] + self.free_vars("crs") + else: + projection_data = {"grid_mapping_name": "latitude_longitude", + "semi_major_axis": self.earth_radius[1], + "inverse_flattening": 0, + } - return None + if "dtype" in projection_data.keys(): + del projection_data["dtype"] - def _create_projection(self, **kwargs): - """ - Create 'projection' and 'projection_data' from projection arguments. - """ + if "data" in projection_data.keys(): + del projection_data["data"] - projection_data = {'grid_mapping_name': 'latitude_longitude', - 'semi_major_axis': self.earth_radius[1], - 'inverse_flattening': 0, - 'inc_lat': kwargs['inc_lat'], - 'inc_lon': kwargs['inc_lon'], - } - # Global domain - if len(kwargs) == 2: - projection_data['lat_orig'] = -90 - projection_data['lon_orig'] = -180 - projection_data['n_lat'] = int(180 // np.float64(projection_data['inc_lat'])) - projection_data['n_lon'] = int(360 // np.float64(projection_data['inc_lon'])) - # Other domains - else: - projection_data['lat_orig'] = kwargs['lat_orig'] - projection_data['lon_orig'] = kwargs['lon_orig'] - projection_data['n_lat'] = kwargs['n_lat'] - projection_data['n_lon'] = kwargs['n_lon'] + if "dimensions" in projection_data.keys(): + del projection_data["dimensions"] - self.projection_data = projection_data - self.projection = self._get_pyproj_projection() + return projection_data - return None - def _create_dimensions(self, netcdf): """ - Create 'spatial_nv' dimensions and the super dimensions 'lev', 'time', 'time_nv', 'lon' and 'lat'. + Create "spatial_nv" dimensions and the super dimensions "lev", "time", "time_nv", "lon" and "lat". Parameters ---------- @@ -204,12 +233,12 @@ class LatLonNes(Nes): super(LatLonNes, self)._create_dimensions(netcdf) - netcdf.createDimension('lon', len(self._lon['data'])) - netcdf.createDimension('lat', len(self._lat['data'])) + netcdf.createDimension("lon", len(self.get_full_longitudes()["data"])) + netcdf.createDimension("lat", len(self.get_full_latitudes()["data"])) # Create spatial_nv (number of vertices) dimension - if (self._lat_bnds is not None) and (self._lon_bnds is not None): - netcdf.createDimension('spatial_nv', 2) + if (self.lat_bnds is not None) and (self.lon_bnds is not None): + netcdf.createDimension("spatial_nv", 2) return None @@ -226,30 +255,26 @@ class LatLonNes(Nes): """ # Get grid resolution - inc_lat = np.float64(self.projection_data['inc_lat']) - inc_lon = np.float64(self.projection_data['inc_lon']) + inc_lat = float64(self.projection_data["inc_lat"]) + inc_lon = float64(self.projection_data["inc_lon"]) # Get coordinates origen - lat_orig = np.float64(self.projection_data['lat_orig']) - lon_orig = np.float64(self.projection_data['lon_orig']) + lat_orig = float64(self.projection_data["lat_orig"]) + lon_orig = float64(self.projection_data["lon_orig"]) # Get number of coordinates - n_lat = int(self.projection_data['n_lat']) - n_lon = int(self.projection_data['n_lon']) + n_lat = int(self.projection_data["n_lat"]) + n_lon = int(self.projection_data["n_lon"]) # Calculate centre latitudes lat_c_orig = lat_orig + (inc_lat / 2) - centre_lat = np.linspace(lat_c_orig, - lat_c_orig + (inc_lat * (n_lat - 1)), - n_lat, dtype=np.float64) + centre_lat = linspace(lat_c_orig, lat_c_orig + (inc_lat * (n_lat - 1)), n_lat, dtype=float64) # Calculate centre longitudes lon_c_orig = lon_orig + (inc_lon / 2) - centre_lon = np.linspace(lon_c_orig, - lon_c_orig + (inc_lon * (n_lon - 1)), - n_lon, dtype=np.float64) + centre_lon = linspace(lon_c_orig, lon_c_orig + (inc_lon * (n_lon - 1)), n_lon, dtype=float64) - return {'data': centre_lat}, {'data': centre_lon} + return {"data": centre_lat}, {"data": centre_lon} def create_providentia_exp_centre_coordinates(self): """ @@ -263,16 +288,17 @@ class LatLonNes(Nes): Dictionary with data of centre coordinates for longitude in 2D (latitude, longitude). """ - model_centre_lon_data, model_centre_lat_data = np.meshgrid(self.lon['data'], self.lat['data']) + model_centre_lon_data, model_centre_lat_data = meshgrid(self.lon["data"], self.lat["data"]) # Calculate centre latitudes - model_centre_lat = {'data': model_centre_lat_data} + model_centre_lat = {"data": model_centre_lat_data} # Calculate centre longitudes - model_centre_lon = {'data': model_centre_lon_data} + model_centre_lon = {"data": model_centre_lon_data} return model_centre_lat, model_centre_lon + # noinspection DuplicatedCode def create_providentia_exp_grid_edge_coordinates(self): """ Calculate grid edge latitudes and longitudes and get model grid outline. @@ -286,38 +312,38 @@ class LatLonNes(Nes): """ # Get grid resolution - inc_lon = np.abs(np.mean(np.diff(self.lon['data']))) - inc_lat = np.abs(np.mean(np.diff(self.lat['data']))) + inc_lon = abs(mean(diff(self.lon["data"]))) + inc_lat = abs(mean(diff(self.lat["data"]))) # Get bounds - lat_bounds = self.create_single_spatial_bounds(self.lat['data'], inc_lat) - lon_bounds = self.create_single_spatial_bounds(self.lon['data'], inc_lon) + lat_bounds = self._create_single_spatial_bounds(self.lat["data"], inc_lat) + lon_bounds = self._create_single_spatial_bounds(self.lon["data"], inc_lon) # Get latitudes for grid edge - left_edge_lat = np.append(lat_bounds.flatten()[::2], lat_bounds.flatten()[-1]) - right_edge_lat = np.flip(left_edge_lat, 0) - top_edge_lat = np.repeat(lat_bounds[-1][-1], len(self.lon['data']) - 1) - bottom_edge_lat = np.repeat(lat_bounds[0][0], len(self.lon['data'])) - lat_grid_edge = np.concatenate((left_edge_lat, top_edge_lat, right_edge_lat, bottom_edge_lat)) + left_edge_lat = append(lat_bounds.flatten()[::2], lat_bounds.flatten()[-1]) + right_edge_lat = flip(left_edge_lat, 0) + top_edge_lat = repeat(lat_bounds[-1][-1], len(self.lon["data"]) - 1) + bottom_edge_lat = repeat(lat_bounds[0][0], len(self.lon["data"])) + lat_grid_edge = concatenate((left_edge_lat, top_edge_lat, right_edge_lat, bottom_edge_lat)) # Get longitudes for grid edge - left_edge_lon = np.repeat(lon_bounds[0][0], len(self.lat['data']) + 1) + left_edge_lon = repeat(lon_bounds[0][0], len(self.lat["data"]) + 1) top_edge_lon = lon_bounds.flatten()[1:-1:2] - right_edge_lon = np.repeat(lon_bounds[-1][-1], len(self.lat['data']) + 1) - bottom_edge_lon = np.flip(lon_bounds.flatten()[:-1:2], 0) - lon_grid_edge = np.concatenate((left_edge_lon, top_edge_lon, right_edge_lon, bottom_edge_lon)) + right_edge_lon = repeat(lon_bounds[-1][-1], len(self.lat["data"]) + 1) + bottom_edge_lon = flip(lon_bounds.flatten()[:-1:2], 0) + lon_grid_edge = concatenate((left_edge_lon, top_edge_lon, right_edge_lon, bottom_edge_lon)) # Create grid outline by stacking the edges in both coordinates - model_grid_outline = np.vstack((lon_grid_edge, lat_grid_edge)).T - grid_edge_lat = {'data': model_grid_outline[:,1]} - grid_edge_lon = {'data': model_grid_outline[:,0]} + model_grid_outline = vstack((lon_grid_edge, lat_grid_edge)).T + grid_edge_lat = {"data": model_grid_outline[:, 1]} + grid_edge_lon = {"data": model_grid_outline[:, 0]} return grid_edge_lat, grid_edge_lon @staticmethod def _set_var_crs(var): """ - Set the grid_mapping to 'crs'. + Set the grid_mapping to "crs". Parameters ---------- @@ -325,14 +351,14 @@ class LatLonNes(Nes): netCDF4-python variable object. """ - var.grid_mapping = 'crs' + var.grid_mapping = "crs" var.coordinates = "lat lon" return None def _create_metadata(self, netcdf): """ - Create the 'crs' variable for the rotated latitude longitude grid_mapping. + Create the "crs" variable for the rotated latitude longitude grid_mapping. Parameters ---------- @@ -341,10 +367,10 @@ class LatLonNes(Nes): """ if self.projection_data is not None: - mapping = netcdf.createVariable('crs', 'i') - mapping.grid_mapping_name = self.projection_data['grid_mapping_name'] - mapping.semi_major_axis = self.projection_data['semi_major_axis'] - mapping.inverse_flattening = self.projection_data['inverse_flattening'] + mapping = netcdf.createVariable("crs", "i") + mapping.grid_mapping_name = self.projection_data["grid_mapping_name"] + mapping.semi_major_axis = self.projection_data["semi_major_axis"] + mapping.inverse_flattening = self.projection_data["inverse_flattening"] return None diff --git a/nes/nc_projections/lcc_nes.py b/nes/nc_projections/lcc_nes.py index 79417ac87f266e6f3cfe86267fdad144a2788cb0..f9eda6e108d204c897b97eabb36e9d03993770af 100644 --- a/nes/nc_projections/lcc_nes.py +++ b/nes/nc_projections/lcc_nes.py @@ -1,12 +1,11 @@ #!/usr/bin/env python -import warnings -import sys -import numpy as np -import pandas as pd +from numpy import float64, linspace, array, mean, diff, append, flip, repeat, concatenate, vstack +from geopandas import GeoDataFrame +from pandas import Index from pyproj import Proj from copy import deepcopy -import geopandas as gpd +from typing import Dict, Any from shapely.geometry import Polygon, Point from .default_nes import Nes @@ -16,25 +15,25 @@ class LCCNes(Nes): Attributes ---------- - _y : dict - Y coordinates dictionary with the complete 'data' key for all the values and the rest of the attributes. - _x : dict - X coordinates dictionary with the complete 'data' key for all the values and the rest of the attributes. + _full_y : dict + Y coordinates dictionary with the complete "data" key for all the values and the rest of the attributes. + _full_x : dict + X coordinates dictionary with the complete "data" key for all the values and the rest of the attributes. y : dict - Y coordinates dictionary with the portion of 'data' corresponding to the rank values. + Y coordinates dictionary with the portion of "data" corresponding to the rank values. x : dict - X coordinates dictionary with the portion of 'data' corresponding to the rank values. + X coordinates dictionary with the portion of "data" corresponding to the rank values. _var_dim : tuple - Tuple with the name of the Y and X dimensions for the variables. - ('y', 'x',) for a LCC projection. + A Tuple with the name of the Y and X dimensions for the variables. + ("y", "x", ) for an LCC projection. _lat_dim : tuple - Tuple with the name of the dimensions of the Latitude values. - ('y', 'x',) for a LCC projection. + A Tuple with the name of the dimensions of the Latitude values. + ("y", "x", ) for an LCC projection. _lon_dim : tuple - Tuple with the name of the dimensions of the Longitude values. - ('y', 'x') for a LCC projection. + ATuple with the name of the dimensions of the Longitude values. + ("y", "x") for an LCC projection. """ - def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method='Y', + def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method="Y", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, balanced=False, times=None, **kwargs): """ @@ -51,8 +50,8 @@ class LCCNes(Nes): dataset: Dataset NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'Y'. - Accepted values: ['X', 'Y', 'T']. + Indicates the parallelization method that you want. Default: "Y". + Accepted values: ["X", "Y", "T"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -69,6 +68,8 @@ class LCCNes(Nes): times : list, None List of times to substitute the current ones while creation. """ + self._full_y = None + self._full_x = None super(LCCNes, self).__init__(comm=comm, path=path, info=info, dataset=dataset, parallel_method=parallel_method, balanced=balanced, @@ -78,28 +79,28 @@ class LCCNes(Nes): if create_nes: # Dimensions screening - self.lat = self._get_coordinate_values(self._lat, 'Y') - self.lon = self._get_coordinate_values(self._lon, 'X') + self.lat = self._get_coordinate_values(self.get_full_latitudes(), "Y") + self.lon = self._get_coordinate_values(self.get_full_longitudes(), "X") else: # Complete dimensions - self._y = self._get_coordinate_dimension('y') - self._x = self._get_coordinate_dimension('x') + self._full_y = self._get_coordinate_dimension("y") + self._full_x = self._get_coordinate_dimension("x") # Dimensions screening - self.y = self._get_coordinate_values(self._y, 'Y') - self.x = self._get_coordinate_values(self._x, 'X') + self.y = self._get_coordinate_values(self.get_full_y(), "Y") + self.x = self._get_coordinate_values(self.get_full_x(), "X") # Set axis limits for parallel writing - self.write_axis_limits = self.get_write_axis_limits() + self.write_axis_limits = self._get_write_axis_limits() - self._var_dim = ('y', 'x') - self._lat_dim = ('y', 'x') - self._lon_dim = ('y', 'x') + self._var_dim = ("y", "x") + self._lat_dim = ("y", "x") + self._lon_dim = ("y", "x") - self.free_vars('crs') + self.free_vars("crs") @staticmethod - def new(comm=None, path=None, info=False, dataset=None, parallel_method='Y', + def new(comm=None, path=None, info=False, dataset=None, parallel_method="Y", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, balanced=False, times=None, **kwargs): """ @@ -116,8 +117,8 @@ class LCCNes(Nes): dataset: Dataset NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'Y'. - Accepted values: ['X', 'Y', 'T']. + Indicates the parallelization method that you want. Default: "Y". + Accepted values: ["X", "Y", "T"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -142,20 +143,96 @@ class LCCNes(Nes): return new - def filter_coordinates_selection(self): + def get_full_y(self) -> Dict[str, Any]: + """ + Retrieve the complete Y information. + + Returns + ------- + Dict[str, Any] + A dictionary containing the complete latitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of latitude values. + attr_name: attr_value, # Latitude attributes. + ... + } + """ + data = self.comm.bcast(self._full_y) + + return data + + def get_full_x(self) -> Dict[str, Any]: + """ + Retrieve the complete X information. + + Returns + ------- + Dict[str, Any] + A dictionary containing the complete longitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of longitude values. + attr_name: attr_value, # Longitude attributes. + ... + } + """ + data = self.comm.bcast(self._full_x) + return data + + def set_full_y(self, data: Dict[str, Any]) -> None: + """ + Set the complete Y information. + + Parameters + ---------- + data : Dict[str, Any] + A dictionary containing the complete latitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of latitude values. + attr_name: attr_value, # Latitude attributes. + ... + } + """ + if self.master: + self._full_y = data + return None + + def set_full_x(self, data: Dict[str, Any]) -> None: + """ + Set the complete rotated longitude information. + + Parameters + ---------- + data : Dict[str, Any] + A dictionary containing the complete longitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of longitude values. + attr_name: attr_value, # Longitude attributes. + ... + } + """ + if self.master: + self._full_x = data + return None + + # noinspection DuplicatedCode + def _filter_coordinates_selection(self): """ Use the selection limits to filter y, x, time, lev, lat, lon, lon_bnds and lat_bnds. """ - idx = self.get_idx_intervals() + idx = self._get_idx_intervals() - self.y = self._get_coordinate_values(self._y, 'Y') - self.x = self._get_coordinate_values(self._x, 'X') + self.y = self._get_coordinate_values(self.get_full_y(), "Y") + self.x = self._get_coordinate_values(self.get_full_x(), "X") - self._y['data'] = self._y['data'][idx['idx_y_min']:idx['idx_y_max']] - self._x['data'] = self._x['data'][idx['idx_x_min']:idx['idx_x_max']] + self.set_full_y({'data': self.y["data"][idx["idx_y_min"]:idx["idx_y_max"]]}) + self.set_full_x({'data': self.x["data"][idx["idx_x_min"]:idx["idx_x_max"]]}) - super(LCCNes, self).filter_coordinates_selection() + super(LCCNes, self)._filter_coordinates_selection() return None @@ -169,80 +246,73 @@ class LCCNes(Nes): Grid projection. """ - projection = Proj(proj='lcc', - ellps='WGS84', + projection = Proj(proj="lcc", + ellps="WGS84", R=self.earth_radius[0], - lat_1=np.float64(self.projection_data['standard_parallel'][0]), - lat_2=np.float64(self.projection_data['standard_parallel'][1]), - lon_0=np.float64(self.projection_data['longitude_of_central_meridian']), - lat_0=np.float64(self.projection_data['latitude_of_projection_origin']), + lat_1=float64(self.projection_data["standard_parallel"][0]), + lat_2=float64(self.projection_data["standard_parallel"][1]), + lon_0=float64(self.projection_data["longitude_of_central_meridian"]), + lat_0=float64(self.projection_data["latitude_of_projection_origin"]), to_meter=1, x_0=0, y_0=0, a=self.earth_radius[1], k_0=1.0, - ) + ) return projection - def _get_projection(self): - """ - Get 'projection' and 'projection_data' from grid details. + def _get_projection_data(self, create_nes, **kwargs): """ + Retrieves projection data based on grid details. - if 'Lambert_Conformal' in self.variables.keys(): - projection_data = self.variables['Lambert_Conformal'] - self.free_vars('Lambert_Conformal') - elif 'Lambert_conformal' in self.variables.keys(): - projection_data = self.variables['Lambert_conformal'] - self.free_vars('Lambert_conformal') + Parameters + ---------- + create_nes : bool + Flag indicating whether to create new object (True) or use existing (False). + **kwargs : dict + Additional keyword arguments for specifying projection details. """ + if create_nes: + projection_data = {"grid_mapping_name": "lambert_conformal_conic", + "standard_parallel": [kwargs["lat_1"], kwargs["lat_2"]], + "longitude_of_central_meridian": kwargs["lon_0"], + "latitude_of_projection_origin": kwargs["lat_0"], + "x_0": kwargs["x_0"], "y_0": kwargs["y_0"], + "inc_x": kwargs["inc_x"], "inc_y": kwargs["inc_y"], + "nx": kwargs["nx"], "ny": kwargs["ny"], + } else: - # We will never have this condition since the LCC grid will never be correctly detected - # since the function __is_lcc in load_nes only detects LCC grids when there is Lambert_conformal - msg = 'There is no variable called Lambert_Conformal, projection has not been defined.' - raise RuntimeError(msg) - - if 'dtype' in projection_data.keys(): - del projection_data['dtype'] - - if 'data' in projection_data.keys(): - del projection_data['data'] - - if 'dimensions' in projection_data.keys(): - del projection_data['dimensions'] - - if isinstance(projection_data['standard_parallel'], str): - projection_data['standard_parallel'] = [projection_data['standard_parallel'].split(', ')[0], - projection_data['standard_parallel'].split(', ')[1]] - - self.projection_data = projection_data - self.projection = self._get_pyproj_projection() - - return None - - def _create_projection(self, **kwargs): - """ - Create 'projection' and 'projection_data' from projection arguments. - """ - - projection_data = {'grid_mapping_name': 'lambert_conformal_conic', - 'standard_parallel': [kwargs['lat_1'], kwargs['lat_2']], - 'longitude_of_central_meridian': kwargs['lon_0'], - 'latitude_of_projection_origin': kwargs['lat_0'], - 'x_0': kwargs['x_0'], 'y_0': kwargs['y_0'], - 'inc_x': kwargs['inc_x'], 'inc_y': kwargs['inc_y'], - 'nx': kwargs['nx'], 'ny': kwargs['ny'], - - } - - self.projection_data = projection_data - self.projection = self._get_pyproj_projection() - - return None - + if "Lambert_Conformal" in self.variables.keys(): + projection_data = self.variables["Lambert_Conformal"] + self.free_vars("Lambert_Conformal") + elif "Lambert_conformal" in self.variables.keys(): + projection_data = self.variables["Lambert_conformal"] + self.free_vars("Lambert_conformal") + else: + # We will never have this condition since the LCC grid will never be correctly detected + # since the function __is_lcc in load_nes only detects LCC grids when there is Lambert_conformal + msg = "There is no variable called Lambert_Conformal, projection has not been defined." + raise RuntimeError(msg) + + if "dtype" in projection_data.keys(): + del projection_data["dtype"] + + if "data" in projection_data.keys(): + del projection_data["data"] + + if "dimensions" in projection_data.keys(): + del projection_data["dimensions"] + + if isinstance(projection_data["standard_parallel"], str): + projection_data["standard_parallel"] = [projection_data["standard_parallel"].split(", ")[0], + projection_data["standard_parallel"].split(", ")[1]] + + return projection_data + + # noinspection DuplicatedCode def _create_dimensions(self, netcdf): """ - Create 'y', 'x' and 'spatial_nv' dimensions and the super dimensions 'lev', 'time', 'time_nv', 'lon' and 'lat' + Create "y", "x" and "spatial_nv" dimensions and the super dimensions "lev", "time", "time_nv", "lon" and "lat" Parameters ---------- @@ -253,18 +323,19 @@ class LCCNes(Nes): super(LCCNes, self)._create_dimensions(netcdf) # Create y and x dimensions - netcdf.createDimension('y', len(self._y['data'])) - netcdf.createDimension('x', len(self._x['data'])) + netcdf.createDimension("y", len(self.get_full_y()["data"])) + netcdf.createDimension("x", len(self.get_full_x()["data"])) # Create spatial_nv (number of vertices) dimension - if (self._lat_bnds is not None) and (self._lon_bnds is not None): - netcdf.createDimension('spatial_nv', 4) + if (self.lat_bnds is not None) and (self.lon_bnds is not None): + netcdf.createDimension("spatial_nv", 4) return None + # noinspection DuplicatedCode def _create_dimension_variables(self, netcdf): """ - Create the 'y' and 'x' variables. + Create the "y" and "x" variables. Parameters ---------- @@ -275,31 +346,34 @@ class LCCNes(Nes): super(LCCNes, self)._create_dimension_variables(netcdf) # LCC Y COORDINATES - y = netcdf.createVariable('y', self._y['data'].dtype, ('y',)) - y.long_name = 'y coordinate of projection' - if 'units' in self._y.keys(): - y.units = self._y['units'] + full_y = self.get_full_y() + y = netcdf.createVariable("y", full_y["data"].dtype, ("y",)) + y.long_name = "y coordinate of projection" + if "units" in full_y.keys(): + y.units = full_y["units"] else: - y.units = 'm' - y.standard_name = 'projection_y_coordinate' + y.units = "m" + y.standard_name = "projection_y_coordinate" if self.size > 1: y.set_collective(True) - y[:] = self._y['data'] + y[:] = full_y["data"] # LCC X COORDINATES - x = netcdf.createVariable('x', self._x['data'].dtype, ('x',)) - x.long_name = 'x coordinate of projection' - if 'units' in self._x.keys(): - x.units = self._x['units'] + full_x = self.get_full_x() + x = netcdf.createVariable("x", full_x["data"].dtype, ("x",)) + x.long_name = "x coordinate of projection" + if "units" in full_x.keys(): + x.units = full_x["units"] else: - x.units = 'm' - x.standard_name = 'projection_x_coordinate' + x.units = "m" + x.standard_name = "projection_x_coordinate" if self.size > 1: x.set_collective(True) - x[:] = self._x['data'] + x[:] = full_x["data"] return None + # noinspection DuplicatedCode def _create_centre_coordinates(self, **kwargs): """ Calculate centre latitudes and longitudes from grid details. @@ -309,34 +383,33 @@ class LCCNes(Nes): netcdf : Dataset NetCDF object. """ + if self.master: + # Get projection details on x + x_0 = float64(self.projection_data["x_0"]) + inc_x = float64(self.projection_data["inc_x"]) + nx = int(self.projection_data["nx"]) - # Get projection details on x - x_0 = np.float64(self.projection_data['x_0']) - inc_x = np.float64(self.projection_data['inc_x']) - nx = int(self.projection_data['nx']) + # Get projection details on y + y_0 = float64(self.projection_data["y_0"]) + inc_y = float64(self.projection_data["inc_y"]) + ny = int(self.projection_data["ny"]) - # Get projection details on y - y_0 = np.float64(self.projection_data['y_0']) - inc_y = np.float64(self.projection_data['inc_y']) - ny = int(self.projection_data['ny']) + # Create a regular grid in metres (1D) + self._full_x = {"data": linspace(x_0 + (inc_x / 2), x_0 + (inc_x / 2) + (inc_x * (nx - 1)), nx, + dtype=float64)} + self._full_y = {"data": linspace(y_0 + (inc_y / 2), y_0 + (inc_y / 2) + (inc_y * (ny - 1)), ny, + dtype=float64)} - # Create a regular grid in metres (1D) - self._x = {'data': np.linspace(x_0 + (inc_x / 2), - x_0 + (inc_x / 2) + (inc_x * (nx - 1)), - nx, dtype=np.float64)} - self._y = {'data': np.linspace(y_0 + (inc_y / 2), - y_0 + (inc_y / 2) + (inc_y * (ny - 1)), - ny, dtype=np.float64)} + # Create a regular grid in metres (1D to 2D) + x = array([self._full_x["data"]] * len(self._full_y["data"])) + y = array([self._full_y["data"]] * len(self._full_x["data"])).T + # Calculate centre latitudes and longitudes (UTM to LCC) + centre_lon, centre_lat = self.projection(x, y, inverse=True) - # Create a regular grid in metres (1D to 2D) - x = np.array([self._x['data']] * len(self._y['data'])) - y = np.array([self._y['data']] * len(self._x['data'])).T - - # Calculate centre latitudes and longitudes (UTM to LCC) - centre_lon, centre_lat = self.projection(x, y, inverse=True) - - return {'data': centre_lat}, {'data': centre_lon} + return {"data": centre_lat}, {"data": centre_lon} + else: + return None, None def create_providentia_exp_centre_coordinates(self): """ @@ -358,6 +431,7 @@ class LCCNes(Nes): return model_centre_lat, model_centre_lon + # noinspection DuplicatedCode def create_providentia_exp_grid_edge_coordinates(self): """ Calculate grid edge latitudes and longitudes and get model grid outline. @@ -369,77 +443,75 @@ class LCCNes(Nes): grid_edge_lon : dict Dictionary with data of grid edge longitudes. """ - # Get grid resolution - inc_x = np.abs(np.mean(np.diff(self.x['data']))) - inc_y = np.abs(np.mean(np.diff(self.y['data']))) + inc_x = abs(mean(diff(self.x["data"]))) + inc_y = abs(mean(diff(self.y["data"]))) # Get bounds for rotated coordinates - y_bnds = self.create_single_spatial_bounds(self.y['data'], inc_y) - x_bnds = self.create_single_spatial_bounds(self.x['data'], inc_x) + y_bnds = self._create_single_spatial_bounds(self.y["data"], inc_y) + x_bnds = self._create_single_spatial_bounds(self.x["data"], inc_x) # Get rotated latitudes for grid edge - left_edge_y = np.append(y_bnds.flatten()[::2], y_bnds.flatten()[-1]) - right_edge_y = np.flip(left_edge_y, 0) - top_edge_y = np.repeat(y_bnds[-1][-1], len(self.x['data']) - 1) - bottom_edge_y = np.repeat(y_bnds[0][0], len(self.x['data'])) - y_grid_edge = np.concatenate((left_edge_y, top_edge_y, right_edge_y, bottom_edge_y)) + left_edge_y = append(y_bnds.flatten()[::2], y_bnds.flatten()[-1]) + right_edge_y = flip(left_edge_y, 0) + top_edge_y = repeat(y_bnds[-1][-1], len(self.x["data"]) - 1) + bottom_edge_y = repeat(y_bnds[0][0], len(self.x["data"])) + y_grid_edge = concatenate((left_edge_y, top_edge_y, right_edge_y, bottom_edge_y)) # Get rotated longitudes for grid edge - left_edge_x = np.repeat(x_bnds[0][0], len(self.y['data']) + 1) + left_edge_x = repeat(x_bnds[0][0], len(self.y["data"]) + 1) top_edge_x = x_bnds.flatten()[1:-1:2] - right_edge_x = np.repeat(x_bnds[-1][-1], len(self.y['data']) + 1) - bottom_edge_x = np.flip(x_bnds.flatten()[:-1:2], 0) - x_grid_edge = np.concatenate((left_edge_x, top_edge_x, right_edge_x, bottom_edge_x)) + right_edge_x = repeat(x_bnds[-1][-1], len(self.y["data"]) + 1) + bottom_edge_x = flip(x_bnds.flatten()[:-1:2], 0) + x_grid_edge = concatenate((left_edge_x, top_edge_x, right_edge_x, bottom_edge_x)) # Get edges for regular coordinates grid_edge_lon_data, grid_edge_lat_data = self.projection(x_grid_edge, y_grid_edge, inverse=True) # Create grid outline by stacking the edges in both coordinates - model_grid_outline = np.vstack((grid_edge_lon_data, grid_edge_lat_data)).T - grid_edge_lat = {'data': model_grid_outline[:,1]} - grid_edge_lon = {'data': model_grid_outline[:,0]} + model_grid_outline = vstack((grid_edge_lon_data, grid_edge_lat_data)).T + grid_edge_lat = {"data": model_grid_outline[:, 1]} + grid_edge_lon = {"data": model_grid_outline[:, 0]} return grid_edge_lat, grid_edge_lon + # noinspection DuplicatedCode def create_spatial_bounds(self): """ Calculate longitude and latitude bounds and set them. """ # Calculate LCC coordinates bounds - inc_x = np.abs(np.mean(np.diff(self._x['data']))) - x_bnds = self.create_single_spatial_bounds(np.array([self._x['data']] * len(self._y['data'])), - inc_x, spatial_nv=4) + full_x = self.get_full_x() + full_y = self.get_full_y() + inc_x = abs(mean(diff(full_x["data"]))) + x_bnds = self._create_single_spatial_bounds(array([full_x["data"]] * len(full_y["data"])), + inc_x, spatial_nv=4) - inc_y = np.abs(np.mean(np.diff(self._y['data']))) - y_bnds = self.create_single_spatial_bounds(np.array([self._y['data']] * len(self._x['data'])).T, - inc_y, spatial_nv=4, inverse=True) + inc_y = abs(mean(diff(full_y["data"]))) + y_bnds = self._create_single_spatial_bounds(array([full_y["data"]] * len(full_x["data"])).T, + inc_y, spatial_nv=4, inverse=True) # Transform LCC bounds to regular bounds lon_bnds, lat_bnds = self.projection(x_bnds, y_bnds, inverse=True) # Obtain regular coordinates bounds - self._lat_bnds = {} - self._lat_bnds['data'] = deepcopy(lat_bnds) - self.lat_bnds = {} - self.lat_bnds['data'] = lat_bnds[self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - :] - - self._lon_bnds = {} - self._lon_bnds['data'] = deepcopy(lon_bnds) - self.lon_bnds = {} - self.lon_bnds['data'] = lon_bnds[self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - :] + self.set_full_latitudes_boundaries({"data": deepcopy(lat_bnds)}) + self.lat_bnds = {"data": lat_bnds[self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + :]} + + self.set_full_longitudes_boundaries({"data": deepcopy(lon_bnds)}) + self.lon_bnds = {"data": lon_bnds[self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + :]} return None @staticmethod def _set_var_crs(var): """ - Set the grid_mapping to 'Lambert_Conformal'. + Set the grid_mapping to "Lambert_Conformal". Parameters ---------- @@ -447,14 +519,14 @@ class LCCNes(Nes): netCDF4-python variable object. """ - var.grid_mapping = 'Lambert_Conformal' + var.grid_mapping = "Lambert_Conformal" var.coordinates = "lat lon" return None def _create_metadata(self, netcdf): """ - Create the 'crs' variable for the lambert conformal grid_mapping. + Create the "crs" variable for the lambert conformal grid_mapping. Parameters ---------- @@ -463,11 +535,11 @@ class LCCNes(Nes): """ if self.projection_data is not None: - mapping = netcdf.createVariable('Lambert_Conformal', 'i') - mapping.grid_mapping_name = self.projection_data['grid_mapping_name'] - mapping.standard_parallel = self.projection_data['standard_parallel'] - mapping.longitude_of_central_meridian = self.projection_data['longitude_of_central_meridian'] - mapping.latitude_of_projection_origin = self.projection_data['latitude_of_projection_origin'] + mapping = netcdf.createVariable("Lambert_Conformal", "i") + mapping.grid_mapping_name = self.projection_data["grid_mapping_name"] + mapping.standard_parallel = self.projection_data["standard_parallel"] + mapping.longitude_of_central_meridian = self.projection_data["longitude_of_central_meridian"] + mapping.latitude_of_projection_origin = self.projection_data["latitude_of_projection_origin"] return None @@ -477,6 +549,8 @@ class LCCNes(Nes): Parameters ---------- + lat_flip : bool + Indicates if the latitudes need to be flipped Up-Down or Down-Up. Default False. path : str Path to the output file. grib_keys : dict @@ -489,9 +563,10 @@ class LCCNes(Nes): raise NotImplementedError("Grib2 format cannot be written in a Lambert Conformal Conic projection.") + # noinspection DuplicatedCode def create_shapefile(self): """ - Create spatial geodataframe (shapefile). + Create spatial GeoDataFrame (shapefile). Returns ------- @@ -502,29 +577,27 @@ class LCCNes(Nes): if self.shapefile is None: # Get latitude and longitude cell boundaries - if self._lat_bnds is None or self._lon_bnds is None: + if self.lat_bnds is None or self.lon_bnds is None: self.create_spatial_bounds() # Reshape arrays to create geometry - aux_b_lats = self.lat_bnds['data'].reshape((self.lat_bnds['data'].shape[0] * self.lat_bnds['data'].shape[1], - self.lat_bnds['data'].shape[2])) - aux_b_lons = self.lon_bnds['data'].reshape((self.lon_bnds['data'].shape[0] * self.lon_bnds['data'].shape[1], - self.lon_bnds['data'].shape[2])) + aux_b_lat = self.lat_bnds["data"].reshape((self.lat_bnds["data"].shape[0] * self.lat_bnds["data"].shape[1], + self.lat_bnds["data"].shape[2])) + aux_b_lon = self.lon_bnds["data"].reshape((self.lon_bnds["data"].shape[0] * self.lon_bnds["data"].shape[1], + self.lon_bnds["data"].shape[2])) # Get polygons from bounds geometry = [] - for i in range(aux_b_lons.shape[0]): - geometry.append(Polygon([(aux_b_lons[i, 0], aux_b_lats[i, 0]), - (aux_b_lons[i, 1], aux_b_lats[i, 1]), - (aux_b_lons[i, 2], aux_b_lats[i, 2]), - (aux_b_lons[i, 3], aux_b_lats[i, 3]), - (aux_b_lons[i, 0], aux_b_lats[i, 0])])) + for i in range(aux_b_lon.shape[0]): + geometry.append(Polygon([(aux_b_lon[i, 0], aux_b_lat[i, 0]), + (aux_b_lon[i, 1], aux_b_lat[i, 1]), + (aux_b_lon[i, 2], aux_b_lat[i, 2]), + (aux_b_lon[i, 3], aux_b_lat[i, 3]), + (aux_b_lon[i, 0], aux_b_lat[i, 0])])) - # Create dataframe cointaining all polygons + # Create dataframe containing all polygons fids = self.get_fids() - gdf = gpd.GeoDataFrame(index=pd.Index(name='FID', data=fids.ravel()), - geometry=geometry, - crs="EPSG:4326") + gdf = GeoDataFrame(index=Index(name="FID", data=fids.ravel()), geometry=geometry, crs="EPSG:4326") self.shapefile = gdf else: @@ -532,6 +605,7 @@ class LCCNes(Nes): return gdf + # noinspection DuplicatedCode def get_centroids_from_coordinates(self): """ Get centroids from geographical coordinates. @@ -544,16 +618,13 @@ class LCCNes(Nes): # Get centroids from coordinates centroids = [] - for lat_ind in range(0, self.lon['data'].shape[0]): - for lon_ind in range(0, self.lon['data'].shape[1]): - centroids.append(Point(self.lon['data'][lat_ind, lon_ind], - self.lat['data'][lat_ind, lon_ind])) + for lat_ind in range(0, self.lon["data"].shape[0]): + for lon_ind in range(0, self.lon["data"].shape[1]): + centroids.append(Point(self.lon["data"][lat_ind, lon_ind], + self.lat["data"][lat_ind, lon_ind])) - # Create dataframe cointaining all points + # Create dataframe containing all points fids = self.get_fids() - centroids_gdf = gpd.GeoDataFrame(index=pd.Index(name='FID', data=fids.ravel()), - geometry=centroids, - crs="EPSG:4326") + centroids_gdf = GeoDataFrame(index=Index(name="FID", data=fids.ravel()), geometry=centroids, crs="EPSG:4326") return centroids_gdf - diff --git a/nes/nc_projections/mercator_nes.py b/nes/nc_projections/mercator_nes.py index 263ecc74693f348c1365c0712a8bf048fcbd25f3..520f9bb4bfa9eea3da36fad13971d96e36bd7125 100644 --- a/nes/nc_projections/mercator_nes.py +++ b/nes/nc_projections/mercator_nes.py @@ -1,12 +1,11 @@ #!/usr/bin/env python -import warnings -import sys -import numpy as np -import pandas as pd +from numpy import float64, linspace, array, mean, diff, append, flip, repeat, concatenate, vstack +from geopandas import GeoDataFrame +from pandas import Index from pyproj import Proj from copy import deepcopy -import geopandas as gpd +from typing import Dict, Any from shapely.geometry import Polygon, Point from nes.nc_projections.default_nes import Nes @@ -16,25 +15,25 @@ class MercatorNes(Nes): Attributes ---------- - _y : dict - Y coordinates dictionary with the complete 'data' key for all the values and the rest of the attributes. - _x : dict - X coordinates dictionary with the complete 'data' key for all the values and the rest of the attributes. + _full_y : dict + Y coordinates dictionary with the complete "data" key for all the values and the rest of the attributes. + _full_x : dict + X coordinates dictionary with the complete "data" key for all the values and the rest of the attributes. y : dict - Y coordinates dictionary with the portion of 'data' corresponding to the rank values. + Y coordinates dictionary with the portion of "data" corresponding to the rank values. x : dict - X coordinates dictionary with the portion of 'data' corresponding to the rank values. + X coordinates dictionary with the portion of "data" corresponding to the rank values. _var_dim : tuple - Tuple with the name of the Y and X dimensions for the variables. - ('y', 'x') for a Mercator projection. + A Tuple with the name of the Y and X dimensions for the variables. + ("y", "x") for a Mercator projection. _lat_dim : tuple - Tuple with the name of the dimensions of the Latitude values. - ('y', 'x') for a Mercator projection. + A Tuple with the name of the dimensions of the Latitude values. + ("y", "x") for a Mercator projection. _lon_dim : tuple - Tuple with the name of the dimensions of the Longitude values. - ('y', 'x') for a Mercator projection. + A Tuple with the name of the dimensions of the Longitude values. + ("y", "x") for a Mercator projection. """ - def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method='Y', + def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method="Y", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, balanced=False, times=None, **kwargs): """ @@ -51,8 +50,8 @@ class MercatorNes(Nes): dataset: Dataset NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'Y'. - Accepted values: ['X', 'Y', 'T']. + Indicates the parallelization method that you want. Default: "Y". + Accepted values: ["X", "Y", "T"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -70,6 +69,8 @@ class MercatorNes(Nes): List of times to substitute the current ones while creation. """ + self._full_y = None + self._full_x = None super(MercatorNes, self).__init__(comm=comm, path=path, info=info, dataset=dataset, parallel_method=parallel_method, balanced=balanced, @@ -79,28 +80,28 @@ class MercatorNes(Nes): if create_nes: # Dimensions screening - self.lat = self._get_coordinate_values(self._lat, 'Y') - self.lon = self._get_coordinate_values(self._lon, 'X') + self.lat = self._get_coordinate_values(self.get_full_latitudes(), "Y") + self.lon = self._get_coordinate_values(self.get_full_longitudes(), "X") else: # Complete dimensions - self._y = self._get_coordinate_dimension('y') - self._x = self._get_coordinate_dimension('x') + self._full_y = self._get_coordinate_dimension("y") + self._full_x = self._get_coordinate_dimension("x") # Dimensions screening - self.y = self._get_coordinate_values(self._y, 'Y') - self.x = self._get_coordinate_values(self._x, 'X') + self.y = self._get_coordinate_values(self.get_full_y(), "Y") + self.x = self._get_coordinate_values(self.get_full_x(), "X") # Set axis limits for parallel writing - self.write_axis_limits = self.get_write_axis_limits() + self.write_axis_limits = self._get_write_axis_limits() - self._var_dim = ('y', 'x') - self._lat_dim = ('y', 'x') - self._lon_dim = ('y', 'x') + self._var_dim = ("y", "x") + self._lat_dim = ("y", "x") + self._lon_dim = ("y", "x") - self.free_vars('crs') + self.free_vars("crs") @staticmethod - def new(comm=None, path=None, info=False, dataset=None, parallel_method='Y', + def new(comm=None, path=None, info=False, dataset=None, parallel_method="Y", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, balanced=False, times=None, **kwargs): """ @@ -117,8 +118,8 @@ class MercatorNes(Nes): dataset: Dataset NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'Y'. - Accepted values: ['X', 'Y', 'T']. + Indicates the parallelization method that you want. Default: "Y". + Accepted values: ["X", "Y", "T"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -143,20 +144,96 @@ class MercatorNes(Nes): return new - def filter_coordinates_selection(self): + def get_full_y(self) -> Dict[str, Any]: + """ + Retrieve the complete Y information. + + Returns + ------- + Dict[str, Any] + A dictionary containing the complete latitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of latitude values. + attr_name: attr_value, # Latitude attributes. + ... + } + """ + data = self.comm.bcast(self._full_y) + + return data + + def get_full_x(self) -> Dict[str, Any]: + """ + Retrieve the complete X information. + + Returns + ------- + Dict[str, Any] + A dictionary containing the complete longitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of longitude values. + attr_name: attr_value, # Longitude attributes. + ... + } + """ + data = self.comm.bcast(self._full_x) + return data + + def set_full_y(self, data: Dict[str, Any]) -> None: + """ + Set the complete Y information. + + Parameters + ---------- + data : Dict[str, Any] + A dictionary containing the complete latitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of latitude values. + attr_name: attr_value, # Latitude attributes. + ... + } + """ + if self.master: + self._full_y = data + return None + + def set_full_x(self, data: Dict[str, Any]) -> None: + """ + Set the complete rotated longitude information. + + Parameters + ---------- + data : Dict[str, Any] + A dictionary containing the complete longitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of longitude values. + attr_name: attr_value, # Longitude attributes. + ... + } + """ + if self.master: + self._full_x = data + return None + + # noinspection DuplicatedCode + def _filter_coordinates_selection(self): """ Use the selection limits to filter y, x, time, lev, lat, lon, lon_bnds and lat_bnds. """ - idx = self.get_idx_intervals() + idx = self._get_idx_intervals() - self.y = self._get_coordinate_values(self._y, 'Y') - self.x = self._get_coordinate_values(self._x, 'X') + self.y = self._get_coordinate_values(self.get_full_y(), "Y") + self.x = self._get_coordinate_values(self.get_full_x(), "X") - self._y['data'] = self._y['data'][idx['idx_y_min']:idx['idx_y_max']] - self._x['data'] = self._x['data'][idx['idx_x_min']:idx['idx_x_max']] + self.set_full_y({'data': self.y["data"][idx["idx_y_min"]:idx["idx_y_max"]]}) + self.set_full_x({'data': self.x["data"][idx["idx_x_min"]:idx["idx_x_max"]]}) - super(MercatorNes, self).filter_coordinates_selection() + super(MercatorNes, self)._filter_coordinates_selection() return None @@ -170,64 +247,58 @@ class MercatorNes(Nes): Grid projection. """ - projection = Proj(proj='merc', + projection = Proj(proj="merc", a=self.earth_radius[1], b=self.earth_radius[0], - lat_ts=np.float64(self.projection_data['standard_parallel']), - lon_0=np.float64(self.projection_data['longitude_of_projection_origin']), - ) + lat_ts=float64(self.projection_data["standard_parallel"]), + lon_0=float64(self.projection_data["longitude_of_projection_origin"]),) return projection - - def _get_projection(self): - """ - Get 'projection' and 'projection_data' from grid details. - """ - if 'mercator' in self.variables.keys(): - projection_data = self.variables['mercator'] - self.free_vars('mercator') + # noinspection DuplicatedCode + def _get_projection_data(self, create_nes, **kwargs): + """ + Retrieves projection data based on grid details. + Parameters + ---------- + create_nes : bool + Flag indicating whether to create new object (True) or use existing (False). + **kwargs : dict + Additional keyword arguments for specifying projection details. + """ + if create_nes: + projection_data = {"grid_mapping_name": "mercator", + "standard_parallel": kwargs["lat_ts"], + "longitude_of_projection_origin": kwargs["lon_0"], + "x_0": kwargs["x_0"], "y_0": kwargs["y_0"], + "inc_x": kwargs["inc_x"], "inc_y": kwargs["inc_y"], + "nx": kwargs["nx"], "ny": kwargs["ny"], + } else: - msg = 'There is no variable called mercator, projection has not been defined.' - raise RuntimeError(msg) + if "mercator" in self.variables.keys(): + projection_data = self.variables["mercator"] + self.free_vars("mercator") - if 'dtype' in projection_data.keys(): - del projection_data['dtype'] - - if 'data' in projection_data.keys(): - del projection_data['data'] - - if 'dimensions' in projection_data.keys(): - del projection_data['dimensions'] + else: + msg = "There is no variable called mercator, projection has not been defined." + raise RuntimeError(msg) - self.projection_data = projection_data - self.projection = self._get_pyproj_projection() + if "dtype" in projection_data.keys(): + del projection_data["dtype"] - return None - - def _create_projection(self, **kwargs): - """ - Create 'projection' and 'projection_data' from projection arguments. - """ + if "data" in projection_data.keys(): + del projection_data["data"] - projection_data = {'grid_mapping_name': 'mercator', - 'standard_parallel': kwargs['lat_ts'], # TODO: Check if True - 'longitude_of_projection_origin': kwargs['lon_0'], - 'x_0': kwargs['x_0'], 'y_0': kwargs['y_0'], - 'inc_x': kwargs['inc_x'], 'inc_y': kwargs['inc_y'], - 'nx': kwargs['nx'], 'ny': kwargs['ny'], - } + if "dimensions" in projection_data.keys(): + del projection_data["dimensions"] + return projection_data - self.projection_data = projection_data - self.projection = self._get_pyproj_projection() - - return None - + # noinspection DuplicatedCode def _create_dimensions(self, netcdf): """ - Create 'y', 'x' and 'spatial_nv' dimensions and the super dimensions 'lev', 'time', 'time_nv', 'lon' and 'lat' + Create "y", "x" and "spatial_nv" dimensions and the super dimensions "lev", "time", "time_nv", "lon" and "lat" Parameters ---------- @@ -238,18 +309,19 @@ class MercatorNes(Nes): super(MercatorNes, self)._create_dimensions(netcdf) # Create y and x dimensions - netcdf.createDimension('y', len(self._y['data'])) - netcdf.createDimension('x', len(self._x['data'])) + netcdf.createDimension("y", len(self.get_full_y()["data"])) + netcdf.createDimension("x", len(self.get_full_x()["data"])) # Create spatial_nv (number of vertices) dimension - if (self._lat_bnds is not None) and (self._lon_bnds is not None): - netcdf.createDimension('spatial_nv', 4) + if (self.lat_bnds is not None) and (self.lon_bnds is not None): + netcdf.createDimension("spatial_nv", 4) return None + # noinspection DuplicatedCode def _create_dimension_variables(self, netcdf): """ - Create the 'y' and 'x' variables. + Create the "y" and "x" variables. Parameters ---------- @@ -260,62 +332,65 @@ class MercatorNes(Nes): super(MercatorNes, self)._create_dimension_variables(netcdf) # MERCATOR Y COORDINATES - y = netcdf.createVariable('y', self._y['data'].dtype, ('y',)) - y.long_name = 'y coordinate of projection' - if 'units' in self._y.keys(): - y.units = self._y['units'] + full_y = self.get_full_y() + y = netcdf.createVariable("y", full_y["data"].dtype, ("y",)) + y.long_name = "y coordinate of projection" + if "units" in full_y.keys(): + y.units = full_y["units"] else: - y.units = 'm' - y.standard_name = 'projection_y_coordinate' + y.units = "m" + y.standard_name = "projection_y_coordinate" if self.size > 1: y.set_collective(True) - y[:] = self._y['data'] + y[:] = full_y["data"] # MERCATOR X COORDINATES - x = netcdf.createVariable('x', self._x['data'].dtype, ('x',)) - x.long_name = 'x coordinate of projection' - if 'units' in self._x.keys(): - x.units = self._x['units'] + full_x = self.get_full_x() + x = netcdf.createVariable("x", full_x["data"].dtype, ("x",)) + x.long_name = "x coordinate of projection" + if "units" in full_x.keys(): + x.units = full_x["units"] else: - x.units = 'm' - x.standard_name = 'projection_x_coordinate' + x.units = "m" + x.standard_name = "projection_x_coordinate" if self.size > 1: x.set_collective(True) - x[:] = self._x['data'] + x[:] = full_x["data"] return None + # noinspection DuplicatedCode def _create_centre_coordinates(self, **kwargs): """ Calculate centre latitudes and longitudes from grid details. """ + if self.master: + # Get projection details on x + x_0 = float64(self.projection_data["x_0"]) + inc_x = float64(self.projection_data["inc_x"]) + nx = int(self.projection_data["nx"]) - # Get projection details on x - x_0 = np.float64(self.projection_data['x_0']) - inc_x = np.float64(self.projection_data['inc_x']) - nx = int(self.projection_data['nx']) + # Get projection details on y + y_0 = float64(self.projection_data["y_0"]) + inc_y = float64(self.projection_data["inc_y"]) + ny = int(self.projection_data["ny"]) - # Get projection details on y - y_0 = np.float64(self.projection_data['y_0']) - inc_y = np.float64(self.projection_data['inc_y']) - ny = int(self.projection_data['ny']) + # Create a regular grid in metres (1D) + self._full_x = {"data": linspace(x_0 + (inc_x / 2), x_0 + (inc_x / 2) + (inc_x * (nx - 1)), nx, + dtype=float64)} + self._full_y = {"data": linspace(y_0 + (inc_y / 2), y_0 + (inc_y / 2) + (inc_y * (ny - 1)), ny, + dtype=float64)} - # Create a regular grid in metres (1D) - self._x = {'data': np.linspace(x_0 + (inc_x / 2), - x_0 + (inc_x / 2) + (inc_x * (nx - 1)), - nx, dtype=np.float64)} - self._y = {'data': np.linspace(y_0 + (inc_y / 2), - y_0 + (inc_y / 2) + (inc_y * (ny - 1)), - ny, dtype=np.float64)} + # Create a regular grid in metres (1D to 2D) + x = array([self._full_x["data"]] * len(self._full_y["data"])) + y = array([self._full_y["data"]] * len(self._full_x["data"])).T - # Create a regular grid in metres (1D to 2D) - x = np.array([self._x['data']] * len(self._y['data'])) - y = np.array([self._y['data']] * len(self._x['data'])).T + # Calculate centre latitudes and longitudes (UTM to Mercator) + centre_lon, centre_lat = self.projection(x, y, inverse=True) - # Calculate centre latitudes and longitudes (UTM to Mercator) - centre_lon, centre_lat = self.projection(x, y, inverse=True) - - return {'data': centre_lat}, {'data': centre_lon} + return {"data": centre_lat}, {"data": centre_lon} + else: + return None, None def create_providentia_exp_centre_coordinates(self): """ @@ -337,6 +412,7 @@ class MercatorNes(Nes): return model_centre_lat, model_centre_lon + # noinspection DuplicatedCode def create_providentia_exp_grid_edge_coordinates(self): """ Calculate grid edge latitudes and longitudes and get model grid outline. @@ -350,75 +426,73 @@ class MercatorNes(Nes): """ # Get grid resolution - inc_x = np.abs(np.mean(np.diff(self.x['data']))) - inc_y = np.abs(np.mean(np.diff(self.y['data']))) + inc_x = abs(mean(diff(self.x["data"]))) + inc_y = abs(mean(diff(self.y["data"]))) # Get bounds for rotated coordinates - y_bounds = self.create_single_spatial_bounds(self.y['data'], inc_y) - x_bounds = self.create_single_spatial_bounds(self.x['data'], inc_x) + y_bounds = self._create_single_spatial_bounds(self.y["data"], inc_y) + x_bounds = self._create_single_spatial_bounds(self.x["data"], inc_x) # Get rotated latitudes for grid edge - left_edge_y = np.append(y_bounds.flatten()[::2], y_bounds.flatten()[-1]) - right_edge_y = np.flip(left_edge_y, 0) - top_edge_y = np.repeat(y_bounds[-1][-1], len(self.x['data']) - 1) - bottom_edge_y = np.repeat(y_bounds[0][0], len(self.x['data'])) - y_grid_edge = np.concatenate((left_edge_y, top_edge_y, right_edge_y, bottom_edge_y)) + left_edge_y = append(y_bounds.flatten()[::2], y_bounds.flatten()[-1]) + right_edge_y = flip(left_edge_y, 0) + top_edge_y = repeat(y_bounds[-1][-1], len(self.x["data"]) - 1) + bottom_edge_y = repeat(y_bounds[0][0], len(self.x["data"])) + y_grid_edge = concatenate((left_edge_y, top_edge_y, right_edge_y, bottom_edge_y)) # Get rotated longitudes for grid edge - left_edge_x = np.repeat(x_bounds[0][0], len(self.y['data']) + 1) + left_edge_x = repeat(x_bounds[0][0], len(self.y["data"]) + 1) top_edge_x = x_bounds.flatten()[1:-1:2] - right_edge_x = np.repeat(x_bounds[-1][-1], len(self.y['data']) + 1) - bottom_edge_x = np.flip(x_bounds.flatten()[:-1:2], 0) - x_grid_edge = np.concatenate((left_edge_x, top_edge_x, right_edge_x, bottom_edge_x)) + right_edge_x = repeat(x_bounds[-1][-1], len(self.y["data"]) + 1) + bottom_edge_x = flip(x_bounds.flatten()[:-1:2], 0) + x_grid_edge = concatenate((left_edge_x, top_edge_x, right_edge_x, bottom_edge_x)) # Get edges for regular coordinates grid_edge_lon_data, grid_edge_lat_data = self.projection(x_grid_edge, y_grid_edge, inverse=True) # Create grid outline by stacking the edges in both coordinates - model_grid_outline = np.vstack((grid_edge_lon_data, grid_edge_lat_data)).T - grid_edge_lat = {'data': model_grid_outline[:,1]} - grid_edge_lon = {'data': model_grid_outline[:,0]} + model_grid_outline = vstack((grid_edge_lon_data, grid_edge_lat_data)).T + grid_edge_lat = {"data": model_grid_outline[:, 1]} + grid_edge_lon = {"data": model_grid_outline[:, 0]} return grid_edge_lat, grid_edge_lon + # noinspection DuplicatedCode def create_spatial_bounds(self): """ Calculate longitude and latitude bounds and set them. """ # Calculate Mercator coordinates bounds - inc_x = np.abs(np.mean(np.diff(self._x['data']))) - x_bnds = self.create_single_spatial_bounds(np.array([self._x['data']] * len(self._y['data'])), - inc_x, spatial_nv=4) + full_x = self.get_full_x() + full_y = self.get_full_y() + inc_x = abs(mean(diff(full_x["data"]))) + x_bnds = self._create_single_spatial_bounds(array([full_x["data"]] * len(full_y["data"])), + inc_x, spatial_nv=4) - inc_y = np.abs(np.mean(np.diff(self._y['data']))) - y_bnds = self.create_single_spatial_bounds(np.array([self._y['data']] * len(self._x['data'])).T, - inc_y, spatial_nv=4, inverse=True) + inc_y = abs(mean(diff(full_y["data"]))) + y_bnds = self._create_single_spatial_bounds(array([full_y["data"]] * len(full_x["data"])).T, + inc_y, spatial_nv=4, inverse=True) # Transform Mercator bounds to regular bounds lon_bnds, lat_bnds = self.projection(x_bnds, y_bnds, inverse=True) # Obtain regular coordinates bounds - self._lat_bnds = {} - self._lat_bnds['data'] = deepcopy(lat_bnds) - self.lat_bnds = {} - self.lat_bnds['data'] = lat_bnds[self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - :] - - self._lon_bnds = {} - self._lon_bnds['data'] = deepcopy(lon_bnds) - self.lon_bnds = {} - self.lon_bnds['data'] = lon_bnds[self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - :] + self.set_full_latitudes_boundaries({"data": deepcopy(lat_bnds)}) + self.lat_bnds = {"data": lat_bnds[self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + :]} + self.set_full_longitudes_boundaries({"data": deepcopy(lon_bnds)}) + self.lon_bnds = {"data": lon_bnds[self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + :]} return None @staticmethod def _set_var_crs(var): """ - Set the grid_mapping to 'mercator'. + Set the grid_mapping to "mercator". Parameters ---------- @@ -426,14 +500,14 @@ class MercatorNes(Nes): netCDF4-python variable object. """ - var.grid_mapping = 'mercator' + var.grid_mapping = "mercator" var.coordinates = "lat lon" return None def _create_metadata(self, netcdf): """ - Create the 'crs' variable for the Mercator grid_mapping. + Create the "crs" variable for the Mercator grid_mapping. Parameters ---------- @@ -442,10 +516,10 @@ class MercatorNes(Nes): """ if self.projection_data is not None: - mapping = netcdf.createVariable('mercator', 'i') - mapping.grid_mapping_name = self.projection_data['grid_mapping_name'] - mapping.standard_parallel = self.projection_data['standard_parallel'] - mapping.longitude_of_projection_origin = self.projection_data['longitude_of_projection_origin'] + mapping = netcdf.createVariable("mercator", "i") + mapping.grid_mapping_name = self.projection_data["grid_mapping_name"] + mapping.standard_parallel = self.projection_data["standard_parallel"] + mapping.longitude_of_projection_origin = self.projection_data["longitude_of_projection_origin"] return None @@ -455,6 +529,8 @@ class MercatorNes(Nes): Parameters ---------- + lat_flip : bool + Indicates if you want to flip latitudes Up-Down path : str Path to the output file. grib_keys : dict @@ -467,9 +543,10 @@ class MercatorNes(Nes): raise NotImplementedError("Grib2 format cannot be written in a Mercator projection.") + # noinspection DuplicatedCode def create_shapefile(self): """ - Create spatial geodataframe (shapefile). + Create spatial GeoDataFrame (shapefile). Returns ------- @@ -480,29 +557,27 @@ class MercatorNes(Nes): if self.shapefile is None: # Get latitude and longitude cell boundaries - if self._lat_bnds is None or self._lon_bnds is None: + if self.lat_bnds is None or self.lon_bnds is None: self.create_spatial_bounds() # Reshape arrays to create geometry - aux_b_lats = self.lat_bnds['data'].reshape((self.lat_bnds['data'].shape[0] * self.lat_bnds['data'].shape[1], - self.lat_bnds['data'].shape[2])) - aux_b_lons = self.lon_bnds['data'].reshape((self.lon_bnds['data'].shape[0] * self.lon_bnds['data'].shape[1], - self.lon_bnds['data'].shape[2])) + aux_b_lat = self.lat_bnds["data"].reshape((self.lat_bnds["data"].shape[0] * self.lat_bnds["data"].shape[1], + self.lat_bnds["data"].shape[2])) + aux_b_lon = self.lon_bnds["data"].reshape((self.lon_bnds["data"].shape[0] * self.lon_bnds["data"].shape[1], + self.lon_bnds["data"].shape[2])) # Get polygons from bounds geometry = [] - for i in range(aux_b_lons.shape[0]): - geometry.append(Polygon([(aux_b_lons[i, 0], aux_b_lats[i, 0]), - (aux_b_lons[i, 1], aux_b_lats[i, 1]), - (aux_b_lons[i, 2], aux_b_lats[i, 2]), - (aux_b_lons[i, 3], aux_b_lats[i, 3]), - (aux_b_lons[i, 0], aux_b_lats[i, 0])])) + for i in range(aux_b_lon.shape[0]): + geometry.append(Polygon([(aux_b_lon[i, 0], aux_b_lat[i, 0]), + (aux_b_lon[i, 1], aux_b_lat[i, 1]), + (aux_b_lon[i, 2], aux_b_lat[i, 2]), + (aux_b_lon[i, 3], aux_b_lat[i, 3]), + (aux_b_lon[i, 0], aux_b_lat[i, 0])])) - # Create dataframe cointaining all polygons + # Create dataframe containing all polygons fids = self.get_fids() - gdf = gpd.GeoDataFrame(index=pd.Index(name='FID', data=fids.ravel()), - geometry=geometry, - crs="EPSG:4326") + gdf = GeoDataFrame(index=Index(name="FID", data=fids.ravel()), geometry=geometry, crs="EPSG:4326") self.shapefile = gdf else: @@ -510,6 +585,7 @@ class MercatorNes(Nes): return gdf + # noinspection DuplicatedCode def get_centroids_from_coordinates(self): """ Get centroids from geographical coordinates. @@ -522,15 +598,13 @@ class MercatorNes(Nes): # Get centroids from coordinates centroids = [] - for lat_ind in range(0, self.lon['data'].shape[0]): - for lon_ind in range(0, self.lon['data'].shape[1]): - centroids.append(Point(self.lon['data'][lat_ind, lon_ind], - self.lat['data'][lat_ind, lon_ind])) + for lat_ind in range(0, self.lon["data"].shape[0]): + for lon_ind in range(0, self.lon["data"].shape[1]): + centroids.append(Point(self.lon["data"][lat_ind, lon_ind], + self.lat["data"][lat_ind, lon_ind])) - # Create dataframe cointaining all points + # Create dataframe containing all points fids = self.get_fids() - centroids_gdf = gpd.GeoDataFrame(index=pd.Index(name='FID', data=fids.ravel()), - geometry=centroids, - crs="EPSG:4326") + centroids_gdf = GeoDataFrame(index=Index(name="FID", data=fids.ravel()), geometry=centroids, crs="EPSG:4326") return centroids_gdf diff --git a/nes/nc_projections/points_nes.py b/nes/nc_projections/points_nes.py index 0a762f86ba0dccd6581c7257c44740e69a65195a..365b9ef5ea00dd6f7e6ae8389b435d10fbb3d3dc 100644 --- a/nes/nc_projections/points_nes.py +++ b/nes/nc_projections/points_nes.py @@ -1,12 +1,13 @@ #!/usr/bin/env python import sys -import warnings -import numpy as np -import pandas as pd +from warnings import warn +from numpy import float64, arange, array, ndarray, generic, issubdtype, character, concatenate +from pandas import Index +from geopandas import GeoDataFrame, points_from_xy +from pyproj import Proj from copy import deepcopy -import geopandas as gpd -from netCDF4 import date2num, stringtochar +from netCDF4 import date2num from .default_nes import Nes @@ -16,37 +17,38 @@ class PointsNes(Nes): Attributes ---------- _var_dim : tuple - Tuple with the name of the Y and X dimensions for the variables. - ('lat', 'lon') for a points grid. + A Tuple with the name of the Y and X dimensions for the variables. + ("lat", "lon", ) for a points grid. _lat_dim : tuple - Tuple with the name of the dimensions of the Latitude values. - ('lat',) for a points grid. + A Tuple with the name of the dimensions of the Latitude values. + ("lat", ) for a points grid. _lon_dim : tuple - Tuple with the name of the dimensions of the Longitude values. - ('lon',) for a points grid. + A Tuple with the name of the dimensions of the Longitude values. + ("lon", ) for a points grid. _station : tuple - Tuple with the name of the dimensions of the station values. - ('station',) for a points grid. + A Tuple with the name of the dimensions of the station values. + ("station", ) for a points grid. """ - def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method='X', - avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, + + def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method="X", + avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, balanced=False, times=None, **kwargs): """ Initialize the PointsNes class. Parameters ---------- - comm: MPI.Communicator + comm: MPI.Comm MPI Communicator. path: str Path to the NetCDF to initialize the object. info: bool Indicates if you want to get reading/writing info. - dataset: Dataset, None + dataset: Dataset or None NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'X'. - accepted values: ['X', 'T']. + Indicates the parallelization method that you want. Default: "X". + accepted values: ["X", "T"]. strlen: int Maximum length of strings in NetCDF. Default: 75. avoid_first_hours : int @@ -70,28 +72,28 @@ class PointsNes(Nes): parallel_method=parallel_method, avoid_first_hours=avoid_first_hours, avoid_last_hours=avoid_last_hours, first_level=first_level, last_level=last_level, create_nes=create_nes, - times=times, **kwargs) + times=times, balanced=balanced, **kwargs) if create_nes: # Dimensions screening - self.lat = self._get_coordinate_values(self._lat, 'X') - self.lon = self._get_coordinate_values(self._lon, 'X') + self.lat = self._get_coordinate_values(self.get_full_latitudes(), "X") + self.lon = self._get_coordinate_values(self.get_full_longitudes(), "X") # Complete dimensions - self._station = {'data': np.arange(len(self._lon['data']))} + self._station = {"data": arange(len(self.get_full_longitudes()["data"]))} # Dimensions screening - self.station = self._get_coordinate_values(self._station, 'X') + self.station = self._get_coordinate_values(self._station, "X") # Set axis limits for parallel writing - self.write_axis_limits = self.get_write_axis_limits() + self.write_axis_limits = self._get_write_axis_limits() - self._var_dim = ('station',) - self._lat_dim = ('station',) - self._lon_dim = ('station',) + self._var_dim = ("station",) + self._lat_dim = ("station",) + self._lon_dim = ("station",) @staticmethod - def new(comm=None, path=None, info=False, dataset=None, parallel_method='X', + def new(comm=None, path=None, info=False, dataset=None, parallel_method="X", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, balanced=False, times=None, **kwargs): """ @@ -108,8 +110,8 @@ class PointsNes(Nes): dataset: Dataset NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'X'. - accepted values: ['X', 'T']. + Indicates the parallelization method that you want. Default: "X". + accepted values: ["X", "T"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -134,29 +136,38 @@ class PointsNes(Nes): return new - def _get_projection(self): + @staticmethod + def _get_pyproj_projection(): """ - Get 'projection' and 'projection_data' from grid details. + Get projection data as in Pyproj library. + + Returns + ---------- + projection : pyproj.Proj + Grid projection. """ - self.projection_data = None - self.projection = None + projection = Proj(proj="latlong", ellps="WGS84",) - return None + return projection - def _create_projection(self, **kwargs): - """ - Create 'projection' and 'projection_data' from projection arguments. + def _get_projection_data(self, create_nes, **kwargs): """ + Retrieves projection data based on grid details. - self.projection_data = None - self.projection = None + Parameters + ---------- + create_nes : bool + Flag indicating whether to create new object (True) or use existing (False). + **kwargs : dict + Additional keyword arguments for specifying projection details. + """ return None - + def _create_dimensions(self, netcdf): """ - Create 'time', 'time_nv', 'station' and 'strlen' dimensions. + Create "time", "time_nv", "station" and "strlen" dimensions. Parameters ---------- @@ -165,25 +176,26 @@ class PointsNes(Nes): """ # Create time dimension - netcdf.createDimension('time', None) + netcdf.createDimension("time", None) # Create time_nv (number of vertices) dimension - if self._time_bnds is not None: - netcdf.createDimension('time_nv', 2) + if self.time_bnds is not None: + netcdf.createDimension("time_nv", 2) # Create station dimension # The number of longitudes is equal to the number of stations - netcdf.createDimension('station', len(self._lon['data'])) + netcdf.createDimension("station", len(self.get_full_longitudes()["data"])) # Create string length dimension if self.strlen is not None: - netcdf.createDimension('strlen', self.strlen) + netcdf.createDimension("strlen", self.strlen) return None + # noinspection DuplicatedCode def _create_dimension_variables(self, netcdf): """ - Create the 'time', 'time_bnds', 'station', 'lat', 'lat_bnds', 'lon' and 'lon_bnds' variables. + Create the "time", "time_bnds", "station", "lat", "lat_bnds", "lon" and "lon_bnds" variables. Parameters ---------- @@ -192,67 +204,66 @@ class PointsNes(Nes): """ # TIMES - time_var = netcdf.createVariable('time', np.float64, ('time',), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - time_var.units = 'hours since {0}'.format( - self._time[self.get_time_id(self.hours_start, first=True)].strftime('%Y-%m-%d %H:%M:%S')) - time_var.standard_name = 'time' - time_var.calendar = 'standard' - time_var.long_name = 'time' - if self._time_bnds is not None: - time_var.bounds = 'time_bnds' + time_var = netcdf.createVariable("time", float64, ("time",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) + time_var.units = "hours since {0}".format( + self.get_full_times()[self._get_time_id(self.hours_start, first=True)].strftime("%Y-%m-%d %H:%M:%S")) + time_var.standard_name = "time" + time_var.calendar = "standard" + time_var.long_name = "time" + if self.time_bnds is not None: + time_var.bounds = "time_bnds" if self.size > 1: time_var.set_collective(True) - time_var[:] = date2num(self._time[self.get_time_id(self.hours_start, first=True): - self.get_time_id(self.hours_end, first=False)], + time_var[:] = date2num(self.get_full_times()[self._get_time_id(self.hours_start, first=True): + self._get_time_id(self.hours_end, first=False)], time_var.units, time_var.calendar) # TIME BOUNDS - if self._time_bnds is not None: - time_bnds_var = netcdf.createVariable('time_bnds', np.float64, ('time', 'time_nv',), zlib=self.zip_lvl, + if self.time_bnds is not None: + time_bnds_var = netcdf.createVariable("time_bnds", float64, ("time", "time_nv",), zlib=self.zip_lvl, complevel=self.zip_lvl) if self.size > 1: time_bnds_var.set_collective(True) - time_bnds_var[:] = date2num(self._time_bnds, time_var.units, calendar='standard') + time_bnds_var[:] = date2num(self.get_full_time_bnds(), time_var.units, calendar="standard") # STATIONS - stations = netcdf.createVariable('station', np.float64, ('station',), zlib=self.zip_lvl > 0, + stations = netcdf.createVariable("station", float64, ("station",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - stations.units = '' - stations.axis = 'X' - stations.long_name = '' - stations.standard_name = 'station' + stations.units = "" + stations.axis = "X" + stations.long_name = "" + stations.standard_name = "station" if self.size > 1: stations.set_collective(True) - stations[:] = self._station['data'] + stations[:] = self._station["data"] # LATITUDES - lat = netcdf.createVariable('lat', np.float64, self._lat_dim, - zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - lat.units = 'degrees_north' - lat.axis = 'Y' - lat.long_name = 'latitude coordinate' - lat.standard_name = 'latitude' - if self._lat_bnds is not None: - lat.bounds = 'lat_bnds' + lat = netcdf.createVariable("lat", float64, self._lat_dim, zlib=self.zip_lvl > 0, complevel=self.zip_lvl) + lat.units = "degrees_north" + lat.axis = "Y" + lat.long_name = "latitude coordinate" + lat.standard_name = "latitude" + if self.lat_bnds is not None: + lat.bounds = "lat_bnds" if self.size > 1: lat.set_collective(True) - lat[:] = self._lat['data'] + lat[:] = self.get_full_latitudes()["data"] # LONGITUDES - lon = netcdf.createVariable('lon', np.float64, self._lon_dim, - zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - lon.units = 'degrees_east' - lon.axis = 'X' - lon.long_name = 'longitude coordinate' - lon.standard_name = 'longitude' - if self._lon_bnds is not None: - lon.bounds = 'lon_bnds' + lon = netcdf.createVariable("lon", float64, self._lon_dim, zlib=self.zip_lvl > 0, complevel=self.zip_lvl) + lon.units = "degrees_east" + lon.axis = "X" + lon.long_name = "longitude coordinate" + lon.standard_name = "longitude" + if self.lon_bnds is not None: + lon.bounds = "lon_bnds" if self.size > 1: lon.set_collective(True) - lon[:] = self._lon['data'] + lon[:] = self.get_full_longitudes()["data"] return None + # noinspection DuplicatedCode def _get_coordinate_values(self, coordinate_info, coordinate_axis, bounds=False): """ Get the coordinate data of the current portion. @@ -260,9 +271,9 @@ class PointsNes(Nes): Parameters ---------- coordinate_info : dict, list - Dictionary with the 'data' key with the coordinate variable values. and the attributes as other keys. + Dictionary with the "data" key with the coordinate variable values. and the attributes as other keys. coordinate_axis : str - Name of the coordinate to extract. Accepted values: ['X']. + Name of the coordinate to extract. Accepted values: ["X"]. bounds : bool Boolean variable to know if there are coordinate bounds. Returns @@ -275,23 +286,23 @@ class PointsNes(Nes): return None if not isinstance(coordinate_info, dict): - values = {'data': deepcopy(coordinate_info)} + values = {"data": deepcopy(coordinate_info)} else: values = deepcopy(coordinate_info) - coordinate_len = len(values['data'].shape) + coordinate_len = len(values["data"].shape) if bounds: coordinate_len -= 1 - if coordinate_axis == 'X': + if coordinate_axis == "X": if coordinate_len == 1: - values['data'] = values['data'][self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + values["data"] = values["data"][self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] elif coordinate_len == 2: - values['data'] = values['data'][self.read_axis_limits['t_min']:self.read_axis_limits['t_max'], - self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + values["data"] = values["data"][self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"], + self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] else: raise NotImplementedError("The coordinate has wrong dimensions: {dim}".format( - dim=values['data'].shape)) + dim=values["data"].shape)) return values @@ -306,7 +317,7 @@ class PointsNes(Nes): Returns ------- - data: np.array + data: array Portion of the variable data corresponding to the rank. """ @@ -315,15 +326,14 @@ class PointsNes(Nes): # Read data in 1 or 2 dimensions if len(var_dims) < 2: - data = nc_var[self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + data = nc_var[self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] elif len(var_dims) == 2: - if 'strlen' in var_dims: - data = nc_var[self.read_axis_limits['x_min']:self.read_axis_limits['x_max'], :] - data = np.array([''.join(i.tobytes().decode('ascii').replace('\x00', '')) for i in data], - dtype=object) + if "strlen" in var_dims: + data = nc_var[self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"], :] + data = array(["".join(i.tobytes().decode("ascii").replace("\x00", "")) for i in data], dtype=object) else: - data = nc_var[self.read_axis_limits['t_min']:self.read_axis_limits['t_max'], - self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + data = nc_var[self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"], + self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] else: raise NotImplementedError("Error with {0}. Only can be read netCDF with 2 dimensions or less".format( var_name)) @@ -333,6 +343,7 @@ class PointsNes(Nes): return data + # noinspection DuplicatedCode def _create_variables(self, netcdf, chunking=False): """ Create the netCDF file variables. @@ -348,52 +359,52 @@ class PointsNes(Nes): if self.variables is not None: for i, (var_name, var_dict) in enumerate(self.variables.items()): # Get data type - if 'dtype' in var_dict.keys(): - var_dtype = var_dict['dtype'] - if (var_dict['data'] is not None) and (var_dtype != var_dict['data'].dtype): + if "dtype" in var_dict.keys(): + var_dtype = var_dict["dtype"] + if (var_dict["data"] is not None) and (var_dtype != var_dict["data"].dtype): msg = "WARNING!!! " msg += "Different data types for variable {0}. ".format(var_name) - msg += "Input dtype={0}. Data dtype={1}.".format(var_dtype, var_dict['data'].dtype) - warnings.warn(msg) + msg += "Input dtype={0}. Data dtype={1}.".format(var_dtype, var_dict["data"].dtype) + warn(msg) sys.stderr.flush() try: - var_dict['data'] = var_dict['data'].astype(var_dtype) - except Exception as e: # TODO: Detect exception - raise e("It was not possible to cast the data to the input dtype.") + var_dict["data"] = var_dict["data"].astype(var_dtype) + except Exception: # TODO: Detect exception + raise TypeError("It was not possible to cast the data to the input dtype.") else: - var_dtype = var_dict['data'].dtype + var_dtype = var_dict["data"].dtype if var_dtype is object: raise TypeError("Data dtype is object. Define dtype explicitly as dictionary key 'dtype'") # Get dimensions when reading datasets - if 'dimensions' in var_dict.keys(): - var_dims = var_dict['dimensions'] + if "dimensions" in var_dict.keys(): + var_dims = var_dict["dimensions"] # Get dimensions when creating new datasets else: - if len(var_dict['data'].shape) == 1: + if len(var_dict["data"].shape) == 1: # For data that depends only on station (e.g. station_code) var_dims = self._var_dim else: # For data that is dependent on time and station (e.g. PM10) - var_dims = ('time',) + self._var_dim + var_dims = ("time",) + self._var_dim - if var_dict['data'] is not None: + if var_dict["data"] is not None: # Ensure data is of type numpy array (to create NES) - if not isinstance(var_dict['data'], (np.ndarray, np.generic)): + if not isinstance(var_dict["data"], (ndarray, generic)): try: - var_dict['data'] = np.array(var_dict['data']) + var_dict["data"] = array(var_dict["data"]) except AttributeError: raise AttributeError("Data for variable {0} must be a numpy array.".format(var_name)) # Convert list of strings to chars for parallelization - if np.issubdtype(var_dtype, np.character): - var_dict['data_aux'] = self.str2char(var_dict['data']) - var_dims += ('strlen',) - var_dtype = 'S1' - + if issubdtype(var_dtype, character): + var_dict["data_aux"] = self._str2char(var_dict["data"]) + var_dims += ("strlen",) + var_dtype = "S1" + if self.info: - print('Rank {0:03d}: Writing {1} var ({2}/{3})'.format(self.rank, var_name, i + 1, + print("Rank {0:03d}: Writing {1} var ({2}/{3})".format(self.rank, var_name, i + 1, len(self.variables))) if not chunking: var = netcdf.createVariable(var_name, var_dtype, var_dims, @@ -402,82 +413,86 @@ class PointsNes(Nes): if self.balanced: raise NotImplementedError("A balanced data cannot be chunked.") if self.master: - chunk_size = var_dict['data'].shape + chunk_size = var_dict["data"].shape else: chunk_size = None chunk_size = self.comm.bcast(chunk_size, root=0) var = netcdf.createVariable(var_name, var_dtype, var_dims, - zlib=self.zip_lvl > 0, complevel=self.zip_lvl, + zlib=self.zip_lvl > 0, complevel=self.zip_lvl, chunksizes=chunk_size) if self.info: - print('Rank {0:03d}: Var {1} created ({2}/{3})'.format( + print("Rank {0:03d}: Var {1} created ({2}/{3})".format( self.rank, var_name, i + 1, len(self.variables))) if self.size > 1: var.set_collective(True) if self.info: - print('Rank {0:03d}: Var {1} collective ({2}/{3})'.format( + print("Rank {0:03d}: Var {1} collective ({2}/{3})".format( self.rank, var_name, i + 1, len(self.variables))) - + for att_name, att_value in var_dict.items(): - if att_name == 'data': + if att_name == "data": if self.info: print("Rank {0:03d}: Filling {1})".format(self.rank, var_name)) - if 'data_aux' in var_dict.keys(): - att_value = var_dict['data_aux'] + if "data_aux" in var_dict.keys(): + att_value = var_dict["data_aux"] if len(att_value.shape) == 1: try: - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max']] = att_value + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] = att_value except IndexError: raise IndexError("Different shapes. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max']].shape, + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]].shape, att_value.shape)) except ValueError: - raise ValueError("Axis limits cannot be accessed. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max']].shape, - att_value.shape)) + raise ValueError("Axis limits cannot be accessed. out_shape={0}, data_shp={1}".format( + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]].shape, + att_value.shape)) elif len(att_value.shape) == 2: - if 'strlen' in var_dims: + if "strlen" in var_dims: try: - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], :] = att_value + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], :] = att_value except IndexError: raise IndexError("Different shapes. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], :].shape, + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], :].shape, att_value.shape)) except ValueError: - raise ValueError("Axis limits cannot be accessed. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], :].shape, - att_value.shape)) + raise ValueError( + "Axis limits cannot be accessed. out_shape={0}, data_shp={1}".format( + var[self.write_axis_limits["x_min"]: + self.write_axis_limits["x_max"]].shape, + att_value.shape)) else: try: - var[self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max']] = att_value + var[self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] = att_value except IndexError: - raise IndexError("Different shapes. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max']].shape, - att_value.shape)) + raise IndexError(f"Different shapes. out_shape={ + var[self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]].shape}, " + f"data_shp={att_value.shape}") except ValueError: - raise ValueError("Axis limits cannot be accessed. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max']].shape, - att_value.shape)) + raise ValueError(f"Axis limits cannot be accessed. out_shape={ + var[self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]].shape}, " + f"data_shp={att_value.shape}") + if self.info: - print('Rank {0:03d}: Var {1} data ({2}/{3})'.format(self.rank, var_name, i + 1, + print("Rank {0:03d}: Var {1} data ({2}/{3})".format(self.rank, var_name, i + 1, len(self.variables))) - elif att_name not in ['chunk_size', 'var_dims', 'dimensions', 'dtype', 'data_aux']: + elif att_name not in ["chunk_size", "var_dims", "dimensions", "dtype", "data_aux"]: var.setncattr(att_name, att_value) - if 'data_aux' in var_dict.keys(): - del var_dict['data_aux'] - + if "data_aux" in var_dict.keys(): + del var_dict["data_aux"] + self._set_var_crs(var) if self.info: - print('Rank {0:03d}: Var {1} completed ({2}/{3})'.format(self.rank, var_name, i + 1, + print("Rank {0:03d}: Var {1} completed ({2}/{3})".format(self.rank, var_name, i + 1, len(self.variables))) - + return None + # noinspection DuplicatedCode def _gather_data(self, data_to_gather): """ Gather all the variable data into the MPI rank 0 to perform a serial write. @@ -491,36 +506,34 @@ class PointsNes(Nes): for var_name, var_info in data_list.items(): try: # noinspection PyArgumentList - data_aux = self.comm.gather(data_list[var_name]['data'], root=0) + data_aux = self.comm.gather(data_list[var_name]["data"], root=0) if self.rank == 0: - shp_len = len(data_list[var_name]['data'].shape) - if self.parallel_method == 'X': + shp_len = len(data_list[var_name]["data"].shape) + if self.parallel_method == "X": # concatenate over station if shp_len == 1: # dimensions = (station) axis = 0 elif shp_len == 2: - if 'strlen' in var_info['dimensions']: + if "strlen" in var_info["dimensions"]: # dimensions = (station, strlen) axis = 0 else: # dimensions = (time, station) - axis = 1 + axis = 1 else: msg = "The points NetCDF must have " msg += "surface values (without levels)." raise NotImplementedError(msg) - elif self.parallel_method == 'T': + elif self.parallel_method == "T": # concatenate over time - if shp_len == 1: + if shp_len == 1: # dimensions = (station) axis = None - continue elif shp_len == 2: - if 'strlen' in var_info['dimensions']: + if "strlen" in var_info["dimensions"]: # dimensions = (station, strlen) axis = None - continue else: # dimensions = (time, station) axis = 0 @@ -528,20 +541,18 @@ class PointsNes(Nes): msg = "The points NetCDF must only have surface values (without levels)." raise NotImplementedError(msg) else: - raise NotImplementedError( + raise NotImplementedError( "Parallel method '{meth}' is not implemented. Use one of these: {accept}".format( - meth=self.parallel_method, accept=['X', 'T'])) - data_list[var_name]['data'] = np.concatenate(data_aux, axis=axis) + meth=self.parallel_method, accept=["X", "T"])) + data_list[var_name]["data"] = concatenate(data_aux, axis=axis) except Exception as e: - msg = "**ERROR** an error has occurred while gathering the '{0}' variable.\n".format(var_name) + msg = f"**ERROR** an error has occurred while gathering the '{var_name}' variable.\n" print(msg) sys.stderr.write(msg) print(e) sys.stderr.write(str(e)) - # print(e, file=sys.stderr) sys.stderr.flush() self.comm.Abort(1) - raise e return data_list @@ -556,12 +567,12 @@ class PointsNes(Nes): """ # Calculate centre latitudes - centre_lat = kwargs['lat'] + centre_lat = kwargs["lat"] # Calculate centre longitudes - centre_lon = kwargs['lon'] + centre_lon = kwargs["lon"] - return {'data': centre_lat}, {'data': centre_lon} + return {"data": centre_lat}, {"data": centre_lon} def _create_metadata(self, netcdf): """ @@ -571,7 +582,7 @@ class PointsNes(Nes): ---------- netcdf : Dataset NetCDF object. - """ + """ return None @@ -594,36 +605,36 @@ class PointsNes(Nes): from .points_nes_providentia import PointsNesProvidentia - points_nes_providentia = PointsNesProvidentia(comm=self.comm, - info=self.info, - balanced=self.balanced, - parallel_method=self.parallel_method, - avoid_first_hours=self.hours_start, - avoid_last_hours=self.hours_end, - first_level=self.first_level, - last_level=self.last_level, + points_nes_providentia = PointsNesProvidentia(comm=self.comm, + info=self.info, + balanced=self.balanced, + parallel_method=self.parallel_method, + avoid_first_hours=self.hours_start, + avoid_last_hours=self.hours_end, + first_level=self.first_level, + last_level=self.last_level, create_nes=True, times=self.time, model_centre_lon=model_centre_lon, model_centre_lat=model_centre_lat, grid_edge_lon=grid_edge_lon, grid_edge_lat=grid_edge_lat, - lat=self.lat['data'], - lon=self.lon['data'] + lat=self.lat["data"], + lon=self.lon["data"] ) - + # Convert dimensions (time, lev, lat, lon) to (station, time) for interpolated variables and reshape data variables = {} interpolated_variables = deepcopy(self.variables) for var_name, var_info in interpolated_variables.items(): variables[var_name] = {} - # ('time', 'lev', 'lat', 'lon') or ('time', 'lat', 'lon') to ('station', 'time') - if len(var_info['dimensions']) != len(var_info['data'].shape): - variables[var_name]['data'] = var_info['data'].T - variables[var_name]['dimensions'] = ('station', 'time') + # ("time", "lev", "lat", "lon") or ("time", "lat", "lon") to ("station", "time") + if len(var_info["dimensions"]) != len(var_info["data"].shape): + variables[var_name]["data"] = var_info["data"].T + variables[var_name]["dimensions"] = ("station", "time") else: - variables[var_name]['data'] = var_info['data'] - variables[var_name]['dimensions'] = var_info['dimensions'] + variables[var_name]["data"] = var_info["data"] + variables[var_name]["dimensions"] = var_info["dimensions"] # Set variables points_nes_providentia.variables = variables @@ -636,6 +647,8 @@ class PointsNes(Nes): Parameters ---------- + lat_flip : bool + Indicates if you want to flip the latitude direction. path : str Path to the output file. grib_keys : dict @@ -650,7 +663,7 @@ class PointsNes(Nes): def create_shapefile(self): """ - Create spatial geodataframe (shapefile). + Create spatial GeoDataFrame (shapefile). Returns ------- @@ -660,13 +673,13 @@ class PointsNes(Nes): if self.shapefile is None: - # Create dataframe cointaining all points + # Create dataframe containing all points gdf = self.get_centroids_from_coordinates() self.shapefile = gdf else: gdf = self.shapefile - + return gdf def get_centroids_from_coordinates(self): @@ -678,17 +691,17 @@ class PointsNes(Nes): centroids_gdf: GeoPandasDataFrame Centroids dataframe. """ - + # Get centroids from coordinates - centroids = gpd.points_from_xy(self.lon['data'], self.lat['data']) - - # Create dataframe cointaining all points - fids = np.arange(len(self._lon['data'])) - fids = fids[self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] - centroids_gdf = gpd.GeoDataFrame(index=pd.Index(name='FID', data=fids), - geometry=centroids, - crs="EPSG:4326") - + centroids = points_from_xy(self.lon["data"], self.lat["data"]) + + # Create dataframe containing all points + fids = arange(len(self.get_full_longitudes()["data"])) + fids = fids[self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] + centroids_gdf = GeoDataFrame(index=Index(name="FID", data=fids), + geometry=centroids, + crs="EPSG:4326") + return centroids_gdf def add_variables_to_shapefile(self, var_list, idx_lev=0, idx_time=0): @@ -704,24 +717,24 @@ class PointsNes(Nes): """ if idx_lev != 0: - msg = 'Error: Points dataset has no level (Level: {0}).'.format(idx_lev) + msg = "Error: Points dataset has no level (Level: {0}).".format(idx_lev) raise ValueError(msg) for var_name in var_list: # station as dimension - if len(self.variables[var_name]['dimensions']) == 1: - self.shapefile[var_name] = self.variables[var_name]['data'][:].ravel() + if len(self.variables[var_name]["dimensions"]) == 1: + self.shapefile[var_name] = self.variables[var_name]["data"][:].ravel() # station and time as dimensions else: - self.shapefile[var_name] = self.variables[var_name]['data'][idx_time, :].ravel() + self.shapefile[var_name] = self.variables[var_name]["data"][idx_time, :].ravel() return None @staticmethod def _get_axis_index_(axis): - if axis == 'T': + if axis == "T": value = 0 - elif axis == 'X': + elif axis == "X": value = 1 else: raise ValueError("Unknown axis: {0}".format(axis)) diff --git a/nes/nc_projections/points_nes_ghost.py b/nes/nc_projections/points_nes_ghost.py index f91d565be7ee565fb3cdf9a8a590a71fc0760189..25b7d3ba0ed656661a6620ae15c161d095dd0641 100644 --- a/nes/nc_projections/points_nes_ghost.py +++ b/nes/nc_projections/points_nes_ghost.py @@ -1,9 +1,9 @@ #!/usr/bin/env python import sys -import warnings -import numpy as np -from netCDF4 import stringtochar, date2num +from warnings import warn +from numpy import float64, empty, ndarray, generic, array, issubdtype, character, concatenate, int64 +from netCDF4 import date2num from copy import deepcopy from .points_nes import PointsNes @@ -14,18 +14,18 @@ class PointsNesGHOST(PointsNes): Attributes ---------- _qa : dict - Quality flags (GHOST checks) dictionary with the complete 'data' key for all the values and the rest of the + Quality flags (GHOST checks) dictionary with the complete "data" key for all the values and the rest of the attributes. _flag : dict - Data flags (given by data provider) dictionary with the complete 'data' key for all the values and the rest of + Data flags (given by data provider) dictionary with the complete "data" key for all the values and the rest of the attributes. _qa : dict - Quality flags (GHOST checks) dictionary with the portion of 'data' corresponding to the rank values. + Quality flags (GHOST checks) dictionary with the portion of "data" corresponding to the rank values. _flag : dict - Data flags (given by data provider) dictionary with the portion of 'data' corresponding to the rank values. + Data flags (given by data provider) dictionary with the portion of "data" corresponding to the rank values. """ - def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method='X', + def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method="X", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, balanced=False, times=None, **kwargs): """ @@ -42,8 +42,8 @@ class PointsNesGHOST(PointsNes): dataset: Dataset NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'X'. - Accepted values: ['X']. + Indicates the parallelization method that you want. Default: "X". + Accepted values: ["X"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -65,18 +65,18 @@ class PointsNesGHOST(PointsNes): parallel_method=parallel_method, avoid_first_hours=avoid_first_hours, avoid_last_hours=avoid_last_hours, first_level=first_level, last_level=last_level, create_nes=create_nes, - times=times, **kwargs) + times=times, balanced=balanced, **kwargs) # Complete dimensions - self._flag = self._get_coordinate_dimension(['flag']) - self._qa = self._get_coordinate_dimension(['qa']) + self._flag = self._get_coordinate_dimension(["flag"]) + self._qa = self._get_coordinate_dimension(["qa"]) # Dimensions screening - self.flag = self._get_coordinate_values(self._flag, 'X') - self.qa = self._get_coordinate_values(self._qa, 'X') + self.flag = self._get_coordinate_values(self._flag, "X") + self.qa = self._get_coordinate_values(self._qa, "X") @staticmethod - def new(comm=None, path=None, info=False, dataset=None, parallel_method='X', + def new(comm=None, path=None, info=False, dataset=None, parallel_method="X", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, balanced=False, times=None, **kwargs): """ @@ -93,8 +93,8 @@ class PointsNesGHOST(PointsNes): dataset: Dataset NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'X'. - Accepted values: ['X']. + Indicates the parallelization method that you want. Default: "X". + Accepted values: ["X"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -121,8 +121,8 @@ class PointsNesGHOST(PointsNes): def _create_dimensions(self, netcdf): """ - Create 'N_flag_codes' and 'N_qa_codes' dimensions and the super dimensions - 'time', 'time_nv', 'station', and 'strlen'. + Create "N_flag_codes" and "N_qa_codes" dimensions and the super dimensions + "time", "time_nv", "station", and "strlen". Parameters ---------- @@ -133,14 +133,15 @@ class PointsNesGHOST(PointsNes): super(PointsNesGHOST, self)._create_dimensions(netcdf) # Create N_flag_codes and N_qa_codes dimensions - netcdf.createDimension('N_flag_codes', self._flag['data'].shape[2]) - netcdf.createDimension('N_qa_codes', self._qa['data'].shape[2]) + netcdf.createDimension("N_flag_codes", self._flag["data"].shape[2]) + netcdf.createDimension("N_qa_codes", self._qa["data"].shape[2]) return None + # noinspection DuplicatedCode def _create_dimension_variables(self, netcdf): """ - Create the 'time', 'time_bnds', 'station', 'lat', 'lat_bnds', 'lon' and 'lon_bnds' variables. + Create the "time", "time_bnds", "station", "lat", "lat_bnds", "lon" and "lon_bnds" variables. Parameters ---------- @@ -149,76 +150,75 @@ class PointsNesGHOST(PointsNes): """ # TIMES - time_var = netcdf.createVariable('time', np.float64, ('time',), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - time_var.units = 'hours since {0}'.format( - self._time[self.get_time_id(self.hours_start, first=True)].strftime('%Y-%m-%d %H:%M:%S')) - time_var.standard_name = 'time' - time_var.calendar = 'standard' - time_var.long_name = 'time' - if self._time_bnds is not None: - time_var.bounds = 'time_bnds' + time_var = netcdf.createVariable("time", float64, ("time",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) + time_var.units = "hours since {0}".format( + self.get_full_times()[self._get_time_id(self.hours_start, first=True)].strftime("%Y-%m-%d %H:%M:%S")) + time_var.standard_name = "time" + time_var.calendar = "standard" + time_var.long_name = "time" + if self.time_bnds is not None: + time_var.bounds = "time_bnds" if self.size > 1: time_var.set_collective(True) - time_var[:] = date2num(self._time[self.get_time_id(self.hours_start, first=True): - self.get_time_id(self.hours_end, first=False)], + time_var[:] = date2num(self.get_full_times()[self._get_time_id(self.hours_start, first=True): + self._get_time_id(self.hours_end, first=False)], time_var.units, time_var.calendar) # TIME BOUNDS - if self._time_bnds is not None: - time_bnds_var = netcdf.createVariable('time_bnds', np.float64, ('time', 'time_nv',), zlib=self.zip_lvl, + if self.time_bnds is not None: + time_bnds_var = netcdf.createVariable("time_bnds", float64, ("time", "time_nv",), zlib=self.zip_lvl, complevel=self.zip_lvl) if self.size > 1: time_bnds_var.set_collective(True) - time_bnds_var[:] = date2num(self._time_bnds, time_var.units, calendar='standard') + time_bnds_var[:] = date2num(self.get_full_time_bnds(), time_var.units, calendar="standard") # STATIONS - stations = netcdf.createVariable('station', np.float64, ('station',), zlib=self.zip_lvl > 0, + stations = netcdf.createVariable("station", float64, ("station",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - stations.units = '' - stations.axis = 'X' - stations.long_name = '' - stations.standard_name = 'station' + stations.units = "" + stations.axis = "X" + stations.long_name = "" + stations.standard_name = "station" if self.size > 1: stations.set_collective(True) - stations[:] = self._station['data'] + stations[:] = self._station["data"] # LATITUDES - lat = netcdf.createVariable('latitude', np.float64, self._lat_dim, - zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - lat.units = 'degrees_north' - lat.axis = 'Y' - lat.long_name = 'latitude coordinate' - lat.standard_name = 'latitude' - if self._lat_bnds is not None: - lat.bounds = 'lat_bnds' + lat = netcdf.createVariable("latitude", float64, self._lat_dim, zlib=self.zip_lvl > 0, complevel=self.zip_lvl) + lat.units = "degrees_north" + lat.axis = "Y" + lat.long_name = "latitude coordinate" + lat.standard_name = "latitude" + if self.lat_bnds is not None: + lat.bounds = "lat_bnds" if self.size > 1: lat.set_collective(True) - lat[:] = self._lat['data'] + lat[:] = self.get_full_latitudes()["data"] # LONGITUDES - lon = netcdf.createVariable('longitude', np.float64, self._lon_dim, - zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - lon.units = 'degrees_east' - lon.axis = 'X' - lon.long_name = 'longitude coordinate' - lon.standard_name = 'longitude' - if self._lon_bnds is not None: - lon.bounds = 'lon_bnds' + lon = netcdf.createVariable("longitude", float64, self._lon_dim, zlib=self.zip_lvl > 0, complevel=self.zip_lvl) + lon.units = "degrees_east" + lon.axis = "X" + lon.long_name = "longitude coordinate" + lon.standard_name = "longitude" + if self.lon_bnds is not None: + lon.bounds = "lon_bnds" if self.size > 1: lon.set_collective(True) - lon[:] = self._lon['data'] + lon[:] = self.get_full_longitudes()["data"] def erase_flags(self): - first_time_idx = self.get_time_id(self.hours_start, first=True) - last_time_idx = self.get_time_id(self.hours_end, first=False) + first_time_idx = self._get_time_id(self.hours_start, first=True) + last_time_idx = self._get_time_id(self.hours_end, first=False) t_len = last_time_idx - first_time_idx - self._qa['data'] = np.empty((len(self._lon['data']), t_len, 0)) - self._flag['data'] = np.empty((len(self._lon['data']), t_len, 0)) + self._qa["data"] = empty((len(self.get_full_longitudes()["data"]), t_len, 0)) + self._flag["data"] = empty((len(self.get_full_longitudes()["data"]), t_len, 0)) return None + # noinspection DuplicatedCode def _get_coordinate_values(self, coordinate_info, coordinate_axis, bounds=False): """ Get the coordinate data of the current portion. @@ -226,9 +226,9 @@ class PointsNesGHOST(PointsNes): Parameters ---------- coordinate_info : dict, list - Dictionary with the 'data' key with the coordinate variable values. and the attributes as other keys. + Dictionary with the "data" key with the coordinate variable values. and the attributes as other keys. coordinate_axis : str - Name of the coordinate to extract. Accepted values: ['X']. + Name of the coordinate to extract. Accepted values: ["X"]. bounds : bool Boolean variable to know if there are coordinate bounds. Returns @@ -241,29 +241,30 @@ class PointsNesGHOST(PointsNes): return None if not isinstance(coordinate_info, dict): - values = {'data': deepcopy(coordinate_info)} + values = {"data": deepcopy(coordinate_info)} else: values = deepcopy(coordinate_info) - coordinate_len = len(values['data'].shape) + coordinate_len = len(values["data"].shape) if bounds: coordinate_len -= 1 - if coordinate_axis == 'X': + if coordinate_axis == "X": if coordinate_len == 1: - values['data'] = values['data'][self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + values["data"] = values["data"][self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] elif coordinate_len == 2: - values['data'] = values['data'][self.read_axis_limits['x_min']:self.read_axis_limits['x_max'], - self.read_axis_limits['t_min']:self.read_axis_limits['t_max']] + values["data"] = values["data"][self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"], + self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"]] elif coordinate_len == 3: - values['data'] = values['data'][self.read_axis_limits['x_min']:self.read_axis_limits['x_max'], - self.read_axis_limits['t_min']:self.read_axis_limits['t_max'], :] + values["data"] = values["data"][self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"], + self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"], :] else: raise NotImplementedError("The coordinate has wrong dimensions: {dim}".format( - dim=values['data'].shape)) + dim=values["data"].shape)) return values + # noinspection DuplicatedCode def _read_variable(self, var_name): """ Read the corresponding variable data according to the current rank. @@ -275,7 +276,7 @@ class PointsNesGHOST(PointsNes): Returns ------- - data: np.array + data: array Portion of the variable data corresponding to the rank. """ @@ -284,16 +285,16 @@ class PointsNesGHOST(PointsNes): # Read data in 1 or 2 dimensions if len(var_dims) < 2: - data = nc_var[self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + data = nc_var[self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] elif len(var_dims) == 2: - data = nc_var[self.read_axis_limits['x_min']:self.read_axis_limits['x_max'], - self.read_axis_limits['t_min']:self.read_axis_limits['t_max']] + data = nc_var[self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"], + self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"]] elif len(var_dims) == 3: - data = nc_var[self.read_axis_limits['x_min']:self.read_axis_limits['x_max'], - self.read_axis_limits['t_min']:self.read_axis_limits['t_max'], + data = nc_var[self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"], + self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"], :] else: - raise NotImplementedError('Error with {0}. Only can be read netCDF with 3 dimensions or less'.format( + raise NotImplementedError("Error with {0}. Only can be read netCDF with 3 dimensions or less".format( var_name)) # Unmask array @@ -301,6 +302,7 @@ class PointsNesGHOST(PointsNes): return data + # noinspection DuplicatedCode def _create_variables(self, netcdf, chunking=False): """ Create the netCDF file variables. @@ -316,49 +318,49 @@ class PointsNesGHOST(PointsNes): if self.variables is not None: for i, (var_name, var_dict) in enumerate(self.variables.items()): # Get data type - if 'dtype' in var_dict.keys(): - var_dtype = var_dict['dtype'] - if (var_dict['data'] is not None) and (var_dtype != var_dict['data'].dtype): + if "dtype" in var_dict.keys(): + var_dtype = var_dict["dtype"] + if (var_dict["data"] is not None) and (var_dtype != var_dict["data"].dtype): msg = "WARNING!!! " msg += "Different data types for variable {0}. ".format(var_name) - msg += "Input dtype={0}. Data dtype={1}.".format(var_dtype, var_dict['data'].dtype) - warnings.warn(msg) + msg += "Input dtype={0}. Data dtype={1}.".format(var_dtype, var_dict["data"].dtype) + warn(msg) sys.stderr.flush() try: - var_dict['data'] = var_dict['data'].astype(var_dtype) - except Exception as e: # TODO: Detect exception - raise e("It was not possible to cast the data to the input dtype.") + var_dict["data"] = var_dict["data"].astype(var_dtype) + except Exception: + raise TypeError("It was not possible to cast the data to the input dtype.") else: - var_dtype = var_dict['data'].dtype + var_dtype = var_dict["data"].dtype if var_dtype is object: raise TypeError("Data dtype is object. Define dtype explicitly as dictionary key 'dtype'") # Get dimensions when reading datasets - if 'dimensions' in var_dict.keys(): - var_dims = var_dict['dimensions'] + if "dimensions" in var_dict.keys(): + var_dims = var_dict["dimensions"] # Get dimensions when creating new datasets else: - if len(var_dict['data'].shape) == 1: + if len(var_dict["data"].shape) == 1: # For data that depends only on station (e.g. station_code) var_dims = self._var_dim else: # For data that is dependent on time and station (e.g. PM10) - var_dims = self._var_dim + ('time',) + var_dims = self._var_dim + ("time",) - if var_dict['data'] is not None: + if var_dict["data"] is not None: # Ensure data is of type numpy array (to create NES) - if not isinstance(var_dict['data'], (np.ndarray, np.generic)): + if not isinstance(var_dict["data"], (ndarray, generic)): try: - var_dict['data'] = np.array(var_dict['data']) + var_dict["data"] = array(var_dict["data"]) except AttributeError: raise AttributeError("Data for variable {0} must be a numpy array.".format(var_name)) # Convert list of strings to chars for parallelization - if np.issubdtype(var_dtype, np.character): - var_dict['data_aux'] = self.str2char(var_dict['data']) - var_dims += ('strlen',) - var_dtype = 'S1' + if issubdtype(var_dtype, character): + var_dict["data_aux"] = self._str2char(var_dict["data"]) + var_dims += ("strlen",) + var_dtype = "S1" if self.info: print("Rank {0:03d}: Writing {1} var ({2}/{3})".format(self.rank, var_name, i + 1, @@ -369,7 +371,7 @@ class PointsNesGHOST(PointsNes): zlib=self.zip_lvl > 0, complevel=self.zip_lvl) else: if self.master: - chunk_size = var_dict['data'].shape + chunk_size = var_dict["data"].shape else: chunk_size = None chunk_size = self.comm.bcast(chunk_size, root=0) @@ -386,63 +388,63 @@ class PointsNesGHOST(PointsNes): self.rank, var_name, i + 1, len(self.variables))) for att_name, att_value in var_dict.items(): - if att_name == 'data': + if att_name == "data": if self.info: print("Rank {0:03d}: Filling {1})".format(self.rank, var_name)) - if 'data_aux' in var_dict.keys(): - att_value = var_dict['data_aux'] + if "data_aux" in var_dict.keys(): + att_value = var_dict["data_aux"] if len(att_value.shape) == 1: try: - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max']] = att_value + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] = att_value except IndexError: raise IndexError("Different shapes. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max']].shape, + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]].shape, att_value.shape)) except ValueError: raise ValueError("Axis limits cannot be accessed. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max']].shape, + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]].shape, att_value.shape)) elif len(att_value.shape) == 2: - if 'strlen' in var_dims: + if "strlen" in var_dims: try: - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], :] = att_value + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], :] = att_value except IndexError: raise IndexError("Different shapes. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], :].shape, + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], :].shape, att_value.shape)) except ValueError: - raise ValueError("Axis limits cannot be accessed. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], :].shape, - att_value.shape)) + raise ValueError(f"Axis limits cannot be accessed. out_shape={ + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], :].shape}," + f" data_shp={att_value.shape}") else: try: - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - self.write_axis_limits['t_min']:self.write_axis_limits['t_max']] = att_value + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"]] = att_value except IndexError: raise IndexError("Different shapes. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - self.write_axis_limits['t_min']:self.write_axis_limits['t_max']].shape, + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"]].shape, att_value.shape)) except ValueError: - raise ValueError("Axis limits cannot be accessed. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - self.write_axis_limits['t_min']:self.write_axis_limits['t_max']].shape, - att_value.shape)) + raise ValueError(f"Axis limits cannot be accessed. out_shape={ + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"]].shape}, " + f"data_shp={att_value.shape}") elif len(att_value.shape) == 3: try: - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], :] = att_value except IndexError: raise IndexError("Different shapes. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], :].shape, att_value.shape)) except ValueError: raise ValueError("Axis limits cannot be accessed. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], :].shape, att_value.shape)) @@ -450,11 +452,11 @@ class PointsNesGHOST(PointsNes): print("Rank {0:03d}: Var {1} data ({2}/{3})".format(self.rank, var_name, i + 1, len(self.variables))) - elif att_name not in ['chunk_size', 'var_dims', 'dimensions', 'dtype', 'data_aux']: + elif att_name not in ["chunk_size", "var_dims", "dimensions", "dtype", "data_aux"]: var.setncattr(att_name, att_value) - if 'data_aux' in var_dict.keys(): - del var_dict['data_aux'] + if "data_aux" in var_dict.keys(): + del var_dict["data_aux"] self._set_var_crs(var) if self.info: @@ -462,7 +464,8 @@ class PointsNesGHOST(PointsNes): len(self.variables))) return None - + + # noinspection DuplicatedCode def _gather_data(self, data_to_gather): """ Gather all the variable data into the MPI rank 0 to perform a serial write. @@ -477,11 +480,11 @@ class PointsNesGHOST(PointsNes): for var_name, var_info in data_list.items(): try: # noinspection PyArgumentList - data_aux = self.comm.gather(data_list[var_name]['data'], root=0) + data_aux = self.comm.gather(data_list[var_name]["data"], root=0) if self.rank == 0: - shp_len = len(data_list[var_name]['data'].shape) + shp_len = len(data_list[var_name]["data"].shape) # concatenate over station - if self.parallel_method == 'X': + if self.parallel_method == "X": if shp_len == 1: # dimensions = (station) axis = 0 @@ -490,41 +493,38 @@ class PointsNesGHOST(PointsNes): # dimensions = (station, time) axis = 0 else: - msg = 'The points NetCDF must have ' - msg += 'surface values (without levels).' + msg = "The points NetCDF must have " + msg += "surface values (without levels)." raise NotImplementedError(msg) - elif self.parallel_method == 'T': + elif self.parallel_method == "T": # concatenate over time if shp_len == 1: # dimensions = (station) axis = None - continue elif shp_len == 2: - if 'strlen' in var_info['dimensions']: + if "strlen" in var_info["dimensions"]: # dimensions = (station, strlen) axis = None - continue else: # dimensions = (station, time) axis = 1 else: - msg = 'The points NetCDF must have ' - msg += 'surface values (without levels).' + msg = "The points NetCDF must have " + msg += "surface values (without levels)." raise NotImplementedError(msg) else: raise NotImplementedError( "Parallel method '{meth}' is not implemented. Use one of these: {accept}".format( - meth=self.parallel_method, accept=['X', 'T'])) - data_list[var_name]['data'] = np.concatenate(data_aux, axis=axis) + meth=self.parallel_method, accept=["X", "T"])) + data_list[var_name]["data"] = concatenate(data_aux, axis=axis) except Exception as e: - print("**ERROR** an error has occurred while gathering the '{0}' variable.\n".format(var_name)) - sys.stderr.write("**ERROR** an error has occurred while gathering the '{0}' variable.\n".format(var_name)) + msg = f"**ERROR** an error has occurred while gathering the '{var_name}' variable.\n" + print(msg) + sys.stderr.write(msg) print(e) sys.stderr.write(str(e)) - # print(e, file=sys.stderr) sys.stderr.flush() self.comm.Abort(1) - raise e return data_list @@ -539,35 +539,38 @@ class PointsNesGHOST(PointsNes): """ # N FLAG CODES - flag = netcdf.createVariable('flag', np.int64, ('station', 'time', 'N_flag_codes',), + flag = netcdf.createVariable("flag", int64, ("station", "time", "N_flag_codes",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - flag.units = '' - flag.axis = '' - flag.long_name = '' - flag.standard_name = 'flag' + flag.units = "" + flag.axis = "" + flag.long_name = "" + flag.standard_name = "flag" if self.size > 1: flag.set_collective(True) - flag[:] = self._flag['data'] + flag[:] = self._flag["data"] # N QA CODES - qa = netcdf.createVariable('qa', np.int64, ('station', 'time', 'N_qa_codes',), + qa = netcdf.createVariable("qa", int64, ("station", "time", "N_qa_codes",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - qa.units = '' - qa.axis = '' - qa.long_name = '' - qa.standard_name = 'N_qa_codes' + qa.units = "" + qa.axis = "" + qa.long_name = "" + qa.standard_name = "N_qa_codes" if self.size > 1: qa.set_collective(True) - qa[:] = self._qa['data'] + qa[:] = self._qa["data"] return None - def to_netcdf(self, path, compression_level=0, serial=False, info=False, chunking=False): + def to_netcdf(self, path, compression_level=0, serial=False, info=False, chunking=False, nc_type="NES", + keep_open=False): """ Write the netCDF output file. Parameters ---------- + keep_open : bool + nc_type : str path : str Path to the output netCDF file. compression_level : int @@ -577,14 +580,14 @@ class PointsNesGHOST(PointsNes): info : bool Indicates if you want to print the information of each writing step by stdout Default: False. chunking : bool - Indicates if you want a chunked netCDF output. Only available with non serial writes. Default: False. + Indicates if you want a chunked netCDF output. Only available with non-serial writes. Default: False. """ if (not serial) and (self.size > 1): - msg = 'WARNING!!! ' - msg += 'GHOST datasets cannot be written in parallel yet. ' - msg += 'Changing to serial mode.' - warnings.warn(msg) + msg = "WARNING!!! " + msg += "GHOST datasets cannot be written in parallel yet. " + msg += "Changing to serial mode." + warn(msg) sys.stderr.flush() super(PointsNesGHOST, self).to_netcdf(path, compression_level=compression_level, @@ -611,157 +614,161 @@ class PointsNesGHOST(PointsNes): first_level=self.first_level, last_level=self.last_level, create_nes=True, - lat=self.lat['data'], - lon=self.lon['data'], + lat=self.lat["data"], + lon=self.lon["data"], times=self.time ) # The version attribute in GHOST files prior to 1.3.3 is called data_version, after it is version - if 'version' in self.global_attrs: - GHOST_version = self.global_attrs['version'] - elif 'data_version' in self.global_attrs: - GHOST_version = self.global_attrs['data_version'] - metadata_variables = self.get_standard_metadata(GHOST_version) + if "version" in self.global_attrs: + ghost_version = self.global_attrs["version"] + elif "data_version" in self.global_attrs: + ghost_version = self.global_attrs["data_version"] + else: + ghost_version = "0.0.0" + metadata_variables = self.get_standard_metadata(ghost_version) self.free_vars(metadata_variables) - self.free_vars('station') + self.free_vars("station") points_nes.variables = deepcopy(self.variables) return points_nes - - def get_standard_metadata(self, GHOST_version): + + @staticmethod + def get_standard_metadata(ghost_version): """ Get all possible GHOST variables for each version. Parameters ---------- - GHOST_version : str + ghost_version : str Version of GHOST file. Returns ---------- metadata_variables[GHOST_version] : list - List of metadata variables for a certain GHOST version + A List of metadata variables for a certain GHOST version """ # This metadata variables are - metadata_variables = {'1.4': ['GHOST_version', 'station_reference', 'station_timezone', 'latitude', 'longitude', - 'altitude', 'sampling_height', 'measurement_altitude', 'ellipsoid', - 'horizontal_datum', 'vertical_datum', 'projection', 'distance_to_building', - 'distance_to_kerb', 'distance_to_junction', 'distance_to_source', 'street_width', - 'street_type', 'daytime_traffic_speed', 'daily_passing_vehicles', 'data_level', - 'climatology', 'station_name', 'city', 'country', - 'administrative_country_division_1', 'administrative_country_division_2', - 'population', 'representative_radius', 'network', 'associated_networks', - 'area_classification', 'station_classification', 'main_emission_source', - 'land_use', 'terrain', 'measurement_scale', - 'ESDAC_Iwahashi_landform_classification', - 'ESDAC_modal_Iwahashi_landform_classification_5km', - 'ESDAC_modal_Iwahashi_landform_classification_25km', - 'ESDAC_Meybeck_landform_classification', - 'ESDAC_modal_Meybeck_landform_classification_5km', - 'ESDAC_modal_Meybeck_landform_classification_25km', - 'GHSL_settlement_model_classification', - 'GHSL_modal_settlement_model_classification_5km', - 'GHSL_modal_settlement_model_classification_25km', - 'Joly-Peuch_classification_code', 'Koppen-Geiger_classification', - 'Koppen-Geiger_modal_classification_5km', - 'Koppen-Geiger_modal_classification_25km', - 'MODIS_MCD12C1_v6_IGBP_land_use', 'MODIS_MCD12C1_v6_modal_IGBP_land_use_5km', - 'MODIS_MCD12C1_v6_modal_IGBP_land_use_25km', 'MODIS_MCD12C1_v6_UMD_land_use', - 'MODIS_MCD12C1_v6_modal_UMD_land_use_5km', - 'MODIS_MCD12C1_v6_modal_UMD_land_use_25km', 'MODIS_MCD12C1_v6_LAI', - 'MODIS_MCD12C1_v6_modal_LAI_5km', 'MODIS_MCD12C1_v6_modal_LAI_25km', - 'WMO_region', 'WWF_TEOW_terrestrial_ecoregion', 'WWF_TEOW_biogeographical_realm', - 'WWF_TEOW_biome', 'UMBC_anthrome_classification', - 'UMBC_modal_anthrome_classification_5km', - 'UMBC_modal_anthrome_classification_25km', - 'EDGAR_v4.3.2_annual_average_BC_emissions', - 'EDGAR_v4.3.2_annual_average_CO_emissions', - 'EDGAR_v4.3.2_annual_average_NH3_emissions', - 'EDGAR_v4.3.2_annual_average_NMVOC_emissions', - 'EDGAR_v4.3.2_annual_average_NOx_emissions', - 'EDGAR_v4.3.2_annual_average_OC_emissions', - 'EDGAR_v4.3.2_annual_average_PM10_emissions', - 'EDGAR_v4.3.2_annual_average_biogenic_PM2.5_emissions', - 'EDGAR_v4.3.2_annual_average_fossilfuel_PM2.5_emissions', - 'EDGAR_v4.3.2_annual_average_SO2_emissions', 'ASTER_v3_altitude', - 'ETOPO1_altitude', 'ETOPO1_max_altitude_difference_5km', - 'GHSL_built_up_area_density', 'GHSL_average_built_up_area_density_5km', - 'GHSL_average_built_up_area_density_25km', 'GHSL_max_built_up_area_density_5km', - 'GHSL_max_built_up_area_density_25km', 'GHSL_population_density', - 'GHSL_average_population_density_5km', 'GHSL_average_population_density_25km', - 'GHSL_max_population_density_5km', 'GHSL_max_population_density_25km', - 'GPW_population_density', 'GPW_average_population_density_5km', - 'GPW_average_population_density_25km', 'GPW_max_population_density_5km', - 'GPW_max_population_density_25km', - 'NOAA-DMSP-OLS_v4_nighttime_stable_lights', - 'NOAA-DMSP-OLS_v4_average_nighttime_stable_lights_5km', - 'NOAA-DMSP-OLS_v4_average_nighttime_stable_lights_25km', - 'NOAA-DMSP-OLS_v4_max_nighttime_stable_lights_5km', - 'NOAA-DMSP-OLS_v4_max_nighttime_stable_lights_25km', - 'OMI_level3_column_annual_average_NO2', - 'OMI_level3_column_cloud_screened_annual_average_NO2', - 'OMI_level3_tropospheric_column_annual_average_NO2', - 'OMI_level3_tropospheric_column_cloud_screened_annual_average_NO2', - 'GSFC_coastline_proximity', 'primary_sampling_type', - 'primary_sampling_instrument_name', - 'primary_sampling_instrument_documented_flow_rate', - 'primary_sampling_instrument_reported_flow_rate', - 'primary_sampling_process_details', 'primary_sampling_instrument_manual_name', - 'primary_sampling_further_details', 'sample_preparation_types', - 'sample_preparation_techniques', 'sample_preparation_process_details', - 'sample_preparation_further_details', 'measurement_methodology', - 'measuring_instrument_name', 'measuring_instrument_sampling_type', - 'measuring_instrument_documented_flow_rate', - 'measuring_instrument_reported_flow_rate', 'measuring_instrument_process_details', - 'measuring_instrument_process_details', 'measuring_instrument_manual_name', - 'measuring_instrument_further_details', 'measuring_instrument_reported_units', - 'measuring_instrument_reported_lower_limit_of_detection', - 'measuring_instrument_documented_lower_limit_of_detection', - 'measuring_instrument_reported_upper_limit_of_detection', - 'measuring_instrument_documented_upper_limit_of_detection', - 'measuring_instrument_reported_uncertainty', - 'measuring_instrument_documented_uncertainty', - 'measuring_instrument_reported_accuracy', - 'measuring_instrument_documented_accuracy', - 'measuring_instrument_reported_precision', - 'measuring_instrument_documented_precision', - 'measuring_instrument_reported_zero_drift', - 'measuring_instrument_documented_zero_drift', - 'measuring_instrument_reported_span_drift', - 'measuring_instrument_documented_span_drift', - 'measuring_instrument_reported_zonal_drift', - 'measuring_instrument_documented_zonal_drift', - 'measuring_instrument_reported_measurement_resolution', - 'measuring_instrument_documented_measurement_resolution', - 'measuring_instrument_reported_absorption_cross_section', - 'measuring_instrument_documented_absorption_cross_section', - 'measuring_instrument_inlet_information', - 'measuring_instrument_calibration_scale', - 'network_provided_volume_standard_temperature', - 'network_provided_volume_standard_pressure', 'retrieval_algorithm', - 'principal_investigator_name', 'principal_investigator_institution', - 'principal_investigator_email_address', 'contact_name', - 'contact_institution', 'contact_email_address', 'meta_update_stamp', - 'data_download_stamp', 'data_revision_stamp', 'network_sampling_details', - 'network_uncertainty_details', 'network_maintenance_details', - 'network_qa_details', 'network_miscellaneous_details', 'data_licence', - 'process_warnings', 'temporal_resolution', - 'reported_lower_limit_of_detection_per_measurement', - 'reported_upper_limit_of_detection_per_measurement', - 'reported_uncertainty_per_measurement', 'derived_uncertainty_per_measurement', - 'day_night_code', 'weekday_weekend_code', 'season_code', - 'hourly_native_representativity_percent', 'hourly_native_max_gap_percent', - 'daily_native_representativity_percent', 'daily_representativity_percent', - 'daily_native_max_gap_percent', 'daily_max_gap_percent', - 'monthly_native_representativity_percent', 'monthly_representativity_percent', - 'monthly_native_max_gap_percent', 'monthly_max_gap_percent', - 'annual_native_representativity_percent', 'annual_native_max_gap_percent', - 'all_representativity_percent', 'all_max_gap_percent'], - } - - return metadata_variables[GHOST_version] - + metadata_variables = {"1.4": ["GHOST_version", "station_reference", "station_timezone", "latitude", "longitude", + "altitude", "sampling_height", "measurement_altitude", "ellipsoid", + "horizontal_datum", "vertical_datum", "projection", "distance_to_building", + "distance_to_kerb", "distance_to_junction", "distance_to_source", "street_width", + "street_type", "daytime_traffic_speed", "daily_passing_vehicles", "data_level", + "climatology", "station_name", "city", "country", + "administrative_country_division_1", "administrative_country_division_2", + "population", "representative_radius", "network", "associated_networks", + "area_classification", "station_classification", "main_emission_source", + "land_use", "terrain", "measurement_scale", + "ESDAC_Iwahashi_landform_classification", + "ESDAC_modal_Iwahashi_landform_classification_5km", + "ESDAC_modal_Iwahashi_landform_classification_25km", + "ESDAC_Meybeck_landform_classification", + "ESDAC_modal_Meybeck_landform_classification_5km", + "ESDAC_modal_Meybeck_landform_classification_25km", + "GHSL_settlement_model_classification", + "GHSL_modal_settlement_model_classification_5km", + "GHSL_modal_settlement_model_classification_25km", + "Joly-Peuch_classification_code", "Koppen-Geiger_classification", + "Koppen-Geiger_modal_classification_5km", + "Koppen-Geiger_modal_classification_25km", + "MODIS_MCD12C1_v6_IGBP_land_use", "MODIS_MCD12C1_v6_modal_IGBP_land_use_5km", + "MODIS_MCD12C1_v6_modal_IGBP_land_use_25km", "MODIS_MCD12C1_v6_UMD_land_use", + "MODIS_MCD12C1_v6_modal_UMD_land_use_5km", + "MODIS_MCD12C1_v6_modal_UMD_land_use_25km", "MODIS_MCD12C1_v6_LAI", + "MODIS_MCD12C1_v6_modal_LAI_5km", "MODIS_MCD12C1_v6_modal_LAI_25km", + "WMO_region", "WWF_TEOW_terrestrial_ecoregion", "WWF_TEOW_biogeographical_realm", + "WWF_TEOW_biome", "UMBC_anthrome_classification", + "UMBC_modal_anthrome_classification_5km", + "UMBC_modal_anthrome_classification_25km", + "EDGAR_v4.3.2_annual_average_BC_emissions", + "EDGAR_v4.3.2_annual_average_CO_emissions", + "EDGAR_v4.3.2_annual_average_NH3_emissions", + "EDGAR_v4.3.2_annual_average_NMVOC_emissions", + "EDGAR_v4.3.2_annual_average_NOx_emissions", + "EDGAR_v4.3.2_annual_average_OC_emissions", + "EDGAR_v4.3.2_annual_average_PM10_emissions", + "EDGAR_v4.3.2_annual_average_biogenic_PM2.5_emissions", + "EDGAR_v4.3.2_annual_average_fossilfuel_PM2.5_emissions", + "EDGAR_v4.3.2_annual_average_SO2_emissions", "ASTER_v3_altitude", + "ETOPO1_altitude", "ETOPO1_max_altitude_difference_5km", + "GHSL_built_up_area_density", "GHSL_average_built_up_area_density_5km", + "GHSL_average_built_up_area_density_25km", "GHSL_max_built_up_area_density_5km", + "GHSL_max_built_up_area_density_25km", "GHSL_population_density", + "GHSL_average_population_density_5km", "GHSL_average_population_density_25km", + "GHSL_max_population_density_5km", "GHSL_max_population_density_25km", + "GPW_population_density", "GPW_average_population_density_5km", + "GPW_average_population_density_25km", "GPW_max_population_density_5km", + "GPW_max_population_density_25km", + "NOAA-DMSP-OLS_v4_nighttime_stable_lights", + "NOAA-DMSP-OLS_v4_average_nighttime_stable_lights_5km", + "NOAA-DMSP-OLS_v4_average_nighttime_stable_lights_25km", + "NOAA-DMSP-OLS_v4_max_nighttime_stable_lights_5km", + "NOAA-DMSP-OLS_v4_max_nighttime_stable_lights_25km", + "OMI_level3_column_annual_average_NO2", + "OMI_level3_column_cloud_screened_annual_average_NO2", + "OMI_level3_tropospheric_column_annual_average_NO2", + "OMI_level3_tropospheric_column_cloud_screened_annual_average_NO2", + "GSFC_coastline_proximity", "primary_sampling_type", + "primary_sampling_instrument_name", + "primary_sampling_instrument_documented_flow_rate", + "primary_sampling_instrument_reported_flow_rate", + "primary_sampling_process_details", "primary_sampling_instrument_manual_name", + "primary_sampling_further_details", "sample_preparation_types", + "sample_preparation_techniques", "sample_preparation_process_details", + "sample_preparation_further_details", "measurement_methodology", + "measuring_instrument_name", "measuring_instrument_sampling_type", + "measuring_instrument_documented_flow_rate", + "measuring_instrument_reported_flow_rate", "measuring_instrument_process_details", + "measuring_instrument_process_details", "measuring_instrument_manual_name", + "measuring_instrument_further_details", "measuring_instrument_reported_units", + "measuring_instrument_reported_lower_limit_of_detection", + "measuring_instrument_documented_lower_limit_of_detection", + "measuring_instrument_reported_upper_limit_of_detection", + "measuring_instrument_documented_upper_limit_of_detection", + "measuring_instrument_reported_uncertainty", + "measuring_instrument_documented_uncertainty", + "measuring_instrument_reported_accuracy", + "measuring_instrument_documented_accuracy", + "measuring_instrument_reported_precision", + "measuring_instrument_documented_precision", + "measuring_instrument_reported_zero_drift", + "measuring_instrument_documented_zero_drift", + "measuring_instrument_reported_span_drift", + "measuring_instrument_documented_span_drift", + "measuring_instrument_reported_zonal_drift", + "measuring_instrument_documented_zonal_drift", + "measuring_instrument_reported_measurement_resolution", + "measuring_instrument_documented_measurement_resolution", + "measuring_instrument_reported_absorption_cross_section", + "measuring_instrument_documented_absorption_cross_section", + "measuring_instrument_inlet_information", + "measuring_instrument_calibration_scale", + "network_provided_volume_standard_temperature", + "network_provided_volume_standard_pressure", "retrieval_algorithm", + "principal_investigator_name", "principal_investigator_institution", + "principal_investigator_email_address", "contact_name", + "contact_institution", "contact_email_address", "meta_update_stamp", + "data_download_stamp", "data_revision_stamp", "network_sampling_details", + "network_uncertainty_details", "network_maintenance_details", + "network_qa_details", "network_miscellaneous_details", "data_licence", + "process_warnings", "temporal_resolution", + "reported_lower_limit_of_detection_per_measurement", + "reported_upper_limit_of_detection_per_measurement", + "reported_uncertainty_per_measurement", "derived_uncertainty_per_measurement", + "day_night_code", "weekday_weekend_code", "season_code", + "hourly_native_representativity_percent", "hourly_native_max_gap_percent", + "daily_native_representativity_percent", "daily_representativity_percent", + "daily_native_max_gap_percent", "daily_max_gap_percent", + "monthly_native_representativity_percent", "monthly_representativity_percent", + "monthly_native_max_gap_percent", "monthly_max_gap_percent", + "annual_native_representativity_percent", "annual_native_max_gap_percent", + "all_representativity_percent", "all_max_gap_percent"], + } + + return metadata_variables[ghost_version] + + # noinspection DuplicatedCode def add_variables_to_shapefile(self, var_list, idx_lev=0, idx_time=0): """ Add variables data to shapefile. @@ -775,24 +782,24 @@ class PointsNesGHOST(PointsNes): """ if idx_lev != 0: - msg = 'Error: Points dataset has no level (Level: {0}).'.format(idx_lev) + msg = "Error: Points dataset has no level (Level: {0}).".format(idx_lev) raise ValueError(msg) for var_name in var_list: # station as dimension - if len(self.variables[var_name]['dimensions']) == 1: - self.shapefile[var_name] = self.variables[var_name]['data'][:].ravel() + if len(self.variables[var_name]["dimensions"]) == 1: + self.shapefile[var_name] = self.variables[var_name]["data"][:].ravel() # station and time as dimensions else: - self.shapefile[var_name] = self.variables[var_name]['data'][:, idx_time].ravel() + self.shapefile[var_name] = self.variables[var_name]["data"][:, idx_time].ravel() return None @staticmethod def _get_axis_index_(axis): - if axis == 'T': + if axis == "T": value = 1 - elif axis == 'X': + elif axis == "X": value = 0 else: raise ValueError("Unknown axis: {0}".format(axis)) diff --git a/nes/nc_projections/points_nes_providentia.py b/nes/nc_projections/points_nes_providentia.py index e35ff3d2789dc3a873bcba595ccd64884fb312c9..6a014e660f89befd87b6055aa29f0cb69707debc 100644 --- a/nes/nc_projections/points_nes_providentia.py +++ b/nes/nc_projections/points_nes_providentia.py @@ -1,10 +1,9 @@ #!/usr/bin/env python import sys -import warnings -import numpy as np +from warnings import warn from copy import deepcopy -from netCDF4 import stringtochar +from numpy import ndarray, generic, array, issubdtype, character, concatenate from .points_nes import PointsNes @@ -14,29 +13,30 @@ class PointsNesProvidentia(PointsNes): Attributes ---------- _model_centre_lon : dict - Model centre longitudes dictionary with the complete 'data' key for all the values and the rest of the + Model centre longitudes dictionary with the complete "data" key for all the values and the rest of the attributes. _model_centre_lat : dict - Model centre latitudes dictionary with the complete 'data' key for all the values and the rest of the + Model centre latitudes dictionary with the complete "data" key for all the values and the rest of the attributes. _grid_edge_lon : dict - Grid edge longitudes dictionary with the complete 'data' key for all the values and the rest of the + Grid edge longitudes dictionary with the complete "data" key for all the values and the rest of the attributes. _grid_edge_lat : dict - Grid edge latitudes dictionary with the complete 'data' key for all the values and the rest of the + Grid edge latitudes dictionary with the complete "data" key for all the values and the rest of the attributes. model_centre_lon : dict - Model centre longitudes dictionary with the portion of 'data' corresponding to the rank values. + Model centre longitudes dictionary with the portion of "data" corresponding to the rank values. model_centre_lat : dict - Model centre latitudes dictionary with the portion of 'data' corresponding to the rank values. + Model centre latitudes dictionary with the portion of "data" corresponding to the rank values. grid_edge_lon : dict - Grid edge longitudes dictionary with the portion of 'data' corresponding to the rank values. + Grid edge longitudes dictionary with the portion of "data" corresponding to the rank values. grid_edge_lat : dict - Grid edge latitudes dictionary with the portion of 'data' corresponding to the rank values. + Grid edge latitudes dictionary with the portion of "data" corresponding to the rank values. """ - def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method='X', + def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method="X", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, - balanced=False, times=None, model_centre_lon=None, model_centre_lat=None, grid_edge_lon=None, grid_edge_lat=None, + balanced=False, times=None, model_centre_lon=None, model_centre_lat=None, grid_edge_lon=None, + grid_edge_lat=None, **kwargs): """ Initialize the PointsNesProvidentia class @@ -52,8 +52,8 @@ class PointsNesProvidentia(PointsNes): dataset: Dataset NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'X'. - Accepted values: ['X']. + Indicates the parallelization method that you want. Default: "X". + Accepted values: ["X"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -70,13 +70,13 @@ class PointsNesProvidentia(PointsNes): times : list, None List of times to substitute the current ones while creation. model_centre_lon : dict - Model centre longitudes dictionary with the portion of 'data' corresponding to the rank values. + Model centre longitudes dictionary with the portion of "data" corresponding to the rank values. model_centre_lat : dict - Model centre latitudes dictionary with the portion of 'data' corresponding to the rank values. + Model centre latitudes dictionary with the portion of "data" corresponding to the rank values. grid_edge_lon : dict - Grid edge longitudes dictionary with the portion of 'data' corresponding to the rank values. + Grid edge longitudes dictionary with the portion of "data" corresponding to the rank values. grid_edge_lat : dict - Grid edge latitudes dictionary with the portion of 'data' corresponding to the rank values. + Grid edge latitudes dictionary with the portion of "data" corresponding to the rank values. """ super(PointsNesProvidentia, self).__init__(comm=comm, path=path, info=info, dataset=dataset, @@ -84,7 +84,7 @@ class PointsNesProvidentia(PointsNes): avoid_first_hours=avoid_first_hours, avoid_last_hours=avoid_last_hours, first_level=first_level, last_level=last_level, - create_nes=create_nes, times=times, **kwargs) + create_nes=create_nes, times=times, balanced=balanced, **kwargs) if create_nes: # Complete dimensions @@ -94,19 +94,19 @@ class PointsNesProvidentia(PointsNes): self._grid_edge_lat = grid_edge_lat else: # Complete dimensions - self._model_centre_lon = self._get_coordinate_dimension(['model_centre_longitude']) - self._model_centre_lat = self._get_coordinate_dimension(['model_centre_latitude']) - self._grid_edge_lon = self._get_coordinate_dimension(['grid_edge_longitude']) - self._grid_edge_lat = self._get_coordinate_dimension(['grid_edge_latitude']) + self._model_centre_lon = self._get_coordinate_dimension(["model_centre_longitude"]) + self._model_centre_lat = self._get_coordinate_dimension(["model_centre_latitude"]) + self._grid_edge_lon = self._get_coordinate_dimension(["grid_edge_longitude"]) + self._grid_edge_lat = self._get_coordinate_dimension(["grid_edge_latitude"]) # Dimensions screening - self.model_centre_lon = self._get_coordinate_values(self._model_centre_lon, '') - self.model_centre_lat = self._get_coordinate_values(self._model_centre_lat, '') - self.grid_edge_lon = self._get_coordinate_values(self._grid_edge_lon, '') - self.grid_edge_lat = self._get_coordinate_values(self._grid_edge_lat, '') + self.model_centre_lon = self._get_coordinate_values(self._model_centre_lon, "") + self.model_centre_lat = self._get_coordinate_values(self._model_centre_lat, "") + self.grid_edge_lon = self._get_coordinate_values(self._grid_edge_lon, "") + self.grid_edge_lat = self._get_coordinate_values(self._grid_edge_lat, "") @staticmethod - def new(comm=None, path=None, info=False, dataset=None, parallel_method='X', + def new(comm=None, path=None, info=False, dataset=None, parallel_method="X", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, balanced=False, times=None, model_centre_lon=None, model_centre_lat=None, grid_edge_lon=None, grid_edge_lat=None, @@ -125,8 +125,8 @@ class PointsNesProvidentia(PointsNes): dataset: Dataset NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'X'. - Accepted values: ['X']. + Indicates the parallelization method that you want. Default: "X". + Accepted values: ["X"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -143,13 +143,13 @@ class PointsNesProvidentia(PointsNes): create_nes : bool Indicates if you want to create the object from scratch (True) or through an existing file. model_centre_lon : dict - Model centre longitudes dictionary with the portion of 'data' corresponding to the rank values. + Model centre longitudes dictionary with the portion of "data" corresponding to the rank values. model_centre_lat : dict - Model centre latitudes dictionary with the portion of 'data' corresponding to the rank values. + Model centre latitudes dictionary with the portion of "data" corresponding to the rank values. grid_edge_lon : dict - Grid edge longitudes dictionary with the portion of 'data' corresponding to the rank values. + Grid edge longitudes dictionary with the portion of "data" corresponding to the rank values. grid_edge_lat : dict - Grid edge latitudes dictionary with the portion of 'data' corresponding to the rank values. + Grid edge latitudes dictionary with the portion of "data" corresponding to the rank values. """ new = PointsNesProvidentia(comm=comm, path=path, info=info, dataset=dataset, @@ -163,8 +163,8 @@ class PointsNesProvidentia(PointsNes): def _create_dimensions(self, netcdf): """ - Create 'grid_edge', 'model_latitude' and 'model_longitude' dimensions and the super dimensions - 'time', 'time_nv', 'station', and 'strlen'. + Create "grid_edge", "model_latitude" and "model_longitude" dimensions and the super dimensions + "time", "time_nv", "station", and "strlen". Parameters ---------- @@ -175,15 +175,15 @@ class PointsNesProvidentia(PointsNes): super(PointsNesProvidentia, self)._create_dimensions(netcdf) # Create grid_edge, model_latitude and model_longitude dimensions - netcdf.createDimension('grid_edge', len(self._grid_edge_lon['data'])) - netcdf.createDimension('model_latitude', self._model_centre_lon['data'].shape[0]) - netcdf.createDimension('model_longitude', self._model_centre_lon['data'].shape[1]) + netcdf.createDimension("grid_edge", len(self._grid_edge_lon["data"])) + netcdf.createDimension("model_latitude", self._model_centre_lon["data"].shape[0]) + netcdf.createDimension("model_longitude", self._model_centre_lon["data"].shape[1]) return None def _create_dimension_variables(self, netcdf): """ - Create the 'model_centre_lon', model_centre_lat', 'grid_edge_lon' and 'grid_edge_lat' variables. + Create the "model_centre_lon", model_centre_lat", "grid_edge_lon" and "grid_edge_lat" variables. Parameters ---------- @@ -194,64 +194,65 @@ class PointsNesProvidentia(PointsNes): super(PointsNesProvidentia, self)._create_dimension_variables(netcdf) # MODEL CENTRE LONGITUDES - model_centre_lon = netcdf.createVariable('model_centre_longitude', 'f8', - ('model_latitude', 'model_longitude',), + model_centre_lon = netcdf.createVariable("model_centre_longitude", "f8", + ("model_latitude", "model_longitude",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - model_centre_lon.units = 'degrees_east' - model_centre_lon.axis = 'X' - model_centre_lon.long_name = 'model centre longitude' - model_centre_lon.standard_name = 'model centre longitude' + model_centre_lon.units = "degrees_east" + model_centre_lon.axis = "X" + model_centre_lon.long_name = "model centre longitude" + model_centre_lon.standard_name = "model centre longitude" if self.size > 1: model_centre_lon.set_collective(True) - msg = '2D meshed grid centre longitudes with ' - msg += '{} longitudes in {} bands of latitude'.format(self._model_centre_lon['data'].shape[1], - self._model_centre_lat['data'].shape[0]) + msg = "2D meshed grid centre longitudes with " + msg += "{} longitudes in {} bands of latitude".format(self._model_centre_lon["data"].shape[1], + self._model_centre_lat["data"].shape[0]) model_centre_lon.description = msg - model_centre_lon[:] = self._model_centre_lon['data'] + model_centre_lon[:] = self._model_centre_lon["data"] # MODEL CENTRE LATITUDES - model_centre_lat = netcdf.createVariable('model_centre_latitude', 'f8', - ('model_latitude','model_longitude',), + model_centre_lat = netcdf.createVariable("model_centre_latitude", "f8", + ("model_latitude", "model_longitude",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - model_centre_lat.units = 'degrees_north' - model_centre_lat.axis = 'Y' - model_centre_lat.long_name = 'model centre latitude' - model_centre_lat.standard_name = 'model centre latitude' + model_centre_lat.units = "degrees_north" + model_centre_lat.axis = "Y" + model_centre_lat.long_name = "model centre latitude" + model_centre_lat.standard_name = "model centre latitude" if self.size > 1: model_centre_lat.set_collective(True) - msg = '2D meshed grid centre longitudes with ' - msg += '{} longitudes in {} bands of latitude'.format(self._model_centre_lon['data'].shape[1], - self._model_centre_lat['data'].shape[0]) - model_centre_lat[:] = self._model_centre_lat['data'] + msg = "2D meshed grid centre longitudes with " + msg += "{} longitudes in {} bands of latitude".format(self._model_centre_lon["data"].shape[1], + self._model_centre_lat["data"].shape[0]) + model_centre_lat[:] = self._model_centre_lat["data"] # GRID EDGE DOMAIN LONGITUDES - grid_edge_lon = netcdf.createVariable('grid_edge_longitude', 'f8', ('grid_edge')) - grid_edge_lon.units = 'degrees_east' - grid_edge_lon.axis = 'X' - grid_edge_lon.long_name = 'grid edge longitude' - grid_edge_lon.standard_name = 'grid edge longitude' + grid_edge_lon = netcdf.createVariable("grid_edge_longitude", "f8", "grid_edge") + grid_edge_lon.units = "degrees_east" + grid_edge_lon.axis = "X" + grid_edge_lon.long_name = "grid edge longitude" + grid_edge_lon.standard_name = "grid edge longitude" if self.size > 1: grid_edge_lon.set_collective(True) - msg = 'Longitude coordinate along edge of grid domain ' - msg += '(going clockwise around grid boundary from bottom-left corner).' + msg = "Longitude coordinate along edge of grid domain " + msg += "(going clockwise around grid boundary from bottom-left corner)." grid_edge_lon.description = msg - grid_edge_lon[:] = self._grid_edge_lon['data'] + grid_edge_lon[:] = self._grid_edge_lon["data"] # GRID EDGE DOMAIN LATITUDES - grid_edge_lat = netcdf.createVariable('grid_edge_latitude', 'f8', ('grid_edge')) - grid_edge_lat.units = 'degrees_north' - grid_edge_lat.axis = 'Y' - grid_edge_lat.long_name = 'grid edge latitude' - grid_edge_lat.standard_name = 'grid edge latitude' + grid_edge_lat = netcdf.createVariable("grid_edge_latitude", "f8", "grid_edge") + grid_edge_lat.units = "degrees_north" + grid_edge_lat.axis = "Y" + grid_edge_lat.long_name = "grid edge latitude" + grid_edge_lat.standard_name = "grid edge latitude" if self.size > 1: grid_edge_lat.set_collective(True) - msg = 'Latitude coordinate along edge of grid domain ' - msg += '(going clockwise around grid boundary from bottom-left corner).' + msg = "Latitude coordinate along edge of grid domain " + msg += "(going clockwise around grid boundary from bottom-left corner)." grid_edge_lat.description = msg - grid_edge_lat[:] = self._grid_edge_lat['data'] + grid_edge_lat[:] = self._grid_edge_lat["data"] - self.free_vars(('model_centre_longitude', 'model_centre_latitude', 'grid_edge_longitude', 'grid_edge_latitude')) + self.free_vars(["model_centre_longitude", "model_centre_latitude", "grid_edge_longitude", "grid_edge_latitude"]) + # noinspection DuplicatedCode def _get_coordinate_values(self, coordinate_info, coordinate_axis, bounds=False): """ Get the coordinate data of the current portion. @@ -259,9 +260,9 @@ class PointsNesProvidentia(PointsNes): Parameters ---------- coordinate_info : dict, list - Dictionary with the 'data' key with the coordinate variable values. and the attributes as other keys. + Dictionary with the "data" key with the coordinate variable values. and the attributes as other keys. coordinate_axis : str - Name of the coordinate to extract. Accepted values: ['X']. + Name of the coordinate to extract. Accepted values: ["X"]. bounds : bool Boolean variable to know if there are coordinate bounds. Returns @@ -274,32 +275,33 @@ class PointsNesProvidentia(PointsNes): return None if not isinstance(coordinate_info, dict): - values = {'data': deepcopy(coordinate_info)} + values = {"data": deepcopy(coordinate_info)} else: values = deepcopy(coordinate_info) - coordinate_len = len(values['data'].shape) + coordinate_len = len(values["data"].shape) if bounds: coordinate_len -= 1 - if coordinate_axis == 'X': + if coordinate_axis == "X": if coordinate_len == 1: - values['data'] = values['data'][self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + values["data"] = values["data"][self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] elif coordinate_len == 2: - values['data'] = values['data'][self.read_axis_limits['x_min']:self.read_axis_limits['x_max'], - self.read_axis_limits['t_min']:self.read_axis_limits['t_max']] + values["data"] = values["data"][self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"], + self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"]] elif coordinate_len == 3: - values['data'] = values['data'][self.read_axis_limits['x_min']:self.read_axis_limits['x_max'], - self.read_axis_limits['t_min']:self.read_axis_limits['t_max'], :] + values["data"] = values["data"][self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"], + self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"], :] else: raise NotImplementedError("The coordinate has wrong dimensions: {dim}".format( - dim=values['data'].shape)) - elif coordinate_axis == '': - # pass for 'model_centre_lon', 'model_centre_lat', 'grid_edge_lon' and 'grid_edge_lat' + dim=values["data"].shape)) + elif coordinate_axis == "": + # pass for "model_centre_lon", "model_centre_lat", "grid_edge_lon" and "grid_edge_lat" pass return values + # noinspection DuplicatedCode def _read_variable(self, var_name): """ Read the corresponding variable data according to the current rank. @@ -311,7 +313,7 @@ class PointsNesProvidentia(PointsNes): Returns ------- - data: np.array + data: array Portion of the variable data corresponding to the rank. """ nc_var = self.dataset.variables[var_name] @@ -319,16 +321,16 @@ class PointsNesProvidentia(PointsNes): # Read data in 1, 2 or 3 dimensions if len(var_dims) < 2: - data = nc_var[self.read_axis_limits['x_min']:self.read_axis_limits['x_max']] + data = nc_var[self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"]] elif len(var_dims) == 2: - data = nc_var[self.read_axis_limits['x_min']:self.read_axis_limits['x_max'], - self.read_axis_limits['t_min']:self.read_axis_limits['t_max']] + data = nc_var[self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"], + self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"]] elif len(var_dims) == 3: - data = nc_var[self.read_axis_limits['x_min']:self.read_axis_limits['x_max'], - self.read_axis_limits['t_min']:self.read_axis_limits['t_max'], + data = nc_var[self.read_axis_limits["x_min"]:self.read_axis_limits["x_max"], + self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"], :] else: - raise NotImplementedError('Error with {0}. Only can be read netCDF with 3 dimensions or less'.format( + raise NotImplementedError("Error with {0}. Only can be read netCDF with 3 dimensions or less".format( var_name)) # Unmask array @@ -336,6 +338,7 @@ class PointsNesProvidentia(PointsNes): return data + # noinspection DuplicatedCode def _create_variables(self, netcdf, chunking=False): """ Create the netCDF file variables. @@ -351,50 +354,50 @@ class PointsNesProvidentia(PointsNes): if self.variables is not None: for i, (var_name, var_dict) in enumerate(self.variables.items()): # Get data type - if 'dtype' in var_dict.keys(): - var_dtype = var_dict['dtype'] - if (var_dict['data'] is not None) and (var_dtype != var_dict['data'].dtype): + if "dtype" in var_dict.keys(): + var_dtype = var_dict["dtype"] + if (var_dict["data"] is not None) and (var_dtype != var_dict["data"].dtype): msg = "WARNING!!! " msg += "Different data types for variable {0}. ".format(var_name) msg += "Input dtype={0}. Data dtype={1}.".format(var_dtype, - var_dict['data'].dtype) - warnings.warn(msg) + var_dict["data"].dtype) + warn(msg) sys.stderr.flush() try: - var_dict['data'] = var_dict['data'].astype(var_dtype) - except Exception as e: # TODO: Detect exception - raise e("It was not possible to cast the data to the input dtype.") + var_dict["data"] = var_dict["data"].astype(var_dtype) + except Exception: # TODO: Detect exception + raise TypeError("It was not possible to cast the data to the input dtype.") else: - var_dtype = var_dict['data'].dtype - if var_dtype is yobject: + var_dtype = var_dict["data"].dtype + if var_dtype is object: raise TypeError("Data dtype is object. Define dtype explicitly as dictionary key 'dtype'") # Get dimensions when reading datasets - if 'dimensions' in var_dict.keys(): - var_dims = var_dict['dimensions'] + if "dimensions" in var_dict.keys(): + var_dims = var_dict["dimensions"] # Get dimensions when creating new datasets else: - if len(var_dict['data'].shape) == 1: + if len(var_dict["data"].shape) == 1: # For data that depends only on station (e.g. station_code) var_dims = self._var_dim else: # For data that is dependent on time and station (e.g. PM10) - var_dims = self._var_dim + ('time',) + var_dims = self._var_dim + ("time",) - if var_dict['data'] is not None: + if var_dict["data"] is not None: # Ensure data is of type numpy array (to create NES) - if not isinstance(var_dict['data'], (np.ndarray, np.generic)): + if not isinstance(var_dict["data"], (ndarray, generic)): try: - var_dict['data'] = np.array(var_dict['data']) + var_dict["data"] = array(var_dict["data"]) except AttributeError: raise AttributeError("Data for variable {0} must be a numpy array.".format(var_name)) # Convert list of strings to chars for parallelization - if np.issubdtype(var_dtype, np.character): - var_dict['data_aux'] = self.str2char(var_dict['data']) - var_dims += ('strlen',) - var_dtype = 'S1' + if issubdtype(var_dtype, character): + var_dict["data_aux"] = self._str2char(var_dict["data"]) + var_dims += ("strlen",) + var_dtype = "S1" if self.info: print("Rank {0:03d}: Writing {1} var ({2}/{3})".format(self.rank, var_name, i + 1, @@ -405,7 +408,7 @@ class PointsNesProvidentia(PointsNes): zlib=self.zip_lvl > 0, complevel=self.zip_lvl) else: if self.master: - chunk_size = var_dict['data'].shape + chunk_size = var_dict["data"].shape else: chunk_size = None chunk_size = self.comm.bcast(chunk_size, root=0) @@ -422,74 +425,74 @@ class PointsNesProvidentia(PointsNes): self.rank, var_name, i + 1, len(self.variables))) for att_name, att_value in var_dict.items(): - if att_name == 'data': + if att_name == "data": if self.info: print("Rank {0:03d}: Filling {1})".format(self.rank, var_name)) - if 'data_aux' in var_dict.keys(): - att_value = var_dict['data_aux'] + if "data_aux" in var_dict.keys(): + att_value = var_dict["data_aux"] if len(att_value.shape) == 1: try: - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max']] = att_value + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] = att_value except IndexError: raise IndexError("Different shapes. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max']].shape, + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]].shape, att_value.shape)) except ValueError: raise ValueError("Axis limits cannot be accessed. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max']].shape, + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]].shape, att_value.shape)) elif len(att_value.shape) == 2: - if 'strlen' in var_dims: + if "strlen" in var_dims: try: - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], :] = att_value + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], :] = att_value except IndexError: - raise IndexError("Different shapes. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], :].shape, - att_value.shape)) + raise IndexError(f"Different shapes. out_shape={ + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], :].shape}," + f" data_shp={att_value.shape}") except ValueError: - raise ValueError("Axis limits cannot be accessed. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], :].shape, - att_value.shape)) + raise ValueError(f"Axis limits cannot be accessed. out_shape={ + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], :].shape}," + f" data_shp={att_value.shape}") else: try: - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - self.write_axis_limits['t_min']:self.write_axis_limits['t_max']] = att_value + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"]] = att_value except IndexError: - raise IndexError("Different shapes. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - self.write_axis_limits['t_min']:self.write_axis_limits['t_max']].shape, - att_value.shape)) + raise IndexError(f"Different shapes. out_shape={ + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"]].shape}, " + f"data_shp={att_value.shape}") except ValueError: - raise ValueError("Axis limits cannot be accessed. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - self.write_axis_limits['t_min']:self.write_axis_limits['t_max']].shape, - att_value.shape)) + raise ValueError(f"Axis limits cannot be accessed. out_shape={ + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"]].shape}, " + f"data_shp={att_value.shape}") elif len(att_value.shape) == 3: try: - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], :] = att_value except IndexError: raise IndexError("Different shapes. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], :].shape, att_value.shape)) except ValueError: raise ValueError("Axis limits cannot be accessed. out_shape={0}, data_shp={1}".format( - var[self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], + var[self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], :].shape, att_value.shape)) if self.info: print("Rank {0:03d}: Var {1} data ({2}/{3})".format(self.rank, var_name, i + 1, len(self.variables))) - elif att_name not in ['chunk_size', 'var_dims', 'dimensions', 'dtype', 'data_aux']: + elif att_name not in ["chunk_size", "var_dims", "dimensions", "dtype", "data_aux"]: var.setncattr(att_name, att_value) - if 'data_aux' in var_dict.keys(): - del var_dict['data_aux'] + if "data_aux" in var_dict.keys(): + del var_dict["data_aux"] self._set_var_crs(var) if self.info: @@ -497,7 +500,8 @@ class PointsNesProvidentia(PointsNes): len(self.variables))) return None - + + # noinspection DuplicatedCode def _gather_data(self, data_to_gather): """ Gather all the variable data into the MPI rank 0 to perform a serial write. @@ -512,11 +516,11 @@ class PointsNesProvidentia(PointsNes): for var_name, var_info in data_list.items(): try: # noinspection PyArgumentList - data_aux = self.comm.gather(data_list[var_name]['data'], root=0) + data_aux = self.comm.gather(data_list[var_name]["data"], root=0) if self.rank == 0: - shp_len = len(data_list[var_name]['data'].shape) + shp_len = len(data_list[var_name]["data"].shape) # concatenate over station - if self.parallel_method == 'X': + if self.parallel_method == "X": if shp_len == 1: # dimensions = (station) axis = 0 @@ -525,45 +529,44 @@ class PointsNesProvidentia(PointsNes): # dimensions = (station, time) axis = 0 else: - msg = 'The points NetCDF must have ' - msg += 'surface values (without levels).' + msg = "The points NetCDF must have " + msg += "surface values (without levels)." raise NotImplementedError(msg) - elif self.parallel_method == 'T': + elif self.parallel_method == "T": # concatenate over time if shp_len == 1: # dimensions = (station) axis = None - continue elif shp_len == 2: - if 'strlen' in var_info['dimensions']: + if "strlen" in var_info["dimensions"]: # dimensions = (station, strlen) axis = None - continue else: # dimensions = (station, time) axis = 1 else: - msg = 'The points NetCDF must have ' - msg += 'surface values (without levels).' + msg = "The points NetCDF must have " + msg += "surface values (without levels)." raise NotImplementedError(msg) else: raise NotImplementedError( "Parallel method '{meth}' is not implemented. Use one of these: {accept}".format( - meth=self.parallel_method, accept=['X', 'T'])) - data_list[var_name]['data'] = np.concatenate(data_aux, axis=axis) + meth=self.parallel_method, accept=["X", "T"])) + data_list[var_name]["data"] = concatenate(data_aux, axis=axis) except Exception as e: - print("**ERROR** an error has occurred while gathering the '{0}' variable.\n".format(var_name)) - sys.stderr.write("**ERROR** an error has occurred while gathering the '{0}' variable.\n".format(var_name)) + msg = f"**ERROR** an error has occurred while gathering the '{var_name}' variable.\n" + print(msg) + sys.stderr.write(msg) print(e) sys.stderr.write(str(e)) # print(e, file=sys.stderr) sys.stderr.flush() self.comm.Abort(1) - raise e return data_list - def to_netcdf(self, path, compression_level=0, serial=False, info=False, chunking=False): + def to_netcdf(self, path, compression_level=0, serial=False, info=False, chunking=False, nc_type="NES", + keep_open=False): """ Write the netCDF output file. @@ -578,14 +581,18 @@ class PointsNesProvidentia(PointsNes): info : bool Indicates if you want to print the information of each writing step by stdout Default: False. chunking : bool - Indicates if you want a chunked netCDF output. Only available with non serial writes. Default: False. + Indicates if you want a chunked netCDF output. Only available with non-serial writes. Default: False. + nc_type : str + Type to NetCDf to write. "CAMS_RA" or "NES" + keep_open : bool + Indicates if you want to keep open the NetCDH to fill the data by time-step """ if (not serial) and (self.size > 1): - msg = 'WARNING!!! ' - msg += 'Providentia datasets cannot be written in parallel yet. ' - msg += 'Changing to serial mode.' - warnings.warn(msg) + msg = "WARNING!!! " + msg += "Providentia datasets cannot be written in parallel yet. " + msg += "Changing to serial mode." + warn(msg) sys.stderr.flush() super(PointsNesProvidentia, self).to_netcdf(path, compression_level=compression_level, @@ -593,6 +600,7 @@ class PointsNesProvidentia(PointsNes): return None + # noinspection DuplicatedCode def add_variables_to_shapefile(self, var_list, idx_lev=0, idx_time=0): """ Add variables data to shapefile. @@ -606,24 +614,24 @@ class PointsNesProvidentia(PointsNes): """ if idx_lev != 0: - msg = 'Error: Points dataset has no level (Level: {0}).'.format(idx_lev) + msg = "Error: Points dataset has no level (Level: {0}).".format(idx_lev) raise ValueError(msg) for var_name in var_list: # station as dimension - if len(self.variables[var_name]['dimensions']) == 1: - self.shapefile[var_name] = self.variables[var_name]['data'][:].ravel() + if len(self.variables[var_name]["dimensions"]) == 1: + self.shapefile[var_name] = self.variables[var_name]["data"][:].ravel() # station and time as dimensions else: - self.shapefile[var_name] = self.variables[var_name]['data'][:, idx_time].ravel() + self.shapefile[var_name] = self.variables[var_name]["data"][:, idx_time].ravel() return None @staticmethod def _get_axis_index_(axis): - if axis == 'T': + if axis == "T": value = 1 - elif axis == 'X': + elif axis == "X": value = 0 else: raise ValueError("Unknown axis: {0}".format(axis)) diff --git a/nes/nc_projections/rotated_nes.py b/nes/nc_projections/rotated_nes.py index c29b1fd9ad869b5e4f350499d04649ea10f4fc91..c5c3794e82503721d16a0c32a57864bcc23faa67 100644 --- a/nes/nc_projections/rotated_nes.py +++ b/nes/nc_projections/rotated_nes.py @@ -1,13 +1,13 @@ #!/usr/bin/env python -import warnings -import sys -import numpy as np -import pandas as pd -import math +from numpy import (float64, linspace, cos, sin, arcsin, arctan2, array, mean, diff, append, flip, repeat, concatenate, + vstack) +from math import pi +from geopandas import GeoDataFrame +from pandas import Index from pyproj import Proj from copy import deepcopy -import geopandas as gpd +from typing import Dict, Any from shapely.geometry import Polygon, Point from .default_nes import Nes @@ -17,25 +17,25 @@ class RotatedNes(Nes): Attributes ---------- - _rlat : dict - Rotated latitudes dictionary with the complete 'data' key for all the values and the rest of the attributes. - _rlon : dict - Rotated longitudes dictionary with the complete 'data' key for all the values and the rest of the attributes. + _full_rlat : dict + Rotated latitudes dictionary with the complete "data" key for all the values and the rest of the attributes. + _full_rlon : dict + Rotated longitudes dictionary with the complete "data" key for all the values and the rest of the attributes. rlat : dict - Rotated latitudes dictionary with the portion of 'data' corresponding to the rank values. + Rotated latitudes dictionary with the portion of "data" corresponding to the rank values. rlon : dict - Rotated longitudes dictionary with the portion of 'data' corresponding to the rank values. + Rotated longitudes dictionary with the portion of "data" corresponding to the rank values. _var_dim : tuple - Tuple with the name of the Y and X dimensions for the variables. - ('rlat', 'rlon') for a rotated projection. + A Tuple with the name of the Y and X dimensions for the variables. + ("rlat", "rlon") for a rotated projection. _lat_dim : tuple - Tuple with the name of the dimensions of the Latitude values. - ('rlat', 'rlon') for a rotated projection. + A Tuple with the name of the dimensions of the Latitude values. + ("rlat", "rlon") for a rotated projection. _lon_dim : tuple - Tuple with the name of the dimensions of the Longitude values. - ('rlat', 'rlon') for a rotated projection. + A Tuple with the name of the dimensions of the Longitude values. + ("rlat", "rlon") for a rotated projection. """ - def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method='Y', + def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method="Y", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, balanced=False, times=None, **kwargs): """ @@ -52,8 +52,8 @@ class RotatedNes(Nes): dataset: Dataset NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'Y'. - Accepted values: ['X', 'Y', 'T']. + Indicates the parallelization method that you want. Default: "Y". + Accepted values: ["X", "Y", "T"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -70,6 +70,8 @@ class RotatedNes(Nes): times : list, None List of times to substitute the current ones while creation. """ + self._full_rlat = None + self._full_rlon = None super(RotatedNes, self).__init__(comm=comm, path=path, info=info, dataset=dataset, balanced=balanced, @@ -79,29 +81,31 @@ class RotatedNes(Nes): times=times, **kwargs) if create_nes: + # Complete dimensions + # self._full_rlat, self._full_rlon = self._create_rotated_coordinates() # Dimensions screening - self.lat = self._get_coordinate_values(self._lat, 'Y') - self.lon = self._get_coordinate_values(self._lon, 'X') + self.lat = self._get_coordinate_values(self.get_full_latitudes(), "Y") + self.lon = self._get_coordinate_values(self.get_full_longitudes(), "X") else: # Complete dimensions - self._rlat = self._get_coordinate_dimension('rlat') - self._rlon = self._get_coordinate_dimension('rlon') + self._full_rlat = self._get_coordinate_dimension("rlat") + self._full_rlon = self._get_coordinate_dimension("rlon") # Dimensions screening - self.rlat = self._get_coordinate_values(self._rlat, 'Y') - self.rlon = self._get_coordinate_values(self._rlon, 'X') + self.rlat = self._get_coordinate_values(self.get_full_rlat(), "Y") + self.rlon = self._get_coordinate_values(self.get_full_rlon(), "X") # Set axis limits for parallel writing - self.write_axis_limits = self.get_write_axis_limits() + self.write_axis_limits = self._get_write_axis_limits() - self._var_dim = ('rlat', 'rlon') - self._lat_dim = ('rlat', 'rlon') - self._lon_dim = ('rlat', 'rlon') + self._var_dim = ("rlat", "rlon") + self._lat_dim = ("rlat", "rlon") + self._lon_dim = ("rlat", "rlon") @staticmethod - def new(comm=None, path=None, info=False, dataset=None, parallel_method='Y', - avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, - create_nes=False, balanced=False, times=None, **kwargs): + def new(comm=None, path=None, info=False, dataset=None, parallel_method="Y", + avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, + balanced=False, times=None, **kwargs): """ Initialize the Nes class. @@ -116,18 +120,22 @@ class RotatedNes(Nes): dataset: Dataset NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'Y'. - Accepted values: ['X', 'Y', 'T']. + Indicates the parallelization method that you want. Default over Y axis + accepted values: ["X", "Y", "T"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int Number of hours to remove from last time steps. + first_level : int + Index of the first level to use. + last_level : int or None + Index of the last level to use. None if it is the last. create_nes : bool Indicates if you want to create the object from scratch (True) or through an existing file. balanced : bool - Indicates if you want a balanced parallelization or not. + Indicates if you want a balanced parallelization or not. Balanced dataset cannot be written in chunking mode. - times : list, None + times : List[datetime] or None List of times to substitute the current ones while creation. """ @@ -138,20 +146,100 @@ class RotatedNes(Nes): return new - def filter_coordinates_selection(self): + def get_full_rlat(self) -> Dict[str, Any]: + """ + Retrieve the complete rotated latitude information. + + Returns + ------- + Dict[str, Any] + A dictionary containing the complete latitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of latitude values. + attr_name: attr_value, # Latitude attributes. + ... + } + """ + data = self.comm.bcast(self._full_rlat) + + return data + + def get_full_rlon(self) -> Dict[str, Any]: + """ + Retrieve the complete rotated longitude information. + + Returns + ------- + Dict[str, Any] + A dictionary containing the complete longitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of longitude values. + attr_name: attr_value, # Longitude attributes. + ... + } + """ + data = self.comm.bcast(self._full_rlon) + return data + + def set_full_rlat(self, data: Dict[str, Any]) -> None: + """ + Set the complete rotated latitude information. + + Parameters + ---------- + data : Dict[str, Any] + A dictionary containing the complete latitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of latitude values. + attr_name: attr_value, # Latitude attributes. + ... + } + """ + if self.master: + self._full_rlat = data + return None + + def set_full_rlon(self, data: Dict[str, Any]) -> None: + """ + Set the complete rotated longitude information. + + Parameters + ---------- + data : Dict[str, Any] + A dictionary containing the complete longitude data and its attributes. + The dictionary structure is: + { + "data": ndarray, # Array of longitude values. + attr_name: attr_value, # Longitude attributes. + ... + } + """ + if self.master: + self._full_rlon = data + return None + + # noinspection DuplicatedCode + def _filter_coordinates_selection(self): """ Use the selection limits to filter rlat, rlon, time, lev, lat, lon, lon_bnds and lat_bnds. """ - idx = self.get_idx_intervals() + idx = self._get_idx_intervals() - self.rlat = self._get_coordinate_values(self._rlat, 'Y') - self.rlon = self._get_coordinate_values(self._rlon, 'X') + full_rlat = self.get_full_rlat() + full_rlon = self.get_full_rlon() - self._rlat['data'] = self._rlat['data'][idx['idx_y_min']:idx['idx_y_max']] - self._rlon['data'] = self._rlon['data'][idx['idx_x_min']:idx['idx_x_max']] + self.rlat = self._get_coordinate_values(full_rlat, "Y") + self.rlon = self._get_coordinate_values(full_rlon, "X") - super(RotatedNes, self).filter_coordinates_selection() + if self.master: + self.set_full_rlat({'data': full_rlat["data"][idx["idx_y_min"]:idx["idx_y_max"]]}) + self.set_full_rlon({'data': full_rlon["data"][idx["idx_x_min"]:idx["idx_x_max"]]}) + + super(RotatedNes, self)._filter_coordinates_selection() return None @@ -165,64 +253,59 @@ class RotatedNes(Nes): Grid projection. """ - projection = Proj(proj='ob_tran', + projection = Proj(proj="ob_tran", o_proj="longlat", - ellps='WGS84', + ellps="WGS84", R=self.earth_radius[0], - o_lat_p=np.float64(self.projection_data['grid_north_pole_latitude']), - o_lon_p=np.float64(self.projection_data['grid_north_pole_longitude']), - ) + o_lat_p=float64(self.projection_data["grid_north_pole_latitude"]), + o_lon_p=float64(self.projection_data["grid_north_pole_longitude"]), + ) return projection - - def _get_projection(self): - """ - Get 'projection' and 'projection_data' from grid details. + + # noinspection DuplicatedCode + def _get_projection_data(self, create_nes, **kwargs): """ + Retrieves projection data based on grid details. - if 'rotated_pole' in self.variables.keys(): - projection_data = self.variables['rotated_pole'] - self.free_vars('rotated_pole') + Parameters + ---------- + create_nes : bool + Flag indicating whether to create new object (True) or use existing (False). + **kwargs : dict + Additional keyword arguments for specifying projection details. + """ + if create_nes: + projection_data = {"grid_mapping_name": "rotated_latitude_longitude", + "grid_north_pole_latitude": 90 - kwargs["centre_lat"], + "grid_north_pole_longitude": -180 + kwargs["centre_lon"], + "inc_rlat": kwargs["inc_rlat"], + "inc_rlon": kwargs["inc_rlon"], + "south_boundary": kwargs["south_boundary"], + "west_boundary": kwargs["west_boundary"], + } else: - msg = 'There is no variable called rotated_pole, projection has not been defined.' - raise RuntimeError(msg) - - if 'dtype' in projection_data.keys(): - del projection_data['dtype'] - - if 'data' in projection_data.keys(): - del projection_data['data'] - - if 'dimensions' in projection_data.keys(): - del projection_data['dimensions'] + if "rotated_pole" in self.variables.keys(): + projection_data = self.variables["rotated_pole"] + self.free_vars("rotated_pole") + else: + msg = "There is no variable called rotated_pole, projection has not been defined." + raise RuntimeError(msg) - self.projection_data = projection_data - self.projection = self._get_pyproj_projection() + if "dtype" in projection_data.keys(): + del projection_data["dtype"] - return None - - def _create_projection(self, **kwargs): - """ - Create 'projection' and 'projection_data' from projection arguments. - """ - - projection_data = {'grid_mapping_name': 'rotated_latitude_longitude', - 'grid_north_pole_latitude': 90 - kwargs['centre_lat'], - 'grid_north_pole_longitude': -180 + kwargs['centre_lon'], - 'inc_rlat': kwargs['inc_rlat'], - 'inc_rlon': kwargs['inc_rlon'], - 'south_boundary': kwargs['south_boundary'], - 'west_boundary': kwargs['west_boundary'], - } + if "data" in projection_data.keys(): + del projection_data["data"] - self.projection_data = projection_data - self.projection = self._get_pyproj_projection() + if "dimensions" in projection_data.keys(): + del projection_data["dimensions"] - return None + return projection_data def _create_dimensions(self, netcdf): """ - Create 'rlat', 'rlon' and 'spatial_nv' dimensions and the super dimensions 'lev', 'time', 'time_nv', 'lon' and 'lat'. + Create "rlat", "rlon" and "spatial_nv" dimensions and the dimensions "lev", "time", "time_nv", "lon" and "lat". Parameters ---------- @@ -232,52 +315,54 @@ class RotatedNes(Nes): super(RotatedNes, self)._create_dimensions(netcdf) + shape = self.get_full_shape() # Create rlat and rlon dimensions - netcdf.createDimension('rlon', len(self._rlon['data'])) - netcdf.createDimension('rlat', len(self._rlat['data'])) + netcdf.createDimension("rlon", shape[1]) + netcdf.createDimension("rlat", shape[0]) # Create spatial_nv (number of vertices) dimension - if (self._lat_bnds is not None) and (self._lon_bnds is not None): - netcdf.createDimension('spatial_nv', 4) + if (self.lat_bnds is not None) and (self.lon_bnds is not None): + netcdf.createDimension("spatial_nv", 4) pass return None def _create_dimension_variables(self, netcdf): """ - Create the 'rlat' and 'rlon' variables. + Create the "rlat" and "rlon" variables. Parameters ---------- netcdf : Dataset NetCDF object. """ - super(RotatedNes, self)._create_dimension_variables(netcdf) # ROTATED LATITUDES - rlat = netcdf.createVariable('rlat', self._rlat['data'].dtype, ('rlat',)) + full_rlat = self.get_full_rlat() + rlat = netcdf.createVariable("rlat", full_rlat["data"].dtype, ("rlat",)) rlat.long_name = "latitude in rotated pole grid" - if 'units' in self._rlat.keys(): - rlat.units = self._rlat['units'] + if "units" in full_rlat.keys(): + rlat.units = full_rlat["units"] else: - rlat.units = 'degrees' + rlat.units = "degrees" rlat.standard_name = "grid_latitude" if self.size > 1: rlat.set_collective(True) - rlat[:] = self._rlat['data'] + rlat[:] = full_rlat["data"] # ROTATED LONGITUDES - rlon = netcdf.createVariable('rlon', self._rlon['data'].dtype, ('rlon',)) + full_rlon = self.get_full_rlon() + rlon = netcdf.createVariable("rlon", full_rlon["data"].dtype, ("rlon",)) rlon.long_name = "longitude in rotated pole grid" - if 'units' in self._rlon.keys(): - rlon.units = self._rlon['units'] + if "units" in full_rlon.keys(): + rlon.units = full_rlon["units"] else: - rlon.units = 'degrees' + rlon.units = "degrees" rlon.standard_name = "grid_longitude" if self.size > 1: rlon.set_collective(True) - rlon[:] = self._rlon['data'] + rlon[:] = full_rlon["data"] return None @@ -288,32 +373,27 @@ class RotatedNes(Nes): Returns ---------- _rlat : dict - Rotated latitudes dictionary with the complete 'data' key for all the values and the rest of the attributes. + Rotated latitudes dictionary with the "data" key for all the values and the rest of the attributes. _rlon : dict - Rotated longitudes dictionary with the complete 'data' key for all the values and the rest of the attributes. + Rotated longitudes dictionary with the "data" key for all the values and the rest of the attributes. """ - # Get grid resolution - inc_rlon = np.float64(self.projection_data['inc_rlon']) - inc_rlat = np.float64(self.projection_data['inc_rlat']) + inc_rlon = float64(self.projection_data["inc_rlon"]) + inc_rlat = float64(self.projection_data["inc_rlat"]) # Get south and west boundaries - south_boundary = np.float64(self.projection_data['south_boundary']) - west_boundary = np.float64(self.projection_data['west_boundary']) + south_boundary = float64(self.projection_data["south_boundary"]) + west_boundary = float64(self.projection_data["west_boundary"]) # Calculate rotated latitudes n_lat = int((abs(south_boundary) / inc_rlat) * 2 + 1) - rlat = np.linspace(south_boundary, - south_boundary + (inc_rlat * (n_lat - 1)), - n_lat, dtype=np.float64) + rlat = linspace(south_boundary, south_boundary + (inc_rlat * (n_lat - 1)), n_lat, dtype=float64) # Calculate rotated longitudes n_lon = int((abs(west_boundary) / inc_rlon) * 2 + 1) - rlon = np.linspace(west_boundary, - west_boundary + (inc_rlon * (n_lon - 1)), - n_lon, dtype=np.float64) + rlon = linspace(west_boundary, west_boundary + (inc_rlon * (n_lon - 1)), n_lon, dtype=float64) - return {'data': rlat}, {'data': rlon} + return {"data": rlat}, {"data": rlon} def rotated2latlon(self, lon_deg, lat_deg, lon_min=-180): """ @@ -321,50 +401,50 @@ class RotatedNes(Nes): Parameters ---------- - lon_deg : numpy.array + lon_deg : array Rotated longitude coordinate. - lat_deg : numpy.array + lat_deg : array Rotated latitude coordinate. lon_min : float Minimum value for the longitudes: -180 (-180 to 180) or 0 (0 to 360). Returns ---------- - almd : numpy.array + almd : array Unrotated longitudes. - aphd : numpy.array + aphd : array Unrotated latitudes. """ # Get centre coordinates - centre_lat = 90 - np.float64(self.projection_data['grid_north_pole_latitude']) - centre_lon = np.float64(self.projection_data['grid_north_pole_longitude']) + 180 + centre_lat = 90 - float64(self.projection_data["grid_north_pole_latitude"]) + centre_lon = float64(self.projection_data["grid_north_pole_longitude"]) + 180 # Convert to radians - degrees_to_radians = math.pi / 180. + degrees_to_radians = pi / 180. tph0 = centre_lat * degrees_to_radians tlm = lon_deg * degrees_to_radians tph = lat_deg * degrees_to_radians tlm0d = -180 + centre_lon - ctph0 = np.cos(tph0) - stph0 = np.sin(tph0) - stlm = np.sin(tlm) - ctlm = np.cos(tlm) - stph = np.sin(tph) - ctph = np.cos(tph) + ctph0 = cos(tph0) + stph0 = sin(tph0) + stlm = sin(tlm) + ctlm = cos(tlm) + stph = sin(tph) + ctph = cos(tph) # Calculate unrotated latitudes sph = (ctph0 * stph) + (stph0 * ctph * ctlm) sph[sph > 1.] = 1. sph[sph < -1.] = -1. - aph = np.arcsin(sph) + aph = arcsin(sph) aphd = aph / degrees_to_radians # Calculate rotated longitudes anum = ctph * stlm denom = (ctlm * ctph - stph0 * sph) / ctph0 - relm = np.arctan2(anum, denom) - math.pi + relm = arctan2(anum, denom) - pi almd = relm / degrees_to_radians + tlm0d almd[almd > (lon_min + 360)] -= 360 almd[almd < lon_min] += 360 @@ -382,15 +462,18 @@ class RotatedNes(Nes): centre_lon : dict Dictionary with data of centre coordinates for longitude in 2D (latitude, longitude). """ - - # Complete dimensions - self._rlat, self._rlon = self._create_rotated_coordinates() + if self.master: + # Complete dimensions + self._full_rlat, self._full_rlon = self._create_rotated_coordinates() - # Calculate centre latitudes and longitudes (1D to 2D) - centre_lon, centre_lat = self.rotated2latlon(np.array([self._rlon['data']] * len(self._rlat['data'])), - np.array([self._rlat['data']] * len(self._rlon['data'])).T) + # Calculate centre latitudes and longitudes (1D to 2D) + centre_lon, centre_lat = self.rotated2latlon( + array([self._full_rlon["data"]] * len(self._full_rlat["data"])), + array([self._full_rlat["data"]] * len(self._full_rlon["data"])).T) - return {'data': centre_lat}, {'data': centre_lon} + return {"data": centre_lat}, {"data": centre_lon} + else: + return None, None def create_providentia_exp_centre_coordinates(self): """ @@ -412,6 +495,7 @@ class RotatedNes(Nes): return model_centre_lat, model_centre_lon + # noinspection DuplicatedCode def create_providentia_exp_grid_edge_coordinates(self): """ Calculate grid edge latitudes and longitudes and get model grid outline. @@ -425,76 +509,75 @@ class RotatedNes(Nes): """ # Get grid resolution - inc_rlon = np.abs(np.mean(np.diff(self.rlon['data']))) - inc_rlat = np.abs(np.mean(np.diff(self.rlat['data']))) + inc_rlon = abs(mean(diff(self.rlon["data"]))) + inc_rlat = abs(mean(diff(self.rlat["data"]))) # Get bounds for rotated coordinates - rlat_bounds = self.create_single_spatial_bounds(self.rlat['data'], inc_rlat) - rlon_bounds = self.create_single_spatial_bounds(self.rlon['data'], inc_rlon) + rlat_bounds = self._create_single_spatial_bounds(self.rlat["data"], inc_rlat) + rlon_bounds = self._create_single_spatial_bounds(self.rlon["data"], inc_rlon) # Get rotated latitudes for grid edge - left_edge_rlat = np.append(rlat_bounds.flatten()[::2], rlat_bounds.flatten()[-1]) - right_edge_rlat = np.flip(left_edge_rlat, 0) - top_edge_rlat = np.repeat(rlat_bounds[-1][-1], len(self.rlon['data']) - 1) - bottom_edge_rlat = np.repeat(rlat_bounds[0][0], len(self.rlon['data'])) - rlat_grid_edge = np.concatenate((left_edge_rlat, top_edge_rlat, right_edge_rlat, bottom_edge_rlat)) + left_edge_rlat = append(rlat_bounds.flatten()[::2], rlat_bounds.flatten()[-1]) + right_edge_rlat = flip(left_edge_rlat, 0) + top_edge_rlat = repeat(rlat_bounds[-1][-1], len(self.rlon["data"]) - 1) + bottom_edge_rlat = repeat(rlat_bounds[0][0], len(self.rlon["data"])) + rlat_grid_edge = concatenate((left_edge_rlat, top_edge_rlat, right_edge_rlat, bottom_edge_rlat)) # Get rotated longitudes for grid edge - left_edge_rlon = np.repeat(rlon_bounds[0][0], len(self.rlat['data']) + 1) + left_edge_rlon = repeat(rlon_bounds[0][0], len(self.rlat["data"]) + 1) top_edge_rlon = rlon_bounds.flatten()[1:-1:2] - right_edge_rlon = np.repeat(rlon_bounds[-1][-1], len(self.rlat['data']) + 1) - bottom_edge_rlon = np.flip(rlon_bounds.flatten()[:-1:2], 0) - rlon_grid_edge = np.concatenate((left_edge_rlon, top_edge_rlon, right_edge_rlon, bottom_edge_rlon)) + right_edge_rlon = repeat(rlon_bounds[-1][-1], len(self.rlat["data"]) + 1) + bottom_edge_rlon = flip(rlon_bounds.flatten()[:-1:2], 0) + rlon_grid_edge = concatenate((left_edge_rlon, top_edge_rlon, right_edge_rlon, bottom_edge_rlon)) # Get edges for regular coordinates grid_edge_lon_data, grid_edge_lat_data = self.rotated2latlon(rlon_grid_edge, rlat_grid_edge) # Create grid outline by stacking the edges in both coordinates - model_grid_outline = np.vstack((grid_edge_lon_data, grid_edge_lat_data)).T + model_grid_outline = vstack((grid_edge_lon_data, grid_edge_lat_data)).T - grid_edge_lat = {'data': model_grid_outline[:,1]} - grid_edge_lon = {'data': model_grid_outline[:,0]} + grid_edge_lat = {"data": model_grid_outline[:, 1]} + grid_edge_lon = {"data": model_grid_outline[:, 0]} return grid_edge_lat, grid_edge_lon + # noinspection DuplicatedCode def create_spatial_bounds(self): """ Calculate longitude and latitude bounds and set them. """ # Calculate rotated coordinates bounds - inc_rlat = np.abs(np.mean(np.diff(self._rlat['data']))) - rlat_bnds = self.create_single_spatial_bounds(np.array([self._rlat['data']] * len(self._rlon['data'])).T, - inc_rlat, spatial_nv=4, inverse=True) + full_rlat = self.get_full_rlat() + full_rlon = self.get_full_rlon() + inc_rlat = abs(mean(diff(full_rlat["data"]))) + rlat_bnds = self._create_single_spatial_bounds(array([full_rlat["data"]] * len(full_rlon["data"])).T, + inc_rlat, spatial_nv=4, inverse=True) - inc_rlon = np.abs(np.mean(np.diff(self._rlon['data']))) - rlon_bnds = self.create_single_spatial_bounds(np.array([self._rlon['data']] * len(self._rlat['data'])), - inc_rlon, spatial_nv=4) + inc_rlon = abs(mean(diff(full_rlon["data"]))) + rlon_bnds = self._create_single_spatial_bounds(array([full_rlon["data"]] * len(full_rlat["data"])), + inc_rlon, spatial_nv=4) # Transform rotated bounds to regular bounds lon_bnds, lat_bnds = self.rotated2latlon(rlon_bnds, rlat_bnds) # Obtain regular coordinates bounds - self._lat_bnds = {} - self._lat_bnds['data'] = deepcopy(lat_bnds) - self.lat_bnds = {} - self.lat_bnds['data'] = lat_bnds[self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - :] - - self._lon_bnds = {} - self._lon_bnds['data'] = deepcopy(lon_bnds) - self.lon_bnds = {} - self.lon_bnds['data']= lon_bnds[self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max'], - :] + self.set_full_latitudes_boundaries({"data": deepcopy(lat_bnds)}) + self.lat_bnds = {"data": lat_bnds[self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + :]} + + self.set_full_longitudes_boundaries({"data": deepcopy(lon_bnds)}) + self.lon_bnds = {"data": lon_bnds[self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"], + :]} return None @staticmethod def _set_var_crs(var): """ - Set the grid_mapping to 'rotated_pole'. + Set the grid_mapping to "rotated_pole". Parameters ---------- @@ -502,14 +585,14 @@ class RotatedNes(Nes): netCDF4-python variable object. """ - var.grid_mapping = 'rotated_pole' - var.coordinates = 'lat lon' + var.grid_mapping = "rotated_pole" + var.coordinates = "lat lon" return None def _create_metadata(self, netcdf): """ - Create the 'crs' variable for the rotated latitude longitude grid_mapping. + Create the "crs" variable for the rotated latitude longitude grid_mapping. Parameters ---------- @@ -518,10 +601,10 @@ class RotatedNes(Nes): """ if self.projection_data is not None: - mapping = netcdf.createVariable('rotated_pole', 'i') - mapping.grid_mapping_name = self.projection_data['grid_mapping_name'] - mapping.grid_north_pole_latitude = self.projection_data['grid_north_pole_latitude'] - mapping.grid_north_pole_longitude = self.projection_data['grid_north_pole_longitude'] + mapping = netcdf.createVariable("rotated_pole", "i") + mapping.grid_mapping_name = self.projection_data["grid_mapping_name"] + mapping.grid_north_pole_latitude = self.projection_data["grid_north_pole_latitude"] + mapping.grid_north_pole_longitude = self.projection_data["grid_north_pole_longitude"] return None @@ -531,6 +614,8 @@ class RotatedNes(Nes): Parameters ---------- + lat_flip : bool + Indicates if you want to flip the latitude coordinates. path : str Path to the output file. grib_keys : dict @@ -543,6 +628,7 @@ class RotatedNes(Nes): raise NotImplementedError("Grib2 format cannot be written in a Rotated pole projection.") + # noinspection DuplicatedCode def create_shapefile(self): """ Create spatial geodataframe (shapefile). @@ -555,14 +641,14 @@ class RotatedNes(Nes): if self.shapefile is None: - if self._lat_bnds is None or self._lon_bnds is None: + if self.lat_bnds is None or self.lon_bnds is None: self.create_spatial_bounds() # Reshape arrays to create geometry - aux_b_lats = self.lat_bnds['data'].reshape((self.lat_bnds['data'].shape[0] * self.lat_bnds['data'].shape[1], - self.lat_bnds['data'].shape[2])) - aux_b_lons = self.lon_bnds['data'].reshape((self.lon_bnds['data'].shape[0] * self.lon_bnds['data'].shape[1], - self.lon_bnds['data'].shape[2])) + aux_b_lats = self.lat_bnds["data"].reshape((self.lat_bnds["data"].shape[0] * self.lat_bnds["data"].shape[1], + self.lat_bnds["data"].shape[2])) + aux_b_lons = self.lon_bnds["data"].reshape((self.lon_bnds["data"].shape[0] * self.lon_bnds["data"].shape[1], + self.lon_bnds["data"].shape[2])) # Get polygons from bounds geometry = [] @@ -575,9 +661,7 @@ class RotatedNes(Nes): # Create dataframe cointaining all polygons fids = self.get_fids() - gdf = gpd.GeoDataFrame(index=pd.Index(name='FID', data=fids.ravel()), - geometry=geometry, - crs="EPSG:4326") + gdf = GeoDataFrame(index=Index(name="FID", data=fids.ravel()), geometry=geometry, crs="EPSG:4326") self.shapefile = gdf else: @@ -585,6 +669,7 @@ class RotatedNes(Nes): return gdf + # noinspection DuplicatedCode def get_centroids_from_coordinates(self): """ Get centroids from geographical coordinates. @@ -597,15 +682,13 @@ class RotatedNes(Nes): # Get centroids from coordinates centroids = [] - for lat_ind in range(0, self.lon['data'].shape[0]): - for lon_ind in range(0, self.lon['data'].shape[1]): - centroids.append(Point(self.lon['data'][lat_ind, lon_ind], - self.lat['data'][lat_ind, lon_ind])) + for lat_ind in range(0, self.lon["data"].shape[0]): + for lon_ind in range(0, self.lon["data"].shape[1]): + centroids.append(Point(self.lon["data"][lat_ind, lon_ind], + self.lat["data"][lat_ind, lon_ind])) # Create dataframe cointaining all points fids = self.get_fids() - centroids_gdf = gpd.GeoDataFrame(index=pd.Index(name='FID', data=fids.ravel()), - geometry=centroids, - crs="EPSG:4326") + centroids_gdf = GeoDataFrame(index=Index(name="FID", data=fids.ravel()), geometry=centroids, crs="EPSG:4326") return centroids_gdf diff --git a/nes/nc_projections/rotated_nested_nes.py b/nes/nc_projections/rotated_nested_nes.py index cafc607677293000c5ef4e7e1cd2d602318cabdd..4517701655ee09272de966e74bc03ef60c87514f 100644 --- a/nes/nc_projections/rotated_nested_nes.py +++ b/nes/nc_projections/rotated_nested_nes.py @@ -1,13 +1,13 @@ #!/usr/bin/env python -import numpy as np +from numpy import linspace, float64 from netCDF4 import Dataset from .rotated_nes import RotatedNes class RotatedNestedNes(RotatedNes): - def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method='Y', + def __init__(self, comm=None, path=None, info=False, dataset=None, parallel_method="Y", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, balanced=False, times=None, **kwargs): """ @@ -21,11 +21,11 @@ class RotatedNestedNes(RotatedNes): Path to the NetCDF to initialize the object. info: bool Indicates if you want to get reading/writing info. - dataset: Dataset + dataset: Dataset or None NetCDF4-python Dataset to initialize the class. parallel_method : str - Indicates the parallelization method that you want. Default: 'Y'. - Accepted values: ['X', 'Y', 'T']. + Indicates the parallelization method that you want. Default: "Y". + Accepted values: ["X", "Y", "T"]. avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -67,46 +67,51 @@ class RotatedNestedNes(RotatedNes): """ # Read variables from parent grid - netcdf = Dataset(projection_data['parent_grid_path'], mode='r') - rlat = netcdf.variables['rlat'][:] - rlon = netcdf.variables['rlon'][:] - rotated_pole = netcdf.variables['rotated_pole'] - - # j_parent_start starts at index 1 so we must subtract 1 - projection_data['inc_rlat'] = (rlat[1] - rlat[0]) / projection_data['parent_ratio'] - projection_data['1st_rlat'] = rlat[int(projection_data['j_parent_start']) - 1] + netcdf = Dataset(projection_data["parent_grid_path"], mode="r") + rlat = netcdf.variables["rlat"][:] + rlon = netcdf.variables["rlon"][:] + rotated_pole = netcdf.variables["rotated_pole"] + + # j_parent_start starts at index 1, so we must subtract 1 + projection_data["inc_rlat"] = (rlat[1] - rlat[0]) / projection_data["parent_ratio"] + projection_data["1st_rlat"] = rlat[int(projection_data["j_parent_start"]) - 1] - # i_parent_start starts at index 1 so we must subtract 1 - projection_data['inc_rlon'] = (rlon[1] - rlon[0]) / projection_data['parent_ratio'] - projection_data['1st_rlon'] = rlon[int(projection_data['i_parent_start']) - 1] + # i_parent_start starts at index 1, so we must subtract 1 + projection_data["inc_rlon"] = (rlon[1] - rlon[0]) / projection_data["parent_ratio"] + projection_data["1st_rlon"] = rlon[int(projection_data["i_parent_start"]) - 1] - projection_data['grid_north_pole_longitude'] = rotated_pole.grid_north_pole_longitude - projection_data['grid_north_pole_latitude'] = rotated_pole.grid_north_pole_latitude + projection_data["grid_north_pole_longitude"] = rotated_pole.grid_north_pole_longitude + projection_data["grid_north_pole_latitude"] = rotated_pole.grid_north_pole_latitude netcdf.close() return projection_data - def _create_projection(self, **kwargs): + def _get_projection_data(self, create_nes, **kwargs): """ - Create 'projection' and 'projection_data' from projection arguments. - """ - - projection_data = {'grid_mapping_name': "", # TODO: Add name - 'parent_grid_path': kwargs['parent_grid_path'], - 'parent_ratio': kwargs['parent_ratio'], - 'i_parent_start': kwargs['i_parent_start'], - 'j_parent_start': kwargs['j_parent_start'], - 'n_rlat': kwargs['n_rlat'], - 'n_rlon': kwargs['n_rlon'] - } + Retrieves projection data based on grid details. - projection_data = self._get_parent_attributes(projection_data) - - self.projection_data = projection_data - self.projection = self._get_pyproj_projection() + Parameters + ---------- + create_nes : bool + Flag indicating whether to create new object (True) or use existing (False). + **kwargs : dict + Additional keyword arguments for specifying projection details. + """ + if create_nes: + projection_data = {"grid_mapping_name": "rotated_latitude_longitude", + "parent_grid_path": kwargs["parent_grid_path"], + "parent_ratio": kwargs["parent_ratio"], + "i_parent_start": kwargs["i_parent_start"], + "j_parent_start": kwargs["j_parent_start"], + "n_rlat": kwargs["n_rlat"], + "n_rlon": kwargs["n_rlon"] + } + projection_data = self._get_parent_attributes(projection_data) + else: + projection_data = super()._get_projection_data(create_nes, **kwargs) - return None + return projection_data def _create_rotated_coordinates(self): """ @@ -115,32 +120,28 @@ class RotatedNestedNes(RotatedNes): Returns ---------- _rlat : dict - Rotated latitudes dictionary with the complete 'data' key for all the values and the rest of the attributes. + Rotated latitudes dictionary with the "data" key for all the values and the rest of the attributes. _rlon : dict - Rotated longitudes dictionary with the complete 'data' key for all the values and the rest of the attributes. + Rotated longitudes dictionary with the "data" key for all the values and the rest of the attributes. """ # Get grid resolution - inc_rlon = self.projection_data['inc_rlon'] - inc_rlat = self.projection_data['inc_rlat'] + inc_rlon = self.projection_data["inc_rlon"] + inc_rlat = self.projection_data["inc_rlat"] # Get number of rotated coordinates - n_rlat = self.projection_data['n_rlat'] - n_rlon = self.projection_data['n_rlon'] + n_rlat = self.projection_data["n_rlat"] + n_rlon = self.projection_data["n_rlon"] # Get first coordinates - first_rlat = self.projection_data['1st_rlat'] - first_rlon = self.projection_data['1st_rlon'] + first_rlat = self.projection_data["1st_rlat"] + first_rlon = self.projection_data["1st_rlon"] # Calculate rotated latitudes - rlat = np.linspace(first_rlat, - first_rlat + (inc_rlat * (n_rlat - 1)), - n_rlat, dtype=np.float64) + rlat = linspace(first_rlat, first_rlat + (inc_rlat * (n_rlat - 1)), n_rlat, dtype=float64) # Calculate rotated longitudes - rlon = np.linspace(first_rlon, - first_rlon + (inc_rlon * (n_rlon - 1)), - n_rlon, dtype=np.float64) + rlon = linspace(first_rlon, first_rlon + (inc_rlon * (n_rlon - 1)), n_rlon, dtype=float64) - return {'data': rlat}, {'data': rlon} + return {"data": rlat}, {"data": rlon} \ No newline at end of file diff --git a/nes/nes_formats/__init__.py b/nes/nes_formats/__init__.py index e09d18e0c36b0962778d3ce1a49f414dcb6381f4..39aaf300587fc924d1c27241bc2f2fbdaf0dba9c 100644 --- a/nes/nes_formats/__init__.py +++ b/nes/nes_formats/__init__.py @@ -2,3 +2,8 @@ from .cams_ra_format import to_netcdf_cams_ra from .monarch_format import to_netcdf_monarch, to_monarch_units from .cmaq_format import to_netcdf_cmaq, to_cmaq_units from .wrf_chem_format import to_netcdf_wrf_chem, to_wrf_chem_units + +__all__ = [ + 'to_netcdf_cams_ra', 'to_netcdf_monarch', 'to_monarch_units', 'to_netcdf_cmaq', 'to_cmaq_units', + 'to_netcdf_wrf_chem', 'to_wrf_chem_units' +] diff --git a/nes/nes_formats/cams_ra_format.py b/nes/nes_formats/cams_ra_format.py index 1c1718a15102676a3d051a7f584aaf531d8229ab..480becccd81cd67e1e2a15426347e439119e0f3c 100644 --- a/nes/nes_formats/cams_ra_format.py +++ b/nes/nes_formats/cams_ra_format.py @@ -1,15 +1,15 @@ #!/usr/bin/env python import sys -import warnings -import numpy as np -import os import nes +from numpy import float64, float32, int32, array +from warnings import warn from netCDF4 import Dataset from mpi4py import MPI from copy import copy +# noinspection DuplicatedCode def to_netcdf_cams_ra(self, path): """ Horizontal methods from one grid to another one. @@ -21,26 +21,26 @@ def to_netcdf_cams_ra(self, path): path : str Path to the output netCDF file. """ - + if not isinstance(self, nes.LatLonNes): raise TypeError("CAMS Re-Analysis format must have Regular Lat-Lon projection") - if '' not in path: - raise ValueError("AMS Re-Analysis path must contain '' as pattern; current: '{0}'".format(path)) + if "" not in path: + raise ValueError(f"AMS Re-Analysis path must contain '' as pattern; current: '{path}'") orig_path = copy(path) - for i_lev, level in enumerate(self.lev['data']): - path = orig_path.replace('', 'l{0}'.format(i_lev)) + for i_lev, level in enumerate(self.lev["data"]): + path = orig_path.replace("", "l{0}".format(i_lev)) # Open NetCDF if self.info: print("Rank {0:03d}: Creating {1}".format(self.rank, path)) if self.size > 1: - netcdf = Dataset(path, format="NETCDF4", mode='w', parallel=True, comm=self.comm, info=MPI.Info()) + netcdf = Dataset(path, format="NETCDF4", mode="w", parallel=True, comm=self.comm, info=MPI.Info()) else: - netcdf = Dataset(path, format="NETCDF4", mode='w', parallel=False) + netcdf = Dataset(path, format="NETCDF4", mode="w", parallel=False) if self.info: print("Rank {0:03d}: NetCDF ready to write".format(self.rank)) - self.to_dtype(data_type=np.float32) + self.to_dtype(data_type=float32) # Create dimensions create_dimensions(self, netcdf) @@ -65,7 +65,7 @@ def to_netcdf_cams_ra(self, path): def create_dimensions(self, netcdf): """ - Create 'time', 'time_bnds', 'lev', 'lon' and 'lat' dimensions. + Create "time", "time_bnds", "lev", "lon" and "lat" dimensions. Parameters ---------- @@ -76,18 +76,18 @@ def create_dimensions(self, netcdf): """ # Create time dimension - netcdf.createDimension('time', None) + netcdf.createDimension("time", None) # Create lev, lon and lat dimensions - netcdf.createDimension('lat', len(self._lat['data'])) - netcdf.createDimension('lon', len(self._lon['data'])) + netcdf.createDimension("lat", len(self.get_full_latitudes()["data"])) + netcdf.createDimension("lon", len(self.get_full_longitudes()["data"])) return None def create_dimension_variables(self, netcdf): """ - Create the 'time', 'time_bnds', 'lev', 'lat', 'lat_bnds', 'lon' and 'lon_bnds' variables. + Create the "time", "time_bnds", "lev", "lat", "lat_bnds", "lon" and "lon_bnds" variables. Parameters ---------- @@ -98,44 +98,44 @@ def create_dimension_variables(self, netcdf): """ # LATITUDES - lat = netcdf.createVariable('lat', np.float64, ('lat',)) - lat.standard_name = 'latitude' - lat.long_name = 'latitude' - lat.units = 'degrees_north' - lat.axis = 'Y' + lat = netcdf.createVariable("lat", float64, ("lat",)) + lat.standard_name = "latitude" + lat.long_name = "latitude" + lat.units = "degrees_north" + lat.axis = "Y" if self.size > 1: lat.set_collective(True) - lat[:] = self._lat['data'] + lat[:] = self.get_full_latitudes()["data"] # LONGITUDES - lon = netcdf.createVariable('lon', np.float64, ('lon',)) - lon.long_name = 'longitude' - lon.standard_name = 'longitude' - lon.units = 'degrees_east' - lon.axis = 'X' + lon = netcdf.createVariable("lon", float64, ("lon",)) + lon.long_name = "longitude" + lon.standard_name = "longitude" + lon.units = "degrees_east" + lon.axis = "X" if self.size > 1: lon.set_collective(True) - lon[:] = self._lon['data'] + lon[:] = self.get_full_longitudes()["data"] # TIMES - time_var = netcdf.createVariable('time', np.float64, ('time',)) - time_var.standard_name = 'time' - time_var.units = 'day as %Y%m%d.%f' - time_var.calendar = 'proleptic_gregorian' - time_var.axis = 'T' + time_var = netcdf.createVariable("time", float64, ("time",)) + time_var.standard_name = "time" + time_var.units = "day as %Y%m%d.%f" + time_var.calendar = "proleptic_gregorian" + time_var.axis = "T" if self.size > 1: time_var.set_collective(True) - time_var[:] = date2num(self._time[self.get_time_id(self.hours_start, first=True): - self.get_time_id(self.hours_end, first=False)], - time_var.units, time_var.calendar) + time_var[:] = __date2num(self.get_full_times()[self._get_time_id(self.hours_start, first=True): + self._get_time_id(self.hours_end, first=False)]) return None +# noinspection DuplicatedCode def create_variables(self, netcdf, i_lev): """ - Create the netCDF file variables. + Create and write variables to a netCDF file. Parameters ---------- @@ -143,14 +143,16 @@ def create_variables(self, netcdf, i_lev): Source projection Nes Object. netcdf : Dataset netcdf4-python open dataset. + i_lev : int + The specific level index to write data for. """ for i, (var_name, var_dict) in enumerate(self.variables.items()): - if var_dict['data'] is not None: + if var_dict["data"] is not None: if self.info: print("Rank {0:03d}: Writing {1} var ({2}/{3})".format(self.rank, var_name, i + 1, len(self.variables))) try: - var = netcdf.createVariable(var_name, np.float32, ('time', 'lat', 'lon',), + var = netcdf.createVariable(var_name, float32, ("time", "lat", "lon",), zlib=True, complevel=7, least_significant_digit=3) if self.info: @@ -164,39 +166,54 @@ def create_variables(self, netcdf, i_lev): if self.info: print("Rank {0:03d}: Filling {1})".format(self.rank, var_name)) - var[self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], - self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max']] = var_dict['data'][:, i_lev, :, :] + var[self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], + self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] = var_dict["data"][:, i_lev, :, :] if self.info: print("Rank {0:03d}: Var {1} data ({2}/{3})".format( self.rank, var_name, i + 1, len(self.variables))) - var.long_name = var_dict['long_name'] - var.units = var_dict['units'] - var.number_of_significant_digits = np.int32(3) + var.long_name = var_dict["long_name"] + var.units = var_dict["units"] + var.number_of_significant_digits = int32(3) if self.info: print("Rank {0:03d}: Var {1} completed ({2}/{3})".format(self.rank, var_name, i + 1, len(self.variables))) except Exception as e: - print("**ERROR** an error has occurred while writing the '{0}' variable".format(var_name)) - # print("**ERROR** an error has occurredred while writing the '{0}' variable".format(var_name), - # file=sys.stderr) + print(f"**ERROR** an error has occurred while writing the '{var_name}' variable") raise e else: - msg = 'WARNING!!! ' - msg += 'Variable {0} was not loaded. It will not be written.'.format(var_name) - warnings.warn(msg) + msg = "WARNING!!! " + msg += "Variable {0} was not loaded. It will not be written.".format(var_name) + warn(msg) sys.stderr.flush() return None -def date2num(time_array, time_units=None, time_calendar=None): +def __date2num(time_array): + """ + Convert an array of datetime objects to numerical values. + + Parameters + ---------- + time_array : List[datetime.datetime] + List of datetime objects to be converted. + + Returns + ------- + numpy.ndarray + Array of numerical time values, with each date represented as a float. + + Notes + ----- + The conversion represents each datetime as a float in the format YYYYMMDD.HH/24. + """ time_res = [] for aux_time in time_array: time_res.append(float(aux_time.strftime("%Y%m%d")) + (float(aux_time.strftime("%H")) / 24)) - time_res = np.array(time_res, dtype=np.float64) + time_res = array(time_res, dtype=float64) return time_res diff --git a/nes/nes_formats/cmaq_format.py b/nes/nes_formats/cmaq_format.py index e8fd3990ee7d31dddb81a2c97025dbd76f362d19..30a5cea70be6de753dcee740783100a2f1db67ad 100644 --- a/nes/nes_formats/cmaq_format.py +++ b/nes/nes_formats/cmaq_format.py @@ -1,20 +1,20 @@ #!/usr/bin/env python -import numpy as np import nes +from numpy import float32, array, ndarray, empty, int32, float64 from netCDF4 import Dataset from mpi4py import MPI from copy import deepcopy from datetime import datetime GLOBAL_ATTRIBUTES_ORDER = [ - 'IOAPI_VERSION', 'EXEC_ID', 'FTYPE', 'CDATE', 'CTIME', 'WDATE', 'WTIME', 'SDATE', 'STIME', 'TSTEP', 'NTHIK', - 'NCOLS', 'NROWS', 'NLAYS', 'NVARS', 'GDTYP', 'P_ALP', 'P_BET', 'P_GAM', 'XCENT', 'YCENT', 'XORIG', 'YORIG', - 'XCELL', 'YCELL', 'VGTYP', 'VGTOP', 'VGLVLS', 'GDNAM', 'UPNAM', 'FILEDESC', 'HISTORY', 'VAR-LIST'] + "IOAPI_VERSION", "EXEC_ID", "FTYPE", "CDATE", "CTIME", "WDATE", "WTIME", "SDATE", "STIME", "TSTEP", "NTHIK", + "NCOLS", "NROWS", "NLAYS", "NVARS", "GDTYP", "P_ALP", "P_BET", "P_GAM", "XCENT", "YCENT", "XORIG", "YORIG", + "XCELL", "YCELL", "VGTYP", "VGTOP", "VGLVLS", "GDNAM", "UPNAM", "FILEDESC", "HISTORY", "VAR-LIST"] # noinspection DuplicatedCode -def to_netcdf_cmaq(self, path, chunking=False, keep_open=False): +def to_netcdf_cmaq(self, path, keep_open=False): """ Create the NetCDF using netcdf4-python methods. @@ -24,13 +24,11 @@ def to_netcdf_cmaq(self, path, chunking=False, keep_open=False): Source projection Nes Object. path : str Path to the output netCDF file. - chunking: bool - Indicates if you want to chunk the output netCDF. keep_open : bool Indicates if you want to keep open the NetCDH to fill the data by time-step. """ - self.to_dtype(np.float32) + self.to_dtype(float32) set_global_attributes(self) change_variable_attributes(self) @@ -39,9 +37,9 @@ def to_netcdf_cmaq(self, path, chunking=False, keep_open=False): if self.info: print("Rank {0:03d}: Creating {1}".format(self.rank, path)) if self.size > 1: - netcdf = Dataset(path, format="NETCDF4", mode='w', parallel=True, comm=self.comm, info=MPI.Info()) + netcdf = Dataset(path, format="NETCDF4", mode="w", parallel=True, comm=self.comm, info=MPI.Info()) else: - netcdf = Dataset(path, format="NETCDF4", mode='w', parallel=False) + netcdf = Dataset(path, format="NETCDF4", mode="w", parallel=False) if self.info: print("Rank {0:03d}: NetCDF ready to write".format(self.rank)) @@ -74,23 +72,23 @@ def change_variable_attributes(self): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. """ for var_name in self.variables.keys(): - if self.variables[var_name]['units'] == 'mol.s-1': - self.variables[var_name]['units'] = "{:<16}".format('mole/s') - self.variables[var_name]['var_desc'] = "{:<80}".format(self.variables[var_name]['long_name']) - self.variables[var_name]['long_name'] = "{:<16}".format(var_name) - elif self.variables[var_name]['units'] == 'g.s-1': - self.variables[var_name]['units'] = "{:<16}".format('g/s') - self.variables[var_name]['var_desc'] = "{:<80}".format(self.variables[var_name]['long_name']) - self.variables[var_name]['long_name'] = "{:<16}".format(var_name) + if self.variables[var_name]["units"] == "mol.s-1": + self.variables[var_name]["units"] = "{:<16}".format("mole/s") + self.variables[var_name]["var_desc"] = "{:<80}".format(self.variables[var_name]["long_name"]) + self.variables[var_name]["long_name"] = "{:<16}".format(var_name) + elif self.variables[var_name]["units"] == "g.s-1": + self.variables[var_name]["units"] = "{:<16}".format("g/s") + self.variables[var_name]["var_desc"] = "{:<80}".format(self.variables[var_name]["long_name"]) + self.variables[var_name]["long_name"] = "{:<16}".format(var_name) else: raise TypeError("The unit '{0}' of specie {1} is not defined correctly. ".format( - self.variables[var_name]['units'], var_name) + "Should be 'mol.s-1' or 'g.s-1'") + self.variables[var_name]["units"], var_name) + "Should be 'mol.s-1' or 'g.s-1'") return None @@ -102,7 +100,7 @@ def to_cmaq_units(self): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. Returns ------- @@ -112,20 +110,20 @@ def to_cmaq_units(self): self.calculate_grid_area(overwrite=False) for var_name in self.variables.keys(): - if isinstance(self.variables[var_name]['data'], np.ndarray): - if self.variables[var_name]['units'] == 'mol.s-1': + if isinstance(self.variables[var_name]["data"], ndarray): + if self.variables[var_name]["units"] == "mol.s-1": # Kmol.m-2.s-1 to mol.s-1 - self.variables[var_name]['data'] = np.array( - self.variables[var_name]['data'] * 1000 * self.cell_measures['cell_area']['data'], dtype=np.float32) - elif self.variables[var_name]['units'] == 'g.s-1': + self.variables[var_name]["data"] = array( + self.variables[var_name]["data"] * 1000 * self.cell_measures["cell_area"]["data"], dtype=float32) + elif self.variables[var_name]["units"] == "g.s-1": # Kg.m-2.s-1 to g.s-1 - self.variables[var_name]['data'] = np.array( - self.variables[var_name]['data'] * 1000 * self.cell_measures['cell_area']['data'], dtype=np.float32) + self.variables[var_name]["data"] = array( + self.variables[var_name]["data"] * 1000 * self.cell_measures["cell_area"]["data"], dtype=float32) else: raise TypeError("The unit '{0}' of specie {1} is not defined correctly. ".format( - self.variables[var_name]['units'], var_name) + "Should be 'mol.s-1' or 'g.s-1'") - self.variables[var_name]['dtype'] = np.float32 + self.variables[var_name]["units"], var_name) + "Should be 'mol.s-1' or 'g.s-1'") + self.variables[var_name]["dtype"] = float32 return self.variables @@ -137,7 +135,7 @@ def create_tflag(self): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. Returns ------- @@ -145,11 +143,11 @@ def create_tflag(self): Array with the content of TFLAG. """ - t_flag = np.empty((len(self.time), len(self.variables), 2)) + t_flag = empty((len(self.time), len(self.variables), 2)) for i_d, aux_date in enumerate(self.time): - y_d = int(aux_date.strftime('%Y%j')) - hms = int(aux_date.strftime('%H%M%S')) + y_d = int(aux_date.strftime("%Y%j")) + hms = int(aux_date.strftime("%H%M%S")) for i_p in range(len(self.variables)): t_flag[i_d, i_p, 0] = y_d t_flag[i_d, i_p, 1] = hms @@ -164,7 +162,7 @@ def str_var_list(self): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. Returns ------- @@ -172,13 +170,14 @@ def str_var_list(self): List of variable names transformed on string. """ - str_var_list = "" + str_var_list_aux = "" for var in self.variables.keys(): - str_var_list += "{:<16}".format(var) + str_var_list_aux += "{:<16}".format(var) - return str_var_list + return str_var_list_aux +# noinspection DuplicatedCode def set_global_attributes(self): """ Set the NetCDF global attributes. @@ -186,7 +185,7 @@ def set_global_attributes(self): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. """ now = datetime.now() @@ -198,86 +197,86 @@ def set_global_attributes(self): current_attributes = deepcopy(self.global_attrs) del self.global_attrs - self.global_attrs = {'IOAPI_VERSION': 'None: made only with NetCDF libraries', - 'EXEC_ID': "{:<80}".format('0.1alpha'), # Editable - 'FTYPE': np.int32(1), # Editable - 'CDATE': np.int32(now.strftime('%Y%j')), - 'CTIME': np.int32(now.strftime('%H%M%S')), - 'WDATE': np.int32(now.strftime('%Y%j')), - 'WTIME': np.int32(now.strftime('%H%M%S')), - 'SDATE': np.int32(self.time[0].strftime('%Y%j')), - 'STIME': np.int32(self.time[0].strftime('%H%M%S')), - 'TSTEP': np.int32(tstep), - 'NTHIK': np.int32(1), # Editable - 'NCOLS': None, # Projection dependent - 'NROWS': None, # Projection dependent - 'NLAYS': np.int32(len(self.lev['data'])), - 'NVARS': None, # Projection dependent - 'GDTYP': None, # Projection dependent - 'P_ALP': None, # Projection dependent - 'P_BET': None, # Projection dependent - 'P_GAM': None, # Projection dependent - 'XCENT': None, # Projection dependent - 'YCENT': None, # Projection dependent - 'XORIG': None, # Projection dependent - 'YORIG': None, # Projection dependent - 'XCELL': None, # Projection dependent - 'YCELL': None, # Projection dependent - 'VGTYP': np.int32(7), # Editable - 'VGTOP': np.float32(5000.), # Editable - 'VGLVLS': np.array([1., 0.], dtype=np.float32), # Editable - 'GDNAM': "{:<16}".format(''), # Editable - 'UPNAM': "{:<16}".format('HERMESv3'), - 'FILEDESC': "", # Editable - 'HISTORY': "", # Editable - 'VAR-LIST': str_var_list(self)} + self.global_attrs = {"IOAPI_VERSION": "None: made only with NetCDF libraries", + "EXEC_ID": "{:<80}".format("0.1alpha"), # Editable + "FTYPE": int32(1), # Editable + "CDATE": int32(now.strftime("%Y%j")), + "CTIME": int32(now.strftime("%H%M%S")), + "WDATE": int32(now.strftime("%Y%j")), + "WTIME": int32(now.strftime("%H%M%S")), + "SDATE": int32(self.time[0].strftime("%Y%j")), + "STIME": int32(self.time[0].strftime("%H%M%S")), + "TSTEP": int32(tstep), + "NTHIK": int32(1), # Editable + "NCOLS": None, # Projection dependent + "NROWS": None, # Projection dependent + "NLAYS": int32(len(self.lev["data"])), + "NVARS": None, # Projection dependent + "GDTYP": None, # Projection dependent + "P_ALP": None, # Projection dependent + "P_BET": None, # Projection dependent + "P_GAM": None, # Projection dependent + "XCENT": None, # Projection dependent + "YCENT": None, # Projection dependent + "XORIG": None, # Projection dependent + "YORIG": None, # Projection dependent + "XCELL": None, # Projection dependent + "YCELL": None, # Projection dependent + "VGTYP": int32(7), # Editable + "VGTOP": float32(5000.), # Editable + "VGLVLS": array([1., 0.], dtype=float32), # Editable + "GDNAM": "{:<16}".format(""), # Editable + "UPNAM": "{:<16}".format("HERMESv3"), + "FILEDESC": "", # Editable + "HISTORY": "", # Editable + "VAR-LIST": str_var_list(self)} # Editable attributes for att_name, att_value in current_attributes.items(): - if att_name == 'EXEC_ID': + if att_name == "EXEC_ID": self.global_attrs[att_name] = "{:<80}".format(att_value) # Editable - elif att_name == 'FTYPE': - self.global_attrs[att_name] = np.int32(att_value) # Editable - elif att_name == 'NTHIK': - self.global_attrs[att_name] = np.int32(att_value) # Editable - elif att_name == 'VGTYP': - self.global_attrs[att_name] = np.int32(att_value) # Editable - elif att_name == 'VGTOP': - self.global_attrs[att_name] = np.float32(att_value) # Editable - elif att_name == 'VGLVLS': - self.global_attrs[att_name] = np.array(att_value.split(), dtype=np.float32) # Editable - elif att_name == 'GDNAM': + elif att_name == "FTYPE": + self.global_attrs[att_name] = int32(att_value) # Editable + elif att_name == "NTHIK": + self.global_attrs[att_name] = int32(att_value) # Editable + elif att_name == "VGTYP": + self.global_attrs[att_name] = int32(att_value) # Editable + elif att_name == "VGTOP": + self.global_attrs[att_name] = float32(att_value) # Editable + elif att_name == "VGLVLS": + self.global_attrs[att_name] = array(att_value.split(), dtype=float32) # Editable + elif att_name == "GDNAM": self.global_attrs[att_name] = "{:<16}".format(att_value) # Editable - elif att_name == 'FILEDESC': + elif att_name == "FILEDESC": self.global_attrs[att_name] = att_value # Editable - elif att_name == 'HISTORY': + elif att_name == "HISTORY": self.global_attrs[att_name] = att_value # Editable # Projection dependent attributes if isinstance(self, nes.LCCNes): - self.global_attrs['NCOLS'] = np.int32(len(self._x['data'])) - self.global_attrs['NROWS'] = np.int32(len(self._y['data'])) - self.global_attrs['NVARS'] = np.int32(len(self.variables)) - self.global_attrs['GDTYP'] = np.int32(2) - - self.global_attrs['P_ALP'] = np.float64(self.projection_data['standard_parallel'][0]) - self.global_attrs['P_BET'] = np.float64(self.projection_data['standard_parallel'][1]) - self.global_attrs['P_GAM'] = np.float64(self.projection_data['longitude_of_central_meridian']) - self.global_attrs['XCENT'] = np.float64(self.projection_data['longitude_of_central_meridian']) - self.global_attrs['YCENT'] = np.float64(self.projection_data['latitude_of_projection_origin']) - self.global_attrs['XORIG'] = np.float64( - self._x['data'][0]) - (np.float64(self._x['data'][1] - self._x['data'][0]) / 2) - self.global_attrs['YORIG'] = np.float64( - self._y['data'][0]) - (np.float64(self._y['data'][1] - self._y['data'][0]) / 2) - self.global_attrs['XCELL'] = np.float64(self._x['data'][1] - self._x['data'][0]) - self.global_attrs['YCELL'] = np.float64(self._y['data'][1] - self._y['data'][0]) + self.global_attrs["NCOLS"] = int32(len(self._full_x["data"])) + self.global_attrs["NROWS"] = int32(len(self._full_y["data"])) + self.global_attrs["NVARS"] = int32(len(self.variables)) + self.global_attrs["GDTYP"] = int32(2) + + self.global_attrs["P_ALP"] = float64(self.projection_data["standard_parallel"][0]) + self.global_attrs["P_BET"] = float64(self.projection_data["standard_parallel"][1]) + self.global_attrs["P_GAM"] = float64(self.projection_data["longitude_of_central_meridian"]) + self.global_attrs["XCENT"] = float64(self.projection_data["longitude_of_central_meridian"]) + self.global_attrs["YCENT"] = float64(self.projection_data["latitude_of_projection_origin"]) + self.global_attrs["XORIG"] = float64( + self._full_x["data"][0]) - (float64(self._full_x["data"][1] - self._full_x["data"][0]) / 2) + self.global_attrs["YORIG"] = float64( + self._full_y["data"][0]) - (float64(self._full_y["data"][1] - self._full_y["data"][0]) / 2) + self.global_attrs["XCELL"] = float64(self._full_x["data"][1] - self._full_x["data"][0]) + self.global_attrs["YCELL"] = float64(self._full_y["data"][1] - self._full_y["data"][0]) return None def create_dimensions(self, netcdf): """ - Create 'time', 'time_bnds', 'lev', 'lon' and 'lat' dimensions. + Create "time", "time_bnds", "lev", "lon" and "lat" dimensions. Parameters ---------- @@ -287,37 +286,38 @@ def create_dimensions(self, netcdf): netcdf4-python open dataset. """ - netcdf.createDimension('TSTEP', len(self._time)) - netcdf.createDimension('DATE-TIME', 2) - netcdf.createDimension('LAY', len(self._lev['data'])) - netcdf.createDimension('VAR', len(self.variables)) + netcdf.createDimension("TSTEP", len(self.get_full_times())) + netcdf.createDimension("DATE-TIME", 2) + netcdf.createDimension("LAY", len(self.get_full_levels()["data"])) + netcdf.createDimension("VAR", len(self.variables)) if isinstance(self, nes.LCCNes): - netcdf.createDimension('COL', len(self._x['data'])) - netcdf.createDimension('ROW', len(self._y['data'])) + netcdf.createDimension("COL", len(self._full_x["data"])) + netcdf.createDimension("ROW", len(self._full_y["data"])) return None def create_dimension_variables(self, netcdf): """ - Create the 'y' and 'x' variables. + Create the "y" and "x" variables. Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. netcdf : Dataset NetCDF object. """ - tflag = netcdf.createVariable('TFLAG', 'i', ('TSTEP', 'VAR', 'DATE-TIME',)) - tflag.setncatts({'units': "{:<16}".format(''), 'long_name': "{:<16}".format('TFLAG'), - 'var_desc': "{:<80}".format('Timestep-valid flags: (1) YYYYDDD or (2) HHMMSS')}) + tflag = netcdf.createVariable("TFLAG", "i", ("TSTEP", "VAR", "DATE-TIME",)) + tflag.setncatts({"units": "{:<16}".format(""), "long_name": "{:<16}".format("TFLAG"), + "var_desc": "{:<80}".format("Timestep-valid flags: (1) YYYYDDD or (2) HHMMSS")}) tflag[:] = create_tflag(self) return None +# noinspection DuplicatedCode def create_variables(self, netcdf): """ Create the netCDF file variables. @@ -331,25 +331,25 @@ def create_variables(self, netcdf): """ for var_name, var_info in self.variables.items(): - var = netcdf.createVariable(var_name, 'f', ('TSTEP', 'LAY', 'ROW', 'COL',), + var = netcdf.createVariable(var_name, "f", ("TSTEP", "LAY", "ROW", "COL",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - var.units = var_info['units'] - var.long_name = str(var_info['long_name']) - var.var_desc = str(var_info['var_desc']) - if var_info['data'] is not None: + var.units = var_info["units"] + var.long_name = str(var_info["long_name"]) + var.var_desc = str(var_info["var_desc"]) + if var_info["data"] is not None: if self.info: print("Rank {0:03d}: Filling {1})".format(self.rank, var_name)) - if isinstance(var_info['data'], int) and var_info['data'] == 0: - var[self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], - self.write_axis_limits['z_min']:self.write_axis_limits['z_max'], - self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max']] = 0 - - elif len(var_info['data'].shape) == 4: - var[self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], - self.write_axis_limits['z_min']:self.write_axis_limits['z_max'], - self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max']] = var_info['data'] + if isinstance(var_info["data"], int) and var_info["data"] == 0: + var[self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], + self.write_axis_limits["z_min"]:self.write_axis_limits["z_max"], + self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] = 0 + + elif len(var_info["data"].shape) == 4: + var[self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], + self.write_axis_limits["z_min"]:self.write_axis_limits["z_max"], + self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] = var_info["data"] return None diff --git a/nes/nes_formats/monarch_format.py b/nes/nes_formats/monarch_format.py index 34a22d10af47ac09313623379d42fb1bbc5778a1..0a50e75eea67563961ef390e139154892efa0d59 100644 --- a/nes/nes_formats/monarch_format.py +++ b/nes/nes_formats/monarch_format.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -import numpy as np import nes +from numpy import float32, array, ndarray from netCDF4 import Dataset from mpi4py import MPI @@ -23,15 +23,15 @@ def to_netcdf_monarch(self, path, chunking=False, keep_open=False): Indicates if you want to keep open the NetCDH to fill the data by time-step. """ - self.to_dtype(np.float32) + self.to_dtype(float32) # Open NetCDF if self.info: print("Rank {0:03d}: Creating {1}".format(self.rank, path)) if self.size > 1: - netcdf = Dataset(path, format="NETCDF4", mode='w', parallel=True, comm=self.comm, info=MPI.Info()) + netcdf = Dataset(path, format="NETCDF4", mode="w", parallel=True, comm=self.comm, info=MPI.Info()) else: - netcdf = Dataset(path, format="NETCDF4", mode='w', parallel=False) + netcdf = Dataset(path, format="NETCDF4", mode="w", parallel=False) if self.info: print("Rank {0:03d}: NetCDF ready to write".format(self.rank)) @@ -39,25 +39,27 @@ def to_netcdf_monarch(self, path, chunking=False, keep_open=False): self._create_dimensions(netcdf) # Create dimension variables - self._lev['data'] = np.array(self._lev['data'], dtype=np.float32) - self._lat['data'] = np.array(self._lat['data'], dtype=np.float32) - self._lat_bnds['data'] = np.array(self._lat_bnds['data'], dtype=np.float32) - self._lon['data'] = np.array(self._lon['data'], dtype=np.float32) - self._lon_bnds['data'] = np.array(self._lon_bnds['data'], dtype=np.float32) + if self.master: + self._full_lev["data"] = array(self._full_lev["data"], dtype=float32) + self._full_lat["data"] = array(self._full_lat["data"], dtype=float32) + self._full_lat_bnds["data"] = array(self._full_lat_bnds["data"], dtype=float32) + self._full_lon["data"] = array(self._full_lon["data"], dtype=float32) + self._full_lon_bnds["data"] = array(self._full_lon_bnds["data"], dtype=float32) + if isinstance(self, nes.RotatedNes): - self._rlat['data'] = np.array(self._rlat['data'], dtype=np.float32) - self._rlon['data'] = np.array(self._rlon['data'], dtype=np.float32) + self._full_rlat["data"] = array(self._full_rlat["data"], dtype=float32) + self._full_rlon["data"] = array(self._full_rlon["data"], dtype=float32) if isinstance(self, nes.LCCNes) or isinstance(self, nes.MercatorNes): - self._y['data'] = np.array(self._y['data'], dtype=np.float32) - self._x['data'] = np.array(self._x['data'], dtype=np.float32) + self._full_y["data"] = array(self._full_y["data"], dtype=float32) + self._full_x["data"] = array(self._full_x["data"], dtype=float32) self._create_dimension_variables(netcdf) if self.info: print("Rank {0:03d}: Dimensions done".format(self.rank)) # Create cell measures - if 'cell_area' in self.cell_measures.keys(): - self.cell_measures['cell_area']['data'] = np.array(self.cell_measures['cell_area']['data'], dtype=np.float32) + if "cell_area" in self.cell_measures.keys(): + self.cell_measures["cell_area"]["data"] = array(self.cell_measures["cell_area"]["data"], dtype=float32) self._create_cell_measures(netcdf) # Create variables @@ -70,7 +72,7 @@ def to_netcdf_monarch(self, path, chunking=False, keep_open=False): if self.global_attrs is not None: for att_name, att_value in self.global_attrs.items(): netcdf.setncattr(att_name, att_value) - netcdf.setncattr('Conventions', 'CF-1.7') + netcdf.setncattr("Conventions", "CF-1.7") if keep_open: self.dataset = netcdf @@ -87,7 +89,7 @@ def to_monarch_units(self): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. Returns ------- @@ -96,17 +98,17 @@ def to_monarch_units(self): """ for var_name in self.variables.keys(): - if isinstance(self.variables[var_name]['data'], np.ndarray): - if self.variables[var_name]['units'] == 'mol.s-1.m-2': + if isinstance(self.variables[var_name]["data"], ndarray): + if self.variables[var_name]["units"] == "mol.s-1.m-2": # Kmol to mol - self.variables[var_name]['data'] = np.array(self.variables[var_name]['data'] * 1000, dtype=np.float32) - elif self.variables[var_name]['units'] == 'kg.s-1.m-2': + self.variables[var_name]["data"] = array(self.variables[var_name]["data"] * 1000, dtype=float32) + elif self.variables[var_name]["units"] == "kg.s-1.m-2": # No unit change needed - self.variables[var_name]['data'] = np.array(self.variables[var_name]['data'], dtype=np.float32) + self.variables[var_name]["data"] = array(self.variables[var_name]["data"], dtype=float32) else: raise TypeError("The unit '{0}' of specie {1} is not defined correctly. ".format( - self.variables[var_name]['units'], var_name) + + self.variables[var_name]["units"], var_name) + "Should be 'mol.s-1.m-2' or 'kg.s-1.m-2'") - self.variables[var_name]['dtype'] = np.float32 + self.variables[var_name]["dtype"] = float32 return self.variables diff --git a/nes/nes_formats/wrf_chem_format.py b/nes/nes_formats/wrf_chem_format.py index d2a71cabe0d4f919de8cd5dde76c5f9af82e9418..6a06af4600efba99eb5ede74efbbd8ecabab0608 100644 --- a/nes/nes_formats/wrf_chem_format.py +++ b/nes/nes_formats/wrf_chem_format.py @@ -1,28 +1,27 @@ #!/usr/bin/env python -import numpy as np import nes +from numpy import float32, int32, ndarray, array, chararray from netCDF4 import Dataset from mpi4py import MPI from copy import deepcopy -from datetime import datetime GLOBAL_ATTRIBUTES_ORDER = [ - 'TITLE', 'START_DATE', 'WEST-EAST_GRID_DIMENSION', 'SOUTH-NORTH_GRID_DIMENSION', 'BOTTOM-TOP_GRID_DIMENSION', 'DX', - 'DY', 'GRIDTYPE', 'DIFF_OPT', 'KM_OPT', 'DAMP_OPT', 'DAMPCOEF', 'KHDIF', 'KVDIF', 'MP_PHYSICS', 'RA_LW_PHYSICS', - 'RA_SW_PHYSICS', 'SF_SFCLAY_PHYSICS', 'SF_SURFACE_PHYSICS', 'BL_PBL_PHYSICS', 'CU_PHYSICS', 'SF_LAKE_PHYSICS', - 'SURFACE_INPUT_SOURCE', 'SST_UPDATE', 'GRID_FDDA', 'GFDDA_INTERVAL_M', 'GFDDA_END_H', 'GRID_SFDDA', - 'SGFDDA_INTERVAL_M', 'SGFDDA_END_H', 'WEST-EAST_PATCH_START_UNSTAG', 'WEST-EAST_PATCH_END_UNSTAG', - 'WEST-EAST_PATCH_START_STAG', 'WEST-EAST_PATCH_END_STAG', 'SOUTH-NORTH_PATCH_START_UNSTAG', - 'SOUTH-NORTH_PATCH_END_UNSTAG', 'SOUTH-NORTH_PATCH_START_STAG', 'SOUTH-NORTH_PATCH_END_STAG', - 'BOTTOM-TOP_PATCH_START_UNSTAG', 'BOTTOM-TOP_PATCH_END_UNSTAG', 'BOTTOM-TOP_PATCH_START_STAG', - 'BOTTOM-TOP_PATCH_END_STAG', 'GRID_ID', 'PARENT_ID', 'I_PARENT_START', 'J_PARENT_START', 'PARENT_GRID_RATIO', 'DT', - 'CEN_LAT', 'CEN_LON', 'TRUELAT1', 'TRUELAT2', 'MOAD_CEN_LAT', 'STAND_LON', 'POLE_LAT', 'POLE_LON', 'GMT', 'JULYR', - 'JULDAY', 'MAP_PROJ', 'MMINLU', 'NUM_LAND_CAT', 'ISWATER', 'ISLAKE', 'ISICE', 'ISURBAN', 'ISOILWATER'] + "TITLE", "START_DATE", "WEST-EAST_GRID_DIMENSION", "SOUTH-NORTH_GRID_DIMENSION", "BOTTOM-TOP_GRID_DIMENSION", "DX", + "DY", "GRIDTYPE", "DIFF_OPT", "KM_OPT", "DAMP_OPT", "DAMPCOEF", "KHDIF", "KVDIF", "MP_PHYSICS", "RA_LW_PHYSICS", + "RA_SW_PHYSICS", "SF_SFCLAY_PHYSICS", "SF_SURFACE_PHYSICS", "BL_PBL_PHYSICS", "CU_PHYSICS", "SF_LAKE_PHYSICS", + "SURFACE_INPUT_SOURCE", "SST_UPDATE", "GRID_FDDA", "GFDDA_INTERVAL_M", "GFDDA_END_H", "GRID_SFDDA", + "SGFDDA_INTERVAL_M", "SGFDDA_END_H", "WEST-EAST_PATCH_START_UNSTAG", "WEST-EAST_PATCH_END_UNSTAG", + "WEST-EAST_PATCH_START_STAG", "WEST-EAST_PATCH_END_STAG", "SOUTH-NORTH_PATCH_START_UNSTAG", + "SOUTH-NORTH_PATCH_END_UNSTAG", "SOUTH-NORTH_PATCH_START_STAG", "SOUTH-NORTH_PATCH_END_STAG", + "BOTTOM-TOP_PATCH_START_UNSTAG", "BOTTOM-TOP_PATCH_END_UNSTAG", "BOTTOM-TOP_PATCH_START_STAG", + "BOTTOM-TOP_PATCH_END_STAG", "GRID_ID", "PARENT_ID", "I_PARENT_START", "J_PARENT_START", "PARENT_GRID_RATIO", "DT", + "CEN_LAT", "CEN_LON", "TRUELAT1", "TRUELAT2", "MOAD_CEN_LAT", "STAND_LON", "POLE_LAT", "POLE_LON", "GMT", "JULYR", + "JULDAY", "MAP_PROJ", "MMINLU", "NUM_LAND_CAT", "ISWATER", "ISLAKE", "ISICE", "ISURBAN", "ISOILWATER"] # noinspection DuplicatedCode -def to_netcdf_wrf_chem(self, path, chunking=False, keep_open=False): +def to_netcdf_wrf_chem(self, path, keep_open=False): """ Create the NetCDF using netcdf4-python methods. @@ -32,13 +31,11 @@ def to_netcdf_wrf_chem(self, path, chunking=False, keep_open=False): Source projection Nes Object. path : str Path to the output netCDF file. - chunking: bool - Indicates if you want to chunk the output netCDF. keep_open : bool Indicates if you want to keep open the NetCDH to fill the data by time-step. """ - self.to_dtype(np.float32) + self.to_dtype(float32) set_global_attributes(self) change_variable_attributes(self) @@ -47,9 +44,9 @@ def to_netcdf_wrf_chem(self, path, chunking=False, keep_open=False): if self.info: print("Rank {0:03d}: Creating {1}".format(self.rank, path)) if self.size > 1: - netcdf = Dataset(path, format="NETCDF4", mode='w', parallel=True, comm=self.comm, info=MPI.Info()) + netcdf = Dataset(path, format="NETCDF4", mode="w", parallel=True, comm=self.comm, info=MPI.Info()) else: - netcdf = Dataset(path, format="NETCDF4", mode='w', parallel=False) + netcdf = Dataset(path, format="NETCDF4", mode="w", parallel=False) if self.info: print("Rank {0:03d}: NetCDF ready to write".format(self.rank)) @@ -82,32 +79,32 @@ def change_variable_attributes(self): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. """ for var_name in self.variables.keys(): - if self.variables[var_name]['units'] == 'mol.h-1.km-2': - self.variables[var_name]['FieldType'] = np.int32(104) - self.variables[var_name]['MemoryOrder'] = "XYZ" - self.variables[var_name]['description'] = "EMISSIONS" - self.variables[var_name]['units'] = "mol km^-2 hr^-1" - self.variables[var_name]['stagger'] = "" - self.variables[var_name]['coordinates'] = "XLONG XLAT" - - elif self.variables[var_name]['units'] == 'ug.s-1.m-2': - self.variables[var_name]['FieldType'] = np.int32(104) - self.variables[var_name]['MemoryOrder'] = "XYZ" - self.variables[var_name]['description'] = "EMISSIONS" - self.variables[var_name]['units'] = "ug/m3 m/s" - self.variables[var_name]['stagger'] = "" - self.variables[var_name]['coordinates'] = "XLONG XLAT" + if self.variables[var_name]["units"] == "mol.h-1.km-2": + self.variables[var_name]["FieldType"] = int32(104) + self.variables[var_name]["MemoryOrder"] = "XYZ" + self.variables[var_name]["description"] = "EMISSIONS" + self.variables[var_name]["units"] = "mol km^-2 hr^-1" + self.variables[var_name]["stagger"] = "" + self.variables[var_name]["coordinates"] = "XLONG XLAT" + + elif self.variables[var_name]["units"] == "ug.s-1.m-2": + self.variables[var_name]["FieldType"] = int32(104) + self.variables[var_name]["MemoryOrder"] = "XYZ" + self.variables[var_name]["description"] = "EMISSIONS" + self.variables[var_name]["units"] = "ug/m3 m/s" + self.variables[var_name]["stagger"] = "" + self.variables[var_name]["coordinates"] = "XLONG XLAT" else: raise TypeError("The unit '{0}' of specie {1} is not defined correctly. ".format( - self.variables[var_name]['units'], var_name) + "Should be 'mol.h-1.km-2' or 'ug.s-1.m-2'") + self.variables[var_name]["units"], var_name) + "Should be 'mol.h-1.km-2' or 'ug.s-1.m-2'") - if 'long_name' in self.variables[var_name].keys(): - del self.variables[var_name]['long_name'] + if "long_name" in self.variables[var_name].keys(): + del self.variables[var_name]["long_name"] return None @@ -119,7 +116,7 @@ def to_wrf_chem_units(self): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. Returns ------- @@ -129,22 +126,22 @@ def to_wrf_chem_units(self): self.calculate_grid_area(overwrite=False) for var_name in self.variables.keys(): - if isinstance(self.variables[var_name]['data'], np.ndarray): - if self.variables[var_name]['units'] == 'mol.h-1.km-2': + if isinstance(self.variables[var_name]["data"], ndarray): + if self.variables[var_name]["units"] == "mol.h-1.km-2": # 10**6 -> from m2 to km2 # 10**3 -> from kmol to mol # 3600 -> from s to h - self.variables[var_name]['data'] = np.array( - self.variables[var_name]['data'] * 10 ** 6 * 10 ** 3 * 3600, dtype=np.float32) - elif self.variables[var_name]['units'] == 'ug.s-1.m-2': + self.variables[var_name]["data"] = array( + self.variables[var_name]["data"] * 10 ** 6 * 10 ** 3 * 3600, dtype=float32) + elif self.variables[var_name]["units"] == "ug.s-1.m-2": # 10**9 -> from kg to ug - self.variables[var_name]['data'] = np.array( - self.variables[var_name]['data'] * 10 ** 9, dtype=np.float32) + self.variables[var_name]["data"] = array( + self.variables[var_name]["data"] * 10 ** 9, dtype=float32) else: raise TypeError("The unit '{0}' of specie {1} is not defined correctly. ".format( - self.variables[var_name]['units'], var_name) + "Should be 'mol.h-1.km-2' or 'ug.s-1.m-2'") - self.variables[var_name]['dtype'] = np.float32 + self.variables[var_name]["units"], var_name) + "Should be 'mol.h-1.km-2' or 'ug.s-1.m-2'") + self.variables[var_name]["dtype"] = float32 return self.variables @@ -156,7 +153,7 @@ def create_times_var(self): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. Returns ------- @@ -164,7 +161,7 @@ def create_times_var(self): Array with the content of TFLAG. """ - aux_times = np.chararray((len(self.time), 19), itemsize=1) + aux_times = chararray((len(self.time), 19), itemsize=1) for i_d, aux_date in enumerate(self.time): aux_times[i_d] = list(aux_date.strftime("%Y-%m-%d_%H:%M:%S")) @@ -172,6 +169,7 @@ def create_times_var(self): return aux_times +# noinspection DuplicatedCode def set_global_attributes(self): """ Set the NetCDF global attributes @@ -179,148 +177,148 @@ def set_global_attributes(self): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. """ - now = datetime.now() - if len(self.time) > 1: - tstep = ((self.time[1] - self.time[0]).seconds // 3600) * 10000 - else: - tstep = 1 * 10000 + # now = datetime.now() + # if len(self.time) > 1: + # tstep = ((self.time[1] - self.time[0]).seconds // 3600) * 10000 + # else: + # tstep = 1 * 10000 current_attributes = deepcopy(self.global_attrs) del self.global_attrs - self.global_attrs = {'TITLE': None, - 'START_DATE': self.time[0].strftime("%Y-%m-%d_%H:%M:%S"), - 'WEST-EAST_GRID_DIMENSION': None, # Projection dependent attributes - 'SOUTH-NORTH_GRID_DIMENSION': None, # Projection dependent attributes - 'BOTTOM-TOP_GRID_DIMENSION': np.int32(45), - 'DX': None, # Projection dependent attributes - 'DY': None, # Projection dependent attributes - 'GRIDTYPE': 'C', - 'DIFF_OPT': np.int32(1), - 'KM_OPT': np.int32(4), - 'DAMP_OPT': np.int32(3), - 'DAMPCOEF': np.float32(0.2), - 'KHDIF': np.float32(0.), - 'KVDIF': np.float32(0.), - 'MP_PHYSICS': np.int32(6), - 'RA_LW_PHYSICS': np.int32(4), - 'RA_SW_PHYSICS': np.int32(4), - 'SF_SFCLAY_PHYSICS': np.int32(2), - 'SF_SURFACE_PHYSICS': np.int32(2), - 'BL_PBL_PHYSICS': np.int32(8), - 'CU_PHYSICS': np.int32(0), - 'SF_LAKE_PHYSICS': np.int32(0), - 'SURFACE_INPUT_SOURCE': None, # Projection dependent attributes - 'SST_UPDATE': np.int32(0), - 'GRID_FDDA': np.int32(0), - 'GFDDA_INTERVAL_M': np.int32(0), - 'GFDDA_END_H': np.int32(0), - 'GRID_SFDDA': np.int32(0), - 'SGFDDA_INTERVAL_M': np.int32(0), - 'SGFDDA_END_H': np.int32(0), - 'WEST-EAST_PATCH_START_UNSTAG': None, # Projection dependent attributes - 'WEST-EAST_PATCH_END_UNSTAG': None, # Projection dependent attributes - 'WEST-EAST_PATCH_START_STAG': None, # Projection dependent attributes - 'WEST-EAST_PATCH_END_STAG': None, # Projection dependent attributes - 'SOUTH-NORTH_PATCH_START_UNSTAG': None, # Projection dependent attributes - 'SOUTH-NORTH_PATCH_END_UNSTAG': None, # Projection dependent attributes - 'SOUTH-NORTH_PATCH_START_STAG': None, # Projection dependent attributes - 'SOUTH-NORTH_PATCH_END_STAG': None, # Projection dependent attributes - 'BOTTOM-TOP_PATCH_START_UNSTAG': None, - 'BOTTOM-TOP_PATCH_END_UNSTAG': None, - 'BOTTOM-TOP_PATCH_START_STAG': None, - 'BOTTOM-TOP_PATCH_END_STAG': None, - 'GRID_ID': np.int32(1), - 'PARENT_ID': np.int32(0), - 'I_PARENT_START': np.int32(1), - 'J_PARENT_START': np.int32(1), - 'PARENT_GRID_RATIO': np.int32(1), - 'DT': np.float32(18.), - 'CEN_LAT': None, # Projection dependent attributes - 'CEN_LON': None, # Projection dependent attributes - 'TRUELAT1': None, # Projection dependent attributes - 'TRUELAT2': None, # Projection dependent attributes - 'MOAD_CEN_LAT': None, # Projection dependent attributes - 'STAND_LON': None, # Projection dependent attributes - 'POLE_LAT': None, # Projection dependent attributes - 'POLE_LON': None, # Projection dependent attributes - 'GMT': np.float32(self.time[0].hour), - 'JULYR': np.int32(self.time[0].year), - 'JULDAY': np.int32(self.time[0].strftime("%j")), - 'MAP_PROJ': None, # Projection dependent attributes - 'MMINLU': 'MODIFIED_IGBP_MODIS_NOAH', - 'NUM_LAND_CAT': np.int32(41), - 'ISWATER': np.int32(17), - 'ISLAKE': np.int32(-1), - 'ISICE': np.int32(15), - 'ISURBAN': np.int32(13), - 'ISOILWATER': np.int32(14), - 'HISTORY': "", # Editable + self.global_attrs = {"TITLE": None, + "START_DATE": self.time[0].strftime("%Y-%m-%d_%H:%M:%S"), + "WEST-EAST_GRID_DIMENSION": None, # Projection dependent attributes + "SOUTH-NORTH_GRID_DIMENSION": None, # Projection dependent attributes + "BOTTOM-TOP_GRID_DIMENSION": int32(45), + "DX": None, # Projection dependent attributes + "DY": None, # Projection dependent attributes + "GRIDTYPE": "C", + "DIFF_OPT": int32(1), + "KM_OPT": int32(4), + "DAMP_OPT": int32(3), + "DAMPCOEF": float32(0.2), + "KHDIF": float32(0.), + "KVDIF": float32(0.), + "MP_PHYSICS": int32(6), + "RA_LW_PHYSICS": int32(4), + "RA_SW_PHYSICS": int32(4), + "SF_SFCLAY_PHYSICS": int32(2), + "SF_SURFACE_PHYSICS": int32(2), + "BL_PBL_PHYSICS": int32(8), + "CU_PHYSICS": int32(0), + "SF_LAKE_PHYSICS": int32(0), + "SURFACE_INPUT_SOURCE": None, # Projection dependent attributes + "SST_UPDATE": int32(0), + "GRID_FDDA": int32(0), + "GFDDA_INTERVAL_M": int32(0), + "GFDDA_END_H": int32(0), + "GRID_SFDDA": int32(0), + "SGFDDA_INTERVAL_M": int32(0), + "SGFDDA_END_H": int32(0), + "WEST-EAST_PATCH_START_UNSTAG": None, # Projection dependent attributes + "WEST-EAST_PATCH_END_UNSTAG": None, # Projection dependent attributes + "WEST-EAST_PATCH_START_STAG": None, # Projection dependent attributes + "WEST-EAST_PATCH_END_STAG": None, # Projection dependent attributes + "SOUTH-NORTH_PATCH_START_UNSTAG": None, # Projection dependent attributes + "SOUTH-NORTH_PATCH_END_UNSTAG": None, # Projection dependent attributes + "SOUTH-NORTH_PATCH_START_STAG": None, # Projection dependent attributes + "SOUTH-NORTH_PATCH_END_STAG": None, # Projection dependent attributes + "BOTTOM-TOP_PATCH_START_UNSTAG": None, + "BOTTOM-TOP_PATCH_END_UNSTAG": None, + "BOTTOM-TOP_PATCH_START_STAG": None, + "BOTTOM-TOP_PATCH_END_STAG": None, + "GRID_ID": int32(1), + "PARENT_ID": int32(0), + "I_PARENT_START": int32(1), + "J_PARENT_START": int32(1), + "PARENT_GRID_RATIO": int32(1), + "DT": float32(18.), + "CEN_LAT": None, # Projection dependent attributes + "CEN_LON": None, # Projection dependent attributes + "TRUELAT1": None, # Projection dependent attributes + "TRUELAT2": None, # Projection dependent attributes + "MOAD_CEN_LAT": None, # Projection dependent attributes + "STAND_LON": None, # Projection dependent attributes + "POLE_LAT": None, # Projection dependent attributes + "POLE_LON": None, # Projection dependent attributes + "GMT": float32(self.time[0].hour), + "JULYR": int32(self.time[0].year), + "JULDAY": int32(self.time[0].strftime("%j")), + "MAP_PROJ": None, # Projection dependent attributes + "MMINLU": "MODIFIED_IGBP_MODIS_NOAH", + "NUM_LAND_CAT": int32(41), + "ISWATER": int32(17), + "ISLAKE": int32(-1), + "ISICE": int32(15), + "ISURBAN": int32(13), + "ISOILWATER": int32(14), + "HISTORY": "", # Editable } # Editable attributes - float_atts = ['DAMPCOEF', 'KHDIF', 'KVDIF', 'CEN_LAT', 'CEN_LON', 'DT'] - int_atts = ['BOTTOM-TOP_GRID_DIMENSION', 'DIFF_OPT', 'KM_OPT', 'DAMP_OPT', - 'MP_PHYSICS', 'RA_LW_PHYSICS', 'RA_SW_PHYSICS', 'SF_SFCLAY_PHYSICS', 'SF_SURFACE_PHYSICS', - 'BL_PBL_PHYSICS', 'CU_PHYSICS', 'SF_LAKE_PHYSICS', 'SURFACE_INPUT_SOURCE', 'SST_UPDATE', - 'GRID_FDDA', 'GFDDA_INTERVAL_M', 'GFDDA_END_H', 'GRID_SFDDA', 'SGFDDA_INTERVAL_M', 'SGFDDA_END_H', - 'BOTTOM-TOP_PATCH_START_UNSTAG', 'BOTTOM-TOP_PATCH_END_UNSTAG', 'BOTTOM-TOP_PATCH_START_STAG', - 'BOTTOM-TOP_PATCH_END_STAG', 'GRID_ID', 'PARENT_ID', 'I_PARENT_START', 'J_PARENT_START', - 'PARENT_GRID_RATIO', 'NUM_LAND_CAT', 'ISWATER', 'ISLAKE', 'ISICE', 'ISURBAN', 'ISOILWATER'] - str_atts = ['GRIDTYPE', 'MMINLU', 'HISTORY'] + float_atts = ["DAMPCOEF", "KHDIF", "KVDIF", "CEN_LAT", "CEN_LON", "DT"] + int_atts = ["BOTTOM-TOP_GRID_DIMENSION", "DIFF_OPT", "KM_OPT", "DAMP_OPT", + "MP_PHYSICS", "RA_LW_PHYSICS", "RA_SW_PHYSICS", "SF_SFCLAY_PHYSICS", "SF_SURFACE_PHYSICS", + "BL_PBL_PHYSICS", "CU_PHYSICS", "SF_LAKE_PHYSICS", "SURFACE_INPUT_SOURCE", "SST_UPDATE", + "GRID_FDDA", "GFDDA_INTERVAL_M", "GFDDA_END_H", "GRID_SFDDA", "SGFDDA_INTERVAL_M", "SGFDDA_END_H", + "BOTTOM-TOP_PATCH_START_UNSTAG", "BOTTOM-TOP_PATCH_END_UNSTAG", "BOTTOM-TOP_PATCH_START_STAG", + "BOTTOM-TOP_PATCH_END_STAG", "GRID_ID", "PARENT_ID", "I_PARENT_START", "J_PARENT_START", + "PARENT_GRID_RATIO", "NUM_LAND_CAT", "ISWATER", "ISLAKE", "ISICE", "ISURBAN", "ISOILWATER"] + str_atts = ["GRIDTYPE", "MMINLU", "HISTORY"] for att_name, att_value in current_attributes.items(): if att_name in int_atts: - self.global_attrs[att_name] = np.int32(att_value) + self.global_attrs[att_name] = int32(att_value) elif att_name in float_atts: - self.global_attrs[att_name] = np.float32(att_value) + self.global_attrs[att_name] = float32(att_value) elif att_name in str_atts: self.global_attrs[att_name] = str(att_value) # Projection dependent attributes if isinstance(self, nes.LCCNes) or isinstance(self, nes.MercatorNes): - self.global_attrs['WEST-EAST_GRID_DIMENSION'] = np.int32(len(self._x['data']) + 1) - self.global_attrs['SOUTH-NORTH_GRID_DIMENSION'] = np.int32(len(self._y['data']) + 1) - self.global_attrs['DX'] = np.float32(self._x['data'][1] - self._x['data'][0]) - self.global_attrs['DY'] = np.float32(self._y['data'][1] - self._y['data'][0]) - self.global_attrs['SURFACE_INPUT_SOURCE'] = np.int32(1) - self.global_attrs['WEST-EAST_PATCH_START_UNSTAG'] = np.int32(1) - self.global_attrs['WEST-EAST_PATCH_END_UNSTAG'] = np.int32(len(self._x['data'])) - self.global_attrs['WEST-EAST_PATCH_START_STAG'] = np.int32(1) - self.global_attrs['WEST-EAST_PATCH_END_STAG'] = np.int32(len(self._x['data']) + 1) - self.global_attrs['SOUTH-NORTH_PATCH_START_UNSTAG'] = np.int32(1) - self.global_attrs['SOUTH-NORTH_PATCH_END_UNSTAG'] = np.int32(len(self._y['data'])) - self.global_attrs['SOUTH-NORTH_PATCH_START_STAG'] = np.int32(1) - self.global_attrs['SOUTH-NORTH_PATCH_END_STAG'] = np.int32(len(self._y['data']) + 1) - - self.global_attrs['POLE_LAT'] = np.float32(90) - self.global_attrs['POLE_LON'] = np.float32(0) + self.global_attrs["WEST-EAST_GRID_DIMENSION"] = int32(len(self._full_x["data"]) + 1) + self.global_attrs["SOUTH-NORTH_GRID_DIMENSION"] = int32(len(self._full_y["data"]) + 1) + self.global_attrs["DX"] = float32(self._full_x["data"][1] - self._full_x["data"][0]) + self.global_attrs["DY"] = float32(self._full_y["data"][1] - self._full_y["data"][0]) + self.global_attrs["SURFACE_INPUT_SOURCE"] = int32(1) + self.global_attrs["WEST-EAST_PATCH_START_UNSTAG"] = int32(1) + self.global_attrs["WEST-EAST_PATCH_END_UNSTAG"] = int32(len(self._full_x["data"])) + self.global_attrs["WEST-EAST_PATCH_START_STAG"] = int32(1) + self.global_attrs["WEST-EAST_PATCH_END_STAG"] = int32(len(self._full_x["data"]) + 1) + self.global_attrs["SOUTH-NORTH_PATCH_START_UNSTAG"] = int32(1) + self.global_attrs["SOUTH-NORTH_PATCH_END_UNSTAG"] = int32(len(self._full_y["data"])) + self.global_attrs["SOUTH-NORTH_PATCH_START_STAG"] = int32(1) + self.global_attrs["SOUTH-NORTH_PATCH_END_STAG"] = int32(len(self._full_y["data"]) + 1) + + self.global_attrs["POLE_LAT"] = float32(90) + self.global_attrs["POLE_LON"] = float32(0) if isinstance(self, nes.LCCNes): - self.global_attrs['MAP_PROJ'] = np.int32(1) - self.global_attrs['TRUELAT1'] = np.float32(self.projection_data['standard_parallel'][0]) - self.global_attrs['TRUELAT2'] = np.float32(self.projection_data['standard_parallel'][1]) - self.global_attrs['MOAD_CEN_LAT'] = np.float32(self.projection_data['latitude_of_projection_origin']) - self.global_attrs['STAND_LON'] = np.float32(self.projection_data['longitude_of_central_meridian']) - self.global_attrs['CEN_LAT'] = np.float32(self.projection_data['latitude_of_projection_origin']) - self.global_attrs['CEN_LON'] = np.float32(self.projection_data['longitude_of_central_meridian']) + self.global_attrs["MAP_PROJ"] = int32(1) + self.global_attrs["TRUELAT1"] = float32(self.projection_data["standard_parallel"][0]) + self.global_attrs["TRUELAT2"] = float32(self.projection_data["standard_parallel"][1]) + self.global_attrs["MOAD_CEN_LAT"] = float32(self.projection_data["latitude_of_projection_origin"]) + self.global_attrs["STAND_LON"] = float32(self.projection_data["longitude_of_central_meridian"]) + self.global_attrs["CEN_LAT"] = float32(self.projection_data["latitude_of_projection_origin"]) + self.global_attrs["CEN_LON"] = float32(self.projection_data["longitude_of_central_meridian"]) elif isinstance(self, nes.MercatorNes): - self.global_attrs['MAP_PROJ'] = np.int32(3) - self.global_attrs['TRUELAT1'] = np.float32(self.projection_data['standard_parallel']) - self.global_attrs['TRUELAT2'] = np.float32(0) - self.global_attrs['MOAD_CEN_LAT'] = np.float32(self.projection_data['standard_parallel']) - self.global_attrs['STAND_LON'] = np.float32(self.projection_data['longitude_of_projection_origin']) - self.global_attrs['CEN_LAT'] = np.float32(self.projection_data['standard_parallel']) - self.global_attrs['CEN_LON'] = np.float32(self.projection_data['longitude_of_projection_origin']) + self.global_attrs["MAP_PROJ"] = int32(3) + self.global_attrs["TRUELAT1"] = float32(self.projection_data["standard_parallel"]) + self.global_attrs["TRUELAT2"] = float32(0) + self.global_attrs["MOAD_CEN_LAT"] = float32(self.projection_data["standard_parallel"]) + self.global_attrs["STAND_LON"] = float32(self.projection_data["longitude_of_projection_origin"]) + self.global_attrs["CEN_LAT"] = float32(self.projection_data["standard_parallel"]) + self.global_attrs["CEN_LON"] = float32(self.projection_data["longitude_of_projection_origin"]) return None def create_dimensions(self, netcdf): """ - Create 'time', 'time_bnds', 'lev', 'lon' and 'lat' dimensions. + Create "time", "time_bnds", "lev", "lon" and "lat" dimensions. Parameters ---------- @@ -330,34 +328,35 @@ def create_dimensions(self, netcdf): netcdf4-python open dataset. """ - netcdf.createDimension('Time', len(self._time)) - netcdf.createDimension('DateStrLen', 19) - netcdf.createDimension('emissions_zdim', len(self._lev['data'])) + netcdf.createDimension("Time", len(self.get_full_times())) + netcdf.createDimension("DateStrLen", 19) + netcdf.createDimension("emissions_zdim", len(self.get_full_levels()["data"])) if isinstance(self, nes.LCCNes): - netcdf.createDimension('west_east', len(self._x['data'])) - netcdf.createDimension('south_north', len(self._y['data'])) + netcdf.createDimension("west_east", len(self._full_x["data"])) + netcdf.createDimension("south_north", len(self._full_y["data"])) return None def create_dimension_variables(self, netcdf): """ - Create the 'y' and 'x' variables. + Create the "y" and "x" variables. Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. netcdf : Dataset NetCDF object. """ - times = netcdf.createVariable('Times', 'S1', ('Time', 'DateStrLen', )) + times = netcdf.createVariable("Times", "S1", ("Time", "DateStrLen", )) times[:] = create_times_var(self) return None +# noinspection DuplicatedCode def create_variables(self, netcdf): """ Create the netCDF file variables. @@ -371,29 +370,29 @@ def create_variables(self, netcdf): """ for var_name, var_info in self.variables.items(): - var = netcdf.createVariable(var_name, 'f', ('Time', 'emissions_zdim', 'south_north', 'west_east',), + var = netcdf.createVariable(var_name, "f", ("Time", "emissions_zdim", "south_north", "west_east",), zlib=self.zip_lvl > 0, complevel=self.zip_lvl) - var.FieldType = var_info['FieldType'] - var.MemoryOrder = var_info['MemoryOrder'] - var.description = var_info['description'] - var.units = var_info['units'] - var.stagger = var_info['stagger'] - var.coordinates = var_info['coordinates'] - - if var_info['data'] is not None: + var.FieldType = var_info["FieldType"] + var.MemoryOrder = var_info["MemoryOrder"] + var.description = var_info["description"] + var.units = var_info["units"] + var.stagger = var_info["stagger"] + var.coordinates = var_info["coordinates"] + + if var_info["data"] is not None: if self.info: print("Rank {0:03d}: Filling {1})".format(self.rank, var_name)) - if isinstance(var_info['data'], int) and var_info['data'] == 0: - var[self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], - self.write_axis_limits['z_min']:self.write_axis_limits['z_max'], - self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max']] = 0 - - elif len(var_info['data'].shape) == 4: - var[self.write_axis_limits['t_min']:self.write_axis_limits['t_max'], - self.write_axis_limits['z_min']:self.write_axis_limits['z_max'], - self.write_axis_limits['y_min']:self.write_axis_limits['y_max'], - self.write_axis_limits['x_min']:self.write_axis_limits['x_max']] = var_info['data'] + if isinstance(var_info["data"], int) and var_info["data"] == 0: + var[self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], + self.write_axis_limits["z_min"]:self.write_axis_limits["z_max"], + self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] = 0 + + elif len(var_info["data"].shape) == 4: + var[self.write_axis_limits["t_min"]:self.write_axis_limits["t_max"], + self.write_axis_limits["z_min"]:self.write_axis_limits["z_max"], + self.write_axis_limits["y_min"]:self.write_axis_limits["y_max"], + self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] = var_info["data"] return None diff --git a/requirements.txt b/requirements.txt index 96fd35af926bbb78911bdd636edbd04c4c08590d..03716339c4452cbf324cc3bc35e5dee335f28b53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,13 @@ -geopandas>=1.0.0 +geopandas rtree>=0.9.0 -pandas>=1.3.5 -netcdf4>=1.6.2 -numpy>=1.20.0 -pyproj~=3.2.1 -setuptools>=66.1.1 -scipy>=1.7.3 -filelock>=3.9.0 +pandas +netcdf4 +numpy +pyproj +setuptools +scipy +filelock python-eccodes>=0.9.5 -mpi4py>=3.1.4 \ No newline at end of file +mpi4py +shapely +python-dateutil \ No newline at end of file diff --git a/setup.py b/setup.py index 4d77e0cc639f7217adb016fb1afe5dcb5413d5ff..fa627cee1b944b0a97bf29c585a22075ebf6d06d 100755 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ setup( # entry_points={ # 'console_scripts': [ - # 'NetCDF_mask = snes.netCDF_mask:run', + # 'NetCDF_mask = nes.utilities.netCDF_mask:run', # ], # }, ) diff --git a/tests/1.1-test_read_write_projection.py b/tests/1.1-test_read_write_projection.py index cd47aaed949f6c238b348d0ab6de3ef5051b4d01..5788b3043cd4e6b21258915b97e002a168f7ef39 100644 --- a/tests/1.1-test_read_write_projection.py +++ b/tests/1.1-test_read_write_projection.py @@ -4,7 +4,7 @@ import sys import timeit import pandas as pd from mpi4py import MPI -from nes import * +from nes import open_netcdf comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -24,6 +24,7 @@ result = pd.DataFrame(index=['read', 'write'], test_name = '1.1.1.Regular' if rank == 0: print(test_name) +comm.Barrier() # Original path: /gpfs/scratch/bsc32/bsc32538/original_files/franco_interp.nc # Regular lat-lon grid from MONARCH @@ -36,7 +37,7 @@ comm.Barrier() result.loc['read', test_name] = timeit.default_timer() - st_time # LOAD VARIABLES -variables = ['O3'] +variables = ['sconcno2'] nessy.keep_vars(variables) nessy.load() diff --git a/tests/1.2-test_create_projection.py b/tests/1.2-test_create_projection.py index fe6f0ba597ba1746ac721d6ad8c99ff84bb0474f..60c470ad0c1acd4d4cee5ce7ff3f9f37211f2fe6 100644 --- a/tests/1.2-test_create_projection.py +++ b/tests/1.2-test_create_projection.py @@ -4,7 +4,7 @@ import sys from mpi4py import MPI import pandas as pd import timeit -from nes import * +from nes import create_nes comm = MPI.COMM_WORLD rank = comm.Get_rank() diff --git a/tests/1.3-test_selecting.py b/tests/1.3-test_selecting.py index 68106b1f08e32533888ae7b43887999ec98f17d2..00bbb23493c5239a27b344e7a2363ed0e17d8a80 100644 --- a/tests/1.3-test_selecting.py +++ b/tests/1.3-test_selecting.py @@ -3,8 +3,7 @@ import sys import timeit import pandas as pd from mpi4py import MPI -from nes import * -import os, sys +from nes import open_netcdf from datetime import datetime comm = MPI.COMM_WORLD @@ -26,7 +25,7 @@ var_list = ['O3'] # ====================================================================================================================== # ====================================== '1.3.1.LatLon' ===================================================== # ====================================================================================================================== -test_name = '1.3.1.LatLon' +test_name = '1.3.1.Selecting_LatLon' if rank == 0: print(test_name) @@ -57,7 +56,7 @@ sys.stdout.flush() # ====================================================================================================================== # ====================================== 1.3.2.Level ===================================================== # ====================================================================================================================== -test_name = '1.3.2.Level' +test_name = '1.3.2.Selecting_Level' if rank == 0: print(test_name) @@ -88,7 +87,7 @@ sys.stdout.flush() # ====================================================================================================================== # ====================================== 1.3.3.Time ===================================================== # ====================================================================================================================== -test_name = '1.3.3.Time' +test_name = '1.3.3.Selecting_Time' if rank == 0: print(test_name) @@ -120,7 +119,7 @@ sys.stdout.flush() # ====================================================================================================================== # ====================================== '1.3.4.Time_min' ===================================================== # ====================================================================================================================== -test_name = '1.3.4.Time_min' +test_name = '1.3.4.Selecting_Time_min' if rank == 0: print(test_name) @@ -151,7 +150,7 @@ sys.stdout.flush() # ====================================================================================================================== # ====================================== '1.3.5.Time_max' ===================================================== # ====================================================================================================================== -test_name = '1.3.5.Time_max' +test_name = '1.3.5.Selecting_Time_max' if rank == 0: print(test_name) diff --git a/tests/2.1-test_spatial_join.py b/tests/2.1-test_spatial_join.py index 98a482df6e61b8b25861aa412cb4b9dd1e6a4aed..e24d443f614fe8fe24cb7c68155dc062c08bd38e 100644 --- a/tests/2.1-test_spatial_join.py +++ b/tests/2.1-test_spatial_join.py @@ -4,8 +4,7 @@ import sys from mpi4py import MPI import pandas as pd import timeit -import numpy as np -from nes import * +from nes import open_netcdf, from_shapefile comm = MPI.COMM_WORLD rank = comm.Get_rank() diff --git a/tests/2.2-test_create_shapefile.py b/tests/2.2-test_create_shapefile.py index 4c44ff01a4a8fa480411ba1b671e7b01e76c0332..6d443a7040deb03a6503ecf6b35654ad8dde26f1 100644 --- a/tests/2.2-test_create_shapefile.py +++ b/tests/2.2-test_create_shapefile.py @@ -4,8 +4,8 @@ import sys import timeit import pandas as pd from mpi4py import MPI -from nes import * import datetime +from nes import create_nes, open_netcdf comm = MPI.COMM_WORLD rank = comm.Get_rank() diff --git a/tests/2.3-test_bounds.py b/tests/2.3-test_bounds.py index a761858fa7e7209093bd6f474bb880152e8c202c..a2a9c1cea863791f286fde85bbf17ac1c0e1999f 100644 --- a/tests/2.3-test_bounds.py +++ b/tests/2.3-test_bounds.py @@ -4,7 +4,7 @@ import sys import timeit import pandas as pd from mpi4py import MPI -from nes import * +from nes import open_netcdf, create_nes comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -14,7 +14,8 @@ parallel_method = 'Y' result_path = "Times_test_2.3_bounds_{0}_{1:03d}.csv".format(parallel_method, size) result = pd.DataFrame(index=['read', 'calculate', 'write'], - columns=['2.3.1.With_bounds', '2.3.2.Without_bounds', "2.3.3.Create_new", "2.3.4.latlon_sel_create_bnds", "2.3.5.rotated_sel_create_bnds"]) + columns=['2.3.1.With_bounds', '2.3.2.Without_bounds', "2.3.3.Create_new", + "2.3.4.latlon_sel_create_bnds", "2.3.5.rotated_sel_create_bnds"]) # ====================================================================================================================== # ===================================== FILE WITH EXISTING BOUNDS ==================================================== @@ -67,8 +68,8 @@ test_name = '2.3.2.Without_bounds' if rank == 0: print(test_name) -# Original path: /gpfs/scratch/bsc32/bsc32538/mr_multiplyby/OUT/stats_bnds/monarch/a45g/regional/daily_max/O3_all/O3_all-000_2021080300.nc -# Rotated grid from MONARCH +# Original path: /gpfs/scratch/bsc32/bsc32538/mr_multiplyby/OUT/stats_bnds/monarch/a45g/regional/daily_max/O3_all +# /O3_all-000_2021080300.nc Rotated grid from MONARCH st_time = timeit.default_timer() path_3 = "/gpfs/projects/bsc32/models/NES_tutorial_data/O3_all-000_2021080300.nc" nessy_3 = open_netcdf(path=path_3, parallel_method=parallel_method, info=True) @@ -183,8 +184,6 @@ result.loc['calculate', test_name] = timeit.default_timer() - st_time # EXPLORE BOUNDS print('FROM NEW GRID - Rank', rank, '-', 'Lat bounds', nessy_7.lat_bnds) print('FROM NEW GRID - Rank', rank, '-', 'Lon bounds', nessy_7.lon_bnds) -#print('FROM NEW GRID - Rank', rank, '-', '_Lat bounds', nessy_7._lat_bnds) -#print('FROM NEW GRID - Rank', rank, '-', '_Lon bounds', nessy_7._lon_bnds) # Check lon_bnds if nessy_7.lon_bnds['data'].shape != (52, 2): @@ -202,8 +201,6 @@ nessy_8 = open_netcdf(test_name.replace(' ', '_') + "_{0:03d}.nc".format(size), # LOAD DATA AND EXPLORE BOUNDS print('FROM NEW GRID AFTER WRITE - Rank', rank, '-', 'Lat bounds', nessy_8.lat_bnds) print('FROM NEW GRID AFTER WRITE - Rank', rank, '-', 'Lon bounds', nessy_8.lon_bnds) -#print('FROM NEW GRID AFTER WRITE - Rank', rank, '-', '_Lat bounds', nessy_8._lat_bnds) -#print('FROM NEW GRID AFTER WRITE - Rank', rank, '-', '_Lon bounds', nessy_8._lon_bnds) # Check lon_bnds if nessy_8.lon_bnds['data'].shape != (52, 2): @@ -225,8 +222,8 @@ if rank == 0: # USE FILE AS 2.3.2 -# Original path: /gpfs/scratch/bsc32/bsc32538/mr_multiplyby/OUT/stats_bnds/monarch/a45g/regional/daily_max/O3_all/O3_all-000_2021080300.nc -# Rotated grid from MONARCH +# Original path: /gpfs/scratch/bsc32/bsc32538/mr_multiplyby/OUT/stats_bnds/monarch/a45g/regional/daily_max/O3_all +# /O3_all-000_2021080300.nc Rotated grid from MONARCH st_time = timeit.default_timer() path_9 = "/gpfs/projects/bsc32/models/NES_tutorial_data/O3_all-000_2021080300.nc" nessy_9 = open_netcdf(path=path_9, parallel_method=parallel_method, info=True) diff --git a/tests/2.4-test_cell_area.py b/tests/2.4-test_cell_area.py index 49821a0f9dd22df5c0aac5f7bc1aaf48dcb46e02..9db836ff9b388d061c06144c50958ea0fb7d7f81 100644 --- a/tests/2.4-test_cell_area.py +++ b/tests/2.4-test_cell_area.py @@ -4,7 +4,7 @@ import sys import timeit import pandas as pd from mpi4py import MPI -from nes import * +from nes import create_nes, open_netcdf, calculate_geometry_area comm = MPI.COMM_WORLD rank = comm.Get_rank() diff --git a/tests/3.1-test_vertical_interp.py b/tests/3.1-test_vertical_interp.py index bbee5781a4b7fa7bf2d13f3347f5d08e46ab2e41..9b78628a1f22a3f8f4d1b50429607198e0104a61 100644 --- a/tests/3.1-test_vertical_interp.py +++ b/tests/3.1-test_vertical_interp.py @@ -3,9 +3,7 @@ import sys import timeit import pandas as pd from mpi4py import MPI -from nes import * -import os, sys -from datetime import datetime +from nes import open_netcdf comm = MPI.COMM_WORLD rank = comm.Get_rank() diff --git a/tests/3.2-test_horiz_interp_bilinear.py b/tests/3.2-test_horiz_interp_bilinear.py index 30188814b9d92b7f1c71ca9066ec80be90277631..4366a8de6a8d55e2b58fe2de65054caea4c63335 100644 --- a/tests/3.2-test_horiz_interp_bilinear.py +++ b/tests/3.2-test_horiz_interp_bilinear.py @@ -3,8 +3,8 @@ import sys import timeit import pandas as pd from mpi4py import MPI -from nes import * -import os, sys +from nes import open_netcdf, create_nes +import os comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -26,6 +26,7 @@ var_list = ['O3'] test_name = '3.2.1.NN_Only interp' if rank == 0: print(test_name) + sys.stdout.flush() # READING st_time = timeit.default_timer() diff --git a/tests/3.3-test_horiz_interp_conservative.py b/tests/3.3-test_horiz_interp_conservative.py index a10f27df87c8ad5282014892d4ef34efa0508c13..90aa72bd920c4d23f6ad950229139356b641634d 100644 --- a/tests/3.3-test_horiz_interp_conservative.py +++ b/tests/3.3-test_horiz_interp_conservative.py @@ -1,10 +1,10 @@ #!/usr/bin/env python import sys +import os import timeit import pandas as pd from mpi4py import MPI -from nes import * -import os, sys +from nes import open_netcdf, create_nes comm = MPI.COMM_WORLD rank = comm.Get_rank() diff --git a/tests/4.1-test_stats.py b/tests/4.1-test_stats.py index 0392d9cfadda5d7ba209e31d601912c06b504762..f11206c72ebc5d7d08bfd90be4dfa2ccbfd0b0a0 100644 --- a/tests/4.1-test_stats.py +++ b/tests/4.1-test_stats.py @@ -4,7 +4,7 @@ import sys import timeit import pandas as pd from mpi4py import MPI -from nes import * +from nes import open_netcdf comm = MPI.COMM_WORLD rank = comm.Get_rank() diff --git a/tests/4.2-test_sum.py b/tests/4.2-test_sum.py index 2fb9129e466ab838670838e040ba8eb19bea3a23..2f1a93c4de5100fe510f35cccd1826ddcd070ae0 100644 --- a/tests/4.2-test_sum.py +++ b/tests/4.2-test_sum.py @@ -5,7 +5,7 @@ from mpi4py import MPI import pandas as pd import timeit import numpy as np -from nes import * +from nes import create_nes comm = MPI.COMM_WORLD rank = comm.Get_rank() diff --git a/tests/4.3-test_write_timestep.py b/tests/4.3-test_write_timestep.py index 77539e7fe0549870d10adde647d58c4a31b9d767..b50c74b13eea96c00a9d4f9ac3a67e29ecceb58e 100644 --- a/tests/4.3-test_write_timestep.py +++ b/tests/4.3-test_write_timestep.py @@ -6,7 +6,7 @@ import pandas as pd import timeit from datetime import datetime, timedelta import numpy as np -from nes import * +from nes import create_nes comm = MPI.COMM_WORLD rank = comm.Get_rank() diff --git a/tests/test_bash_mn4.cmd b/tests/test_bash.mn4.sh similarity index 100% rename from tests/test_bash_mn4.cmd rename to tests/test_bash.mn4.sh diff --git a/tests/test_bash.mn5.sh b/tests/test_bash.mn5.sh new file mode 100644 index 0000000000000000000000000000000000000000..13e0a1b92542f3b8cdc5046e79d002b2e80f46a8 --- /dev/null +++ b/tests/test_bash.mn5.sh @@ -0,0 +1,46 @@ +#!/bin/bash +#SBATCH --qos=gp_debug +#SBATCH -A bsc32 +#SBATCH --cpus-per-task=1 +#SBATCH -n 4 +#SBATCH -t 00:10:00 +#SBATCH -J NES-test +#SBATCH --output=log_NES-tests_mn5_%j.out +#SBATCH --error=log_NES-tests_mn5_%j.err +#SBATCH --exclusive + +set -xuve + +module purge +module load anaconda +source /apps/GPP/ANACONDA/2023.07/etc/profile.d/conda.sh +conda deactivate +conda activate /gpfs/projects/bsc32/repository/apps/conda_envs/NES_dev +export PYTHONPATH=/gpfs/projects/bsc32/repository/apps/conda_envs/NES_dev/lib/python3.12/site-packages +export SLURM_CPU_BIND=none +export PYTHONPATH=/gpfs/scratch/bsc32/bsc032538/AC_PostProcess/NES:$PYTHONPATH + + +#conda activate /gpfs/projects/bsc32/repository/apps/conda_envs/NES_v1.1.4 +##export PYTHONPATH=/gpfs/projects/bsc32/repository/apps/conda_envs/NES_v1.1.4/lib/python3.12/site-packages:$PYTHONPATH +#export PYTHONPATH=/gpfs/projects/bsc32/repository/apps/conda_envs/NES_v1.1.4/lib/python3.12/site-packages +#export SLURM_CPU_BIND=none + +cd /gpfs/scratch/bsc32/bsc032538/AC_PostProcess/NES/tests || exit + +mpirun -np 4 python 1.1-test_read_write_projection.py +mpirun -np 4 python 1.2-test_create_projection.py +mpirun -np 4 python 1.3-test_selecting.py + +mpirun -np 4 python 2.1-test_spatial_join.py +mpirun -np 4 python 2.2-test_create_shapefile.py +mpirun -np 4 python 2.3-test_bounds.py +mpirun -np 4 python 2.4-test_cell_area.py + +mpirun -np 4 python 3.1-test_vertical_interp.py +mpirun -np 4 python 3.2-test_horiz_interp_bilinear.py +mpirun -np 4 python 3.3-test_horiz_interp_conservative.py + +mpirun -np 4 python 4.1-test_stats.py +mpirun -np 4 python 4.2-test_sum.py +mpirun -np 4 python 4.3-test_write_timestep.py diff --git a/tests/test_bash_nord3v2.cmd b/tests/test_bash.nord3v2.sh similarity index 100% rename from tests/test_bash_nord3v2.cmd rename to tests/test_bash.nord3v2.sh diff --git a/tests/unit/test_imports.py b/tests/unit/test_imports.py index b851d90524f417e34a02deaae85ab1fd4486d5af..346ebadcddcb721858236fa969b52fca9a464140 100644 --- a/tests/unit/test_imports.py +++ b/tests/unit/test_imports.py @@ -7,7 +7,7 @@ class TestImports(unittest.TestCase): 'sys', 'os', 'time', 'timeit', 'math', 'calendar', 'datetime', 'warnings', 'geopandas', 'pandas', 'numpy', 'shapely', 'mpi4py', 'netCDF4', 'pyproj', 'configargparse', 'filelock', - 'pytz', 'eccodes'] + 'eccodes'] for module_name in imports_to_test: with self.subTest(module=module_name): @@ -101,13 +101,6 @@ class TestImports(unittest.TestCase): except ImportError as e: self.fail(f"Import error: {e}") - def test_pytz(self): - try: - import pytz - print("pytz: ", pytz.__version__) - except ImportError as e: - self.fail(f"Import error: {e}") - if __name__ == '__main__': unittest.main() diff --git a/tutorials/2.Creation/2.3.Create_Points_XVPCA.ipynb b/tutorials/2.Creation/2.3.Create_Points_XVPCA.ipynb index f5eca197963d045397a140068419b20ee32b64f1..1055297795b08fddefe9cd26f3c2df5be4f5b38c 100644 --- a/tutorials/2.Creation/2.3.Create_Points_XVPCA.ipynb +++ b/tutorials/2.Creation/2.3.Create_Points_XVPCA.ipynb @@ -528,6 +528,612 @@ "df_pm10" ] }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "df_pm10 = df_pm10.set_index('Estació')\n", + "df_pm10.iloc[1] = df_pm10.columns\n", + "df_pm10.columns = df_pm10.iloc[0]\n", + "df_pm10 = df_pm10[1:]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Codi europeuES1438AES1928AES2027AES0559AES0691AES0567AES1870AES1852AES1900AES1843A...ES1123AES1117AES2033AES2011AES1982AES1778AES1680AES1362AES9994AES1874A
Estació
DiaPM10 Barcelona (Eixample)PM10 Badalona (guàrdia urbana)PM10 Badalona (Assamblea de Catalunya)PM10 Barcelona (Pl. de la Universitat)PM10 Barcelona (Poblenou)PM10 Barcelona (Zona Universitària)PM10 Barcelona (el Port Vell)PM10 Barcelona (IES Goya)PM10 Barcelona (IES Verdaguer)PM10 Berga (IES Guillem de Berguedà)...PM10 Constantí (Gaudí)PM10 Vila-seca (RENFE)PM10 Sitges (Vallcarca-oficines)PM10 Sant Vicenç dels Horts (Àlaba)PM10 Montsec (OAM)PM10 Montseny (la Castanya)PM10 Caldes de Montbui (Ajuntament)PM10 Sant Feliu de Llobregat (Eugeni d'Ors)PM 10 La Seu d'Urgell (CC Les Monges)PM10 Vic (Centre Cívic Santa Anna)
2017-01-01 00:00:0019.6NaN2020.225.616.529NaN23.8NaN...12.97NaN1122.499.5002997.936455NaNNaNNaNNaN
2017-01-02 00:00:0027.220.862331.63522.82817.232.4NaN...NaN25.382625.391.8296189.7870043222.06NaNNaN
2017-01-03 00:00:0035.7NaN323736.230.931NaN35.8NaN...21.836.494830.658.09460716.978294335.84NaNNaN
2017-01-04 00:00:0030.934.49353239.326.83230.339.4NaN...29.1734.334739.544.37736812.56556NaNNaNNaNNaN
..................................................................
2017-12-27 00:00:0017.57.591016.91413.121NaN20.8NaN...1222.95NaNNaN13.066751NaN10.3NaNNaN
2017-12-28 00:00:0017NaN1417.915NaN1314.516NaN...NaN6.5NaN9.97613.351872NaN26.81NaNNaN
2017-12-29 00:00:0024.6212423.225.815.321NaN25.9NaN...8.869.56NaN23.7614.219732NaN14.09NaNNaN
2017-12-30 00:00:0027.4NaN1522.316.611.21610.718.8NaN...NaNNaNNaN19.041.0911874.713029NaNNaNNaNNaN
2017-12-31 00:00:0017.312.51316.317.69.914NaN17.4NaN...12.77NaNNaN15.232.156595.024302NaNNaNNaNNaN
\n", + "

366 rows × 83 columns

\n", + "
" + ], + "text/plain": [ + "Codi europeu ES1438A \\\n", + "Estació \n", + "Dia PM10 Barcelona (Eixample) \n", + "2017-01-01 00:00:00 19.6 \n", + "2017-01-02 00:00:00 27.2 \n", + "2017-01-03 00:00:00 35.7 \n", + "2017-01-04 00:00:00 30.9 \n", + "... ... \n", + "2017-12-27 00:00:00 17.5 \n", + "2017-12-28 00:00:00 17 \n", + "2017-12-29 00:00:00 24.6 \n", + "2017-12-30 00:00:00 27.4 \n", + "2017-12-31 00:00:00 17.3 \n", + "\n", + "Codi europeu ES1928A \\\n", + "Estació \n", + "Dia PM10 Badalona (guàrdia urbana) \n", + "2017-01-01 00:00:00 NaN \n", + "2017-01-02 00:00:00 20.86 \n", + "2017-01-03 00:00:00 NaN \n", + "2017-01-04 00:00:00 34.49 \n", + "... ... \n", + "2017-12-27 00:00:00 7.59 \n", + "2017-12-28 00:00:00 NaN \n", + "2017-12-29 00:00:00 21 \n", + "2017-12-30 00:00:00 NaN \n", + "2017-12-31 00:00:00 12.5 \n", + "\n", + "Codi europeu ES2027A \\\n", + "Estació \n", + "Dia PM10 Badalona (Assamblea de Catalunya) \n", + "2017-01-01 00:00:00 20 \n", + "2017-01-02 00:00:00 23 \n", + "2017-01-03 00:00:00 32 \n", + "2017-01-04 00:00:00 35 \n", + "... ... \n", + "2017-12-27 00:00:00 10 \n", + "2017-12-28 00:00:00 14 \n", + "2017-12-29 00:00:00 24 \n", + "2017-12-30 00:00:00 15 \n", + "2017-12-31 00:00:00 13 \n", + "\n", + "Codi europeu ES0559A \\\n", + "Estació \n", + "Dia PM10 Barcelona (Pl. de la Universitat) \n", + "2017-01-01 00:00:00 20.2 \n", + "2017-01-02 00:00:00 31.6 \n", + "2017-01-03 00:00:00 37 \n", + "2017-01-04 00:00:00 32 \n", + "... ... \n", + "2017-12-27 00:00:00 16.9 \n", + "2017-12-28 00:00:00 17.9 \n", + "2017-12-29 00:00:00 23.2 \n", + "2017-12-30 00:00:00 22.3 \n", + "2017-12-31 00:00:00 16.3 \n", + "\n", + "Codi europeu ES0691A \\\n", + "Estació \n", + "Dia PM10 Barcelona (Poblenou) \n", + "2017-01-01 00:00:00 25.6 \n", + "2017-01-02 00:00:00 35 \n", + "2017-01-03 00:00:00 36.2 \n", + "2017-01-04 00:00:00 39.3 \n", + "... ... \n", + "2017-12-27 00:00:00 14 \n", + "2017-12-28 00:00:00 15 \n", + "2017-12-29 00:00:00 25.8 \n", + "2017-12-30 00:00:00 16.6 \n", + "2017-12-31 00:00:00 17.6 \n", + "\n", + "Codi europeu ES0567A \\\n", + "Estació \n", + "Dia PM10 Barcelona (Zona Universitària) \n", + "2017-01-01 00:00:00 16.5 \n", + "2017-01-02 00:00:00 22.8 \n", + "2017-01-03 00:00:00 30.9 \n", + "2017-01-04 00:00:00 26.8 \n", + "... ... \n", + "2017-12-27 00:00:00 13.1 \n", + "2017-12-28 00:00:00 NaN \n", + "2017-12-29 00:00:00 15.3 \n", + "2017-12-30 00:00:00 11.2 \n", + "2017-12-31 00:00:00 9.9 \n", + "\n", + "Codi europeu ES1870A ES1852A \\\n", + "Estació \n", + "Dia PM10 Barcelona (el Port Vell) PM10 Barcelona (IES Goya) \n", + "2017-01-01 00:00:00 29 NaN \n", + "2017-01-02 00:00:00 28 17.2 \n", + "2017-01-03 00:00:00 31 NaN \n", + "2017-01-04 00:00:00 32 30.3 \n", + "... ... ... \n", + "2017-12-27 00:00:00 21 NaN \n", + "2017-12-28 00:00:00 13 14.5 \n", + "2017-12-29 00:00:00 21 NaN \n", + "2017-12-30 00:00:00 16 10.7 \n", + "2017-12-31 00:00:00 14 NaN \n", + "\n", + "Codi europeu ES1900A \\\n", + "Estació \n", + "Dia PM10 Barcelona (IES Verdaguer) \n", + "2017-01-01 00:00:00 23.8 \n", + "2017-01-02 00:00:00 32.4 \n", + "2017-01-03 00:00:00 35.8 \n", + "2017-01-04 00:00:00 39.4 \n", + "... ... \n", + "2017-12-27 00:00:00 20.8 \n", + "2017-12-28 00:00:00 16 \n", + "2017-12-29 00:00:00 25.9 \n", + "2017-12-30 00:00:00 18.8 \n", + "2017-12-31 00:00:00 17.4 \n", + "\n", + "Codi europeu ES1843A ... \\\n", + "Estació ... \n", + "Dia PM10 Berga (IES Guillem de Berguedà) ... \n", + "2017-01-01 00:00:00 NaN ... \n", + "2017-01-02 00:00:00 NaN ... \n", + "2017-01-03 00:00:00 NaN ... \n", + "2017-01-04 00:00:00 NaN ... \n", + "... ... ... \n", + "2017-12-27 00:00:00 NaN ... \n", + "2017-12-28 00:00:00 NaN ... \n", + "2017-12-29 00:00:00 NaN ... \n", + "2017-12-30 00:00:00 NaN ... \n", + "2017-12-31 00:00:00 NaN ... \n", + "\n", + "Codi europeu ES1123A ES1117A \\\n", + "Estació \n", + "Dia PM10 Constantí (Gaudí) PM10 Vila-seca (RENFE) \n", + "2017-01-01 00:00:00 12.97 NaN \n", + "2017-01-02 00:00:00 NaN 25.38 \n", + "2017-01-03 00:00:00 21.8 36.49 \n", + "2017-01-04 00:00:00 29.17 34.33 \n", + "... ... ... \n", + "2017-12-27 00:00:00 12 22.95 \n", + "2017-12-28 00:00:00 NaN 6.5 \n", + "2017-12-29 00:00:00 8.86 9.56 \n", + "2017-12-30 00:00:00 NaN NaN \n", + "2017-12-31 00:00:00 12.77 NaN \n", + "\n", + "Codi europeu ES2033A \\\n", + "Estació \n", + "Dia PM10 Sitges (Vallcarca-oficines) \n", + "2017-01-01 00:00:00 11 \n", + "2017-01-02 00:00:00 26 \n", + "2017-01-03 00:00:00 48 \n", + "2017-01-04 00:00:00 47 \n", + "... ... \n", + "2017-12-27 00:00:00 NaN \n", + "2017-12-28 00:00:00 NaN \n", + "2017-12-29 00:00:00 NaN \n", + "2017-12-30 00:00:00 NaN \n", + "2017-12-31 00:00:00 NaN \n", + "\n", + "Codi europeu ES2011A ES1982A \\\n", + "Estació \n", + "Dia PM10 Sant Vicenç dels Horts (Àlaba) PM10 Montsec (OAM) \n", + "2017-01-01 00:00:00 22.49 9.500299 \n", + "2017-01-02 00:00:00 25.39 1.829618 \n", + "2017-01-03 00:00:00 30.65 8.094607 \n", + "2017-01-04 00:00:00 39.54 4.377368 \n", + "... ... ... \n", + "2017-12-27 00:00:00 NaN 1 \n", + "2017-12-28 00:00:00 9.976 1 \n", + "2017-12-29 00:00:00 23.76 1 \n", + "2017-12-30 00:00:00 19.04 1.091187 \n", + "2017-12-31 00:00:00 15.23 2.15659 \n", + "\n", + "Codi europeu ES1778A \\\n", + "Estació \n", + "Dia PM10 Montseny (la Castanya) \n", + "2017-01-01 00:00:00 7.936455 \n", + "2017-01-02 00:00:00 9.787004 \n", + "2017-01-03 00:00:00 16.97829 \n", + "2017-01-04 00:00:00 12.56556 \n", + "... ... \n", + "2017-12-27 00:00:00 3.066751 \n", + "2017-12-28 00:00:00 3.351872 \n", + "2017-12-29 00:00:00 4.219732 \n", + "2017-12-30 00:00:00 4.713029 \n", + "2017-12-31 00:00:00 5.024302 \n", + "\n", + "Codi europeu ES1680A \\\n", + "Estació \n", + "Dia PM10 Caldes de Montbui (Ajuntament) \n", + "2017-01-01 00:00:00 NaN \n", + "2017-01-02 00:00:00 32 \n", + "2017-01-03 00:00:00 43 \n", + "2017-01-04 00:00:00 NaN \n", + "... ... \n", + "2017-12-27 00:00:00 NaN \n", + "2017-12-28 00:00:00 NaN \n", + "2017-12-29 00:00:00 NaN \n", + "2017-12-30 00:00:00 NaN \n", + "2017-12-31 00:00:00 NaN \n", + "\n", + "Codi europeu ES1362A \\\n", + "Estació \n", + "Dia PM10 Sant Feliu de Llobregat (Eugeni d'Ors) \n", + "2017-01-01 00:00:00 NaN \n", + "2017-01-02 00:00:00 22.06 \n", + "2017-01-03 00:00:00 35.84 \n", + "2017-01-04 00:00:00 NaN \n", + "... ... \n", + "2017-12-27 00:00:00 10.3 \n", + "2017-12-28 00:00:00 26.81 \n", + "2017-12-29 00:00:00 14.09 \n", + "2017-12-30 00:00:00 NaN \n", + "2017-12-31 00:00:00 NaN \n", + "\n", + "Codi europeu ES9994A \\\n", + "Estació \n", + "Dia PM 10 La Seu d'Urgell (CC Les Monges) \n", + "2017-01-01 00:00:00 NaN \n", + "2017-01-02 00:00:00 NaN \n", + "2017-01-03 00:00:00 NaN \n", + "2017-01-04 00:00:00 NaN \n", + "... ... \n", + "2017-12-27 00:00:00 NaN \n", + "2017-12-28 00:00:00 NaN \n", + "2017-12-29 00:00:00 NaN \n", + "2017-12-30 00:00:00 NaN \n", + "2017-12-31 00:00:00 NaN \n", + "\n", + "Codi europeu ES1874A \n", + "Estació \n", + "Dia PM10 Vic (Centre Cívic Santa Anna) \n", + "2017-01-01 00:00:00 NaN \n", + "2017-01-02 00:00:00 NaN \n", + "2017-01-03 00:00:00 NaN \n", + "2017-01-04 00:00:00 NaN \n", + "... ... \n", + "2017-12-27 00:00:00 NaN \n", + "2017-12-28 00:00:00 NaN \n", + "2017-12-29 00:00:00 NaN \n", + "2017-12-30 00:00:00 NaN \n", + "2017-12-31 00:00:00 NaN \n", + "\n", + "[366 rows x 83 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_pm10" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -537,7 +1143,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -680,7 +1286,7 @@ "[134 rows x 4 columns]" ] }, - "execution_count": 4, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -702,42 +1308,7 @@ }, { "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array(['ES1438A', 'ES1928A', 'ES2027A', 'ES0559A', 'ES0691A', 'ES0567A',\n", - " 'ES1870A', 'ES1852A', 'ES1900A', 'ES1843A', 'ES1839A', 'ES0963A',\n", - " 'ES0266A', 'ES1891A', 'ES1135A', 'ES1397A', 'ES0392A', 'ES1861A',\n", - " 'ES1126A', 'ES1390A', 'ES1413A', 'ES0586A', 'ES1931A', 'ES0704A',\n", - " 'ES2012A', 'ES1871A', 'ES1929A', 'ES1895A', 'ES0971A', 'ES1231A',\n", - " 'ES1453A', 'ES0700A', 'ES1663A', 'ES1775A', 'ES1018A', 'ES1842A',\n", - " 'ES1887A', 'ES1936A', 'ES0991A', 'ES1999A', 'ES2034A', 'ES1588A',\n", - " 'ES2009A', 'ES2017A', 'ES1909A', 'ES0977A', 'ES1754A', 'ES1120A',\n", - " 'ES2071A', 'ES1855A', 'ES1841A', 'ES1814A', 'ES1965A', 'ES2079A',\n", - " 'ES1559A', 'ES1665A', 'ES1220A', 'ES1555A', 'ES1248A', 'ES1896A',\n", - " 'ES1480A', 'ES1856A', 'ES1851A', 'ES1964A', 'ES1225A', 'ES1312A',\n", - " 'ES1262A', 'ES1910A', 'ES1983A', 'ES1899A', 'ES1903A', 'ES1396A',\n", - " 'ES1348A', 'ES1123A', 'ES1117A', 'ES2033A', 'ES2011A', 'ES1982A',\n", - " 'ES1778A', 'ES1680A', 'ES1362A', 'ES9994A', 'ES1874A'],\n", - " dtype=object)" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "station_codes_pm10 = df_pm10.iloc[0].values[1:]\n", - "station_codes_pm10" - ] - }, - { - "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -760,48 +1331,174 @@ "\n", " \n", " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", - " \n", - " \n", - " \n", - " \n", " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", @@ -809,85 +1506,287 @@ " \n", " \n", " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", "
Codi europeuES0266AES0392AES0395AES0559AES0567AES0584AES0586AES0691AES0692AES0694A...ES2090AES0554AES0977AES1398AES1200AES2087AES2091AES2088AES1908AES9994A
Estacióstation.codelatlonstandardised_network_provided_area_classification
0ES0266A41.3793222.086140urban-centre
1ES0392A41.7277041.838531urban-suburbanDiaPM10 Esplugues de Llobregat (CEIP Isidre Martí)PM10 Manresa (CEIP La Font)NaNPM10 Barcelona (Pl. de la Universitat)PM10 Barcelona (Zona Universitària)NaNPM10 Molins de Rei (Ajuntament)PM10 Barcelona (Poblenou)NaNNaN...NaNNaNPM10 L'Arboç (CEIP Sant Julià)NaNNaNNaNNaNNaNNaNPM 10 La Seu d'Urgell (CC Les Monges)
2ES0559A41.3874242.164918urban-centre2017-01-01 00:00:00NaNNaNNaN20.216.5NaNNaN25.6NaNNaN...NaNNaN29.77NaNNaNNaNNaNNaNNaNNaN
3ES0567A41.3849062.119574urban-centre2017-01-02 00:00:00NaNNaNNaN31.622.8NaN23.8235NaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
4ES0586A41.4136212.015986urban-centre2017-01-03 00:00:00NaN27.63NaN3730.9NaN36.0736.2NaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
2017-01-04 00:00:00NaNNaNNaN3226.8NaN29.5939.3NaNNaN...NaNNaN28.28NaNNaNNaNNaNNaNNaNNaN
...............................................................
78ES2034A41.5441050.829933rural2017-12-27 00:00:00NaNNaNNaN16.913.1NaN6.6314NaNNaN...NaNNaN18.22NaNNaNNaNNaNNaNNaNNaN
79ES2071A41.1200641.254472urban-centre2017-12-28 00:00:00NaN11.91NaN17.9NaNNaN22.5615NaNNaN...NaNNaN2.74NaNNaNNaNNaNNaNNaNNaN
80ES2079A41.5596071.995963urban-suburban2017-12-29 00:00:00NaNNaNNaN23.215.3NaN16.1225.8NaNNaN...NaNNaN8.21NaNNaNNaNNaNNaNNaNNaN
81ES0977A41.6047221.6047222017-12-30 00:00:00NaN23.71NaN22.311.2NaNNaN16.6NaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
82ES9994A42.3583631.4594552017-12-31 00:00:00NaNNaNNaN16.39.9NaNNaN17.6NaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
\n", - "

83 rows × 4 columns

\n", + "

366 rows × 134 columns

\n", "" ], "text/plain": [ - " station.code lat lon \\\n", - "0 ES0266A 41.379322 2.086140 \n", - "1 ES0392A 41.727704 1.838531 \n", - "2 ES0559A 41.387424 2.164918 \n", - "3 ES0567A 41.384906 2.119574 \n", - "4 ES0586A 41.413621 2.015986 \n", - ".. ... ... ... \n", - "78 ES2034A 41.544105 0.829933 \n", - "79 ES2071A 41.120064 1.254472 \n", - "80 ES2079A 41.559607 1.995963 \n", - "81 ES0977A 41.604722 1.604722 \n", - "82 ES9994A 42.358363 1.459455 \n", + "Codi europeu ES0266A \\\n", + "Estació \n", + "Dia PM10 Esplugues de Llobregat (CEIP Isidre Martí) \n", + "2017-01-01 00:00:00 NaN \n", + "2017-01-02 00:00:00 NaN \n", + "2017-01-03 00:00:00 NaN \n", + "2017-01-04 00:00:00 NaN \n", + "... ... \n", + "2017-12-27 00:00:00 NaN \n", + "2017-12-28 00:00:00 NaN \n", + "2017-12-29 00:00:00 NaN \n", + "2017-12-30 00:00:00 NaN \n", + "2017-12-31 00:00:00 NaN \n", "\n", - " standardised_network_provided_area_classification \n", - "0 urban-centre \n", - "1 urban-suburban \n", - "2 urban-centre \n", - "3 urban-centre \n", - "4 urban-centre \n", - ".. ... \n", - "78 rural \n", - "79 urban-centre \n", - "80 urban-suburban \n", - "81 NaN \n", - "82 NaN \n", + "Codi europeu ES0392A ES0395A \\\n", + "Estació \n", + "Dia PM10 Manresa (CEIP La Font) NaN \n", + "2017-01-01 00:00:00 NaN NaN \n", + "2017-01-02 00:00:00 NaN NaN \n", + "2017-01-03 00:00:00 27.63 NaN \n", + "2017-01-04 00:00:00 NaN NaN \n", + "... ... ... \n", + "2017-12-27 00:00:00 NaN NaN \n", + "2017-12-28 00:00:00 11.91 NaN \n", + "2017-12-29 00:00:00 NaN NaN \n", + "2017-12-30 00:00:00 23.71 NaN \n", + "2017-12-31 00:00:00 NaN NaN \n", "\n", - "[83 rows x 4 columns]" + "Codi europeu ES0559A \\\n", + "Estació \n", + "Dia PM10 Barcelona (Pl. de la Universitat) \n", + "2017-01-01 00:00:00 20.2 \n", + "2017-01-02 00:00:00 31.6 \n", + "2017-01-03 00:00:00 37 \n", + "2017-01-04 00:00:00 32 \n", + "... ... \n", + "2017-12-27 00:00:00 16.9 \n", + "2017-12-28 00:00:00 17.9 \n", + "2017-12-29 00:00:00 23.2 \n", + "2017-12-30 00:00:00 22.3 \n", + "2017-12-31 00:00:00 16.3 \n", + "\n", + "Codi europeu ES0567A ES0584A \\\n", + "Estació \n", + "Dia PM10 Barcelona (Zona Universitària) NaN \n", + "2017-01-01 00:00:00 16.5 NaN \n", + "2017-01-02 00:00:00 22.8 NaN \n", + "2017-01-03 00:00:00 30.9 NaN \n", + "2017-01-04 00:00:00 26.8 NaN \n", + "... ... ... \n", + "2017-12-27 00:00:00 13.1 NaN \n", + "2017-12-28 00:00:00 NaN NaN \n", + "2017-12-29 00:00:00 15.3 NaN \n", + "2017-12-30 00:00:00 11.2 NaN \n", + "2017-12-31 00:00:00 9.9 NaN \n", + "\n", + "Codi europeu ES0586A \\\n", + "Estació \n", + "Dia PM10 Molins de Rei (Ajuntament) \n", + "2017-01-01 00:00:00 NaN \n", + "2017-01-02 00:00:00 23.82 \n", + "2017-01-03 00:00:00 36.07 \n", + "2017-01-04 00:00:00 29.59 \n", + "... ... \n", + "2017-12-27 00:00:00 6.63 \n", + "2017-12-28 00:00:00 22.56 \n", + "2017-12-29 00:00:00 16.12 \n", + "2017-12-30 00:00:00 NaN \n", + "2017-12-31 00:00:00 NaN \n", + "\n", + "Codi europeu ES0691A ES0692A ES0694A ... ES2090A \\\n", + "Estació ... \n", + "Dia PM10 Barcelona (Poblenou) NaN NaN ... NaN \n", + "2017-01-01 00:00:00 25.6 NaN NaN ... NaN \n", + "2017-01-02 00:00:00 35 NaN NaN ... NaN \n", + "2017-01-03 00:00:00 36.2 NaN NaN ... NaN \n", + "2017-01-04 00:00:00 39.3 NaN NaN ... NaN \n", + "... ... ... ... ... ... \n", + "2017-12-27 00:00:00 14 NaN NaN ... NaN \n", + "2017-12-28 00:00:00 15 NaN NaN ... NaN \n", + "2017-12-29 00:00:00 25.8 NaN NaN ... NaN \n", + "2017-12-30 00:00:00 16.6 NaN NaN ... NaN \n", + "2017-12-31 00:00:00 17.6 NaN NaN ... NaN \n", + "\n", + "Codi europeu ES0554A ES0977A ES1398A ES1200A \\\n", + "Estació \n", + "Dia NaN PM10 L'Arboç (CEIP Sant Julià) NaN NaN \n", + "2017-01-01 00:00:00 NaN 29.77 NaN NaN \n", + "2017-01-02 00:00:00 NaN NaN NaN NaN \n", + "2017-01-03 00:00:00 NaN NaN NaN NaN \n", + "2017-01-04 00:00:00 NaN 28.28 NaN NaN \n", + "... ... ... ... ... \n", + "2017-12-27 00:00:00 NaN 18.22 NaN NaN \n", + "2017-12-28 00:00:00 NaN 2.74 NaN NaN \n", + "2017-12-29 00:00:00 NaN 8.21 NaN NaN \n", + "2017-12-30 00:00:00 NaN NaN NaN NaN \n", + "2017-12-31 00:00:00 NaN NaN NaN NaN \n", + "\n", + "Codi europeu ES2087A ES2091A ES2088A ES1908A \\\n", + "Estació \n", + "Dia NaN NaN NaN NaN \n", + "2017-01-01 00:00:00 NaN NaN NaN NaN \n", + "2017-01-02 00:00:00 NaN NaN NaN NaN \n", + "2017-01-03 00:00:00 NaN NaN NaN NaN \n", + "2017-01-04 00:00:00 NaN NaN NaN NaN \n", + "... ... ... ... ... \n", + "2017-12-27 00:00:00 NaN NaN NaN NaN \n", + "2017-12-28 00:00:00 NaN NaN NaN NaN \n", + "2017-12-29 00:00:00 NaN NaN NaN NaN \n", + "2017-12-30 00:00:00 NaN NaN NaN NaN \n", + "2017-12-31 00:00:00 NaN NaN NaN NaN \n", + "\n", + "Codi europeu ES9994A \n", + "Estació \n", + "Dia PM 10 La Seu d'Urgell (CC Les Monges) \n", + "2017-01-01 00:00:00 NaN \n", + "2017-01-02 00:00:00 NaN \n", + "2017-01-03 00:00:00 NaN \n", + "2017-01-04 00:00:00 NaN \n", + "... ... \n", + "2017-12-27 00:00:00 NaN \n", + "2017-12-28 00:00:00 NaN \n", + "2017-12-29 00:00:00 NaN \n", + "2017-12-30 00:00:00 NaN \n", + "2017-12-31 00:00:00 NaN \n", + "\n", + "[366 rows x 134 columns]" ] }, - "execution_count": 6, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "df_stations_pm10 = df_stations[df_stations['station.code'].isin(station_codes_pm10)].reset_index(drop=True)\n", - "df_stations_pm10" + "df_pm10 = df_pm10.reindex(columns=list(df_stations['station.code']))\n", + "df_pm10" ] }, { @@ -908,18 +1807,18 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ - "times = df_pm10['Estació'].iloc[2:]\n", - "lat = df_stations_pm10['lat']\n", - "lon = df_stations_pm10['lon']" + "times = df_pm10.index[1:]\n", + "lat = df_stations['lat']\n", + "lon = df_stations['lon']" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -935,27 +1834,27 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ - "variables = {'station_name': {'data': df_pm10.columns.str.replace('PM10 ', '').str.replace('PM 10 ', '').to_numpy()[1:],\n", - " 'dimensions': ('station',),\n", - " 'dtype': str},\n", - " 'station_code': {'data': df_stations_pm10['station.code'],\n", + "variables = {'station_name': {'data': df_pm10.iloc[0].str.replace('PM10 ', '').str.replace('PM 10 ', '').to_numpy(),\n", + " 'dimensions': ('station',),\n", + " 'dtype': str},\n", + " 'station_code': {'data': df_pm10.columns,\n", " 'dimensions': ('station',),\n", " 'dtype': str},\n", - " 'area_classification': {'data': df_stations_pm10['standardised_network_provided_area_classification'],\n", + " 'area_classification': {'data': df_stations['standardised_network_provided_area_classification'],\n", " 'dimensions': ('station',),\n", " 'dtype': str},\n", - " 'pm10': {'data': df_pm10.iloc[2:, 1:].to_numpy().T,\n", - " 'dimensions': ('station', 'time',),\n", + " 'pm10': {'data': df_pm10.iloc[1:, :].to_numpy(),\n", + " 'dimensions': ('time', 'station',),\n", " 'dtype': float}}" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -971,7 +1870,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -1013,43 +1912,43 @@ " \n", " \n", " 2\n", - " POINT (2.16492 41.38742)\n", + " POINT (2.01460 41.56782)\n", " \n", " \n", " 3\n", - " POINT (2.11957 41.38491)\n", + " POINT (2.16492 41.38742)\n", " \n", " \n", " 4\n", - " POINT (2.01599 41.41362)\n", + " POINT (2.11957 41.38491)\n", " \n", " \n", " ...\n", " ...\n", " \n", " \n", - " 78\n", - " POINT (0.82993 41.54410)\n", + " 129\n", + " POINT (2.25730 41.92928)\n", " \n", " \n", - " 79\n", - " POINT (1.25447 41.12006)\n", + " 130\n", + " POINT (0.55350 40.57990)\n", " \n", " \n", - " 80\n", - " POINT (1.99596 41.55961)\n", + " 131\n", + " POINT (2.25065 41.77106)\n", " \n", " \n", - " 81\n", - " POINT (1.60472 41.60472)\n", + " 132\n", + " POINT (1.85656 41.23907)\n", " \n", " \n", - " 82\n", + " 133\n", " POINT (1.45945 42.35836)\n", " \n", " \n", "\n", - "

83 rows × 1 columns

\n", + "

134 rows × 1 columns

\n", "" ], "text/plain": [ @@ -1057,20 +1956,20 @@ "FID \n", "0 POINT (2.08614 41.37932)\n", "1 POINT (1.83853 41.72770)\n", - "2 POINT (2.16492 41.38742)\n", - "3 POINT (2.11957 41.38491)\n", - "4 POINT (2.01599 41.41362)\n", + "2 POINT (2.01460 41.56782)\n", + "3 POINT (2.16492 41.38742)\n", + "4 POINT (2.11957 41.38491)\n", ".. ...\n", - "78 POINT (0.82993 41.54410)\n", - "79 POINT (1.25447 41.12006)\n", - "80 POINT (1.99596 41.55961)\n", - "81 POINT (1.60472 41.60472)\n", - "82 POINT (1.45945 42.35836)\n", + "129 POINT (2.25730 41.92928)\n", + "130 POINT (0.55350 40.57990)\n", + "131 POINT (2.25065 41.77106)\n", + "132 POINT (1.85656 41.23907)\n", + "133 POINT (1.45945 42.35836)\n", "\n", - "[83 rows x 1 columns]" + "[134 rows x 1 columns]" ] }, - "execution_count": 11, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -1081,7 +1980,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -1124,7 +2023,7 @@ " POINT (2.08614 41.37932)\n", " ES0266A\n", " urban-centre\n", - " 19.600000\n", + " NaN\n", " \n", " \n", " 1\n", @@ -1135,24 +2034,24 @@ " \n", " \n", " 2\n", - " POINT (2.16492 41.38742)\n", - " ES0559A\n", + " POINT (2.01460 41.56782)\n", + " ES0395A\n", " urban-centre\n", - " 20.000000\n", + " NaN\n", " \n", " \n", " 3\n", - " POINT (2.11957 41.38491)\n", - " ES0567A\n", + " POINT (2.16492 41.38742)\n", + " ES0559A\n", " urban-centre\n", - " 20.200000\n", + " 20.2\n", " \n", " \n", " 4\n", - " POINT (2.01599 41.41362)\n", - " ES0586A\n", + " POINT (2.11957 41.38491)\n", + " ES0567A\n", " urban-centre\n", - " 25.600000\n", + " 16.5\n", " \n", " \n", " ...\n", @@ -1162,35 +2061,35 @@ " ...\n", " \n", " \n", - " 78\n", - " POINT (0.82993 41.54410)\n", - " ES2034A\n", - " rural\n", - " 7.936455\n", + " 129\n", + " POINT (2.25730 41.92928)\n", + " ES2087A\n", + " NaN\n", + " NaN\n", " \n", " \n", - " 79\n", - " POINT (1.25447 41.12006)\n", - " ES2071A\n", - " urban-centre\n", + " 130\n", + " POINT (0.55350 40.57990)\n", + " ES2091A\n", + " NaN\n", " NaN\n", " \n", " \n", - " 80\n", - " POINT (1.99596 41.55961)\n", - " ES2079A\n", - " urban-suburban\n", + " 131\n", + " POINT (2.25065 41.77106)\n", + " ES2088A\n", + " NaN\n", " NaN\n", " \n", " \n", - " 81\n", - " POINT (1.60472 41.60472)\n", - " ES0977A\n", + " 132\n", + " POINT (1.85656 41.23907)\n", + " ES1908A\n", " NaN\n", " NaN\n", " \n", " \n", - " 82\n", + " 133\n", " POINT (1.45945 42.35836)\n", " ES9994A\n", " NaN\n", @@ -1198,28 +2097,28 @@ " \n", " \n", "\n", - "

83 rows × 4 columns

\n", + "

134 rows × 4 columns

\n", "" ], "text/plain": [ - " geometry station_code area_classification pm10\n", - "FID \n", - "0 POINT (2.08614 41.37932) ES0266A urban-centre 19.600000\n", - "1 POINT (1.83853 41.72770) ES0392A urban-suburban NaN\n", - "2 POINT (2.16492 41.38742) ES0559A urban-centre 20.000000\n", - "3 POINT (2.11957 41.38491) ES0567A urban-centre 20.200000\n", - "4 POINT (2.01599 41.41362) ES0586A urban-centre 25.600000\n", - ".. ... ... ... ...\n", - "78 POINT (0.82993 41.54410) ES2034A rural 7.936455\n", - "79 POINT (1.25447 41.12006) ES2071A urban-centre NaN\n", - "80 POINT (1.99596 41.55961) ES2079A urban-suburban NaN\n", - "81 POINT (1.60472 41.60472) ES0977A NaN NaN\n", - "82 POINT (1.45945 42.35836) ES9994A NaN NaN\n", + " geometry station_code area_classification pm10\n", + "FID \n", + "0 POINT (2.08614 41.37932) ES0266A urban-centre NaN\n", + "1 POINT (1.83853 41.72770) ES0392A urban-suburban NaN\n", + "2 POINT (2.01460 41.56782) ES0395A urban-centre NaN\n", + "3 POINT (2.16492 41.38742) ES0559A urban-centre 20.2\n", + "4 POINT (2.11957 41.38491) ES0567A urban-centre 16.5\n", + ".. ... ... ... ...\n", + "129 POINT (2.25730 41.92928) ES2087A NaN NaN\n", + "130 POINT (0.55350 40.57990) ES2091A NaN NaN\n", + "131 POINT (2.25065 41.77106) ES2088A NaN NaN\n", + "132 POINT (1.85656 41.23907) ES1908A NaN NaN\n", + "133 POINT (1.45945 42.35836) ES9994A NaN NaN\n", "\n", - "[83 rows x 4 columns]" + "[134 rows x 4 columns]" ] }, - "execution_count": 12, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -1227,7 +2126,7 @@ "source": [ "nessy.shapefile['station_code'] = nessy.variables['station_code']['data']\n", "nessy.shapefile['area_classification'] = nessy.variables['area_classification']['data']\n", - "nessy.shapefile['pm10'] = pd.to_numeric(nessy.variables['pm10']['data'][:, 0]) # Transform from object to float\n", + "nessy.shapefile['pm10'] = pd.to_numeric(nessy.variables['pm10']['data'][0, :]) # Transform from object to float\n", "nessy.shapefile" ] }, @@ -1240,12 +2139,12 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "metadata": {}, "outputs": [ { "data": { - "image/png": "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9BKuuuohVq3qRElJIxo0WMYvfpFF584dA04tIomWDMMZlPZCAODukereyK0iQqQCe/fuZdmyZYwePZprr72WkpISmjdvXu7MiR/+sCVLlz7Ls89OZ926dVx//fWcd167gJKLiFTqbDP7JvrcgJzosWZniFTX7t27mTZtGllZWezZs4cFCxbw6quv8sorr1T62pycHH7zm5trPqSIhJqT+HUiqsrd4xJQRYSkNXdny5YtfPHFFyxbtoydO3cydOhQ6tSpA8Add9yh9RpEpIrCP8UzXtLjU0raO1QsfPnll3z++edEIqUzmYqKinj44YeZMWMG/fv3P1w8HJKRoT3qREQqoiJCUs7mzZt54403iEQiZGRkkJ2dTSQSoWnTpuTl5fHRRx8xcODAw9ffcsstAaYVkVSTJDdWxoWKCEkJa9euZeHChezdu5fZs2fz8ssvU7t27XKvbdGiRYLTiUi6UREhEnKff/45CxYs4KuvvuK8887j5ptvxsy4/fbbg44mIpIWVERI0lm1ahWvvvoqp556Kn379tUOlyISKo6FfnZGvKiIkKSxZ88ennzySfLz87nzzjuDjiMiUq5k2IArXtLjU0rSe/nll1mxYgVDhgzRLpgiIiGhIkJC6cCBAyxevJjWrVuzaNEiioqKuP/++4OOJSISE91YKVLDFixYwGeffUbjxo1ZtWoVjRs3Zv/+/WRkZFBcXEy3bt14/fXX2b59+xFTMkVEwkxTPEVq0Icffsi0adO47LLLuPXWWw+fLyoqYvXq1Zx11lmHV4nMz88PKqaIiFRCRYQkxMGDB1m7di2zZs3CzBgzZsx3rsnJyaFt27YBpBMRiR/HOFiingiR47Jx40YWLVrEz372M3JzcznnnHNYvnx50LFERGqWQySSHkWENgaQGrFo0SLGjRvHpk2byM3NpU+fPixZsiToWCIiEkfqiZC4W7RoEd27d+ebb77h1VdfpUePHjz//PNBxxIRSQh342AkPX69psenlIRxdxYtWsTUqVN57LHHqFu3LlOnTg06lohIwpQWEekxnKEiQqps2bJlLF26lG+//ZZIJEKnTp04//zzAZgxYwbXX389TZs2pVevXgEnFRGRmqQiQipUWFjIM888wz//+U9+8YtfULt2bbZt28ZPfvITbrnllsPTMJ944gkKCgro3bs3a9as4eqrrw44uYhIgBz1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aNmyJUOHDuX999+ne/fu5ObmMn78eM444wzq1q0bdDwRkfShdSIkGeXk5HDhhRfSsGFDMjIyuPLKK1myZEnQsURE0suhKZ7xeISciogUtmHDBpo2bRp0DBERSVEqIlLYihUraNGiRdAxRETSi9aJkGS3YcMGcnNzg44hIpKedE+EJKuCggKefvppbSEuIiI1Sj0RKeZf//oX77zzDqNGjQo6iohIekqj2RkqIlLEp59+yuzZs+nQoQP33ntv0HFERNJXGm3ApSIiya1du5aXXnqJ1q1bM2zYsKDjiIhIGlERkaT27dvHpEmTOPnkk9XzICISJodmZ6QBFRFJ6LnnnmPnzp0MGjRIq1GKiISN7omQILk7L730Enl5eWzbto29e/dSt25dLrroIv7yl7/Qq1cv8vPzg44pIiJpTkVEiLg706dPZ/HixfTs2ZPc3Fx++MMfctJJJ/H111/TqVMnXnnlFZo1axZ0VBERORb1REhN+eCDD1i7di2ZmZm4O8XFxRQWFpKRkUHXrl257rrrvvOaPn36MH78eBUQIiJhp9kZUlO2bdtGu3btcHcOHDiAmVGr1rH/GJYuXUpRURGXXHJJglKKiIhUTkVEAhUVFTF+/Hh69OjBwYMHycrKqvQ17k63bt3YtGlTAhKKiMhx0+wMiTd355FHHuH++++v0oyK4cOHM2LECHJycmownYiIxI1mZ0i8PfbYY8eckrl//34WLVrExIkT6devH61atWLFihX84x//4N///neC04qISLWpiJB4mjNnDl27duW00047fM7dGTJkCOeffz5ZWVlcc8015OfnM3ToUNq0acPOnTvZvXs3M2fODDC5iIhIxVRE1IB33nmPCRNmkZ1di44d/4vvfS+H888///D3x44dS1FREQMHDmTgwIGsXbuWFi1asGPHDvr37w+ULmedm5tL69atg/oYIiJSHZqdIdX14IPjmDBhF7t2XQFEePbZKVxwwdYjpm3Wq1ePoUOHAvDOO+9QUlJCRsZ/dmV/4YUXyM7O5tprr010fBERiQfdWClVtW3bNv7nf75k165DBUMWJSW/YseOv1JcXEzt2rUByMvLY8GCBXTt2hWAjIwMIpEIU6dOZcuWLXTv3p2zzz47oE8hIiISGxURcfS///s2Gzac9Z3zGzc257PPPqNNmzYA9OzZk/nz5zNlyhSysrI4ePAg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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -1276,12 +2175,12 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "metadata": {}, "outputs": [ { "data": { - "image/png": "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RKNGjahfvz5HjhyhXr16jB49mrCwMMxmM8eOHctsHxISknn1JygoiMjIyGzF6+eff+bJJ5/MnGe1cuXKt1wupX///phMJpo1a8aFCxdyzd+9e3cqV64MwPr161m/fj3BwcFA+pIs4eHhUrycye7du+natavRMYQQRcDP716Ohb+W56VDs9kbP79eBdq/m5sbdvvfy63cuAzKjUugQNblUq6/fuedd6hevTp//PEHdrsdi8WS+f0blzIxm81YrVZ27tzJiBEjAJg+fTpa62z7dXS5FCDXS5E359daM3ny5MxjO7syd8/r/PnzrF69WqZ/EqKUcHPzoU6dZ/JsU6fOMwV+3qt69erExMQQFxfHtWvXMi+75WT58uXY7XYiIiI4ceIEjRs3Jj4+nho1amAymfjyyy9vOcN827ZtCQsLIywsjL59+9KjRw8+/fRTkpLSi/PFixcLtFyKj48PiYm5D1rp2bMnn376KVeuXAHg7NmzmcukOKMyceZltVr55JNPSE1N5fbbb2f69OlGRxJCFKHrz3Hd/JyX2exd6Oe83N3defXVV2nbti316tWjSZMmubZt3LgxHTt25MKFC8yfPx+LxcKoUaN48MEHWb58OZ07d852tnYrvXr1IiwsjNatW+Ph4cF9993HzJkzWbx4MSNHjmTGjBmkpaXxyCOP0LJly1z3ExgYiJubGy1btmTIkCHZRln36NGDw4cP849//AOA8uXLs2jRIqddVbnUL4mSnJzMtGnTmDhxogyJF8LF5HdJlPQZNn7kWmoMnh5++Pn1khk2nIgsieKgnTt3smHDBqZMmSIjC4UoA9zcfPD3H3jrhsLlldriderUKXbv3s1//vMfo6MIIYQoYqVywIbWmoULFzJ69GijowghhCgGpbJ4rVq1isGDBxsdQwghRDEplZcNT5w4Qf/+/Y2OIYQoYQlWG2tjLnMhNY3qHu709qtEBTez0bFEMSh1xWvr1q3cc889RscQQpSwdyPPM/dUDEm2vx8o/s/xszx/ux9j695mYDJRHErdZcN9+/bRpk0bo2MIIUrQu5HnmX3yfJbCBZBkszP75HnejTxf7BmcZUmUoshRt25d/vrrryJKVDxKVfGKjo7G39/f6BhCiBKUYLUx91TeM0HMPRVDojXvmS0KwxWXFMnNrWYAcRalqnitXr1a7nUJUcasjbmc7YzrZkk2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\n", + "image/png": "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Hs2bN8+IjyIymZOTE++//74sgJoNJFvANE1rDjxQSp3UNK1+Eu+PAEzAyv84RR2llL+mae7ALk3TLiml9v97p2eFbQE87cSRis8g8og5c+aQL18+Bg8enOEz0IeHh+Pl5cVXX30FQEBAAG5ublhZWfH9998DEBERwfjx47GyssJsNsf3UDSZTDg5OaVpVefx48czffp0Pv30UylgWeTRo0fp7r1669YtGjRokEGJRJoppZ77B5gI3AVuAgFAJLDi2XsfA0cAh+TO82x/H+CL5ParXr26EiIpt27dUl27ds3w844cOVIFBgam+fi4uDg1YMAAdezYsRQf88UXX6izZ8+qsWPHpuvaImWuX7+uevfurT744AN1/vx5pZRSZrNZhYSEqLi4OKWUUjExMWrbtm3KbDY/91yxsbFqz549av78+erMmTOZnj03A06oFNSPpP4kewemlBrGsw4az+7AvlBKddY0rTEwFKinlIpM6lhN0/IBBqVU+LOv3wXGpLHWCkHJkiUpXLhwmo9ft24dr7/+evy0UIsXL+bhw4cEBQXh5uaW5vNaWVnx7bff0rJlS3766adkB79aLBasra3x8vLi999/5+HDh7i7u6f5+uJ/zp8/j7Ozc6JHEb6+vsyePRuA/v37U6NGDfbs2UPFihW5f/8+cXFx5MuXj7p16/LVV18xYcKE/7yGtbU1DRo0oF69evz444+89NJLmfqZRNLSM5B5DmDL02ZBgKNKqZ6aphUDFimlmgKFgY3P3rcCVimldqQzs8jjatSowcyZM+nfv3+K9l+xYgUdO3Zk4sSJeHp6smzZMgwGA0FBQbRr144aNTKuc+z8+fP54osvsLe3x8nJKb5J8t/2799P9erVAejVqxfdunWjT58+1KxZM8Oy5CWPHz/m9u3beHl54evri6OjI5qm4erqSv/+/QkLC8Pa2jr+eeXMmTPZt28f33//ffy2sLAwnJ2dATh8+HCKrmswGFi2bBlt27bF1tY2cz6c+E8ykFnkODNnzqRWrVq89tprye67evVqgoKCOHv2LIUKFcrQXovJmT17NtWqVaNUqVJ4eHgQFhbGF198wZgxY4iKimLChAm8/fbbtG/fHoCJEydStmxZ2rVrl2UZc4thw4ZRrFgxbt++zbvvvss777wDPJ0A2tfXF4PBwJAhQ1I8IfPGjRuJjo7mgw8+SHbf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\n", "text/plain": [ "
" ] @@ -1318,7 +2217,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -1327,20 +2226,20 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/esarchive/scratch/avilanova/software/NES/nes/nc_projections/points_nes.py:361: UserWarning: WARNING!!! Different data types for variable station_name. Input dtype=. Data dtype=object.\n", + "/esarchive/scratch/avilanova/software/NES/nes/nc_projections/points_nes.py:357: UserWarning: WARNING!!! Different data types for variable station_name. Input dtype=. Data dtype=object.\n", " warnings.warn(msg)\n", - "/esarchive/scratch/avilanova/software/NES/nes/nc_projections/points_nes.py:361: UserWarning: WARNING!!! Different data types for variable station_code. Input dtype=. Data dtype=object.\n", + "/esarchive/scratch/avilanova/software/NES/nes/nc_projections/points_nes.py:357: UserWarning: WARNING!!! Different data types for variable station_code. Input dtype=. Data dtype=object.\n", " warnings.warn(msg)\n", - "/esarchive/scratch/avilanova/software/NES/nes/nc_projections/points_nes.py:361: UserWarning: WARNING!!! Different data types for variable area_classification. Input dtype=. Data dtype=object.\n", + "/esarchive/scratch/avilanova/software/NES/nes/nc_projections/points_nes.py:357: UserWarning: WARNING!!! Different data types for variable area_classification. Input dtype=. Data dtype=object.\n", " warnings.warn(msg)\n", - "/esarchive/scratch/avilanova/software/NES/nes/nc_projections/points_nes.py:361: UserWarning: WARNING!!! Different data types for variable pm10. Input dtype=. Data dtype=object.\n", + "/esarchive/scratch/avilanova/software/NES/nes/nc_projections/points_nes.py:357: UserWarning: WARNING!!! Different data types for variable pm10. Input dtype=. Data dtype=object.\n", " warnings.warn(msg)\n" ] }, diff --git a/tutorials/Jupyter_bash_nord3v2.cmd b/tutorials/Jupyter_bash_nord3v2.cmd deleted file mode 100644 index c6a91fa647fe01afbb9668e1d82ebcb6f2d4d740..0000000000000000000000000000000000000000 --- a/tutorials/Jupyter_bash_nord3v2.cmd +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -#SBATCH --ntasks 1 -#SBATCH --time 02:00:00 -#SBATCH --job-name NES-tutorial -#SBATCH --output log_NES-tutorial-%J.out -#SBATCH --error log_NES-tutorial-%J.err -#SBATCH --exclusive -#SBATCH --qos debug - -# get tunneling info -XDG_RUNTIME_DIR="" -port=$(shuf -i8000-9999 -n1) -node=$(hostname -s) -user=$(whoami) - -# print tunneling instructions jupyter-log -echo -e " - -MacOS or linux terminal command to create your ssh tunnel -ssh -N -L ${port}:${node}:${port} ${user}@nord4.bsc.es - -Use a Browser on your local machine to go to: -localhost:${port} (prefix w/ https:// if using password) -" - -# load modules or conda environments here -module load jupyterlab/3.0.9-foss-2019b-Python-3.7.4 -module load NES/1.1.3-nord3-v2-foss-2019b-Python-3.7.4 - - -# DON'T USE ADDRESS BELOW. -# DO USE TOKEN BELOW -jupyter-lab --no-browser --port=${port} --ip=${node}