diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..45dd9ef6a4 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = baybe/utils/plotting.py \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a397314067..644a0e6597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Subpackages for the available recommender types +- Multi-style plotting capabilities for generated example plots +- JSON file for plotting themes +- Smoke testing in relevant tox environments ### Changed - `Recommender`s now share their core logic via their base class diff --git a/baybe/utils/plotting.py b/baybe/utils/plotting.py new file mode 100644 index 0000000000..5c6e72035a --- /dev/null +++ b/baybe/utils/plotting.py @@ -0,0 +1,134 @@ +"""Plotting utilities.""" + +import json +import os +import sys +import warnings +from pathlib import Path +from typing import Any, Dict, Tuple + +import matplotlib.pyplot as plt +from matplotlib.axes import Axes +from matplotlib.figure import Figure + + +def create_example_plots( + ax: Axes, + path: Path, + base_name: str, +) -> None: + """Create plots from an Axes object and save them as a svg file. + + The plots will be saved in the location specified by ``path``. + The attribute ``base_name`` is used to define the name of the outputs. + + If the ``SMOKE_TEST`` variable is set, no plots are being created and this method + immediately returns. + + The function attempts to read the predefined themes from ``plotting_themes.json``. + For each theme it finds, a file ``{base_name}_{theme}.svg`` is being created. + If the file cannot be found, if the JSON cannot be loaded or if the JSON is not well + configured, a fallback theme is used. + + Args: + ax: The Axes object containing the figure that should be plotted. + path: The path to the directory in which the plots should be saved. + base_name: The base name that is used for naming the output files. + """ + # Check whether we immediately return due to just running a SMOKE_TEST + if "SMOKE_TEST" in os.environ: + return + + # Define a fallback theme in case no configuration is found + fallback: Dict[str, Any] = { + "color": "black", + "figsize": (24, 8), + "fontsize": 22, + "framealpha": 0.3, + } + + # Try to find the plotting themes by backtracking + # Get the absolute path of the current script + script_path = Path(sys.path[0]).resolve() + while ( + not Path(script_path / "plotting_themes.json").is_file() + and script_path != script_path.parent + ): + script_path = script_path.parent + if script_path == script_path.parent: + warnings.warn("No themes for plotting found. A fallback theme is used.") + themes = {"fallback": fallback} + else: + # Open the file containing all the themes + # If we reach this point, we know that the file exists, so we try to load it. + # If the file is no proper json, the fallback theme is used. + try: + themes = json.load(open(script_path / "plotting_themes.json")) + except json.JSONDecodeError: + warnings.warn( + "The JSON containing the themes could not be loaded." + "A fallback theme is used.", + UserWarning, + ) + themes = {"fallback": fallback} + + for theme_name in themes: + # Get all of the values from the themes + # TODO This can probably be generalized and improved later on such that the + # keys fit the rc_params of matplotlib + # TODO We might want to add a generalization here + necessary_keys = ("color", "figsize", "fontsize", "framealpha") + if not all(key in themes[theme_name] for key in necessary_keys): + warnings.warn( + "Provided theme does not contain the necessary keys." + "Using a fallback theme instead.", + UserWarning, + ) + current_theme = fallback + else: + current_theme = themes[theme_name] + color: str = current_theme["color"] + figsize: Tuple[int, int] = current_theme["figsize"] + fontsize: int = current_theme["fontsize"] + framealpha: float = current_theme["framealpha"] + + # Adjust the axes of the plot + for key in ax.spines.keys(): + ax.spines[key].set_color(color) + ax.xaxis.label.set_color(color) + ax.xaxis.label.set_fontsize(fontsize) + ax.yaxis.label.set_color(color) + ax.yaxis.label.set_fontsize(fontsize) + + # Adjust the size of the ax + # mypy thinks that ax.figure might become None, hence the explicit ignore + if isinstance(ax.figure, Figure): + ax.figure.set_size_inches(*figsize) + else: + warnings.warn("Could not adjust size of plot due to it not being a Figure.") + + # Adjust the labels + for label in ax.get_xticklabels() + ax.get_yticklabels(): + label.set_color(color) + label.set_fontsize(fontsize) + + # Adjust the legend + legend = ax.get_legend() + legend.get_frame().set_alpha(framealpha) + legend.get_title().set_color(color) + legend.get_title().set_fontsize(fontsize) + for text in legend.get_texts(): + text.set_fontsize(fontsize) + text.set_color(color) + + output_path = Path(path, f"{base_name}_{theme_name}.svg") + # mypy thinks that ax.figure might become None, hence the explicit ignore + if isinstance(ax.figure, Figure): + ax.figure.savefig( + output_path, + format="svg", + transparent=True, + ) + else: + warnings.warn("Plots could not be saved.") + plt.close() diff --git a/docs/scripts/utils.py b/docs/scripts/utils.py index aa54034ef2..83e8dd18ca 100644 --- a/docs/scripts/utils.py +++ b/docs/scripts/utils.py @@ -110,6 +110,7 @@ def create_example_documentation(example_dest_dir: str, ignore_examples: bool): # Include the name of the file to the toctree # Format it by replacing underscores and capitalizing the words file_name = file.stem + formatted = " ".join(word.capitalize() for word in file_name.split("_")) # Remove duplicate "constraints" for the files in the constraints folder. if "Constraints" in folder_name and "Constraints" in formatted: @@ -162,7 +163,24 @@ def create_example_documentation(example_dest_dir: str, ignore_examples: bool): with open(markdown_path, "r", encoding="UTF-8") as markdown_file: lines = markdown_file.readlines() + # Delete lines we do not want to have in our documentation + lines = [line for line in lines if "![svg]" not in line] lines = [line for line in lines if "![png]" not in line] + lines = [line for line in lines if "
+ + + + + + + 2024-02-16T10:29:16.552511 + image/svg+xml + + + Matplotlib v3.8.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/Backtesting/botorch_analytical_light.svg b/examples/Backtesting/botorch_analytical_light.svg new file mode 100644 index 0000000000..63e04a5175 --- /dev/null +++ b/examples/Backtesting/botorch_analytical_light.svg @@ -0,0 +1,1328 @@ + + + + + + + + 2024-02-16T10:29:16.569568 + image/svg+xml + + + Matplotlib v3.8.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/plotting_themes.json b/examples/plotting_themes.json new file mode 100644 index 0000000000..1dece5ee4b --- /dev/null +++ b/examples/plotting_themes.json @@ -0,0 +1,20 @@ +{ + "dark": { + "color": "white", + "figsize": [ + 24, + 8 + ], + "fontsize": 22, + "framealpha": 0.3 + }, + "light": { + "color": "black", + "figsize": [ + 24, + 8 + ], + "fontsize": 22, + "framealpha": 0.3 + } +} \ No newline at end of file diff --git a/mypy.ini b/mypy.ini index 1972c91542..ef9147e251 100644 --- a/mypy.ini +++ b/mypy.ini @@ -40,4 +40,4 @@ ignore_missing_imports = True ignore_missing_imports = True [mypy-mordred] -ignore_missing_imports = True +ignore_missing_imports = True \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8dde74272a..fdb2ae868b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,7 +122,8 @@ mypy = [ "mypy>=1.6.1", "pandas-stubs>=2.0.3.230814", "funcy-stubs>=0.1.1", - "types-requests>=2.31.0.20240106" + "types-requests>=2.31.0.20240106", + "types-seaborn>=0.12.2" ] simulation = [ diff --git a/tox.ini b/tox.ini index 3f21262723..beb59affbf 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,8 @@ isolated_build = True description = Run PyTest with all extra functionality extras = test,chem,examples,simulation,onnx passenv = CI +setenv = + SMOKE_TEST = true commands = python --version pytest -p no:warnings --cov=baybe --durations=5 {posargs} @@ -15,6 +17,8 @@ commands = description = Run PyTest with core functionality extras = test passenv = CI +setenv = + SMOKE_TEST = true commands = python --version pytest -p no:warnings --cov=baybe --durations=5 {posargs} @@ -51,6 +55,8 @@ commands = description = Build documentation extras = docs passenv = BAYBE_DOCS_LINKCHECK_IGNORE +setenv = + SMOKE_TEST = true commands = python --version python docs/scripts/convert_code_to_documentation.py {posargs} \ No newline at end of file