Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/cd-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ instance/
docs/_build/
docs/source/fortuna.*
docs/source/scripts.*
docs/source/auto_examples/
docs/source/*.nblink

# PyBuilder
Expand Down
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
'nbsphinx',
'nbsphinx_link',
'numpydoc',
"sphinx_gallery.gen_gallery"
]

# Include module names for objects
Expand Down Expand Up @@ -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 ------------------------------------------


Expand Down
1 change: 1 addition & 0 deletions docs/source/toc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

plugin_example
notebooks
auto_examples/index

.. toctree::
:hidden:
Expand Down
5 changes: 5 additions & 0 deletions modopt/examples/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
========
Examples
========

This is a collection of Python scripts demonstrating the use of ModOpt.
10 changes: 10 additions & 0 deletions modopt/examples/__init__.py
Original file line number Diff line number Diff line change
@@ -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

"""
46 changes: 46 additions & 0 deletions modopt/examples/conftest.py
Original file line number Diff line number Diff line change
@@ -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)
153 changes: 153 additions & 0 deletions modopt/examples/example_lasso_forward_backward.py
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions modopt/opt/algorithms/forward_backward.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down