From 05568967d1cd916e37b73db724934c742a9b87ed Mon Sep 17 00:00:00 2001 From: richardarsenault Date: Sun, 28 Dec 2025 21:49:07 -0500 Subject: [PATCH 1/6] first pass for PyGMET, although package not included. Must be downloaded separately --- src/xhydro/pygmet/make_toml_config_pygmet.py | 256 ++++++++++++++++++ .../pygmet/make_toml_settings_pygmet.py | 234 ++++++++++++++++ .../subsample_vector_format_stations.py | 102 +++++++ .../pygmet/transform_to_station_order.py | 172 ++++++++++++ tests/subset_grid_temperature.nc | Bin 0 -> 25664 bytes tests/subset_oi_tp.nc | Bin 0 -> 21959 bytes tests/test_pygmet_prep.py | 83 ++++++ 7 files changed, 847 insertions(+) create mode 100644 src/xhydro/pygmet/make_toml_config_pygmet.py create mode 100644 src/xhydro/pygmet/make_toml_settings_pygmet.py create mode 100644 src/xhydro/pygmet/subsample_vector_format_stations.py create mode 100644 src/xhydro/pygmet/transform_to_station_order.py create mode 100644 tests/subset_grid_temperature.nc create mode 100644 tests/subset_oi_tp.nc create mode 100644 tests/test_pygmet_prep.py diff --git a/src/xhydro/pygmet/make_toml_config_pygmet.py b/src/xhydro/pygmet/make_toml_config_pygmet.py new file mode 100644 index 00000000..0db06f83 --- /dev/null +++ b/src/xhydro/pygmet/make_toml_config_pygmet.py @@ -0,0 +1,256 @@ +""" +Hard-coded TOML template writer. + +This module writes a new TOML config file based on a fixed template, replacing only +the fields marked with "# MAKE_DYNAMIC" in the original template. +""" + +from __future__ import annotations +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from datetime import date, datetime +from pathlib import Path +from typing import Any + + +@dataclass(frozen=True) +class RawToml: + """Inject TOML verbatim (advanced use).""" + + text: str + + +def _toml_escape_single_quoted(s: str) -> str: + """ + Manage the single quotes in strings. + + Parameters + ---------- + s : str + String that has single quotes and needs to be processed. + + Returns + ------- + str : + The string with the processed single quotes. + """ + # TOML single-quoted strings are literal except that you must escape single quotes. + return "'" + s.replace("'", "''") + "'" + + +def to_toml(value: Any) -> str: + """ + Convert common Python types to TOML literals. + + Parameters + ---------- + value : Any + Value to convert to TOML literal. + + Returns + ------- + str : + The string literal writable to TOML file. + """ + if isinstance(value, RawToml): + return value.text + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, int): + return str(value) + if isinstance(value, float): + return repr(value) + if isinstance(value, (datetime, date)): + return _toml_escape_single_quoted(value.isoformat()) + if isinstance(value, str): + return _toml_escape_single_quoted(value) + if isinstance(value, (list, tuple)): + return "[" + ", ".join(to_toml(v) for v in value) + "]" + if value is None: + # TOML has no null; choose what your application expects. + return '""' + raise TypeError(f"Unsupported type for TOML conversion: {type(value).__name__}") + + +# Keys whose values should be provided at runtime: +DYNAMIC_KEYS: Sequence[str] = ( + "case_name", + "num_processes", + "modelsettings_file", + "input_stn_all", + "infile_grid_domain", + "outpath_parent", + "date_start", + "date_end", + "input_vars", + "target_vars", + "target_vars_WithProbability", + "minRange_vars", + "maxRange_vars", + "transform_vars", + "predictor_name_static_stn", + "predictor_name_static_grid", + "ensemble_end", + "clen", # codespell:ignore clen + "auto_corr_method", + "target_vars_max_constrain", +) + +# The hard-coded template (generated from testcase.config.static_withTemperature.toml): +TOML_TEMPLATE = """\ +######################################################################################################################## +# TEMPLATE FILE TO RUN Pygmet for downscaling / generator +######################################################################################################################## + +case_name = {case_name} + +num_processes = {num_processes} + +# model setting file +modelsettings_file = {modelsettings_file} + +######################################################################################################################## +# Input and output information +######################################################################################################################## + +input_stn_all = {input_stn_all} + +infile_grid_domain = {infile_grid_domain} + +outpath_parent = {outpath_parent} + +# file list containing dynamic predictor inputs (optional). Give it a nonsense string (e.g., "NA") can turn off dynamic +# predictors +dynamic_predictor_filelist = "NA" + +######################################################################################################################## +# Period to process +######################################################################################################################## + +date_start = {date_start} +date_end = {date_end} + +######################################################################################################################## +# Variables to process +######################################################################################################################## + +input_vars = {input_vars} + +# Target variables to output, should always contain precip as first variable +target_vars = {target_vars} + +# input variables may need some conversion to get target variables. if input_var and target_var have the same variable +# name, no need to add mapping relation +mapping_InOut_var = [] + +# Target variables that will get an additional output probability of exceedance (P(exc) and P(non exc)) +target_vars_WithProbability = {target_vars_WithProbability} + +# Set the minimum and maximum range for each variable, must be same order as input_vars +minRange_vars = {minRange_vars} +maxRange_vars = {maxRange_vars} + +############################## dynamic predictors for regression +# only useful when dynamic_predictor_filelist is valid + +# dynamic predictors for each target_vars. Empty list means no dynamic predictors +dynamic_predictor_name = [] + +# dynamic predictors may needs some processing. two keywords: "interp" an "transform" +# Example "cube_root_prec_rate:interp=linear:transform=boxcox" +dynamic_predictor_operation = [] + + +# Transformation to use for each variable, can be any of: +# 'log', 'logit', 'direct', 'sqrt', 'cbrt', 'boxcox', 'yeojohnson', 'standardize' +transform_vars = {transform_vars} + +######################################################################################################################## +# Static predictors +######################################################################################################################## + +# Names of variables in input_stn_all file (static predictors) +predictor_name_static_stn = {predictor_name_static_stn} + +# Names of variables in infile_grid_domain file (static predictors) +predictor_name_static_grid = {predictor_name_static_grid} + +######################################################################################################################## +# Stochastic and correlation settings +######################################################################################################################## + +# Number of ensemble members to generate (inclusive end index) +# run ensemble or not: true or false +ensemble_flag = true + +# ensemble settings +ensemble_start = 1 +ensemble_end = {ensemble_end} + +# link variables for random number generation dependence +linkvar = [] + +# Correlation length for each variable (same order as input_vars) +clen = {clen} # codespell:ignore clen +lag1_auto_cc = [-9999, -9999, -9999] # corresponding to target_vars +cross_cc = [] # corresponding to linkvar + +# Method for auto-correlation for each variable (same order as input_vars) +auto_corr_method = {auto_corr_method} + +######################################################################################################################## +# Constraints +######################################################################################################################## +rolling_window = 31 # 31-monthly rolling mean to remove monthly cycle +# Max constrain list (subset of target_vars) +target_vars_max_constrain = {target_vars_max_constrain} +""" + + +def render_config_toml(params: Mapping[str, Any], strict: bool = True) -> str: + """ + Render the template into a TOML config string. + + Parameters + ---------- + params : Mapping[str, Any] + The list of parameters to write to the file, in replacement of the template tags. + strict : bool + True if all DYNAMIC_KEYS must be present in `params`. + + Returns + ------- + str : + The full template with tags replaced. + """ + missing = [k for k in DYNAMIC_KEYS if k not in params] + if strict and missing: + raise KeyError(f"Missing replacements for keys: {missing}") + + mapping = {k: to_toml(params[k]) for k in DYNAMIC_KEYS if k in params} + + # format_map will raise KeyError if any placeholder isn't present; strict above prevents that. + return TOML_TEMPLATE.format_map(mapping) + + +def write_config_toml(output_path: str | Path, params: Mapping[str, Any], strict: bool = True) -> bool: + """ + Write a fresh model settings TOML file from the hard-coded template. + + Parameters + ---------- + output_path : str or Path + Path to the filled-in template file we want to write to disk. + params : Mapping[str,Any] + The list of parameters to write to the file, in replacement of the template tags. + strict : bool + True if all DYNAMIC_KEYS must be present in `params`. + + Returns + ------- + bool : + True if the code executes with no error. + """ + output_path = Path(output_path) + output_path.write_text(render_config_toml(params, strict=strict), encoding="utf-8") + return True diff --git a/src/xhydro/pygmet/make_toml_settings_pygmet.py b/src/xhydro/pygmet/make_toml_settings_pygmet.py new file mode 100644 index 00000000..3cc924d1 --- /dev/null +++ b/src/xhydro/pygmet/make_toml_settings_pygmet.py @@ -0,0 +1,234 @@ +""" +Hard-coded template writer for the PyGMET settings file. + +This module writes a new TOML settings file from a fixed template, replacing templated values with desired values. +""" + +from __future__ import annotations +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Any + + +@dataclass(frozen=True) +class RawToml: + """Inject TOML verbatim.""" + + text: str + + +def _toml_escape_single_quoted(s: str) -> str: + """ + Manage the single quotes in strings. + + Parameters + ---------- + s : str + String that has single quotes and needs to be processed. + + Returns + ------- + str : + The string with the processed single quotes. + """ + # TOML single-quoted strings are literal except that you must escape single quotes. + return "'" + s.replace("'", "''") + "'" + + +def to_toml(value: Any) -> str: + """ + Convert common Python types to TOML literals. + + Parameters + ---------- + value : Any + Value to convert to TOML literal. + + Returns + ------- + str : + The string literal writable to TOML file. + """ + if isinstance(value, RawToml): + return value.text + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, int): + return str(value) + if isinstance(value, float): + return repr(value) + if isinstance(value, str): + return _toml_escape_single_quoted(value) + if isinstance(value, (list, tuple)): + return "[" + ", ".join(to_toml(v) for v in value) + "]" + if value is None: + return "''" + raise TypeError(f"Unsupported type for TOML conversion: {type(value).__name__}") + + +DYNAMIC_KEYS: Sequence[str] = ( + "stn_lat_name", + "stn_lon_name", + "grid_lat_name", + "grid_lon_name", + "grid_mask_name", + "dynamic_grid_lat_name", + "dynamic_grid_lon_name", + "nearstn_min", + "nearstn_max", + "try_radius", + "initial_distance", +) + +TOML_TEMPLATE = """\ +# Some default settings + +######################################################################################################################## +# general settings +######################################################################################################################## + +# master seed: control all probabilistic process (e.g., probabilistic estimation, machine learning methods) +# a negative value means random generation without reproducibility +master_seed = 20230104 + +######################################################################################################################## +# settings for gridded estimation using regression or machine learning methods +######################################################################################################################## + +############################## station/grid file dimension name +# the spatial dims of input stations +stn_lat_name = {stn_lat_name} +stn_lon_name = {stn_lon_name} + +# the 2D spatial dims of the target grid domain +# note that target grid domain netcdf must have x/y dims, while lat and lon are 2D arrays +grid_lat_name = {grid_lat_name} +grid_lon_name = {grid_lon_name} +grid_mask_name = {grid_mask_name} + +# the 2D spatial dims of dynamic predictor inputs +dynamic_grid_lat_name = {dynamic_grid_lat_name} +dynamic_grid_lon_name = {dynamic_grid_lon_name} + +######################################################################################################################## +# default settings +# they are as useful as the above settings but using their default values does not affect model run for any case +######################################################################################################################## + +# gridding methods: locally weighted regression and meachine learning methods. +# Sklearn module is used to support most functions: https://scikit-learn.org/stable/supervised_learning.html +# Locally weighted regression. +# Two original methods are LWR:Linear and LWR:Logistic. +# Sklearn-based methods support simple usage with "model.fit()" and "model.predict" or "model.predict_prob", +# in the format of LWR:linear_model.METHOD +# Examples of METHOD are LinearRegression, LogisticRegression, Ridge, BayesianRidge, ARDRegression, Lasso, ElasticNet, Lars, etc +# Global regression using machine learnig methods: +# Machine learning methods are supported by sklearn. Parameters of methods supported by sklearn can be defined at the bottom of this +# configuration file (optional) +# Examples: ensemble.RandomForestRegressor, ensemble.RandomForestClassifier, neural_network.MLPRegressor, neural_network.MLPClassifier, +# ensemble.GradientBoostingClassifier, ensemble.GradientBoostingRegressor +# The parameters of sklearn methods can be defined in the [sklearn] section +gridcore_continuous = 'LWR:Linear' +gridcore_classification = 'LWR:Logistic' # for probability of event +n_splits = 10 # only useful for machine learning methods. cross validation to generate uncertainty estimates. + +# output random fields +output_randomfield = false + +# Number of stations to consider for each target point. nearstn_min<=nearstn_max. +nearstn_min = {nearstn_min} # nearby stations: minimum number +nearstn_max = {nearstn_max} # nearby stations: maximum number + +# first try this radius (km). if not enough, expand. Could be useful to reduce computation time for large domain search. +try_radius = {try_radius} + +# overwrite existing files +overwrite_stninfo = true +overwrite_station_cc = true +overwrite_weight = true +overwrite_cv_reg = true +overwrite_grid_reg = true +overwrite_ens = true +overwrite_spcorr = true + +######################################################################################################################## +# distance-based weight calculation +######################################################################################################################## + +initial_distance = {initial_distance} # Initial Search Distance in km (expanded if need be) + +# Weight calculation formula. Only two variables/parameters are allowed in the formula +# dist: distance between points (km in the script) +# maxdist (optional): max(initial_distance, max(dist)+1), which is a parameter used in weight calculation +# 3 is the exponential factor and is the default parameter +weight_formula = '(1 - (dist / maxdist) ** 3) ** 3' + + +######################################################################################################################## +# method-related settings +# default values can be directly used +######################################################################################################################## + +[transform] +# note: the name must be consistent with transform_vars +[transform.boxcox] +exp = 4 + +[sklearn] +# if no parameters are provided or if the section does not even exist, default parameters will be used. +# just provide the method name, no need to include the submodule name +[sklearn.RandomForestRegressor] +n_estimators = 500 # a example of RandomForestRegressor parameter +n_jobs = 5 +[sklearn.RandomForestClassifier] +n_estimators = 500 # a example of RandomForestRegressor parameter +n_jobs = 5 +""" + + +def render_model_settings_toml(params: Mapping[str, Any], strict: bool = True) -> str: + """ + Render the template into a TOML settings string. + + Parameters + ---------- + params : Mapping[str, Any] + The list of parameters to write to the file, in replacement of the template tags. + strict : bool + True if all DYNAMIC_KEYS must be present in `params`. + + Returns + ------- + str : + The full template with tags replaced. + """ + missing = [k for k in DYNAMIC_KEYS if k not in params] + if strict and missing: + raise KeyError(f"Missing replacements for keys: {missing}") + + mapping = {k: to_toml(params[k]) for k in DYNAMIC_KEYS if k in params} + return TOML_TEMPLATE.format_map(mapping) + + +def write_settings_toml(output_path: str | Path, params: Mapping[str, Any], strict: bool = True) -> bool: + """ + Write a fresh model settings TOML file from the hard-coded template. + + Parameters + ---------- + output_path : str or Path + Path to the filled-in template file we want to write to disk. + params : Mapping[str,Any] + The list of parameters to write to the file, in replacement of the template tags. + strict : bool + True if all DYNAMIC_KEYS must be present in `params`. + + Returns + ------- + bool : + True if the code executes with no error. + """ + output_path = Path(output_path) + output_path.write_text(render_model_settings_toml(params, strict=strict), encoding="utf-8") + return True diff --git a/src/xhydro/pygmet/subsample_vector_format_stations.py b/src/xhydro/pygmet/subsample_vector_format_stations.py new file mode 100644 index 00000000..dd473326 --- /dev/null +++ b/src/xhydro/pygmet/subsample_vector_format_stations.py @@ -0,0 +1,102 @@ +"""Subsampling tools to select stations for PyGMET variability.""" + +import numpy as np +import xarray as xr + + +def isel_every_about_n( + path_to_nc: str, + dim: str, + outpath: str, + rng: float | None, + threshold: float = 0.1, + n: int = 10, + jitter: int = 1, +): + """ + Select along `dim` with step sizes drawn from {n-jitter, …, n+jitter}. Works for Dataset or DataArray. + + Parameters + ---------- + path_to_nc : str + Path to the file containing the dataset of the full data series (lat/long) that we want to subsample. + dim : str + Dimension to operate the subsampling on. Example: stn for stations in a vector of stations. + outpath : str + Path to the output file to write to disk. + rng : float + Initial seed for the random number generator. Using 'None' will make it random. + threshold : float + Threshold for the amount of precipitation considered trace, as to not influence the distribution vs machine + precision of the reanalysis models. + n : int + Average interval length to jump from one to the next sampling point. + jitter : int + Noise added to the interval to randomize the search pattern and not systematically sample ever n points. + + Returns + ------- + bool : + Returns True if the function exited properly. + """ + # Clean precip (threshold and format) + ds = clean_precip(path_to_nc, threshold=threshold) + + # Get the random seed. + rng = np.random.default_rng(rng) + + # Maximum available value to sample. + k = ds.sizes[dim] + + # Draw steps until we pass the end; then build cumulative indices. + steps = [] + start = 0 + + while True: + step = n + rng.integers(-jitter, jitter + 1) + if step <= 0: + continue # guard against pathological jitter + steps.append(step) + if start + sum(steps) >= k: + break + idx = start + np.cumsum([0] + steps[:-1]) # starting index + cumulative steps + idx = idx[idx < k] + + # Build and save the dataset with the dimension subsampled according to the built index. + subset = ds.isel({dim: idx}) + subset.to_netcdf(outpath) + + return True + + +def clean_precip( + ds_path: str, + threshold: float = 0.1, +) -> xr.Dataset: + """ + Convert to float32 and remove any negative precipitation value. + + Parameters + ---------- + ds_path : str + Path to the netcdf file containing the precip to load and clean. + threshold : float + Threshold for the amount of precipitation considered trace, as to not influence the distribution vs machine + precision of the reanalysis models. + + Returns + ------- + xr.Dataset : + The cleaned precipitation dataset. + """ + # Load the precipitation dataset. + ds = xr.open_dataset(ds_path) + + # Convert to float32. + ds["precip"] = ds["precip"].astype(np.float32) + + # Apply threshold for minimum trace value. + ds["precip"].values[ds["precip"].values < threshold] = 0.0 + + # Return the cleaned dataset + return ds diff --git a/src/xhydro/pygmet/transform_to_station_order.py b/src/xhydro/pygmet/transform_to_station_order.py new file mode 100644 index 00000000..06ee99f8 --- /dev/null +++ b/src/xhydro/pygmet/transform_to_station_order.py @@ -0,0 +1,172 @@ +"""Functions for processing OI data into formats used by PyGMET.""" + +import numpy as np +import xarray as xr + + +def convert_2d_nc_to_1d_stations(path_nc_oi_precip: str, path_nc_grid_temperature: str, outpath: str) -> bool: + """ + Transform the 2D ERA5Land data into a 1D station-like format. + + Parameters + ---------- + path_nc_oi_precip : str + Path to the nc file containing the output of the optimal interpolation results. + path_nc_grid_temperature : str + Path to the nc file containing the gridded temperature matching the times and locations of the OI precip + product. + outpath : str + Path to the output file containing the nan-less station-like vector netcdf of ERA5Land + OI grids (used as + stations in PyGMET). + + Returns + ------- + bool : + True if the code ran correctly, error message if not. + """ + # NetCDF file with ERA5Land precipitation processed with OI (only precip) + ds = xr.open_dataset(path_nc_oi_precip) + + # OI code will return a percentile, so this is to take care of that particular case. + if "percentile" in ds: + ds = ds.sel(percentile=50) + + # Transpose to work with data in correct dimensions. + ds = ds.transpose("longitude", "latitude", "time") + + # NetCDF file with ERA5Land tmin and tmax. + dstemp = xr.open_dataset(path_nc_grid_temperature) + + # Get the same times. + dstemp = dstemp.sel(time=slice(ds["time"].values[0], ds["time"].values[-1])) + + # We want to create a mask of NaN values. Precipitation processed with OI will not have NaNs so we need to use the + # temperature to find them. + maskvar = np.empty((len(ds.longitude), len(ds.latitude))) + maskvar[:] = 1 + temp_c = dstemp["tasmax"].transpose("longitude", "latitude", "time") + + # find the nan values by taking a random day. I don't like taking the 1st so I use 10 here. + nanmask = temp_c[:, :, 10].isnull() + maskvar[nanmask] = 0 + + # Make the nan mask 3D for the application to the initial dataset of lat x lon x time. + mask_3d_broadcast = np.broadcast_to(nanmask, [ds["tp"].shape[2], nanmask.shape[0], nanmask.shape[1]]) + mask_3d_broadcast = mask_3d_broadcast.transpose(1, 2, 0) + + # Apply the mask to precipitation. Recall that temperature already has the NaNs. + ds["tp"].values = ds["tp"].where(~mask_3d_broadcast, np.nan) + + # Stack in station 1D by unrolling the 3D matrices into a 2D (station x time) matrix. Do this for the precip and + # also the temperature. + da_stn = ds["tp"].stack(stn=("latitude", "longitude")) + da_stn_tmax = dstemp["tasmax"].stack(stn=("latitude", "longitude")) + da_stn_tmin = dstemp["tasmin"].stack(stn=("latitude", "longitude")) + + # Get the latitude and longitude values associated with each station. + lat_vals = da_stn.indexes["stn"].get_level_values("latitude") + lon_vals = da_stn.indexes["stn"].get_level_values("longitude") + + # Remove stations that are all NaNs. + nan_columns = da_stn.isnull().all(dim="time").values + valid_mask = ~nan_columns + lat_vals = lat_vals[valid_mask] + lon_vals = lon_vals[valid_mask] + + da_stn = da_stn.dropna(dim="stn", how="any") + da_stn_tmax = da_stn_tmax.dropna(dim="stn", how="any") + da_stn_tmin = da_stn_tmin.dropna(dim="stn", how="any") + + # Rebuild the index of stations starting at 0. + da_stn = da_stn.assign_coords(stn=np.arange(da_stn.sizes["stn"])) + + # Make the dataset, write to disk. + ds = xr.Dataset( + { + "precip": (("stn", "time"), da_stn.values.transpose()), + "tmax": (("stn", "time"), da_stn_tmax.values.transpose()), + "tmin": (("stn", "time"), da_stn_tmin.values.transpose()), + "latitude": (("stn"), lat_vals), + "longitude": (("stn"), lon_vals), + }, + coords={"time": ds.time.values, "stn": np.arange(0, da_stn.sizes["stn"])}, + ) + + ds["stnid"] = ds["stn"] + ds.to_netcdf(outpath) + + return True + + +def make_target_pygmet_grid( + ds_in_path: str, + outpath: str, +): + """ + Build the netcdf Dataset required by PyGMET to use as the output grid. + + Parameters + ---------- + ds_in_path : str + Path to the template dataset to use as the basis for 2D grid. + outpath : str + Path to the 2D mask file. + + Returns + ------- + bool : + True if the function exits with no error. + """ + # Open the dataset + ds_in = xr.open_dataset(ds_in_path) + lat_grid = ds_in["latitude"].values + lon_grid = ds_in["longitude"].values + + # Define some fixed parameters + nparam = 1 + + # Number of latitudes and longitudes (i.e. size of grid) + ny = lat_grid.shape[0] + nx = lon_grid.shape[0] + + # indices of the coordinates + param = np.arange(nparam) # ex. [0] + + # Create the variables + # step for longitude and latitude + dx = np.full((nparam,), np.round(ds_in.longitude[1].values - ds_in.longitude[0].values, 5)) + dy = np.full((nparam,), np.round(ds_in.latitude[1].values - ds_in.latitude[0].values, 5)) + + # Origins for latitude and longitude (from where the steps begin) + startx = np.array(np.round(ds_in.longitude.values[0], 5)).reshape(1) # longitude origin + starty = np.array(np.round(ds_in.latitude.values[0], 5)).reshape(1) # latitude origin + + # Rebuild 2D array of lat/lon + lon2d, lat2d = np.meshgrid(lon_grid, lat_grid) + + # Dimensions (lat, lon) + elev = np.zeros((ny, nx)) + 1.0 # altitude (m), pygmet can use it as an extra covariate, but we don't. + mask = np.ones((ny, nx), dtype=np.int32) # mask of 0/1, but we will force it later. + + # Build the xarray Dataset + ds = xr.Dataset( + data_vars={ + "dx": (("param",), dx), + "dy": (("param",), dy), + "startx": (("param",), startx), + "starty": (("param",), starty), + "elev": (("y", "x"), elev), + "latitude": (("y", "x"), lat2d), + "longitude": (("y", "x"), lon2d), + "mask": (("y", "x"), mask), + }, + # Add coordinates + coords={ + "param": param, + "y": lat_grid, + "x": lon_grid, + }, + ) + + # Write to disk + ds.to_netcdf(outpath) diff --git a/tests/subset_grid_temperature.nc b/tests/subset_grid_temperature.nc new file mode 100644 index 0000000000000000000000000000000000000000..c09a127f025145b5a0925b231e05226fc2d91afb GIT binary patch literal 25664 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 literal 0 HcmV?d00001 diff --git a/tests/test_pygmet_prep.py b/tests/test_pygmet_prep.py new file mode 100644 index 00000000..6c834d1a --- /dev/null +++ b/tests/test_pygmet_prep.py @@ -0,0 +1,83 @@ +from xhydro.pygmet.make_toml_config_pygmet import write_config_toml +from xhydro.pygmet.make_toml_settings_pygmet import write_settings_toml +from xhydro.pygmet.subsample_vector_format_stations import isel_every_about_n +from xhydro.pygmet.transform_to_station_order import convert_2d_nc_to_1d_stations, make_target_pygmet_grid + + +def test_make_pygmet_config(): + params = { + "case_name": "My_case_01", + "num_processes": 10, + "modelsettings_file": "model.settings_xhydro.toml", + "input_stn_all": "./stations.nc", + "infile_grid_domain": "./grid_domain.nc", + "outpath_parent": "./output_dir", + "date_start": "1970-01-01", + "date_end": "2024-12-31", + "input_vars": ["precip", "tmin", "tmax"], + "target_vars": ["precip", "tmin", "tmax"], + "target_vars_WithProbability": ["precip"], + "minRange_vars": [0.0, -70.0, -70.0], + "maxRange_vars": [250.0, 70.0, 70.0], + "transform_vars": ["boxcox", "", ""], + "predictor_name_static_stn": ["latitude", "longitude"], + "predictor_name_static_grid": ["latitude", "longitude"], + "ensemble_end": 100, + "clen": [150, 800, 800], # codespell:ignore clen + "auto_corr_method": ["direct", "anomaly", "anomaly"], + "target_vars_max_constrain": ["precip"], + } + + write_config_toml("model.config_xhydro.toml", params, strict=True) + + +def test_make_pygmet_settings(): + params = { + "stn_lat_name": "latitude", + "stn_lon_name": "longitude", + "grid_lat_name": "latitude", + "grid_lon_name": "longitude", + "grid_mask_name": "mask", + "dynamic_grid_lat_name": "latitude", + "dynamic_grid_lon_name": "longitude", + "nearstn_min": 2, + "nearstn_max": 16, + "try_radius": 200, + "initial_distance": 200, + } + + write_settings_toml("model.settings_xhydro.toml", params, strict=True) + + +def test_convert_2d_to_1d(): + path_nc_oi_precip = "./subset_oi_tp.nc" + path_nc_grid_temperature = "./subset_grid_temperature.nc" + outpath = "./subset_reordered_grids_to_stations.nc/" + + convert_2d_nc_to_1d_stations( + path_nc_oi_precip, + path_nc_grid_temperature, + outpath, + ) + + +def test_subsample_stations_for_pygmet(): + path_to_nc = "./subset_reordered_grids_to_stations.nc" + outpath = "./subset_oi_vector_format_subsampled.nc" + + # Take roughly 1 out of every ~10 stations + isel_every_about_n( + path_to_nc=path_to_nc, + dim="stn", + outpath=outpath, + rng=42, + threshold=0.1, + n=10, + jitter=3, + ) + + +def test_make_target_pygmet_grid(): + ds_in_path = "./subset_grid_temperature.nc" + outpath = "./new_grid_output_shape.nc" + make_target_pygmet_grid(ds_in_path, outpath) From 8e02978fe3fd6fb072f19c443a642a5146e21be5 Mon Sep 17 00:00:00 2001 From: richardarsenault Date: Mon, 29 Dec 2025 10:31:31 -0500 Subject: [PATCH 2/6] updated nan-mask logic for PyGMET output grids --- src/xhydro/pygmet/transform_to_station_order.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/xhydro/pygmet/transform_to_station_order.py b/src/xhydro/pygmet/transform_to_station_order.py index 06ee99f8..6d5d87c0 100644 --- a/src/xhydro/pygmet/transform_to_station_order.py +++ b/src/xhydro/pygmet/transform_to_station_order.py @@ -145,8 +145,10 @@ def make_target_pygmet_grid( lon2d, lat2d = np.meshgrid(lon_grid, lat_grid) # Dimensions (lat, lon) - elev = np.zeros((ny, nx)) + 1.0 # altitude (m), pygmet can use it as an extra covariate, but we don't. - mask = np.ones((ny, nx), dtype=np.int32) # mask of 0/1, but we will force it later. + elev = np.ones((ny, nx)) # altitude (m), pygmet can use it as an extra covariate, but we don't. + + # Make a mask based on the position of NaNs in the original gridded dataset. + mask = ds_in.tasmax.isel(time=1).transpose("latitude", "longitude")[:, :].notnull().astype(int).values # Build the xarray Dataset ds = xr.Dataset( From 81936983922f974314394b2370175df0d66dbdf5 Mon Sep 17 00:00:00 2001 From: richardarsenault Date: Mon, 2 Feb 2026 20:04:38 -0500 Subject: [PATCH 3/6] updated to use data from xhydro-testdata --- tests/test_pygmet_prep.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_pygmet_prep.py b/tests/test_pygmet_prep.py index 6c834d1a..3be60bc3 100644 --- a/tests/test_pygmet_prep.py +++ b/tests/test_pygmet_prep.py @@ -2,6 +2,7 @@ from xhydro.pygmet.make_toml_settings_pygmet import write_settings_toml from xhydro.pygmet.subsample_vector_format_stations import isel_every_about_n from xhydro.pygmet.transform_to_station_order import convert_2d_nc_to_1d_stations, make_target_pygmet_grid +from xhydro.testing.helpers import deveraux def test_make_pygmet_config(): @@ -50,8 +51,9 @@ def test_make_pygmet_settings(): def test_convert_2d_to_1d(): - path_nc_oi_precip = "./subset_oi_tp.nc" - path_nc_grid_temperature = "./subset_grid_temperature.nc" + path_nc_oi_precip = deveraux(branch="pygmet").fetch("pygmet/subset_oi_tp.nc") + path_nc_grid_temperature = deveraux(branch="pygmet").fetch("pygmet/subset_grid_temperature.nc") + outpath = "./subset_reordered_grids_to_stations.nc/" convert_2d_nc_to_1d_stations( From 6a6940934c08d1201dd9a295137523d2ad2289ee Mon Sep 17 00:00:00 2001 From: richardarsenault Date: Fri, 6 Feb 2026 15:56:26 -0500 Subject: [PATCH 4/6] changed xhydro-testdata paths due to merging of test branch --- src/xhydro/testing/helpers.py | 2 +- src/xhydro/testing/registry.txt | 5 +++++ tests/subset_grid_temperature.nc | Bin 25664 -> 0 bytes tests/subset_oi_tp.nc | Bin 21959 -> 0 bytes tests/test_pygmet_prep.py | 6 +++--- 5 files changed, 9 insertions(+), 4 deletions(-) delete mode 100644 tests/subset_grid_temperature.nc delete mode 100644 tests/subset_oi_tp.nc diff --git a/src/xhydro/testing/helpers.py b/src/xhydro/testing/helpers.py index ebd31e25..8244bd30 100644 --- a/src/xhydro/testing/helpers.py +++ b/src/xhydro/testing/helpers.py @@ -29,7 +29,7 @@ "populate_testing_data", ] -default_testdata_version = "v2025.8.14" +default_testdata_version = "v2026.02.04" """Default version of the testing data to use when fetching datasets.""" default_testdata_repo_url = "https://raw.githubusercontent.com/hydrologie/xhydro-testdata/" diff --git a/src/xhydro/testing/registry.txt b/src/xhydro/testing/registry.txt index 4152058c..2f69afb6 100644 --- a/src/xhydro/testing/registry.txt +++ b/src/xhydro/testing/registry.txt @@ -15,6 +15,11 @@ optimal_interpolation/OI_data.zip sha256:9cd881a19fc82bda560e636d3f6a2c40718b82f optimal_interpolation/OI_data_corrected.zip sha256:48ee08325bd35c6bce5c0e52e3ee25df27c830720929060f607fb0417c476941 pmp/CMIP.CCCma.CanESM5.historical.r1i1p1f1.day.gn.zarr.zip sha256:d5775b2f09381f2f3a7cc06af76f62fb216b60252aab3a602280a513554d59ad pmp/CMIP.CCCma.CanESM5.historical.r1i1p1f1.fx.gn.zarr.zip sha256:dc7c92fc098ca5adf43e76b25e3f79b70815ed961e945952983bb68b3c380cf1 +precip_oi/ERA5Land_with_OI_flags_validation_subset.nc sha256:50fc8d562443efe82998735c50268019532493bc02b47e4a47f6672708c87b5f +precip_oi/ERA5_land_3_variables_raw_subset.nc sha256:09ee75f73bb4e88bddb6c9184245766ebe8cc6b13b58ef58a4754e523138ff1f +precip_oi/stations_flags_clean_subset.nc sha256:03513b859e6c99ab02728f49b0e3e7382375e7311c68e33c8d59d6f734eb08d1 +pygmet/subset_grid_temperature.nc sha256:66a3086d9aaacbedac6633050380b9b7685407c952a3f8629a16caea4d1ef8db +pygmet/subset_oi_tp.nc sha256:1cddcb439168891e4ad025c7708c6d0ce1017bbb013ac2fe46589dc3fb5953da ravenpy/Debit_Riviere_Rouge.nc sha256:d0a27de5eb3cb466e60669d894296bcbc4e9f590edc1ae2490685babd10b2d22 ravenpy/ERA5_Riviere_Rouge_global.nc sha256:341ac746130a0d3e3189d3a41dc8528d6bd22869a519b68e134959407ad200a3 ravenpy/hru_subset.zip sha256:969b4a0d0c1f0c24dbb5baca98676f2573178cd58ef4162aa6b9fce51ceecf06 diff --git a/tests/subset_grid_temperature.nc b/tests/subset_grid_temperature.nc deleted file mode 100644 index c09a127f025145b5a0925b231e05226fc2d91afb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25664 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 diff --git a/tests/test_pygmet_prep.py b/tests/test_pygmet_prep.py index 3be60bc3..7974ad80 100644 --- a/tests/test_pygmet_prep.py +++ b/tests/test_pygmet_prep.py @@ -51,8 +51,8 @@ def test_make_pygmet_settings(): def test_convert_2d_to_1d(): - path_nc_oi_precip = deveraux(branch="pygmet").fetch("pygmet/subset_oi_tp.nc") - path_nc_grid_temperature = deveraux(branch="pygmet").fetch("pygmet/subset_grid_temperature.nc") + path_nc_oi_precip = deveraux().fetch("pygmet/subset_oi_tp.nc") + path_nc_grid_temperature = deveraux().fetch("pygmet/subset_grid_temperature.nc") outpath = "./subset_reordered_grids_to_stations.nc/" @@ -80,6 +80,6 @@ def test_subsample_stations_for_pygmet(): def test_make_target_pygmet_grid(): - ds_in_path = "./subset_grid_temperature.nc" + ds_in_path = deveraux().fetch("pygmet/subset_grid_temperature.nc") outpath = "./new_grid_output_shape.nc" make_target_pygmet_grid(ds_in_path, outpath) From d0d615803bff448e20fd9d8e4b50da9ad39c8b4a Mon Sep 17 00:00:00 2001 From: richardarsenault Date: Sun, 15 Feb 2026 21:34:35 -0500 Subject: [PATCH 5/6] updated pygmet tests to autogenerate data and skip xhydro-testdata --- tests/test_pygmet_prep.py | 175 ++++++++++++++++++++++++++++++++------ 1 file changed, 147 insertions(+), 28 deletions(-) diff --git a/tests/test_pygmet_prep.py b/tests/test_pygmet_prep.py index 7974ad80..a644f7d9 100644 --- a/tests/test_pygmet_prep.py +++ b/tests/test_pygmet_prep.py @@ -1,8 +1,13 @@ +import tempfile + +import numpy as np +import pandas as pd +import xarray as xr + from xhydro.pygmet.make_toml_config_pygmet import write_config_toml from xhydro.pygmet.make_toml_settings_pygmet import write_settings_toml from xhydro.pygmet.subsample_vector_format_stations import isel_every_about_n from xhydro.pygmet.transform_to_station_order import convert_2d_nc_to_1d_stations, make_target_pygmet_grid -from xhydro.testing.helpers import deveraux def test_make_pygmet_config(): @@ -28,8 +33,9 @@ def test_make_pygmet_config(): "auto_corr_method": ["direct", "anomaly", "anomaly"], "target_vars_max_constrain": ["precip"], } - - write_config_toml("model.config_xhydro.toml", params, strict=True) + tempout = tempfile.mkdtemp() + outpath = tempout + "/model.config_xhydro.toml" + write_config_toml(outpath, params, strict=True) def test_make_pygmet_settings(): @@ -46,40 +52,153 @@ def test_make_pygmet_settings(): "try_radius": 200, "initial_distance": 200, } + tempout = tempfile.mkdtemp() + outpath = tempout + "/model.settings_xhydro.toml" + write_settings_toml(outpath, params, strict=True) + + +class TestPreparePygmetInputs: + # Prepare the data for the subset OI precip grid. + # Coordinates + percentile = np.array([50], dtype=np.int64) + time = pd.date_range("1970-01-01", periods=15, freq="D") + latitude = np.round(50.0 - 0.1 * np.arange(10), 1).astype(np.float64) + longitude = np.round(-78.0 + 0.1 * np.arange(10), 1).astype(np.float64) + + # Repeatable + rng = np.random.default_rng(12345) # seed = repeatable + tp = rng.random((len(percentile), len(time), len(latitude), len(longitude))).astype(np.float64) + + # Build dataset + ds = xr.Dataset( + data_vars={"tp": (("percentile", "time", "latitude", "longitude"), tp)}, + coords={ + "percentile": percentile, + "time": time, + "latitude": latitude, + "longitude": longitude, + }, + ) + + # Write the file to temp location + tmpfile = tempfile.mkdtemp() + path_nc_oi_precip = tmpfile + "/subset_oi_tp.nc" + ds.to_netcdf(path_nc_oi_precip) + + # Now prepare the data for the subset temperature grid. + # Coordinates + time = pd.date_range("1970-01-01", periods=15, freq="D") # datetime64[ns] + longitude = np.round(-78.0 + 0.1 * np.arange(10), 1).astype(np.float64) # -78.0 ... -77.1 + latitude = np.round(50.0 - 0.1 * np.arange(10), 1).astype(np.float64) # 50.0 ... 49.1 - write_settings_toml("model.settings_xhydro.toml", params, strict=True) + # Repeatable + rng = np.random.default_rng(12345) + shape_3d = (len(longitude), len(latitude), len(time)) -def test_convert_2d_to_1d(): - path_nc_oi_precip = deveraux().fetch("pygmet/subset_oi_tp.nc") - path_nc_grid_temperature = deveraux().fetch("pygmet/subset_grid_temperature.nc") + # Base mean temperature field + tmean = rng.normal(loc=12.0, scale=6.0, size=shape_3d).astype(np.float32) - outpath = "./subset_reordered_grids_to_stations.nc/" + # Positive daily range + trange = rng.uniform(3.0, 12.0, size=shape_3d).astype(np.float32) + tasmin = (tmean - 0.5 * trange).astype(np.float32) + tasmax = (tmean + 0.5 * trange).astype(np.float32) - convert_2d_nc_to_1d_stations( - path_nc_oi_precip, - path_nc_grid_temperature, - outpath, + # Altitude (m), repeatable + altitude = rng.uniform(0, 1200, size=(len(longitude), len(latitude))).astype(np.float64) + + # Build dataset + ds = xr.Dataset( + data_vars={ + "tasmax": (("longitude", "latitude", "time"), tasmax), + "tasmin": (("longitude", "latitude", "time"), tasmin), + "altitude": (("longitude", "latitude"), altitude), + }, + coords={ + "longitude": longitude, + "latitude": latitude, + "time": time, + }, ) + # Write file to temp location as well. + tmpfile_temperature = tempfile.mkdtemp() + path_nc_grid_temperature = tmpfile_temperature + "/subset_grid_temperature.nc" + ds.to_netcdf(path_nc_grid_temperature) + + # Intermediate data for reordered stations + stn = np.arange(100, dtype=np.int64) + time = pd.date_range("1970-01-01", periods=15, freq="D") # datetime64[ns] + # Build station lat/lon from a 10x10 grid (same values/ranges as previous grid example) + lon_1d = np.round(-78.0 + 0.1 * np.arange(10), 1).astype(np.float64) + lat_1d = np.round(50.0 - 0.1 * np.arange(10), 1).astype(np.float64) + + # IMPORTANT: indexing="ij" matches grid dim order (longitude, latitude) + lon2d, lat2d = np.meshgrid(lon_1d, lat_1d, indexing="ij") # both shape (10, 10) + + # Flatten to stations (100 points). Order="C" = latitude varies fastest within each longitude. + longitude = lon2d.ravel(order="C") + latitude = lat2d.ravel(order="C") + + # Station IDs + stnid = (stn).astype(np.int64) -def test_subsample_stations_for_pygmet(): - path_to_nc = "./subset_reordered_grids_to_stations.nc" - outpath = "./subset_oi_vector_format_subsampled.nc" + # 2) Repeatable data + rng = np.random.default_rng(12345) # seed -> repeatable + shape = (len(stn), len(time)) - # Take roughly 1 out of every ~10 stations - isel_every_about_n( - path_to_nc=path_to_nc, - dim="stn", - outpath=outpath, - rng=42, - threshold=0.1, - n=10, - jitter=3, + # Precip + precip = rng.gamma(shape=2.0, scale=2.0, size=shape).astype(np.float64) + + # Temperatures + tmean = rng.normal(loc=12.0, scale=6.0, size=shape).astype(np.float32) # mean °C + trange = rng.uniform(3.0, 12.0, size=shape).astype(np.float32) # daily range °C (>0) + tmin = (tmean - 0.5 * trange).astype(np.float32) + tmax = (tmean + 0.5 * trange).astype(np.float32) + + # 3) Build Dataset + ds_stn = xr.Dataset( + data_vars={ + "precip": (("stn", "time"), precip), + "tmax": (("stn", "time"), tmax), + "tmin": (("stn", "time"), tmin), + "latitude": (("stn",), latitude), + "longitude": (("stn",), longitude), + "stnid": (("stn",), stnid), + }, + coords={ + "stn": stn, + "time": time, + }, ) + # Write file to temp location as well. + tmpfile_reordered = tempfile.mkdtemp() + path_nc_grid_reordered = tmpfile_reordered + "/subset_reordered_grids_to_stations.nc" + ds_stn.to_netcdf(path_nc_grid_reordered) + + # tempout folder + tempout = tempfile.mkdtemp() + + def test_convert_2d_to_1d(self): + # Run the code + convert_2d_nc_to_1d_stations( + self.path_nc_oi_precip, + self.path_nc_grid_temperature, + self.tempout + "/subset_reordered_grids_to_stations.nc/", + ) + + def test_subsample_stations_for_pygmet(self): + # Take roughly 1 out of every ~10 stations + isel_every_about_n( + path_to_nc=self.path_nc_grid_reordered, + dim="stn", + outpath=self.tempout + "/subset_oi_vector_format_subsampled.nc/", + rng=42, + threshold=0.1, + n=10, + jitter=3, + ) -def test_make_target_pygmet_grid(): - ds_in_path = deveraux().fetch("pygmet/subset_grid_temperature.nc") - outpath = "./new_grid_output_shape.nc" - make_target_pygmet_grid(ds_in_path, outpath) + def test_make_target_pygmet_grid(self): + make_target_pygmet_grid(self.path_nc_grid_temperature, self.tempout + "/new_grid_output_shape.nc") From 9cc0f021361a648ca2956dd03ed75d58c4ac272a Mon Sep 17 00:00:00 2001 From: richardarsenault Date: Mon, 16 Feb 2026 08:26:12 -0500 Subject: [PATCH 6/6] cleaned tests and added more detailed checks --- tests/test_pygmet_prep.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_pygmet_prep.py b/tests/test_pygmet_prep.py index a644f7d9..7491900c 100644 --- a/tests/test_pygmet_prep.py +++ b/tests/test_pygmet_prep.py @@ -3,6 +3,7 @@ import numpy as np import pandas as pd import xarray as xr +from numpy.ma.testutils import assert_almost_equal from xhydro.pygmet.make_toml_config_pygmet import write_config_toml from xhydro.pygmet.make_toml_settings_pygmet import write_settings_toml @@ -188,6 +189,12 @@ def test_convert_2d_to_1d(self): self.tempout + "/subset_reordered_grids_to_stations.nc/", ) + ds = xr.open_dataset(self.tempout + "/subset_reordered_grids_to_stations.nc/") + assert_almost_equal(ds.precip.isel(stn=5, time=5).values, 0.2762543, 5) + assert_almost_equal(ds.tmax.isel(stn=6, time=5).values, 13.288241, 5) + assert ds.stn.shape[0] == 100 + assert ds.stn[-1] == 99 + def test_subsample_stations_for_pygmet(self): # Take roughly 1 out of every ~10 stations isel_every_about_n( @@ -200,5 +207,18 @@ def test_subsample_stations_for_pygmet(self): jitter=3, ) + ds = xr.open_dataset(self.tempout + "/subset_oi_vector_format_subsampled.nc/") + assert_almost_equal(ds.precip.isel(stn=5, time=5).values, 3.057068, 5) + assert_almost_equal(ds.tmax.isel(stn=6, time=5).values, 10.385146, 5) + assert ds.stn.shape[0] == 11 + assert ds.stn[-1] == 96 + def test_make_target_pygmet_grid(self): - make_target_pygmet_grid(self.path_nc_grid_temperature, self.tempout + "/new_grid_output_shape.nc") + make_target_pygmet_grid( + self.path_nc_grid_temperature, + self.tempout + "/new_grid_output_shape.nc", + ) + + ds = xr.open_dataset(self.tempout + "/new_grid_output_shape.nc") + assert ds.y[0] == 50 + assert ds.mask[5, 5].values == 1