diff --git a/.github/workflows/test_pysteps.yml b/.github/workflows/test_pysteps.yml index 321b971eb..3a4c0488b 100644 --- a/.github/workflows/test_pysteps.yml +++ b/.github/workflows/test_pysteps.yml @@ -100,7 +100,17 @@ jobs: working-directory: ${{github.workspace}}/pysteps_data env: PYSTEPSRC: ${{github.workspace}}/pysteps_data/pystepsrc - run: pytest --pyargs pysteps --cov=pysteps --cov-report=xml --cov-report=term -ra + PYSTEPS_REPO_ROOT: ${{github.workspace}} + run: pytest --pyargs pysteps --cov=pysteps --cov-report=xml --cov-report=term -ra --basetemp=${{github.workspace}}/pytest-tmp + + - name: Upload example test images + if: always() + uses: actions/upload-artifact@v4 + with: + name: example-test-images-${{ matrix.os }}-${{ matrix.python-version }} + path: ${{github.workspace}}/pytest-tmp/**/fig_*.png + if-no-files-found: ignore + retention-days: 7 - name: Upload coverage to Codecov (Linux only) if: matrix.os == 'ubuntu-latest' diff --git a/ci/ci_test_env.yml b/ci/ci_test_env.yml index 08db99442..0fd6d4566 100644 --- a/ci/ci_test_env.yml +++ b/ci/ci_test_env.yml @@ -34,3 +34,4 @@ dependencies: - pytest-cov - pip: - cookiecutter + - git+https://github.com/pySTEPS/pysteps-nwp-importers diff --git a/examples/ens_kalman_filter_blended_forecast.py b/examples/ens_kalman_filter_blended_forecast.py index e139b1b0c..256375069 100644 --- a/examples/ens_kalman_filter_blended_forecast.py +++ b/examples/ens_kalman_filter_blended_forecast.py @@ -22,13 +22,13 @@ from datetime import datetime, timedelta import numpy as np +import pysteps_nwp_importers from matplotlib import pyplot as plt import pysteps -from pysteps import io, rcparams, blending +from pysteps import blending, io, rcparams from pysteps.utils import aggregate_fields_space from pysteps.visualization import plot_precip_field -import pysteps_nwp_importers ################################################################################ # Read the radar images and the NWP forecast @@ -87,11 +87,9 @@ + nwp_data_source["fn_ext"], ) nwp_importer = io.get_method("dwd_nwp", "importer") -kwargs = nwp_data_source["importer_kwargs"] -# Resolve grid_file_path relative to PYSTEPS_DATA_PATH -kwargs["grid_file_path"] = os.path.join( - os.environ["PYSTEPS_DATA_PATH"], kwargs["grid_file_path"] -) +# grid_file_path in importer_kwargs is already resolvable from the repo root, +# like every other data_sources path in pystepsrc. +kwargs = dict(nwp_data_source["importer_kwargs"]) nwp_precip, _, nwp_metadata = nwp_importer(filename, **kwargs) # We lower the number of ens members to 10 to reduce the memory needs in the # example here. However, it is advised to have a minimum of 20 members for the @@ -250,7 +248,10 @@ n_cascade_levels=6, # No. of cascade levels precip_thr=log_thr_prec, # Precip threshold norain_thr=0.0005, # Minimum of 0.5% precip needed, otherwise 'zero rainfall' - num_workers=4, # No. of parallel threads + # No. of parallel threads. Kept at 1 so that the forecast (and the plots + # in this example) are exactly reproducible: multi-threaded FFT/dask + # reductions are not guaranteed to be bit-for-bit deterministic. + num_workers=1, noise_stddev_adj="auto", # Standard deviation adjustment noise_method="ssft", # SSFT as noise method enable_combination=True, # Enable combination @@ -258,6 +259,7 @@ extrap_kwargs={"interp_order": 3, "map_coordinates_mode": "nearest"}, combination_kwargs=combination_kwargs, filter_kwargs={"include_mean": True}, + seed=42, # Fixed seed for reproducible ensemble members ) # Transform the data back into mm/h @@ -304,6 +306,7 @@ colorbar=False, ) +plt.show() ################################################################################ # References diff --git a/examples/linda_nowcasts.py b/examples/linda_nowcasts.py index a121ccaf3..45ee64f3f 100644 --- a/examples/linda_nowcasts.py +++ b/examples/linda_nowcasts.py @@ -137,6 +137,7 @@ n_ens_members=40, num_workers=8, measure_time=True, + seed=42, # Fixed seed for reproducible ensemble members )[0] # Compute 40-member STEPS nowcast for comparison @@ -151,6 +152,7 @@ kmperpixel=2.0, timestep=datasource_params["timestep"], vel_pert_method=None, + seed=42, # Fixed seed for reproducible ensemble members ) # Convert reflectivity nowcast to rain rate diff --git a/examples/optical_flow_methods_convergence.py b/examples/optical_flow_methods_convergence.py index 9208af594..f9a8ce45b 100644 --- a/examples/optical_flow_methods_convergence.py +++ b/examples/optical_flow_methods_convergence.py @@ -18,8 +18,8 @@ Let's first load the libraries that we will use. """ -from datetime import datetime import time +from datetime import datetime import matplotlib.pyplot as plt import numpy as np @@ -27,7 +27,7 @@ from scipy.ndimage import uniform_filter import pysteps as stp -from pysteps import motion, io, rcparams +from pysteps import io, motion, rcparams from pysteps.motion.vet import morph from pysteps.visualization import plot_precip_field, quiver @@ -237,12 +237,14 @@ def create_observations(input_precip, motion_type, num_times=9): synthetic_observations = np.ma.masked_invalid(synthetic_observations) - synthetic_observations.data[np.ma.getmaskarray(synthetic_observations)] = 0 + synthetic_observations.data[np.ma.getmaskarray(synthetic_observations)] = np.nan return ideal_motion, synthetic_observations -def plot_optflow_method_convergence(input_precip, optflow_method_name, motion_type): +def plot_optflow_method_convergence( + input_precip, optflow_method_name, motion_type, remove_nans=False +): """ Test the convergence to the actual solution of the optical flow method used. @@ -276,7 +278,11 @@ def plot_optflow_method_convergence(input_precip, optflow_method_name, motion_ty elapsed_time = time.perf_counter() - computed_motion = oflow_method(precip_obs, verbose=False) + precip_for_motion = precip_obs.data + if remove_nans: + precip_for_motion[precip_obs.mask] = np.nanmin(precip_for_motion) + + computed_motion = oflow_method(precip_for_motion, verbose=False) print( f"{optflow_method_name} computation time: " @@ -299,11 +305,13 @@ def plot_optflow_method_convergence(input_precip, optflow_method_name, motion_ty # Compare retrieved motion field with the ideal one plt.figure(figsize=(9, 4)) plt.subplot(1, 2, 1) - ax = plot_precip_field(precip_obs[0], title="Reference motion") + precip_plot_data = precip_obs[0].data + precip_plot_data[precip_obs[0].mask] = np.nan + ax = plot_precip_field(precip_plot_data, title="Reference motion") quiver(ideal_motion, step=25, ax=ax) plt.subplot(1, 2, 2) - ax = plot_precip_field(precip_obs[0], title="Retrieved motion") + ax = plot_precip_field(precip_plot_data, title="Retrieved motion") quiver(computed_motion, step=25, ax=ax) # To evaluate the accuracy of the computed_motion vectors, we will use @@ -314,9 +322,7 @@ def plot_optflow_method_convergence(input_precip, optflow_method_name, motion_ty mse = ((ideal_motion - computed_motion)[:, precip_mask] ** 2).mean() rel_mse = mse / (ideal_motion[:, precip_mask] ** 2).mean() - plt.suptitle( - f"{optflow_method_name} " f"Relative RMSE: {np.sqrt(rel_mse) * 100:.2f}%" - ) + plt.suptitle(f"{optflow_method_name} Relative RMSE: {np.sqrt(rel_mse) * 100:.2f}%") plt.show() @@ -362,17 +368,17 @@ def plot_optflow_method_convergence(input_precip, optflow_method_name, motion_ty # # Constant motion x-direction # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -plot_optflow_method_convergence(reference_field, "DARTS", "linear_x") +plot_optflow_method_convergence(reference_field, "DARTS", "linear_x", remove_nans=True) ################################################################################ # Constant motion y-direction # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -plot_optflow_method_convergence(reference_field, "DARTS", "linear_y") +plot_optflow_method_convergence(reference_field, "DARTS", "linear_y", remove_nans=True) ################################################################################ # Rotational motion # ~~~~~~~~~~~~~~~~~ -plot_optflow_method_convergence(reference_field, "DARTS", "rotor") +plot_optflow_method_convergence(reference_field, "DARTS", "rotor", remove_nans=True) ################################################################################ # Farneback diff --git a/examples/rainfarm_downscale.py b/examples/rainfarm_downscale.py index a40a318e2..8eb074b5a 100644 --- a/examples/rainfarm_downscale.py +++ b/examples/rainfarm_downscale.py @@ -114,6 +114,9 @@ def upscale_field(precip, metadata, scale_factor): # # We can now use RainFARM to downscale the precipitation field. +# Fixed seed for reproducible stochastic noise fields +np.random.seed(42) + # Basic downscaling precip_hr = rainfarm.downscale(precip_lr, ds_factor=scale_factor) diff --git a/examples/steps_blended_forecast.py b/examples/steps_blended_forecast.py index 484dc4207..2c369dad1 100644 --- a/examples/steps_blended_forecast.py +++ b/examples/steps_blended_forecast.py @@ -16,7 +16,7 @@ from matplotlib import pyplot as plt import pysteps -from pysteps import io, rcparams, blending, nowcasts +from pysteps import blending, io, nowcasts, rcparams from pysteps.visualization import plot_precip_field ################################################################################ @@ -169,6 +169,7 @@ kmperpixel=radar_metadata["xpixelsize"] / 1000.0, noise_stddev_adj="auto", vel_pert_method=None, + seed=42, # Fixed seed for reproducible ensemble members ) # Transform the data back into mm/h @@ -349,6 +350,7 @@ ) ax3.axis("off") +plt.show() ################################################################################ # Blend the external nowcast with NWP - ensemble mode @@ -375,6 +377,7 @@ probmatching_method="cdf", mask_method="incremental", weights_method="bps", + seed=42, # Fixed seed for reproducible ensemble members ) # Transform the data back into mm/h diff --git a/pysteps/blending/ens_kalman_filter_methods.py b/pysteps/blending/ens_kalman_filter_methods.py index 161b7242a..b015353f9 100644 --- a/pysteps/blending/ens_kalman_filter_methods.py +++ b/pysteps/blending/ens_kalman_filter_methods.py @@ -433,6 +433,16 @@ def __init__(self, config, params): self.__degradation_timestep = 0.2 self.__inflation_factor_obs_tmp = 1.0 + # Dedicated, per-member random generators for the iterative probability + # matching resampling below, so that "seed" fully controls the + # reproducibility of the forecast instead of relying on the global, + # unseeded numpy random state. + seed = self._config.seed + self.__randgen_probmatch = [ + np.random.RandomState(None if seed is None else seed + j) + for j in range(self._config.n_ens_members) + ] + print("Initialize masked ensemble Kalman filter") print("========================================") print("") @@ -603,6 +613,7 @@ def worker(j): first_array=background_ensemble[j], second_array=observation_ensemble[j], probability_first_array=1 - self.__sampling_probability, + randgen=self.__randgen_probmatch[j], ).reshape(self.__params.len_y, self.__params.len_x) dask_worker_collection = [] diff --git a/pysteps/blending/pca_ens_kalman_filter.py b/pysteps/blending/pca_ens_kalman_filter.py index dcab310ca..a3122e2cb 100644 --- a/pysteps/blending/pca_ens_kalman_filter.py +++ b/pysteps/blending/pca_ens_kalman_filter.py @@ -666,6 +666,15 @@ def __init__( self.__ens_member = ens_member + # Dedicated, per-member random generator for selecting noise fields from + # the pool in __iterate, so that the "seed" config option fully controls + # the reproducibility of the forecast instead of relying on the global, + # unseeded numpy random state. + seed = self.__forecast_state.config.seed + self.__randgen_select = np.random.RandomState( + None if seed is None else seed + ens_member + ) + # Bundle single steps of the forecast. def run_forecast_step(self, nwp, is_correction_timestep=False): # Decompose precipitation field. @@ -813,7 +822,9 @@ def __iterate(self): # precipitation mask and the standard deviation coefficients. epsilon = ( self.__forecast_state.noise_field_pool[ - np.random.randint(self.__forecast_state.config.n_noise_fields) + self.__randgen_select.randint( + self.__forecast_state.config.n_noise_fields + ) ] * self.__forecast_state.precip_mask[self.__ens_member][None, :, :] * self.__forecast_state.params.noise_std_coeffs[:, None, None] diff --git a/pysteps/io/importers.py b/pysteps/io/importers.py index d51cf9a53..58c7afe8a 100644 --- a/pysteps/io/importers.py +++ b/pysteps/io/importers.py @@ -519,10 +519,10 @@ def _import_bom_rf3_geodata(ds_rainfall): if getattr(ds_rainfall.variables["x"], "units") == "km": factor_scale = 1000.0 - geodata["x1"] = xmin * factor_scale - geodata["y1"] = ymin * factor_scale - geodata["x2"] = xmax * factor_scale - geodata["y2"] = ymax * factor_scale + geodata["x1"] = (xmin - xpixelsize * 0.5) * factor_scale + geodata["y1"] = (ymin - ypixelsize * 0.5) * factor_scale + geodata["x2"] = (xmax + xpixelsize * 0.5) * factor_scale + geodata["y2"] = (ymax + ypixelsize * 0.5) * factor_scale geodata["xpixelsize"] = xpixelsize * factor_scale geodata["ypixelsize"] = ypixelsize * factor_scale geodata["cartesian_unit"] = "m" @@ -1807,37 +1807,6 @@ def import_dwd_hdf5(filename, qty="RATE", **kwargs): if precip is None: raise IOError("requested quantity %s not found" % qty) - # Get the projection and grid information from the HDF5 file - pr = pyproj.Proj(file_content["where"]["projdef"]) - ll_x, ll_y = pr( - file_content["where"]["LL_lon"], - file_content["where"]["LL_lat"], - ) - ur_x, ur_y = pr( - file_content["where"]["UR_lon"], - file_content["where"]["UR_lat"], - ) - - # Determine domain corners in geographic and carthesian coordinates - if len([k for k in file_content["where"].keys() if "_lat" in k]) == 4: - lr_x, lr_y = pr( - file_content["where"]["LR_lon"], - file_content["where"]["LR_lat"], - ) - ul_x, ul_y = pr( - file_content["where"]["UL_lon"], - file_content["where"]["UL_lat"], - ) - x1 = min(ll_x, ul_x) - y1 = min(ll_y, lr_y) - x2 = max(lr_x, ur_x) - y2 = max(ul_y, ur_y) - else: - x1 = ll_x - y1 = ll_y - x2 = ur_x - y2 = ur_y - # Get the grid cell size if ( "where" in file_content["dataset1"].keys() @@ -1849,8 +1818,12 @@ def import_dwd_hdf5(filename, qty="RATE", **kwargs): xpixelsize = file_content["where"]["xscale"] ypixelsize = file_content["where"]["yscale"] else: - xpixelsize = None - ypixelsize = None + raise ValueError("No xscale found in ODIM hdf5 file") + + x1 = -xpixelsize * 0.5 + x2 = -xpixelsize * 0.5 + precip.shape[1] * xpixelsize + y1 = xpixelsize * 0.5 - precip.shape[0] * ypixelsize + y2 = ypixelsize * 0.5 # Get the unit and transform if qty == "ACRR": diff --git a/pysteps/motion/darts.py b/pysteps/motion/darts.py index 4e5050d48..6de47a794 100644 --- a/pysteps/motion/darts.py +++ b/pysteps/motion/darts.py @@ -11,8 +11,9 @@ DARTS """ -import numpy as np import time + +import numpy as np from numpy.linalg import lstsq, svd from pysteps import utils diff --git a/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_00.png b/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_00.png new file mode 100644 index 000000000..5d5e945dc Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_01.png b/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_01.png new file mode 100644 index 000000000..bbfaee88d Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_02.png b/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_02.png new file mode 100644 index 000000000..af01b8af6 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_02.png differ diff --git a/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_03.png b/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_03.png new file mode 100644 index 000000000..3084f7be5 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_03.png differ diff --git a/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_04.png b/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_04.png new file mode 100644 index 000000000..8682a0223 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/LK_buffer_mask/fig_04.png differ diff --git a/pysteps/tests/image_comparison_files/examples/advection_correction/fig_00.png b/pysteps/tests/image_comparison_files/examples/advection_correction/fig_00.png new file mode 100644 index 000000000..ef5b57c3c Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/advection_correction/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/anvil_nowcast/fig_00.png b/pysteps/tests/image_comparison_files/examples/anvil_nowcast/fig_00.png new file mode 100644 index 000000000..9fd59fc88 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/anvil_nowcast/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/data_transformations/fig_00.png b/pysteps/tests/image_comparison_files/examples/data_transformations/fig_00.png new file mode 100644 index 000000000..616ec6852 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/data_transformations/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/data_transformations/fig_01.png b/pysteps/tests/image_comparison_files/examples/data_transformations/fig_01.png new file mode 100644 index 000000000..a5c0ad149 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/data_transformations/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/ens_kalman_filter_blended_forecast/fig_00.png b/pysteps/tests/image_comparison_files/examples/ens_kalman_filter_blended_forecast/fig_00.png new file mode 100644 index 000000000..506463d06 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/ens_kalman_filter_blended_forecast/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/ens_kalman_filter_blended_forecast/fig_01.png b/pysteps/tests/image_comparison_files/examples/ens_kalman_filter_blended_forecast/fig_01.png new file mode 100644 index 000000000..6267e70d3 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/ens_kalman_filter_blended_forecast/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/linda_nowcasts/fig_00.png b/pysteps/tests/image_comparison_files/examples/linda_nowcasts/fig_00.png new file mode 100644 index 000000000..4a258b295 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/linda_nowcasts/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/linda_nowcasts/fig_01.png b/pysteps/tests/image_comparison_files/examples/linda_nowcasts/fig_01.png new file mode 100644 index 000000000..55745b666 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/linda_nowcasts/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/linda_nowcasts/fig_02.png b/pysteps/tests/image_comparison_files/examples/linda_nowcasts/fig_02.png new file mode 100644 index 000000000..a15201cfb Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/linda_nowcasts/fig_02.png differ diff --git a/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_00.png b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_00.png new file mode 100644 index 000000000..2dcad6521 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_01.png b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_01.png new file mode 100644 index 000000000..de761b13b Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_02.png b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_02.png new file mode 100644 index 000000000..d6bcfacef Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_02.png differ diff --git a/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_03.png b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_03.png new file mode 100644 index 000000000..268d6f762 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_03.png differ diff --git a/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_04.png b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_04.png new file mode 100644 index 000000000..d3134faab Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_04.png differ diff --git a/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_05.png b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_05.png new file mode 100644 index 000000000..45040ef5f Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_05.png differ diff --git a/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_06.png b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_06.png new file mode 100644 index 000000000..5e32509cf Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_06.png differ diff --git a/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_07.png b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_07.png new file mode 100644 index 000000000..45ff3f9fa Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_07.png differ diff --git a/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_08.png b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_08.png new file mode 100644 index 000000000..35dc2efa3 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_08.png differ diff --git a/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_09.png b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_09.png new file mode 100644 index 000000000..166dc4f84 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_09.png differ diff --git a/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_10.png b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_10.png new file mode 100644 index 000000000..e1791dfcb Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_10.png differ diff --git a/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_11.png b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_11.png new file mode 100644 index 000000000..e0d4aa05e Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_11.png differ diff --git a/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_12.png b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_12.png new file mode 100644 index 000000000..a17000042 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/optical_flow_methods_convergence/fig_12.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_cascade_decomposition/fig_00.png b/pysteps/tests/image_comparison_files/examples/plot_cascade_decomposition/fig_00.png new file mode 100644 index 000000000..c86e84174 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_cascade_decomposition/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_cascade_decomposition/fig_01.png b/pysteps/tests/image_comparison_files/examples/plot_cascade_decomposition/fig_01.png new file mode 100644 index 000000000..685585211 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_cascade_decomposition/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_cascade_decomposition/fig_02.png b/pysteps/tests/image_comparison_files/examples/plot_cascade_decomposition/fig_02.png new file mode 100644 index 000000000..d8bffecba Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_cascade_decomposition/fig_02.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_cascade_decomposition/fig_03.png b/pysteps/tests/image_comparison_files/examples/plot_cascade_decomposition/fig_03.png new file mode 100644 index 000000000..06a9b96d8 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_cascade_decomposition/fig_03.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_custom_precipitation_range/fig_00.png b/pysteps/tests/image_comparison_files/examples/plot_custom_precipitation_range/fig_00.png new file mode 100644 index 000000000..7294e9756 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_custom_precipitation_range/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_custom_precipitation_range/fig_01.png b/pysteps/tests/image_comparison_files/examples/plot_custom_precipitation_range/fig_01.png new file mode 100644 index 000000000..00ed4c446 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_custom_precipitation_range/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_00.png b/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_00.png new file mode 100644 index 000000000..a1554e7f5 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_01.png b/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_01.png new file mode 100644 index 000000000..150db7e4b Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_02.png b/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_02.png new file mode 100644 index 000000000..e597b9603 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_02.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_03.png b/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_03.png new file mode 100644 index 000000000..0bc746003 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_03.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_04.png b/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_04.png new file mode 100644 index 000000000..786537fc0 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_ensemble_verification/fig_04.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_extrapolation_nowcast/fig_00.png b/pysteps/tests/image_comparison_files/examples/plot_extrapolation_nowcast/fig_00.png new file mode 100644 index 000000000..c86e84174 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_extrapolation_nowcast/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_extrapolation_nowcast/fig_01.png b/pysteps/tests/image_comparison_files/examples/plot_extrapolation_nowcast/fig_01.png new file mode 100644 index 000000000..3480f91cc Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_extrapolation_nowcast/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_extrapolation_nowcast/fig_02.png b/pysteps/tests/image_comparison_files/examples/plot_extrapolation_nowcast/fig_02.png new file mode 100644 index 000000000..0d2cd345a Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_extrapolation_nowcast/fig_02.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_linear_blending/fig_00.png b/pysteps/tests/image_comparison_files/examples/plot_linear_blending/fig_00.png new file mode 100644 index 000000000..460ad0bd4 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_linear_blending/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_linear_blending/fig_01.png b/pysteps/tests/image_comparison_files/examples/plot_linear_blending/fig_01.png new file mode 100644 index 000000000..fc24f3781 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_linear_blending/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_noise_generators/fig_00.png b/pysteps/tests/image_comparison_files/examples/plot_noise_generators/fig_00.png new file mode 100644 index 000000000..dcd51985c Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_noise_generators/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_noise_generators/fig_01.png b/pysteps/tests/image_comparison_files/examples/plot_noise_generators/fig_01.png new file mode 100644 index 000000000..9029b26bb Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_noise_generators/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_noise_generators/fig_02.png b/pysteps/tests/image_comparison_files/examples/plot_noise_generators/fig_02.png new file mode 100644 index 000000000..3177702ac Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_noise_generators/fig_02.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_00.png b/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_00.png new file mode 100644 index 000000000..bae6dbfb6 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_01.png b/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_01.png new file mode 100644 index 000000000..bb4f9548c Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_02.png b/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_02.png new file mode 100644 index 000000000..57c157583 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_02.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_03.png b/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_03.png new file mode 100644 index 000000000..e784b1e2b Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_03.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_04.png b/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_04.png new file mode 100644 index 000000000..4eb7cd00a Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_optical_flow/fig_04.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_00.png b/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_00.png new file mode 100644 index 000000000..4c3ffc7e7 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_01.png b/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_01.png new file mode 100644 index 000000000..6abf27945 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_02.png b/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_02.png new file mode 100644 index 000000000..6137e612b Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_02.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_03.png b/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_03.png new file mode 100644 index 000000000..0eadc296e Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_03.png differ diff --git a/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_04.png b/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_04.png new file mode 100644 index 000000000..b1f16668a Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/plot_steps_nowcast/fig_04.png differ diff --git a/pysteps/tests/image_comparison_files/examples/probability_forecast/fig_00.png b/pysteps/tests/image_comparison_files/examples/probability_forecast/fig_00.png new file mode 100644 index 000000000..2c1c4e88b Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/probability_forecast/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/probability_forecast/fig_01.png b/pysteps/tests/image_comparison_files/examples/probability_forecast/fig_01.png new file mode 100644 index 000000000..d27c0ee5b Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/probability_forecast/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/probability_forecast/fig_02.png b/pysteps/tests/image_comparison_files/examples/probability_forecast/fig_02.png new file mode 100644 index 000000000..81d50db86 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/probability_forecast/fig_02.png differ diff --git a/pysteps/tests/image_comparison_files/examples/probability_forecast/fig_03.png b/pysteps/tests/image_comparison_files/examples/probability_forecast/fig_03.png new file mode 100644 index 000000000..716507d5b Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/probability_forecast/fig_03.png differ diff --git a/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_00.png b/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_00.png new file mode 100644 index 000000000..6f6101f8a Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_01.png b/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_01.png new file mode 100644 index 000000000..f2dc31b07 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_02.png b/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_02.png new file mode 100644 index 000000000..19e414292 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_02.png differ diff --git a/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_03.png b/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_03.png new file mode 100644 index 000000000..f69d78708 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_03.png differ diff --git a/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_04.png b/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_04.png new file mode 100644 index 000000000..cb61208e8 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_04.png differ diff --git a/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_05.png b/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_05.png new file mode 100644 index 000000000..eb9cc423d Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/rainfarm_downscale/fig_05.png differ diff --git a/pysteps/tests/image_comparison_files/examples/steps_blended_forecast/fig_00.png b/pysteps/tests/image_comparison_files/examples/steps_blended_forecast/fig_00.png new file mode 100644 index 000000000..5971a2b63 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/steps_blended_forecast/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/steps_blended_forecast/fig_01.png b/pysteps/tests/image_comparison_files/examples/steps_blended_forecast/fig_01.png new file mode 100644 index 000000000..289ffadd4 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/steps_blended_forecast/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/steps_blended_forecast/fig_02.png b/pysteps/tests/image_comparison_files/examples/steps_blended_forecast/fig_02.png new file mode 100644 index 000000000..6cb257dfd Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/steps_blended_forecast/fig_02.png differ diff --git a/pysteps/tests/image_comparison_files/examples/steps_blended_forecast/fig_03.png b/pysteps/tests/image_comparison_files/examples/steps_blended_forecast/fig_03.png new file mode 100644 index 000000000..e294c9f4d Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/steps_blended_forecast/fig_03.png differ diff --git a/pysteps/tests/image_comparison_files/examples/thunderstorm_detection_and_tracking/fig_00.png b/pysteps/tests/image_comparison_files/examples/thunderstorm_detection_and_tracking/fig_00.png new file mode 100644 index 000000000..54cbcf5d5 Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/thunderstorm_detection_and_tracking/fig_00.png differ diff --git a/pysteps/tests/image_comparison_files/examples/thunderstorm_detection_and_tracking/fig_01.png b/pysteps/tests/image_comparison_files/examples/thunderstorm_detection_and_tracking/fig_01.png new file mode 100644 index 000000000..235222efa Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/thunderstorm_detection_and_tracking/fig_01.png differ diff --git a/pysteps/tests/image_comparison_files/examples/thunderstorm_detection_and_tracking/fig_02.png b/pysteps/tests/image_comparison_files/examples/thunderstorm_detection_and_tracking/fig_02.png new file mode 100644 index 000000000..abd40248c Binary files /dev/null and b/pysteps/tests/image_comparison_files/examples/thunderstorm_detection_and_tracking/fig_02.png differ diff --git a/pysteps/tests/test_examples.py b/pysteps/tests/test_examples.py new file mode 100644 index 000000000..7e411401d --- /dev/null +++ b/pysteps/tests/test_examples.py @@ -0,0 +1,140 @@ +""" +Regression tests for the sphinx-gallery examples under ``examples/``. + +Each example script is executed as-is and every matplotlib figure it +produces is compared, pixel by pixel, against a reference PNG stored under +``image_comparison_files/examples//``. + +To (re)generate the baseline images after an intentional change to an +example's plots, run: + + PYSTEPS_UPDATE_BASELINE_IMAGES=1 pytest pysteps/tests/test_examples.py + +and commit the updated PNGs. +""" + +import os +import runpy +import shutil +import warnings +from pathlib import Path + +import matplotlib + +matplotlib.use("Agg") + +import matplotlib.pyplot as plt +import pytest +from matplotlib.testing.compare import compare_images + + +def _resolve_dir(default: Path, repo_relative: str) -> Path: + """Resolve a directory that lives in the source checkout but is not + shipped with the installed package (e.g. in CI, where pysteps is + installed with ``pip install .`` rather than in editable mode). + + Falls back to ``$PYSTEPS_REPO_ROOT / repo_relative`` when the + package-relative ``default`` path doesn't exist on disk. + """ + if default.is_dir(): + return default + repo_root = os.environ.get("PYSTEPS_REPO_ROOT") + if repo_root: + candidate = Path(repo_root) / repo_relative + if candidate.is_dir(): + return candidate + return default + + +EXAMPLES_DIR = _resolve_dir( + Path(__file__).resolve().parents[2] / "examples", "examples" +) +BASELINE_DIR = _resolve_dir( + Path(__file__).resolve().parent / "image_comparison_files" / "examples", + "pysteps/tests/image_comparison_files/examples", +) + +# RMS pixel value tolerance (0-255 scale) passed to +# matplotlib.testing.compare.compare_images. 0 requires an exact pixel match. +IMAGE_COMPARISON_TOLERANCE = 1.0 + +UPDATE_BASELINE_IMAGES = bool(os.environ.get("PYSTEPS_UPDATE_BASELINE_IMAGES")) + +EXAMPLE_SCRIPTS = [ + "advection_correction.py", + "anvil_nowcast.py", + "data_transformations.py", + "ens_kalman_filter_blended_forecast.py", + "linda_nowcasts.py", + "LK_buffer_mask.py", + "optical_flow_methods_convergence.py", + "plot_cascade_decomposition.py", + "plot_custom_precipitation_range.py", + "plot_ensemble_verification.py", + "plot_extrapolation_nowcast.py", + "plot_linear_blending.py", + "plot_noise_generators.py", + "plot_optical_flow.py", + "plot_steps_nowcast.py", + "probability_forecast.py", + "rainfarm_downscale.py", + "steps_blended_forecast.py", + "thunderstorm_detection_and_tracking.py", +] + + +def _run_example(script_name, tmp_path, monkeypatch): + """Execute an example script and return the list of figures it created.""" + i = [0] + + def show_to_fig(): + plt.savefig(tmp_path / f"fig_{i[0]:02d}.png") + plt.close("all") + i[0] += 1 + + monkeypatch.setattr(plt, "show", show_to_fig) + + plt.close("all") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + runpy.run_path(str(EXAMPLES_DIR / script_name), run_name="__main__") + + +@pytest.mark.parametrize("script_name", EXAMPLE_SCRIPTS) +def test_example_plots(script_name, tmp_path, monkeypatch): + example_name = Path(script_name).stem + baseline_dir = BASELINE_DIR / example_name + + try: + if UPDATE_BASELINE_IMAGES: + baseline_dir.mkdir(parents=True, exist_ok=True) + for old_baseline in baseline_dir.glob("fig_*.png"): + old_baseline.unlink() + + _run_example(script_name, tmp_path, monkeypatch) + + if UPDATE_BASELINE_IMAGES: + for file in sorted(tmp_path.glob("fig_*.png")): + shutil.copy(file, baseline_dir / file.name) + return + + baseline_images = sorted(baseline_dir.glob("fig_*.png")) + assert baseline_images, ( + f"No baseline images found for {script_name} in {baseline_dir}. " + "Generate them with PYSTEPS_UPDATE_BASELINE_IMAGES=1 pytest ..." + ) + actual_images = sorted(tmp_path.glob("fig_*.png")) + assert len(actual_images) == len(baseline_images), ( + f"{script_name} produced {len(actual_images)} figure(s) but " + f"{len(baseline_images)} baseline image(s) exist in {baseline_dir}" + ) + + for image in actual_images: + actual_path = image + baseline_path = baseline_dir / image.name + result = compare_images( + str(baseline_path), str(actual_path), tol=IMAGE_COMPARISON_TOLERANCE + ) + assert result is None, result + finally: + plt.close("all")