invexapi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. invexapi-0.1.0/.gitignore +20 -0
  2. invexapi-0.1.0/LICENSE +28 -0
  3. invexapi-0.1.0/PKG-INFO +178 -0
  4. invexapi-0.1.0/README.md +152 -0
  5. invexapi-0.1.0/environment.yml +15 -0
  6. invexapi-0.1.0/examples/_data.py +32 -0
  7. invexapi-0.1.0/examples/deblurring_log_fista.py +90 -0
  8. invexapi-0.1.0/examples/denoising_admm_tv.py +87 -0
  9. invexapi-0.1.0/examples/denoising_admm_tv_log.py +82 -0
  10. invexapi-0.1.0/examples/denoising_quasinorm.py +67 -0
  11. invexapi-0.1.0/imgs/logo.png +0 -0
  12. invexapi-0.1.0/invexapi/__init__.py +45 -0
  13. invexapi-0.1.0/invexapi/metadata.py +139 -0
  14. invexapi-0.1.0/invexapi/optim/__init__.py +7 -0
  15. invexapi-0.1.0/invexapi/optim/_certification.py +28 -0
  16. invexapi-0.1.0/invexapi/optim/_linesearch.py +28 -0
  17. invexapi-0.1.0/invexapi/optim/admm.py +137 -0
  18. invexapi-0.1.0/invexapi/optim/base.py +44 -0
  19. invexapi-0.1.0/invexapi/optim/conjugate_gradient.py +79 -0
  20. invexapi-0.1.0/invexapi/optim/fista.py +67 -0
  21. invexapi-0.1.0/invexapi/optim/gradient_descent.py +48 -0
  22. invexapi-0.1.0/invexapi/penalties/__init__.py +23 -0
  23. invexapi-0.1.0/invexapi/penalties/base.py +177 -0
  24. invexapi-0.1.0/invexapi/penalties/convex.py +62 -0
  25. invexapi-0.1.0/invexapi/penalties/l1.py +45 -0
  26. invexapi-0.1.0/invexapi/penalties/log.py +86 -0
  27. invexapi-0.1.0/invexapi/penalties/operators.py +100 -0
  28. invexapi-0.1.0/invexapi/penalties/quasinorm.py +134 -0
  29. invexapi-0.1.0/pyproject.toml +33 -0
  30. invexapi-0.1.0/tests/__init__.py +0 -0
  31. invexapi-0.1.0/tests/reference_numpy.py +73 -0
  32. invexapi-0.1.0/tests/test_admm.py +69 -0
  33. invexapi-0.1.0/tests/test_certificates.py +88 -0
  34. invexapi-0.1.0/tests/test_l1_penalty.py +30 -0
  35. invexapi-0.1.0/tests/test_log_penalty.py +57 -0
  36. invexapi-0.1.0/tests/test_metadata.py +48 -0
  37. invexapi-0.1.0/tests/test_operators.py +28 -0
  38. invexapi-0.1.0/tests/test_optimizers.py +92 -0
  39. invexapi-0.1.0/tests/test_quasinorm_penalty.py +67 -0
  40. invexapi-0.1.0/tests/test_solver_base.py +17 -0
@@ -0,0 +1,20 @@
1
+
2
+ # ---- Python bytecode ---------------------------------------------------------
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.pyo
6
+
7
+ # ---- IDE / editor artefacts --------------------------------------------------
8
+ .cache/
9
+ .vscode/
10
+ .claude/
11
+ .pytest_cache/
12
+ probes/
13
+ graphify-out/
14
+
15
+ # ---- Example data (fetched separately, see examples/data/download.py) --------
16
+ examples/data/
17
+
18
+ # ---- Miscellaneous -----------------------------------------------------------
19
+ CLAUDE.md
20
+ .gitignore
invexapi-0.1.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Samuel Pinilla
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.5
2
+ Name: invexapi
3
+ Version: 0.1.0
4
+ Summary: PyTorch Optimizers and Neural Network building blocks for Invex Results.
5
+ Project-URL: Homepage, https://github.com/samuelpinilla/invexapi
6
+ Author-email: Samuel Pinilla <spinilla@ieee.org>
7
+ License-Expression: BSD-3-Clause
8
+ License-File: LICENSE
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: License :: OSI Approved :: BSD License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: torch>=2.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: numpy; extra == 'dev'
17
+ Requires-Dist: pytest; extra == 'dev'
18
+ Provides-Extra: examples
19
+ Requires-Dist: imageio; extra == 'examples'
20
+ Requires-Dist: matplotlib; extra == 'examples'
21
+ Requires-Dist: numpy; extra == 'examples'
22
+ Requires-Dist: pillow; extra == 'examples'
23
+ Requires-Dist: pywavelets; extra == 'examples'
24
+ Requires-Dist: scipy; extra == 'examples'
25
+ Description-Content-Type: text/markdown
26
+
27
+ <p align="center">
28
+ <img src="imgs/logo.png" alt="InvexAPI" width="480">
29
+ </p>
30
+
31
+ <p align="center">
32
+ <a href="LICENSE"><img alt="License: BSD-3-Clause" src="https://img.shields.io/badge/license-BSD--3--Clause-blue.svg"></a>
33
+ <img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10%2B-blue.svg">
34
+ <img alt="PyTorch only" src="https://img.shields.io/badge/backend-PyTorch-ee4c2c.svg">
35
+ </p>
36
+
37
+ <h1 align="center">invexapi</h1>
38
+
39
+ <p align="center">
40
+ A minimal, PyTorch-only toolkit for <b>invex</b> optimization: penalties with
41
+ proven optimality certificates, and the solvers that use them.
42
+ </p>
43
+
44
+ ---
45
+
46
+ ## Why invexity?
47
+
48
+ Invex functions are a strict generalization of convexity: every stationary point
49
+ is still a global minimum, but the function itself need not be convex. That gives
50
+ non-convex penalties (sparsity-promoting, non-smooth, highly structured) the same
51
+ global-optimality guarantee convex optimization enjoys — without paying for it
52
+ with local minima.
53
+
54
+ `invexapi` packages this idea as code: penalties that carry a machine-checkable
55
+ **certificate** of which mathematical class they belong to (convex, invex,
56
+ quasi-convex, quasi-invex), and generic solvers that read those certificates to
57
+ warn you when no optimality guarantee applies.
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install -e . # editable install
63
+ pip install -e ".[dev]" # + pytest, numpy for the test suite
64
+ pip install -e ".[examples]" # + deps used only by examples/
65
+ ```
66
+
67
+ ## Quick start
68
+
69
+ ```python
70
+ import torch
71
+ from invexapi import QuasinormInvexPenalty
72
+ from invexapi.optim import FISTA
73
+
74
+ class DataFidelity:
75
+ def __init__(self, y): self.y = y
76
+ def value(self, x): return 0.5 * torch.sum((x - self.y) ** 2)
77
+ def grad(self, x): return x - self.y
78
+
79
+ y = torch.randn(100)
80
+ smooth = DataFidelity(y)
81
+ penalty = QuasinormInvexPenalty(lamb=0.1, q=0.5)
82
+ solver = FISTA(smooth, penalty, step=1.0)
83
+
84
+ x_hat, history = solver.run(y.clone())
85
+ ```
86
+
87
+ Any object exposing the right methods (`value`, `grad`, `prox`) works as a
88
+ `smooth`/`penalty`/`objective` — the built-in penalties are just one plug-in
89
+ choice.
90
+
91
+ ## What's inside
92
+
93
+ **Penalties** (`invexapi.penalties`) — loss/penalty terms plus a certificate of
94
+ what's provably known about each one:
95
+
96
+ | Penalty | Form | Certified as |
97
+ |---|---|---|
98
+ | `QuasinormInvexPenalty(lamb, q)` | `λ·\|x\|^q` | invex |
99
+ | `LogInvexPenalty(lamb)` | `log(1+\|x\|) − \|x\|/(2+2\|x\|)` | invex |
100
+ | `TikhonovPenalty(lamb)` | `λ/2·‖x‖²` | convex, invex, quasi-convex |
101
+ | `L1Penalty(lamb)` | `λ·‖x‖₁` | convex, invex, quasi-convex |
102
+
103
+ Certificates are attached explicitly and never inferred — convexity composes
104
+ additively, but invexity does not, so a combined objective only carries a
105
+ certificate someone has actually proven for it.
106
+
107
+ **Solvers** (`invexapi.optim`) — generic, decoupled from the penalties above:
108
+
109
+ - `GradientDescent` — with optional Armijo backtracking line search
110
+ - `FISTA` — accelerated proximal gradient for `smooth(x) + penalty(x)`
111
+ - `NonlinearCG` — Polak-Ribière+ with automatic restart
112
+ - `LinearizedADMM` — for `smooth(x) + penalty(D@x)` (e.g. total variation)
113
+
114
+ All four warn (never error) when run on an objective without a convex/invex
115
+ certificate, since no global-optimum guarantee applies in that case.
116
+
117
+ **Linear operators** (`invexapi.penalties.operators`) — `Identity` and
118
+ `FiniteDifference2D` (2D total variation), each with a verified adjoint.
119
+
120
+ **Structured documentation** (`invexapi.metadata`) — design provenance,
121
+ rejected alternatives, and invariants as introspectable dataclasses rather than
122
+ prose, exportable as JSON for downstream tooling:
123
+
124
+ ```python
125
+ import invexapi
126
+ print(invexapi.metadata.dump_all_json(indent=2))
127
+ ```
128
+
129
+ ## GPU support
130
+
131
+ Nothing in `invexapi` is device-specific — every operation derives its device
132
+ from its input tensors. Move your data to CUDA before calling a solver and
133
+ everything downstream follows:
134
+
135
+ ```python
136
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
137
+ x_hat, history = solver.run(y.to(device))
138
+ ```
139
+
140
+ ## Testing
141
+
142
+ ```bash
143
+ pytest
144
+ ```
145
+
146
+ Penalty math is validated against independent NumPy reference implementations on
147
+ fixed-seed random inputs, with no CUDA dependency anywhere in this repo.
148
+
149
+ ## Examples
150
+
151
+ See `examples/` for denoising and deblurring scripts covering FISTA and
152
+ Linearized ADMM with total variation, using both the quasinorm and log invex
153
+ penalties. The ADMM/TV examples need a test image, fetched separately:
154
+
155
+ ```bash
156
+ python examples/data/download.py
157
+ ```
158
+
159
+ ## License
160
+
161
+ BSD 3-Clause, see [LICENSE](LICENSE).
162
+
163
+ ## References
164
+
165
+ This library reproduces and generalizes results from the following papers:
166
+
167
+ 1. Pinilla, S., Sanabria, A., Bi, J., & Egiazarian, K. (2025). *What makes neural
168
+ networks trainable? Invexity as a structural design principle in AI*.
169
+ 2. Pinilla, S., & Thiyagalingam, J. (2024). *Global optimality for non-linear
170
+ constrained restoration problems via invexity*. International Conference on
171
+ Learning Representations (ICLR), 2024, pp. 11990–12027.
172
+ 3. Pinilla, S., Mu, T., Bourne, N., & Thiyagalingam, J. (2022). *Improved imaging
173
+ by invex regularizers with global optima guarantees*. Advances in Neural
174
+ Information Processing Systems (NeurIPS), 35, pp. 10780–10794.
175
+ 4. Pinilla, S., Yeung, S.-L., & Thiyagalingam, J. (2024). *Global convergence of
176
+ alternating direction method of multipliers for invex objective losses*. IEEE
177
+ International Conference on Acoustics, Speech and Signal Processing (ICASSP)
178
+ 2024, pp. 9361–9365.
@@ -0,0 +1,152 @@
1
+ <p align="center">
2
+ <img src="imgs/logo.png" alt="InvexAPI" width="480">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="LICENSE"><img alt="License: BSD-3-Clause" src="https://img.shields.io/badge/license-BSD--3--Clause-blue.svg"></a>
7
+ <img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10%2B-blue.svg">
8
+ <img alt="PyTorch only" src="https://img.shields.io/badge/backend-PyTorch-ee4c2c.svg">
9
+ </p>
10
+
11
+ <h1 align="center">invexapi</h1>
12
+
13
+ <p align="center">
14
+ A minimal, PyTorch-only toolkit for <b>invex</b> optimization: penalties with
15
+ proven optimality certificates, and the solvers that use them.
16
+ </p>
17
+
18
+ ---
19
+
20
+ ## Why invexity?
21
+
22
+ Invex functions are a strict generalization of convexity: every stationary point
23
+ is still a global minimum, but the function itself need not be convex. That gives
24
+ non-convex penalties (sparsity-promoting, non-smooth, highly structured) the same
25
+ global-optimality guarantee convex optimization enjoys — without paying for it
26
+ with local minima.
27
+
28
+ `invexapi` packages this idea as code: penalties that carry a machine-checkable
29
+ **certificate** of which mathematical class they belong to (convex, invex,
30
+ quasi-convex, quasi-invex), and generic solvers that read those certificates to
31
+ warn you when no optimality guarantee applies.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install -e . # editable install
37
+ pip install -e ".[dev]" # + pytest, numpy for the test suite
38
+ pip install -e ".[examples]" # + deps used only by examples/
39
+ ```
40
+
41
+ ## Quick start
42
+
43
+ ```python
44
+ import torch
45
+ from invexapi import QuasinormInvexPenalty
46
+ from invexapi.optim import FISTA
47
+
48
+ class DataFidelity:
49
+ def __init__(self, y): self.y = y
50
+ def value(self, x): return 0.5 * torch.sum((x - self.y) ** 2)
51
+ def grad(self, x): return x - self.y
52
+
53
+ y = torch.randn(100)
54
+ smooth = DataFidelity(y)
55
+ penalty = QuasinormInvexPenalty(lamb=0.1, q=0.5)
56
+ solver = FISTA(smooth, penalty, step=1.0)
57
+
58
+ x_hat, history = solver.run(y.clone())
59
+ ```
60
+
61
+ Any object exposing the right methods (`value`, `grad`, `prox`) works as a
62
+ `smooth`/`penalty`/`objective` — the built-in penalties are just one plug-in
63
+ choice.
64
+
65
+ ## What's inside
66
+
67
+ **Penalties** (`invexapi.penalties`) — loss/penalty terms plus a certificate of
68
+ what's provably known about each one:
69
+
70
+ | Penalty | Form | Certified as |
71
+ |---|---|---|
72
+ | `QuasinormInvexPenalty(lamb, q)` | `λ·\|x\|^q` | invex |
73
+ | `LogInvexPenalty(lamb)` | `log(1+\|x\|) − \|x\|/(2+2\|x\|)` | invex |
74
+ | `TikhonovPenalty(lamb)` | `λ/2·‖x‖²` | convex, invex, quasi-convex |
75
+ | `L1Penalty(lamb)` | `λ·‖x‖₁` | convex, invex, quasi-convex |
76
+
77
+ Certificates are attached explicitly and never inferred — convexity composes
78
+ additively, but invexity does not, so a combined objective only carries a
79
+ certificate someone has actually proven for it.
80
+
81
+ **Solvers** (`invexapi.optim`) — generic, decoupled from the penalties above:
82
+
83
+ - `GradientDescent` — with optional Armijo backtracking line search
84
+ - `FISTA` — accelerated proximal gradient for `smooth(x) + penalty(x)`
85
+ - `NonlinearCG` — Polak-Ribière+ with automatic restart
86
+ - `LinearizedADMM` — for `smooth(x) + penalty(D@x)` (e.g. total variation)
87
+
88
+ All four warn (never error) when run on an objective without a convex/invex
89
+ certificate, since no global-optimum guarantee applies in that case.
90
+
91
+ **Linear operators** (`invexapi.penalties.operators`) — `Identity` and
92
+ `FiniteDifference2D` (2D total variation), each with a verified adjoint.
93
+
94
+ **Structured documentation** (`invexapi.metadata`) — design provenance,
95
+ rejected alternatives, and invariants as introspectable dataclasses rather than
96
+ prose, exportable as JSON for downstream tooling:
97
+
98
+ ```python
99
+ import invexapi
100
+ print(invexapi.metadata.dump_all_json(indent=2))
101
+ ```
102
+
103
+ ## GPU support
104
+
105
+ Nothing in `invexapi` is device-specific — every operation derives its device
106
+ from its input tensors. Move your data to CUDA before calling a solver and
107
+ everything downstream follows:
108
+
109
+ ```python
110
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
111
+ x_hat, history = solver.run(y.to(device))
112
+ ```
113
+
114
+ ## Testing
115
+
116
+ ```bash
117
+ pytest
118
+ ```
119
+
120
+ Penalty math is validated against independent NumPy reference implementations on
121
+ fixed-seed random inputs, with no CUDA dependency anywhere in this repo.
122
+
123
+ ## Examples
124
+
125
+ See `examples/` for denoising and deblurring scripts covering FISTA and
126
+ Linearized ADMM with total variation, using both the quasinorm and log invex
127
+ penalties. The ADMM/TV examples need a test image, fetched separately:
128
+
129
+ ```bash
130
+ python examples/data/download.py
131
+ ```
132
+
133
+ ## License
134
+
135
+ BSD 3-Clause, see [LICENSE](LICENSE).
136
+
137
+ ## References
138
+
139
+ This library reproduces and generalizes results from the following papers:
140
+
141
+ 1. Pinilla, S., Sanabria, A., Bi, J., & Egiazarian, K. (2025). *What makes neural
142
+ networks trainable? Invexity as a structural design principle in AI*.
143
+ 2. Pinilla, S., & Thiyagalingam, J. (2024). *Global optimality for non-linear
144
+ constrained restoration problems via invexity*. International Conference on
145
+ Learning Representations (ICLR), 2024, pp. 11990–12027.
146
+ 3. Pinilla, S., Mu, T., Bourne, N., & Thiyagalingam, J. (2022). *Improved imaging
147
+ by invex regularizers with global optima guarantees*. Advances in Neural
148
+ Information Processing Systems (NeurIPS), 35, pp. 10780–10794.
149
+ 4. Pinilla, S., Yeung, S.-L., & Thiyagalingam, J. (2024). *Global convergence of
150
+ alternating direction method of multipliers for invex objective losses*. IEEE
151
+ International Conference on Acoustics, Speech and Signal Processing (ICASSP)
152
+ 2024, pp. 9361–9365.
@@ -0,0 +1,15 @@
1
+ name: invexapi-dev
2
+ channels:
3
+ - pytorch
4
+ - conda-forge
5
+ dependencies:
6
+ - python>=3.10
7
+ - pytorch>=2.0
8
+ - numpy
9
+ - scipy
10
+ - pywavelets
11
+ - imageio
12
+ - pillow
13
+ - matplotlib
14
+ - pytest
15
+ - pip
@@ -0,0 +1,32 @@
1
+ """Shared image-loading helper for the examples/ scripts that use real data.
2
+
3
+ Not part of the invexapi package — a plain sibling module the example scripts
4
+ import from each other, kept out of invexapi/ since loading/normalizing a test
5
+ image is an examples-only concern, not a library concern.
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+ import torch
12
+ from PIL import Image
13
+
14
+ DATA_DIR = Path(__file__).resolve().parent / "data"
15
+
16
+
17
+ def load_image(relative_path: str) -> torch.Tensor:
18
+ """Load an image under examples/data/ as a float tensor normalized to [0, 1].
19
+
20
+ ``relative_path`` is relative to examples/data/, e.g. ``"images/noisy_image.tif"``.
21
+ Normalization divides by the dtype's max value (e.g. 65535 for uint16) so the
22
+ result is comparable to the [0, 1]-scaled synthetic signals the other examples
23
+ use; if the file is already floating point, it's returned as-is.
24
+ """
25
+ path = DATA_DIR / relative_path
26
+ raw = np.array(Image.open(path))
27
+
28
+ if np.issubdtype(raw.dtype, np.integer):
29
+ factor = float(np.iinfo(raw.dtype).max)
30
+ raw = raw.astype(np.float32) / factor
31
+
32
+ return torch.from_numpy(raw.astype(np.float32))
@@ -0,0 +1,90 @@
1
+ """1D deblurring with the log invex penalty, via FISTA.
2
+
3
+ Adapted from the FISTA/momentum deblurring loop in
4
+ ``codes NEURIPS/model-based/modelEq10.py`` (DCT-domain deconvolution + invex prox on
5
+ wavelet coefficients), simplified to a synthetic 1D blur/sparse signal so the example
6
+ needs no external image files, ``pywt``, or ``scipy`` — just ``invexapi`` and
7
+ ``torch``. The structure (smooth data-fidelity gradient step + invex prox step, run
8
+ through FISTA) mirrors the paper's pipeline exactly; only the transform (identity here
9
+ vs. DCT+wavelet there) is simplified, so the log penalty's prox stays exact rather
10
+ than approximated.
11
+
12
+ Problem: recover a sparse signal ``x_true`` from a blurred, noisy observation
13
+ ``y = A x_true + noise`` (A = box blur), minimizing ``0.5*||Ax-y||^2 + lamb*g(x)``
14
+ where ``g`` is the log invex penalty from ``codes NEURIPS/model-based/modelEq10.py``.
15
+
16
+ Running this script still prints FISTA's UserWarning: the data-fidelity term alone
17
+ is certified convex, but the *combined* objective isn't (see
18
+ ``invexapi.penalties.Sum`` — certificates never compose automatically), so the
19
+ global-optimum guarantee is still withheld here even though half the objective is
20
+ provably well-behaved.
21
+ """
22
+
23
+ import torch
24
+
25
+ from invexapi import Certificate, Loss, LogInvexPenalty
26
+ from invexapi.optim import FISTA
27
+
28
+
29
+ def _blur_matrix(n: int, width: int = 5) -> torch.Tensor:
30
+ A = torch.zeros(n, n)
31
+ half = width // 2
32
+ for i in range(n):
33
+ lo, hi = max(0, i - half), min(n, i + half + 1)
34
+ A[i, lo:hi] = 1.0 / (hi - lo)
35
+ return A
36
+
37
+
38
+ class _DataFidelity(Loss):
39
+ """0.5*||Ax-y||^2. A@x is linear, so this quadratic is convex by inspection —
40
+ certified here (with no paper reference, since it's not from the source
41
+ papers) to show a Loss outside invexapi.penalties declaring its own certificate.
42
+ This does NOT silence FISTA's warning below: the *combined* smooth+penalty
43
+ objective (invexapi.penalties.Sum) still has no certificate of its own, by
44
+ design (see Sum's docstring) — only smooth's half is certified here."""
45
+
46
+ def __init__(self, A: torch.Tensor, y: torch.Tensor):
47
+ super().__init__()
48
+ self.A = A
49
+ self.y = y
50
+ self._certify("convex", Certificate(status="assumed", source="manual"))
51
+
52
+ def value(self, x: torch.Tensor) -> torch.Tensor:
53
+ r = self.A @ x - self.y
54
+ return 0.5 * torch.sum(r * r)
55
+
56
+ def grad(self, x: torch.Tensor) -> torch.Tensor:
57
+ return self.A.T @ (self.A @ x - self.y)
58
+
59
+
60
+ def main():
61
+ torch.manual_seed(0)
62
+ n = 200
63
+ sparsity = 20
64
+
65
+ x_true = torch.zeros(n)
66
+ idx = torch.randperm(n)[:sparsity]
67
+ x_true[idx] = torch.randn(sparsity) * 3.0
68
+
69
+ A = _blur_matrix(n)
70
+ noise = torch.randn(n) * 0.01
71
+ y = A @ x_true + noise
72
+
73
+ smooth = _DataFidelity(A, y)
74
+ penalty = LogInvexPenalty(lamb=5e-3)
75
+
76
+ L = torch.linalg.eigvalsh(A.T @ A).max().item()
77
+ solver = FISTA(smooth, penalty, step=1.0 / L, max_iter=300, tol=1e-10)
78
+
79
+ x_hat, history = solver.run(y.clone())
80
+
81
+ blurred_mse = torch.mean((y - x_true) ** 2).item()
82
+ deblurred_mse = torch.mean((x_hat - x_true) ** 2).item()
83
+
84
+ print(f"objective history: {history[0]:.4f} -> {history[-1]:.4f}")
85
+ print(f"MSE blurred: {blurred_mse:.4f}")
86
+ print(f"MSE deblurred: {deblurred_mse:.4f}")
87
+
88
+
89
+ if __name__ == "__main__":
90
+ main()
@@ -0,0 +1,87 @@
1
+ """Total-variation image denoising via LinearizedADMM, on a real test image.
2
+
3
+ Adapted from ``code_ICLR/ADMM/ADMM_Lq.py``: TV-regularized denoising
4
+ (``min_x 0.5*||x-y||^2 + lamb*sum(|Dx|^q)``) where ``D`` is the 2D finite-difference
5
+ (TV) operator and the penalty is the quasinorm invex penalty already implemented in
6
+ this library — same configuration as the reference script, run here with
7
+ ``LinearizedADMM`` + ``FiniteDifference2D`` + ``QuasinormInvexPenalty`` instead of
8
+ the reference's cupy/CUDA kernel, on ``examples/data/images/noisy_image.tif``
9
+ instead of the reference's own test image.
10
+
11
+ Running this script prints FISTA/GD/CG's usual UserWarning-style caveat: `smooth`
12
+ here (this example's own `_DataFidelity`) is uncertified, so LinearizedADMM warns
13
+ that no global-optimum guarantee applies — expected, same as the other examples.
14
+
15
+ Runs on CUDA automatically if available (``python examples/denoising_admm_tv.py``),
16
+ or force CPU with ``--device cpu``. Nothing in invexapi is device-specific — every
17
+ op inherits its device from its input tensors — so the only thing this script does
18
+ for GPU support is move the image tensor to the target device before construction;
19
+ everything downstream (the penalty's prox, the TV operator, the solver) follows.
20
+ """
21
+
22
+ import argparse
23
+ import time
24
+
25
+ import matplotlib.pyplot as plt
26
+ import torch
27
+
28
+ from invexapi import Loss, QuasinormInvexPenalty
29
+ from invexapi.optim import LinearizedADMM
30
+ from invexapi.penalties.operators import FiniteDifference2D
31
+
32
+ from _data import load_image
33
+
34
+
35
+ class _DataFidelity(Loss):
36
+ def __init__(self, y: torch.Tensor):
37
+ super().__init__()
38
+ self.y = y
39
+
40
+ def value(self, x: torch.Tensor) -> torch.Tensor:
41
+ return 0.5 * torch.sum((x - self.y) ** 2)
42
+
43
+ def grad(self, x: torch.Tensor) -> torch.Tensor:
44
+ return x - self.y
45
+
46
+
47
+ def main():
48
+ parser = argparse.ArgumentParser()
49
+ parser.add_argument(
50
+ "--device",
51
+ default="cuda" if torch.cuda.is_available() else "cpu",
52
+ help="torch device to run on, e.g. 'cuda', 'cuda:1', 'cpu' (default: cuda if available)",
53
+ )
54
+ args = parser.parse_args()
55
+ device = torch.device(args.device)
56
+
57
+ y = load_image("images/noisy_image.tif").to(device)
58
+
59
+ print(f"running on: {device}")
60
+
61
+ smooth = _DataFidelity(y)
62
+ penalty = QuasinormInvexPenalty(lamb=1.1e-1, q=0.85)
63
+ solver = LinearizedADMM(
64
+ smooth,
65
+ penalty,
66
+ D=FiniteDifference2D(),
67
+ rho=1.6,
68
+ project=lambda x: x.clamp(min=0.0),
69
+ max_iter=200,
70
+ tol=1e-8,
71
+ )
72
+
73
+ start_time = time.time()
74
+ x_hat, history = solver.run(y.clone())
75
+ end_time = time.time()
76
+
77
+ print(f"image shape: {tuple(y.shape)}")
78
+ print(f"objective history: {history[0]:.4f} -> {history[-1]:.4f}")
79
+ print(f"denoised range: [{x_hat.min().item():.4f}, {x_hat.max().item():.4f}]")
80
+ print(f"mean |x_hat - y|: {torch.mean((x_hat - y).abs()).item():.6f}")
81
+ print(f"elapsed time: {end_time - start_time:.4f} seconds")
82
+
83
+ plt.imshow(x_hat.cpu().numpy(), cmap="gray")
84
+ plt.show()
85
+
86
+ if __name__ == "__main__":
87
+ main()
@@ -0,0 +1,82 @@
1
+ """Total-variation image denoising via LinearizedADMM, using the log penalty.
2
+
3
+ Same setup as ``denoising_admm_tv.py`` (``min_x 0.5*||x-y||^2 + lamb*g(Dx)`` via
4
+ ``LinearizedADMM`` + ``FiniteDifference2D`` on
5
+ ``examples/data/images/noisy_image.tif``), but with ``g`` = ``LogInvexPenalty``
6
+ instead of ``QuasinormInvexPenalty`` — the reference ADMM scripts only pair TV with
7
+ the quasinorm/L1/Llq penalties, so there is no original CUDA kernel to validate
8
+ this specific combination against; it demonstrates that any `Penalty` (paper-sourced
9
+ or not) works as `LinearizedADMM`'s second argument, per its generic `penalty.prox`
10
+ contract.
11
+
12
+ Runs on CUDA automatically if available, or force CPU with ``--device cpu`` (see
13
+ ``denoising_admm_tv.py``'s docstring for why no other device-handling is needed).
14
+ """
15
+
16
+ import argparse
17
+ import time
18
+
19
+ import matplotlib.pyplot as plt
20
+ import torch
21
+
22
+ from invexapi import Loss, LogInvexPenalty
23
+ from invexapi.optim import LinearizedADMM
24
+ from invexapi.penalties.operators import FiniteDifference2D
25
+
26
+ from _data import load_image
27
+
28
+
29
+ class _DataFidelity(Loss):
30
+ def __init__(self, y: torch.Tensor):
31
+ super().__init__()
32
+ self.y = y
33
+
34
+ def value(self, x: torch.Tensor) -> torch.Tensor:
35
+ return 0.5 * torch.sum((x - self.y) ** 2)
36
+
37
+ def grad(self, x: torch.Tensor) -> torch.Tensor:
38
+ return x - self.y
39
+
40
+
41
+ def main():
42
+ parser = argparse.ArgumentParser()
43
+ parser.add_argument(
44
+ "--device",
45
+ default="cuda" if torch.cuda.is_available() else "cpu",
46
+ help="torch device to run on, e.g. 'cuda', 'cuda:1', 'cpu' (default: cuda if available)",
47
+ )
48
+ args = parser.parse_args()
49
+ device = torch.device(args.device)
50
+
51
+ y = load_image("images/noisy_image.tif").to(device)
52
+
53
+ print(f"running on: {device}")
54
+
55
+ smooth = _DataFidelity(y)
56
+ penalty = LogInvexPenalty(lamb=3e-1)
57
+ solver = LinearizedADMM(
58
+ smooth,
59
+ penalty,
60
+ D=FiniteDifference2D(),
61
+ rho=1.6,
62
+ project=lambda x: x.clamp(min=0.0),
63
+ max_iter=200,
64
+ tol=1e-8,
65
+ )
66
+
67
+ start_time = time.time()
68
+ x_hat, history = solver.run(y.clone())
69
+ end_time = time.time()
70
+
71
+ print(f"image shape: {tuple(y.shape)}")
72
+ print(f"objective history: {history[0]:.4f} -> {history[-1]:.4f}")
73
+ print(f"denoised range: [{x_hat.min().item():.4f}, {x_hat.max().item():.4f}]")
74
+ print(f"mean |x_hat - y|: {torch.mean((x_hat - y).abs()).item():.6f}")
75
+ print(f"elapsed time: {end_time - start_time:.4f} seconds")
76
+
77
+ plt.imshow(x_hat.cpu().numpy(), cmap="gray")
78
+ plt.show()
79
+
80
+
81
+ if __name__ == "__main__":
82
+ main()