invexapi 0.1.0__py3-none-any.whl

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.
invexapi/__init__.py ADDED
@@ -0,0 +1,45 @@
1
+ from . import metadata, optim, penalties
2
+ from .metadata import DesignDecision, Invariant, Provenance
3
+ from .optim import LinearizedADMM
4
+ from .penalties import (
5
+ Certificate,
6
+ FiniteDifference2D,
7
+ Identity,
8
+ L1Penalty,
9
+ LinearOperator,
10
+ Loss,
11
+ LogInvexPenalty,
12
+ ManualVerifier,
13
+ Penalty,
14
+ QuasinormInvexPenalty,
15
+ Reference,
16
+ Sum,
17
+ TikhonovPenalty,
18
+ Verifier,
19
+ )
20
+
21
+ __version__ = "0.1.0"
22
+
23
+ __all__ = [
24
+ "optim",
25
+ "penalties",
26
+ "metadata",
27
+ "Loss",
28
+ "Penalty",
29
+ "Certificate",
30
+ "Reference",
31
+ "Verifier",
32
+ "ManualVerifier",
33
+ "Sum",
34
+ "QuasinormInvexPenalty",
35
+ "LogInvexPenalty",
36
+ "TikhonovPenalty",
37
+ "L1Penalty",
38
+ "LinearOperator",
39
+ "Identity",
40
+ "FiniteDifference2D",
41
+ "LinearizedADMM",
42
+ "Provenance",
43
+ "DesignDecision",
44
+ "Invariant",
45
+ ]
invexapi/metadata.py ADDED
@@ -0,0 +1,139 @@
1
+ """Rich prose docstrings elsewhere in this library tend to tangle three different
2
+ kinds of information together: which source file/kernel an implementation was
3
+ transcribed from, why one design was chosen over a rejected alternative, and what
4
+ invariant must be preserved across future edits. This module gives each of those
5
+ its own small dataclass, plus a ``@documented`` decorator that attaches them to a
6
+ class/function as introspectable attributes and registers it so a tool can retrieve
7
+ every documented item as plain JSON via ``dump_all_json()`` without parsing docstrings
8
+ or importing this library at all (if it just wants a static snapshot generated once
9
+ and shipped elsewhere).
10
+
11
+ Deliberately plain stdlib dataclasses not something being validated as external input,
12
+ and invexapi's only required dependency is PyTorch.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from dataclasses import asdict, dataclass
19
+ from typing import Any, Callable, Optional, Sequence
20
+
21
+ __all__ = [
22
+ "Reference",
23
+ "Provenance",
24
+ "DesignDecision",
25
+ "Invariant",
26
+ "documented",
27
+ "dump_all",
28
+ "dump_all_json",
29
+ ]
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class Reference:
34
+ """A citation backing a mathematical or design claim."""
35
+
36
+ authors: str
37
+ title: str
38
+ venue: str
39
+ year: int
40
+ locator: str # e.g. "Lemma 1, item 5 (Eq. 10)", "Section 3.1.4"
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class Provenance:
45
+ """Which source file(s)/kernel(s) an implementation was transcribed from."""
46
+
47
+ files: tuple[str, ...]
48
+ description: str
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class DesignDecision:
53
+ """A choice made, an alternative rejected, and why.
54
+
55
+ ``reference`` is set when the decision is backed by a specific paper result
56
+ (e.g. "this prox formula because Theorem 3 proves it's the global optimizer"),
57
+ left ``None`` for plain engineering judgement calls.
58
+ """
59
+
60
+ choice: str
61
+ rejected: str
62
+ rationale: str
63
+ reference: Optional[Reference] = None
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class Invariant:
68
+ """A constraint that must be preserved across future edits."""
69
+
70
+ statement: str
71
+
72
+
73
+ _REGISTRY: list[type | Callable[..., Any]] = []
74
+
75
+
76
+ def documented(
77
+ *,
78
+ provenance: Sequence[Provenance] = (),
79
+ decisions: Sequence[DesignDecision] = (),
80
+ invariants: Sequence[Invariant] = (),
81
+ example: Optional[Callable[[], Any]] = None,
82
+ ):
83
+ """Class/function decorator attaching structured documentation.
84
+
85
+ ``provenance``/``decisions``/``invariants`` describe the decorated class or
86
+ function itself, not any particular instance, so they're stored as plain
87
+ attributes rather than requiring instantiation (unlike
88
+ :class:`invexapi.penalties.Certificate`, which genuinely can vary per instance).
89
+
90
+ ``example``, if given, is a zero-argument callable building a representative
91
+ instance purely so :func:`dump_all` can read its ``Certificate``s — only
92
+ meaningful for classes (e.g. concrete ``Penalty``s) whose certificates don't
93
+ depend on constructor arguments.
94
+ """
95
+
96
+ def wrap(obj):
97
+ obj.provenance = tuple(provenance)
98
+ obj.design_decisions = tuple(decisions)
99
+ obj.invariants = tuple(invariants)
100
+ obj._metadata_example = example
101
+ _REGISTRY.append(obj)
102
+ return obj
103
+
104
+ return wrap
105
+
106
+
107
+ def _certificates_for(obj) -> dict[str, Any]:
108
+ example = getattr(obj, "_metadata_example", None)
109
+ if example is None:
110
+ return {}
111
+ instance = example()
112
+ certs = {}
113
+ for name in ("convex", "invex", "quasi_convex", "quasi_invex"):
114
+ cert = getattr(instance, name, None)
115
+ if cert is not None:
116
+ certs[name] = asdict(cert)
117
+ return certs
118
+
119
+
120
+ def dump_all() -> list[dict[str, Any]]:
121
+ """Every ``@documented`` class/function as plain, JSON-serializable dicts."""
122
+ entries = []
123
+ for obj in _REGISTRY:
124
+ entries.append(
125
+ {
126
+ "name": obj.__qualname__,
127
+ "module": obj.__module__,
128
+ "provenance": [asdict(p) for p in obj.provenance],
129
+ "design_decisions": [asdict(d) for d in obj.design_decisions],
130
+ "invariants": [asdict(i) for i in obj.invariants],
131
+ "certificates": _certificates_for(obj),
132
+ }
133
+ )
134
+ return entries
135
+
136
+
137
+ def dump_all_json(**kwargs: Any) -> str:
138
+ """:func:`dump_all`, serialized to a JSON string."""
139
+ return json.dumps(dump_all(), **kwargs)
@@ -0,0 +1,7 @@
1
+ from .admm import LinearizedADMM
2
+ from .base import Solver
3
+ from .conjugate_gradient import NonlinearCG
4
+ from .fista import FISTA
5
+ from .gradient_descent import GradientDescent
6
+
7
+ __all__ = ["Solver", "GradientDescent", "FISTA", "NonlinearCG", "LinearizedADMM"]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import warnings
4
+
5
+ from ..metadata import DesignDecision, documented
6
+
7
+
8
+ @documented(
9
+ decisions=[
10
+ DesignDecision(
11
+ choice="duck-type obj via getattr(obj, ..., None), don't require isinstance(obj, Loss)",
12
+ rejected="requiring obj to be a Loss instance before checking certificates",
13
+ rationale=(
14
+ "optimizers stay duck-typed throughout this library; an object with "
15
+ "no invex/convex attributes at all is treated identically to a Loss "
16
+ "with those attributes set to None — no certificate either way"
17
+ ),
18
+ )
19
+ ],
20
+ )
21
+ def warn_if_unproven(obj, label: str) -> None:
22
+ """Warn if ``obj`` carries no certificate backing a global-optimum claim."""
23
+ if getattr(obj, "invex", None) is None and getattr(obj, "convex", None) is None:
24
+ warnings.warn(
25
+ f"{label} carries no convex/invex certificate — convergence to a "
26
+ "global optimum is not guaranteed.",
27
+ stacklevel=3,
28
+ )
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+
5
+
6
+ def backtracking(
7
+ value_fn,
8
+ x: torch.Tensor,
9
+ direction: torch.Tensor,
10
+ grad: torch.Tensor,
11
+ step0: float = 1.0,
12
+ shrink: float = 0.5,
13
+ c1: float = 1e-4,
14
+ max_backtracks: int = 50,
15
+ ) -> float:
16
+ """Armijo backtracking line search along ``direction`` from ``x``.
17
+
18
+ Returns a step size ``t`` such that ``value_fn(x + t*direction)`` gives
19
+ sufficient decrease, or the smallest tried step if none satisfies it.
20
+ """
21
+ f0 = value_fn(x)
22
+ slope = torch.sum(grad * direction)
23
+ t = step0
24
+ for _ in range(max_backtracks):
25
+ if value_fn(x + t * direction) <= f0 + c1 * t * slope:
26
+ return t
27
+ t *= shrink
28
+ return t
invexapi/optim/admm.py ADDED
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, Optional
4
+
5
+ import torch
6
+
7
+ from ..metadata import DesignDecision, Invariant, Provenance, documented
8
+ from ..penalties.operators import Identity, LinearOperator
9
+ from .base import Solver
10
+
11
+ __all__ = ["LinearizedADMM"]
12
+
13
+
14
+ @documented(
15
+ provenance=[
16
+ Provenance(
17
+ files=(
18
+ "code from conference paper Global Optimality for Nonlinear "
19
+ "Constrained Restoration Problems via Invexity",
20
+ ),
21
+ description=(
22
+ "TV-regularized denoising via ADMM with a linearized/relaxed "
23
+ "x-update (fixed alpha=0.5 extrapolation, fixed rho, no adaptive "
24
+ "scheme); z-update and dual update are standard exact ADMM"
25
+ ),
26
+ )
27
+ ],
28
+ decisions=[
29
+ DesignDecision(
30
+ choice="name the class LinearizedADMM, not ADMM",
31
+ rejected="calling it ADMM (unqualified)",
32
+ rationale=(
33
+ "the x-update is a single relaxed/extrapolated gradient step, not "
34
+ "an exact ADMM subproblem solve — Linearized ADMM is a real, "
35
+ "published variant name; calling it plain ADMM would imply a "
36
+ "stronger per-iteration guarantee than this implementation gives"
37
+ ),
38
+ ),
39
+ DesignDecision(
40
+ choice="warn on smooth and penalty separately, not via a combined Sum",
41
+ rejected="reusing invexapi.penalties.Sum like FISTA does",
42
+ rationale=(
43
+ "Sum assumes smooth and penalty share x's domain; here penalty "
44
+ "operates on D@x, generally a different shape (e.g. TV's D stacks "
45
+ "a vertical+horizontal gradient field), so no single combined "
46
+ "object can be constructed to certify"
47
+ ),
48
+ ),
49
+ ],
50
+ invariants=[
51
+ Invariant(
52
+ "the primal residual used for the stopping criterion and the dual "
53
+ "update must be computed from the NEWLY updated z, not the z from "
54
+ "before this iteration's prox step — using the old z is trivially "
55
+ "zero whenever x0 already minimizes smooth alone (e.g. x0=y for "
56
+ "0.5||x-y||^2), causing a spurious immediate 'convergence'."
57
+ )
58
+ ],
59
+ )
60
+ class LinearizedADMM(Solver):
61
+ """Linearized ADMM for ``min_x smooth(x) + penalty(D@x)``.
62
+
63
+ Named ``LinearizedADMM``, not ``ADMM``, because its x-update is a single
64
+ relaxed/extrapolated gradient step (transcribed exactly from the source ADMM
65
+ scripts), not an exact ADMM subproblem solve.
66
+
67
+ ``smooth`` needs ``.grad(x)`` (and optionally ``.value(x)``); ``penalty`` needs
68
+ ``.prox(z, step)`` (and optionally ``.value(z)``), operating on ``D@x``'s
69
+ domain, which may differ in shape from ``x`` (e.g. total-variation's ``D``
70
+ stacks a vertical and horizontal gradient field). ``D`` defaults to
71
+ :class:`~invexapi.penalties.operators.Identity`. ``project``, if given, is
72
+ applied to ``x`` after every update (e.g. ``lambda x: x.clamp(min=0)`` for
73
+ natural-image pixel constraints, matching the source scripts) — defaulting to
74
+ no projection, since forcing non-negativity isn't appropriate for a general
75
+ solver.
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ smooth,
81
+ penalty,
82
+ D: Optional[LinearOperator] = None,
83
+ rho: float = 1.0,
84
+ alpha: float = 0.5,
85
+ project: Optional[Callable[[torch.Tensor], torch.Tensor]] = None,
86
+ max_iter: int = 100,
87
+ tol: float = 1e-6,
88
+ ):
89
+ super().__init__(max_iter=max_iter, tol=tol)
90
+ self.smooth = smooth
91
+ self.penalty = penalty
92
+ self.D = D if D is not None else Identity()
93
+ self.rho = rho
94
+ self.alpha = alpha
95
+ self.project = project
96
+
97
+ def run(self, x0: torch.Tensor):
98
+ # Sum (used by FISTA) assumes smooth and penalty share x's domain; here
99
+ # penalty operates on D@x, generally a different shape, so there is no
100
+ # single combined object to check — warn on each half separately instead.
101
+ self._warn_if_unproven(self.smooth, "smooth")
102
+ self._warn_if_unproven(self.penalty, "penalty")
103
+
104
+ x = x0.clone()
105
+ x_relaxed = x0.clone()
106
+ z = self.D.apply(x0)
107
+ d = torch.zeros_like(z)
108
+ history = []
109
+
110
+ for _ in range(self.max_iter):
111
+ x1 = (1.0 - self.alpha) * x_relaxed + self.alpha * x
112
+ grad_coupling = self.rho * self.D.adjoint(self.D.apply(x1) - z + d)
113
+ grad_smooth = self.smooth.grad(x1)
114
+ x_new = x - (1.0 / (2.0 * self.rho)) * (grad_coupling + grad_smooth)
115
+ if self.project is not None:
116
+ x_new = self.project(x_new)
117
+
118
+ x_relaxed = (1.0 - self.alpha) * x_relaxed + self.alpha * x_new
119
+
120
+ Dx = self.D.apply(x_new)
121
+ z = self.penalty.prox(Dx + d / self.rho, 1.0 / self.rho)
122
+ primal_residual = Dx - z
123
+ d = d + self.rho * primal_residual
124
+
125
+ if hasattr(self.smooth, "value") and hasattr(self.penalty, "value"):
126
+ history.append((self.smooth.value(x_new) + self.penalty.value(Dx)).item())
127
+
128
+ x = x_new
129
+
130
+ # Standard ADMM stopping criterion: the primal residual Dx - z (not
131
+ # x's own movement, which can be zero for a degenerate iteration or
132
+ # two before the z/dual feedback has caught up, e.g. when x0 already
133
+ # minimizes smooth alone) must vanish.
134
+ if primal_residual.norm() < self.tol * max(Dx.norm(), 1.0):
135
+ break
136
+
137
+ return x_relaxed, history
invexapi/optim/base.py ADDED
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import List, Tuple
5
+
6
+ import torch
7
+
8
+ from ..metadata import DesignDecision, documented
9
+ from ._certification import warn_if_unproven
10
+
11
+ __all__ = ["Solver"]
12
+
13
+
14
+ @documented(
15
+ decisions=[
16
+ DesignDecision(
17
+ choice="standardize only max_iter/tol and the run(x0)->(x,history) contract",
18
+ rejected="a fully unified constructor signature across all solvers",
19
+ rationale=(
20
+ "GradientDescent/NonlinearCG take a single duck-typed objective "
21
+ "while FISTA takes smooth+penalty — a real shape difference, not "
22
+ "incidental duplication to force into one signature"
23
+ ),
24
+ )
25
+ ],
26
+ )
27
+ class Solver(ABC):
28
+ """Common shape for GradientDescent, FISTA, and NonlinearCG."""
29
+
30
+ def __init__(self, max_iter: int = 100, tol: float = 1e-6):
31
+ self.max_iter = max_iter
32
+ self.tol = tol
33
+
34
+ @abstractmethod
35
+ def run(self, x0: torch.Tensor) -> Tuple[torch.Tensor, List[float]]:
36
+ """Run the solver from ``x0``, returning ``(x_final, objective_history)``."""
37
+
38
+ def _warn_if_unproven(self, obj, label: str) -> None:
39
+ warn_if_unproven(obj, label)
40
+
41
+ def _record(self, history: List[float], obj, x: torch.Tensor) -> None:
42
+ """Append ``obj.value(x)`` to ``history`` if ``obj`` exposes ``value``."""
43
+ if hasattr(obj, "value"):
44
+ history.append(obj.value(x).item())
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import torch
6
+
7
+ from ..metadata import DesignDecision, Invariant, documented
8
+ from ._linesearch import backtracking
9
+ from .base import Solver
10
+
11
+ __all__ = ["NonlinearCG"]
12
+
13
+
14
+ @documented(
15
+ decisions=[
16
+ DesignDecision(
17
+ choice="Polak-Ribière+ (PR beta clamped to >= 0, i.e. automatic restart)",
18
+ rejected="plain Polak-Ribière or Fletcher-Reeves without restart",
19
+ rationale=(
20
+ "clamping the PR coefficient to be non-negative automatically "
21
+ "restarts the direction to steepest descent whenever the plain PR "
22
+ "formula would go negative, which is known to improve robustness "
23
+ "on non-quadratic (e.g. invex) objectives over unrestarted variants"
24
+ ),
25
+ )
26
+ ],
27
+ invariants=[
28
+ Invariant("beta_pr is always clamped to >= 0 before use as the restart coefficient.")
29
+ ],
30
+ )
31
+ class NonlinearCG(Solver):
32
+ """Nonlinear conjugate gradient (Polak-Ribière+, with restart) on any
33
+ objective exposing ``.grad(x)`` (and, for the default backtracking line
34
+ search, ``.value(x)``).
35
+
36
+ Suited to general (invex or otherwise) differentiable objectives, unlike
37
+ linear CG which only solves quadratic ``Ax=b`` systems.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ objective,
43
+ step: Optional[float] = None,
44
+ max_iter: int = 100,
45
+ tol: float = 1e-6,
46
+ ):
47
+ super().__init__(max_iter=max_iter, tol=tol)
48
+ if step is None and not hasattr(objective, "value"):
49
+ raise ValueError("objective must implement value(x) when step is None")
50
+ self.objective = objective
51
+ self.step = step
52
+
53
+ def run(self, x0: torch.Tensor):
54
+ self._warn_if_unproven(self.objective, "objective")
55
+ x = x0.clone()
56
+ history = []
57
+
58
+ grad = self.objective.grad(x)
59
+ direction = -grad
60
+
61
+ for _ in range(self.max_iter):
62
+ self._record(history, self.objective, x)
63
+ if grad.norm() < self.tol:
64
+ break
65
+
66
+ if self.step is not None:
67
+ t = self.step
68
+ else:
69
+ t = backtracking(self.objective.value, x, direction, grad)
70
+ x = x + t * direction
71
+
72
+ grad_new = self.objective.grad(x)
73
+ beta_pr = torch.sum(grad_new * (grad_new - grad)) / torch.sum(grad * grad).clamp_min(1e-12)
74
+ beta = torch.clamp(beta_pr, min=0.0) # Polak-Ribière+ restart
75
+
76
+ direction = -grad_new + beta * direction
77
+ grad = grad_new
78
+
79
+ return x, history
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+
5
+ import torch
6
+
7
+ from ..metadata import DesignDecision, documented
8
+ from ..penalties import Sum
9
+ from .base import Solver
10
+
11
+ __all__ = ["FISTA"]
12
+
13
+
14
+ @documented(
15
+ decisions=[
16
+ DesignDecision(
17
+ choice="warn/record off self.combined = Sum(smooth, penalty), not smooth/penalty individually",
18
+ rejected="checking smooth's and penalty's certificates separately",
19
+ rationale=(
20
+ "a global-optimum claim is about the COMBINED objective smooth+penalty "
21
+ "minimizes, and certificates don't compose from the parts (see "
22
+ "invexapi.penalties.Sum) — checking the parts separately could miss "
23
+ "that the combination itself is uncertified even when both halves are"
24
+ ),
25
+ )
26
+ ],
27
+ )
28
+ class FISTA(Solver):
29
+ """Accelerated proximal gradient (FISTA) for ``min_x smooth(x) + penalty(x)``.
30
+
31
+ ``smooth`` must implement ``.grad(x)`` (and optionally ``.value(x)`` for the
32
+ tracked history); ``penalty`` must implement ``.prox(x, step)`` (and optionally
33
+ ``.value(x)``). ``step`` should be <= 1/L where L is the Lipschitz constant of
34
+ ``smooth.grad``.
35
+ """
36
+
37
+ def __init__(self, smooth, penalty, step: float, max_iter: int = 100, tol: float = 1e-6):
38
+ super().__init__(max_iter=max_iter, tol=tol)
39
+ self.smooth = smooth
40
+ self.penalty = penalty
41
+ self.combined = Sum(smooth, penalty)
42
+ self.step = step
43
+
44
+ def run(self, x0: torch.Tensor):
45
+ self._warn_if_unproven(self.combined, "smooth+penalty")
46
+ x_prev = x0.clone()
47
+ y = x0.clone()
48
+ t_prev = 1.0
49
+ history = []
50
+
51
+ for _ in range(self.max_iter):
52
+ grad = self.smooth.grad(y)
53
+ x = self.penalty.prox(y - self.step * grad, self.step)
54
+
55
+ self._record(history, self.combined, x)
56
+
57
+ if (x - x_prev).norm() < self.tol * max(x_prev.norm(), 1.0):
58
+ x_prev = x
59
+ break
60
+
61
+ t = (1.0 + math.sqrt(1.0 + 4.0 * t_prev ** 2)) / 2.0
62
+ y = x + ((t_prev - 1.0) / t) * (x - x_prev)
63
+
64
+ x_prev = x
65
+ t_prev = t
66
+
67
+ return x_prev, history
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import torch
6
+
7
+ from ._linesearch import backtracking
8
+ from .base import Solver
9
+
10
+ __all__ = ["GradientDescent"]
11
+
12
+
13
+ class GradientDescent(Solver):
14
+ """Gradient descent on any objective exposing ``.grad(x)`` (and, for the
15
+ default backtracking line search, ``.value(x)``).
16
+
17
+ If ``step`` is given, a fixed step size is used; otherwise each iteration
18
+ picks its step via Armijo backtracking.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ objective,
24
+ step: Optional[float] = None,
25
+ max_iter: int = 100,
26
+ tol: float = 1e-6,
27
+ ):
28
+ super().__init__(max_iter=max_iter, tol=tol)
29
+ if step is None and not hasattr(objective, "value"):
30
+ raise ValueError("objective must implement value(x) when step is None")
31
+ self.objective = objective
32
+ self.step = step
33
+
34
+ def run(self, x0: torch.Tensor):
35
+ self._warn_if_unproven(self.objective, "objective")
36
+ x = x0.clone()
37
+ history = []
38
+ for _ in range(self.max_iter):
39
+ grad = self.objective.grad(x)
40
+ self._record(history, self.objective, x)
41
+ if grad.norm() < self.tol:
42
+ break
43
+ if self.step is not None:
44
+ t = self.step
45
+ else:
46
+ t = backtracking(self.objective.value, x, -grad, grad)
47
+ x = x - t * grad
48
+ return x, history
@@ -0,0 +1,23 @@
1
+ from .base import Certificate, Loss, ManualVerifier, Penalty, Reference, Sum, Verifier
2
+ from .convex import TikhonovPenalty
3
+ from .l1 import L1Penalty
4
+ from .log import LogInvexPenalty
5
+ from .operators import FiniteDifference2D, Identity, LinearOperator
6
+ from .quasinorm import QuasinormInvexPenalty
7
+
8
+ __all__ = [
9
+ "Loss",
10
+ "Penalty",
11
+ "Certificate",
12
+ "Reference",
13
+ "Verifier",
14
+ "ManualVerifier",
15
+ "Sum",
16
+ "QuasinormInvexPenalty",
17
+ "LogInvexPenalty",
18
+ "TikhonovPenalty",
19
+ "L1Penalty",
20
+ "LinearOperator",
21
+ "Identity",
22
+ "FiniteDifference2D",
23
+ ]