From fd5598679084f836e7ce7d14f5981268c377041c Mon Sep 17 00:00:00 2001 From: MLopez-Ibanez <2620021+MLopez-Ibanez@users.noreply.github.com> Date: Sun, 8 Sep 2024 12:44:07 +0100 Subject: [PATCH] python/ Convert examples into sphinx galleries. --- python/.gitignore | 2 + python/Makefile | 9 ++-- python/doc/Makefile | 4 ++ python/doc/source/conf.py | 64 +++++++++++++++++++++++++-- python/doc/source/examples/index.rst | 8 ---- python/doc/source/examples/metrics.md | 16 ------- python/doc/source/index.rst | 4 +- python/examples/README.rst | 8 ++++ python/examples/plot_hv_approx.py | 60 +++++++++++++++++++++++++ python/examples/plot_metrics.py | 41 +++++++++++++++++ python/pyproject.toml | 8 ++++ python/requirements_dev.txt | 12 +++-- python/src/moocore/_moocore.py | 3 ++ 13 files changed, 203 insertions(+), 36 deletions(-) delete mode 100644 python/doc/source/examples/index.rst delete mode 100644 python/doc/source/examples/metrics.md create mode 100644 python/examples/README.rst create mode 100644 python/examples/plot_hv_approx.py create mode 100644 python/examples/plot_metrics.py diff --git a/python/.gitignore b/python/.gitignore index e40fb287..7c9af9f4 100644 --- a/python/.gitignore +++ b/python/.gitignore @@ -77,6 +77,8 @@ instance/ doc/_build/ doc/jupyter_execute/ doc/source/reference/generated/ +doc/source/auto_examples +doc/source/sg_execution_times.rst # PyBuilder .pybuilder/ diff --git a/python/Makefile b/python/Makefile index 78759bfa..eefed447 100644 --- a/python/Makefile +++ b/python/Makefile @@ -1,6 +1,6 @@ # This file should be almost identical to # https://github.com/multi-objective/mooplot/blob/main/python/Makefile -.PHONY : install build test doc clean docdeps pre-commit +.PHONY : install build test docs fastdocs clean docdeps pre-commit install: build python3 -m pip install -e . --disable-pip-version-check @@ -21,12 +21,15 @@ docdeps: show: $(MAKE) -C doc show -doc: +docs: $(MAKE) -C doc clean html +fastdocs: + $(MAKE) -C doc clean html-noplot + clean: $(MAKE) -C doc clean $(MAKE) -C src/moocore/libmoocore/ clean find . -name '__pycache__' | xargs $(RM) -r - $(RM) -rf .pytest_cache .tox build src/*.egg-info/ doc/source/reference/generated + $(RM) -rf .pytest_cache .tox build src/*.egg-info/ doc/source/reference/generated doc/source/auto_examples $(RM) -f .coverage coverage.xml c_coverage.xml dist/* diff --git a/python/doc/Makefile b/python/doc/Makefile index a06d7ad8..e7dc88c7 100644 --- a/python/doc/Makefile +++ b/python/doc/Makefile @@ -24,6 +24,10 @@ help: show: @python3 -c "import webbrowser; webbrowser.open_new_tab('file://$(root_dir)/$(BUILDDIR)/html/index.html')" +html-noplot: + @$(SPHINXBUILD) -D plot_gallery=0 -b html $(ALLSPHINXOPTS) "$(SOURCEDIR)" "$(BUILDDIR)/html" + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile diff --git a/python/doc/source/conf.py b/python/doc/source/conf.py index daaef7de..7c9fcf4e 100644 --- a/python/doc/source/conf.py +++ b/python/doc/source/conf.py @@ -12,11 +12,20 @@ import sphinx import moocore +# Set plotly renderer to capture _repr_html_ for sphinx-gallery +try: + import plotly.io +except ImportError: + pass +else: + plotly.io.renderers.default = "sphinx_gallery" + project = "moocore" _full_version = moocore.__version__ release = _full_version # _full_version.split("+", 1)[0] version = _full_version # ".".join(release.split(".")[:2]) year = date.today().year +# Can we get this from pyproject.toml ? author = "Manuel López-Ibáñez and Fergus Rooney" copyright = f"2024-{year}, {author}" html_site_root = f"https://multi-objective.github.io/{project}/python/" @@ -40,9 +49,10 @@ "sphinx.ext.autosummary", # Create neat summary tables for modules/classes/methods etc "sphinx_copybutton", # A small sphinx extension to add a "copy" button to code blocks. "sphinx.ext.mathjax", - "myst_nb", - "sphinx.ext.autosectionlabel", + # "sphinx.ext.autosectionlabel", DO NOT USE: causes duplicated labels. "sphinxcontrib.bibtex", + "sphinx_gallery.gen_gallery", + "matplotlib.sphinxext.plot_directive", ] # ----------------------------------------------------------------------------- @@ -122,6 +132,13 @@ def setup(app): # Add light/dark mode and documentation version switcher: # "navbar_end": ["theme-switcher", "version-switcher", "navbar-icon-links"], "navbar_end": ["theme-switcher", "navbar-icon-links"], + "icon_links": [ + { + "name": "PyPI", + "url": f"https://pypi.org/project/{project}", + "icon": "fa-solid fa-box", + }, + ], # "icon_links": [ # { # # Label for this link @@ -155,7 +172,7 @@ def setup(app): templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path . +# This pattern also affects html_static_path and html_extra_path. exclude_patterns = [ "_build", "Thumbs.db", @@ -164,6 +181,10 @@ def setup(app): "_templates", "modules.rst", "source", + # Exclude .py and .ipynb files in auto_examples generated by sphinx-gallery. + # This is to prevent sphinx from complaining about duplicate source files. + "auto_examples/*.ipynb", + "auto_examples/*.py", ] suppress_warnings = ["mystnb.unknown_mime_type"] @@ -190,3 +211,40 @@ def setup(app): "sympy": ("https://docs.sympy.org/latest/", None), "mooplot": ("https://multi-objective.github.io/mooplot/python/", None), } + +# From https://github.com/scikit-learn/scikit-learn/blob/main/doc/conf.py +sphinx_gallery_conf = { + "examples_dirs": "../../examples", + "gallery_dirs": "auto_examples", + # Directory where function/class granular galleries are stored. + "backreferences_dir": "reference/generated/backreferences", + # Modules for which function/class level galleries are created. + "doc_module": (project), + # Regexes to match objects to exclude from implicit backreferences. + # The default option is an empty set, i.e. exclude nothing. + # To exclude everything, use: '.*' + "exclude_implicit_doc": {r"pyplot\.show"}, + "show_memory": False, + # "reference_url": {"mooplot": None}, + # "subsection_order": SubSectionTitleOrder("../examples"), + # "within_subsection_order": SKExampleTitleSortKey, + # "binder": { + # "org": "scikit-learn", + # "repo": "scikit-learn", + # "binderhub_url": "https://mybinder.org", + # "branch": binder_branch, + # "dependencies": "./binder/requirements.txt", + # "use_jupyter_lab": True, + # }, + # avoid generating too many cross links + "inspect_global_variables": False, + "remove_config_comments": True, + "matplotlib_animations": True, + # "plot_gallery": "True", + # "recommender": {"enable": True, "n_examples": 5, "min_df": 12}, + # "reset_modules": ("matplotlib", "seaborn"), +} +# if with_jupyterlite: +# sphinx_gallery_conf["jupyterlite"] = { +# "notebook_modification_function": notebook_modification_function +# } diff --git a/python/doc/source/examples/index.rst b/python/doc/source/examples/index.rst deleted file mode 100644 index 2a15cd92..00000000 --- a/python/doc/source/examples/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Examples -======== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - metrics.md diff --git a/python/doc/source/examples/metrics.md b/python/doc/source/examples/metrics.md deleted file mode 100644 index 9b7cf247..00000000 --- a/python/doc/source/examples/metrics.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -file_format: mystnb -kernelspec: - name: python3 ---- - -# Computing unary quality metrics -## Read data sets - -```{code-cell} -import moocore as moo -spherical = moo.read_datasets(moo.get_dataset_path("spherical-250-10-3d.txt")) -uniform = moo.read_datasets(moo.get_dataset_path("uniform-250-10-3d.txt")) -print(spherical.shape) -print(uniform.shape) -``` diff --git a/python/doc/source/index.rst b/python/doc/source/index.rst index 7fd97a18..2a3d5908 100644 --- a/python/doc/source/index.rst +++ b/python/doc/source/index.rst @@ -9,7 +9,7 @@ moocore: Core Algorithms for Multi-Objective Optimization :hidden: API reference - Examples + Examples **Version**: |version| @@ -53,7 +53,7 @@ performance measures, performance assessment :img-top: _static/index_getting_started.svg :class-card: intro-card :shadow: md - :link: examples + :link: auto_examples :link-type: ref Detailed examples and tutorials. diff --git a/python/examples/README.rst b/python/examples/README.rst new file mode 100644 index 00000000..60c4628c --- /dev/null +++ b/python/examples/README.rst @@ -0,0 +1,8 @@ +.. _auto_examples: + +Examples +======== + +These are longer and more detailed examples than those accompanying the +documentation of each function. These examples may require additional packages +to run. diff --git a/python/examples/plot_hv_approx.py b/python/examples/plot_hv_approx.py new file mode 100644 index 00000000..d7f8d374 --- /dev/null +++ b/python/examples/plot_hv_approx.py @@ -0,0 +1,60 @@ +r"""Comparing methods for approximating the hypervolume +=================================================== + +This example shows how to approximate the hypervolume metric of the ``CPFs.txt`` dataset using both HypE, :func:`moocore.whv_hype()`, and DZ2019, :func:`moocore.hv_approx()` for several +values of the number of samples between :math:`10^1` and :math:`10^5`. We repeat each +calculation 10 times to account for stochasticity. +""" + +import numpy as np +import moocore + +ref = 2.1 +x = moocore.get_dataset("CPFs.txt")[:, :-1] +x = moocore.filter_dominated(x) +x = moocore.normalise(x, to_range=[1, 2]) +true_hv = moocore.hypervolume(x, ref=ref) +rng1 = np.random.default_rng(42) +rng2 = np.random.default_rng(42) + +hype = {} +dz = {} +for i in range(1, 6): + hype[i] = [] + dz[i] = [] + for r in range(15): + res = moocore.whv_hype(x, ref=ref, ideal=0, nsamples=10**i, seed=rng1) + hype[i].append(res) + res = moocore.hv_approx(x, ref=ref, nsamples=10**i, seed=rng2) + dz[i].append(res) +print( + f"True HV : {true_hv:.5f}", + f"Mean HYPE : {np.mean(hype[5]):.5f} [{np.min(hype[5]):.5f}, {np.max(hype[5]):.5f}]", + f"Mean DZ2019: {np.mean(dz[5]):.5f} [{np.min(dz[5]):.5f}, {np.max(dz[5]):.5f}]", + sep="\n", +) + + +# %% +# Next, we plot the results. + +import pandas as pd + +hype = pd.DataFrame(hype) +dz = pd.DataFrame(dz) +hype["Method"] = "HypE" +dz["Method"] = "DZ2019" +df = ( + pd.concat([hype, dz]) + .reset_index(names="rep") + .melt(id_vars=["rep", "Method"], var_name="samples") +) +df["samples"] = 10 ** df["samples"] +df["value"] = np.abs(df["value"] - true_hv) / true_hv + +import matplotlib.pyplot as plt +import seaborn as sns + +ax = sns.lineplot(x="samples", y="value", hue="Method", data=df, marker="o") +ax.set(xscale="log", yscale="log", ylabel="Relative error") +plt.show() diff --git a/python/examples/plot_metrics.py b/python/examples/plot_metrics.py new file mode 100644 index 00000000..dd877dc8 --- /dev/null +++ b/python/examples/plot_metrics.py @@ -0,0 +1,41 @@ +"""Computing Multi-Objective Quality Metrics +========================================= + +TODO: Expand this + +""" + +import numpy as np +import moocore + +# %% +# First, read the datasets. +# + +spherical = moocore.get_dataset("spherical-250-10-3d.txt") +uniform = moocore.get_dataset("uniform-250-10-3d.txt") + +ref = 1.1 +ref_set = moocore.filter_dominated( + np.vstack((spherical[:, :-1], uniform[:, :-1])) +) + + +def apply_within_sets(x, fun, **kwargs): + """Apply ``fun`` for each dataset in ``x``.""" + _, uniq_index = np.unique(x[:, -1], return_index=True) + x_split = np.vsplit(x[:, :-1], uniq_index[1:]) + return [fun(g, **kwargs) for g in x_split] + + +uniform_igd_plus = apply_within_sets(uniform, moocore.igd_plus, ref=ref_set) +spherical_igd_plus = apply_within_sets( + spherical, moocore.igd_plus, ref=ref_set +) + +print(f""" + Uniform Spherical + ------- --------- +Mean IGD+: {np.mean(uniform_igd_plus):.5f} {np.mean(spherical_igd_plus):.5f} + +""") diff --git a/python/pyproject.toml b/python/pyproject.toml index 0b9f5a3a..ceda71be 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ "cffi>=1.15.1", "numpy>=1.22.3", ] + urls.Documentation = "https://multi-objective.github.io/moocore/python/" urls.Homepage = "https://multi-objective.github.io/moocore/python/" urls.Source = "https://github.com/multi-objective/moocore/" @@ -114,6 +115,13 @@ lint.ignore = [ "D213", # multi-line-summary-second-line ] +lint.per-file-ignores."*examples*/*.py" = [ + "D205", # 1 blank line required between summary line and description + "D400", # First line should end with a period + "D415", # First line should end with a period, question mark, or exclamation point + "E402", # Module level import not at top of file +] + [tool.pytest.ini_options] doctest_optionflags = "NUMBER" addopts = [ diff --git a/python/requirements_dev.txt b/python/requirements_dev.txt index dccfd914..2a82bbad 100644 --- a/python/requirements_dev.txt +++ b/python/requirements_dev.txt @@ -1,9 +1,9 @@ +setuptools>=70.1,<74 # Sync with .pre-commit-config.yaml +cffi >= 1.15.1 numpy >= 1.22.3 pre-commit >= 3.3.2 -ruff >= 0.1.4 # Sync with .pre-commit-config.yaml -setuptools >= 65.5.1 -cffi >= 1.15.1 +ruff >= 0.1.4 tox >= 4.6.2 # Sync with tox.ini pytest >= 7 # Sync with tox.ini @@ -16,7 +16,7 @@ pydata_sphinx_theme jupyter ipykernel kaleido -myst-nb +sphinx-gallery sphinxcontrib-napoleon sphinxcontrib-bibtex sphinx-autodoc-typehints @@ -24,3 +24,7 @@ sphinx-copybutton sphinx-design jupyterlab ipywidgets + +# Gallery examples +pandas +seaborn diff --git a/python/src/moocore/_moocore.py b/python/src/moocore/_moocore.py index c2a0817b..edba52f1 100644 --- a/python/src/moocore/_moocore.py +++ b/python/src/moocore/_moocore.py @@ -486,6 +486,9 @@ def hv_approx( >>> moocore.hv_approx(x, ref = reference, maximise = True, seed = 42) 1.0563125590974458 + .. minigallery:: moocore.hv_approx + :add-heading: + """ # Convert to numpy.array in case the user provides a list. We use # np.asarray to convert it to floating-point, otherwise if a user inputs