diff --git a/.github/workflows/cd-build.yml b/.github/workflows/cd-build.yml index 1e49f8bc..fca9feb1 100644 --- a/.github/workflows/cd-build.yml +++ b/.github/workflows/cd-build.yml @@ -62,9 +62,7 @@ jobs: - name: Set up Conda with Python 3.8 uses: conda-incubator/setup-miniconda@v2 with: - auto-update-conda: true - python-version: 3.8 - auto-activate-base: false + python-version: "3.8" - name: Install dependencies shell: bash -l {0} diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index b24bea79..c4ba28a0 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -43,7 +43,7 @@ jobs: python -m pip install --upgrade pip python -m pip install -r develop.txt python -m pip install -r docs/requirements.txt - python -m pip install astropy "scikit-image<0.20" scikit-learn + python -m pip install astropy "scikit-image<0.20" scikit-learn matplotlib python -m pip install tensorflow>=2.4.1 python -m pip install twine python -m pip install . @@ -108,11 +108,11 @@ jobs: python --version python -m pip install --upgrade pip python -m pip install -r develop.txt - python -m pip install astropy "scikit-image<0.20" scikit-learn + python -m pip install astropy "scikit-image<0.20" scikit-learn matplotlib python -m pip install . - name: Run Tests shell: bash -l {0} run: | export PATH=/usr/share/miniconda/bin:$PATH - python setup.py test + pytest -n 2 diff --git a/.gitignore b/.gitignore index 06dff8db..f9eaaa68 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,7 @@ instance/ docs/_build/ docs/source/fortuna.* docs/source/scripts.* +docs/source/auto_examples/ docs/source/*.nblink # PyBuilder diff --git a/docs/requirements.txt b/docs/requirements.txt index 4d2a14fb..c9e29c88 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,3 +6,4 @@ numpydoc==1.1.0 sphinx==4.3.1 sphinxcontrib-bibtex==2.4.1 sphinxawesome-theme==3.2.1 +sphinx-gallery==0.11.1 diff --git a/docs/source/conf.py b/docs/source/conf.py index fb954f6d..46564b9f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -45,6 +45,7 @@ 'nbsphinx', 'nbsphinx_link', 'numpydoc', + "sphinx_gallery.gen_gallery" ] # Include module names for objects @@ -145,6 +146,18 @@ # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. html_show_copyright = True + + +# -- Options for Sphinx Gallery ---------------------------------------------- + +sphinx_gallery_conf = { + "examples_dirs": ["../../modopt/examples/"], + "filename_pattern": "/example_", + "ignore_pattern": r"/(__init__|conftest)\.py", +} + + + # -- Options for nbshpinx output ------------------------------------------ diff --git a/docs/source/toc.rst b/docs/source/toc.rst index 84a6af87..ef5753f5 100644 --- a/docs/source/toc.rst +++ b/docs/source/toc.rst @@ -25,6 +25,7 @@ plugin_example notebooks + auto_examples/index .. toctree:: :hidden: diff --git a/modopt/examples/README.rst b/modopt/examples/README.rst new file mode 100644 index 00000000..e6ffbe27 --- /dev/null +++ b/modopt/examples/README.rst @@ -0,0 +1,5 @@ +======== +Examples +======== + +This is a collection of Python scripts demonstrating the use of ModOpt. diff --git a/modopt/examples/__init__.py b/modopt/examples/__init__.py new file mode 100644 index 00000000..d7e77357 --- /dev/null +++ b/modopt/examples/__init__.py @@ -0,0 +1,10 @@ +"""EXAMPLES. + +This module contains documented examples that demonstrate the usage of various +ModOpt tools. + +These examples also serve as integration tests for various methods. + +:Author: Pierre-Antoine Comby + +""" diff --git a/modopt/examples/conftest.py b/modopt/examples/conftest.py new file mode 100644 index 00000000..73358679 --- /dev/null +++ b/modopt/examples/conftest.py @@ -0,0 +1,46 @@ +"""TEST CONFIGURATION. + +This module contains methods for configuring the testing of the example +scripts. + +:Author: Pierre-Antoine Comby + +Notes +----- +Based on: +https://stackoverflow.com/questions/56807698/how-to-run-script-as-pytest-test + +""" +from pathlib import Path +import runpy +import pytest + +def pytest_collect_file(path, parent): + """Pytest hook. + + Create a collector for the given path, or None if not relevant. + The new node needs to have the specified parent as parent. + """ + p = Path(path) + if p.suffix == '.py' and 'example' in p.name: + return Script.from_parent(parent, path=p, name=p.name) + + +class Script(pytest.File): + """Script files collected by pytest.""" + + def collect(self): + """Collect the script as its own item.""" + yield ScriptItem.from_parent(self, name=self.name) + +class ScriptItem(pytest.Item): + """Item script collected by pytest.""" + + def runtest(self): + """Run the script as a test.""" + runpy.run_path(str(self.path)) + + def repr_failure(self, excinfo): + """Return only the error traceback of the script.""" + excinfo.traceback = excinfo.traceback.cut(path=self.path) + return super().repr_failure(excinfo) diff --git a/modopt/examples/example_lasso_forward_backward.py b/modopt/examples/example_lasso_forward_backward.py new file mode 100644 index 00000000..7f820000 --- /dev/null +++ b/modopt/examples/example_lasso_forward_backward.py @@ -0,0 +1,153 @@ +# noqa: D205 +""" +Solving the LASSO Problem with the Forward Backward Algorithm. +============================================================== + +This an example to show how to solve an example LASSO Problem +using the Forward-Backward Algorithm. + +In this example we are going to use: + - Modopt Operators (Linear, Gradient, Proximal) + - Modopt implementation of solvers + - Modopt Metric API. +TODO: add reference to LASSO paper. +""" + +import numpy as np +import matplotlib.pyplot as plt + +from modopt.opt.algorithms import ForwardBackward, POGM +from modopt.opt.cost import costObj +from modopt.opt.linear import LinearParent, Identity +from modopt.opt.gradient import GradBasic +from modopt.opt.proximity import SparseThreshold +from modopt.math.matrix import PowerMethod +from modopt.math.stats import mse + +# %% +# Here we create a instance of the LASSO Problem + +BETA_TRUE = np.array( + [3.0, 1.5, 0, 0, 2, 0, 0, 0] +) # 8 original values from lLASSO Paper +DIM = len(BETA_TRUE) + + +rng = np.random.default_rng() +sigma_noise = 1 +obs = 20 +# create a measurement matrix with decaying covariance matrix. +cov = 0.4 ** abs((np.arange(DIM) * np.ones((DIM, DIM))).T - np.arange(DIM)) +x = rng.multivariate_normal(np.zeros(DIM), cov, obs) + +y = x @ BETA_TRUE +y_noise = y + (sigma_noise * np.random.standard_normal(obs)) + + +# %% +# Next we create Operators for solving the problem. + +# MatrixOperator could also work here. +lin_op = LinearParent(lambda b: x @ b, lambda bb: x.T @ bb) +grad_op = GradBasic(y_noise, op=lin_op.op, trans_op=lin_op.adj_op) + +prox_op = SparseThreshold(Identity(), 1, thresh_type="soft") + +# %% +# In order to get the best convergence rate, we first determine the Lipschitz constant of the gradient Operator +# + +calc_lips = PowerMethod(grad_op.trans_op_op, 8, data_type="float32", auto_run=True) +lip = calc_lips.spec_rad +print("lipschitz constant:", lip) + +# %% +# Solving using FISTA algorithm +# ----------------------------- +# +# TODO: Add description/Reference of FISTA. + +cost_op_fista = costObj([grad_op, prox_op], verbose=False) + +fb_fista = ForwardBackward( + np.zeros(8), + beta_param=1 / lip, + grad=grad_op, + prox=prox_op, + cost=cost_op_fista, + metric_call_period=1, + auto_iterate=False, # Just to give us the pleasure of doing things by ourself. +) + +fb_fista.iterate() + +# %% +# After the run we can have a look at the results + +print(fb_fista.x_final) +mse_fista = mse(fb_fista.x_final, BETA_TRUE) +plt.stem(fb_fista.x_final, label="estimation", linefmt="C0-") +plt.stem(BETA_TRUE, label="reference", linefmt="C1-") +plt.legend() +plt.title(f"FISTA Estimation MSE={mse_fista:.4f}") + +# sphinx_gallery_start_ignore +assert mse(fb_fista.x_final, BETA_TRUE) < 1 +# sphinx_gallery_end_ignore + + +# %% +# Solving Using the POGM Algorithm +# -------------------------------- +# +# TODO: Add description/Reference to POGM. + + +cost_op_pogm = costObj([grad_op, prox_op], verbose=False) + +fb_pogm = POGM( + np.zeros(8), + np.zeros(8), + np.zeros(8), + np.zeros(8), + beta_param=1 / lip, + grad=grad_op, + prox=prox_op, + cost=cost_op_pogm, + metric_call_period=1, + auto_iterate=False, # Just to give us the pleasure of doing things by ourself. +) + +fb_pogm.iterate() + +# %% +# After the run we can have a look at the results + +print(fb_pogm.x_final) +mse_pogm = mse(fb_pogm.x_final, BETA_TRUE) + +plt.stem(fb_pogm.x_final, label="estimation", linefmt="C0-") +plt.stem(BETA_TRUE, label="reference", linefmt="C1-") +plt.legend() +plt.title(f"FISTA Estimation MSE={mse_pogm:.4f}") +# +# sphinx_gallery_start_ignore +assert mse(fb_pogm.x_final, BETA_TRUE) < 1 + +# %% +# Comparing the Two algorithms +# ---------------------------- + +plt.figure() +plt.semilogy(cost_op_fista._cost_list, label="FISTA convergence") +plt.semilogy(cost_op_pogm._cost_list, label="POGM convergence") +plt.xlabel("iterations") +plt.ylabel("Cost Function") +plt.legend() +plt.show() + + +# %% +# We can see that the two algorithm converges quickly, and POGM requires less iterations. +# However the POGM iterations are more costly, so a proper benchmark with time measurement is needed. +# Check the benchopt benchmark for more details. diff --git a/modopt/opt/algorithms/forward_backward.py b/modopt/opt/algorithms/forward_backward.py index 6923a6af..83f45e8b 100644 --- a/modopt/opt/algorithms/forward_backward.py +++ b/modopt/opt/algorithms/forward_backward.py @@ -817,9 +817,9 @@ class POGM(SetUp): Initial guess for the :math:`y` variable z : numpy.ndarray Initial guess for the :math:`z` variable - grad + grad : GradBasic Gradient operator class - prox + prox : ProximalParent Proximity operator class cost : class instance or str, optional Cost function class instance (default is ``'auto'``); Use ``'auto'`` to