From ed446bbdea452fa1c7b59783b1812126d722a1aa Mon Sep 17 00:00:00 2001 From: Carles Tena Date: Tue, 4 Jun 2024 15:15:58 +0200 Subject: [PATCH 01/11] Added Jupyter Lab functionalities --- environment.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index 334f31a..bc32357 100755 --- a/environment.yml +++ b/environment.yml @@ -1,6 +1,6 @@ --- -name: NES_v1.1.4 +name: NES_v1.1.X channels: - conda-forge @@ -16,4 +16,6 @@ dependencies: - eccodes - python-eccodes - filelock - - configargparse \ No newline at end of file + - configargparse + - jupyter + - ipykernel \ No newline at end of file -- GitLab From 0439231c02e62d04bcc2d004d1593b1d38317b29 Mon Sep 17 00:00:00 2001 From: Carles Tena Date: Thu, 13 Jun 2024 15:33:35 +0200 Subject: [PATCH 02/11] Base-tests OK --- environment.yml | 3 -- tests/{test_bash_mn4.cmd => test_bash.mn4.sh} | 0 tests/test_bash.mn5.sh | 46 +++++++++++++++++++ ..._bash_nord3v2.cmd => test_bash.nord3v2.sh} | 0 4 files changed, 46 insertions(+), 3 deletions(-) rename tests/{test_bash_mn4.cmd => test_bash.mn4.sh} (100%) create mode 100644 tests/test_bash.mn5.sh rename tests/{test_bash_nord3v2.cmd => test_bash.nord3v2.sh} (100%) diff --git a/environment.yml b/environment.yml index bc32357..d2a1892 100755 --- a/environment.yml +++ b/environment.yml @@ -1,6 +1,3 @@ ---- - -name: NES_v1.1.X channels: - conda-forge 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 0000000..9cf3ce2 --- /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 02:00: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 -- GitLab From 4e89f1372388e8556a1644548b15b7c234695c69 Mon Sep 17 00:00:00 2001 From: Carles Tena Date: Thu, 13 Jun 2024 18:23:28 +0200 Subject: [PATCH 03/11] Refactor code --- nes/create_nes.py | 71 +- nes/load_nes.py | 108 +- nes/methods/cell_measures.py | 120 +- nes/methods/horizontal_interpolation.py | 309 ++-- nes/methods/spatial_join.py | 125 +- nes/methods/vertical_interpolation.py | 180 +- nes/nc_projections/default_nes.py | 1701 +++++++++--------- nes/nc_projections/latlon_nes.py | 185 +- nes/nc_projections/lcc_nes.py | 332 ++-- nes/nc_projections/mercator_nes.py | 307 ++-- nes/nc_projections/points_nes.py | 394 ++-- nes/nc_projections/points_nes_ghost.py | 596 +++--- nes/nc_projections/points_nes_providentia.py | 369 ++-- nes/nc_projections/rotated_nes.py | 331 ++-- nes/nc_projections/rotated_nested_nes.py | 78 +- requirements.txt | 20 +- tests/unit/test_imports.py | 9 +- 17 files changed, 2657 insertions(+), 2578 deletions(-) diff --git a/nes/create_nes.py b/nes/create_nes.py index 98f81f0..2fbfdac 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 * -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): """ @@ -21,11 +20,13 @@ def create_nes(comm=None, info=False, projection=None, parallel_method='Y', bala 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']. + 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. + times : List[datetime] + List of Date times avoid_first_hours : int Number of hours to remove from first time steps. avoid_last_hours : int @@ -43,8 +44,8 @@ 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: @@ -57,66 +58,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 +128,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 +141,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 b300869..588c5bf 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 warnings import warn from .nc_projections import * -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. + A List of Nes objects or list of paths to concatenate. 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. 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/cell_measures.py b/nes/methods/cell_measures.py index 5288a02..bfae6a4 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 @@ -19,33 +18,33 @@ def calculate_grid_area(self): 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,26 +68,27 @@ 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) + 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 @@ -96,8 +96,8 @@ def calculate_geometry_area(geometry_list, earth_radius_minor_axis=6356752.3142, # 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) + 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 @@ -112,10 +112,10 @@ def calculate_cell_area(grid_corner_lon, grid_corner_lat, 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,7 +124,7 @@ 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]) @@ -138,13 +138,13 @@ def mod_huiliers_area(cell_corner_lon, cell_corner_lat): 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) @@ -164,13 +164,13 @@ def mod_huiliers_area(cell_corner_lon, cell_corner_lat): 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): @@ -180,33 +180,33 @@ def tri_area(point_0, point_1, point_2): 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) + 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) + 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) + 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 @@ -217,10 +217,10 @@ def cross_product(a, b): 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], @@ -234,11 +234,11 @@ def norm(cp): 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): @@ -247,19 +247,19 @@ def lon_lat_to_cartesian(lon, lat, earth_radius_major_axis=6378137.0): 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 006f5ff..5a20bac 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 @@ -53,10 +52,10 @@ def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind='Neares if info and self.master: print("Creating Weight Matrix") # Obtain weight matrix - if self.parallel_method == 'T': + 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']: + 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: @@ -68,14 +67,14 @@ def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind='Neares # 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() @@ -101,28 +100,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,15 +143,15 @@ 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 @@ -165,37 +164,36 @@ def get_src_data(comm, var_data, idx, parallel_method): ---------- comm : MPI.Communicator. 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 @@ -214,7 +212,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 @@ -242,7 +240,7 @@ def get_weights_idx_t_axis(self, dst_grid, weight_matrix_path, kind, n_neighbour 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() @@ -282,12 +280,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 @@ -312,7 +310,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 @@ -335,7 +333,7 @@ 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)): @@ -346,7 +344,7 @@ def get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighbou 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() @@ -382,12 +380,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 +396,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. @@ -420,7 +418,7 @@ 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. + A Communicator to read the weight matrix. parallel_method : str Nes parallel method to read the weight matrix. @@ -434,11 +432,11 @@ 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 @@ -470,24 +468,24 @@ def create_nn_weight_matrix(self, dst_grid, n_neighbours=4, wm_path=None, info=F 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._lat["data"], dtype=float32) + src_lon = array(self._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._lat["data"], dtype=float32) + dst_lon = array(dst_grid._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._lat["data"], axis=0) + dst_lon = expand_dims(dst_grid._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 @@ -510,7 +508,7 @@ def create_nn_weight_matrix(self, dst_grid, n_neighbours=4, wm_path=None, info=F 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)] @@ -525,14 +523,14 @@ 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._lev = {"data": arange(inverse_dists_transf.shape[1]), "units": ""} if wm_path is not None: weight_matrix.to_netcdf(wm_path) @@ -566,49 +564,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 +617,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,13 +642,13 @@ 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)] @@ -665,36 +662,36 @@ 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],) + 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],) + 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],) - 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)) @@ -713,22 +710,22 @@ 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): @@ -737,25 +734,25 @@ def lon_lat_to_cartesian_ecef(lon, lat): 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 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 gridcell 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 d647ebb..b6b1a49 100644 --- a/nes/methods/spatial_join.py +++ b/nes/methods/spatial_join.py @@ -1,12 +1,11 @@ #!/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 GeoDataFrame, sjoin_nearest, sjoin, read_file +from pandas import DataFrame +from numpy import array, uint32, nan from shapely.errors import TopologicalError @@ -17,11 +16,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 @@ -46,19 +45,19 @@ def spatial_join(self, ext_shp, method=None, var_list=None, info=False, apply_bb 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': + 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': + 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) 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 @@ -76,7 +75,7 @@ def prepare_external_shapefile(self, ext_shp, var_list, info=False, apply_bbox=T Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. ext_shp : GeoDataFrame or str External shapefile or path to it. var_list : List[str] or None @@ -96,20 +95,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: @@ -128,7 +127,7 @@ def get_bbox(self): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. Returns ------- @@ -136,8 +135,8 @@ 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 @@ -149,7 +148,7 @@ def spatial_join_nearest(self, ext_shp, info=False): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. ext_shp : GeoDataFrame External shapefile. info : bool @@ -165,20 +164,20 @@ 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 @@ -190,7 +189,7 @@ def spatial_join_centroid(self, ext_shp, info=False): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. ext_shp : GeoDataFrame External shapefile. info : bool @@ -211,16 +210,16 @@ 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 @@ -232,7 +231,7 @@ def spatial_join_intersection(self, ext_shp, info=False): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. ext_shp : GeoDataFrame External shapefile. info : bool @@ -240,66 +239,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 d1868e4..6c32f15 100644 --- a/nes/methods/vertical_interpolation.py +++ b/nes/methods/vertical_interpolation.py @@ -2,8 +2,8 @@ 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 @@ -28,22 +28,22 @@ def add_4d_vertical_info(self, info_to_add): 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 +58,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 +99,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 +107,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. @@ -132,7 +132,7 @@ def interpolate_vertical(self, new_levels, new_src_vertical=None, kind='linear', 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 + do_extrapolation = "extrapolate" in extrapolate_options if len(self.lev) == 1: raise RuntimeError("1D data cannot be vertically interpolated.") @@ -154,59 +154,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 +218,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 +253,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/default_nes.py b/nes/nc_projections/default_nes.py index 2433863..8597cb7 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 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.Communicator MPI communicator. rank : int MPI rank. @@ -44,59 +45,136 @@ 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. + ... + }, + ... + } + _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. + _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. + ... + } _lat : dict - Latitudes dictionary with the complete 'data' key for all the values and the rest of the attributes. + 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. + ... + } _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. + 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. + ... + } + _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. + ... + } + _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', + 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): """ @@ -114,7 +192,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 +240,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: @@ -188,16 +266,16 @@ class Nes(object): 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._lev = {"data": array([0]), + "units": "", + "positive": "up"} self._lat, self._lon = self._create_centre_coordinates(**kwargs) # Set axis limits for parallel reading 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 = 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 @@ -226,9 +304,9 @@ class Nes(object): # 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._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() # Complete cell measures @@ -238,13 +316,13 @@ class Nes(object): 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 = 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.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) # Cell measures screening self.cell_measures = self._get_cell_measures_values(self._cell_measures) @@ -267,8 +345,8 @@ class Nes(object): # 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']] + 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"]] self.hours_start = 0 self.hours_end = 0 @@ -276,7 +354,7 @@ class Nes(object): self.first_level = 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 +372,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,8 +406,8 @@ 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 @@ -377,14 +455,14 @@ class Nes(object): 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 +477,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 +502,7 @@ class Nes(object): Parameters ---------- other : Nes - Nes to be summed + A Nes to be summed Returns ------- @@ -437,7 +515,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 +524,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 +535,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): """ @@ -504,21 +582,21 @@ class Nes(object): return self._lev 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 + if new_direction not in ["up", "down"]: + raise ValueError(f"Level direction mus be up or down. '{new_direction}' is not a valid option") + self._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' + if "positive" in self._lev.keys(): + if self._lev["positive"] == "up": + self._lev["positive"] = "down" + self.lev["positive"] = "down" else: - self._lev['positive'] = 'up' - self.lev['positive'] = 'up' + self._lev["positive"] = "up" + self.lev["positive"] = "up" return True def clear_communicator(self): @@ -586,7 +664,7 @@ 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.time = deepcopy(time_list) @@ -600,12 +678,12 @@ 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: @@ -616,27 +694,26 @@ class Nes(object): 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) + 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'] + 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): """ @@ -644,7 +721,7 @@ class Nes(object): Parameters ---------- - coordinates : np.array + coordinates : array Coordinates in degrees (latitude or longitude). inc : float Increment between centre values. @@ -655,8 +732,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 +743,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 @@ -685,17 +762,17 @@ class Nes(object): Calculate longitude and latitude bounds and set them. """ - 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) + inc_lat = abs(mean(diff(self._lat["data"]))) + lat_bnds = self.create_single_spatial_bounds(self._lat["data"], inc_lat, spatial_nv=2) - 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'], :]} + 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) + inc_lon = abs(mean(diff(self._lon["data"]))) + lon_bnds = self.create_single_spatial_bounds(self._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._lon_bnds = {"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 +794,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 +836,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 +862,7 @@ class Nes(object): return None + @property def get_time_interval(self): """ Calculate the interrval of hours between time steps. @@ -806,7 +884,7 @@ class Nes(object): 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). @@ -814,7 +892,7 @@ class Nes(object): 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: @@ -834,14 +912,14 @@ class Nes(object): 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 @@ -853,9 +931,9 @@ class Nes(object): 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 + # var_info["data"] = None if loaded_vars: raise ValueError("Some variables have been loaded. Use select function before load.") @@ -895,14 +973,14 @@ class Nes(object): 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 = 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.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.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) # Filter dimensions self.filter_coordinates_selection() @@ -919,29 +997,29 @@ class Nes(object): 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']] + 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: + 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']] + 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"]] if self._lat_bnds is not None: - self._lat_bnds['data'] = self._lat_bnds['data'][idx['idx_y_min']:idx['idx_y_max'], :] + 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'], :] + 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']] + 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'], :] + 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'], :] + self._lon_bnds["data"] = self._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 @@ -977,49 +1055,49 @@ 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} + 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(self._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"] = self._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(self._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(self._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(self._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"] = self._lon["data"].shape[-1] else: - if len(self._lon['data'].shape) == 1: + if len(self._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(self._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 +1109,25 @@ class Nes(object): Modify variables to keep only the last time step. """ - if self.parallel_method == 'T': + 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] 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 +1137,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,30 +1185,30 @@ 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_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]]] @@ -1138,22 +1217,22 @@ class Nes(object): 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) + 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.time = [aux_time] @@ -1161,7 +1240,7 @@ class Nes(object): 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 +1249,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]]) + if axis == "Z": + self.lev["data"] = array([self.lev["data"][0]]) + self._lev["data"] = array([self._lev["data"][0]]) return None @@ -1213,7 +1292,7 @@ class Nes(object): Parameters ---------- - time : datetime.datetime + time : datetime Time element. Returns @@ -1239,10 +1318,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 +1334,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 +1356,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 @@ -1322,77 +1401,77 @@ 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'] + 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._lev["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 @@ -1409,44 +1488,43 @@ class Nes(object): 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,25 +1552,25 @@ 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._lev["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 @@ -1522,14 +1600,14 @@ class Nes(object): return idx @staticmethod - def get_coordinate_id(array, value, axis=0): + def get_coordinate_id(myarray, value, axis=0): """ Get the index of the corresponding coordinate value. Parameters ---------- - array : np.array - Array with the coordinate data + myarray : array + An Array with the coordinate data value : float Coordinate value to search. axis : int @@ -1541,7 +1619,7 @@ class Nes(object): int Index of the coordinate array. """ - idx = (np.abs(array - value)).argmin(axis=axis).min() + idx = (abs(myarray - value)).argmin(axis=axis).min() return idx @@ -1554,7 +1632,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 +1640,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 +1660,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 +1689,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 +1723,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 +1755,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 +1776,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 +1795,25 @@ 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 +1829,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 "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 +1849,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 @@ -1792,7 +1863,7 @@ class Nes(object): time_bnds = self.comm.bcast(time_bnds, root=0) - self.free_vars('time_bnds') + self.free_vars("time_bnds") return time_bnds @@ -1807,20 +1878,20 @@ 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 "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: @@ -1832,7 +1903,7 @@ class Nes(object): 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 +1925,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 +1943,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): @@ -1887,14 +1958,14 @@ class Nes(object): 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'] = '' + nc_var["data"] = self.dataset.variables[dimension_name][:] + if hasattr(nc_var, "units"): + if nc_var["units"] in ["unitless", "-"]: + nc_var["units"] = "" self.free_vars(dimension_name) except KeyError: - nc_var = {'data': np.array([0]), - 'units': ''} + nc_var = {"data": array([0]), + "units": ""} return nc_var @@ -1905,9 +1976,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 +1991,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 +2033,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 +2049,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 +2072,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 +2085,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"]: 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 +2117,7 @@ class Nes(object): Returns ------- - data: np.array + data: array Portion of the variable data corresponding to the rank. """ @@ -2057,55 +2128,55 @@ 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'], + # 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( + # 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( 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 +2188,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 +2197,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 +2213,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 +2241,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 +2288,7 @@ class Nes(object): Returns ------- list - List of var names added. + A List of var names added. """ if isinstance(aux_nessy, str): @@ -2228,7 +2299,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) @@ -2297,29 +2368,29 @@ 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} - if self.parallel_method == 'Y': - y_len = self._lat['data'].shape[0] - axis_limits['y_min'] = (y_len // self.size) * self.rank + if self.parallel_method == "Y": + y_len = self._lat["data"].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 = self._lon["data"].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': + 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["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 @@ -2335,31 +2406,31 @@ class Nes(object): """ 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': + 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' + 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 +2457,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 +2466,24 @@ 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) + 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 +2497,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,16 +2506,16 @@ 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' + 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, 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: 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) @@ -2452,78 +2523,78 @@ class Nes(object): # TIME BOUNDS if self._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(self._time_bnds, time_var.units, calendar="standard") # LEVELS - lev = netcdf.createVariable('lev', np.float32, ('lev',), + 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 self._lev.keys(): + lev.units = self._lev["units"] else: - lev.units = '' - if 'positive' in self._lev.keys(): - lev.positive = self._lev['positive'] + lev.units = "" + if "positive" in self._lev.keys(): + lev.positive = self._lev["positive"] if self.size > 1: lev.set_collective(True) - lev[:] = np.array(self._lev['data'], dtype=np.float32) + lev[:] = array(self._lev["data"], dtype=float32) # LATITUDES - lat = netcdf.createVariable('lat', np.float32, self._lat_dim, + 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' + 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.bounds = "lat_bnds" if self.size > 1: lat.set_collective(True) - lat[:] = np.array(self._lat['data'], dtype=np.float32) + lat[:] = array(self._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',), + 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(self._lat_bnds["data"], dtype=float32) # LONGITUDES - lon = netcdf.createVariable('lon', np.float32, self._lon_dim, + 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' + 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.bounds = "lon_bnds" if self.size > 1: lon.set_collective(True) - lon[:] = np.array(self._lon['data'], dtype=np.float32) + lon[:] = array(self._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',), + 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(self._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,16 +2603,16 @@ 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' + 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, 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: 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) @@ -2549,94 +2620,93 @@ class Nes(object): # TIME BOUNDS if self._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(self._time_bnds, time_var.units, calendar="standard") # LEVELS - lev = netcdf.createVariable('lev', self._lev['data'].dtype, ('lev',), + lev = netcdf.createVariable("lev", self._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 self._lev.keys(): + lev.units = self._lev["units"] else: - lev.units = '' - if 'positive' in self._lev.keys(): - lev.positive = self._lev['positive'] + lev.units = "" + if "positive" in self._lev.keys(): + lev.positive = self._lev["positive"] if self.size > 1: lev.set_collective(True) - lev[:] = self._lev['data'] + lev[:] = self._lev["data"] # LATITUDES - lat = netcdf.createVariable('lat', self._lat['data'].dtype, self._lat_dim, + lat = netcdf.createVariable("lat", self._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' + 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.bounds = "lat_bnds" if self.size > 1: lat.set_collective(True) - lat[:] = self._lat['data'] + lat[:] = self._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',), + lat_bnds_var = netcdf.createVariable("lat_bnds", self._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[:] = self._lat_bnds["data"] # LONGITUDES - lon = netcdf.createVariable('lon', self._lon['data'].dtype, self._lon_dim, + lon = netcdf.createVariable("lon", self._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' + 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.bounds = "lon_bnds" if self.size > 1: lon.set_collective(True) - lon[:] = self._lon['data'] + lon[:] = self._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',), + lon_bnds_var = netcdf.createVariable("lon_bnds", self._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[:] = self._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)) @@ -2645,7 +2715,7 @@ class Nes(object): 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 +2726,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 +2744,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 +2799,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 +2816,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'] + 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 +2869,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,15 +2891,15 @@ 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: @@ -2837,17 +2907,17 @@ class Nes(object): 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 +2985,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 +3012,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 +3024,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, type="NES", keep_open=False): """ Write the netCDF output file. @@ -2972,7 +3042,7 @@ class Nes(object): 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' + 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 """ @@ -2997,36 +3067,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 type in ["NES", "DEFAULT"]: new_nc.__to_netcdf_py(path, keep_open=keep_open) - elif type == 'CAMS_RA': + elif type == "CAMS_RA": new_nc.__to_netcdf_cams_ra(path) - elif type == 'MONARCH': + elif type == "MONARCH": to_netcdf_monarch(new_nc, path, chunking=chunking, keep_open=keep_open) - elif type == 'CMAQ': + elif type == "CMAQ": to_netcdf_cmaq(new_nc, path, keep_open=keep_open) - elif type == 'WRF_CHEM': + elif 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 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 +3132,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,48 +3151,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]: + 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._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]: + 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._time[0]).total_seconds() - codes_set(clone_id, 'forecastTime', int(n_secs)) + 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, array(newval.ravel(), dtype="float64")) # codes_set_values(clone_id, newval.ravel()) codes_write(clone_id, fout) del newval @@ -3151,7 +3221,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: @@ -3187,21 +3257,21 @@ class Nes(object): 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 +3287,11 @@ 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 = 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 = GeoDataFrame(index=Index(name="FID", data=fids.ravel()), geometry=geometry, crs="EPSG:4326") self.shapefile = gdf else: @@ -3242,7 +3310,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 +3319,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 +3336,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 +3354,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 +3406,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 +3422,17 @@ 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") + fids = 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 = GeoDataFrame(index=Index(name="FID", data=fids.ravel()), geometry=centroids, crs="EPSG:4326") return centroids_gdf @@ -3384,11 +3450,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 +3464,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 +3474,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,15 +3488,15 @@ 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) @@ -3454,32 +3520,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 +3554,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 +3564,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 +3578,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 @@ -3531,27 +3596,27 @@ class Nes(object): # 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 # 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 gridcell 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 +3631,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 +3641,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 +3649,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 +3678,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 +3690,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 +3718,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 +3742,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 +3760,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,7 +3782,7 @@ 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] @@ -3727,13 +3792,13 @@ class Nes(object): Returns ------- - np.array + array 2D array with the FID data. """ - 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']] + fids = 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"]] return fids diff --git a/nes/nc_projections/latlon_nes.py b/nes/nc_projections/latlon_nes.py index 4ecad93..79a653e 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._lat, "Y") + self.lon = self._get_coordinate_values(self._lon, "X") # Set axis limits for parallel writing 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,33 +131,34 @@ class LatLonNes(Nes): Grid projection. """ - projection = Proj(proj='latlong', - ellps='WGS84',) + projection = Proj(proj="latlong", + ellps="WGS84",) return projection - + + # noinspection DuplicatedCode def _get_projection(self): """ - Get 'projection' and 'projection_data' from grid details. + Get "projection" and "projection_data" from grid details. """ - if 'crs' in self.variables.keys(): - projection_data = self.variables['crs'] - self.free_vars('crs') + 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, + projection_data = {"grid_mapping_name": "latitude_longitude", + "semi_major_axis": self.earth_radius[1], + "inverse_flattening": 0, } - if 'dtype' in projection_data.keys(): - del projection_data['dtype'] + if "dtype" in projection_data.keys(): + del projection_data["dtype"] - if 'data' in projection_data.keys(): - del projection_data['data'] + if "data" in projection_data.keys(): + del projection_data["data"] - if 'dimensions' in projection_data.keys(): - del projection_data['dimensions'] + if "dimensions" in projection_data.keys(): + del projection_data["dimensions"] self.projection_data = projection_data self.projection = self._get_pyproj_projection() @@ -165,27 +167,27 @@ class LatLonNes(Nes): def _create_projection(self, **kwargs): """ - Create 'projection' and 'projection_data' from projection arguments. + Create "projection" and "projection_data" from projection arguments. """ - 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'], + 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'])) + 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'] + 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"] self.projection_data = projection_data self.projection = self._get_pyproj_projection() @@ -194,7 +196,7 @@ class LatLonNes(Nes): 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 +206,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._lon["data"])) + netcdf.createDimension("lat", len(self._lat["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) + netcdf.createDimension("spatial_nv", 2) return None @@ -226,30 +228,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 +261,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 +285,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 +324,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 +340,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 79417ac..b0d5cc3 100644 --- a/nes/nc_projections/lcc_nes.py +++ b/nes/nc_projections/lcc_nes.py @@ -1,12 +1,10 @@ #!/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 shapely.geometry import Polygon, Point from .default_nes import Nes @@ -17,24 +15,24 @@ class LCCNes(Nes): Attributes ---------- _y : dict - Y coordinates dictionary with the complete 'data' key for all the values and the rest of the attributes. + 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. + 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 +49,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 @@ -78,28 +76,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._lat, "Y") + self.lon = self._get_coordinate_values(self._lon, "X") else: # Complete dimensions - self._y = self._get_coordinate_dimension('y') - self._x = self._get_coordinate_dimension('x') + self._y = self._get_coordinate_dimension("y") + self._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._y, "Y") + self.x = self._get_coordinate_values(self._x, "X") # Set axis limits for parallel writing 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 +114,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,6 +140,7 @@ class LCCNes(Nes): return new + # noinspection DuplicatedCode def filter_coordinates_selection(self): """ Use the selection limits to filter y, x, time, lev, lat, lon, lon_bnds and lat_bnds. @@ -149,11 +148,11 @@ class LCCNes(Nes): 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._y, "Y") + self.x = self._get_coordinate_values(self._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._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"]] super(LCCNes, self).filter_coordinates_selection() @@ -169,51 +168,51 @@ 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. + Get "projection" and "projection_data" from 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') + 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.' + 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 "dtype" in projection_data.keys(): + del projection_data["dtype"] - if 'data' in projection_data.keys(): - del projection_data['data'] + if "data" in projection_data.keys(): + del projection_data["data"] - if 'dimensions' in projection_data.keys(): - del projection_data['dimensions'] + 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]] + 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() @@ -222,27 +221,27 @@ class LCCNes(Nes): def _create_projection(self, **kwargs): """ - Create 'projection' and 'projection_data' from projection arguments. + 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'], - - } + 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 + # 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 +252,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._y["data"])) + netcdf.createDimension("x", len(self._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) + 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 +275,32 @@ 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'] + 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"] 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[:] = self._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'] + 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"] 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[:] = self._x["data"] return None + # noinspection DuplicatedCode def _create_centre_coordinates(self, **kwargs): """ Calculate centre latitudes and longitudes from grid details. @@ -311,32 +312,27 @@ class LCCNes(Nes): """ # 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']) + 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 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']) + y_0 = float64(self.projection_data["y_0"]) + inc_y = float64(self.projection_data["inc_y"]) + ny = int(self.projection_data["ny"]) # 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)} - + self._x = {"data": linspace(x_0 + (inc_x / 2), x_0 + (inc_x / 2) + (inc_x * (nx - 1)), nx, dtype=float64)} + self._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 to 2D) - x = np.array([self._x['data']] * len(self._y['data'])) - y = np.array([self._y['data']] * len(self._x['data'])).T + x = array([self._x["data"]] * len(self._y["data"])) + y = 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} def create_providentia_exp_centre_coordinates(self): """ @@ -358,6 +354,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 +366,73 @@ 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 = abs(mean(diff(self._x["data"]))) + x_bnds = self.create_single_spatial_bounds(array([self._x["data"]] * len(self._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 = abs(mean(diff(self._y["data"]))) + y_bnds = self.create_single_spatial_bounds(array([self._y["data"]] * len(self._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._lat_bnds = {"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._lon_bnds = {"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 +440,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 +456,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 +470,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 +484,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 ------- @@ -506,25 +502,23 @@ class LCCNes(Nes): 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 +526,7 @@ class LCCNes(Nes): return gdf + # noinspection DuplicatedCode def get_centroids_from_coordinates(self): """ Get centroids from geographical coordinates. @@ -544,16 +539,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 263ecc7..7c2a84a 100644 --- a/nes/nc_projections/mercator_nes.py +++ b/nes/nc_projections/mercator_nes.py @@ -1,12 +1,10 @@ #!/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 shapely.geometry import Polygon, Point from nes.nc_projections.default_nes import Nes @@ -17,24 +15,24 @@ class MercatorNes(Nes): Attributes ---------- _y : dict - Y coordinates dictionary with the complete 'data' key for all the values and the rest of the attributes. + 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. + 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 +49,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 @@ -79,28 +77,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._lat, "Y") + self.lon = self._get_coordinate_values(self._lon, "X") else: # Complete dimensions - self._y = self._get_coordinate_dimension('y') - self._x = self._get_coordinate_dimension('x') + self._y = self._get_coordinate_dimension("y") + self._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._y, "Y") + self.x = self._get_coordinate_values(self._x, "X") # Set axis limits for parallel writing 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 +115,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,6 +141,7 @@ class MercatorNes(Nes): return new + # noinspection DuplicatedCode def filter_coordinates_selection(self): """ Use the selection limits to filter y, x, time, lev, lat, lon, lon_bnds and lat_bnds. @@ -150,11 +149,11 @@ class MercatorNes(Nes): 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._y, "Y") + self.x = self._get_coordinate_values(self._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._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"]] super(MercatorNes, self).filter_coordinates_selection() @@ -170,36 +169,36 @@ 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 - + + # noinspection DuplicatedCode def _get_projection(self): """ - Get 'projection' and 'projection_data' from grid details. + Get "projection" and "projection_data" from grid details. """ - if 'mercator' in self.variables.keys(): - projection_data = self.variables['mercator'] - self.free_vars('mercator') + if "mercator" in self.variables.keys(): + projection_data = self.variables["mercator"] + self.free_vars("mercator") else: - msg = 'There is no variable called mercator, projection has not been defined.' + msg = "There is no variable called mercator, projection has not been defined." raise RuntimeError(msg) - if 'dtype' in projection_data.keys(): - del projection_data['dtype'] + if "dtype" in projection_data.keys(): + del projection_data["dtype"] - if 'data' in projection_data.keys(): - del projection_data['data'] + if "data" in projection_data.keys(): + del projection_data["data"] - if 'dimensions' in projection_data.keys(): - del projection_data['dimensions'] + if "dimensions" in projection_data.keys(): + del projection_data["dimensions"] self.projection_data = projection_data self.projection = self._get_pyproj_projection() @@ -208,26 +207,26 @@ class MercatorNes(Nes): def _create_projection(self, **kwargs): """ - Create 'projection' and 'projection_data' from projection arguments. + Create "projection" and "projection_data" from projection arguments. """ - 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'], + 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"], } - 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 +237,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._y["data"])) + netcdf.createDimension("x", len(self._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) + 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 +260,59 @@ 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'] + 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"] 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[:] = self._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'] + 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"] 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[:] = self._x["data"] return None + # noinspection DuplicatedCode def _create_centre_coordinates(self, **kwargs): """ Calculate centre latitudes and longitudes from grid details. """ # 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']) + 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 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']) + y_0 = float64(self.projection_data["y_0"]) + inc_y = float64(self.projection_data["inc_y"]) + ny = int(self.projection_data["ny"]) # 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)} + self._x = {"data": linspace(x_0 + (inc_x / 2), x_0 + (inc_x / 2) + (inc_x * (nx - 1)), nx, dtype=float64)} + self._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 to 2D) - x = np.array([self._x['data']] * len(self._y['data'])) - y = np.array([self._y['data']] * len(self._x['data'])).T + x = array([self._x["data"]] * len(self._y["data"])) + y = 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) - return {'data': centre_lat}, {'data': centre_lon} + return {"data": centre_lat}, {"data": centre_lon} def create_providentia_exp_centre_coordinates(self): """ @@ -337,6 +334,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 +348,72 @@ 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 = abs(mean(diff(self._x["data"]))) + x_bnds = self.create_single_spatial_bounds(array([self._x["data"]] * len(self._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 = abs(mean(diff(self._y["data"]))) + y_bnds = self.create_single_spatial_bounds(array([self._y["data"]] * len(self._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._lat_bnds = {"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._lon_bnds = {"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 +421,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 +437,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 +450,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 +464,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 ------- @@ -484,25 +482,23 @@ class MercatorNes(Nes): 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 +506,7 @@ class MercatorNes(Nes): return gdf + # noinspection DuplicatedCode def get_centroids_from_coordinates(self): """ Get centroids from geographical coordinates. @@ -522,15 +519,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 0a762f8..608a02a 100644 --- a/nes/nc_projections/points_nes.py +++ b/nes/nc_projections/points_nes.py @@ -1,12 +1,12 @@ #!/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 copy import deepcopy -import geopandas as gpd -from netCDF4 import date2num, stringtochar +from netCDF4 import date2num from .default_nes import Nes @@ -16,20 +16,21 @@ 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. @@ -45,8 +46,8 @@ class PointsNes(Nes): dataset: Dataset, 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 +71,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._lat, "X") + self.lon = self._get_coordinate_values(self._lon, "X") # Complete dimensions - self._station = {'data': np.arange(len(self._lon['data']))} + self._station = {"data": arange(len(self._lon["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._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 +109,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 @@ -136,7 +137,7 @@ class PointsNes(Nes): def _get_projection(self): """ - Get 'projection' and 'projection_data' from grid details. + Get "projection" and "projection_data" from grid details. """ self.projection_data = None @@ -146,17 +147,17 @@ class PointsNes(Nes): def _create_projection(self, **kwargs): """ - Create 'projection' and 'projection_data' from projection arguments. + Create "projection" and "projection_data" from projection arguments. """ self.projection_data = None self.projection = None 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 +166,25 @@ 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) + 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._lon["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', '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,14 +193,14 @@ 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' + time_var = netcdf.createVariable("time", 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.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): @@ -208,48 +209,46 @@ class PointsNes(Nes): # 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, + 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._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' + 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' + lat.bounds = "lat_bnds" if self.size > 1: lat.set_collective(True) - lat[:] = self._lat['data'] + lat[:] = self._lat["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' + 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' + lon.bounds = "lon_bnds" if self.size > 1: lon.set_collective(True) - lon[:] = self._lon['data'] + lon[:] = self._lon["data"] return None @@ -260,9 +259,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 +274,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 +305,7 @@ class PointsNes(Nes): Returns ------- - data: np.array + data: array Portion of the variable data corresponding to the rank. """ @@ -315,15 +314,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 +331,7 @@ class PointsNes(Nes): return data + # noinspection DuplicatedCode def _create_variables(self, netcdf, chunking=False): """ Create the netCDF file variables. @@ -348,52 +347,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,80 +401,83 @@ 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 def _gather_data(self, data_to_gather): @@ -491,33 +493,33 @@ 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 @@ -528,12 +530,12 @@ 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) @@ -556,12 +558,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 +573,7 @@ class PointsNes(Nes): ---------- netcdf : Dataset NetCDF object. - """ + """ return None @@ -594,36 +596,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 +638,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 +654,7 @@ class PointsNes(Nes): def create_shapefile(self): """ - Create spatial geodataframe (shapefile). + Create spatial GeoDataFrame (shapefile). Returns ------- @@ -660,13 +664,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 +682,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._lon["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 +708,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 f91d565..aa70570 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,14 +150,14 @@ 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' + time_var = netcdf.createVariable("time", 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.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): @@ -165,48 +166,46 @@ class PointsNesGHOST(PointsNes): # 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, + 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._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' + 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' + lat.bounds = "lat_bnds" if self.size > 1: lat.set_collective(True) - lat[:] = self._lat['data'] + lat[:] = self._lat["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' + 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' + lon.bounds = "lon_bnds" if self.size > 1: lon.set_collective(True) - lon[:] = self._lon['data'] + lon[:] = self._lon["data"] def erase_flags(self): @@ -214,11 +213,12 @@ class PointsNesGHOST(PointsNes): 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._lon["data"]), t_len, 0)) + self._flag["data"] = empty((len(self._lon["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,17 +493,17 @@ 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 @@ -508,20 +511,20 @@ class PointsNesGHOST(PointsNes): # 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 @@ -539,35 +542,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, type="NES", + keep_open=False): """ Write the netCDF output file. Parameters ---------- + keep_open : bool + type : str path : str Path to the output netCDF file. compression_level : int @@ -577,14 +583,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 +617,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 +785,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 e35ff3d..c926221 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,17 +529,17 @@ 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 @@ -543,17 +547,18 @@ class PointsNesProvidentia(PointsNes): # 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) @@ -563,7 +568,8 @@ class PointsNesProvidentia(PointsNes): 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, type="NES", + keep_open=False): """ Write the netCDF output file. @@ -578,14 +584,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. + 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 +603,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 +617,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 c29b1fd..f91c8f7 100644 --- a/nes/nc_projections/rotated_nes.py +++ b/nes/nc_projections/rotated_nes.py @@ -1,13 +1,12 @@ #!/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 shapely.geometry import Polygon, Point from .default_nes import Nes @@ -18,24 +17,24 @@ class RotatedNes(Nes): Attributes ---------- _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 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. + 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 +51,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 @@ -80,28 +79,28 @@ class RotatedNes(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._lat, "Y") + self.lon = self._get_coordinate_values(self._lon, "X") else: # Complete dimensions - self._rlat = self._get_coordinate_dimension('rlat') - self._rlon = self._get_coordinate_dimension('rlon') + self._rlat = self._get_coordinate_dimension("rlat") + self._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._rlat, "Y") + self.rlon = self._get_coordinate_values(self._rlon, "X") # Set axis limits for parallel writing 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 +115,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,6 +141,7 @@ class RotatedNes(Nes): return new + # noinspection DuplicatedCode def filter_coordinates_selection(self): """ Use the selection limits to filter rlat, rlon, time, lev, lat, lon, lon_bnds and lat_bnds. @@ -145,11 +149,11 @@ class RotatedNes(Nes): idx = self.get_idx_intervals() - 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._rlat, "Y") + self.rlon = self._get_coordinate_values(self._rlon, "X") - 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["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"]] super(RotatedNes, self).filter_coordinates_selection() @@ -165,36 +169,37 @@ 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 - + + # noinspection DuplicatedCode def _get_projection(self): """ - Get 'projection' and 'projection_data' from grid details. + Get "projection" and "projection_data" from grid details. """ - if 'rotated_pole' in self.variables.keys(): - projection_data = self.variables['rotated_pole'] - self.free_vars('rotated_pole') + 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.' + 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 "dtype" in projection_data.keys(): + del projection_data["dtype"] - if 'data' in projection_data.keys(): - del projection_data['data'] + if "data" in projection_data.keys(): + del projection_data["data"] - if 'dimensions' in projection_data.keys(): - del projection_data['dimensions'] + if "dimensions" in projection_data.keys(): + del projection_data["dimensions"] self.projection_data = projection_data self.projection = self._get_pyproj_projection() @@ -203,17 +208,17 @@ class RotatedNes(Nes): def _create_projection(self, **kwargs): """ - Create 'projection' and 'projection_data' from projection arguments. + 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'], - } + 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"], + } self.projection_data = projection_data self.projection = self._get_pyproj_projection() @@ -222,7 +227,7 @@ class RotatedNes(Nes): 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 ---------- @@ -233,19 +238,19 @@ class RotatedNes(Nes): super(RotatedNes, self)._create_dimensions(netcdf) # Create rlat and rlon dimensions - netcdf.createDimension('rlon', len(self._rlon['data'])) - netcdf.createDimension('rlat', len(self._rlat['data'])) + netcdf.createDimension("rlon", len(self._rlon["data"])) + netcdf.createDimension("rlat", len(self._rlat["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) + 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 ---------- @@ -256,28 +261,28 @@ class RotatedNes(Nes): super(RotatedNes, self)._create_dimension_variables(netcdf) # ROTATED LATITUDES - rlat = netcdf.createVariable('rlat', self._rlat['data'].dtype, ('rlat',)) + rlat = netcdf.createVariable("rlat", self._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 self._rlat.keys(): + rlat.units = self._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[:] = self._rlat["data"] # ROTATED LONGITUDES - rlon = netcdf.createVariable('rlon', self._rlon['data'].dtype, ('rlon',)) + rlon = netcdf.createVariable("rlon", self._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 self._rlon.keys(): + rlon.units = self._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[:] = self._rlon["data"] return None @@ -288,32 +293,28 @@ 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 +322,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 @@ -387,10 +388,10 @@ class RotatedNes(Nes): self._rlat, self._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) + centre_lon, centre_lat = self.rotated2latlon(array([self._rlon["data"]] * len(self._rlat["data"])), + array([self._rlat["data"]] * len(self._rlon["data"])).T) - return {'data': centre_lat}, {'data': centre_lon} + return {"data": centre_lat}, {"data": centre_lon} def create_providentia_exp_centre_coordinates(self): """ @@ -412,6 +413,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 +427,73 @@ 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 = abs(mean(diff(self._rlat["data"]))) + rlat_bnds = self.create_single_spatial_bounds(array([self._rlat["data"]] * len(self._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 = abs(mean(diff(self._rlon["data"]))) + rlon_bnds = self.create_single_spatial_bounds(array([self._rlon["data"]] * len(self._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._lat_bnds = {"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._lon_bnds = {"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 +501,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 +517,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 +530,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 +544,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). @@ -559,10 +561,10 @@ class RotatedNes(Nes): 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 +577,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 +585,7 @@ class RotatedNes(Nes): return gdf + # noinspection DuplicatedCode def get_centroids_from_coordinates(self): """ Get centroids from geographical coordinates. @@ -597,15 +598,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 cafc607..a3618c4 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): """ @@ -24,8 +24,8 @@ class RotatedNestedNes(RotatedNes): 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 @@ -67,21 +67,21 @@ 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() @@ -89,17 +89,17 @@ class RotatedNestedNes(RotatedNes): def _create_projection(self, **kwargs): """ - Create 'projection' and 'projection_data' from projection arguments. + 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'] - } + 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"] + } projection_data = self._get_parent_attributes(projection_data) @@ -115,32 +115,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/requirements.txt b/requirements.txt index 96fd35a..0371633 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/tests/unit/test_imports.py b/tests/unit/test_imports.py index b851d90..346ebad 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() -- GitLab From 282dbce57e8bafd18da4b22d6a97829f6610d93d Mon Sep 17 00:00:00 2001 From: Carles Tena Date: Mon, 17 Jun 2024 13:55:52 +0200 Subject: [PATCH 04/11] Refactor code --- nes/create_nes.py | 74 +++- nes/methods/cell_measures.py | 42 +- nes/methods/horizontal_interpolation.py | 73 ++-- nes/methods/spatial_join.py | 24 +- nes/methods/vertical_interpolation.py | 7 +- nes/nc_projections/default_nes.py | 258 ++++++++++--- nes/nc_projections/latlon_nes.py | 9 +- nes/nc_projections/lcc_nes.py | 20 +- nes/nc_projections/mercator_nes.py | 20 +- nes/nc_projections/points_nes.py | 18 +- nes/nc_projections/points_nes_ghost.py | 14 +- nes/nc_projections/points_nes_providentia.py | 4 +- nes/nc_projections/rotated_nes.py | 20 +- nes/nc_projections/rotated_nested_nes.py | 2 +- nes/nes_formats/cams_ra_format.py | 121 +++--- nes/nes_formats/cmaq_format.py | 260 ++++++------- nes/nes_formats/monarch_format.py | 48 +-- nes/nes_formats/wrf_chem_format.py | 387 +++++++++---------- 18 files changed, 806 insertions(+), 595 deletions(-) diff --git a/nes/create_nes.py b/nes/create_nes.py index 2fbfdac..06d27dd 100644 --- a/nes/create_nes.py +++ b/nes/create_nes.py @@ -15,26 +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. - times : List[datetime] - List of Date times - 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.Communicator, 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: @@ -50,7 +82,7 @@ def create_nes(comm=None, info=False, projection=None, parallel_method="Y", bala 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 = [] diff --git a/nes/methods/cell_measures.py b/nes/methods/cell_measures.py index bfae6a4..0290ec3 100644 --- a/nes/methods/cell_measures.py +++ b/nes/methods/cell_measures.py @@ -89,7 +89,7 @@ def calculate_geometry_area(geometry_list, earth_radius_minor_axis=6356752.3142, 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) + 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 @@ -98,15 +98,14 @@ def calculate_geometry_area(geometry_list, earth_radius_minor_axis=6356752.3142, geometry_corner_lon, geometry_corner_lat = geometry_list[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) + 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. @@ -126,12 +125,12 @@ def calculate_cell_area(grid_corner_lon, grid_corner_lat, n_cells = grid_corner_lon.shape[0] 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/). @@ -147,8 +146,8 @@ def mod_huiliers_area(cell_corner_lon, cell_corner_lat): 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,11 +159,11 @@ 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 - my_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): @@ -173,7 +172,7 @@ def mod_huiliers_area(cell_corner_lon, cell_corner_lat): 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/). @@ -189,18 +188,18 @@ def tri_area(point_0, point_1, point_2): """ # Get length of side a (between point 0 and 1) - tmp_vec = cross_product(point_0, point_1) - sin_a = norm(tmp_vec) + 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) - sin_b = norm(tmp_vec) + 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) - sin_c = norm(tmp_vec) + tmp_vec = __cross_product(point_2, point_1) + sin_c = __norm(tmp_vec) c = arcsin(sin_c) # Calculate area @@ -211,7 +210,7 @@ def tri_area(point_0, point_1, point_2): return area -def cross_product(a, b): +def __cross_product(a, b): """ Calculate cross product between two points. @@ -228,7 +227,7 @@ 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. @@ -241,7 +240,8 @@ def norm(cp): 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. diff --git a/nes/methods/horizontal_interpolation.py b/nes/methods/horizontal_interpolation.py index 5a20bac..6d4d4ef 100644 --- a/nes/methods/horizontal_interpolation.py +++ b/nes/methods/horizontal_interpolation.py @@ -53,11 +53,11 @@ def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind="Neares 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) + 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) + 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)) @@ -114,7 +114,7 @@ def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind="Neares # 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) + 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): @@ -156,7 +156,7 @@ def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind="Neares 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. @@ -199,7 +199,7 @@ def get_src_data(comm, var_data, idx, parallel_method): # 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. @@ -227,6 +227,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 @@ -235,7 +236,7 @@ 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: @@ -244,17 +245,17 @@ def get_weights_idx_t_axis(self, dst_grid, weight_matrix_path, kind, n_neighbour 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) @@ -266,9 +267,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: @@ -297,7 +298,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. @@ -325,6 +326,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: @@ -339,7 +341,7 @@ def get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighbou 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: @@ -348,18 +350,18 @@ def get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighbou 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) @@ -368,9 +370,9 @@ 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) + 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) @@ -409,7 +411,7 @@ def get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighbou 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. @@ -441,7 +443,8 @@ def read_weight_matrix(weight_matrix_path, comm=None, parallel_method="T"): return weight_matrix -def create_nn_weight_matrix(self, dst_grid, n_neighbours=4, wm_path=None, info=False): +# noinspection DuplicatedCode +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. @@ -488,7 +491,7 @@ def create_nn_weight_matrix(self, dst_grid, n_neighbours=4, wm_path=None, info=F 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. @@ -496,15 +499,15 @@ 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 = \ @@ -537,7 +540,8 @@ def create_nn_weight_matrix(self, dst_grid, n_neighbours=4, wm_path=None, info=F 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. @@ -702,7 +706,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. @@ -728,13 +733,13 @@ def lon_lat_to_cartesian(lon, lat, radius=6378137.0): 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 between the two lat/lon pairs could lead to inaccurate results depending on the distance diff --git a/nes/methods/spatial_join.py b/nes/methods/spatial_join.py index b6b1a49..3c302e4 100644 --- a/nes/methods/spatial_join.py +++ b/nes/methods/spatial_join.py @@ -42,18 +42,18 @@ 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": # Nearest centroids to the shapefile polygons - spatial_join_nearest(self, ext_shp=ext_shp, info=info) + __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) + __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"] @@ -62,7 +62,7 @@ def spatial_join(self, ext_shp, method=None, var_list=None, info=False, apply_bb 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. @@ -97,7 +97,7 @@ def prepare_external_shapefile(self, ext_shp, var_list, info=False, apply_bbox=T print("\tReading external shapefile") # ext_shp = read_file(ext_shp, include_fields=var_list, mask=self.shapefile.geometry) if apply_bbox: - ext_shp = 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 = read_file(ext_shp, include_fields=var_list) else: @@ -120,7 +120,7 @@ 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). @@ -141,7 +141,8 @@ def get_bbox(self): 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. @@ -182,7 +183,8 @@ def spatial_join_nearest(self, ext_shp, info=False): 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. @@ -224,7 +226,7 @@ def spatial_join_centroid(self, ext_shp, info=False): 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. diff --git a/nes/methods/vertical_interpolation.py b/nes/methods/vertical_interpolation.py index 6c32f15..86f03e9 100644 --- a/nes/methods/vertical_interpolation.py +++ b/nes/methods/vertical_interpolation.py @@ -26,7 +26,7 @@ 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. @@ -131,7 +131,10 @@ 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) + src_levels_aux = None + fill_value = None + + extrapolate_options = __parse_extrapolate(extrapolate_options) do_extrapolation = "extrapolate" in extrapolate_options if len(self.lev) == 1: diff --git a/nes/nc_projections/default_nes.py b/nes/nc_projections/default_nes.py index 8597cb7..1bdfd32 100644 --- a/nes/nc_projections/default_nes.py +++ b/nes/nc_projections/default_nes.py @@ -242,6 +242,9 @@ class Nes(object): # Get minor and major axes of Earth self.earth_radius = self.get_earth_radius("WGS84") + self.projection_data = None + self.projection = None + # Time resolution and climatology will be modified, if needed, during the time variable reading self._time_resolution = "hours" self._climatology = False @@ -272,7 +275,7 @@ class Nes(object): self._lat, self._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() # Dimensions screening self.time = self._time[self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"]] @@ -290,7 +293,7 @@ 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() @@ -313,7 +316,7 @@ class Nes(object): 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() # Dimensions screening self.time = self._time[self.read_axis_limits["t_min"]:self.read_axis_limits["t_max"]] @@ -328,7 +331,7 @@ class Nes(object): 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() + self.write_axis_limits = self._get_write_axis_limits() # Set NetCDF attributes self.global_attrs = self.__get_global_attributes() @@ -344,7 +347,7 @@ class Nes(object): self.vertical_var_name = None # Filtering (portion of the filter coordinates function) - idx = self.get_idx_intervals() + 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"]] @@ -576,12 +579,52 @@ class Nes(object): return nessy def get_full_times(self): + """ + Retrieve the complete list of original time step values. + + Returns + ------- + List[datetime] + The complete list of original time step values from the netCDF data. + """ return self._time def get_full_levels(self): + """ + 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. + ... + } + """ return self._lev def set_level_direction(self, 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") self._lev["positive"] = new_direction @@ -590,6 +633,14 @@ class Nes(object): return True def reverse_level_direction(self): + """ + 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": self._lev["positive"] = "down" @@ -626,18 +677,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): @@ -706,6 +782,25 @@ class Nes(object): return None def set_time_resolution(self, new_resolution): + """ + 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 @@ -715,7 +810,7 @@ class Nes(object): 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. @@ -763,13 +858,13 @@ class Nes(object): """ inc_lat = abs(mean(diff(self._lat["data"]))) - lat_bnds = self.create_single_spatial_bounds(self._lat["data"], inc_lat, spatial_nv=2) + lat_bnds = self._create_single_spatial_bounds(self._lat["data"], inc_lat, spatial_nv=2) 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 = abs(mean(diff(self._lon["data"]))) - lon_bnds = self.create_single_spatial_bounds(self._lon["data"], inc_lon, spatial_nv=2) + lon_bnds = self._create_single_spatial_bounds(self._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"], :]} @@ -926,14 +1021,50 @@ class Nes(object): 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. """ loaded_vars = False for var_info in self.variables.values(): 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.") @@ -970,7 +1101,7 @@ 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"]] @@ -983,19 +1114,19 @@ class Nes(object): self.lon_bnds = self._get_coordinate_values(self._lon_bnds, "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() + 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"]] @@ -1046,7 +1177,7 @@ class Nes(object): return None - def get_idx_intervals(self): + def _get_idx_intervals(self): """ Calculate the index intervals @@ -1055,8 +1186,8 @@ 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 = {"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} @@ -1064,11 +1195,11 @@ class Nes(object): if self.lat_min is None: 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(self._lat["data"], self.lat_min, axis=0) if self.lat_max is None: idx["idx_y_max"] = self._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(self._lat["data"], self.lat_max, axis=0) + 1 if idx["idx_y_min"] > idx["idx_y_max"]: idx_aux = copy(idx["idx_y_min"]) @@ -1084,7 +1215,7 @@ class Nes(object): 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(self._lon["data"], self.lon_min, axis=axis) if self.lon_max is None: idx["idx_x_max"] = self._lon["data"].shape[-1] else: @@ -1092,7 +1223,7 @@ class Nes(object): 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(self._lon["data"], self.lon_max, axis=axis) + 1 if idx["idx_x_min"] > idx["idx_x_max"]: idx_aux = copy(idx["idx_x_min"]) @@ -1110,7 +1241,7 @@ class Nes(object): """ if self.parallel_method == "T": - raise NotImplementedError("Statistics are not implemented on time axis paralelitation method.") + raise NotImplementedError("Statistics are not implemented on time axis parallelization method.") aux_time = self._time[0].replace(hour=0, minute=0, second=0, microsecond=0) self._time = [aux_time] self.time = [aux_time] @@ -1374,7 +1505,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. @@ -1386,11 +1517,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. @@ -1406,7 +1537,7 @@ class Nes(object): "z_min": None, "z_max": None, "t_min": None, "t_max": None} - idx = self.get_idx_intervals() + 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: @@ -1475,7 +1606,7 @@ class Nes(object): return axis_limits - def get_read_axis_limits_balanced(self): + def _get_read_axis_limits_balanced(self): """ Calculate the 4D reading balanced axis limits. @@ -1485,7 +1616,7 @@ 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": @@ -1574,7 +1705,7 @@ class Nes(object): 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. @@ -1600,13 +1731,13 @@ class Nes(object): return idx @staticmethod - def get_coordinate_id(myarray, value, axis=0): + def _get_coordinate_id(my_array, value, axis=0): """ Get the index of the corresponding coordinate value. Parameters ---------- - myarray : array + my_array : array An Array with the coordinate data value : float Coordinate value to search. @@ -1619,11 +1750,11 @@ class Nes(object): int Index of the coordinate array. """ - idx = (abs(myarray - 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. """ @@ -2341,7 +2472,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. @@ -2353,11 +2484,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. @@ -2394,7 +2525,7 @@ class Nes(object): return axis_limits - def get_write_axis_limits_balanced(self): + def _get_write_axis_limits_balanced(self): """ Calculate the 4D reading balanced axis limits. @@ -2712,7 +2843,7 @@ class Nes(object): 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." @@ -2784,7 +2915,7 @@ class Nes(object): # Convert list of strings to chars for parallelization if issubdtype(var_dtype, character): - var_dict["data_aux"] = self.str2char(var_dict["data"]) + var_dict["data_aux"] = self._str2char(var_dict["data"]) var_dims += ("strlen",) var_dtype = "S1" @@ -3193,7 +3324,6 @@ class Nes(object): newval[isnan(newval)] = 0. codes_set_values(clone_id, array(newval.ravel(), dtype="float64")) - # codes_set_values(clone_id, newval.ravel()) codes_write(clone_id, fout) del newval codes_release(gid) @@ -3243,7 +3373,7 @@ class Nes(object): def create_shapefile(self): """ - Create spatial geodataframe (shapefile). + Create spatial GeoDataFrame (shapefile). Returns ------- @@ -3301,7 +3431,7 @@ class Nes(object): def write_shapefile(self, path): """ - Save spatial geodataframe (shapefile). + Save spatial GeoDataFrame (shapefile). Parameters ---------- @@ -3427,7 +3557,7 @@ class Nes(object): centroids.append(Point(self.lon["data"][lon_ind], self.lat["data"][lat_ind])) - # Create dataframe cointaining all points + # Create dataframe containing all points fids = 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"], @@ -3594,9 +3724,9 @@ 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 # between the two lat/lon pairs could lead to inaccurate results depending on the distance @@ -3802,3 +3932,31 @@ class Nes(object): self.write_axis_limits["x_min"]:self.write_axis_limits["x_max"]] return fids + + def create_providentia_exp_centre_coordinates(self): + """ + Calculate centre latitudes and longitudes from original coordinates and store as 2D arrays. + + Returns + ---------- + 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). + """ + + raise NotImplementedError("create_providentia_exp_centre_coordinates function is not implemented by default") + + # 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 79a653e..5038d0b 100644 --- a/nes/nc_projections/latlon_nes.py +++ b/nes/nc_projections/latlon_nes.py @@ -68,7 +68,7 @@ class LatLonNes(Nes): self.lon = self._get_coordinate_values(self._lon, "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",) @@ -131,8 +131,7 @@ class LatLonNes(Nes): Grid projection. """ - projection = Proj(proj="latlong", - ellps="WGS84",) + projection = Proj(proj="latlong", ellps="WGS84",) return projection @@ -289,8 +288,8 @@ class LatLonNes(Nes): 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 = append(lat_bounds.flatten()[::2], lat_bounds.flatten()[-1]) diff --git a/nes/nc_projections/lcc_nes.py b/nes/nc_projections/lcc_nes.py index b0d5cc3..64a2fa9 100644 --- a/nes/nc_projections/lcc_nes.py +++ b/nes/nc_projections/lcc_nes.py @@ -88,7 +88,7 @@ class LCCNes(Nes): self.x = self._get_coordinate_values(self._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") @@ -141,12 +141,12 @@ class LCCNes(Nes): return new # noinspection DuplicatedCode - def filter_coordinates_selection(self): + 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") @@ -154,7 +154,7 @@ class LCCNes(Nes): 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"]] - super(LCCNes, self).filter_coordinates_selection() + super(LCCNes, self)._filter_coordinates_selection() return None @@ -371,8 +371,8 @@ class LCCNes(Nes): 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 = append(y_bnds.flatten()[::2], y_bnds.flatten()[-1]) @@ -406,12 +406,12 @@ class LCCNes(Nes): # Calculate LCC coordinates bounds inc_x = abs(mean(diff(self._x["data"]))) - x_bnds = self.create_single_spatial_bounds(array([self._x["data"]] * len(self._y["data"])), - inc_x, spatial_nv=4) + x_bnds = self._create_single_spatial_bounds(array([self._x["data"]] * len(self._y["data"])), + inc_x, spatial_nv=4) inc_y = abs(mean(diff(self._y["data"]))) - y_bnds = self.create_single_spatial_bounds(array([self._y["data"]] * len(self._x["data"])).T, - inc_y, spatial_nv=4, inverse=True) + y_bnds = self._create_single_spatial_bounds(array([self._y["data"]] * len(self._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) diff --git a/nes/nc_projections/mercator_nes.py b/nes/nc_projections/mercator_nes.py index 7c2a84a..e7ac387 100644 --- a/nes/nc_projections/mercator_nes.py +++ b/nes/nc_projections/mercator_nes.py @@ -89,7 +89,7 @@ class MercatorNes(Nes): self.x = self._get_coordinate_values(self._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") @@ -142,12 +142,12 @@ class MercatorNes(Nes): return new # noinspection DuplicatedCode - def filter_coordinates_selection(self): + 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") @@ -155,7 +155,7 @@ class MercatorNes(Nes): 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"]] - super(MercatorNes, self).filter_coordinates_selection() + super(MercatorNes, self)._filter_coordinates_selection() return None @@ -352,8 +352,8 @@ class MercatorNes(Nes): 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 = append(y_bounds.flatten()[::2], y_bounds.flatten()[-1]) @@ -387,12 +387,12 @@ class MercatorNes(Nes): # Calculate Mercator coordinates bounds inc_x = abs(mean(diff(self._x["data"]))) - x_bnds = self.create_single_spatial_bounds(array([self._x["data"]] * len(self._y["data"])), - inc_x, spatial_nv=4) + x_bnds = self._create_single_spatial_bounds(array([self._x["data"]] * len(self._y["data"])), + inc_x, spatial_nv=4) inc_y = abs(mean(diff(self._y["data"]))) - y_bnds = self.create_single_spatial_bounds(array([self._y["data"]] * len(self._x["data"])).T, - inc_y, spatial_nv=4, inverse=True) + y_bnds = self._create_single_spatial_bounds(array([self._y["data"]] * len(self._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) diff --git a/nes/nc_projections/points_nes.py b/nes/nc_projections/points_nes.py index 608a02a..9f50e8b 100644 --- a/nes/nc_projections/points_nes.py +++ b/nes/nc_projections/points_nes.py @@ -43,7 +43,7 @@ class PointsNes(Nes): 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". @@ -85,7 +85,7 @@ class PointsNes(Nes): 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",) @@ -182,6 +182,7 @@ class PointsNes(Nes): return None + # noinspection DuplicatedCode def _create_dimension_variables(self, netcdf): """ Create the "time", "time_bnds", "station", "lat", "lat_bnds", "lon" and "lon_bnds" variables. @@ -195,7 +196,7 @@ class PointsNes(Nes): # TIMES time_var = netcdf.createVariable("time", 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")) + 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" @@ -203,8 +204,8 @@ class PointsNes(Nes): 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._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 BOUNDS @@ -252,6 +253,7 @@ class PointsNes(Nes): return None + # noinspection DuplicatedCode def _get_coordinate_values(self, coordinate_info, coordinate_axis, bounds=False): """ Get the coordinate data of the current portion. @@ -387,7 +389,7 @@ class PointsNes(Nes): # Convert list of strings to chars for parallelization if issubdtype(var_dtype, character): - var_dict["data_aux"] = self.str2char(var_dict["data"]) + var_dict["data_aux"] = self._str2char(var_dict["data"]) var_dims += ("strlen",) var_dtype = "S1" @@ -480,6 +482,7 @@ class PointsNes(Nes): 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. @@ -517,12 +520,10 @@ class PointsNes(Nes): if shp_len == 1: # dimensions = (station) axis = None - continue elif shp_len == 2: if "strlen" in var_info["dimensions"]: # dimensions = (station, strlen) axis = None - continue else: # dimensions = (time, station) axis = 0 @@ -540,7 +541,6 @@ class PointsNes(Nes): 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 diff --git a/nes/nc_projections/points_nes_ghost.py b/nes/nc_projections/points_nes_ghost.py index aa70570..1c75d68 100644 --- a/nes/nc_projections/points_nes_ghost.py +++ b/nes/nc_projections/points_nes_ghost.py @@ -152,7 +152,7 @@ class PointsNesGHOST(PointsNes): # TIMES time_var = netcdf.createVariable("time", 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")) + 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" @@ -160,8 +160,8 @@ class PointsNesGHOST(PointsNes): 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._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 BOUNDS @@ -209,8 +209,8 @@ class PointsNesGHOST(PointsNes): 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"] = empty((len(self._lon["data"]), t_len, 0)) @@ -358,7 +358,7 @@ class PointsNesGHOST(PointsNes): # Convert list of strings to chars for parallelization if issubdtype(var_dtype, character): - var_dict["data_aux"] = self.str2char(var_dict["data"]) + var_dict["data_aux"] = self._str2char(var_dict["data"]) var_dims += ("strlen",) var_dtype = "S1" @@ -501,12 +501,10 @@ class PointsNesGHOST(PointsNes): if shp_len == 1: # dimensions = (station) axis = None - continue elif shp_len == 2: if "strlen" in var_info["dimensions"]: # dimensions = (station, strlen) axis = None - continue else: # dimensions = (station, time) axis = 1 diff --git a/nes/nc_projections/points_nes_providentia.py b/nes/nc_projections/points_nes_providentia.py index c926221..3bcb105 100644 --- a/nes/nc_projections/points_nes_providentia.py +++ b/nes/nc_projections/points_nes_providentia.py @@ -395,7 +395,7 @@ class PointsNesProvidentia(PointsNes): # Convert list of strings to chars for parallelization if issubdtype(var_dtype, character): - var_dict["data_aux"] = self.str2char(var_dict["data"]) + var_dict["data_aux"] = self._str2char(var_dict["data"]) var_dims += ("strlen",) var_dtype = "S1" @@ -537,12 +537,10 @@ class PointsNesProvidentia(PointsNes): if shp_len == 1: # dimensions = (station) axis = None - continue elif shp_len == 2: if "strlen" in var_info["dimensions"]: # dimensions = (station, strlen) axis = None - continue else: # dimensions = (station, time) axis = 1 diff --git a/nes/nc_projections/rotated_nes.py b/nes/nc_projections/rotated_nes.py index f91c8f7..26033a3 100644 --- a/nes/nc_projections/rotated_nes.py +++ b/nes/nc_projections/rotated_nes.py @@ -91,7 +91,7 @@ class RotatedNes(Nes): self.rlon = self._get_coordinate_values(self._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") @@ -142,12 +142,12 @@ class RotatedNes(Nes): return new # noinspection DuplicatedCode - def filter_coordinates_selection(self): + 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") @@ -155,7 +155,7 @@ class RotatedNes(Nes): 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"]] - super(RotatedNes, self).filter_coordinates_selection() + super(RotatedNes, self)._filter_coordinates_selection() return None @@ -431,8 +431,8 @@ class RotatedNes(Nes): 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 = append(rlat_bounds.flatten()[::2], rlat_bounds.flatten()[-1]) @@ -467,12 +467,12 @@ class RotatedNes(Nes): # Calculate rotated coordinates bounds inc_rlat = abs(mean(diff(self._rlat["data"]))) - rlat_bnds = self.create_single_spatial_bounds(array([self._rlat["data"]] * len(self._rlon["data"])).T, - inc_rlat, spatial_nv=4, inverse=True) + rlat_bnds = self._create_single_spatial_bounds(array([self._rlat["data"]] * len(self._rlon["data"])).T, + inc_rlat, spatial_nv=4, inverse=True) inc_rlon = abs(mean(diff(self._rlon["data"]))) - rlon_bnds = self.create_single_spatial_bounds(array([self._rlon["data"]] * len(self._rlat["data"])), - inc_rlon, spatial_nv=4) + rlon_bnds = self._create_single_spatial_bounds(array([self._rlon["data"]] * len(self._rlat["data"])), + inc_rlon, spatial_nv=4) # Transform rotated bounds to regular bounds lon_bnds, lat_bnds = self.rotated2latlon(rlon_bnds, rlat_bnds) diff --git a/nes/nc_projections/rotated_nested_nes.py b/nes/nc_projections/rotated_nested_nes.py index a3618c4..ab056ca 100644 --- a/nes/nc_projections/rotated_nested_nes.py +++ b/nes/nc_projections/rotated_nested_nes.py @@ -21,7 +21,7 @@ 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". diff --git a/nes/nes_formats/cams_ra_format.py b/nes/nes_formats/cams_ra_format.py index 1c1718a..74bfc37 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. @@ -24,23 +24,23 @@ def to_netcdf_cams_ra(self, path): 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._lat["data"])) + netcdf.createDimension("lon", len(self._lon["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._lat["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._lon["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._time[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 e8fd399..d6cef43 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._x["data"])) + self.global_attrs["NROWS"] = int32(len(self._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._x["data"][0]) - (float64(self._x["data"][1] - self._x["data"][0]) / 2) + self.global_attrs["YORIG"] = float64( + self._y["data"][0]) - (float64(self._y["data"][1] - self._y["data"][0]) / 2) + self.global_attrs["XCELL"] = float64(self._x["data"][1] - self._x["data"][0]) + self.global_attrs["YCELL"] = float64(self._y["data"][1] - self._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._time)) + netcdf.createDimension("DATE-TIME", 2) + netcdf.createDimension("LAY", len(self._lev["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._x["data"])) + netcdf.createDimension("ROW", len(self._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 34a22d1..c7d67e2 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,25 @@ 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) + self._lev["data"] = array(self._lev["data"], dtype=float32) + self._lat["data"] = array(self._lat["data"], dtype=float32) + self._lat_bnds["data"] = array(self._lat_bnds["data"], dtype=float32) + self._lon["data"] = array(self._lon["data"], dtype=float32) + self._lon_bnds["data"] = array(self._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._rlat["data"] = array(self._rlat["data"], dtype=float32) + self._rlon["data"] = array(self._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._y["data"] = array(self._y["data"], dtype=float32) + self._x["data"] = array(self._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 +70,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 +87,7 @@ def to_monarch_units(self): Parameters ---------- self : nes.Nes - Nes Object. + A Nes Object. Returns ------- @@ -96,17 +96,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 d2a71ca..4959eec 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._x["data"]) + 1) + self.global_attrs["SOUTH-NORTH_GRID_DIMENSION"] = int32(len(self._y["data"]) + 1) + self.global_attrs["DX"] = float32(self._x["data"][1] - self._x["data"][0]) + self.global_attrs["DY"] = float32(self._y["data"][1] - self._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._x["data"])) + self.global_attrs["WEST-EAST_PATCH_START_STAG"] = int32(1) + self.global_attrs["WEST-EAST_PATCH_END_STAG"] = int32(len(self._x["data"]) + 1) + self.global_attrs["SOUTH-NORTH_PATCH_START_UNSTAG"] = int32(1) + self.global_attrs["SOUTH-NORTH_PATCH_END_UNSTAG"] = int32(len(self._y["data"])) + self.global_attrs["SOUTH-NORTH_PATCH_START_STAG"] = int32(1) + self.global_attrs["SOUTH-NORTH_PATCH_END_STAG"] = int32(len(self._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._time)) + netcdf.createDimension("DateStrLen", 19) + netcdf.createDimension("emissions_zdim", len(self._lev["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._x["data"])) + netcdf.createDimension("south_north", len(self._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 -- GitLab From 95777f87edbc3d4a8bfba36747f7ecd554de9fbf Mon Sep 17 00:00:00 2001 From: Carles Tena Date: Mon, 17 Jun 2024 14:34:22 +0200 Subject: [PATCH 05/11] Refactor code --- nes/__init__.py | 8 +++++++- nes/create_nes.py | 2 +- nes/load_nes.py | 2 +- nes/methods/__init__.py | 4 ++++ nes/methods/horizontal_interpolation.py | 4 ++-- nes/methods/spatial_join.py | 5 ++--- nes/methods/vertical_interpolation.py | 1 - nes/nc_projections/__init__.py | 5 +++++ nes/nc_projections/default_nes.py | 2 +- nes/nes_formats/__init__.py | 5 +++++ nes/nes_formats/cams_ra_format.py | 2 +- 11 files changed, 29 insertions(+), 11 deletions(-) diff --git a/nes/__init__.py b/nes/__init__.py index ed8dfd3..c28657e 100644 --- a/nes/__init__.py +++ b/nes/__init__.py @@ -4,5 +4,11 @@ __version__ = "1.1.4" 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 06d27dd..fbabb7c 100644 --- a/nes/create_nes.py +++ b/nes/create_nes.py @@ -4,7 +4,7 @@ import warnings import sys from netCDF4 import num2date from mpi4py import MPI -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, diff --git a/nes/load_nes.py b/nes/load_nes.py index 588c5bf..ff632c2 100644 --- a/nes/load_nes.py +++ b/nes/load_nes.py @@ -6,7 +6,7 @@ from numpy import empty from mpi4py import MPI from netCDF4 import Dataset from warnings import warn -from .nc_projections import * +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"] diff --git a/nes/methods/__init__.py b/nes/methods/__init__.py index 772adac..35b6346 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/horizontal_interpolation.py b/nes/methods/horizontal_interpolation.py index 6d4d4ef..f45186b 100644 --- a/nes/methods/horizontal_interpolation.py +++ b/nes/methods/horizontal_interpolation.py @@ -59,8 +59,8 @@ def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind="Neares 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: diff --git a/nes/methods/spatial_join.py b/nes/methods/spatial_join.py index 3c302e4..eb35864 100644 --- a/nes/methods/spatial_join.py +++ b/nes/methods/spatial_join.py @@ -1,9 +1,8 @@ #!/usr/bin/env python import sys -import nes from warnings import warn, filterwarnings -from geopandas import GeoDataFrame, sjoin_nearest, sjoin, read_file +from geopandas import sjoin_nearest, sjoin, read_file from pandas import DataFrame from numpy import array, uint32, nan from shapely.errors import TopologicalError @@ -76,7 +75,7 @@ def __prepare_external_shapefile(self, ext_shp, var_list, info=False, apply_bbox ---------- self : nes.Nes A Nes Object. - ext_shp : GeoDataFrame or str + ext_shp : geopandas.GeoDataFrame or str External shapefile or path to it. var_list : List[str] or None External shapefile variables to be computed. diff --git a/nes/methods/vertical_interpolation.py b/nes/methods/vertical_interpolation.py index 86f03e9..bf44e3f 100644 --- a/nes/methods/vertical_interpolation.py +++ b/nes/methods/vertical_interpolation.py @@ -1,7 +1,6 @@ #!/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 from copy import copy diff --git a/nes/nc_projections/__init__.py b/nes/nc_projections/__init__.py index fa530f9..4839ec5 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 1bdfd32..ed74eeb 100644 --- a/nes/nc_projections/default_nes.py +++ b/nes/nc_projections/default_nes.py @@ -14,7 +14,7 @@ from mpi4py import MPI from shapely.geometry import Polygon, Point from copy import deepcopy, copy from dateutil.relativedelta import relativedelta -from typing import Union, List +from typing import Union 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, \ diff --git a/nes/nes_formats/__init__.py b/nes/nes_formats/__init__.py index e09d18e..39aaf30 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 74bfc37..fc4dec3 100644 --- a/nes/nes_formats/cams_ra_format.py +++ b/nes/nes_formats/cams_ra_format.py @@ -21,7 +21,7 @@ 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: -- GitLab From 1861c908a4dd906da65457ca2f1f5d4c86965081 Mon Sep 17 00:00:00 2001 From: Carles Tena Date: Thu, 27 Jun 2024 10:55:31 +0200 Subject: [PATCH 06/11] Memory optimization --- environment.yml | 1 + nes/create_nes.py | 2 +- nes/load_nes.py | 2 +- nes/methods/cell_measures.py | 2 +- nes/methods/horizontal_interpolation.py | 53 +- nes/methods/vertical_interpolation.py | 4 +- nes/nc_projections/default_nes.py | 793 ++++++++++++------ nes/nc_projections/latlon_nes.py | 130 +-- nes/nc_projections/lcc_nes.py | 265 ++++-- nes/nc_projections/mercator_nes.py | 249 ++++-- nes/nc_projections/points_nes.py | 62 +- nes/nc_projections/points_nes_ghost.py | 24 +- nes/nc_projections/rotated_nes.py | 235 ++++-- nes/nc_projections/rotated_nested_nes.py | 39 +- nes/nes_formats/cams_ra_format.py | 12 +- nes/nes_formats/cmaq_format.py | 20 +- nes/nes_formats/monarch_format.py | 20 +- nes/nes_formats/wrf_chem_format.py | 24 +- tests/1.1-test_read_write_projection.py | 5 +- tests/1.2-test_create_projection.py | 2 +- tests/1.3-test_selecting.py | 13 +- tests/2.1-test_spatial_join.py | 3 +- tests/2.2-test_create_shapefile.py | 2 +- tests/2.3-test_bounds.py | 17 +- tests/2.4-test_cell_area.py | 2 +- tests/3.1-test_vertical_interp.py | 4 +- tests/3.2-test_horiz_interp_bilinear.py | 5 +- tests/3.3-test_horiz_interp_conservative.py | 4 +- tests/4.1-test_stats.py | 2 +- tests/4.2-test_sum.py | 2 +- tests/4.3-test_write_timestep.py | 2 +- tests/test_bash.mn5.sh | 2 +- ...sh_nord3v2.cmd => Jupyter_bash.nord3v2.sh} | 9 +- 33 files changed, 1292 insertions(+), 719 deletions(-) rename tutorials/{Jupyter_bash_nord3v2.cmd => Jupyter_bash.nord3v2.sh} (63%) diff --git a/environment.yml b/environment.yml index d2a1892..fe29057 100755 --- a/environment.yml +++ b/environment.yml @@ -14,5 +14,6 @@ dependencies: - python-eccodes - filelock - configargparse + - openpyxl - jupyter - ipykernel \ No newline at end of file diff --git a/nes/create_nes.py b/nes/create_nes.py index fbabb7c..ce8b619 100644 --- a/nes/create_nes.py +++ b/nes/create_nes.py @@ -15,7 +15,7 @@ def create_nes(comm=None, info=False, projection=None, parallel_method="Y", bala Parameters ---------- - comm : MPI.Communicator, optional + 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. diff --git a/nes/load_nes.py b/nes/load_nes.py index ff632c2..542b583 100644 --- a/nes/load_nes.py +++ b/nes/load_nes.py @@ -245,7 +245,7 @@ def concatenate_netcdfs(nessy_list, comm=None, info=False, parallel_method="Y", ---------- nessy_list : list A List of Nes objects or list of paths to concatenate. - comm : MPI.Communicator + comm : MPI.Comm MPI Communicator. info: bool Indicates if you want to get reading/writing info. diff --git a/nes/methods/cell_measures.py b/nes/methods/cell_measures.py index 0290ec3..185d033 100644 --- a/nes/methods/cell_measures.py +++ b/nes/methods/cell_measures.py @@ -14,7 +14,7 @@ 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 diff --git a/nes/methods/horizontal_interpolation.py b/nes/methods/horizontal_interpolation.py index f45186b..5d20f71 100644 --- a/nes/methods/horizontal_interpolation.py +++ b/nes/methods/horizontal_interpolation.py @@ -48,9 +48,9 @@ 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, @@ -61,6 +61,7 @@ def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind="Neares else: 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: @@ -78,6 +79,8 @@ def interpolate_horizontal(self, dst_grid, weight_matrix_path=None, kind="Neares # Copy NES final_dst = dst_grid.copy() + + sys.stdout.flush() final_dst.set_communicator(dst_grid.comm) # Remove original file information @@ -87,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 @@ -162,7 +165,7 @@ def __get_src_data(comm, var_data, idx, parallel_method): Parameters ---------- - comm : MPI.Communicator. + comm : MPI.Comm. MPI communicator. var_data : array Rank source data. @@ -419,7 +422,7 @@ def __read_weight_matrix(weight_matrix_path, comm=None, parallel_method="T"): ---------- weight_matrix_path : str Path of the weight matrix. - comm : MPI.Communicator + comm : MPI.Comm A Communicator to read the weight matrix. parallel_method : str Nes parallel method to read the weight matrix. @@ -443,7 +446,7 @@ def __read_weight_matrix(weight_matrix_path, comm=None, parallel_method="T"): return weight_matrix -# noinspection DuplicatedCode +# 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. @@ -466,25 +469,26 @@ def __create_nn_weight_matrix(self, dst_grid, n_neighbours=4, wm_path=None, info 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 = array(self._lat["data"], dtype=float32) - src_lon = array(self._lon["data"], dtype=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 = meshgrid(src_lon, src_lat) # Destination - dst_lat = array(dst_grid._lat["data"], dtype=float32) - dst_lon = array(dst_grid._lon["data"], dtype=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 = expand_dims(dst_grid._lat["data"], axis=0) - dst_lon = 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: @@ -515,8 +519,8 @@ def __create_nn_weight_matrix(self, dst_grid, n_neighbours=4, wm_path=None, info 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 @@ -533,7 +537,7 @@ def __create_nn_weight_matrix(self, dst_grid, n_neighbours=4, wm_path=None, info 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": arange(inverse_dists_transf.shape[1]), "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) @@ -656,8 +660,8 @@ def __create_area_conservative_weight_matrix(self, dst_nes, wm_path=None, flux=F 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 @@ -672,16 +676,9 @@ def __create_area_conservative_weight_matrix(self, dst_nes, wm_path=None, flux=F "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": empty(shape_flat), "units": "-"} weight_matrix.variables["weight"]["data"][:] = -999 diff --git a/nes/methods/vertical_interpolation.py b/nes/methods/vertical_interpolation.py index bf44e3f..23ca712 100644 --- a/nes/methods/vertical_interpolation.py +++ b/nes/methods/vertical_interpolation.py @@ -312,11 +312,11 @@ def interpolate_vertical(self, new_levels, new_src_vertical=None, kind="linear", # Update level information new_lev_info = {"data": array(new_levels)} - if "positive" in self._lev.keys(): + 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(): diff --git a/nes/nc_projections/default_nes.py b/nes/nc_projections/default_nes.py index ed74eeb..4861d44 100644 --- a/nes/nc_projections/default_nes.py +++ b/nes/nc_projections/default_nes.py @@ -14,7 +14,7 @@ from mpi4py import MPI from shapely.geometry import Polygon, Point from copy import deepcopy, copy from dateutil.relativedelta import relativedelta -from typing import Union +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, \ @@ -27,7 +27,7 @@ class Nes(object): Attributes ---------- - comm : MPI.Communicator + comm : MPI.Comm MPI communicator. rank : int MPI rank. @@ -55,30 +55,30 @@ class Nes(object): }, ... } - _time : List[datetime] + _full_time : List[datetime] Complete list of original time step values. - _lev : Dict[str, array] + _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. ... } - _lat : dict + _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. ... } - _lon : dict + _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. ... } - _lat_bnds : dict + _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. { @@ -86,7 +86,7 @@ class Nes(object): attr_name: attr_value, # Latitude bounds attributes. ... } - _lon_bnds : dict + _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. { @@ -174,9 +174,11 @@ class Nes(object): ... } """ - 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 @@ -242,9 +244,6 @@ class Nes(object): # Get minor and major axes of Earth self.earth_radius = self.get_earth_radius("WGS84") - self.projection_data = None - self.projection = None - # Time resolution and climatology will be modified, if needed, during the time variable reading self._time_resolution = "hours" self._climatology = False @@ -257,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": 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.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) @@ -300,42 +301,42 @@ class Nes(object): # 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() + # 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 @@ -348,14 +349,28 @@ class Nes(object): # 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"]] + 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", avoid_first_hours=0, avoid_last_hours=0, first_level=0, last_level=None, create_nes=False, @@ -415,7 +430,7 @@ class Nes(object): return None return strlen - + def set_strlen(self, strlen=75): """ Set the strlen @@ -442,18 +457,18 @@ 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 @@ -578,7 +593,7 @@ class Nes(object): return nessy - def get_full_times(self): + def get_full_times(self) -> List[datetime]: """ Retrieve the complete list of original time step values. @@ -587,9 +602,29 @@ class Nes(object): List[datetime] The complete list of original time step values from the netCDF data. """ - return self._time + 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. - def get_full_levels(self): + 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. @@ -604,7 +639,250 @@ class Nes(object): ... } """ - return self._lev + 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_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): """ @@ -627,7 +905,8 @@ class Nes(object): """ if new_direction not in ["up", "down"]: raise ValueError(f"Level direction mus be up or down. '{new_direction}' is not a valid option") - self._lev["positive"] = new_direction + if self.master: + self._full_lev["positive"] = new_direction self.lev["positive"] = new_direction return True @@ -641,12 +920,14 @@ class Nes(object): bool True if the direction was reversed successfully. """ - if "positive" in self._lev.keys(): - if self._lev["positive"] == "up": - self._lev["positive"] = "down" + 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" + if self.master: + self._full_lev["positive"] = "up" self.lev["positive"] = "up" return True @@ -725,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 @@ -742,7 +1022,7 @@ class Nes(object): """ 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 @@ -763,13 +1043,13 @@ class Nes(object): 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)) + msg += "(time:{0}, bnds:{1}). Time bounds will not be set.".format(len(self.time), len(time_bnds)) warn(msg) sys.stderr.flush() else: @@ -856,17 +1136,20 @@ 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 = abs(mean(diff(self._lat["data"]))) - lat_bnds = self._create_single_spatial_bounds(self._lat["data"], inc_lat, spatial_nv=2) - - self._lat_bnds = {"data": deepcopy(lat_bnds)} + 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"], :]} - - inc_lon = abs(mean(diff(self._lon["data"]))) - lon_bnds = self._create_single_spatial_bounds(self._lon["data"], inc_lon, spatial_nv=2) - self._lon_bnds = {"data": deepcopy(lon_bnds)} + # 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.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 @@ -967,11 +1250,13 @@ 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): """ @@ -1002,7 +1287,7 @@ 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] = {} @@ -1060,7 +1345,7 @@ class Nes(object): 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: @@ -1074,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: @@ -1085,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 @@ -1104,14 +1389,14 @@ class Nes(object): 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() @@ -1128,29 +1413,32 @@ class Nes(object): 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"]] - - 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 @@ -1163,19 +1451,28 @@ 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): """ @@ -1186,6 +1483,8 @@ class Nes(object): dict Dictionary with the index intervals """ + 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, @@ -1195,11 +1494,11 @@ class Nes(object): if self.lat_min is None: 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"]) @@ -1211,19 +1510,19 @@ class Nes(object): if self.lon_min is None: 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"]) @@ -1242,8 +1541,8 @@ class Nes(object): if self.parallel_method == "T": raise NotImplementedError("Statistics are not implemented on time axis parallelization method.") - aux_time = self._time[0].replace(hour=0, minute=0, second=0, microsecond=0) - self._time = [aux_time] + 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(): @@ -1320,7 +1619,7 @@ class Nes(object): 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) @@ -1344,7 +1643,7 @@ class Nes(object): 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) @@ -1364,10 +1663,11 @@ class Nes(object): 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]]] + 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: @@ -1410,10 +1710,10 @@ class Nes(object): if axis == "T": self.set_time_bnds([self.time[0], self.time[-1]]) self.time = [self.time[0]] - self._time = [self._time[0]] + self.set_full_times([self.time[0]]) if axis == "Z": self.lev["data"] = array([self.lev["data"][0]]) - self._lev["data"] = array([self._lev["data"][0]]) + self.set_full_levels(self.lev) return None @@ -1598,7 +1898,7 @@ class Nes(object): 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 @@ -1697,7 +1997,7 @@ class Nes(object): 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 @@ -1722,11 +2022,12 @@ 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 @@ -1929,7 +2230,6 @@ class Nes(object): time : List[datetime] List of times (datetime) of the NetCDF data. """ - if self.master: nc_var = self.dataset.variables["time"] time_data, units, calendar = self.__parse_time(nc_var) @@ -1943,7 +2243,6 @@ class Nes(object): 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") return time @@ -1963,8 +2262,8 @@ class Nes(object): A List of time bounds (datetime) of the NetCDF data. """ - if self.master: - if not create_nes: + 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: @@ -1992,8 +2291,6 @@ class Nes(object): else: time_bnds = None - time_bnds = self.comm.bcast(time_bnds, root=0) - self.free_vars("time_bnds") return time_bnds @@ -2015,12 +2312,13 @@ class Nes(object): Longitude bounds of the NetCDF data. """ - if self.master: - if not create_nes: + 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"][:])} else: @@ -2031,8 +2329,6 @@ 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"]) @@ -2087,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": array([0]), - "units": ""} + if self.master: + nc_var = {"data": array([0]), + "units": ""} + else: + nc_var = None return nc_var @@ -2299,11 +2600,6 @@ class Nes(object): 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( var_name)) else: @@ -2503,19 +2799,19 @@ class Nes(object): "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 = self._lat["data"].shape[0] + 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] + 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) + 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) @@ -2535,18 +2831,18 @@ 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] + len_to_split = my_shape[0] min_axis = "y_min" max_axis = "y_max" elif self.parallel_method == "X": - len_to_split = self._lon["data"].shape[-1] + len_to_split = my_shape[-1] min_axis = "x_min" max_axis = "x_max" elif self.parallel_method == "T": - len_to_split = len(self._time) + len_to_split = len(self.get_full_times()) min_axis = "t_min" max_axis = "t_max" else: @@ -2600,7 +2896,8 @@ class Nes(object): netcdf.createDimension("time", None) # Create time_nv (number of vertices) dimension - if self._time_bnds is not None: + 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 @@ -2637,22 +2934,24 @@ class Nes(object): """ # TIMES + 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, self._time[0].strftime("%Y-%m-%d %H:%M:%S")) + 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 self._time_bnds is not None: + if full_time_bnds is not None: if self._climatology: time_var.climatology = self._climatology_var_name else: 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, float64, ("time", "time_nv",), zlib=self.zip_lvl, complevel=self.zip_lvl) @@ -2661,65 +2960,70 @@ class Nes(object): 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 + 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"] + if "positive" in full_lev.keys(): + lev.positive = full_lev["positive"] if self.size > 1: lev.set_collective(True) - lev[:] = array(self._lev["data"], dtype=float32) + lev[:] = array(full_lev["data"], dtype=float32) # LATITUDES + 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: + if full_lat_bnds is not None: lat.bounds = "lat_bnds" if self.size > 1: lat.set_collective(True) - lat[:] = array(self._lat["data"], dtype=float32) + lat[:] = array(full_lat["data"], dtype=float32) # LATITUDES BOUNDS - if self._lat_bnds is not None: + 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[:] = array(self._lat_bnds["data"], dtype=float32) + lat_bnds_var[:] = array(full_lat_bnds["data"], dtype=float32) # LONGITUDES + 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: + if full_lon_bnds is not None: lon.bounds = "lon_bnds" if self.size > 1: lon.set_collective(True) - lon[:] = array(self._lon["data"], dtype=float32) + lon[:] = array(full_lon["data"], dtype=float32) # LONGITUDES BOUNDS - if self._lon_bnds is not None: + 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[:] = array(self._lon_bnds["data"], dtype=float32) + lon_bnds_var[:] = array(full_lon_bnds["data"], dtype=float32) return None @@ -2734,22 +3038,24 @@ class Nes(object): """ # TIMES + 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, self._time[0].strftime("%Y-%m-%d %H:%M:%S")) + 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 self._time_bnds is not None: + if full_time_bnds is not None: if self._climatology: time_var.climatology = self._climatology_var_name else: 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, float64, ("time", "time_nv",), zlib=self.zip_lvl, complevel=self.zip_lvl) @@ -2758,65 +3064,70 @@ class Nes(object): 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"] + 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: + 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, + 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: + 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, + 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 @@ -2950,7 +3261,7 @@ class Nes(object): 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)) if "data_aux" in var_dict.keys(): att_value = var_dict["data_aux"] if isinstance(att_value, int) and att_value == 0: @@ -3034,7 +3345,7 @@ class Nes(object): 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, @@ -3298,10 +3609,10 @@ class Nes(object): 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._time[0]).total_seconds() + 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._time[0]).total_seconds() + n_secs = (time - self.get_full_times()[0]).total_seconds() codes_set(clone_id, "forecastTime", int(n_secs)) # Level dependent keys @@ -3383,7 +3694,7 @@ 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 @@ -3417,10 +3728,8 @@ 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 = 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"]] + + fids = self.get_fids() gdf = GeoDataFrame(index=Index(name="FID", data=fids.ravel()), geometry=geometry, crs="EPSG:4326") self.shapefile = gdf @@ -3558,10 +3867,7 @@ class Nes(object): self.lat["data"][lat_ind])) # Create dataframe containing all points - fids = 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"]] + fids = self.get_fids() centroids_gdf = GeoDataFrame(index=Index(name="FID", data=fids.ravel()), geometry=centroids, crs="EPSG:4326") return centroids_gdf @@ -3629,10 +3935,8 @@ class Nes(object): 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 @@ -3916,23 +4220,6 @@ class Nes(object): return earth_radius_dict[ellps] - def get_fids(self): - """ - Obtain the FIDs in a 2D format. - - Returns - ------- - array - 2D array with the FID data. - """ - - fids = 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"]] - - return fids - def create_providentia_exp_centre_coordinates(self): """ Calculate centre latitudes and longitudes from original coordinates and store as 2D arrays. diff --git a/nes/nc_projections/latlon_nes.py b/nes/nc_projections/latlon_nes.py index 5038d0b..35d68c8 100644 --- a/nes/nc_projections/latlon_nes.py +++ b/nes/nc_projections/latlon_nes.py @@ -64,8 +64,8 @@ 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() @@ -136,63 +136,91 @@ class LatLonNes(Nes): return projection # noinspection DuplicatedCode - 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 "crs" in self.variables.keys(): - projection_data = self.variables["crs"] - self.free_vars("crs") - else: + 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. + + 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. + + """ + 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, + } - 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"] - - self.projection_data = projection_data - self.projection = self._get_pyproj_projection() - - 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 // 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"] + 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". @@ -205,11 +233,11 @@ 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): + if (self.lat_bnds is not None) and (self.lon_bnds is not None): netcdf.createDimension("spatial_nv", 2) return None diff --git a/nes/nc_projections/lcc_nes.py b/nes/nc_projections/lcc_nes.py index 64a2fa9..f9eda6e 100644 --- a/nes/nc_projections/lcc_nes.py +++ b/nes/nc_projections/lcc_nes.py @@ -5,6 +5,7 @@ from geopandas import GeoDataFrame from pandas import Index from pyproj import Proj from copy import deepcopy +from typing import Dict, Any from shapely.geometry import Polygon, Point from .default_nes import Nes @@ -14,9 +15,9 @@ class LCCNes(Nes): Attributes ---------- - _y : dict + _full_y : dict Y coordinates dictionary with the complete "data" key for all the values and the rest of the attributes. - _x : dict + _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. @@ -67,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, @@ -76,16 +79,16 @@ 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() @@ -140,6 +143,81 @@ class LCCNes(Nes): return new + 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): """ @@ -148,11 +226,11 @@ class LCCNes(Nes): 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() @@ -184,59 +262,52 @@ class LCCNes(Nes): 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 "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 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() + 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": "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"], - } + if "dimensions" in projection_data.keys(): + del projection_data["dimensions"] - self.projection_data = projection_data - self.projection = self._get_pyproj_projection() + if isinstance(projection_data["standard_parallel"], str): + projection_data["standard_parallel"] = [projection_data["standard_parallel"].split(", ")[0], + projection_data["standard_parallel"].split(", ")[1]] - return None + return projection_data # noinspection DuplicatedCode def _create_dimensions(self, netcdf): @@ -252,11 +323,11 @@ 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): + if (self.lat_bnds is not None) and (self.lon_bnds is not None): netcdf.createDimension("spatial_nv", 4) return None @@ -275,28 +346,30 @@ class LCCNes(Nes): super(LCCNes, self)._create_dimension_variables(netcdf) # LCC Y COORDINATES - y = netcdf.createVariable("y", self._y["data"].dtype, ("y",)) + 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 self._y.keys(): - y.units = self._y["units"] + if "units" in full_y.keys(): + y.units = full_y["units"] else: 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",)) + 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 self._x.keys(): - x.units = self._x["units"] + if "units" in full_x.keys(): + x.units = full_x["units"] else: 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 @@ -310,29 +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 = float64(self.projection_data["x_0"]) - inc_x = 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 = float64(self.projection_data["y_0"]) - inc_y = 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": linspace(x_0 + (inc_x / 2), x_0 + (inc_x / 2) + (inc_x * (nx - 1)), nx, dtype=float64)} - self._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 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 = array([self._x["data"]] * len(self._y["data"])) - y = 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) - # 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): """ @@ -405,24 +482,26 @@ class LCCNes(Nes): """ # Calculate LCC coordinates bounds - inc_x = abs(mean(diff(self._x["data"]))) - x_bnds = self._create_single_spatial_bounds(array([self._x["data"]] * len(self._y["data"])), + 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 = abs(mean(diff(self._y["data"]))) - y_bnds = self._create_single_spatial_bounds(array([self._y["data"]] * len(self._x["data"])).T, + 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 = {"data": deepcopy(lat_bnds)} + 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._lon_bnds = {"data": deepcopy(lon_bnds)} + 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"], :]} @@ -498,7 +577,7 @@ 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 diff --git a/nes/nc_projections/mercator_nes.py b/nes/nc_projections/mercator_nes.py index e7ac387..520f9bb 100644 --- a/nes/nc_projections/mercator_nes.py +++ b/nes/nc_projections/mercator_nes.py @@ -5,6 +5,7 @@ from geopandas import GeoDataFrame from pandas import Index from pyproj import Proj from copy import deepcopy +from typing import Dict, Any from shapely.geometry import Polygon, Point from nes.nc_projections.default_nes import Nes @@ -14,9 +15,9 @@ class MercatorNes(Nes): Attributes ---------- - _y : dict + _full_y : dict Y coordinates dictionary with the complete "data" key for all the values and the rest of the attributes. - _x : dict + _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. @@ -68,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, @@ -77,16 +80,16 @@ 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() @@ -141,6 +144,81 @@ class MercatorNes(Nes): return new + 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): """ @@ -149,11 +227,11 @@ class MercatorNes(Nes): 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() @@ -178,50 +256,44 @@ class MercatorNes(Nes): return projection # noinspection DuplicatedCode - def _get_projection(self): + def _get_projection_data(self, create_nes, **kwargs): """ - Get "projection" and "projection_data" from grid details. - """ - - if "mercator" in self.variables.keys(): - projection_data = self.variables["mercator"] - self.free_vars("mercator") + 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"] - - self.projection_data = projection_data - self.projection = self._get_pyproj_projection() + else: + msg = "There is no variable called mercator, projection has not been defined." + raise RuntimeError(msg) - return None - - def _create_projection(self, **kwargs): - """ - Create "projection" and "projection_data" from projection arguments. - """ + if "dtype" in projection_data.keys(): + del projection_data["dtype"] - 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"], - } + 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 # noinspection DuplicatedCode def _create_dimensions(self, netcdf): @@ -237,11 +309,11 @@ 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): + if (self.lat_bnds is not None) and (self.lon_bnds is not None): netcdf.createDimension("spatial_nv", 4) return None @@ -260,28 +332,30 @@ class MercatorNes(Nes): super(MercatorNes, self)._create_dimension_variables(netcdf) # MERCATOR Y COORDINATES - y = netcdf.createVariable("y", self._y["data"].dtype, ("y",)) + 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 self._y.keys(): - y.units = self._y["units"] + if "units" in full_y.keys(): + y.units = full_y["units"] else: 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",)) + 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 self._x.keys(): - x.units = self._x["units"] + if "units" in full_x.keys(): + x.units = full_x["units"] else: 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 @@ -290,29 +364,33 @@ class MercatorNes(Nes): """ 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 = float64(self.projection_data["x_0"]) - inc_x = 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 = float64(self.projection_data["y_0"]) + inc_y = float64(self.projection_data["inc_y"]) + ny = int(self.projection_data["ny"]) - # Create a regular grid in metres (1D) - self._x = {"data": linspace(x_0 + (inc_x / 2), x_0 + (inc_x / 2) + (inc_x * (nx - 1)), nx, dtype=float64)} - self._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._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 to 2D) - x = array([self._x["data"]] * len(self._y["data"])) - y = array([self._y["data"]] * len(self._x["data"])).T + # 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 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): """ @@ -386,24 +464,25 @@ class MercatorNes(Nes): """ # Calculate Mercator coordinates bounds - inc_x = abs(mean(diff(self._x["data"]))) - x_bnds = self._create_single_spatial_bounds(array([self._x["data"]] * len(self._y["data"])), + 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 = abs(mean(diff(self._y["data"]))) - y_bnds = self._create_single_spatial_bounds(array([self._y["data"]] * len(self._x["data"])).T, + 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 = {"data": deepcopy(lat_bnds)} + 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._lon_bnds = {"data": deepcopy(lon_bnds)} + 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"], :]} @@ -478,7 +557,7 @@ 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 diff --git a/nes/nc_projections/points_nes.py b/nes/nc_projections/points_nes.py index 9f50e8b..61adc2c 100644 --- a/nes/nc_projections/points_nes.py +++ b/nes/nc_projections/points_nes.py @@ -5,6 +5,7 @@ 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 from netCDF4 import date2num from .default_nes import Nes @@ -37,7 +38,7 @@ class PointsNes(Nes): Parameters ---------- - comm: MPI.Communicator + comm: MPI.Comm MPI Communicator. path: str Path to the NetCDF to initialize the object. @@ -75,11 +76,11 @@ class PointsNes(Nes): 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": 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") @@ -135,23 +136,32 @@ 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 @@ -169,12 +179,12 @@ class PointsNes(Nes): netcdf.createDimension("time", None) # Create time_nv (number of vertices) dimension - if self._time_bnds is not None: + 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: @@ -196,25 +206,25 @@ class PointsNes(Nes): # TIMES time_var = netcdf.createVariable("time", 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")) + 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: + 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): + 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: + 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", float64, ("station",), zlib=self.zip_lvl > 0, @@ -233,11 +243,11 @@ class PointsNes(Nes): lat.axis = "Y" lat.long_name = "latitude coordinate" lat.standard_name = "latitude" - if self._lat_bnds is not None: + 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", float64, self._lon_dim, zlib=self.zip_lvl > 0, complevel=self.zip_lvl) @@ -245,11 +255,11 @@ class PointsNes(Nes): lon.axis = "X" lon.long_name = "longitude coordinate" lon.standard_name = "longitude" - if self._lon_bnds is not None: + 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 @@ -687,7 +697,7 @@ class PointsNes(Nes): centroids = points_from_xy(self.lon["data"], self.lat["data"]) # Create dataframe containing all points - fids = arange(len(self._lon["data"])) + 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, diff --git a/nes/nc_projections/points_nes_ghost.py b/nes/nc_projections/points_nes_ghost.py index 1c75d68..e394f03 100644 --- a/nes/nc_projections/points_nes_ghost.py +++ b/nes/nc_projections/points_nes_ghost.py @@ -152,25 +152,25 @@ class PointsNesGHOST(PointsNes): # TIMES time_var = netcdf.createVariable("time", 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")) + 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: + 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: + 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", float64, ("station",), zlib=self.zip_lvl > 0, @@ -189,11 +189,11 @@ class PointsNesGHOST(PointsNes): lat.axis = "Y" lat.long_name = "latitude coordinate" lat.standard_name = "latitude" - if self._lat_bnds is not None: + 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", float64, self._lon_dim, zlib=self.zip_lvl > 0, complevel=self.zip_lvl) @@ -201,11 +201,11 @@ class PointsNesGHOST(PointsNes): lon.axis = "X" lon.long_name = "longitude coordinate" lon.standard_name = "longitude" - if self._lon_bnds is not None: + 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): @@ -213,8 +213,8 @@ class PointsNesGHOST(PointsNes): last_time_idx = self._get_time_id(self.hours_end, first=False) t_len = last_time_idx - first_time_idx - self._qa["data"] = empty((len(self._lon["data"]), t_len, 0)) - self._flag["data"] = 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 diff --git a/nes/nc_projections/rotated_nes.py b/nes/nc_projections/rotated_nes.py index 26033a3..31cc45c 100644 --- a/nes/nc_projections/rotated_nes.py +++ b/nes/nc_projections/rotated_nes.py @@ -7,6 +7,7 @@ from geopandas import GeoDataFrame from pandas import Index from pyproj import Proj from copy import deepcopy +from typing import Dict, Any from shapely.geometry import Polygon, Point from .default_nes import Nes @@ -16,9 +17,9 @@ class RotatedNes(Nes): Attributes ---------- - _rlat : dict + _full_rlat : dict Rotated latitudes dictionary with the complete "data" key for all the values and the rest of the attributes. - _rlon : dict + _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. @@ -69,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, @@ -78,17 +81,19 @@ 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() @@ -141,6 +146,81 @@ class RotatedNes(Nes): return new + 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): """ @@ -149,11 +229,15 @@ class RotatedNes(Nes): 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") + + 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() @@ -180,50 +264,44 @@ class RotatedNes(Nes): return projection # noinspection DuplicatedCode - 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 "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"] - - self.projection_data = projection_data - self.projection = self._get_pyproj_projection() + 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) - return None - - def _create_projection(self, **kwargs): - """ - Create "projection" and "projection_data" from projection arguments. - """ + if "dtype" in projection_data.keys(): + del projection_data["dtype"] - 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): """ @@ -237,12 +315,13 @@ 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): + if (self.lat_bnds is not None) and (self.lon_bnds is not None): netcdf.createDimension("spatial_nv", 4) pass @@ -257,32 +336,33 @@ class RotatedNes(Nes): 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.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.standard_name = "grid_longitude" if self.size > 1: rlon.set_collective(True) - rlon[:] = self._rlon["data"] + rlon[:] = full_rlon["data"] return None @@ -297,7 +377,6 @@ class RotatedNes(Nes): _rlon : dict Rotated longitudes dictionary with the "data" key for all the values and the rest of the attributes. """ - # Get grid resolution inc_rlon = float64(self.projection_data["inc_rlon"]) inc_rlat = float64(self.projection_data["inc_rlat"]) @@ -383,15 +462,17 @@ 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(array([self._rlon["data"]] * len(self._rlat["data"])), - 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): """ @@ -466,24 +547,26 @@ class RotatedNes(Nes): """ # Calculate rotated coordinates bounds - inc_rlat = abs(mean(diff(self._rlat["data"]))) - rlat_bnds = self._create_single_spatial_bounds(array([self._rlat["data"]] * len(self._rlon["data"])).T, + 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 = abs(mean(diff(self._rlon["data"]))) - rlon_bnds = self._create_single_spatial_bounds(array([self._rlon["data"]] * len(self._rlat["data"])), + 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 = {"data": deepcopy(lat_bnds)} + 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._lon_bnds = {"data": deepcopy(lon_bnds)} + 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"], :]} @@ -557,7 +640,7 @@ 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 diff --git a/nes/nc_projections/rotated_nested_nes.py b/nes/nc_projections/rotated_nested_nes.py index ab056ca..4517701 100644 --- a/nes/nc_projections/rotated_nested_nes.py +++ b/nes/nc_projections/rotated_nested_nes.py @@ -87,26 +87,31 @@ class RotatedNestedNes(RotatedNes): 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): """ diff --git a/nes/nes_formats/cams_ra_format.py b/nes/nes_formats/cams_ra_format.py index fc4dec3..480becc 100644 --- a/nes/nes_formats/cams_ra_format.py +++ b/nes/nes_formats/cams_ra_format.py @@ -79,8 +79,8 @@ def create_dimensions(self, netcdf): 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 @@ -106,7 +106,7 @@ def create_dimension_variables(self, netcdf): if self.size > 1: lat.set_collective(True) - lat[:] = self._lat["data"] + lat[:] = self.get_full_latitudes()["data"] # LONGITUDES lon = netcdf.createVariable("lon", float64, ("lon",)) @@ -116,7 +116,7 @@ def create_dimension_variables(self, netcdf): 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", float64, ("time",)) @@ -126,8 +126,8 @@ def create_dimension_variables(self, netcdf): 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[:] = __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 diff --git a/nes/nes_formats/cmaq_format.py b/nes/nes_formats/cmaq_format.py index d6cef43..30a5cea 100644 --- a/nes/nes_formats/cmaq_format.py +++ b/nes/nes_formats/cmaq_format.py @@ -254,8 +254,8 @@ def set_global_attributes(self): # Projection dependent attributes if isinstance(self, nes.LCCNes): - self.global_attrs["NCOLS"] = int32(len(self._x["data"])) - self.global_attrs["NROWS"] = int32(len(self._y["data"])) + 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) @@ -265,11 +265,11 @@ def set_global_attributes(self): 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._x["data"][0]) - (float64(self._x["data"][1] - self._x["data"][0]) / 2) + self._full_x["data"][0]) - (float64(self._full_x["data"][1] - self._full_x["data"][0]) / 2) self.global_attrs["YORIG"] = float64( - self._y["data"][0]) - (float64(self._y["data"][1] - self._y["data"][0]) / 2) - self.global_attrs["XCELL"] = float64(self._x["data"][1] - self._x["data"][0]) - self.global_attrs["YCELL"] = float64(self._y["data"][1] - self._y["data"][0]) + 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 @@ -286,13 +286,13 @@ def create_dimensions(self, netcdf): netcdf4-python open dataset. """ - netcdf.createDimension("TSTEP", len(self._time)) + netcdf.createDimension("TSTEP", len(self.get_full_times())) netcdf.createDimension("DATE-TIME", 2) - netcdf.createDimension("LAY", len(self._lev["data"])) + 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 diff --git a/nes/nes_formats/monarch_format.py b/nes/nes_formats/monarch_format.py index c7d67e2..0a50e75 100644 --- a/nes/nes_formats/monarch_format.py +++ b/nes/nes_formats/monarch_format.py @@ -39,17 +39,19 @@ def to_netcdf_monarch(self, path, chunking=False, keep_open=False): self._create_dimensions(netcdf) # Create dimension variables - self._lev["data"] = array(self._lev["data"], dtype=float32) - self._lat["data"] = array(self._lat["data"], dtype=float32) - self._lat_bnds["data"] = array(self._lat_bnds["data"], dtype=float32) - self._lon["data"] = array(self._lon["data"], dtype=float32) - self._lon_bnds["data"] = array(self._lon_bnds["data"], dtype=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"] = array(self._rlat["data"], dtype=float32) - self._rlon["data"] = array(self._rlon["data"], dtype=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"] = array(self._y["data"], dtype=float32) - self._x["data"] = array(self._x["data"], dtype=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: diff --git a/nes/nes_formats/wrf_chem_format.py b/nes/nes_formats/wrf_chem_format.py index 4959eec..6a06af4 100644 --- a/nes/nes_formats/wrf_chem_format.py +++ b/nes/nes_formats/wrf_chem_format.py @@ -279,19 +279,19 @@ def set_global_attributes(self): # Projection dependent attributes if isinstance(self, nes.LCCNes) or isinstance(self, nes.MercatorNes): - self.global_attrs["WEST-EAST_GRID_DIMENSION"] = int32(len(self._x["data"]) + 1) - self.global_attrs["SOUTH-NORTH_GRID_DIMENSION"] = int32(len(self._y["data"]) + 1) - self.global_attrs["DX"] = float32(self._x["data"][1] - self._x["data"][0]) - self.global_attrs["DY"] = float32(self._y["data"][1] - self._y["data"][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._x["data"])) + 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._x["data"]) + 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._y["data"])) + 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._y["data"]) + 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) @@ -328,12 +328,12 @@ def create_dimensions(self, netcdf): netcdf4-python open dataset. """ - netcdf.createDimension("Time", len(self._time)) + netcdf.createDimension("Time", len(self.get_full_times())) netcdf.createDimension("DateStrLen", 19) - netcdf.createDimension("emissions_zdim", len(self._lev["data"])) + 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 diff --git a/tests/1.1-test_read_write_projection.py b/tests/1.1-test_read_write_projection.py index cd47aae..5788b30 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 fe6f0ba..60c470a 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 68106b1..00bbb23 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 98a482d..e24d443 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 4c44ff0..6d443a7 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 a761858..a2a9c1c 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 49821a0..9db836f 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 bbee578..9b78628 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 3018881..4366a8d 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 a10f27d..90aa72b 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 0392d9c..f11206c 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 2fb9129..2f1a93c 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 77539e7..b50c74b 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.mn5.sh b/tests/test_bash.mn5.sh index 9cf3ce2..13e0a1b 100644 --- a/tests/test_bash.mn5.sh +++ b/tests/test_bash.mn5.sh @@ -3,7 +3,7 @@ #SBATCH -A bsc32 #SBATCH --cpus-per-task=1 #SBATCH -n 4 -#SBATCH -t 02:00:00 +#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 diff --git a/tutorials/Jupyter_bash_nord3v2.cmd b/tutorials/Jupyter_bash.nord3v2.sh similarity index 63% rename from tutorials/Jupyter_bash_nord3v2.cmd rename to tutorials/Jupyter_bash.nord3v2.sh index c6a91fa..8b38a16 100644 --- a/tutorials/Jupyter_bash_nord3v2.cmd +++ b/tutorials/Jupyter_bash.nord3v2.sh @@ -25,7 +25,14 @@ 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 +# module load NES/1.1.3-nord3-v2-foss-2019b-Python-3.7.4 + +source /gpfs/projects/bsc32/software/suselinux/11/software/Miniconda3/4.7.10/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 # DON'T USE ADDRESS BELOW. -- GitLab From c749057c233881d4935a4cdb98e56c09ba205130 Mon Sep 17 00:00:00 2001 From: Carles Tena Date: Fri, 28 Jun 2024 13:58:26 +0200 Subject: [PATCH 07/11] Refactor code to avoid IDE warnings --- nes/nc_projections/default_nes.py | 26 ++++++++++---------- nes/nc_projections/points_nes.py | 3 +-- nes/nc_projections/points_nes_ghost.py | 5 ++-- nes/nc_projections/points_nes_providentia.py | 5 ++-- nes/nc_projections/rotated_nes.py | 5 ++-- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/nes/nc_projections/default_nes.py b/nes/nc_projections/default_nes.py index 4861d44..5b0d492 100644 --- a/nes/nc_projections/default_nes.py +++ b/nes/nc_projections/default_nes.py @@ -1258,7 +1258,7 @@ class Nes(object): 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. @@ -1266,8 +1266,8 @@ class Nes(object): ---------- 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 ------- @@ -1275,7 +1275,7 @@ class Nes(object): 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: @@ -3466,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. @@ -3483,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 + 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 @@ -3509,15 +3509,15 @@ 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 = f"Unknown NetCDF type '{nc_type}'. " @@ -3527,7 +3527,7 @@ class Nes(object): 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": self.__to_netcdf_cams_ra(path) diff --git a/nes/nc_projections/points_nes.py b/nes/nc_projections/points_nes.py index 61adc2c..365b9ef 100644 --- a/nes/nc_projections/points_nes.py +++ b/nes/nc_projections/points_nes.py @@ -215,7 +215,7 @@ class PointsNes(Nes): if self.size > 1: time_var.set_collective(True) 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)], + self._get_time_id(self.hours_end, first=False)], time_var.units, time_var.calendar) # TIME BOUNDS @@ -553,7 +553,6 @@ class PointsNes(Nes): sys.stderr.write(str(e)) sys.stderr.flush() self.comm.Abort(1) - raise e return data_list diff --git a/nes/nc_projections/points_nes_ghost.py b/nes/nc_projections/points_nes_ghost.py index e394f03..25b7d3b 100644 --- a/nes/nc_projections/points_nes_ghost.py +++ b/nes/nc_projections/points_nes_ghost.py @@ -525,7 +525,6 @@ class PointsNesGHOST(PointsNes): sys.stderr.write(str(e)) sys.stderr.flush() self.comm.Abort(1) - raise e return data_list @@ -563,7 +562,7 @@ class PointsNesGHOST(PointsNes): return None - 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. @@ -571,7 +570,7 @@ class PointsNesGHOST(PointsNes): Parameters ---------- keep_open : bool - type : str + nc_type : str path : str Path to the output netCDF file. compression_level : int diff --git a/nes/nc_projections/points_nes_providentia.py b/nes/nc_projections/points_nes_providentia.py index 3bcb105..6a014e6 100644 --- a/nes/nc_projections/points_nes_providentia.py +++ b/nes/nc_projections/points_nes_providentia.py @@ -562,11 +562,10 @@ class PointsNesProvidentia(PointsNes): # 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, 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. @@ -583,7 +582,7 @@ class PointsNesProvidentia(PointsNes): 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 + 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 diff --git a/nes/nc_projections/rotated_nes.py b/nes/nc_projections/rotated_nes.py index 31cc45c..c5c3794 100644 --- a/nes/nc_projections/rotated_nes.py +++ b/nes/nc_projections/rotated_nes.py @@ -467,8 +467,9 @@ class RotatedNes(Nes): self._full_rlat, self._full_rlon = self._create_rotated_coordinates() # 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) + 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} else: -- GitLab From 7484d3f3cd196e556fe1e2861f00fbe9484eabd4 Mon Sep 17 00:00:00 2001 From: cpinero Date: Thu, 4 Jul 2024 14:24:06 +0200 Subject: [PATCH 08/11] Remove add_offset and scale_factor from variable. --- nes/nc_projections/default_nes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nes/nc_projections/default_nes.py b/nes/nc_projections/default_nes.py index 2433863..670bb6a 100644 --- a/nes/nc_projections/default_nes.py +++ b/nes/nc_projections/default_nes.py @@ -2024,7 +2024,7 @@ class Nes(object): # 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 = '' -- GitLab From b1dee5d7c6e7a8a606a2e9ff79fa72592a8c8395 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Cortezon Date: Mon, 8 Jul 2024 14:49:06 +0200 Subject: [PATCH 09/11] Update tutorial --- .../2.Creation/2.3.Create_Points_XVPCA.ipynb | 1327 ++++++++++++++--- 1 file changed, 1113 insertions(+), 214 deletions(-) diff --git a/tutorials/2.Creation/2.3.Create_Points_XVPCA.ipynb b/tutorials/2.Creation/2.3.Create_Points_XVPCA.ipynb index f5eca19..1055297 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" ] }, -- GitLab From 22a3c82ecebc05643f6f00eca3ce5a6cb1dfee8e Mon Sep 17 00:00:00 2001 From: Carles Tena Date: Fri, 20 Sep 2024 15:35:10 +0200 Subject: [PATCH 10/11] Some general updates --- CHANGELOG.rst | 8 +++++ nes/__init__.py | 4 +-- nes/methods/horizontal_interpolation.py | 6 ++-- setup.py | 2 +- tutorials/Jupyter_bash.nord3v2.sh | 40 ------------------------- 5 files changed, 15 insertions(+), 45 deletions(-) delete mode 100644 tutorials/Jupyter_bash.nord3v2.sh diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a382275..344a302 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,14 @@ CHANGELOG .. start-here +1.1.5 +============ + +* Release date: 2024/08/14 +* Changes and new features: + + * to_netcdf function changes the type argument to nc_type + 1.1.4 ============ diff --git a/nes/__init__.py b/nes/__init__.py index c28657e..76e058a 100644 --- a/nes/__init__.py +++ b/nes/__init__.py @@ -1,5 +1,5 @@ -__date__ = "2024-05-31" -__version__ = "1.1.4" +__date__ = "2024-08-14" +__version__ = "1.1.5" from .load_nes import open_netcdf, concatenate_netcdfs # from .load_nes import open_raster diff --git a/nes/methods/horizontal_interpolation.py b/nes/methods/horizontal_interpolation.py index 5d20f71..25efef6 100644 --- a/nes/methods/horizontal_interpolation.py +++ b/nes/methods/horizontal_interpolation.py @@ -373,7 +373,10 @@ def __get_weights_idx_xy_axis(self, dst_grid, weight_matrix_path, kind, n_neighb 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) else: @@ -474,7 +477,6 @@ def __create_nn_weight_matrix(self, dst_grid, n_neighbours=4, wm_path=None, info print("\tCreating Nearest Neighbour Weight Matrix with {0} neighbours".format(n_neighbours)) sys.stdout.flush() # Source - src_lat = array(self._full_lat["data"], dtype=float32) src_lon = array(self._full_lon["data"], dtype=float32) diff --git a/setup.py b/setup.py index 4d77e0c..fa627ce 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/tutorials/Jupyter_bash.nord3v2.sh b/tutorials/Jupyter_bash.nord3v2.sh deleted file mode 100644 index 8b38a16..0000000 --- a/tutorials/Jupyter_bash.nord3v2.sh +++ /dev/null @@ -1,40 +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 - -source /gpfs/projects/bsc32/software/suselinux/11/software/Miniconda3/4.7.10/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 - - -# DON'T USE ADDRESS BELOW. -# DO USE TOKEN BELOW -jupyter-lab --no-browser --port=${port} --ip=${node} -- GitLab From e4d394a4dcc7f31ffe8d3916c55a16749ff6d7a2 Mon Sep 17 00:00:00 2001 From: Carles Tena Date: Fri, 20 Sep 2024 15:36:59 +0200 Subject: [PATCH 11/11] Releasing --- CHANGELOG.rst | 3 ++- nes/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 344a302..bcbcb43 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,10 +7,11 @@ CHANGELOG 1.1.5 ============ -* Release date: 2024/08/14 +* 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/nes/__init__.py b/nes/__init__.py index 76e058a..e315033 100644 --- a/nes/__init__.py +++ b/nes/__init__.py @@ -1,4 +1,4 @@ -__date__ = "2024-08-14" +__date__ = "2024-09-20" __version__ = "1.1.5" from .load_nes import open_netcdf, concatenate_netcdfs -- GitLab