From 73781238eeeea956f76227a5f0490e23d7556c8b Mon Sep 17 00:00:00 2001 From: Carles Tena Date: Sat, 5 Jul 2025 10:37:51 +0200 Subject: [PATCH 1/5] Entry points into a single nes command --- Makefile | 1 + environment.yml | 3 + nes/entry_points/__init__.py | 6 ++ nes/entry_points/checker.py | 47 +++++++++++++ nes/entry_points/cli.py | 87 +++++++++++++++++++++++++ nes/entry_points/reorder_longitudes.py | 44 +++++++++++++ nes/utilities/checker.py | 49 -------------- nes/utilities/reorder_longitudes_cli.py | 38 ----------- requirements.txt | 9 ++- setup.py | 3 +- 10 files changed, 197 insertions(+), 90 deletions(-) create mode 100644 nes/entry_points/__init__.py create mode 100644 nes/entry_points/checker.py create mode 100644 nes/entry_points/cli.py create mode 100644 nes/entry_points/reorder_longitudes.py delete mode 100644 nes/utilities/checker.py delete mode 100644 nes/utilities/reorder_longitudes_cli.py diff --git a/Makefile b/Makefile index 6c8788b..1a198bc 100644 --- a/Makefile +++ b/Makefile @@ -47,6 +47,7 @@ create_activate_deactivate_dirs: write_activate_vars_script: echo '#!/bin/bash' > $(CONDA_ENV_PATH)/etc/conda/activate.d/env_vars.sh echo 'export SLURM_CPU_BIND=none' >> $(CONDA_ENV_PATH)/etc/conda/activate.d/env_vars.sh + echo 'eval "$$(register-python-argcomplete nes)"' >> $(CONDA_ENV_PATH)/etc/conda/activate.d/env_vars.sh #Task to write the env_vars.sh inside deactivate.d to unset those variables when the environment is deactivated write_deactivate_vars_script: diff --git a/environment.yml b/environment.yml index e2e658a..a1943e8 100755 --- a/environment.yml +++ b/environment.yml @@ -27,6 +27,9 @@ dependencies: - openpyxl - jupyter - ipykernel + - pip + - pip: + - argcomplete variables: CC: mpicc diff --git a/nes/entry_points/__init__.py b/nes/entry_points/__init__.py new file mode 100644 index 0000000..cb8e474 --- /dev/null +++ b/nes/entry_points/__init__.py @@ -0,0 +1,6 @@ +from .checker import run_checks +from .reorder_longitudes import reorder_longitudes + +__all__ = [ + 'run_checks', 'reorder_longitudes' +] \ No newline at end of file diff --git a/nes/entry_points/checker.py b/nes/entry_points/checker.py new file mode 100644 index 0000000..fb81228 --- /dev/null +++ b/nes/entry_points/checker.py @@ -0,0 +1,47 @@ +from nes.load_nes import open_netcdf +from numpy import isinf, isnan + + +def run_checks(file: str, check_nan: bool = True, check_inf: bool = True): + """ + Check for NaN and/or Inf values in all variables of a NetCDF file. + + Parameters + ---------- + file : str + Path to the input NetCDF file. + check_nan : bool, optional + Whether to check for NaN (Not a Number) values. Default is True. + check_inf : bool, optional + Whether to check for infinite (Inf) values. Default is True. + + Raises + ------ + ValueError + If any variable contains NaN or Inf values, a ValueError is raised + indicating the variable name. + + Notes + ----- + This function uses the NES `open_netcdf()` interface to load the file and + reads all variables into memory before performing the checks. + """ + # Open the NetCDF file with NES (metadata only) + dataset = open_netcdf(file) + + # Load all variable data into memory + dataset.load() + + # Loop over all variables in the dataset + for var_name, var_info in dataset.variables.items(): + # Check for Inf values, if requested + has_inf = isinf(var_info["data"]).any() if check_inf else False + + # Check for NaN values, if requested + has_nan = isnan(var_info["data"]).any() if check_nan else False + + # Raise an error if problematic values are found + if has_nan or has_inf: + raise ValueError(f"Variable '{var_name}' contains NaN or Inf values.") + + return True \ No newline at end of file diff --git a/nes/entry_points/cli.py b/nes/entry_points/cli.py new file mode 100644 index 0000000..340bfcd --- /dev/null +++ b/nes/entry_points/cli.py @@ -0,0 +1,87 @@ +import traceback +from mpi4py import MPI +from configargparse import ArgParser +import argcomplete + + +def _add_check_subparser(subparsers): + """ + Add the 'check' subcommand to the CLI. + + Parameters + ---------- + subparsers : argparse._SubParsersAction + The subparsers object returned by `add_subparsers()` on the main parser. + """ + from nes.entry_points import run_checks + + check_parser = subparsers.add_parser("check", help="Run checks on a NetCDF file") + check_parser.add_argument("-f", "--file", required=True, help="Input NetCDF file path") + check_parser.add_argument("--nan", dest="check_nan", action="store_true", help="Check for NaN values") + check_parser.add_argument("--no-nan", dest="check_nan", action="store_false", help="Do not check NaN values") + check_parser.add_argument("--inf", dest="check_inf", action="store_true", help="Check for Inf values") + check_parser.add_argument("--no-inf", dest="check_inf", action="store_false", help="Do not check Inf values") + check_parser.set_defaults(check_nan=True, check_inf=True) + check_parser.set_defaults(func=run_checks) + + +def _add_reorder_subparser(subparsers): + """ + Add the 'reorder' subcommand to the CLI. + + Parameters + ---------- + subparsers : argparse._SubParsersAction + The subparsers object returned by `add_subparsers()` on the main parser. + """ + from nes.entry_points import reorder_longitudes + + reorder_parser = subparsers.add_parser("reorder", help="Reorder longitudes in a NetCDF file") + reorder_parser.add_argument("-f", "--file", required=True, help="Input NetCDF file path") + reorder_parser.add_argument("-o", "--output", help="Output NetCDF file path") + reorder_parser.set_defaults(func=reorder_longitudes) + + +def _filter_args(func, args_namespace): + """ + Filters the argparse namespace to only include arguments that match the function's signature. + """ + import inspect + sig = inspect.signature(func) + arg_keys = set(sig.parameters.keys()) + args_dict = vars(args_namespace) + filtered = {k: args_dict[k] for k in arg_keys if k in args_dict} + return filtered + + +def main(): + """ + Main entry point for the NES command-line interface. + + Defines subcommands and parses arguments using `configargparse`. This allows + command-line configuration and also supports loading options from config files. + """ + parser = ArgParser( + description="NES - NetCDF for Earth Science utilities", + default_config_files=['~/.nes_config'] + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + _add_check_subparser(subparsers) + _add_reorder_subparser(subparsers) + + # Enable autocomplete + argcomplete.autocomplete(parser) + + args = parser.parse_args() + + try: + filtered_args = _filter_args(args.func, args) + args.func(**filtered_args) + except Exception as e: + print(f"Process {MPI.COMM_WORLD.Get_rank()}: NES critical error detected {e}, aborting MPI.", flush=True) + print(f"Process {MPI.COMM_WORLD.Get_rank()}: Traceback:\n{traceback.format_exc()}", flush=True) + + MPI.COMM_WORLD.Abort(1) + + return \ No newline at end of file diff --git a/nes/entry_points/reorder_longitudes.py b/nes/entry_points/reorder_longitudes.py new file mode 100644 index 0000000..111e198 --- /dev/null +++ b/nes/entry_points/reorder_longitudes.py @@ -0,0 +1,44 @@ +from nes.load_nes import open_netcdf +from mpi4py import MPI + + +def reorder_longitudes(infile: str, outfile: str): + """ + Convert longitudes in a NetCDF file to the [-180, 180] range and save the modified file. + + Parameters + ---------- + infile : str + Path to the input NetCDF file. + outfile : str + Path where the reordered NetCDF file will be saved. + + Raises + ------ + ValueError + If the script is run using more than one MPI process. + + Notes + ----- + This function must be executed in serial mode only. + It uses `convert_longitudes()` from the NES API, which updates coordinate values + and ensures variables depending on longitude are shifted accordingly. + """ + # Enforce serial execution (1 MPI process) + comm = MPI.COMM_WORLD + if comm.Get_size() > 1: + raise ValueError("This script must be run with a single process (serial mode only).") + + # Open the input NetCDF file + nc = open_netcdf(infile) + + # Load data into memory + nc.load() + + # Reorder longitudes from [0, 360] to [-180, 180] + nc.convert_longitudes() + + # Save the result to the output path + nc.to_netcdf(outfile) + + return True \ No newline at end of file diff --git a/nes/utilities/checker.py b/nes/utilities/checker.py deleted file mode 100644 index 673400c..0000000 --- a/nes/utilities/checker.py +++ /dev/null @@ -1,49 +0,0 @@ -from ..load_nes import open_netcdf -import argparse -from numpy import isinf, isnan - -def run_checks(): - parser = argparse.ArgumentParser(description="Check NaN in a NetCDF file.") - - # Define expected arguments - parser.add_argument("--file", "-f", type=str, help="Input NetCDF file path") - parser.add_argument("--nan", "-n", type=bool, default=True, help="Check NaNs") - parser.add_argument("--inf", "-i", type=bool, default=True, help="Check infs") - - - # Parse arguments - args = parser.parse_args() - - # Call your function with parsed arguments - infile = args.file - do_nans = args.nan - do_infs = args.inf - - # Lee solo metadatos - nessy = open_netcdf(infile) - - # nessy.variables = {'var1': {'data': None, units= kg}} - # Lee matrices - nessy.load() - # nessy.variables = {'var1': {'data': ARRAY, units= kg}} - - for var_name, var_info in nessy.variables.items(): - # var_name = 'var_1' - # var_info = {'data: np.array, units: 'kg'} - if do_infs: - has_inf = isinf(var_info['data']).any() - else: - has_inf = False - - if do_nans: - has_nan = isnan(var_info['data']).any() - else: - has_nan = False - - if has_inf or has_nan: - ValueError(f"{var_name} contains NaN or Inf") - else: - pass - return - -# bash_funcion -f -n My_File \ No newline at end of file diff --git a/nes/utilities/reorder_longitudes_cli.py b/nes/utilities/reorder_longitudes_cli.py deleted file mode 100644 index 8f97c80..0000000 --- a/nes/utilities/reorder_longitudes_cli.py +++ /dev/null @@ -1,38 +0,0 @@ -from ..load_nes import open_netcdf -import argparse -from mpi4py import MPI - - -def reorder_longitudes_cli(): - """ - Converts longitudes in a NetCDF file and saves the modified file. - - Returns: - None - """ - comm = MPI.COMM_WORLD - if comm.Get_size() > 1: - raise ValueError("Parallel not implemented yet. This script must be run with a single process.") - - parser = argparse.ArgumentParser(description="Reorder longitudes in a NetCDF file.") - - # Define expected arguments - parser.add_argument("infile", help="Input NetCDF file path") - parser.add_argument("outfile", help="Output NetCDF file path") - - # Parse arguments - args = parser.parse_args() - - # Call your function with parsed arguments - infile = args.infile - outfile = args.outfile - - # open - nc = open_netcdf(infile) - # load - nc.load() - # convert longitudes from default_projections - nc.convert_longitudes() - # save - nc.to_netcdf(outfile) - return None diff --git a/requirements.txt b/requirements.txt index 82c385c..d852638 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,11 @@ filelock eccodes mpi4py shapely -python-dateutil \ No newline at end of file +python-dateutil +configargparse +tomli +toml +importlib-metadata +packaging +pytest +argcomplete \ No newline at end of file diff --git a/setup.py b/setup.py index 1252cbe..7ce6acd 100755 --- a/setup.py +++ b/setup.py @@ -67,8 +67,7 @@ setup( entry_points={ "console_scripts": [ - "nes_reorder_longitudes=nes.utilities.reorder_longitudes_cli:reorder_longitudes_cli", - "nes_check=nes.utilities.checker:run_checks" + "nes=nes.entry_points.cli:main", ] } ) \ No newline at end of file -- GitLab From 2dcdd9f06ff94394b9d50545437e1763974d5f3e Mon Sep 17 00:00:00 2001 From: Carles Tena Date: Sat, 5 Jul 2025 12:23:46 +0200 Subject: [PATCH 2/5] interpolation entry point to be tested --- Makefile | 2 +- nes/__init__.py | 2 +- nes/create_nes.py | 2 +- nes/entry_points/__init__.py | 3 +- nes/entry_points/cli.py | 109 ++++++++++++++++++- nes/entry_points/interpolate.py | 183 ++++++++++++++++++++++++++++++++ 6 files changed, 292 insertions(+), 9 deletions(-) create mode 100644 nes/entry_points/interpolate.py diff --git a/Makefile b/Makefile index 1a198bc..1cb74ce 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -version=1.1.10 +version=1.1.11b ### PATHS: Change them if needed # Paths to the NES software diff --git a/nes/__init__.py b/nes/__init__.py index 8bc6b43..8e110e9 100644 --- a/nes/__init__.py +++ b/nes/__init__.py @@ -1,5 +1,5 @@ __date__ = "2025-07-03" -__version__ = "1.1.10" +__version__ = "1.1.11b" __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 ce8b619..7e9594d 100644 --- a/nes/create_nes.py +++ b/nes/create_nes.py @@ -97,7 +97,7 @@ def create_nes(comm=None, info=False, projection=None, parallel_method="Y", bala 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": + elif projection in ["rotated-nested", "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"] diff --git a/nes/entry_points/__init__.py b/nes/entry_points/__init__.py index cb8e474..21ed35d 100644 --- a/nes/entry_points/__init__.py +++ b/nes/entry_points/__init__.py @@ -1,6 +1,7 @@ from .checker import run_checks from .reorder_longitudes import reorder_longitudes +from .interpolate import interpolate __all__ = [ - 'run_checks', 'reorder_longitudes' + 'run_checks', 'reorder_longitudes', 'interpolate' ] \ No newline at end of file diff --git a/nes/entry_points/cli.py b/nes/entry_points/cli.py index 340bfcd..4f1435f 100644 --- a/nes/entry_points/cli.py +++ b/nes/entry_points/cli.py @@ -4,14 +4,98 @@ from configargparse import ArgParser import argcomplete +def _add_interpolate_subparser(subparsers): + """ + Add the 'interpolate' subcommand to the NES CLI. + + Parameters + ---------- + subparsers : argparse._SubParsersAction + The subparsers object returned by `add_subparsers()` on the main parser. + + This subcommand supports interpolation of NetCDF data along either horizontal or vertical axes. + Destination grid can be loaded from a file or created from projection parameters. + """ + from nes.entry_points import interpolate + + interp_parser = subparsers.add_parser("interpolate", help="Interpolate data onto a different grid") + + # Main input/output + general = interp_parser.add_argument_group("General options") + general.add_argument("-s", "--source", required=True, help="Path to source NetCDF file") + general.add_argument("-o", "--output", help="Path to output NetCDF file") + general.add_argument("--axis", choices=["horizontal", "vertical"], default="horizontal", + help="Interpolation axis (default: horizontal)") + + dst_group = general.add_mutually_exclusive_group(required=True) + dst_group.add_argument("-d", "--destination", help="Path to destination NetCDF file") + dst_group.add_argument("--projection", help="Projection type to generate destination grid (e.g. regular, rotated, lcc)") + + # Horizontal interpolation options + horizontal = interp_parser.add_argument_group("Horizontal interpolation options") + horizontal.add_argument("--kind", choices=["NearestNeighbour", "Conservative"], + help="Interpolation method for horizontal axis") + horizontal.add_argument("--n-neighbours", type=int, help="Number of neighbors (NearestNeighbour)") + horizontal.add_argument("--flux", action="store_true", help="Treat variables as fluxes (Conservative only)") + horizontal.add_argument("--keep-nan", action="store_true", help="Keep NaN values after interpolation") + horizontal.add_argument("--fix-border", action="store_true", help="Fix border effects (NearestNeighbour only)") + horizontal.add_argument("--weight-matrix-path", help="Path to weight matrix file") + horizontal.add_argument("--only-create-wm", action="store_true", help="Only generate weight matrix") + horizontal.add_argument("--to-providentia", action="store_true", help="Format output for Providentia") + + # Vertical interpolation options + vertical = interp_parser.add_argument_group("Vertical interpolation options") + vertical.add_argument("--method", help="Interpolation method for vertical axis (e.g. linear)") + vertical.add_argument("--extrapolate", action="store_true", help="Allow extrapolation in vertical interpolation") + + # Grid creation arguments + grid = interp_parser.add_argument_group("Grid creation options (for --projection)") + grid.add_argument("--lat_orig", type=float, help="Latitude origin (regular/global)") + grid.add_argument("--lon_orig", type=float, help="Longitude origin (regular/global)") + grid.add_argument("--inc_lat", type=float, help="Latitude increment (regular/global)") + grid.add_argument("--inc_lon", type=float, help="Longitude increment (regular/global)") + grid.add_argument("--n_lat", type=int, help="Number of latitude points (regular/global)") + grid.add_argument("--n_lon", type=int, help="Number of longitude points (regular/global)") + + grid.add_argument("--centre_lat", type=float, help="Rotated pole latitude (rotated)") + grid.add_argument("--centre_lon", type=float, help="Rotated pole longitude (rotated)") + grid.add_argument("--west_boundary", type=float, help="Western boundary (rotated)") + grid.add_argument("--south_boundary", type=float, help="Southern boundary (rotated)") + grid.add_argument("--inc_rlat", type=float, help="Latitude increment (rotated)") + grid.add_argument("--inc_rlon", type=float, help="Longitude increment (rotated)") + + grid.add_argument("--parent_grid_path", help="Path to parent grid NetCDF (rotated_nested)") + grid.add_argument("--parent_ratio", type=int, help="Parent ratio (rotated_nested)") + grid.add_argument("--i_parent_start", type=int, help="Parent grid i index start (rotated_nested)") + grid.add_argument("--j_parent_start", type=int, help="Parent grid j index start (rotated_nested)") + grid.add_argument("--n_rlat", type=int, help="Number of lat points (rotated_nested)") + grid.add_argument("--n_rlon", type=int, help="Number of lon points (rotated_nested)") + + grid.add_argument("--lat_1", type=float, help="First standard parallel (LCC projection)") + grid.add_argument("--lat_2", type=float, help="Second standard parallel (LCC projection)") + grid.add_argument("--lon_0", type=float, help="Central meridian (LCC projection)") + grid.add_argument("--x_0", type=float, help="False easting (LCC projection)") + grid.add_argument("--y_0", type=float, help="False northing (LCC projection)") + grid.add_argument("--dx", type=float, help="Grid spacing in x direction (LCC projection)") + grid.add_argument("--dy", type=float, help="Grid spacing in y direction (LCC projection)") + grid.add_argument("--nx", type=int, help="Number of grid points in x (LCC projection)") + grid.add_argument("--ny", type=int, help="Number of grid points in y (LCC projection)") + + grid.add_argument("--lat_ts", type=float, help="Latitude of true scale (Mercator projection)") + + interp_parser.set_defaults(func=interpolate) + + def _add_check_subparser(subparsers): """ - Add the 'check' subcommand to the CLI. + Add the 'check' subcommand to the NES CLI. Parameters ---------- subparsers : argparse._SubParsersAction The subparsers object returned by `add_subparsers()` on the main parser. + + This subcommand checks for the presence of NaN and Inf values in a NetCDF file. """ from nes.entry_points import run_checks @@ -27,12 +111,14 @@ def _add_check_subparser(subparsers): def _add_reorder_subparser(subparsers): """ - Add the 'reorder' subcommand to the CLI. + Add the 'reorder' subcommand to the NES CLI. Parameters ---------- subparsers : argparse._SubParsersAction The subparsers object returned by `add_subparsers()` on the main parser. + + This subcommand reorders longitudes in a NetCDF file to ensure standard representation. """ from nes.entry_points import reorder_longitudes @@ -44,7 +130,19 @@ def _add_reorder_subparser(subparsers): def _filter_args(func, args_namespace): """ - Filters the argparse namespace to only include arguments that match the function's signature. + Filters arguments from argparse to only include those relevant to the target function. + + Parameters + ---------- + func : Callable + The function to match arguments against. + args_namespace : argparse.Namespace + The full set of parsed CLI arguments. + + Returns + ------- + dict + A dictionary containing only the arguments accepted by the function. """ import inspect sig = inspect.signature(func) @@ -58,8 +156,8 @@ def main(): """ Main entry point for the NES command-line interface. - Defines subcommands and parses arguments using `configargparse`. This allows - command-line configuration and also supports loading options from config files. + Sets up the available subcommands, parses user input from the CLI, + and dispatches execution to the appropriate subcommand handler. """ parser = ArgParser( description="NES - NetCDF for Earth Science utilities", @@ -67,6 +165,7 @@ def main(): ) subparsers = parser.add_subparsers(dest="command", required=True) + # Add subcommands _add_check_subparser(subparsers) _add_reorder_subparser(subparsers) diff --git a/nes/entry_points/interpolate.py b/nes/entry_points/interpolate.py new file mode 100644 index 0000000..d8d73b5 --- /dev/null +++ b/nes/entry_points/interpolate.py @@ -0,0 +1,183 @@ +from nes import open_netcdf, create_nes + + +def validate_axis_options(args): + """ + Validates that interpolation axis and related options are consistent. + + This function ensures that horizontal and vertical interpolation options are not mixed. + It raises an error if options intended for one axis are used with the other, or if the axis is not supported. + + Parameters + ---------- + args : Namespace + Parsed arguments from the CLI, including interpolation axis and all related options. + + Raises + ------ + ValueError + If vertical options are used with horizontal axis or vice versa, or if an invalid axis is specified. + """ + if args.axis == "horizontal": + if args.method or args.extrapolate: + raise ValueError("Vertical options (--method, --extrapolate) cannot be used with horizontal interpolation.") + elif args.axis == "vertical": + if args.kind or args.n_neighbours or args.flux or args.weight_matrix_path or args.fix_border: + raise ValueError("Horizontal interpolation options cannot be used with vertical interpolation.") + else: + raise ValueError(f"Unsupported interpolation axis: {args.axis}") + + +def get_destination(args): + """ + Determines the destination grid for interpolation. + + This function either loads an existing destination file (if --destination is provided), + or creates a new grid using the specified projection parameters (if --projection is provided). + Only the relevant parameters for each projection type are passed to `create_nes`. + + Parameters + ---------- + args : Namespace + Parsed arguments from the CLI, containing options such as destination file path, + projection type, and all necessary projection parameters. + + Returns + ------- + nes.NES + A loaded or newly created NES object representing the destination grid. + + Raises + ------ + ValueError + If neither --destination nor --projection is provided, + or if the projection type is unsupported. + """ + if args.destination: + destination = open_netcdf(args.destination) + nessy = destination + + if not args.projection: + raise ValueError("Either --destination or --projection must be specified.") + + valid_projections = ["regular", "global", "rotated", "rotated_nested", "lcc", "mercator"] + proj = args.projection.lower() + if proj not in valid_projections: + raise ValueError(f"Unsupported projection type: {args.projection}. Valid options are: {', '.join(valid_projections)}") + + if proj == "regular": + nessy = create_nes( + projection=proj, + lat_orig=args.lat_orig, + lon_orig=args.lon_orig, + inc_lat=args.inc_lat, + inc_lon=args.inc_lon, + n_lat=args.n_lat, + n_lon=args.n_lon, + ) + elif proj == "global": + nessy = create_nes( + projection=proj, + inc_lat=args.inc_lat, + inc_lon=args.inc_lon, + ) + elif proj == "rotated": + nessy = create_nes( + projection=proj, + centre_lat=args.centre_lat, + centre_lon=args.centre_lon, + west_boundary=args.west_boundary, + south_boundary=args.south_boundary, + inc_rlat=args.inc_rlat, + inc_rlon=args.inc_rlon, + ) + elif proj == "rotated_nested": + nessy = create_nes( + projection=proj, + parent_grid_path=args.parent_grid_path, + parent_ratio=args.parent_ratio, + i_parent_start=args.i_parent_start, + j_parent_start=args.j_parent_start, + n_rlat=args.n_rlat, + n_rlon=args.n_rlon, + ) + elif proj == "lcc": + nessy = create_nes( + projection=proj, + lat_1=args.lat_1, + lat_2=args.lat_2, + lon_0=args.lon_0, + lat_0=args.lat_0, + nx=args.nx, + ny=args.ny, + inc_x=args.inc_x, + inc_y=args.inc_y, + x_0=args.x_0, + y_0=args.y_0, + ) + elif proj == "mercator": + nessy = create_nes( + projection=proj, + lat_ts=args.lat_ts, + lon_0=args.lon_0, + nx=args.nx, + ny=args.ny, + inc_x=args.inc_x, + inc_y=args.inc_y, + x_0=args.x_0, + y_0=args.y_0, + ) + else: + raise ValueError(f"Unsupported projection type: {args.projection}") + + return nessy + + +def interpolate(args): + """ + Main entry point for the NES interpolation CLI command. + + Depending on the selected interpolation axis (horizontal or vertical), this function: + - Loads the source NetCDF dataset. + - Loads or creates the destination grid based on CLI options. + - Calls the appropriate interpolation method. + + Parameters + ---------- + args : Namespace + Parsed arguments from the CLI, including interpolation settings and input/output file paths. + + Raises + ------ + ValueError + If axis-specific options are misused or required arguments are missing. + """ + validate_axis_options(args) + + source = open_netcdf(args.source) + source.load() + + destination = get_destination(args) + + if args.axis == "horizontal": + source.interpolate_horizontal( + source, + destination, + kind=args.kind, + n_neighbours=args.n_neighbours, + flux=args.flux, + keep_nan=args.keep_nan, + fix_border=args.fix_border, + weight_matrix_path=args.weight_matrix_path, + only_create_wm=args.only_create_wm, + to_providentia=args.to_providentia, + output=args.output + ) + elif args.axis == "vertical": + source.interpolate_vertical( + source, + destination, + method=args.method, + extrapolate=args.extrapolate, + output=args.output + ) \ No newline at end of file -- GitLab From ed999477633a1ed6162971e7cb7513445eb6f57e Mon Sep 17 00:00:00 2001 From: Carles Tena Date: Sat, 5 Jul 2025 12:51:46 +0200 Subject: [PATCH 3/5] interpolation entry point to be tested --- nes/entry_points/__init__.py | 3 +- nes/entry_points/cli.py | 34 ++++++++++++++++-- nes/entry_points/geostructure.py | 60 ++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 nes/entry_points/geostructure.py diff --git a/nes/entry_points/__init__.py b/nes/entry_points/__init__.py index 21ed35d..110a256 100644 --- a/nes/entry_points/__init__.py +++ b/nes/entry_points/__init__.py @@ -1,7 +1,8 @@ from .checker import run_checks from .reorder_longitudes import reorder_longitudes from .interpolate import interpolate +from .geostructure import nc2geostructure __all__ = [ - 'run_checks', 'reorder_longitudes', 'interpolate' + 'run_checks', 'reorder_longitudes', 'interpolate', 'nc2geostructure' ] \ No newline at end of file diff --git a/nes/entry_points/cli.py b/nes/entry_points/cli.py index 4f1435f..e39c1d2 100644 --- a/nes/entry_points/cli.py +++ b/nes/entry_points/cli.py @@ -4,6 +4,32 @@ from configargparse import ArgParser import argcomplete +def _add_nc2geostructure_subparser(subparsers): + """ + Add the 'geostructure' subcommand to the NES CLI. + + This command extracts geospatial features from a NetCDF file and saves them as a geostructure. + + Parameters + ---------- + subparsers : argparse._SubParsersAction + The subparsers object returned by `add_subparsers()` on the main parser. + """ + from nes.entry_points import nc2geostructure + + # TODO: TEST + geo_parser = subparsers.add_parser("nc2geostructure", help="Convert NetCDF to geospatial structure (GeoJSON, shapefile) (TESTING PHASE)") + geo_parser.add_argument("-i", "--input", required=True, help="Path to input NetCDF file") + geo_parser.add_argument("-o", "--output", required=True, help="Path to output geostructure") + geo_parser.add_argument( + "--var-list", nargs="+", + help="List of variable names to include in the geostructure. If omitted, all variables will be included." + ) + geo_parser.add_argument("--time-step", type=int, default=0, help="Time step index to extract (default: 0)") + geo_parser.add_argument("--level", type=int, default=0, help="Vertical level index to extract (default: 0)") + geo_parser.set_defaults(func=nc2geostructure) + + def _add_interpolate_subparser(subparsers): """ Add the 'interpolate' subcommand to the NES CLI. @@ -18,7 +44,8 @@ def _add_interpolate_subparser(subparsers): """ from nes.entry_points import interpolate - interp_parser = subparsers.add_parser("interpolate", help="Interpolate data onto a different grid") + # TODO: TEST + interp_parser = subparsers.add_parser("interpolate", help="Interpolate data onto a different grid (TESTING PHASE)") # Main input/output general = interp_parser.add_argument_group("General options") @@ -122,7 +149,8 @@ def _add_reorder_subparser(subparsers): """ from nes.entry_points import reorder_longitudes - reorder_parser = subparsers.add_parser("reorder", help="Reorder longitudes in a NetCDF file") + # TODO: Add support for parallel version + reorder_parser = subparsers.add_parser("reorder", help="Reorder longitudes in a NetCDF file (ONLY SERIAL)") reorder_parser.add_argument("-f", "--file", required=True, help="Input NetCDF file path") reorder_parser.add_argument("-o", "--output", help="Output NetCDF file path") reorder_parser.set_defaults(func=reorder_longitudes) @@ -166,8 +194,10 @@ def main(): subparsers = parser.add_subparsers(dest="command", required=True) # Add subcommands + _add_nc2geostructure_subparser(subparsers) _add_check_subparser(subparsers) _add_reorder_subparser(subparsers) + _add_interpolate_subparser(subparsers) # Enable autocomplete argcomplete.autocomplete(parser) diff --git a/nes/entry_points/geostructure.py b/nes/entry_points/geostructure.py new file mode 100644 index 0000000..40e49a1 --- /dev/null +++ b/nes/entry_points/geostructure.py @@ -0,0 +1,60 @@ +""" +NES CLI Utility: Convert NetCDF to geospatial vector format (Shapefile, GeoJSON) + +This script defines a function `nc2geostructure` that extracts a selected time step and level +from a NetCDF file and writes the corresponding geospatial structure as a shapefile, +optionally filtering specific variables. + +Intended to be used as part of a CLI interface (e.g. via `nes geostructure`). +""" + +from nes import open_netcdf + +def nc2geostructure(input_file: str, output_file: str, var_list: list=None, time_step:int=0, level:int=0): + """ + Extracts geospatial data from a NetCDF file and writes it as a shapefile. + + Parameters + ---------- + input_file : str + Path to the source NetCDF file. + output_file : str + Path where the output shapefile will be written. + var_list : list, optional + List of variable names to include in the shapefile. If None, all variables are used. + time_step : int, default=0 + Index of the time step to extract. + level : int, default=0 + Index of the vertical level to extract. + + Returns + ------- + None + """ + # Open the NetCDF file using NES + nessy = open_netcdf(input_file) + + # If no variable list is provided, use all available variables in the file + if var_list is None: + var_list = nessy.variables.keys() + + # Select the desired time step and vertical level for extraction + nessy.sel( + time_min=nessy.time[time_step], # Minimum time index to extract + time_max=nessy.time[time_step], # Maximum time index to extract (same as min for single step) + level_min=level, # Minimum vertical level to extract + level_max=level # Maximum vertical level to extract (same as min for single level) + ) + + # Filter to keep only the specified variables and load the data into memory + nessy.keep(var_list) + nessy.load() + + # Create the base geospatial structure (shapefile) and attach the selected variables + nessy.create_shapefile() + nessy.add_variables_to_shapefile(idx_time=0, idx_lev=0) + + # Write the geospatial structure to the output shapefile + nessy.write_shapefile(output_file) + + return None -- GitLab From 472899395b0dac522d86c9005c4d14cb76784b48 Mon Sep 17 00:00:00 2001 From: Carles Tena Date: Mon, 7 Jul 2025 16:49:17 +0200 Subject: [PATCH 4/5] new entry points structure --- Makefile | 2 +- nes/__init__.py | 2 +- nes/{entry_points => cli}/__init__.py | 0 nes/{entry_points => cli}/checker.py | 0 nes/{entry_points => cli}/cli.py | 16 ++++++++-------- nes/{entry_points => cli}/geostructure.py | 0 nes/{entry_points => cli}/interpolate.py | 0 nes/{entry_points => cli}/reorder_longitudes.py | 0 setup.py | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) rename nes/{entry_points => cli}/__init__.py (100%) rename nes/{entry_points => cli}/checker.py (100%) rename nes/{entry_points => cli}/cli.py (95%) rename nes/{entry_points => cli}/geostructure.py (100%) rename nes/{entry_points => cli}/interpolate.py (100%) rename nes/{entry_points => cli}/reorder_longitudes.py (100%) diff --git a/Makefile b/Makefile index 1cb74ce..8d83dfa 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -version=1.1.11b +version=1.1.11 ### PATHS: Change them if needed # Paths to the NES software diff --git a/nes/__init__.py b/nes/__init__.py index 8e110e9..0234c94 100644 --- a/nes/__init__.py +++ b/nes/__init__.py @@ -1,5 +1,5 @@ __date__ = "2025-07-03" -__version__ = "1.1.11b" +__version__ = "1.1.11" __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/entry_points/__init__.py b/nes/cli/__init__.py similarity index 100% rename from nes/entry_points/__init__.py rename to nes/cli/__init__.py diff --git a/nes/entry_points/checker.py b/nes/cli/checker.py similarity index 100% rename from nes/entry_points/checker.py rename to nes/cli/checker.py diff --git a/nes/entry_points/cli.py b/nes/cli/cli.py similarity index 95% rename from nes/entry_points/cli.py rename to nes/cli/cli.py index e39c1d2..124af48 100644 --- a/nes/entry_points/cli.py +++ b/nes/cli/cli.py @@ -15,7 +15,7 @@ def _add_nc2geostructure_subparser(subparsers): subparsers : argparse._SubParsersAction The subparsers object returned by `add_subparsers()` on the main parser. """ - from nes.entry_points import nc2geostructure + from nes.cli import nc2geostructure # TODO: TEST geo_parser = subparsers.add_parser("nc2geostructure", help="Convert NetCDF to geospatial structure (GeoJSON, shapefile) (TESTING PHASE)") @@ -42,20 +42,20 @@ def _add_interpolate_subparser(subparsers): This subcommand supports interpolation of NetCDF data along either horizontal or vertical axes. Destination grid can be loaded from a file or created from projection parameters. """ - from nes.entry_points import interpolate + from nes.cli import interpolate # TODO: TEST interp_parser = subparsers.add_parser("interpolate", help="Interpolate data onto a different grid (TESTING PHASE)") # Main input/output general = interp_parser.add_argument_group("General options") - general.add_argument("-s", "--source", required=True, help="Path to source NetCDF file") + general.add_argument("-i", "--input", required=True, help="Path to source NetCDF file") general.add_argument("-o", "--output", help="Path to output NetCDF file") general.add_argument("--axis", choices=["horizontal", "vertical"], default="horizontal", help="Interpolation axis (default: horizontal)") dst_group = general.add_mutually_exclusive_group(required=True) - dst_group.add_argument("-d", "--destination", help="Path to destination NetCDF file") + dst_group.add_argument("-d", "--destination", help="Path to destination grid NetCDF file") dst_group.add_argument("--projection", help="Projection type to generate destination grid (e.g. regular, rotated, lcc)") # Horizontal interpolation options @@ -124,10 +124,10 @@ def _add_check_subparser(subparsers): This subcommand checks for the presence of NaN and Inf values in a NetCDF file. """ - from nes.entry_points import run_checks + from nes.cli import run_checks check_parser = subparsers.add_parser("check", help="Run checks on a NetCDF file") - check_parser.add_argument("-f", "--file", required=True, help="Input NetCDF file path") + check_parser.add_argument("-i", "--input", required=True, help="Input NetCDF file path") check_parser.add_argument("--nan", dest="check_nan", action="store_true", help="Check for NaN values") check_parser.add_argument("--no-nan", dest="check_nan", action="store_false", help="Do not check NaN values") check_parser.add_argument("--inf", dest="check_inf", action="store_true", help="Check for Inf values") @@ -147,11 +147,11 @@ def _add_reorder_subparser(subparsers): This subcommand reorders longitudes in a NetCDF file to ensure standard representation. """ - from nes.entry_points import reorder_longitudes + from nes.cli import reorder_longitudes # TODO: Add support for parallel version reorder_parser = subparsers.add_parser("reorder", help="Reorder longitudes in a NetCDF file (ONLY SERIAL)") - reorder_parser.add_argument("-f", "--file", required=True, help="Input NetCDF file path") + reorder_parser.add_argument("-i", "--input", required=True, help="Input NetCDF file path") reorder_parser.add_argument("-o", "--output", help="Output NetCDF file path") reorder_parser.set_defaults(func=reorder_longitudes) diff --git a/nes/entry_points/geostructure.py b/nes/cli/geostructure.py similarity index 100% rename from nes/entry_points/geostructure.py rename to nes/cli/geostructure.py diff --git a/nes/entry_points/interpolate.py b/nes/cli/interpolate.py similarity index 100% rename from nes/entry_points/interpolate.py rename to nes/cli/interpolate.py diff --git a/nes/entry_points/reorder_longitudes.py b/nes/cli/reorder_longitudes.py similarity index 100% rename from nes/entry_points/reorder_longitudes.py rename to nes/cli/reorder_longitudes.py diff --git a/setup.py b/setup.py index 7ce6acd..75d0eea 100755 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( entry_points={ "console_scripts": [ - "nes=nes.entry_points.cli:main", + "nes=nes.cli.cli:main", ] } ) \ No newline at end of file -- GitLab From 519a2d48bd467bf7d8e40f4819d375583cdddd7b Mon Sep 17 00:00:00 2001 From: Carles Tena Date: Tue, 8 Jul 2025 15:45:43 +0200 Subject: [PATCH 5/5] new entry points structure --- 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 66b1260..cf1a1e2 100644 --- a/nes/nc_projections/default_nes.py +++ b/nes/nc_projections/default_nes.py @@ -4728,7 +4728,7 @@ class Nes(object): chunking : bool Indicates if you want a chunked netCDF output. Only available with non-serial writes. Default: False. nc_type : str - Type to NetCDf to write. "CAMS_RA" or "NES" + Type to NetCDf to write. "CAMS_RA" or "NES", MONARCH, MOCAGE, WRF_CHEM, CMAQ. keep_open : bool Indicates if you want to keep open the NetCDH to fill the data by time-step """ -- GitLab