From 811ffca704543908cdda3e66ec77843f41fa4b0e Mon Sep 17 00:00:00 2001 From: Theo Guyard <theo.guyard@insa-rennes.fr> Date: Fri, 21 Mar 2025 10:50:21 -0400 Subject: [PATCH] Move repo --- .gitignore | 15 -- LICENSE | 21 -- MANIFEST.in | 13 -- README.md | 87 +------- exp/ICASSP/convergence.py | 86 -------- exp/ICASSP/performance_profiles.py | 118 ----------- exp/utils.py | 22 -- notebooks/examples.ipynb | 260 ------------------------ pyproject.toml | 3 - requirements.txt | 4 - setup.cfg | 2 - setup.py | 32 --- srenet/__init__.py | 1 - srenet/algebra.py | 95 --------- srenet/data.py | 88 -------- srenet/enet.py | 19 -- srenet/projgrad.py | 316 ----------------------------- srenet/utils.py | 130 ------------ tests/convergence.py | 64 ------ tests/identification.py | 48 ----- 20 files changed, 1 insertion(+), 1423 deletions(-) delete mode 100644 .gitignore delete mode 100644 LICENSE delete mode 100644 MANIFEST.in delete mode 100644 exp/ICASSP/convergence.py delete mode 100644 exp/ICASSP/performance_profiles.py delete mode 100644 exp/utils.py delete mode 100644 notebooks/examples.ipynb delete mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 srenet/__init__.py delete mode 100644 srenet/algebra.py delete mode 100644 srenet/data.py delete mode 100644 srenet/enet.py delete mode 100644 srenet/projgrad.py delete mode 100644 srenet/utils.py delete mode 100644 tests/convergence.py delete mode 100644 tests/identification.py diff --git a/.gitignore b/.gitignore deleted file mode 100644 index fb38690..0000000 --- a/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -.DS_Store -.vscode -.ipynb_checkpoints/ - -# Python .gitignore -build/ -dist/ -*.egg-info/ -*.egg -*.py[cod] -__pycache__/ -*.so -*~ -.tox -.cache \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 0b6a1b6..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 Guyard Theo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 4ec2135..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,13 +0,0 @@ -include pyproject.toml - -# Include the README -include *.md - -# Include the license file -include LICENSE - -# Include setup.py -include setup.py - -# Include the data files -recursive-include data * \ No newline at end of file diff --git a/README.md b/README.md index ca7f897..7a1b81e 100644 --- a/README.md +++ b/README.md @@ -1,86 +1 @@ -# Screen & Relax : A fast solving procedure for the Elastic-Net problem by safe identification of the solution support. - -This repository implements the Screen & Relax method solving the Elastic-Net problem. We introduced this method in the paper - -> Guyard, T., Herzet, C., & Elvira, C. (2022, May). Screen & relax: accelerating the resolution of Elastic-Net by safe identification of the solution support. In *ICASSP 2022-2022 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP)* (pp. 5443-5447). IEEE. - -available [here](https://arxiv.org/pdf/2110.07281.pdf). This paper contains theoretical and numerical results that can be reproduced with this toolbox. If you encounter a bug or something unexpected, please let me know by raising an issue on the project page or by contacting me by [mail](mailto:theo.guyard@insa-rennes.fr). - -## Requirements - -The `srenet` (**S**creen & **R**elax method for the **E**lastic-**NET** problem) package works with Python 3.9+. Please refer to the [Python download page](https://www.python.org/downloads/) for install instructions. - -**Dependencies** : [NumPy](http://www.numpy.org), [SciPy](https://www.scipy.org), [Matplotlib](http://matplotlib.org), [Pandas](https://pandas.pydata.org) - -## Install instructions - -1. Clone the repository -```bash -git clone https://gitlab.insa-rennes.fr/Theo.Guyard/screen-and-relax.git -``` - -2. Enter the project folder -```bash -cd screen-and-relax -``` - -3. Ensure that Python 3.9+ is installed -```bash -python -V -> Python 3.9.1 (or higher) -``` - -4. Install external dependencies -```bash -pip install --upgrade pip -pip install -r requirements.txt -``` - -5. Install the `srenet` package -```bash -pip install . -``` - -## ICASSP experiments - -Experiments presented in the Screen & Relax paper submitted to [ICASSP 2022](https://2022.ieeeicassp.org) are located in the folder `screen-and-relax/exp/ICASSP/`. The two available experiments are -* `convergence.py` : convergence of the four compared methods on a single problem instance -* `performance_profiles.py` : figure 1 of the "Screen & Relax" paper - -To run experiments, make sure to be located at the project root and run experiments as follows. -```bash -cd screen-and-relax -python exp/ICASSP/convergence.py -python exp/ICASSP/performance_profiles.py -``` - -You can also edit the experimental setup directly in the script, just follow the comments documenting the code. - -## Running demo examples - -We also provide a notebook explaining how to use the `srenet` package. You can use it as following : -```bash -cd notebooks -jupyter notebook -``` -You may have to install Jupyter. Please refer to the [Jupyter website](https://jupyter.org) for install instructions. - -## Licence - -This software is distributed under the [MIT Licence](https://mit-license.org). - - -## Cite this work - -If you use this package for your own work, please consider citing it as : - -```bibtex -@inproceedings{guyard2022screen, - title={Screen \& relax: accelerating the resolution of Elastic-Net by safe identification of the solution support}, - author={Guyard, Th{\'e}o and Herzet, C{\'e}dric and Elvira, Cl{\'e}ment}, - booktitle={ICASSP 2022-2022 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP)}, - pages={5443--5447}, - year={2022}, - organization={IEEE} -} -``` +This repository is deprecated. Please refer to the new repository [here](https://github.com/TheoGuyard/code_2022_ICASSP_ScreenAndRelax). diff --git a/exp/ICASSP/convergence.py b/exp/ICASSP/convergence.py deleted file mode 100644 index f8f4cf1..0000000 --- a/exp/ICASSP/convergence.py +++ /dev/null @@ -1,86 +0,0 @@ -# ICASSP experient - Convergence - -import os, sys -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -import warnings -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt -from srenet.data import sample_data -from srenet.projgrad import projgrad -from utils import get_params_from_string, get_label_from_string, get_color_from_string - - -################################ Configuration ################################# - -# This is the only part of the code you may have to edit - -m, n = 100, 300 # Dictionnary dimension -dic_gen = 'gaussian' # Dictionnary generation method : `gaussian`, `uniform`, `dct` or `toeplitz` -obs_gen = 'gaussian' # Observation generation method : `gaussian` or `posgaussian` -l1ratio = 0.2 # Ratio lambda/lambda_max where lambda_max = max(A.T * y) -l2ratio = 0.5 # Ration epsilon/lambda_max where lambda_max = max(A.T * y) - - -################################## Experiment ################################## - -print("-------------------------------") -print("ICASSP experiment - Convergence\n") - -methods = ['aPG','aPGs','aPGr','S&R'] -res = {method: {} for method in methods} - -x0 = np.zeros(n) -A, y = sample_data(m, n, dic_gen, obs_gen) -lmax = np.max(A.T @ y) -l1 = l1ratio * lmax -l2 = l2ratio * lmax - -for method in methods: - print(f"Method {method} is running ...") - params = get_params_from_string(method) - params.maxit = 10_000 - params.tol = 1e-15 - params.verbosity = False - x, mnt = projgrad(x0,A,y,l1,l2,params) - res[method]['x'] = x - res[method]['mnt'] = mnt - - -############################## Plot results ################################ - -plt.rcParams["font.size"] = 12 -plt.rcParams["axes.labelsize"] = 14 -plt.rcParams["axes.titlesize"] = 14 -plt.rcParams["lines.linewidth"] = 2 -plt.rcParams["lines.markersize"] = 5 -plt.rcParams["xtick.labelsize"] = 12 -plt.rcParams["ytick.labelsize"] = 12 -plt.rcParams["text.usetex"] = True -plt.rcParams["font.family"] = "serif" -plt.rcParams["font.serif"] = ["Helvetica"] - -fig, ax = plt.subplots(1, 1, figsize=(6,4)) - -for method in methods: - ax.plot( - res[method]['mnt'].flops, - res[method]['mnt'].obj - res[method]['mnt'].obj[-1], - label=get_label_from_string(method), - color=get_color_from_string(method), - marker= '.', - ) -ax.set_xlabel(r"FLOPs") -ax.set_ylabel(r"Objective error") -ax.set_xscale('log') -ax.set_yscale('log') - -plt.legend() -plt.grid(b=True, which='major', axis='y') -plt.grid(b=True, which='minor', axis='y', alpha=0.2) -plt.minorticks_on() -plt.tight_layout() -plt.show() - -print("-------------------------------") \ No newline at end of file diff --git a/exp/ICASSP/performance_profiles.py b/exp/ICASSP/performance_profiles.py deleted file mode 100644 index f4dcaa8..0000000 --- a/exp/ICASSP/performance_profiles.py +++ /dev/null @@ -1,118 +0,0 @@ -# ICASSP experient - Performance profiles - -import os, sys -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -import warnings -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt -from srenet.data import sample_data -from srenet.projgrad import projgrad -from utils import get_params_from_string, get_label_from_string, get_color_from_string - - -################################ Configuration ################################# - -# This is the only part of the code you may have to edit - -m, n = 100, 300 # Dictionnary dimension -dic_gen = 'gaussian' # Dictionnary generation method : `gaussian`, `uniform`, `dct` or `toeplitz` -obs_gen = 'gaussian' # Observation generation method : `gaussian` or `posgaussian` -l1ratio = 0.2 # Ratio lambda/lambda_max where lambda_max = max(A.T * y) -l2ratio = 0.5 # Ration epsilon/lambda_max where lambda_max = max(A.T * y) -flops = 2_000_000 # FLOPs budget -repeats = 100 # Number of instances tested - - -################################## Experiment ################################## - -print("---------------------------------------") -print("ICASSP experiment - Performance profiles\n") - -tolmax = 1. -tolmin = 1e-15 -tolnb = 30 -tols = np.logspace(np.log10(tolmax), np.log10(tolmin), tolnb) -methods = ['aPG','aPGs','aPGr','S&R'] -solved = np.full((len(methods), tolnb, repeats), 0) - -for rep in range(repeats): - print(f"Repeat {rep+1}/{repeats}") - - x0 = np.zeros(n) - A, y = sample_data(m, n, dic_gen, obs_gen) - lmax = np.max(A.T @ y) - l1 = l1ratio * lmax - l2 = l2ratio * lmax - - for (mi,method) in enumerate(methods): - try: - print(f" Method {method} is running ...") - - params = get_params_from_string(method) - params.maxit = 10_000 - params.tol = tolmin - params.verbosity = False - - x, mnt = projgrad(x0,A,y,l1,l2,params) - - if not mnt.stopcrit.has_converged: - warnings.warn("Method did not converge.") - continue - - for ti, tol in enumerate(tols): - its_solved = np.where(mnt.gap < tol)[0] - if len(its_solved) > 0: - it_solved = its_solved[0] - if mnt.flops[it_solved] < flops: - solved[mi,ti,rep] = 1 - else: - solved[mi,ti,rep] = 0 - - except Exception as e: - warnings.warn("Repeat failed with the following error :") - print(e) - -solved = pd.DataFrame( - np.nanmean(solved, axis=-1), - columns = ['{:.2e}'.format(tol) for tol in tols], - index = [method for method in methods], - ) - -############################## Plot results ################################ - -plt.rcParams["font.size"] = 12 -plt.rcParams["axes.labelsize"] = 18 -plt.rcParams["axes.titlesize"] = 18 -plt.rcParams["lines.linewidth"] = 2 -plt.rcParams["lines.markersize"] = 5 -plt.rcParams["xtick.labelsize"] = 12 -plt.rcParams["ytick.labelsize"] = 12 -plt.rcParams["text.usetex"] = True -plt.rcParams["font.family"] = "serif" -plt.rcParams["font.serif"] = ["Helvetica"] - -fig, ax = plt.subplots(1, 1, figsize=(6,4)) - -for method in methods: - ax.plot( - tols, - solved.loc[method,:], - label=get_label_from_string(method), - color=get_color_from_string(method), - marker= '.', - ) -ax.set_xlabel(r"$\tau$") -ax.set_ylabel(r"$\rho_{\%}$") -ax.set_xscale('log') -ax.yaxis.set_major_formatter(lambda x,pos: r"{0:.0f}\%".format(x * 100)) - -plt.legend() -plt.grid(b=True, which='major', axis='y') -plt.grid(b=True, which='minor', axis='y', alpha=0.2) -plt.minorticks_on() -plt.tight_layout() -plt.show() - -print("---------------------------------------") \ No newline at end of file diff --git a/exp/utils.py b/exp/utils.py deleted file mode 100644 index 01fda60..0000000 --- a/exp/utils.py +++ /dev/null @@ -1,22 +0,0 @@ -from srenet.utils import ProjgradParams - -def get_params_from_string(name): - if name == 'aPG' : return ProjgradParams(screen=False, relax=False) - elif name == 'aPGs': return ProjgradParams(screen=True , relax=False) - elif name == 'aPGr': return ProjgradParams(screen=False, relax=True) - elif name == 'S&R' : return ProjgradParams(screen=True , relax=True) - else: raise ValueError("Method name not understood.") - -def get_label_from_string(name): - if name == 'aPG' : return r"aPG" - elif name == 'aPGs': return r"aPGs" - elif name == 'aPGr': return r"aPGr" - elif name == 'S&R' : return r"S\&R" - else: raise ValueError("Method name not understood.") - -def get_color_from_string(name): - if name == 'aPG' : return "royalblue" - elif name == 'aPGs': return "navy" - elif name == 'aPGr': return "darkorange" - elif name == 'S&R' : return "firebrick" - else: raise ValueError("Method name not understood.") \ No newline at end of file diff --git a/notebooks/examples.ipynb b/notebooks/examples.ipynb deleted file mode 100644 index 5eb6b07..0000000 --- a/notebooks/examples.ipynb +++ /dev/null @@ -1,260 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "source": [ - "# Screen & Relax : A fast solving procedure for the Elastic-Net problem by safe identification of the solution support.\n", - "## Example notebook\n", - "---" - ], - "metadata": {} - }, - { - "cell_type": "markdown", - "source": [ - "First of all, import dependencies" - ], - "metadata": {} - }, - { - "cell_type": "code", - "execution_count": 4, - "source": [ - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "from srenet.data import sample_data\n", - "from srenet.projgrad import projgrad\n", - "from srenet.utils import ProjgradParams" - ], - "outputs": [], - "metadata": {} - }, - { - "cell_type": "markdown", - "source": [ - "The problem addressed is the non-negative Elastic-Net given by" - ], - "metadata": {} - }, - { - "cell_type": "markdown", - "source": [ - "$$min_{\\mathbf{x}\\geq\\mathbf{0}} \\ \\Big\\{ \\ \\tfrac{1}{2}\\|\\mathbf{y}-\\mathbf{A}\\mathbf{x}\\|_2^2 + \\lambda \\|\\mathbf{x}\\|_1 + \\tfrac{\\epsilon}{2}\\|\\mathbf{x}\\|_2^2 \\ \\Big\\}$$" - ], - "metadata": {} - }, - { - "cell_type": "markdown", - "source": [ - "Parameters of the problem are defined as" - ], - "metadata": {} - }, - { - "cell_type": "code", - "execution_count": 6, - "source": [ - "m = 100 # y size\n", - "n = 300 # x size\n", - "gen_dic = 'gaussian' # Generation method for A\n", - "gen_obs = 'gaussian' # Generation method for y\n", - "l1ratio = 0.5 # Ratio lambda / np.max(A.T * y)\n", - "l2ratio = 0.2 # Ratio epsilon / np.max(A.T * y)" - ], - "outputs": [], - "metadata": {} - }, - { - "cell_type": "markdown", - "source": [ - "Data of the problem can be sampled with the following lines of code" - ], - "metadata": {} - }, - { - "cell_type": "code", - "execution_count": 7, - "source": [ - "x0 = np.zeros(n) # Initial point for the projected gradient\n", - "A, y = sample_data(m, n, gen_dic, gen_obs) # Dictionary and observation\n", - "l1 = l1ratio * np.max(A.T @ y) # Value of \\lambda\n", - "l2 = l2ratio * np.max(A.T @ y) # Value of \\epsilon " - ], - "outputs": [], - "metadata": {} - }, - { - "cell_type": "markdown", - "source": [ - "Now, run a Projected Gradient method to solve the problem :\n", - "* Without screening and relaxing tests\n", - "* With screning tests only\n", - "* With relaxing tests only\n", - "* With both screening and relaxing tests (Screen & Relax method)" - ], - "metadata": {} - }, - { - "cell_type": "code", - "execution_count": 8, - "source": [ - "x, mnt = projgrad(x0, A, y, l1, l2, ProjgradParams(verbosity=False,screen=False,relax=False))\n", - "x_s, mnt_s = projgrad(x0, A, y, l1, l2, ProjgradParams(verbosity=False,screen=True,relax=False))\n", - "x_r, mnt_r = projgrad(x0, A, y, l1, l2, ProjgradParams(verbosity=False,screen=False,relax=True))\n", - "x_sr, mnt_sr = projgrad(x0, A, y, l1, l2, ProjgradParams(verbosity=False,screen=True,relax=True))\n", - "res = {\n", - " 'aPG' : {'x': x, 'mnt': mnt},\n", - " 'aPGs': {'x': x_s, 'mnt': mnt_s},\n", - " 'aPGr': {'x': x_r, 'mnt': mnt_r},\n", - " 'S&R' : {'x': x_sr, 'mnt': mnt_sr},\n", - "}\n", - "# x : optimizer of the problem\n", - "# mnt : monitoring values" - ], - "outputs": [], - "metadata": {} - }, - { - "cell_type": "markdown", - "source": [ - "You can display some monitoring values that are outputted by the algorithm" - ], - "metadata": {} - }, - { - "cell_type": "code", - "execution_count": 9, - "source": [ - "print(\"Monitoring values\")\n", - "print(\"-----------------\")\n", - "for k, v in res.items():\n", - " print(k)\n", - " print(\" Stop crit : {}\".format(v['mnt'].stopcrit))\n", - " print(\" Last Gap : {:.2e}\".format(v['mnt'].gap[-1]))\n", - " print(\" Last Obj : {:.2e}\".format(v['mnt'].obj[-1]))\n", - " print(\" Iters : {}\".format(len(v['mnt'].gap)))\n", - " print(\" FLOPs : {:.2e}\".format(v['mnt'].flops[-1]))" - ], - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Monitoring values\n", - "-----------------\n", - "aPG\n", - " Stop crit : StopCrit.CONVERGED\n", - " Last Gap : 2.78e-16\n", - " Last Obj : 4.68e-01\n", - " Iters : 199\n", - " FLOPs : 2.48e+07\n", - "aPGs\n", - " Stop crit : StopCrit.CONVERGED\n", - " Last Gap : 2.78e-16\n", - " Last Obj : 4.68e-01\n", - " Iters : 199\n", - " FLOPs : 2.25e+06\n", - "aPGr\n", - " Stop crit : StopCrit.CONVERGED\n", - " Last Gap : 5.55e-17\n", - " Last Obj : 4.68e-01\n", - " Iters : 34\n", - " FLOPs : 4.37e+06\n", - "S&R\n", - " Stop crit : StopCrit.CONVERGED\n", - " Last Gap : 1.11e-16\n", - " Last Obj : 4.68e-01\n", - " Iters : 34\n", - " FLOPs : 1.18e+06\n" - ] - } - ], - "metadata": {} - }, - { - "cell_type": "markdown", - "source": [ - "You can also construct various graphics with monitoring values :" - ], - "metadata": {} - }, - { - "cell_type": "code", - "execution_count": 10, - "source": [ - "fig, axs = plt.subplots(1,3,figsize=(12,3))\n", - "\n", - "for k,v in res.items():\n", - " axs[0].plot(v['mnt'].flops, v['mnt'].obj-v['mnt'].obj[-1], label=k, marker='.')\n", - " axs[1].plot(v['mnt'].flops, v['mnt'].gap, label=k, marker='.')\n", - "\n", - "I_pos = np.sum(np.abs(res['S&R']['x']) > 1e-8)\n", - "I_null = np.sum(np.abs(res['S&R']['x']) <= 1e-8)\n", - "axs[2].plot(res['S&R']['mnt'].gap, res['S&R']['mnt'].sc_size/I_null, label=\"% Screened\", marker='.')\n", - "axs[2].plot(res['S&R']['mnt'].gap, res['S&R']['mnt'].rc_size/I_pos, label=\"% Relaxed\", marker='.')\n", - "\n", - " \n", - "axs[0].set_xlabel(\"FLOPs\")\n", - "axs[0].set_ylabel(\"Objective error\")\n", - "axs[0].set_xscale('log')\n", - "axs[0].set_yscale('log')\n", - "axs[0].grid(b=True, which='major', axis='y')\n", - "axs[0].legend()\n", - "axs[1].set_xlabel(\"FLOPs\")\n", - "axs[1].set_ylabel(\"Duality Gap\")\n", - "axs[1].set_xscale('log')\n", - "axs[1].set_yscale('log')\n", - "axs[1].grid(b=True, which='major', axis='y')\n", - "axs[1].legend() \n", - "axs[2].yaxis.set_major_formatter(lambda x,pos: r\"{0:.0f}%\".format(x * 100))\n", - "axs[2].invert_xaxis()\n", - "axs[2].set_xlabel(\"GAP\")\n", - "axs[2].set_ylabel(\"Indices identified in S&R method\")\n", - "axs[2].set_xscale('log')\n", - "axs[2].grid(b=True, which='major', axis='y')\n", - "axs[2].legend()\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ], - "outputs": [ - { - "output_type": "display_data", - "data": { - "image/png": "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", - "text/plain": [ - "<Figure size 864x216 with 3 Axes>" - ] - }, - "metadata": { - "needs_background": "light" - } - } - ], - "metadata": {} - } - ], - "metadata": { - "kernelspec": { - "name": "python3", - "display_name": "Python 3.9.1 64-bit ('srenet': pyenv)" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.1" - }, - "interpreter": { - "hash": "bc219ed3a4d84e3940fd3bdf8d6746735ad7a4a1b4666e5401f7a1755a2774f1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 73a7fd1..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,3 +0,0 @@ -[build-system] -requires = ["setuptools>=40.8.0", "wheel"] -build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 0731cd3..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -matplotlib==3.4.2 -numpy==1.21.0 -pandas==1.2.5 -scipy==1.7.0 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 0792c86..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -license_files = LICENSE \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 13e34a7..0000000 --- a/setup.py +++ /dev/null @@ -1,32 +0,0 @@ -from setuptools import setup, find_packages -import pathlib - -here = pathlib.Path(__file__).parent.resolve() -long_description = (here / 'README.md').read_text(encoding='utf-8') - -PACKAGE_NAME = 'srenet' - -setup( - name=PACKAGE_NAME, - version='1.0.0', - description='Screen & Relax : A fast solving procedure for the Elastic-Net problem by safe identification of the solution support.', - long_description=long_description, - long_description_content_type='text/markdown', - author='Theo Guyard', - author_email='theo.guyard@insa-rennes.fr', - - classifiers=[ - 'Development Status :: 1 - Planning', - 'Intended Audience :: Science/Research', - 'Topic :: Scientific/Engineering :: Mathematics', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.9', - ], - - keywords='optimization, compressive sensing, screening', - - packages=find_packages(), - include_package_data=True, - python_requires='>=3.9, <4', - -) \ No newline at end of file diff --git a/srenet/__init__.py b/srenet/__init__.py deleted file mode 100644 index c2aad6e..0000000 --- a/srenet/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'Theo Guyard' diff --git a/srenet/algebra.py b/srenet/algebra.py deleted file mode 100644 index 3148448..0000000 --- a/srenet/algebra.py +++ /dev/null @@ -1,95 +0,0 @@ -import numpy as np - -def coherence(A): - """ - Return the mutual coherence of a matrix A. - """ - if A.shape[1] > 0: - gram = A.T @ A - n = gram.shape[0] - for i in range(n): - gram[i,i] = 0. - return np.max(np.abs(gram)) - else: - return 0 - -def update_data(Ginv,B,Aw,b,yw,tw,i,A,y,S,Sbar,l1,l2,AtA,gram,Aty_l1): - """ - Update the reduced problem data when a new indice is relaxed. - """ - - n = Ginv.shape[0] # Curr. number of relaxed atoms - Si = np.sum(S[:i]) # Position of i in S - Sbari = np.sum(Sbar[:i]) # Position of i in Sbar - perm = np.insert(np.arange(n), Si, -1) # Perm. last <-> ith row/column - - # ------------------------------------------------------------------------ # - # Values computed to update problem data # - # Value Expression Dimensions # - # ------------------------------------------------------------------------ # - g = AtA[S,i] # |S| x 0 - q = gram[i,i] # 1 x 0 - Ginv_g = Ginv @ g # |S| x 0 - d = 1 / (q - (g @ Ginv_g)) # 1 x 0 - Ginv_g_d = d * Ginv_g # |S| x 0 - d_Ginv_g_Ginv_g = np.outer(Ginv_g_d,Ginv_g) # |S| x |S| - Ginv_11 = Ginv + d_Ginv_g_Ginv_g # |S| x |S| - Ginv_12 = - Ginv_g_d # |S| x |S| - Bred = np.delete(B,Sbari,axis=1) # |S|-1 x |S|-1 - d_B_g_AtA = d * (B.T @ g + AtA[Sbar,i]) # |Sbar| x 0 - B2 = np.delete(d_B_g_AtA,Sbari) # |Sbar|-1 x 0 - B1 = Bred + np.outer(Ginv_g,B2.T) # |Sbar|-1 x |S|-1 - b1 = b # |S| x 0 - b1 += Aty_l1[S] @ d_Ginv_g_Ginv_g # |S| x 0 - b1 -= Aty_l1[i] * Ginv_g_d # |S| x 0 - b2 = - Aty_l1[S] @ Ginv_g_d + Aty_l1[i] * d # 1 x 0 - Aw1 = np.outer(A[:,S] @ Ginv_g - A[:,i], B2) # |S| x |S| - yw1 = A[:,S] @ b1 # m x 0 - yw2 = b2 * A[:,i] # 1 x 0 - tw1 = B1.T @ (l1 + l2*b1) # |S| x 0 - tw2 = - B2 * (l1 + l2*b2) # 1 x 0 - - # ----------------------------- FLOPS count ------------------------------ # - # Above computatations - # Ginv_g : |S|^2/2 (symmetric matrix) - # d : 2 + |S| - # Ginv_g_d : |S| - # d_Ginv_g_Ginv_g : |S|^2/2 (symmetric outer product) - # Ginv_11 : |S|^2/2 (sum of symmetric matrices) - # Ginv_12 : |S| - # d_B_g_AtA : |S||Sbar| + |Sbar| - # B1 : |S||Sbar| - # b1 : |S| + 4 - # b2 : |S| + 3 - # Aw1 : |S|^2 + m + |S| - # yw1 : m|S| - # yw2 : 1 - # tw1 : 3|S| - # tw2 : 4 - # TOTAL : (5/2)|S|^2 + 2|S||Sbar| + m|S| + 9|S| + |Sbar| + 14 - # ------------------------------------------------------------------------ # - - # Update of Ginv - Ginv_new = np.vstack(( - np.hstack((np.reshape(Ginv_11,(n,n)), np.reshape(Ginv_12,(n,1)))), - np.hstack((np.reshape(Ginv_12,(1,n)), np.reshape(d,(1,1)))) - )) - Ginv_new = Ginv_new[:,perm][perm,:] - - # Update of B - B_new = np.vstack((B1, -B2.T)) - B_new = B_new[perm,:] - - # Update of b - b_new = np.hstack((np.reshape(b1,(n,)), np.reshape(b2,(1,)))) - b_new = b_new[perm] - - # Update of Aw - Aw_new = np.delete(Aw,Sbari,axis=1) - Aw_new += Aw1 - - # Update of yw and tw - yw = y - yw1 - yw2 - tw = l1 + tw1 + tw2 - - return Ginv_new, B_new, Aw_new, b_new, yw, tw \ No newline at end of file diff --git a/srenet/data.py b/srenet/data.py deleted file mode 100644 index 5b5a991..0000000 --- a/srenet/data.py +++ /dev/null @@ -1,88 +0,0 @@ -import numpy as np -from scipy.fftpack import dct - -DIC_GENERATIONS = ['gaussian', 'uniform', 'dct', 'toeplitz'] -OBS_GENERATIONS = ['gaussian', 'posgaussian'] - -def _normalize(x): - if len(x.shape) == 2: - for i in range(x.shape[1]): - x[:, i] /= np.linalg.norm(x[:, i], 2) - elif len(x.shape) == 1: - x /= np.linalg.norm(x,2) - else: - raise ValueError("Require x with 1 or 2 dimensions.") - return x - -def _gaussian_dic(m, n): - A = np.random.randn(n, m).T - return A - -def _uniform_dic(m, n): - A = np.random.rand(n, m).T - return A - -def _dct_dic(m, n): - A = dct(np.eye(max(m,n))) - indices = np.random.permutation(n) - A = A[indices,:] - A = A[:n,:m].T - return A - -def _toeplitz_dic(m, n): - ranget = np.linspace(-10, 10, m) - offset = 3. - rangemu = np.linspace(np.min(ranget)+offset, np.max(ranget)-offset, n) - A = np.zeros((m, n)) - for j in range(n): - A[:,j] = np.exp(-.5 * (ranget - rangemu[j])**2) - return A - -def _gaussian_obs(m,pos=False): - y = np.random.randn(m) - if pos: - y = np.maximum(y,0) - return y - -def sample_data(m,n,dic_gen,obs_gen): - """ - Sample a normalized (m,n) matrix and a normalized (m,) vector using given - generation methods. - - Parameters - ---------- - m: int - Dimension of y. - n: int - Number of columns in A. - dic_gen: {'gaussian', 'uniform', 'dct', 'toeplitz'} - Generation method for A. - dic_gen: {'gaussian', 'posgaussian'} - Generation method for y. - """ - - if dic_gen not in DIC_GENERATIONS: - raise ValueError(f"Values allowed for parameter dic_gen are " \ - " {DIC_GENERATIONS}.") - if obs_gen not in OBS_GENERATIONS: - raise ValueError(f"Values allowed for parameter obs_gen are " \ - " {OBS_GENERATIONS}.") - - if dic_gen == 'gaussian': - A = _gaussian_dic(m, n) - elif dic_gen == 'uniform': - A = _uniform_dic(m, n) - elif dic_gen == 'dct': - A = _dct_dic(m, n) - elif dic_gen == 'toeplitz': - A = _toeplitz_dic(m, n) - - if obs_gen == 'gaussian': - y = _gaussian_obs(m,False) - elif obs_gen == 'posgaussian': - y = _gaussian_obs(m,True) - - A = _normalize(A) - y = _normalize(y) - - return A, y diff --git a/srenet/enet.py b/srenet/enet.py deleted file mode 100644 index 6a232f6..0000000 --- a/srenet/enet.py +++ /dev/null @@ -1,19 +0,0 @@ -import numpy as np - -def primal_value(x, A, y, l1, l2): - x = np.maximum(x,0) - pv = 0.5 * np.linalg.norm(y-A@x,2)**2 + l1 * np.sum(x) + \ - (l2/2) * np.linalg.norm(x,2)**2 - return pv - -def dual_value(u, A, y, l1, l2): - dv = 0.5 * (np.linalg.norm(y,2)**2 - np.linalg.norm(y-u,2)**2 - \ - (1./l2) * np.linalg.norm(np.maximum(A.T @ u-l1,0),2)**2 - ) - return dv - -def duality_gap(x, u, A, y, l1, l2): - x = np.maximum(x,0) - pv = primal_value(x, A, y, l1, l2) - dv = dual_value(u, A, y, l1, l2) - return np.abs(pv-dv) \ No newline at end of file diff --git a/srenet/projgrad.py b/srenet/projgrad.py deleted file mode 100644 index 91b339f..0000000 --- a/srenet/projgrad.py +++ /dev/null @@ -1,316 +0,0 @@ -import time -import numpy as np -from srenet.algebra import coherence, update_data -from srenet.enet import primal_value, duality_gap -from srenet.utils import ( - StopCrit, - ProjgradParams, - ProjgradMonitor, - validate_inputs, - print_header, - print_trace, - print_footer, -) - -def projgrad(x0, A, y, l1, l2, params=ProjgradParams()): - """ - Solve the non-negative Elastic-Net problem - min_{x>=0} (1/2) ||y-Ax||_2^2 + l1 ||x||_1 + (l2/2) ||x||_2^2 - using a projected-gradient algorithm. Can be overloaded with screening and - relaxing tests. - - Parameters - ---------- - x0 : np.ndarray - Initial point with shape (n,). - A : np.ndarray - Dictionary with shape (m,n). - y : np.ndarray - Target observation with shape (m,). - l1 : float, positive - L1 regularization parameter. - l2 : float, positive - L2 regularization parameter. - params : ProjgradParams - Optional parameters for the algorithm (see `utils.ProjgradParams`). - - Returns - ------- - xopt : np.ndarray - Solution of the problem with shape (n,). - monitor : ProjgradMonitor - Monitoring values (see `utils.ProjgradMonitor`). - """ - - validate_inputs(x0, A, y, l1, l2) - params.validate() - - if params.verbosity: - print_header(A, l1, l2, params) - - monitor = ProjgradMonitor() - - ########################### Initialization ############################# - - # /!\ No FLOPs are counted for the initialization as all values can be - # /!\ computed off-line - - start_time = time.time() - it = 0 - - # Precomputed values - m, n = A.shape - ninit = n - sqrt_l2 = np.sqrt(l2) - Aty = A.T @ y - Aty_l1 = Aty - l1 - AtA = A.T @ A - gram = AtA + l2 * np.eye(n) - L = np.max(np.abs(np.linalg.eigvals(gram))) - - # Iterates - x = np.maximum(x0,0) - u = y - A @ x - xold = np.copy(x) - zold = np.copy(x) - told = 1 - - # Best gap recorded so far for screening and relaxing tests - best_x = np.copy(x) - best_Atu = np.copy(A.T @ u) - best_gap = duality_gap(x,u,A,y,l1,l2) - - # Screened and relaxed coefficients - idx = np.arange(n) # Initial indices - S = np.zeros(n,dtype=bool) # Indices relaxed - S[params.Sinit] = True # Indices relaxed at initialization - Sbar = np.invert(S) # Indices not fixed yet - sc = set() # Screened coefficients - rc = set() # Relaxed coefficients - rc.update(idx[S]) - - # Data of the transformed problem - Ginv = np.linalg.inv(gram[:,S][S,:]) # (A[:,S].T @ A[:,S] + l2 * I)^{-1} - b = Ginv @ Aty_l1[S] # Ginv @ Aty_l1[S] - B = - Ginv @ A[:,S].T @ A[:,Sbar] # - Ginv @ AtA[S,Sbar] - yw = y - A[:,S] @ b # y - A[:,S] @ b - Aw = A[:,Sbar] + A[:,S] @ B # A[:,Sbar] + A[:,S] @ B - tw = l1 + B.T @ (l1 + l2 * b) # l1 + B.T @ (l1 + l2 * b) - - # Monitoring - monitor.obj.append(primal_value(x,A,y,l1,l2)) - monitor.gap.append(duality_gap(x,u,A,y,l1,l2)) - monitor.flops.append(0) - monitor.sc_size.append(len(sc)) - monitor.rc_size.append(len(rc)) - if params.coherence: - monitor.coherence.append(coherence(Aw)) - else: - monitor.coherence.append(np.nan) - - - ############################# Main loop ################################ - - while True: - - it += 1 - flops = 0 # FLOPs of the current iteration - S_size = np.sum(S) # ie. card(S) - Sbar_size = np.sum(Sbar) # ie. card(Sbar) - - # Iterates update - Awz = Aw @ zold[Sbar] - u = yw - Awz - Awtu = Aw.T @ u - BBz = B.T @ (B @ zold[Sbar]) - Awtu_tw = Awtu - tw - grad = Awtu_tw - l2 * (zold[Sbar] + BBz) - x[Sbar] = np.maximum(zold[Sbar] + grad/L, 0) - x[S] = np.maximum(b + B @ x[Sbar],0) - t = (1 + np.sqrt(1 + 4 * told ** 2)) / 2 - z = x + ((told - 1) / t) * (x - xold) - - # ------------------------- FLOPs count ------------------------------ # - # Iterates update - # Awz : 2m|Sbar| - # u : m - # Awtu : 2m|Sbar| - # BBz : 4|Sbar||S| - # Awtu_tw : |Sbar| - # grad : 2|Sbar| + |S| - # x[Sbar] : 2|Sbar| - # x[S] : 2|S|^2 + |S| - # t : 6 - # z : 3n + 2 - flops += 4*m*Sbar_size + 4*Sbar_size*S_size + 2*S_size**2 + 3*n + m + \ - 5*Sbar_size + 2*S_size + 8 - # -------------------------------------------------------------------- # - - # Gap computation - zpos = np.maximum(zold[Sbar],0) - Atu = np.zeros(n) - Atu[S] = A[:,S].T @ u - Atu[Sbar] = Awtu - B.T @ Atu[S] - y_u = y - u - v = np.maximum(Atu-l1, 0) - vpos = v[v>0] - pv = 0.5*u@u + tw@zpos + (l2/2)*(zpos@BBz + zpos@zpos + b@b) + l1*np.sum(b) - dv = 0.5 * (1 - (y_u@y_u) - (1/l2)*(vpos@vpos)) - gap = np.abs(pv-dv) - - # Update best primal and dual points recorded so far - if gap < best_gap: - best_gap = gap - best_x = np.copy(np.maximum(zold,0)) - best_Atu = np.copy(Atu) - - # ------------------------- FLOPs count ------------------------------ # - # GAP computation - # Atu[S] : 2m|S| - # Atu[Sbar] : n + 2|S|m - # y_u : m - # v : n - # pv : 2m + 6|Sbar| + 3|S| + 5 - # dv : 2m + 2|vpos| + 4 - # gap : 1 - flops += 4*m*S_size + 2*n + 5*m + 5*Sbar_size + 3*S_size + 10 - # -------------------------------------------------------------------- # - - - if params.screen: - - # Screening test - radius = np.sqrt(2 * best_gap) - screen = np.where(best_Atu <= l1 - radius)[0] - sc.update(idx[screen]) - - # Dimension reduction - if len(screen) > 0: - screen_Sbar = np.where(best_Atu[Sbar] <= l1 - radius)[0] - A = np.delete(A, screen, axis=1) - gram = np.delete(gram, screen, axis=0) - gram = np.delete(gram, screen, axis=1) - AtA = np.delete(AtA, screen, axis=0) - AtA = np.delete(AtA, screen, axis=1) - Atu = np.delete(Atu, screen) - Aty = np.delete(Aty, screen) - Aty_l1 = np.delete(Aty_l1, screen) - x = np.delete(x, screen) - z = np.delete(z, screen) - best_x = np.delete(best_x, screen) - best_Atu = np.delete(best_Atu, screen) - idx = np.delete(idx, screen) - S = np.delete(S, screen) - Sbar = np.delete(Sbar, screen) - Aw = np.delete(Aw, screen_Sbar, axis=1) - B = np.delete(B, screen_Sbar, axis=1) - tw = np.delete(tw, screen_Sbar) - _, n = A.shape - - # ----------------------- FLOPs count ---------------------------- # - # Screening - # radius : 2 - # screen : 1 - flops += 3 - # ---------------------------------------------------------------- # - - if params.relax: - - # Relaxing test - radius = np.sqrt(2 * best_gap) / sqrt_l2 - relax_Atu = np.where(best_Atu > l1 + radius * sqrt_l2)[0] - relax_x = np.where(best_x > radius)[0] - relax = np.unique(np.hstack((relax_Atu, relax_x))) - rc.update(idx[relax]) - - # ----------------------- FLOPs count ---------------------------- # - # Relaxing - # radius : 2 - # relax_Atu : 2 - flops += 4 - # ---------------------------------------------------------------- # - - # Update problem data - if len(relax) > 0: - for i in relax[Sbar[relax]]: - Ginv, B, Aw, b, yw, tw = update_data(Ginv, B, Aw, b, yw, tw, - i, A, y, S, Sbar, l1, - l2, AtA, gram, Aty_l1 - ) - - # -------------------- FLOPs count ----------------------- # - # Update problem data - # See algebra.update_data for FLOPs count detail - flops += 1.5*S_size**2 + S_size*Sbar_size + m*S_size + \ - 9*S_size + Sbar_size + 14 - # -------------------------------------------------------- # - - S[i] = True - Sbar[i] = False - S_size += 1 - Sbar_size += 1 - - - if len(sc)+len(rc) == ninit: - - # Closed form for the optimizer - x[S] = b - x[Sbar] = 0 - - # /!\ Following operations are only used for monitoring purpose - # /!\ but are not needed in practice because convergence is - # /!\ already certified. Thus, no FLOPs are counted. - monitor.obj.append(primal_value(x,A,y,l1,l2)) - monitor.gap.append(duality_gap(x,y-A@x,A,y,l1,l2)) - monitor.flops.append(monitor.flops[-1] + flops) - monitor.sc_size.append(len(sc)) - monitor.rc_size.append(len(rc)) - monitor.stopcrit = StopCrit.CONVERGED - if params.coherence: - monitor.coherence.append(coherence(Aw)) - else: - monitor.coherence.append(np.nan) - if params.verbosity and it % params.showevery == 0: - print_trace(it, monitor) - break - - - # Displays and monitoring - monitor.obj.append(pv) - monitor.gap.append(gap) - monitor.flops.append(monitor.flops[-1] + flops) - monitor.sc_size.append(len(sc)) - monitor.rc_size.append(len(rc)) - if params.coherence: - monitor.coherence.append(coherence(Aw)) - else: - monitor.coherence.append(np.nan) - - if params.verbosity and it % params.showevery == 0: - print_trace(it, monitor) - - if gap <= params.tol: - monitor.stopcrit = StopCrit.CONVERGED - break - elif it+1 >= params.maxit: - monitor.stopcrit = StopCrit.INTERRUPTED - break - - xold = np.copy(x) - zold = np.copy(z) - told = np.copy(t) - - - ############################## Postprocessing ############################## - - monitor.sc = sc - monitor.rc = rc - monitor.solvetime = time.time() - start_time - - xopt = np.zeros(ninit) - xopt[idx] = x - - if params.verbosity: - print_footer(monitor) - - return xopt, monitor diff --git a/srenet/utils.py b/srenet/utils.py deleted file mode 100644 index cc92b97..0000000 --- a/srenet/utils.py +++ /dev/null @@ -1,130 +0,0 @@ -import warnings -import numpy as np -from dataclasses import dataclass, field -from enum import Enum - -_WIDTH = 75 - -class StopCrit(Enum): - NOT_OPTIMIZED = -1 - INTERRUPTED = 0 - CONVERGED = 1 - - @property - def has_converged(self): - return (self.value > 0) - -@dataclass -class TypeValidator: - def validate_type(self): - for f_name, f_def in self.__dataclass_fields__.items(): - f_type = type(getattr(self, f_name)) - if not (f_type == list or f_type == np.ndarray): - if not issubclass(f_type,f_def.type): - raise ValueError(f"Expected {f_def.type} for {f_name}") - -@dataclass -class ProjgradParams(TypeValidator): - """ - Parameters for the method projgrad in srenet.projgrad. - """ - maxit : int = 10_000 # Maximum number of iterations - tol : float = 1e-15 # Tolerance required on the gap - verbosity : bool = True # Toogle verbosity of the method - showevery : int = 10 # Show iter info every `showevery` - screen : bool = False # Toogle screening tests - relax : bool = False # Toogle relaxing tests - coherence : bool = False # Compute coherence every iterations - Sinit : list[int] = field(default_factory=list) # Init. knowledge of S - def validate(self): - self.validate_type() - -@dataclass -class ProjgradMonitor(): - """ - Monitoring values of the method projgrad in srenet.projgrad. - """ - obj : list[float] = field(default_factory=list) # Objective trace - gap : list[float] = field(default_factory=list) # Gap trace - flops : list[int] = field(default_factory=list) # FLOPs trace - stopcrit : StopCrit = StopCrit.NOT_OPTIMIZED # Stopping criterion - solvetime : float = -1 # Solvetime - sc : set[int] = field(default_factory=set) # Screened coefs - rc : set[int] = field(default_factory=set) # Relaxed coefs - sc_size : list[int] = field(default_factory=list) # Nb screened coefs over iters. - rc_size : list[int] = field(default_factory=list) # Nb relaxed coefs over iters. - coherence : list[float] = field(default_factory=list) # Coherence trace - -def validate_inputs(x0, A, y, l1, l2): - if not issubclass(type(x0),np.ndarray): - raise ValueError("Expected np.ndarray for x0") - if not issubclass(type(A),np.ndarray): - raise ValueError("Expected np.ndarray for A") - if not issubclass(type(y),np.ndarray): - raise ValueError("Expected np.ndarray for y") - if y.shape[0] != A.shape[0]: - raise ValueError("Dimension missmatch between A and y") - if x0.shape[0] != A.shape[1]: - raise ValueError("Dimension missmatch between A and x0") - if not issubclass(type(l1),float): - raise ValueError("Expected float for l1") - if not issubclass(type(l2),float): - raise ValueError("Expected float for l2") - if l1 <= 0: - raise ValueError("Expected a positive value for l1") - if l2 <= 0: - raise ValueError("Expected a positive value for l2") - if not np.allclose(np.linalg.norm(y,2),1): - raise ValueError("Expected y to be normalized") - for i in range(A.shape[1]): - if not np.allclose(np.linalg.norm(A[:,i],2),1): - raise ValueError("Expected columns of A to be normalized") - -def print_header(A, l1, l2, params): - print('\n'+'*'*_WIDTH) - print(("Projected-Gradient for the Elastic-Net problem\n").center(_WIDTH)) - print("Parameters") - print(" dim : {:d}x{:d}".format(A.shape[0],A.shape[1])) - print(" l1 : {:.2e}".format(l1)) - print(" l2 : {:.2e}".format(l2)) - print(" maxit : {:d}".format(params.maxit)) - print(" tol : {:.2e}".format(params.maxit)) - print(" screen : {}".format(params.screen)) - print(" relax : {}".format(params.relax)) - - print() - - HEADER = "{}|{}{}{}{}{}".format( - 'Iter'.ljust(7), - 'Obj'.rjust(10), - 'Gap'.rjust(10), - 'Flops'.rjust(10), - 'Screen'.rjust(7), - 'Relax'.rjust(7), - ) - - print(HEADER) - print("-"*len(HEADER)) - -def print_trace(it, monitor): - TRACE = "{}|{}{}{}{}{}".format( - '{:d}'.format(it).ljust(7), - '{:.2e}'.format(monitor.obj[it]).rjust(10), - '{:.2e}'.format(monitor.gap[it]).rjust(10), - '{:.2e}'.format(monitor.flops[it]).rjust(10), - '{:d}'.format(monitor.sc_size[it]).rjust(7), - '{:d}'.format(monitor.rc_size[it]).rjust(7), - ) - print(TRACE) - -def print_footer(monitor): - print() - if not monitor.stopcrit.has_converged: - warnings.warn("Algorithm did not converge") - print("Stop crit : {}".format(monitor.stopcrit.name)) - print("Obj value : {:.5e}".format(monitor.obj[-1])) - print("Final gap : {:.5e}".format(monitor.gap[-1])) - print("Flops : {:.5e}".format(monitor.flops[-1])) - print("Solve time : {:.2f} seconds".format(monitor.solvetime)) - print() - print('*'*_WIDTH + '\n') \ No newline at end of file diff --git a/tests/convergence.py b/tests/convergence.py deleted file mode 100644 index 4051d6d..0000000 --- a/tests/convergence.py +++ /dev/null @@ -1,64 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -from srenet.data import sample_data -from srenet.projgrad import projgrad -from srenet.utils import ProjgradParams - -m, n = 100, 300 # Size of A -gen_dic = 'gaussian' # Sampling method for A -gen_obs = 'gaussian' # Sampling method for y -l1ratio = 0.1 # l1-regularizer / max(A.T @ y) -l2ratio = 0.1 # l2-regularizer / max(A.T @ y) - -x0 = np.zeros(n) -A, y = sample_data(m, n, gen_dic, gen_obs) -lmax = np.max(A.T @ y) -l1 = l1ratio * lmax -l2 = l2ratio * lmax - -x, mnt = projgrad(x0, A, y, l1, l2, ProjgradParams(verbosity=False,screen=False,relax=False)) -x_s, mnt_s = projgrad(x0, A, y, l1, l2, ProjgradParams(verbosity=False,screen=True,relax=False)) -x_r, mnt_r = projgrad(x0, A, y, l1, l2, ProjgradParams(verbosity=False,screen=False,relax=True)) -x_sr, mnt_sr = projgrad(x0, A, y, l1, l2, ProjgradParams(verbosity=False,screen=True,relax=True)) - -res = { - 'aPG' : {'x': x, 'mnt': mnt}, - 'aPGs': {'x': x_s, 'mnt': mnt_s}, - 'aPGr': {'x': x_r, 'mnt': mnt_r}, - 'S\&R' : {'x': x_sr, 'mnt': mnt_sr}, -} - -print("----------------") -print("Convergence test") -for k, v in res.items(): - print(k) - print(" Stop crit : {}".format(v['mnt'].stopcrit)) - print(" Last Gap : {:.2e}".format(v['mnt'].gap[-1])) - print(" Last Obj : {:.2e}".format(v['mnt'].obj[-1])) - print(" Iters : {}".format(len(v['mnt'].gap))) - print(" FLOPs : {:.2e}".format(v['mnt'].flops[-1])) - -plt.rcParams["font.size"] = 12 -plt.rcParams["axes.labelsize"] = 12 -plt.rcParams["axes.titlesize"] = 18 -plt.rcParams["lines.linewidth"] = 2 -plt.rcParams["lines.markersize"] = 5 -plt.rcParams["xtick.labelsize"] = 12 -plt.rcParams["ytick.labelsize"] = 12 -plt.rcParams["text.usetex"] = True -plt.rcParams["font.family"] = "serif" -plt.rcParams["font.serif"] = ["Helvetica"] - -fig, ax = plt.subplots(1,1,figsize=(4,3)) -for k,v in res.items(): - ax.plot(v['mnt'].flops, v['mnt'].obj-v['mnt'].obj[-1], label=k, marker='.') -ax.set_xlabel(r"FLOPs") -ax.set_ylabel(r"$f(x)-f(x^{\star})$") -ax.set_xscale('log') -ax.set_yscale('log') -ax.grid(b=True, which='major', axis='y') -ax.legend() -plt.tight_layout() -plt.show() - -print("----------------") \ No newline at end of file diff --git a/tests/identification.py b/tests/identification.py deleted file mode 100644 index 0aefba1..0000000 --- a/tests/identification.py +++ /dev/null @@ -1,48 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -from srenet.data import sample_data -from srenet.projgrad import projgrad -from srenet.utils import ProjgradParams - -m, n = 100, 300 # Size of A -gen_dic = 'gaussian' # Sampling method for A -gen_obs = 'gaussian' # Sampling method for y -l1ratio = 0.1 # l1-regularizer / max(A.T @ y) -l2ratio = 0.1 # l2-regularizer / max(A.T @ y) - -x0 = np.zeros(n) -A, y = sample_data(m, n, gen_dic, gen_obs) -lmax = np.max(A.T @ y) -l1 = l1ratio * lmax -l2 = l2ratio * lmax - -params = ProjgradParams(verbosity=False, screen=True, relax=True) -x, mnt = projgrad(x0, A, y, l1, l2, params) -I_pos = np.sum(np.abs(x) > 1e-8) -I_null = np.sum(np.abs(x) <= 1e-8) - -print("-------------------") -print("Identification test\n") -print("aPGsr") -print(" Stop crit : {}".format(mnt.stopcrit)) -print(" Last Gap : {:.2e}".format(mnt.gap[-1])) -print(" Last Obj : {:.2e}".format(mnt.obj[-1])) -print(" Iters : {}".format(len(mnt.gap))) -print(" FLOPS : {:.2e}".format(mnt.flops[-1])) -print("Sparisty of the solution : {}/{} ({:.2%})".format(I_pos, n, I_pos/n)) - -fig, ax = plt.subplots(1,1,figsize=(4,3)) - -ax.plot(mnt.gap, mnt.sc_size/I_null, label="% Screened", marker='.') -ax.plot(mnt.gap, mnt.rc_size/I_pos, label="% Relaxed", marker='.') -ax.yaxis.set_major_formatter(lambda x,pos: r"{0:.0f}%".format(x * 100)) -ax.invert_xaxis() -ax.set_xlabel("GAP") -ax.set_ylabel("Indices identified") -ax.set_xscale('log') -ax.grid(b=True, which='major', axis='y') -ax.legend() -plt.tight_layout() -plt.show() - -print("-------------------") \ No newline at end of file -- GitLab