diff --git a/cset-workflow/app/run_cset_recipe/bin/run-cset-recipe.py b/cset-workflow/app/run_cset_recipe/bin/run-cset-recipe.py index 525727788..4091fb3f5 100755 --- a/cset-workflow/app/run_cset_recipe/bin/run-cset-recipe.py +++ b/cset-workflow/app/run_cset_recipe/bin/run-cset-recipe.py @@ -159,6 +159,7 @@ def parallel(): f"--input-dir={data_directory()}", f"--output-dir={output_directory()}", f"--style-file={os.getenv('COLORBAR_FILE', '')}", + f"--plot-resolution={os.getenv('PLOT_RESOLUTION', '')}", "--parallel-only", ), check=True, @@ -188,6 +189,7 @@ def collate(): f"--recipe={recipe_file()}", f"--output-dir={output_directory()}", f"--style-file={os.getenv('COLORBAR_FILE', '')}", + f"--plot-resolution={os.getenv('PLOT_RESOLUTION', '')}", "--collate-only", ), check=True, diff --git a/cset-workflow/flow.cylc b/cset-workflow/flow.cylc index 3f9a866de..d47328ec0 100644 --- a/cset-workflow/flow.cylc +++ b/cset-workflow/flow.cylc @@ -72,6 +72,7 @@ URL = https://metoffice.github.io/CSET LOGLEVEL = {{LOGLEVEL}} WEB_DIR = {{WEB_DIR}} COLORBAR_FILE = {{COLORBAR_FILE}} + PLOT_RESOLUTION = {{PLOT_RESOLUTION}} [[PARALLEL]] script = rose task-run -v --app-key=run_cset_recipe diff --git a/cset-workflow/meta/rose-meta.conf b/cset-workflow/meta/rose-meta.conf index 1fc6c0b9e..c9ed487c8 100644 --- a/cset-workflow/meta/rose-meta.conf +++ b/cset-workflow/meta/rose-meta.conf @@ -171,6 +171,16 @@ help= type=quoted compulsory=true +[template variables=PLOT_RESOLUTION] +ns=General +description=Resolution of output plot in dpi. +help=This is passed through to the plotting operators and sets the resolution + of the output plots to the given number of pixels per inch. If unset + defaults to 100 dpi. The plots are all 8 by 8 inches, so this corresponds + to 800 by 800 pixels. +type=integer +compulsory=true + [template variables=WEB_DIR] ns=General description=Path to directory that is served by the webserver. diff --git a/src/CSET/__init__.py b/src/CSET/__init__.py index 90e256ae2..92c75fc19 100644 --- a/src/CSET/__init__.py +++ b/src/CSET/__init__.py @@ -76,6 +76,9 @@ def main(): parser_bake.add_argument( "-s", "--style-file", type=Path, help="colour bar definition to use" ) + parser_bake.add_argument( + "--plot-resolution", type=int, help="plotting resolution in dpi" + ) parser_bake.set_defaults(func=_bake_command) parser_graph = subparsers.add_parser("graph", help="visualise a recipe file") @@ -207,10 +210,15 @@ def _bake_command(args, unparsed_args): args.output_dir, recipe_variables, args.style_file, + args.plot_resolution, ) if not args.parallel_only: execute_recipe_collate( - args.recipe, args.output_dir, recipe_variables, args.style_file + args.recipe, + args.output_dir, + recipe_variables, + args.style_file, + args.plot_resolution, ) diff --git a/src/CSET/operators/__init__.py b/src/CSET/operators/__init__.py index cd473f176..8101457dc 100644 --- a/src/CSET/operators/__init__.py +++ b/src/CSET/operators/__init__.py @@ -142,7 +142,14 @@ def _step_parser(step: dict, step_input: any) -> str: return operator(**kwargs) -def _run_steps(recipe, steps, step_input, output_directory: Path, style_file: Path): +def _run_steps( + recipe, + steps, + step_input, + output_directory: Path, + style_file: Path = None, + plot_resolution: int = None, +) -> None: """Execute the steps in a recipe.""" original_working_directory = Path.cwd() os.chdir(output_directory) @@ -159,6 +166,8 @@ def _run_steps(recipe, steps, step_input, output_directory: Path, style_file: Pa # Create metadata file used by some steps. if style_file: recipe["style_file_path"] = str(style_file) + if plot_resolution: + recipe["plot_resolution"] = plot_resolution _write_metadata(recipe) # Execute the recipe. for step in steps: @@ -174,6 +183,7 @@ def execute_recipe_parallel( output_directory: Path, recipe_variables: dict = None, style_file: Path = None, + plot_resolution: int = None, ) -> None: """Parse and executes the parallel steps from a recipe file. @@ -188,8 +198,12 @@ def execute_recipe_parallel( input. output_directory: Path Pathlike indicating desired location of output. - recipe_variables: dict + recipe_variables: dict, optional Dictionary of variables for the recipe. + style_file: Path, optional + Path to a style file. + plot_resolution: int, optional + Resolution of plots in dpi. Raises ------ @@ -213,7 +227,7 @@ def execute_recipe_parallel( logging.error("Output directory is a file. %s", output_directory) raise err steps = recipe["parallel"] - _run_steps(recipe, steps, step_input, output_directory, style_file) + _run_steps(recipe, steps, step_input, output_directory, style_file, plot_resolution) def execute_recipe_collate( @@ -221,6 +235,7 @@ def execute_recipe_collate( output_directory: Path, recipe_variables: dict = None, style_file: Path = None, + plot_resolution: int = None, ) -> None: """Parse and execute the collation steps from a recipe file. @@ -234,6 +249,10 @@ def execute_recipe_collate( Pathlike indicating desired location of output. Must already exist. recipe_variables: dict Dictionary of variables for the recipe. + style_file: Path, optional + Path to a style file. + plot_resolution: int, optional + Resolution of plots in dpi. Raises ------ @@ -249,4 +268,6 @@ def execute_recipe_collate( recipe = parse_recipe(recipe_yaml, recipe_variables) # If collate doesn't exist treat it as having no steps. steps = recipe.get("collate", []) - _run_steps(recipe, steps, output_directory, output_directory, style_file) + _run_steps( + recipe, steps, output_directory, output_directory, style_file, plot_resolution + ) diff --git a/src/CSET/operators/plot.py b/src/CSET/operators/plot.py index f25d2f7b1..b96d8a18e 100644 --- a/src/CSET/operators/plot.py +++ b/src/CSET/operators/plot.py @@ -181,6 +181,11 @@ def _colorbar_map_levels(varname: str, **kwargs): return cmap, levels, norm +def _get_plot_resolution() -> int: + """Get resolution of rasterised plots in pixels per inch.""" + return get_recipe_metadata().get("plot_resolution", 100) + + def _plot_and_save_contour_plot( cube: iris.cube.Cube, filename: str, @@ -200,7 +205,7 @@ def _plot_and_save_contour_plot( """ # Setup plot details, size, resolution, etc. - fig = plt.figure(figsize=(15, 15), facecolor="w", edgecolor="k") + fig = plt.figure(figsize=(8, 8), facecolor="w", edgecolor="k") # Specify the color bar cmap, levels, norm = _colorbar_map_levels(cube.name()) @@ -262,7 +267,7 @@ def _plot_and_save_contour_plot( cbar.set_label(label=f"{cube.name()} ({cube.units})", size=20) # Save plot. - fig.savefig(filename, bbox_inches="tight", dpi=150) + fig.savefig(filename, bbox_inches="tight", dpi=_get_plot_resolution()) logging.info("Saved contour plot to %s", filename) plt.close(fig) @@ -293,7 +298,7 @@ def _plot_and_save_postage_stamp_contour_plot( # Use the smallest square grid that will fit the members. grid_size = int(math.ceil(math.sqrt(len(cube.coord(stamp_coordinate).points)))) - fig = plt.figure(figsize=(10, 10)) + fig = plt.figure(figsize=(8, 8)) # Specify the color bar cmap, levels, norm = _colorbar_map_levels(cube.name()) @@ -325,7 +330,7 @@ def _plot_and_save_postage_stamp_contour_plot( # Overall figure title. fig.suptitle(title) - fig.savefig(filename, bbox_inches="tight", dpi=150) + fig.savefig(filename, bbox_inches="tight", dpi=_get_plot_resolution()) logging.info("Saved contour postage stamp plot to %s", filename) plt.close(fig) @@ -361,7 +366,7 @@ def _plot_and_save_line_series( ax.autoscale() # Save plot. - fig.savefig(filename, bbox_inches="tight", dpi=150) + fig.savefig(filename, bbox_inches="tight", dpi=_get_plot_resolution()) logging.info("Saved line plot to %s", filename) plt.close(fig) @@ -450,7 +455,7 @@ def _plot_and_save_vertical_line_series( ax.autoscale() # Save plot. - fig.savefig(filename, bbox_inches="tight", dpi=150) + fig.savefig(filename, bbox_inches="tight", dpi=_get_plot_resolution()) logging.info("Saved line plot to %s", filename) plt.close(fig) @@ -506,7 +511,7 @@ def _plot_and_save_scatter_plot( ax.autoscale() # Save plot. - fig.savefig(filename, bbox_inches="tight", dpi=150) + fig.savefig(filename, bbox_inches="tight", dpi=_get_plot_resolution()) logging.info("Saved scatter plot to %s", filename) plt.close(fig) @@ -565,7 +570,7 @@ def _plot_and_save_histogram_series( ) # Save plot. - fig.savefig(filename, bbox_inches="tight", dpi=150) + fig.savefig(filename, bbox_inches="tight", dpi=_get_plot_resolution()) logging.info("Saved line plot to %s", filename) plt.close(fig) @@ -614,7 +619,7 @@ def _plot_and_save_postage_stamp_histogram_series( # Use the smallest square grid that will fit the members. grid_size = int(math.ceil(math.sqrt(len(cube.coord(stamp_coordinate).points)))) - fig = plt.figure(figsize=(10, 10), facecolor="w", edgecolor="k") + fig = plt.figure(figsize=(8, 8), facecolor="w", edgecolor="k") # Make a subplot for each member. for member, subplot in zip( cube.slices_over(stamp_coordinate), range(1, grid_size**2 + 1), strict=False @@ -633,7 +638,7 @@ def _plot_and_save_postage_stamp_histogram_series( # Overall figure title. fig.suptitle(title) - fig.savefig(filename, bbox_inches="tight", dpi=150) + fig.savefig(filename, bbox_inches="tight", dpi=_get_plot_resolution()) logging.info("Saved histogram postage stamp plot to %s", filename) plt.close(fig) @@ -648,7 +653,7 @@ def _plot_and_save_postage_stamps_in_single_plot_histogram_series( histtype: str = "step", **kwargs, ): - fig, ax = plt.subplots(figsize=(10, 10), facecolor="w", edgecolor="k") + fig, ax = plt.subplots(figsize=(8, 8), facecolor="w", edgecolor="k") ax.set_title(title) ax.set_xlim(vmin, vmax) ax.set_xlabel(f"{cube.name()} / {cube.units}") @@ -670,7 +675,7 @@ def _plot_and_save_postage_stamps_in_single_plot_histogram_series( ax.legend() # Save the figure to a file - plt.savefig(filename) + plt.savefig(filename, bbox_inches="tight", dpi=_get_plot_resolution()) # Close the figure plt.close(fig) diff --git a/tests/operators/test_plots.py b/tests/operators/test_plots.py index 386cb8e88..a84e10a03 100644 --- a/tests/operators/test_plots.py +++ b/tests/operators/test_plots.py @@ -255,3 +255,17 @@ def test_scatter_plot_too_many_y_dimensions( cube_x = collapse.collapse(vertical_profile_cube, ["time"], "MEAN")[0:4] with pytest.raises(ValueError): plot.scatter_plot(cube_x, cube_y) + + +def test_get_plot_resolution(tmp_working_dir): + """Test getting the plot resolution.""" + with open("meta.json", "wt", encoding="UTF-8") as fp: + fp.write('{"plot_resolution": 72}') + resolution = plot._get_plot_resolution() + assert resolution == 72 + + +def test_get_plot_resolution_unset(tmp_working_dir): + """Test getting the default plot resolution when unset.""" + resolution = plot._get_plot_resolution() + assert resolution == 100 diff --git a/tests/test_run_recipes.py b/tests/test_run_recipes.py index e7a49564a..f395e0e62 100644 --- a/tests/test_run_recipes.py +++ b/tests/test_run_recipes.py @@ -94,3 +94,11 @@ def test_run_steps_style_file_metadata_written(tmp_path: Path): with open(tmp_path / "meta.json", "rb") as fp: metadata = json.load(fp) assert metadata["style_file_path"] == style_file_path + + +def test_run_steps_plot_resolution_metadata_written(tmp_path: Path): + """Style file path metadata written out.""" + CSET.operators._run_steps({}, [], None, tmp_path, plot_resolution=72) + with open(tmp_path / "meta.json", "rb") as fp: + metadata = json.load(fp) + assert metadata["plot_resolution"] == 72