From 51119bfa04f0c3748f42c027c07eef4a3c30c37b Mon Sep 17 00:00:00 2001 From: mrava87 Date: Tue, 14 Apr 2026 20:57:57 +0100 Subject: [PATCH 01/73] feat: started to add class based primal solvers --- pyproximal/optimization/__init__.py | 3 +- pyproximal/optimization/basesolver.py | 136 +++++ pyproximal/optimization/cls_primal.py | 821 ++++++++++++++++++++++++++ 3 files changed, 959 insertions(+), 1 deletion(-) create mode 100644 pyproximal/optimization/basesolver.py create mode 100644 pyproximal/optimization/cls_primal.py diff --git a/pyproximal/optimization/__init__.py b/pyproximal/optimization/__init__.py index 694aac14..75052100 100644 --- a/pyproximal/optimization/__init__.py +++ b/pyproximal/optimization/__init__.py @@ -48,4 +48,5 @@ """ -from . import primal, primaldual, bregman, segmentation, sr3, palm, pnp +from . import cls_primal, primal +from . import primaldual, bregman, segmentation, sr3, palm, pnp diff --git a/pyproximal/optimization/basesolver.py b/pyproximal/optimization/basesolver.py new file mode 100644 index 00000000..d64d8395 --- /dev/null +++ b/pyproximal/optimization/basesolver.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +__all__ = ["Solver"] + +import time +from abc import ABCMeta, abstractmethod +from typing import TYPE_CHECKING, Any + +from pylops.optimization.basesolver import Solver as pSolver +from pylops.optimization.callback import Callbacks + +if TYPE_CHECKING: + from pyproximal.ProxOperator import ProxOperator + +_units = {"B": 1, "KB": 1024, "MB": 1024**2, "GB": 1024**3} + + +class Solver(pSolver, metaclass=ABCMeta): + r"""Solver + + This is a template class which a user must subclass when implementing a new solver. + This class comprises of the following mandatory methods: + + - ``__init__``: initialization method the solver object is created and + callbacks (if any present) are registered + - ``memory_usage``: a method to compute upfront the memory used by each + step of the solver + - ``setup``: a method that is invoked to setup the solver, basically it will create + anything required prior to applying a step of the solver + - ``step``: a method applying a single step of the solver + - ``run``: a method applying multiple steps of the solver + - ``finalize``: a method that is invoked at the end of the optimization process. It can + be used to do some final clean-up of the properties of the operator that we want + to expose to the user + - ``solve``: a method applying the entire optimization loop of the solver for a + certain number of steps + + and optional methods: + + - ``_print_solver``: a method print on screen details of the solver (already implemented) + - ``_print_setup``: a method print on screen details of the setup process + - ``_print_step``: a method print on screen details of each step + - ``_print_finalize``: a method print on screen details of the finalize process + - ``callback``: a method implementing a callback function, which is called after + every step of the solver + + Parameters + ---------- + callbacks : :obj:`pylops.optimization.callback.Callbacks` + Callbacks object used to implement custom callbacks + + Attributes + ---------- + iiter : :obj:`int` + Iteration counter. + tstart : :obj:`float` + Time at the start of the optimization process. + tend : :obj:`float` + Time at the end of the optimization process. Available + only after ``finalize`` is called. + telapsed : :obj:`float` + Total time elapsed during the optimization process. Available + only after ``finalize`` is called. + + """ + + def __init__( + self, + callbacks: Callbacks = None, + ) -> None: + self.callbacks = callbacks + self._registercallbacks() + self.iiter = 0 + self.tstart = time.time() + + def _print_solver(self, text: str = "", nbar: int = 80) -> None: + print(f"{type(self).__name__}" + text) + print("-" * nbar) + + def _print_finalize(self, *args: Any, nbar: int = 80, **kwargs: Any) -> None: + print( + f"\nIterations = {self.iiter} Total time (s) = {self.telapsed:.2f}" + ) + print("-" * nbar + "\n") + + @abstractmethod + def setup( + self, + proxf: ProxOperator | list[ProxOperator], + proxg: ProxOperator | None = None, + *args: Any, + show: bool = False, + **kwargs: Any, + ) -> None: + """Setup solver + + This method is used to setup the solver. Users can change the function signature + by including any other input parameter required during the setup stage + + Parameters + ---------- + proxf : :obj:`pyproximal."ProxOperator"` or :obj:`list` + Proximal operator(s) to be used in the optimization + proxg : :obj:`pyproximal."ProxOperator"`, optional + Proximal operator for the regularization term (if None, no regularization is used) + show : :obj:`bool`, optional + Display setup log + + """ + pass + + @abstractmethod + def solve( + self, + proxf: ProxOperator | list[ProxOperator], + proxg: ProxOperator | None = None, + *args, + show: bool = False, + **kwargs, + ) -> Any: + """Solve + + This method is used to run the entire optimization process. Users can change the + function signature by including any other input parameter required by the solver + + Parameters + ---------- + proxf : :obj:`pyproximal."ProxOperator"` or :obj:`list` + Proximal operator(s) to be used in the optimization + proxg : :obj:`pyproximal."ProxOperator"`, optional + Proximal operator for the regularization term (if None, no regularization is used) + show : :obj:`bool`, optional + Display finalize log + + """ + pass diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py new file mode 100644 index 00000000..be1b678f --- /dev/null +++ b/pyproximal/optimization/cls_primal.py @@ -0,0 +1,821 @@ +__all__ = [ + "ProximalPoint", +] + +import time +from typing import TYPE_CHECKING, Optional + +import numpy as np +from pylops.optimization.callback import _callback_stop +from pylops.utils.backend import get_array_module, to_numpy +from pylops.utils.typing import NDArray, Tmemunit + +from pyproximal.optimization.basesolver import Solver +from pyproximal.ProxOperator import ProxOperator +from pyproximal.utils.bilinear import BilinearOperator + +if TYPE_CHECKING: + from pylops.linearoperator import LinearOperator + + +def _backtracking( + x: NDArray, + tau: float, + proxf: ProxOperator, + proxg: ProxOperator, + epsg: float, + beta: float = 0.5, + niterback: int = 10, +) -> tuple[NDArray, float]: + r"""Backtracking + + Line-search algorithm for finding step sizes in proximal algorithms when + the Lipschitz constant of the operator is unknown (or expensive to + estimate). + + """ + + def ftilde(x: NDArray, y: NDArray, f: ProxOperator, tau: float) -> float: + xy = x - y + return float( + f(y) + np.dot(f.grad(y), xy) + (1.0 / (2.0 * tau)) * np.linalg.norm(xy) ** 2 + ) + + iiterback = 0 + while iiterback < niterback: + z = proxg.prox(x - tau * proxf.grad(x), epsg * tau) + ft = ftilde(z, x, proxf, tau) + if proxf(z) <= ft: + break + tau *= beta + iiterback += 1 + return z, tau + + +def _x0z0_init( + x0: NDArray | None, + z0: NDArray | None, + Op: Optional["LinearOperator"] = None, + z0name: str | None = "z0", + Opname: str | None = "Op", +) -> tuple[NDArray, NDArray]: + r"""Initialize x0 and z0 + + Initialize x0 and z0 using the following convention. + + For ``Op=None``: + - if both are provided, they are simply returned; + - if only one is provided (the other is ``None``), the one provided + is copied to the other one. + + For ``Op!=None``, ``x0`` must be provided, and: + - if both are provided, they are simply returned; + - if ``z0`` is not provided, set to ``Op @ x0``. + + Parameters + ---------- + x0 : :obj:`numpy.ndarray` + Initial vector + z0 : :obj:`numpy.ndarray` + Initial auxiliary vector + Op : :obj:`pylops.LinearOperator`, optional + Linear Operator to apply to ``x0`` + z0name : :obj:`str`, optional + Name to display in error message instead of ``z0`` + Opname : :obj:`str`, optional + Name to display in error message instead of ``Op`` + + """ + if x0 is None and z0 is None: + msg = f"Both x0 or {z0name} are None, provide either of them or both" + raise ValueError(msg) + + if Op is None: + if x0 is None: + x0 = z0.copy() # type: ignore[union-attr] + elif z0 is None: + z0 = x0.copy() + else: + if x0 is None: + msg = f"x0 must be provided when {Opname} is also provided" + raise ValueError(msg) + elif z0 is None: + z0 = Op @ x0 + return x0, z0 + + +class ProximalPoint(Solver): + r"""Proximal point algorithm + + Solves the following minimization problem using Proximal point algorithm: + + .. math:: + + \mathbf{x} = \argmin_\mathbf{x} f(\mathbf{x}) + + where :math:`f(\mathbf{x})` is any convex function that has a known + proximal operator. + + Notes + ----- + The Proximal point algorithm can be expressed by the following recursion: + + .. math:: + + \mathbf{x}^{k+1} = \prox_{\tau f}(\mathbf{x}^k) + + """ + + def _print_setup(self, xcomplex: bool = False) -> None: + self._print_solver(nbar=60) + + strpar = f"Proximal operator: {type(self.prox).__name__}" + if self.niter is not None: + strpar1 = f"tau = {self.tau:6e}\ttol = {self.tol:6e}\tniter = {self.niter}" + else: + strpar1 = f"tau = {self.tau:6e}\ttol = {self.tol:6e}" + print(strpar) + print(strpar1) + print("-" * 60 + "\n") + if not xcomplex: + head1 = " Itn x[0] f" + else: + head1 = " Itn x[0] f" + print(head1) + + def _print_step(self, x: NDArray) -> None: + if self.tol is None: + self.pf = self.prox(x) + strx = f"{x[0]:1.2e} " if np.iscomplexobj(x) else f"{x[0]:11.4e} " + msg = f"{self.iiter:6g} " + strx + f"{self.pf:11.4e}" + print(msg) + + def memory_usage( + self, + show: bool = False, + unit: Tmemunit = "B", + ) -> float: + pass + + def setup( + self, + prox: "ProxOperator", + x0: NDArray, + tau: float, + niter: int | None = None, + tol: float = 1e-4, + show: bool = False, + ) -> NDArray: + r"""Setup solver + + Parameters + ---------- + prox : :obj:`pyproximal."ProxOperator"` + Proximal operator + x0 : :obj:`numpy.ndarray` + Initial guess + tau : :obj:`float` + Positive scalar weight + niter : :obj:`int`, optional + Number of iterations (default to ``None`` in case a user wants to + manually step over the solver) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display setup log + + Returns + ------- + x : :obj:`numpy.ndarray` + Initial guess + + """ + self.prox = prox + self.x0 = x0 + self.tau = tau + self.niter = niter + self.tol = tol + + self.ncp = get_array_module(x0) + + # create variables to track the objective function and iterations + self.pf, self.pfold = np.inf, np.inf + self.pfs: list = [] + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(np.iscomplexobj(x0)) + return x0 + + def step(self, x: NDArray, show: bool = False) -> NDArray: + r"""Run one step of solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by a step of the + proximal point algorithm + show : :obj:`bool`, optional + Display iteration log + + Returns + ------- + x : :obj:`numpy.ndarray` + Updated model vector + + """ + x = self.prox.prox(x, self.tau) + + # tolerance check: break iterations if overall + # objective does not decrease below tolerance + if self.tol is not None: + self.pfold = self.pf + self.pf = self.prox(x) + if np.abs(1.0 - self.pf / self.pfold) < self.tol: + self.tolbreak = True + + self.iiter += 1 + if show: + self._print_step(x) + if self.tol is not None or show: + self.pfs.append(float(self.pf)) + return x + + def run( + self, + x: NDArray, + niter: int | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> NDArray: + r"""Run solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by multiple steps of + the proximal point algorithm + niter : :obj:`int`, optional + Number of iterations. Can be set to ``None`` if already + provided in the setup call + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + + """ + niter = self.niter if niter is None else niter + if niter is None: + msg = "`niter` must not be None" + raise ValueError(msg) + while self.iiter < niter and not self.tolbreak: + showstep = ( + True + if show + and ( + self.iiter < itershow[0] + or niter - self.iiter < itershow[1] + or self.iiter % itershow[2] == 0 + ) + else False + ) + x = self.step(x, showstep) + self.callback(x) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x + + def finalize(self, show: bool = False) -> None: + r"""Finalize solver + + Parameters + ---------- + show : :obj:`bool`, optional + Display finalize log + + """ + self.tend = time.time() + self.telapsed = self.tend - self.tstart + if show: + self._print_finalize(nbar=60) + + def solve( + self, + prox: "ProxOperator", + x0: NDArray, + tau: float, + niter: int = 10, + tol: float = 1e-4, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, int, NDArray]: + r"""Run entire solver + + Parameters + ---------- + prox : :obj:`pyproximal."ProxOperator"` + Proximal operator + x0 : :obj:`numpy.ndarray`, optional + Initial guess + tau : :obj:`float` + Positive scalar weight + niter : :obj:`int`, optional + Number of iterations + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping + criterion). If ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + iiter : :obj:`int` + Number of executed iterations + pfs : :obj:`numpy.ndarray` + History of the objective function + + """ + x = self.setup(prox=prox, x0=x0, tau=tau, niter=niter, tol=tol, show=show) + x = self.run(x, niter, show=show, itershow=itershow) + self.finalize(show) + return x, self.iiter, self.pfs + + +class ProximalGradient(Solver): + r"""Proximal gradient (optionally accelerated) + + Solves the following minimization problem using (Accelerated) Proximal + gradient algorithm: + + .. math:: + + \mathbf{x} = \argmin_\mathbf{x} f(\mathbf{x}) + \epsilon g(\mathbf{x}) + + where :math:`f(\mathbf{x})` is a smooth convex function with a uniquely + defined gradient and :math:`g(\mathbf{x})` is any convex function that + has a known proximal operator. + + Notes + ----- + The Proximal gradient algorithm can be expressed by the following recursion: + + .. math:: + + \mathbf{x}^{k+1} = \mathbf{y}^k + \eta (\prox_{\tau^k \epsilon g}(\mathbf{y}^k - + \tau^k \nabla f(\mathbf{y}^k)) - \mathbf{y}^k) \\ + \mathbf{y}^{k+1} = \mathbf{x}^k + \omega^k + (\mathbf{x}^k - \mathbf{x}^{k-1}) + + where at each iteration :math:`\tau^k` can be estimated by back-tracking + as follows: + + .. math:: + + \begin{aligned} + &\tau = \tau^{k-1} &\\ + &repeat \; \mathbf{z} = \prox_{\tau \epsilon g}(\mathbf{x}^k - + \tau \nabla f(\mathbf{x}^k)), \tau = \beta \tau \quad if \; + f(\mathbf{z}) \leq \tilde{f}_\tau(\mathbf{z}, \mathbf{x}^k) \\ + &\tau^k = \tau, \quad \mathbf{x}^{k+1} = \mathbf{z} &\\ + \end{aligned} + + where :math:`\tilde{f}_\tau(\mathbf{x}, \mathbf{y}) = f(\mathbf{y}) + + \nabla f(\mathbf{y})^T (\mathbf{x} - \mathbf{y}) + + 1/(2\tau)||\mathbf{x} - \mathbf{y}||_2^2`. + + Different accelerations are provided: + + - ``acceleration=None``: :math:`\omega^k = 0`; + - ``acceleration=vandenberghe`` [1]_: :math:`\omega^k = k / (k + 3)` for ` + - ``acceleration=fista``: :math:`\omega^k = (t_{k-1}-1)/t_k` where + :math:`t_k = (1 + \sqrt{1+4t_{k-1}^{2}}) / 2` [2]_ + + .. [1] Vandenberghe, L., "Fast proximal gradient methods", 2010. + .. [2] Beck, A., and Teboulle, M. "A Fast Iterative Shrinkage-Thresholding + Algorithm for Linear Inverse Problems", SIAM Journal on + Imaging Sciences, vol. 2, pp. 183-202. 2009. + + """ + + def _print_setup(self, epsg_print, xcomplex: bool = False) -> None: + self._print_solver(nbar=81) + + strpar = ( + f"Proximal operator (f): {type(self.proxf).__name__}\n" + f"Proximal operator (g): {type(self.proxg).__name__}\n" + ) + strpar1 = f"tau = {self.tau:4.2e}\t\tbacktrack = {self.backtracking}\tbeta = {self.beta}" + strpar2 = f"epsg = {epsg_print}\t\tniter = {self.niter}\t\ttol = {self.tol}" + strpar3 = f"niterback = {self.niterback}\t\tacceleration = {self.acceleration}" + print(strpar) + print(strpar1) + print(strpar2) + print(strpar3) + print("-" * 81 + "\n") + if not xcomplex: + head1 = " Itn x[0] f g J=f+eps*g tau" + else: + head1 = " Itn x[0] f g J=f+eps*g tau" + print(head1) + + def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: + if self.tol is None: + pf, pg = self.proxf(x), self.proxg(x) + self.pfg = pf + np.sum(self.epsg[self.iiter - 1] * pg) + x0 = to_numpy(x[0]) if x.ndim == 1 else to_numpy(x[0, 0]) + strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " + msg = ( + f"{self.iiter:6g} " + + strx + + f"{pf:10.3e} {pg:10.3e} {self.pfg:10.3e} {self.tau:11.2e}" + ) + print(msg) + + def memory_usage( + self, + show: bool = False, + unit: Tmemunit = "B", + ) -> float: + pass + + def setup( + self, + proxf: ProxOperator, + proxg: ProxOperator, + x0: NDArray, + epsg: float | NDArray = 1.0, + tau: float | None = None, + backtracking: bool = False, + beta: float = 0.5, + eta: float = 1.0, + niter: int = 10, + niterback: int = 100, + acceleration: str | None = None, + tol: float | None = None, + show: bool = False, + ) -> NDArray: + r"""Setup solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function (must have ``grad`` implemented) + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + x0 : :obj:`numpy.ndarray` + Initial vector + epsg : :obj:`float` or :obj:`numpy.ndarray`, optional + Scaling factor of g function + tau : :obj:`float` or :obj:`numpy.ndarray`, optional + Positive scalar weight, which should satisfy the following condition + to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is + the Lipschitz constant of :math:`\nabla f`. When ``tau=None``, + backtracking is used to adaptively estimate the best tau at each + iteration. Finally, note that :math:`\tau` can be chosen to be a vector + when dealing with problems with multiple right-hand-sides + backtracking : :obj:`bool`, optional + Force backtracking, even if ``tau`` is not equal to ``None``. In this case + the chosen ``tau`` will be used as the initial guess in the first + step of backtracking + beta : :obj:`float`, optional + Backtracking parameter (must be between 0 and 1) + eta : :obj:`float`, optional + Relaxation parameter (must be between 0 and 1, 0 excluded). + niter : :obj:`int`, optional + Number of iterations of iterative scheme + niterback : :obj:`int`, optional + Max number of iterations of backtracking + acceleration : :obj:`str`, optional + Acceleration (``None``, ``vandenberghe`` or ``fista``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display iterations log + + Returns + ------- + x : :obj:`numpy.ndarray` + Initial guess + + """ + self.proxf = proxf + self.proxg = proxg + self.x0 = x0 + self.backtracking = backtracking + self.beta = beta + self.eta = eta + self.niter = niter + self.niterback = niterback + self.tol = tol + + self.ncp = get_array_module(x0) + + # check if epgs is a vector + self.epsg = np.asarray(epsg, dtype=float) + if self.epsg.size == 1: + self.epsg = self.epsg * np.ones(niter) + epsg_print = str(self.epsg[0]) + else: + epsg_print = "Multi" + + # set tau + self.tau = tau + if tau is None: + self.backtracking = True + self.tau = 1.0 + + # check acceleration + if acceleration in [None, "None", "vandenberghe", "fista"]: + self.acceleration = acceleration + else: + msg = "Acceleration should be None, vandenberghe or fista" + raise NotImplementedError(msg) + + # set initial vectors + x = x0.copy() + y = x.copy() + + # for accelaration + self.t = 1.0 + + # create variables to track the objective function and iterations + self.pfg, self.pfgold = np.inf, np.inf + self.pfs: list = [] + self.pfgs: list = [] + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(epsg_print, np.iscomplexobj(x0)) + return x, y + + def step(self, x: NDArray, y: NDArray, show: bool = False) -> NDArray: + r"""Run one step of solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by a step of the + proximal gradient algorithm + y : :obj:`numpy.ndarray` + Additional model vector to be updated by a step of the + proximal gradient algorithm + show : :obj:`bool`, optional + Display iteration log + + Returns + ------- + x : :obj:`numpy.ndarray` + Updated model vector + + """ + xold = x.copy() + + # proximal step + if not self.backtracking: + if self.eta == 1.0: + x = self.proxg.prox( + y - self.tau * self.proxf.grad(y), self.epsg[self.iiter] * self.tau + ) + else: + x = x + self.eta * ( + self.proxg.prox( + x - self.tau * self.proxf.grad(x), + self.epsg[self.iiter] * self.tau, + ) + - x + ) + else: + x, self.tau = _backtracking( + y, + self.tau, + self.proxf, + self.proxg, + self.epsg[self.iiter], + beta=self.beta, + niterback=self.niterback, + ) + if self.eta != 1.0: + x = x + self.eta * ( + self.proxg.prox( + x - self.tau * self.proxf.grad(x), + self.epsg[self.iiter] * self.tau, + ) + - x + ) + + # update internal parameters for bilinear operator + if isinstance(self.proxf, BilinearOperator): + self.proxf.updatexy(x) + + # update y + if self.acceleration == "vandenberghe": + omega = self.iiter / (self.iiter + 3) + elif self.acceleration == "fista": + told = self.t + self.t = (1.0 + np.sqrt(1.0 + 4.0 * self.t**2)) / 2.0 + omega = (told - 1.0) / self.t + else: + omega = 0 + y = x + omega * (x - xold) + + # tolerance check: break iterations if overall + # objective does not decrease below tolerance + if self.tol is not None: + self.pfgold = self.pfg + pf, pg = self.proxf(x), self.proxg(x) + self.pfg = pf + np.sum(self.epsg[self.iiter] * pg) + if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + self.tolbreak = True + else: + pf, pg = None, None + + self.iiter += 1 + if show: + self._print_step(x, pf, pg) + if self.tol is not None or show: + self.pfgs.append(float(self.pfg)) + return x, y + + def run( + self, + x: NDArray, + y: NDArray, + niter: int | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> NDArray: + r"""Run solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by multiple steps of + the proximal gradient algorithm + y : :obj:`numpy.ndarray` + Additional model vector to be updated by multiple steps of + the proximal gradient algorithm + niter : :obj:`int`, optional + Number of iterations. Can be set to ``None`` if already + provided in the setup call + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + y : :obj:`numpy.ndarray` + Additional estimated model + + """ + niter = self.niter if niter is None else niter + if niter is None: + msg = "`niter` must not be None" + raise ValueError(msg) + while self.iiter < niter and not self.tolbreak: + showstep = ( + True + if show + and ( + self.iiter < itershow[0] + or niter - self.iiter < itershow[1] + or self.iiter % itershow[2] == 0 + ) + else False + ) + x, y = self.step(x, y, showstep) + self.callback(x) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x, y + + def finalize(self, show: bool = False) -> None: + r"""Finalize solver + + Parameters + ---------- + show : :obj:`bool`, optional + Display finalize log + + """ + self.tend = time.time() + self.telapsed = self.tend - self.tstart + if show: + self._print_finalize(nbar=81) + + def solve( + self, + proxf: ProxOperator, + proxg: ProxOperator, + x0: NDArray, + epsg: float | NDArray = 1.0, + tau: float | None = None, + backtracking: bool = False, + beta: float = 0.5, + eta: float = 1.0, + niter: int = 10, + niterback: int = 100, + acceleration: str | None = None, + tol: float | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, int, NDArray]: + r"""Run entire solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function (must have ``grad`` implemented) + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + x0 : :obj:`numpy.ndarray` + Initial vector + epsg : :obj:`float` or :obj:`numpy.ndarray`, optional + Scaling factor of g function + tau : :obj:`float` or :obj:`numpy.ndarray`, optional + Positive scalar weight, which should satisfy the following condition + to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is + the Lipschitz constant of :math:`\nabla f`. When ``tau=None``, + backtracking is used to adaptively estimate the best tau at each + iteration. Finally, note that :math:`\tau` can be chosen to be a vector + when dealing with problems with multiple right-hand-sides + backtracking : :obj:`bool`, optional + Force backtracking, even if ``tau`` is not equal to ``None``. In this case + the chosen ``tau`` will be used as the initial guess in the first + step of backtracking + beta : :obj:`float`, optional + Backtracking parameter (must be between 0 and 1) + eta : :obj:`float`, optional + Relaxation parameter (must be between 0 and 1, 0 excluded). + niter : :obj:`int`, optional + Number of iterations of iterative scheme + niterback : :obj:`int`, optional + Max number of iterations of backtracking + acceleration : :obj:`str`, optional + Acceleration (``None``, ``vandenberghe`` or ``fista``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping + criterion). If ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + iiter : :obj:`int` + Number of executed iterations + pfgs : :obj:`numpy.ndarray` + History of the objective function + + """ + x, y = self.setup( + proxf=proxf, + proxg=proxg, + x0=x0, + epsg=epsg, + tau=tau, + backtracking=backtracking, + beta=beta, + eta=eta, + niter=niter, + niterback=niterback, + acceleration=acceleration, + tol=tol, + show=show, + ) + + x, y = self.run(x, y, niter, show=show, itershow=itershow) + self.finalize(show) + return x, y, self.iiter, self.pfgs From 3605b1d74c9785cdb063fb39781fd8d7a9cf068c Mon Sep 17 00:00:00 2001 From: mrava87 Date: Tue, 14 Apr 2026 20:58:52 +0100 Subject: [PATCH 02/73] test: added tests for ProximalPoint --- pytests/test_solver.py | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/pytests/test_solver.py b/pytests/test_solver.py index 8480e76f..2c5b5dcc 100644 --- a/pytests/test_solver.py +++ b/pytests/test_solver.py @@ -15,8 +15,9 @@ GeneralizedProximalGradient, LinearizedADMM, ProximalGradient, + ProximalPoint, ) -from pyproximal.proximal import L1, L2 +from pyproximal.proximal import L1, L2, Quadratic par1 = {"n": 8, "m": 10, "dtype": "float32"} # float64 par2 = {"n": 8, "m": 10, "dtype": "float64"} # float32 @@ -137,6 +138,29 @@ def test_GPG_weights(par): ) +@pytest.mark.parametrize("par", [(par1), (par2)]) +def test_ProximalPoint(par): + """Check solution of ProximalPoint for quadratic function equals the solution of the + associated system of linear equations + """ + np.random.seed(10) + m = par["m"] + + # Random mixing matrix + A = np.random.normal(0.0, 1.0, (m, m)) + A = A.T @ A + + # Model and data + x = np.linspace(-5.0, 5.0, par["m"]) + y = A @ x + + # Proximal point algorithm with quadatic function + quad = Quadratic(Op=MatrixMult(A), b=-y, niter=2) + xpp = ProximalPoint(quad, x0=np.zeros_like(x), tau=0.1, niter=1000, tol=0) + + assert_array_almost_equal(xpp, x, decimal=2) + + @pytest.mark.parametrize("par", [(par1), (par2)]) def test_PG_GPG(par): """Check equivalency of ProximalGradient and GeneralizedProximalGradient when using @@ -210,20 +234,20 @@ def test_ADMM_DRS(par): # ADMM l2 = L2(Op=Rop, b=y, niter=10, warm=True) l1 = L1(sigma=5e-1) - xadmm, zadmm = ADMM(l2, l1, x0=np.zeros(m), tau=tau, niter=100, show=True) + xadmm, zadmm = ADMM(l2, l1, x0=np.zeros(m), tau=tau, niter=100) # DRS with g first l2 = L2(Op=Rop, b=y, niter=10, warm=True) l1 = L1(sigma=5e-1) xdrs_g, ydrs_g = DouglasRachfordSplitting( - l2, l1, x0=np.zeros(m), tau=tau, niter=100, show=True, gfirst=True + l2, l1, x0=np.zeros(m), tau=tau, niter=100, gfirst=True ) # DRS with f first l2 = L2(Op=Rop, b=y, niter=10, warm=True) l1 = L1(sigma=5e-1) xdrs_f, ydrs_f = DouglasRachfordSplitting( - l2, l1, x0=np.zeros(m), tau=tau, niter=100, show=True, gfirst=False + l2, l1, x0=np.zeros(m), tau=tau, niter=100, gfirst=False ) assert_array_almost_equal(xadmm, xdrs_g, decimal=2) @@ -262,13 +286,11 @@ def test_PPXA_with_ADMM(par: dict[str, Any]) -> None: x0=np.zeros(m), tau=tau, niter=2000, # niter=1500 makes this test fail for seeds 0 to 499 - show=True, ) xppxa = PPXA( [l2, l1], x0=np.zeros(m), tau=np.random.uniform(3 * tau, 5 * tau), - show=True, ) assert_array_almost_equal(xppxa, xadmm, decimal=2) @@ -312,13 +334,11 @@ def test_PPXA_with_GPG(par: dict[str, Any]) -> None: x0=np.zeros(m), tau=tau, niter=200, # niter=150 makes this test fail for seeds 0 to 499 - show=True, ) xppxa = PPXA( [l2_1, l2_2, l1_1, l1_2], x0=np.zeros(m), tau=np.random.uniform(3 * tau, 5 * tau), - show=True, ) assert_array_almost_equal(xppxa, xgpg, decimal=2) @@ -356,13 +376,11 @@ def test_ConsensusADMM_with_ADMM(par: dict[str, Any]) -> None: x0=np.zeros(m), tau=tau, niter=2000, # niter=1500 makes this test fail for seeds 0 to 499 - show=True, ) xcadmm = ConsensusADMM( [l2, l1], x0=np.random.normal(0.0, 1.0, m), # x0=np.zeros(m), tau=np.random.uniform(3 * tau, 5 * tau), - show=True, ) assert_array_almost_equal(xcadmm, xadmm, decimal=2) @@ -416,7 +434,6 @@ def test_ConsensusADMM_with_ADMM_for_Lasso(par: dict[str, Any]) -> None: x0=np.random.normal(0.0, 1.0, m), # x0=np.zeros(m), tau=np.random.uniform(3 * tau, 5 * tau), niter=20000, # niter=15000 makes this test fail for seeds 0 to 499 - show=True, ) # 1/2 || [R1; R2; R3] ||_2^2 + ||x||_1 @@ -426,7 +443,6 @@ def test_ConsensusADMM_with_ADMM_for_Lasso(par: dict[str, Any]) -> None: x0=np.zeros(m), tau=tau, niter=15000, # niter=10000 makes this test fail for seeds 0 to 499 - show=True, ) assert_array_almost_equal(xcadmm, xadmm, decimal=2) @@ -471,13 +487,11 @@ def test_ConsensusADMM_with_GPG(par: dict[str, Any]) -> None: x0=np.zeros(m), tau=tau, niter=200, # niter=150 makes this test fail for seeds 0 to 499 - show=True, ) xppxa = ConsensusADMM( [l2_1, l2_2, l1_1, l1_2], x0=np.zeros(m), tau=np.random.uniform(3 * tau, 5 * tau), - show=True, ) assert_array_almost_equal(xppxa, xgpg, decimal=2) From 057265c4228b130ce925930e0f7e460d89859a04 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 15 Apr 2026 21:34:12 +0100 Subject: [PATCH 03/73] feat: added class based AndersonProximalGradient (and fix mypy errors) --- pyproximal/optimization/basesolver.py | 6 +- pyproximal/optimization/cls_primal.py | 484 ++++++++++++++++++++++++-- 2 files changed, 467 insertions(+), 23 deletions(-) diff --git a/pyproximal/optimization/basesolver.py b/pyproximal/optimization/basesolver.py index d64d8395..222979af 100644 --- a/pyproximal/optimization/basesolver.py +++ b/pyproximal/optimization/basesolver.py @@ -15,7 +15,7 @@ _units = {"B": 1, "KB": 1024, "MB": 1024**2, "GB": 1024**3} -class Solver(pSolver, metaclass=ABCMeta): +class Solver(pSolver, metaclass=ABCMeta): # type: ignore[misc] r"""Solver This is a template class which a user must subclass when implementing a new solver. @@ -114,9 +114,9 @@ def solve( self, proxf: ProxOperator | list[ProxOperator], proxg: ProxOperator | None = None, - *args, + *args: Any, show: bool = False, - **kwargs, + **kwargs: Any, ) -> Any: """Solve diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index be1b678f..5a125e65 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -3,7 +3,7 @@ ] import time -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, cast import numpy as np from pylops.optimization.callback import _callback_stop @@ -126,14 +126,18 @@ class ProximalPoint(Solver): """ + pf: float + def _print_setup(self, xcomplex: bool = False) -> None: self._print_solver(nbar=60) strpar = f"Proximal operator: {type(self.prox).__name__}" if self.niter is not None: - strpar1 = f"tau = {self.tau:6e}\ttol = {self.tol:6e}\tniter = {self.niter}" + strpar1 = ( + f"tau = {self.tau:6e}\ttol = {str(self.tol)}\tniter = {self.niter}" + ) else: - strpar1 = f"tau = {self.tau:6e}\ttol = {self.tol:6e}" + strpar1 = f"tau = {self.tau:6e}\ttol = {str(self.tol)}" print(strpar) print(strpar1) print("-" * 60 + "\n") @@ -154,10 +158,10 @@ def memory_usage( self, show: bool = False, unit: Tmemunit = "B", - ) -> float: + ) -> None: pass - def setup( + def setup( # type: ignore[override] self, prox: "ProxOperator", x0: NDArray, @@ -201,7 +205,7 @@ def setup( # create variables to track the objective function and iterations self.pf, self.pfold = np.inf, np.inf - self.pfs: list = [] + self.pfs: list[float] = [] self.tolbreak = False self.iiter = 0 @@ -311,7 +315,7 @@ def finalize(self, show: bool = False) -> None: if show: self._print_finalize(nbar=60) - def solve( + def solve( # type: ignore[override] self, prox: "ProxOperator", x0: NDArray, @@ -415,7 +419,7 @@ class ProximalGradient(Solver): """ - def _print_setup(self, epsg_print, xcomplex: bool = False) -> None: + def _print_setup(self, epsg_print: str, xcomplex: bool = False) -> None: self._print_solver(nbar=81) strpar = ( @@ -423,7 +427,9 @@ def _print_setup(self, epsg_print, xcomplex: bool = False) -> None: f"Proximal operator (g): {type(self.proxg).__name__}\n" ) strpar1 = f"tau = {self.tau:4.2e}\t\tbacktrack = {self.backtracking}\tbeta = {self.beta}" - strpar2 = f"epsg = {epsg_print}\t\tniter = {self.niter}\t\ttol = {self.tol}" + strpar2 = ( + f"epsg = {epsg_print}\t\tniter = {self.niter}\t\ttol = {str(self.tol)}" + ) strpar3 = f"niterback = {self.niterback}\t\tacceleration = {self.acceleration}" print(strpar) print(strpar1) @@ -453,10 +459,10 @@ def memory_usage( self, show: bool = False, unit: Tmemunit = "B", - ) -> float: + ) -> None: pass - def setup( + def setup( # type: ignore[override] self, proxf: ProxOperator, proxg: ProxOperator, @@ -471,7 +477,7 @@ def setup( acceleration: str | None = None, tol: float | None = None, show: bool = False, - ) -> NDArray: + ) -> tuple[NDArray, NDArray]: r"""Setup solver Parameters @@ -515,6 +521,8 @@ def setup( ------- x : :obj:`numpy.ndarray` Initial guess + y : :obj:`numpy.ndarray` + Initial guess for the auxiliary variable """ self.proxf = proxf @@ -559,8 +567,8 @@ def setup( # create variables to track the objective function and iterations self.pfg, self.pfgold = np.inf, np.inf - self.pfs: list = [] - self.pfgs: list = [] + self.pfs: list[float] = [] + self.pfgs: list[float] = [] self.tolbreak = False self.iiter = 0 @@ -569,7 +577,9 @@ def setup( self._print_setup(epsg_print, np.iscomplexobj(x0)) return x, y - def step(self, x: NDArray, y: NDArray, show: bool = False) -> NDArray: + def step( + self, x: NDArray, y: NDArray, show: bool = False + ) -> tuple[NDArray, NDArray]: r"""Run one step of solver Parameters @@ -587,6 +597,8 @@ def step(self, x: NDArray, y: NDArray, show: bool = False) -> NDArray: ------- x : :obj:`numpy.ndarray` Updated model vector + y : :obj:`numpy.ndarray` + Updated additional model vector """ xold = x.copy() @@ -608,7 +620,7 @@ def step(self, x: NDArray, y: NDArray, show: bool = False) -> NDArray: else: x, self.tau = _backtracking( y, - self.tau, + cast(float, self.tau), self.proxf, self.proxg, self.epsg[self.iiter], @@ -648,7 +660,7 @@ def step(self, x: NDArray, y: NDArray, show: bool = False) -> NDArray: if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: self.tolbreak = True else: - pf, pg = None, None + pf, pg = 0.0, 0.0 self.iiter += 1 if show: @@ -664,7 +676,7 @@ def run( niter: int | None = None, show: bool = False, itershow: tuple[int, int, int] = (10, 10, 10), - ) -> NDArray: + ) -> tuple[NDArray, NDArray]: r"""Run solver Parameters @@ -730,7 +742,7 @@ def finalize(self, show: bool = False) -> None: if show: self._print_finalize(nbar=81) - def solve( + def solve( # type: ignore[override] self, proxf: ProxOperator, proxg: ProxOperator, @@ -746,7 +758,7 @@ def solve( tol: float | None = None, show: bool = False, itershow: tuple[int, int, int] = (10, 10, 10), - ) -> tuple[NDArray, int, NDArray]: + ) -> tuple[NDArray, NDArray, int, NDArray]: r"""Run entire solver Parameters @@ -794,6 +806,8 @@ def solve( ------- x : :obj:`numpy.ndarray` Estimated model + y : :obj:`numpy.ndarray` + Additional estimated model iiter : :obj:`int` Number of executed iterations pfgs : :obj:`numpy.ndarray` @@ -819,3 +833,433 @@ def solve( x, y = self.run(x, y, niter, show=show, itershow=itershow) self.finalize(show) return x, y, self.iiter, self.pfgs + + +class AndersonProximalGradient(Solver): + r"""Proximal gradient with Anderson acceleration + + Solves the following minimization problem using the Proximal + gradient algorithm with Anderson acceleration: + + .. math:: + + \mathbf{x} = \argmin_\mathbf{x} f(\mathbf{x}) + \epsilon g(\mathbf{x}) + + where :math:`f(\mathbf{x})` is a smooth convex function with a uniquely + defined gradient and :math:`g(\mathbf{x})` is any convex function that + has a known proximal operator. + + Notes + ----- + The Proximal gradient algorithm with Anderson acceleration can be expressed by the + following recursion [1]_: + + .. math:: + m_k = min(m, k)\\ + \mathbf{g}^{k} = \mathbf{x}^{k} - \tau^k \nabla f(\mathbf{x}^k)\\ + \mathbf{r}^{k} = \mathbf{g}^{k} - \mathbf{g}^{k}\\ + \mathbf{G}^{k} = [\mathbf{g}^{k},..., \mathbf{g}^{k-m_k}]\\ + \mathbf{R}^{k} = [\mathbf{r}^{k},..., \mathbf{r}^{k-m_k}]\\ + \alpha_k = (\mathbf{R}^{kT} \mathbf{R}^{k})^{-1} \mathbf{1} / \mathbf{1}^T + (\mathbf{R}^{kT} \mathbf{R}^{k})^{-1} \mathbf{1}\\ + \mathbf{y}^{k+1} = \mathbf{G}^{k} \alpha_k\\ + \mathbf{x}^{k+1} = \prox_{\tau^{k+1} g}(\mathbf{y}^{k+1}) + + where :math:`m` equals ``nhistory``, :math:`k=1,2,...,n_{iter}`, :math:`\mathbf{y}^{0}=\mathbf{x}^{0}`, + :math:`\mathbf{y}^{1}=\mathbf{x}^{0} - \tau^0 \nabla f(\mathbf{x}^0)`, + :math:`\mathbf{x}^{1}=\prox_{\tau^k g}(\mathbf{y}^{1})`, and + :math:`\mathbf{g}^{0}=\mathbf{y}^{1}`. + + Refer to [1]_ for the guarded version of the algorithm (when ``safeguard=True``). + + .. [1] Mai, V., and Johansson, M. "Anderson Acceleration of Proximal Gradient + Methods", 2020. + + """ + + def _print_setup(self, epsg_print: str, xcomplex: bool = False) -> None: + self._print_solver(nbar=81) + + strpar = ( + f"Proximal operator (f): {type(self.proxf).__name__}\n" + f"Proximal operator (g): {type(self.proxg).__name__}\n" + ) + strpar1 = ( + f"tau = {self.tau:4.2e}\t\tepsg = {epsg_print}\t\tniter = {self.niter}" + ) + strpar2 = f"nhist = {self.nhistory}\t\tepsr = {self.epsr:4.2e}" + strpar3 = f"guard = {str(self.safeguard)}\t\ttol = {str(self.tol)}" + print(strpar) + print(strpar1) + print(strpar2) + print(strpar3) + print("-" * 81 + "\n") + if not xcomplex: + head1 = " Itn x[0] f g J=f+eps*g tau" + else: + head1 = " Itn x[0] f g J=f+eps*g tau" + print(head1) + + def _print_step(self, x: NDArray, pg: float | None) -> None: + if self.tol is None: + self.pf, pg = self.proxf(x), self.proxg(x) + self.pfg = self.pf + np.sum(self.epsg[self.iiter - 1] * pg) + x0 = to_numpy(x[0]) if x.ndim == 1 else to_numpy(x[0, 0]) + strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " + msg = ( + f"{self.iiter:6g} " + + strx + + f"{self.pf:10.3e} {pg:10.3e} {self.pfg:10.3e} {self.tau:11.2e}" + ) + print(msg) + + def memory_usage( + self, + show: bool = False, + unit: Tmemunit = "B", + ) -> None: + pass + + def setup( # type: ignore[override] + self, + proxf: ProxOperator, + proxg: ProxOperator, + x0: NDArray, + epsg: float | NDArray = 1.0, + tau: float | NDArray = 1.0, + niter: int = 10, + nhistory: int = 10, + epsr: float = 1e-10, + safeguard: bool = False, + tol: float | None = None, + show: bool = False, + ) -> tuple[NDArray, NDArray]: + r"""Setup solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function (must have ``grad`` implemented) + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + x0 : :obj:`numpy.ndarray` + Initial vector + epsg : :obj:`float` or :obj:`numpy.ndarray`, optional + Scaling factor of g function + tau : :obj:`float` or :obj:`numpy.ndarray`, optional + Positive scalar weight, which should satisfy the following condition + to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is + the Lipschitz constant of :math:`\nabla f`. N ote that :math:`\tau` + can be chosen to be a vector when dealing with problems with + multiple right-hand-sides + niter : :obj:`int`, optional + Number of iterations of iterative scheme + nhistory : :obj:`int`, optional + Number of previous iterates to be kept in memory (to compute the scaling factors) + epsr : :obj:`float`, optional + Scaling factor for regularization added to the inverse of :math:\mathbf{R}^T \mathbf{R}` + safeguard : :obj:`bool`, optional + Apply safeguarding strategy to the update (``True``) or not (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display iterations log + + Returns + ------- + x : :obj:`numpy.ndarray` + Initial guess + + """ + self.proxf = proxf + self.proxg = proxg + self.x0 = x0 + self.tau = tau + self.niter = niter + self.nhistory = nhistory + self.epsr = epsr + self.safeguard = safeguard + self.tol = tol + + self.ncp = get_array_module(x0) + + # check if epgs is a vector + self.epsg = np.asarray(epsg, dtype=float) + if self.epsg.size == 1: + self.epsg = self.epsg * np.ones(niter) + epsg_print = str(self.epsg[0]) + else: + epsg_print = "Multi" + + # set initial vectors + y = x0 - self.tau * proxf.grad(x0) + x = self.proxg.prox(y, self.epsg[0] * self.tau) + + # set history of iterates for Anderson acceleration + g = y.copy() + r = g - x0 + self.R, self.G = ( + [ + g, + ], + [ + r, + ], + ) + + # create variables to track the objective function and iterations + self.pf = self.proxf(x) + print("Initial pf: %10.3e" % self.pf) + self.pfg, self.pfgold = np.inf, np.inf + self.pfs: list[float] = [] + self.pfgs: list[float] = [] + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(epsg_print, np.iscomplexobj(x0)) + return x, y + + def step( + self, x: NDArray, y: NDArray, show: bool = False + ) -> tuple[NDArray, NDArray]: + r"""Run one step of solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by a step of the + proximal gradient algorithm + y : :obj:`numpy.ndarray` + Additional model vector to be updated by a step of the + proximal gradient algorithm + show : :obj:`bool`, optional + Display iteration log + + Returns + ------- + x : :obj:`numpy.ndarray` + Updated model vector + y : :obj:`numpy.ndarray` + Updated additional model vector + + """ + + # update fix point + g = x - self.tau * self.proxf.grad(x) + r = g - y + + # update history vectors + self.R.insert(0, r) + self.G.insert(0, g) + if self.iiter >= self.nhistory - 1: + self.R.pop(-1) + self.G.pop(-1) + + # solve for alpha coefficients + Rstack = np.vstack(self.R) + Rinv = np.linalg.pinv( + Rstack @ Rstack.T + self.epsr * np.linalg.norm(Rstack) ** 2 + ) + ones = np.ones(min(self.nhistory, self.iiter + 2)) + Rinvones = Rinv @ ones + alpha = Rinvones / (ones[None] @ Rinvones) + + if not self.safeguard: + # update auxiliary variable + y = np.vstack(self.G).T @ alpha + + # update main variable + x = self.proxg.prox(y, self.epsg[self.iiter] * self.tau) + else: + # update auxiliary variable + ytest = np.vstack(self.G).T @ alpha + + # update main variable + xtest = self.proxg.prox(ytest, self.epsg[self.iiter] * self.tau) + + # check if function is decreased, otherwise do basic PG step + pfold, self.pf = self.pf, self.proxf(xtest) + if ( + self.pf + <= pfold - self.tau * np.linalg.norm(self.proxf.grad(x)) ** 2 / 2 + ): + y = ytest + x = xtest + else: + x = self.proxg.prox(g, self.epsg[self.iiter] * self.tau) + y = g + + # tolerance check: break iterations if overall + # objective does not decrease below tolerance + if self.tol is not None: + self.pfgold = self.pfg + self.pf, pg = self.proxf(x), self.proxg(x) + self.pfg = self.pf + np.sum(self.epsg[self.iiter] * pg) + if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + self.tolbreak = True + else: + pg = 0.0 + + self.iiter += 1 + if show: + self._print_step(x, pg) + if self.tol is not None or show: + self.pfgs.append(float(self.pfg)) + return x, y + + def run( + self, + x: NDArray, + y: NDArray, + niter: int | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> NDArray: + r"""Run solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by multiple steps of + the Anderson proximal gradient algorithm + y : :obj:`numpy.ndarray` + Additional model vector to be updated by multiple steps of + the proximal gradient algorithm + niter : :obj:`int`, optional + Number of iterations. Can be set to ``None`` if already + provided in the setup call + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + y : :obj:`numpy.ndarray` + Additional estimated model + + """ + niter = self.niter if niter is None else niter + if niter is None: + msg = "`niter` must not be None" + raise ValueError(msg) + while self.iiter < niter and not self.tolbreak: + showstep = ( + True + if show + and ( + self.iiter < itershow[0] + or niter - self.iiter < itershow[1] + or self.iiter % itershow[2] == 0 + ) + else False + ) + x, y = self.step(x, y, showstep) + self.callback(x) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x, y + + def finalize(self, show: bool = False) -> None: + r"""Finalize solver + + Parameters + ---------- + show : :obj:`bool`, optional + Display finalize log + + """ + self.tend = time.time() + self.telapsed = self.tend - self.tstart + if show: + self._print_finalize(nbar=81) + + def solve( # type: ignore[override] + self, + proxf: ProxOperator, + proxg: ProxOperator, + x0: NDArray, + epsg: float | NDArray = 1.0, + tau: float | NDArray = 1.0, + niter: int = 10, + nhistory: int = 10, + epsr: float = 1e-10, + safeguard: bool = False, + tol: float | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray, int, NDArray]: + r"""Run entire solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function (must have ``grad`` implemented) + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + x0 : :obj:`numpy.ndarray` + Initial vector + epsg : :obj:`float` or :obj:`numpy.ndarray`, optional + Scaling factor of g function + tau : :obj:`float` or :obj:`numpy.ndarray`, optional + Positive scalar weight, which should satisfy the following condition + to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is + the Lipschitz constant of :math:`\nabla f`. N ote that :math:`\tau` + can be chosen to be a vector when dealing with problems with + multiple right-hand-sides + niter : :obj:`int`, optional + Number of iterations of iterative scheme + nhistory : :obj:`int`, optional + Number of previous iterates to be kept in memory (to compute the scaling factors) + epsr : :obj:`float`, optional + Scaling factor for regularization added to the inverse of :math:\mathbf{R}^T \mathbf{R}` + safeguard : :obj:`bool`, optional + Apply safeguarding strategy to the update (``True``) or not (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping + criterion). If ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + y : :obj:`numpy.ndarray` + Additional estimated model + iiter : :obj:`int` + Number of executed iterations + pfgs : :obj:`numpy.ndarray` + History of the objective function + + """ + x, y = self.setup( + proxf=proxf, + proxg=proxg, + x0=x0, + epsg=epsg, + tau=tau, + niter=niter, + nhistory=nhistory, + epsr=epsr, + safeguard=safeguard, + tol=tol, + show=show, + ) + + x, y = self.run(x, y, niter, show=show, itershow=itershow) + self.finalize(show) + return x, y, self.iiter, self.pfgs From 8814e307a403a1bf3fbdfa16d1e0eecf53612ce0 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 15 Apr 2026 22:41:54 +0100 Subject: [PATCH 04/73] feat: added class based GeneralizedProximalGradient --- pyproximal/optimization/cls_primal.py | 428 +++++++++++++++++++++++++- pytests/test_solver.py | 65 ++++ 2 files changed, 492 insertions(+), 1 deletion(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index 5a125e65..d0f84182 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -3,10 +3,12 @@ ] import time +from collections.abc import Sequence from typing import TYPE_CHECKING, Optional, cast import numpy as np -from pylops.optimization.callback import _callback_stop +import pylops +from pylops.optimization.callback import Callbacks from pylops.utils.backend import get_array_module, to_numpy from pylops.utils.typing import NDArray, Tmemunit @@ -18,6 +20,17 @@ from pylops.linearoperator import LinearOperator +# need to check pylops version since _callback_stop +# is only available in pylops>=2.6.0 +sp_version = pylops.__version__.split(".") +if int(sp_version[0]) < 2 or (int(sp_version[0]) == 2 and int(sp_version[1]) < 6): + + def _callback_stop(callbacks: Sequence[Callbacks]) -> bool: + return False +else: + from pylops.optimization.callback import _callback_stop # type: ignore[no-redef] + + def _backtracking( x: NDArray, tau: float, @@ -1263,3 +1276,416 @@ def solve( # type: ignore[override] x, y = self.run(x, y, niter, show=show, itershow=itershow) self.finalize(show) return x, y, self.iiter, self.pfgs + + +class GeneralizedProximalGradient(Solver): + r"""Generalized Proximal gradient + + Solves the following minimization problem using Generalized Proximal + gradient algorithm: + + .. math:: + + \mathbf{x} = \argmin_\mathbf{x} \sum_{i=1}^n f_i(\mathbf{x}) + + \sum_{j=1}^m \epsilon_j g_j(\mathbf{x}),~~n,m \in \mathbb{N}^+ + + where the :math:`f_i(\mathbf{x})` are smooth convex functions with a uniquely + defined gradient and the :math:`g_j(\mathbf{x})` are any convex function that + have a known proximal operator. + + Notes + ----- + The Generalized Proximal gradient algorithm can be expressed by the + following recursion [1]_: + + .. math:: + \text{for } j=1,\cdots,n, \\ + ~~~~\mathbf z_j^{k+1} = \mathbf z_j^{k} + \eta + \left[prox_{\frac{\tau^k \epsilon_j}{w_j} g_j}\left(2 \mathbf{x}^{k} - \mathbf{z}_j^{k} + - \tau^k \sum_{i=1}^n \nabla f_i(\mathbf{x}^{k})\right) - \mathbf{x}^{k} \right] \\ + \mathbf{x}^{k+1} = \sum_{j=1}^n w_j \mathbf z_j^{k+1} \\ + + where :math:`\sum_{j=1}^n w_j=1`. In the current implementation, :math:`w_j=1/n` when + not provided. + + .. [1] Raguet, H., Fadili, J. and PeyrΓ©, G. "Generalized Forward-Backward Splitting", + arXiv, 2012. + + """ + + def _print_setup(self, epsg_print: str, xcomplex: bool = False) -> None: + self._print_solver(nbar=65) + + strpar = ( + f"Proximal operators (f): {[type(proxf).__name__ for proxf in self.proxfs]}\n" + f"Proximal operators (g): {[type(proxg).__name__ for proxg in self.proxgs]}\n" + ) + strpar1 = f"tau = {self.tau:4.2e}\tepsg = {epsg_print}\tniter = {self.niter}" + print(strpar) + print(strpar1) + print("-" * 65 + "\n") + if not xcomplex: + head1 = " Itn x[0] f g J=f+g" + else: + head1 = ( + " Itn x[0] f g J=f+g" + ) + print(head1) + + def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: + if self.tol is None: + pf = np.sum([proxf(x) for proxf in self.proxfs]) + pg = np.sum( + [ + eg * proxg(x) + for proxg, eg in zip(self.proxgs, self.epsg, strict=True) + ] + ) + self.pfg = pf + pg + x0 = to_numpy(x[0]) if x.ndim == 1 else to_numpy(x[0, 0]) + strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " + msg = ( + f"{self.iiter:6g} " + + strx + + f"{pf:10.3e} {pg:10.3e} {self.pfg:10.3e}" + ) + print(msg) + + def memory_usage( + self, + show: bool = False, + unit: Tmemunit = "B", + ) -> None: + pass + + def setup( # type: ignore[override] + self, + proxfs: list[ProxOperator], + proxgs: list[ProxOperator], + x0: NDArray, + tau: float, + epsg: float | NDArray = 1.0, + weights: NDArray | None = None, + eta: float = 1.0, + niter: int = 10, + acceleration: str | None = None, + tol: float | None = None, + show: bool = False, + ) -> tuple[NDArray, NDArray]: + r"""Setup solver + + Parameters + ---------- + proxfs : :obj:`list` + Proximal operators of the :math:`f_i` functions (must have ``grad`` implemented) + proxgs : :obj:`list` + Proximal operators of the :math:`g_j` functions + x0 : :obj:`numpy.ndarray` + Initial vector + tau : :obj:`float` + Positive scalar weight, which should satisfy the following condition + to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is + the Lipschitz constant of :math:`\sum_{i=1}^n \nabla f_i`. + epsg : :obj:`float` or :obj:`numpy.ndarray`, optional + Scaling factor(s) of ``g`` function(s) + weights : :obj:`float`, optional + Weighting factors of ``g`` functions. Must sum to 1. + eta : :obj:`float`, optional + Relaxation parameter (must be between 0 and 1, 0 excluded). Note that + this will be only used when ``acceleration=None``. + niter : :obj:`int`, optional + Number of iterations of iterative scheme + acceleration: :obj:`str`, optional + Acceleration (``None``, ``vandenberghe`` or ``fista``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display iterations log + + Returns + ------- + x : :obj:`numpy.ndarray` + Initial guess + y : :obj:`numpy.ndarray` + Initial guess for the auxiliary variable + + """ + self.proxfs = proxfs + self.proxgs = proxgs + self.x0 = x0 + self.tau = tau + self.eta = eta + self.niter = niter + self.tol = tol + + self.ncp = get_array_module(x0) + + # check if weights sum to 1 + self.weights = ( + np.ones(len(proxgs)) / len(proxgs) if weights is None else weights + ) + if len(self.weights) != len(self.proxgs) or np.sum(self.weights) != 1.0: + msg = f"weights={self.weights} must be an array of size {len(self.proxgs)} summing to 1" + raise ValueError(msg) + + # check if epgs is a vector + self.epsg = np.asarray(epsg, dtype=float) + if self.epsg.size == 1: + self.epsg = epsg * np.ones(len(proxgs)) + epsg_print = str(self.epsg[0]) + else: + epsg_print = "Multi" + + # check acceleration + if acceleration in [None, "None", "vandenberghe", "fista"]: + self.acceleration = acceleration + else: + msg = "Acceleration should be None, vandenberghe or fista" + raise NotImplementedError(msg) + + # set initial vectors + x = x0.copy() + y = x.copy() + self.zs = [x.copy() for _ in range(len(proxgs))] + + # for accelaration + self.t = 1.0 + + # create variables to track the objective function and iterations + self.pfg, self.pfgold = np.inf, np.inf + self.pfgs: list[float] = [] + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(epsg_print, np.iscomplexobj(x0)) + return x, y + + def step( + self, x: NDArray, y: NDArray, show: bool = False + ) -> tuple[NDArray, NDArray]: + r"""Run one step of solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by a step of the + proximal gradient algorithm + y : :obj:`numpy.ndarray` + Additional model vector to be updated by a step of the + proximal gradient algorithm + show : :obj:`bool`, optional + Display iteration log + + Returns + ------- + x : :obj:`numpy.ndarray` + Updated model vector + y : :obj:`numpy.ndarray` + Updated additional model vector + + """ + xold = x.copy() + + # gradient + grad = np.zeros_like(x) + for _, proxf in enumerate(self.proxfs): + grad += proxf.grad(x) + + # proximal step + x = np.zeros_like(x) + for i, proxg in enumerate(self.proxgs): + ztmp = 2 * y - self.zs[i] - self.tau * grad + ztmp = proxg.prox(ztmp, self.tau * self.epsg[i] / self.weights[i]) + self.zs[i] += self.eta * (ztmp - y) + x += self.weights[i] * self.zs[i] + + # update y + if self.acceleration == "vandenberghe": + omega = self.iiter / (self.iiter + 3) + elif self.acceleration == "fista": + told = self.t + self.t = (1.0 + np.sqrt(1.0 + 4.0 * self.t**2)) / 2.0 + omega = (told - 1.0) / self.t + else: + omega = 0 + y = x + omega * (x - xold) + + # tolerance check: break iterations if overall + # objective does not decrease below tolerance + if self.tol is not None: + self.pfgold = self.pfg + pf = np.sum([proxf(x) for proxf in self.proxfs]) + pg = np.sum( + [ + eg * proxg(x) + for proxg, eg in zip(self.proxgs, self.epsg, strict=True) + ] + ) + self.pfg = pf + pg + if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + self.tolbreak = True + else: + pf, pg = 0.0, 0.0 + + self.iiter += 1 + if show: + self._print_step(x, pf, pg) + if self.tol is not None or show: + self.pfgs.append(float(self.pfg)) + return x, y + + def run( + self, + x: NDArray, + y: NDArray, + niter: int | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray]: + r"""Run solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by multiple steps of + the proximal gradient algorithm + y : :obj:`numpy.ndarray` + Additional model vector to be updated by multiple steps of + the proximal gradient algorithm + niter : :obj:`int`, optional + Number of iterations. Can be set to ``None`` if already + provided in the setup call + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + y : :obj:`numpy.ndarray` + Additional estimated model + + """ + niter = self.niter if niter is None else niter + if niter is None: + msg = "`niter` must not be None" + raise ValueError(msg) + while self.iiter < niter and not self.tolbreak: + showstep = ( + True + if show + and ( + self.iiter < itershow[0] + or niter - self.iiter < itershow[1] + or self.iiter % itershow[2] == 0 + ) + else False + ) + x, y = self.step(x, y, showstep) + self.callback(x) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x, y + + def finalize(self, show: bool = False) -> None: + r"""Finalize solver + + Parameters + ---------- + show : :obj:`bool`, optional + Display finalize log + + """ + self.tend = time.time() + self.telapsed = self.tend - self.tstart + if show: + self._print_finalize(nbar=65) + + def solve( # type: ignore[override] + self, + proxfs: list[ProxOperator], + proxgs: list[ProxOperator], + x0: NDArray, + tau: float, + epsg: float | NDArray = 1.0, + weights: NDArray | None = None, + eta: float = 1.0, + niter: int = 10, + acceleration: str | None = None, + tol: float | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray, int, NDArray]: + r"""Run entire solver + + Parameters + ---------- + proxfs : :obj:`list` + Proximal operators of the :math:`f_i` functions (must have ``grad`` implemented) + proxgs : :obj:`list` + Proximal operators of the :math:`g_j` functions + x0 : :obj:`numpy.ndarray` + Initial vector + tau : :obj:`float` + Positive scalar weight, which should satisfy the following condition + to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is + the Lipschitz constant of :math:`\sum_{i=1}^n \nabla f_i`. + epsg : :obj:`float` or :obj:`numpy.ndarray`, optional + Scaling factor(s) of ``g`` function(s) + weights : :obj:`float`, optional + Weighting factors of ``g`` functions. Must sum to 1. + eta : :obj:`float`, optional + Relaxation parameter (must be between 0 and 1, 0 excluded). Note that + this will be only used when ``acceleration=None``. + niter : :obj:`int`, optional + Number of iterations of iterative scheme + acceleration: :obj:`str`, optional + Acceleration (``None``, ``vandenberghe`` or ``fista``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping + criterion). If ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + y : :obj:`numpy.ndarray` + Additional estimated model + iiter : :obj:`int` + Number of executed iterations + pfgs : :obj:`numpy.ndarray` + History of the objective function + + """ + x, y = self.setup( + proxfs=proxfs, + proxgs=proxgs, + x0=x0, + tau=tau, + epsg=epsg, + weights=weights, + eta=eta, + niter=niter, + acceleration=acceleration, + tol=tol, + show=show, + ) + + x, y = self.run(x, y, niter, show=show, itershow=itershow) + self.finalize(show) + return x, y, self.iiter, self.pfgs diff --git a/pytests/test_solver.py b/pytests/test_solver.py index 2c5b5dcc..a2105a04 100644 --- a/pytests/test_solver.py +++ b/pytests/test_solver.py @@ -4,6 +4,7 @@ import pytest from numpy.testing import assert_array_almost_equal from pylops.basicoperators import Identity, MatrixMult +from pylops.optimization.sparsity import fista, ista from pyproximal.optimization.primal import ( ADMM, @@ -23,6 +24,14 @@ par2 = {"n": 8, "m": 10, "dtype": "float64"} # float32 +def test_ProximalGradient_unknown_acceleration(): + """Check that an error is raised if an unknown acceleration + method is provided to ProximalGradient solver + """ + with pytest.raises(NotImplementedError, match="Acceleration should "): + _ = ProximalGradient(proxf=L2(), proxg=L1(), x0=None, acceleration="unknown") + + def test_HQS_noinitial(): """Check that an error is raised if no initial value is provided to HQS solver @@ -161,6 +170,62 @@ def test_ProximalPoint(par): assert_array_almost_equal(xpp, x, decimal=2) +@pytest.mark.parametrize("par", [(par1), (par2)]) +def test_PG_ISTA(par): + """Check equivalency of ProximalGradient and ISTA/FISTA (PyLops)""" + np.random.seed(0) + n, m = par["n"], par["m"] + + # Define sparse model + x = np.zeros(m) + x[2], x[4] = 1, 0.5 + + # Random mixing matrix + R = np.random.normal(0.0, 1.0, (n, m)) + Rop = MatrixMult(R) + + y = Rop @ x + + # Step size + L = (Rop.H * Rop).eigs(1).real + tau = 0.99 / L + + for solver, acceleration in zip( + [ista, fista], + [None, "fista"], + strict=True, + ): + # ISTA/FISTA + eps = 5e-1 + xista = solver( + Rop, + y, + niter=100, + alpha=tau, + eps=eps, + tol=1e-8, + monitorres=False, + show=False, + )[0] + + # PG + l2 = L2(Op=Rop, b=y) + l1 = L1() + epsg = eps * 0.5 # to compensate for 0.5 in ISTA: thresh = eps * alpha * 0.5 + xpg = ProximalGradient( + l2, + l1, + x0=np.zeros(m), + tau=tau, + epsg=epsg, + acceleration=acceleration, + niter=100, + tol=1e-8, + ) + + assert_array_almost_equal(xpg, xista, decimal=2) + + @pytest.mark.parametrize("par", [(par1), (par2)]) def test_PG_GPG(par): """Check equivalency of ProximalGradient and GeneralizedProximalGradient when using From cd4f78d32563c6ea42bbc751645dfefca5538e68 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sat, 18 Apr 2026 20:22:47 +0100 Subject: [PATCH 05/73] feat: added class based HQS --- pyproximal/optimization/cls_primal.py | 409 ++++++++++++++++++++++++-- pyproximal/optimization/primal.py | 1 + 2 files changed, 391 insertions(+), 19 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index d0f84182..3a999296 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -218,7 +218,7 @@ def setup( # type: ignore[override] # create variables to track the objective function and iterations self.pf, self.pfold = np.inf, np.inf - self.pfs: list[float] = [] + self.cost: list[float] = [] self.tolbreak = False self.iiter = 0 @@ -258,7 +258,7 @@ def step(self, x: NDArray, show: bool = False) -> NDArray: if show: self._print_step(x) if self.tol is not None or show: - self.pfs.append(float(self.pf)) + self.cost.append(float(self.pf)) return x def run( @@ -325,6 +325,7 @@ def finalize(self, show: bool = False) -> None: """ self.tend = time.time() self.telapsed = self.tend - self.tstart + if show: self._print_finalize(nbar=60) @@ -366,14 +367,14 @@ def solve( # type: ignore[override] Estimated model iiter : :obj:`int` Number of executed iterations - pfs : :obj:`numpy.ndarray` + cost : :obj:`list` History of the objective function """ x = self.setup(prox=prox, x0=x0, tau=tau, niter=niter, tol=tol, show=show) x = self.run(x, niter, show=show, itershow=itershow) self.finalize(show) - return x, self.iiter, self.pfs + return x, self.iiter, self.cost class ProximalGradient(Solver): @@ -580,8 +581,7 @@ def setup( # type: ignore[override] # create variables to track the objective function and iterations self.pfg, self.pfgold = np.inf, np.inf - self.pfs: list[float] = [] - self.pfgs: list[float] = [] + self.cost: list[float] = [] self.tolbreak = False self.iiter = 0 @@ -679,7 +679,7 @@ def step( if show: self._print_step(x, pf, pg) if self.tol is not None or show: - self.pfgs.append(float(self.pfg)) + self.cost.append(float(self.pfg)) return x, y def run( @@ -752,6 +752,7 @@ def finalize(self, show: bool = False) -> None: """ self.tend = time.time() self.telapsed = self.tend - self.tstart + if show: self._print_finalize(nbar=81) @@ -823,7 +824,7 @@ def solve( # type: ignore[override] Additional estimated model iiter : :obj:`int` Number of executed iterations - pfgs : :obj:`numpy.ndarray` + cost : :obj:`list` History of the objective function """ @@ -845,7 +846,7 @@ def solve( # type: ignore[override] x, y = self.run(x, y, niter, show=show, itershow=itershow) self.finalize(show) - return x, y, self.iiter, self.pfgs + return x, y, self.iiter, self.cost class AndersonProximalGradient(Solver): @@ -1023,10 +1024,8 @@ def setup( # type: ignore[override] # create variables to track the objective function and iterations self.pf = self.proxf(x) - print("Initial pf: %10.3e" % self.pf) self.pfg, self.pfgold = np.inf, np.inf - self.pfs: list[float] = [] - self.pfgs: list[float] = [] + self.cost: list[float] = [] self.tolbreak = False self.iiter = 0 @@ -1120,7 +1119,7 @@ def step( if show: self._print_step(x, pg) if self.tol is not None or show: - self.pfgs.append(float(self.pfg)) + self.cost.append(float(self.pfg)) return x, y def run( @@ -1193,6 +1192,7 @@ def finalize(self, show: bool = False) -> None: """ self.tend = time.time() self.telapsed = self.tend - self.tstart + if show: self._print_finalize(nbar=81) @@ -1255,7 +1255,7 @@ def solve( # type: ignore[override] Additional estimated model iiter : :obj:`int` Number of executed iterations - pfgs : :obj:`numpy.ndarray` + cost : :obj:`list` History of the objective function """ @@ -1275,7 +1275,7 @@ def solve( # type: ignore[override] x, y = self.run(x, y, niter, show=show, itershow=itershow) self.finalize(show) - return x, y, self.iiter, self.pfgs + return x, y, self.iiter, self.cost class GeneralizedProximalGradient(Solver): @@ -1454,7 +1454,7 @@ def setup( # type: ignore[override] # create variables to track the objective function and iterations self.pfg, self.pfgold = np.inf, np.inf - self.pfgs: list[float] = [] + self.cost: list[float] = [] self.tolbreak = False self.iiter = 0 @@ -1534,7 +1534,7 @@ def step( if show: self._print_step(x, pf, pg) if self.tol is not None or show: - self.pfgs.append(float(self.pfg)) + self.cost.append(float(self.pfg)) return x, y def run( @@ -1607,6 +1607,7 @@ def finalize(self, show: bool = False) -> None: """ self.tend = time.time() self.telapsed = self.tend - self.tstart + if show: self._print_finalize(nbar=65) @@ -1668,7 +1669,7 @@ def solve( # type: ignore[override] Additional estimated model iiter : :obj:`int` Number of executed iterations - pfgs : :obj:`numpy.ndarray` + cost : :obj:`list` History of the objective function """ @@ -1688,4 +1689,374 @@ def solve( # type: ignore[override] x, y = self.run(x, y, niter, show=show, itershow=itershow) self.finalize(show) - return x, y, self.iiter, self.pfgs + return x, y, self.iiter, self.cost + + +class HQS(Solver): + r"""Half Quadratic splitting + + Solves the following minimization problem using Half Quadratic splitting + algorithm: + + .. math:: + + \mathbf{x},\mathbf{z} = \argmin_{\mathbf{x},\mathbf{z}} + f(\mathbf{x}) + g(\mathbf{z}) \\ + s.t. \; \mathbf{x}=\mathbf{z} + + where :math:`f(\mathbf{x})` and :math:`g(\mathbf{z})` are any convex + function that has a known proximal operator. + + Notes + ----- + The HQS algorithm can be expressed by the following recursion [1]_: + + .. math:: + + \mathbf{z}^{k+1} = \prox_{\tau g}(\mathbf{x}^{k}) \\ + \mathbf{x}^{k+1} = \prox_{\tau f}(\mathbf{z}^{k+1}) + + for ``gfirst=False``, or + + .. math:: + + \mathbf{x}^{k+1} = \prox_{\tau f}(\mathbf{z}^{k}) \\ + \mathbf{z}^{k+1} = \prox_{\tau g}(\mathbf{x}^{k+1}) + + for ``gfirst=False``. Note that ``x`` and ``z`` converge to each other, + however if iterations are stopped too early ``x`` is guaranteed to belong to + the domain of ``f`` while ``z`` is guaranteed to belong to the domain of ``g``. + Depending on the problem either of the two may be the best solution. + + .. [1] D., Geman, and C., Yang, "Nonlinear image recovery with halfquadratic + regularization", IEEE Transactions on Image Processing, + 4, 7, pp. 932-946, 1995. + + """ + + def _print_setup(self, tau_print: str, xcomplex: bool = False) -> None: + self._print_solver(nbar=65) + + strpar = ( + f"Proximal operator (f): {type(self.proxf).__name__}\n" + f"Proximal operator (g): {type(self.proxg).__name__}\n" + ) + strpar1 = f"tau = {tau_print}\tniter = {self.niter}" + print(strpar) + print(strpar1) + print("-" * 65 + "\n") + if not xcomplex: + head1 = " Itn x[0] f g J=f+g" + else: + head1 = ( + " Itn x[0] f g J=f+g" + ) + print(head1) + + def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: + if self.tol is None: + pf, pg = self.proxf(x), self.proxg(x) + self.pfg = pf + pg + x0 = to_numpy(x[0]) if x.ndim == 1 else to_numpy(x[0, 0]) + strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " + msg = ( + f"{self.iiter:6g} " + + strx + + f"{pf:10.3e} {pg:10.3e} {self.pfg:10.3e}" + ) + print(msg) + + def memory_usage( + self, + show: bool = False, + unit: Tmemunit = "B", + ) -> None: + pass + + def setup( # type: ignore[override] + self, + proxf: ProxOperator, + proxg: ProxOperator, + x0: NDArray, + tau: float | NDArray, + z0: NDArray | None = None, + niter: int = 10, + gfirst: bool = True, + tol: float | None = None, + show: bool = False, + ) -> tuple[NDArray, NDArray]: + r"""Setup solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + x0 : :obj:`numpy.ndarray` + Initial vector (not required when ``gfirst=False``, can pass ``None``) + tau : :obj:`float` or :obj:`numpy.ndarray` + Positive scalar weight, which should satisfy the following condition + to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is + the Lipschitz constant of :math:`\nabla f`. Finally note that + :math:`\tau` can be chosen to be a vector of size ``niter`` such that + different :math:`\tau` is used at different iterations (i.e., continuation + strategy) + z0 : :obj:`numpy.ndarray`, optional + Initial z vector (not required when ``gfirst=True``) + niter : :obj:`int` + Number of iterations of iterative scheme + gfirst : :obj:`bool`, optional + Apply Proximal of operator ``g`` first (``True``) or Proximal of + operator ``f`` first (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display iterations log + + Returns + ------- + x : :obj:`numpy.ndarray` + Initial guess + z : :obj:`numpy.ndarray` + Initial guess for the auxiliary variable + + Raises + ------ + ValueError + If both ``x0`` and ``z0`` are set to ``None`` + + """ + self.proxf = proxf + self.proxg = proxg + self.niter = niter + self.gfirst = gfirst + self.tol = tol + + self.ncp = get_array_module(x0) + + # check if tau is a vector + self.tau = self.ncp.asarray(tau, dtype=float) + if tau.size == 1: + tau_print = str(self.tau) + self.tau = self.tau * np.ones(niter) + else: + tau_print = "Variable" + + # set initial vectors + x, z = _x0z0_init(x0, z0) + + # for accelaration + self.t = 1.0 + + # create variables to track the objective function and iterations + self.pfg, self.pfgold = np.inf, np.inf + self.cost: list[float] = [] + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(tau_print, np.iscomplexobj(x0)) + return x, z + + def step( + self, x: NDArray, z: NDArray, show: bool = False + ) -> tuple[NDArray, NDArray]: + r"""Run one step of solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by a step of the + proximal gradient algorithm + z : :obj:`numpy.ndarray` + Additional model vector to be updated by a step of the + proximal gradient algorithm + show : :obj:`bool`, optional + Display iteration log + + Returns + ------- + x : :obj:`numpy.ndarray` + Updated model vector + z : :obj:`numpy.ndarray` + Updated additional model vector + + """ + # proximal steps + if self.gfirst: + z = self.proxg.prox(x, self.tau[self.iiter]) + x = self.proxf.prox(z, self.tau[self.iiter]) + else: + x = self.proxf.prox(z, self.tau[self.iiter]) + z = self.proxg.prox(x, self.tau[self.iiter]) + + # tolerance check: break iterations if overall + # objective does not decrease below tolerance + if self.tol is not None: + self.pfgold = self.pfg + pf = self.proxf(x) + pg = self.proxg(x) + self.pfg = pf + pg + if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + self.tolbreak = True + else: + pf, pg = 0.0, 0.0 + + self.iiter += 1 + if show: + self._print_step(x, pf, pg) + if self.tol is not None or show: + self.cost.append(float(self.pfg)) + return x, z + + def run( + self, + x: NDArray, + z: NDArray, + niter: int | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray]: + r"""Run solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by multiple steps of + the proximal gradient algorithm + z : :obj:`numpy.ndarray` + Additional model vector to be updated by multiple steps of + the proximal gradient algorithm + niter : :obj:`int`, optional + Number of iterations. Can be set to ``None`` if already + provided in the setup call + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + z : :obj:`numpy.ndarray` + Additional estimated model + + """ + niter = self.niter if niter is None else niter + if niter is None: + msg = "`niter` must not be None" + raise ValueError(msg) + while self.iiter < niter and not self.tolbreak: + showstep = ( + True + if show + and ( + self.iiter < itershow[0] + or niter - self.iiter < itershow[1] + or self.iiter % itershow[2] == 0 + ) + else False + ) + x, z = self.step(x, z, showstep) + self.callback(x) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x, z + + def finalize(self, show: bool = False) -> None: + r"""Finalize solver + + Parameters + ---------- + show : :obj:`bool`, optional + Display finalize log + + """ + self.tend = time.time() + self.telapsed = self.tend - self.tstart + + if show: + self._print_finalize(nbar=65) + + def solve( # type: ignore[override] + self, + proxf: ProxOperator, + proxg: ProxOperator, + x0: NDArray, + tau: float | NDArray, + z0: NDArray | None = None, + niter: int = 10, + gfirst: bool = True, + tol: float | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray, int, NDArray]: + r"""Run entire solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + x0 : :obj:`numpy.ndarray` + Initial vector (not required when ``gfirst=False``, can pass ``None``) + tau : :obj:`float` or :obj:`numpy.ndarray` + Positive scalar weight, which should satisfy the following condition + to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is + the Lipschitz constant of :math:`\nabla f`. Finally note that + :math:`\tau` can be chosen to be a vector of size ``niter`` such that + different :math:`\tau` is used at different iterations (i.e., continuation + strategy) + z0 : :obj:`numpy.ndarray`, optional + Initial z vector (not required when ``gfirst=True``) + niter : :obj:`int` + Number of iterations of iterative scheme + gfirst : :obj:`bool`, optional + Apply Proximal of operator ``g`` first (``True``) or Proximal of + operator ``f`` first (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping + criterion). If ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + y : :obj:`numpy.ndarray` + Additional estimated model + iiter : :obj:`int` + Number of executed iterations + cost : :obj:`list` + History of the objective function + + """ + x, z = self.setup( + proxf=proxf, + proxg=proxg, + x0=x0, + tau=tau, + z0=z0, + niter=niter, + gfirst=gfirst, + tol=tol, + show=show, + ) + + x, z = self.run(x, z, niter, show=show, itershow=itershow) + self.finalize(show) + return x, z, self.iiter, self.cost diff --git a/pyproximal/optimization/primal.py b/pyproximal/optimization/primal.py index fad1fe74..12ad1e41 100644 --- a/pyproximal/optimization/primal.py +++ b/pyproximal/optimization/primal.py @@ -332,6 +332,7 @@ def ProximalGradient( if acceleration not in [None, "None", "vandenberghe", "fista"]: msg = "Acceleration should be None, vandenberghe or fista" raise NotImplementedError(msg) + if show: tstart = time.time() print( From 014158aef5b1377be86505f42a5093f7e775e583 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sat, 18 Apr 2026 20:24:38 +0100 Subject: [PATCH 06/73] feat: added HQS tests --- pytests/test_solver.py | 52 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/pytests/test_solver.py b/pytests/test_solver.py index a2105a04..e416f944 100644 --- a/pytests/test_solver.py +++ b/pytests/test_solver.py @@ -4,6 +4,7 @@ import pytest from numpy.testing import assert_array_almost_equal from pylops.basicoperators import Identity, MatrixMult +from pylops.optimization.leastsquares import regularized_inversion from pylops.optimization.sparsity import fista, ista from pyproximal.optimization.primal import ( @@ -274,6 +275,57 @@ def test_PG_GPG(par): assert_array_almost_equal(xpg, xgpg, decimal=2) +@pytest.mark.parametrize("par", [(par1), (par2)]) +def test_HQS_ADMM_L2(par): + """Check that HQS/ADMM can be used to solved a pure L2-based objective function + (and compare with LSQR - note that despite the trajectory will be different, + they should converge to the same solution) + """ + np.random.seed(0) + n, m = par["n"], par["m"] + + # Define sparse model + x = np.random.normal(0.0, 1.0, m).astype(par["dtype"]) + + # Random mixing matrix + R = np.random.normal(0.0, 1.0, (n, m)).astype(par["dtype"]) + Rop = MatrixMult(R, dtype=par["dtype"]) + + y = Rop @ x + + # Step size + L = (Rop.H * Rop).eigs(1).real + tau = 0.99 / L + eps = 1e-1 + + # L2 + Iop = Identity(m, dtype=par["dtype"]) + xl2 = regularized_inversion( + Rop, + y, + Regs=[ + Iop, + ], + epsRs=[ + np.sqrt(eps), + ], + iter_lim=1000, + )[0] + + # HQS + l2 = L2(Op=Rop, b=y, niter=10, warm=True) + l2reg = L2(sigma=eps) + xhqs = HQS(l2, l2reg, x0=np.zeros(m), tau=tau, niter=1000)[0] + + # ADMM + l2 = L2(Op=Rop, b=y, niter=10, warm=True) + l2reg = L2(sigma=eps) + xadmm = ADMM(l2, l2reg, x0=np.zeros(m), tau=tau, niter=1000)[0] + + assert_array_almost_equal(xl2, xhqs, decimal=2) + assert_array_almost_equal(xl2, xadmm, decimal=2) + + @pytest.mark.parametrize("par", [(par1), (par2)]) def test_ADMM_DRS(par): """Check equivalency of ADMM and DouglasRachfordSplitting From c302ec6264c36bf45d9f861343d4a7be6c8cf421 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sat, 18 Apr 2026 20:43:32 +0100 Subject: [PATCH 07/73] minor: added Tmemunit to typing --- pyproximal/optimization/cls_primal.py | 3 ++- pyproximal/utils/typing.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index 3a999296..2f7caf09 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -10,11 +10,12 @@ import pylops from pylops.optimization.callback import Callbacks from pylops.utils.backend import get_array_module, to_numpy -from pylops.utils.typing import NDArray, Tmemunit +from pylops.utils.typing import NDArray from pyproximal.optimization.basesolver import Solver from pyproximal.ProxOperator import ProxOperator from pyproximal.utils.bilinear import BilinearOperator +from pyproximal.utils.typing import Tmemunit if TYPE_CHECKING: from pylops.linearoperator import LinearOperator diff --git a/pyproximal/utils/typing.py b/pyproximal/utils/typing.py index 05c801c0..a90e103f 100644 --- a/pyproximal/utils/typing.py +++ b/pyproximal/utils/typing.py @@ -4,8 +4,11 @@ ] from collections.abc import Callable +from typing import Literal from pylops.utils.typing import NDArray FloatCallableLike = float | NDArray | Callable[[int], float | NDArray] IntCallableLike = int | Callable[[int], int] + +Tmemunit = Literal["B", "KB", "MB", "GB"] From 21949bafdd0f64c30d79de03128b227e3d8c9579 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sat, 18 Apr 2026 20:46:09 +0100 Subject: [PATCH 08/73] minor: fix mypy error --- pyproximal/optimization/cls_primal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index 2f7caf09..fc6c8424 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -1839,7 +1839,7 @@ def setup( # type: ignore[override] # check if tau is a vector self.tau = self.ncp.asarray(tau, dtype=float) - if tau.size == 1: + if self.tau.size == 1: tau_print = str(self.tau) self.tau = self.tau * np.ones(niter) else: From 63edabc3e05f2ba5dd3cc1b4442081d2bdac02a4 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 20 Apr 2026 21:51:40 +0100 Subject: [PATCH 09/73] feat: added finalize and callback to basesolver --- pyproximal/optimization/basesolver.py | 59 ++++ pyproximal/optimization/cls_primal.py | 464 +++++++++++++++++++++----- 2 files changed, 439 insertions(+), 84 deletions(-) diff --git a/pyproximal/optimization/basesolver.py b/pyproximal/optimization/basesolver.py index 222979af..954b2ab3 100644 --- a/pyproximal/optimization/basesolver.py +++ b/pyproximal/optimization/basesolver.py @@ -8,6 +8,7 @@ from pylops.optimization.basesolver import Solver as pSolver from pylops.optimization.callback import Callbacks +from pylops.utils.typing import NDArray if TYPE_CHECKING: from pyproximal.ProxOperator import ProxOperator @@ -134,3 +135,61 @@ def solve( """ pass + + def finalize(self, nbar: int = 60, show: bool = False) -> None: + r"""Finalize solver + + Parameters + ---------- + nbar : :obj:`int`, optional + Number of ``-`` in the bar dividing iterations + from finalize messages in the print message of + the solver + show : :obj:`bool`, optional + Display finalize log + + """ + self.tend = time.time() + self.telapsed = self.tend - self.tstart + + if show: + self._print_finalize(nbar=nbar) + + def callback( # noqa: B027 + self, + x: NDArray, + z: NDArray | None = None, + *args, + **kwargs, + ) -> None: + """Callback routine + + This routine must be passed by the user. Its function signature must contain + either a single input that contains the current solution or two inputs + that contain the current solutions for methods that apply splitting + (when using the `solve` method it will be automatically invoked after + each step of the solve) + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current solution + z : :obj:`numpy.ndarray` + Current additional solution + + Examples + -------- + >>> import numpy as np + >>> from pyproximal.optimization.cls_primal import ADMM + >>> def callback(x, z): + ... print(f"Running callback, current solutions {x} - {z}") + ... + >>> admmsolve.callback = callback + + >>> x = np.ones(2) + >>> z = np.zeros(2) + >>> admmsolve.callback(x, z) + Running callback, current solutions [1. 1.] - [0. 0.] + + """ + pass diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index fc6c8424..9019eda7 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -2,7 +2,6 @@ "ProximalPoint", ] -import time from collections.abc import Sequence from typing import TYPE_CHECKING, Optional, cast @@ -315,21 +314,6 @@ def run( break return x - def finalize(self, show: bool = False) -> None: - r"""Finalize solver - - Parameters - ---------- - show : :obj:`bool`, optional - Display finalize log - - """ - self.tend = time.time() - self.telapsed = self.tend - self.tstart - - if show: - self._print_finalize(nbar=60) - def solve( # type: ignore[override] self, prox: "ProxOperator", @@ -374,7 +358,7 @@ def solve( # type: ignore[override] """ x = self.setup(prox=prox, x0=x0, tau=tau, niter=niter, tol=tol, show=show) x = self.run(x, niter, show=show, itershow=itershow) - self.finalize(show) + self.finalize(60, show) return x, self.iiter, self.cost @@ -573,7 +557,7 @@ def setup( # type: ignore[override] msg = "Acceleration should be None, vandenberghe or fista" raise NotImplementedError(msg) - # set initial vectors + # initialize solver x = x0.copy() y = x.copy() @@ -742,21 +726,6 @@ def run( break return x, y - def finalize(self, show: bool = False) -> None: - r"""Finalize solver - - Parameters - ---------- - show : :obj:`bool`, optional - Display finalize log - - """ - self.tend = time.time() - self.telapsed = self.tend - self.tstart - - if show: - self._print_finalize(nbar=81) - def solve( # type: ignore[override] self, proxf: ProxOperator, @@ -846,7 +815,7 @@ def solve( # type: ignore[override] ) x, y = self.run(x, y, niter, show=show, itershow=itershow) - self.finalize(show) + self.finalize(81, show) return x, y, self.iiter, self.cost @@ -1007,7 +976,7 @@ def setup( # type: ignore[override] else: epsg_print = "Multi" - # set initial vectors + # initialize solver y = x0 - self.tau * proxf.grad(x0) x = self.proxg.prox(y, self.epsg[0] * self.tau) @@ -1182,21 +1151,6 @@ def run( break return x, y - def finalize(self, show: bool = False) -> None: - r"""Finalize solver - - Parameters - ---------- - show : :obj:`bool`, optional - Display finalize log - - """ - self.tend = time.time() - self.telapsed = self.tend - self.tstart - - if show: - self._print_finalize(nbar=81) - def solve( # type: ignore[override] self, proxf: ProxOperator, @@ -1275,7 +1229,7 @@ def solve( # type: ignore[override] ) x, y = self.run(x, y, niter, show=show, itershow=itershow) - self.finalize(show) + self.finalize(81, show) return x, y, self.iiter, self.cost @@ -1445,7 +1399,7 @@ def setup( # type: ignore[override] msg = "Acceleration should be None, vandenberghe or fista" raise NotImplementedError(msg) - # set initial vectors + # initialize solver x = x0.copy() y = x.copy() self.zs = [x.copy() for _ in range(len(proxgs))] @@ -1597,21 +1551,6 @@ def run( break return x, y - def finalize(self, show: bool = False) -> None: - r"""Finalize solver - - Parameters - ---------- - show : :obj:`bool`, optional - Display finalize log - - """ - self.tend = time.time() - self.telapsed = self.tend - self.tstart - - if show: - self._print_finalize(nbar=65) - def solve( # type: ignore[override] self, proxfs: list[ProxOperator], @@ -1689,7 +1628,7 @@ def solve( # type: ignore[override] ) x, y = self.run(x, y, niter, show=show, itershow=itershow) - self.finalize(show) + self.finalize(65, show) return x, y, self.iiter, self.cost @@ -1743,8 +1682,10 @@ def _print_setup(self, tau_print: str, xcomplex: bool = False) -> None: f"Proximal operator (g): {type(self.proxg).__name__}\n" ) strpar1 = f"tau = {tau_print}\tniter = {self.niter}" + strpar2 = f"gfirst = {self.gfirst}\ttol = {self.tol}" print(strpar) print(strpar1) + print(strpar2) print("-" * 65 + "\n") if not xcomplex: head1 = " Itn x[0] f g J=f+g" @@ -1840,12 +1781,12 @@ def setup( # type: ignore[override] # check if tau is a vector self.tau = self.ncp.asarray(tau, dtype=float) if self.tau.size == 1: - tau_print = str(self.tau) + tau_print = str(np.round(self.tau, 6)) self.tau = self.tau * np.ones(niter) else: tau_print = "Variable" - # set initial vectors + # initialize solver x, z = _x0z0_init(x0, z0) # for accelaration @@ -1972,20 +1913,375 @@ def run( break return x, z - def finalize(self, show: bool = False) -> None: - r"""Finalize solver + def solve( # type: ignore[override] + self, + proxf: ProxOperator, + proxg: ProxOperator, + x0: NDArray, + tau: float | NDArray, + z0: NDArray | None = None, + niter: int = 10, + gfirst: bool = True, + tol: float | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray, int, NDArray]: + r"""Run entire solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + x0 : :obj:`numpy.ndarray` + Initial vector (not required when ``gfirst=False``, can pass ``None``) + tau : :obj:`float` or :obj:`numpy.ndarray` + Positive scalar weight, which should satisfy the following condition + to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is + the Lipschitz constant of :math:`\nabla f`. Finally note that + :math:`\tau` can be chosen to be a vector of size ``niter`` such that + different :math:`\tau` is used at different iterations (i.e., continuation + strategy) + z0 : :obj:`numpy.ndarray`, optional + Initial z vector (not required when ``gfirst=True``) + niter : :obj:`int` + Number of iterations of iterative scheme + gfirst : :obj:`bool`, optional + Apply Proximal of operator ``g`` first (``True``) or Proximal of + operator ``f`` first (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping + criterion). If ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + y : :obj:`numpy.ndarray` + Additional estimated model + iiter : :obj:`int` + Number of executed iterations + cost : :obj:`list` + History of the objective function + + """ + x, z = self.setup( + proxf=proxf, + proxg=proxg, + x0=x0, + tau=tau, + z0=z0, + niter=niter, + gfirst=gfirst, + tol=tol, + show=show, + ) + + x, z = self.run(x, z, niter, show=show, itershow=itershow) + self.finalize(65, show) + return x, z, self.iiter, self.cost + + +class ADMM(Solver): + r"""Alternating Direction Method of Multipliers + + Solves the following minimization problem using Alternating Direction + Method of Multipliers: + + .. math:: + + \mathbf{x},\mathbf{z} = \argmin_{\mathbf{x},\mathbf{z}} + f(\mathbf{x}) + g(\mathbf{z}) \\ + s.t. \; \mathbf{x}=\mathbf{z} + + where :math:`f(\mathbf{x})` and :math:`g(\mathbf{z})` are any convex + function that has a known proximal operator. + + ADMM can also solve the problem of the form above with a more general + constraint: :math:`\mathbf{Ax}+\mathbf{Bz}=\mathbf{c}`. This routine implements + the special case where :math:`\mathbf{A}=\mathbf{I}`, :math:`\mathbf{B}=-\mathbf{I}`, + and :math:`\mathbf{c}=\mathbf{0}`, as a general algorithm can be obtained for any choice of + :math:`f` and :math:`g` provided they have a known proximal operator. + + On the other hand, for more general choice of :math:`\mathbf{A}`, :math:`\mathbf{B}`, + and :math:`\mathbf{c}`, the iterations are not generalizable, i.e. they depend on the choice of + the :math:`f` and :math:`g` functions. For this reason, we currently only provide an additional + solver for the special case where :math:`f` is a :class:`pyproximal.proximal.L2` + operator with a linear operator :math:`\mathbf{G}` and data :math:`\mathbf{y}`, + :math:`\mathbf{B}=-\mathbf{I}` and :math:`\mathbf{c}=\mathbf{0}`, + called :func:`pyproximal.optimization.primal.ADMML2`. Note that for the very same choice + of :math:`\mathbf{B}` and :math:`\mathbf{c}`, the :func:`pyproximal.optimization.primal.LinearizedADMM` + can also be used (and this does not require a specific choice of :math:`f`). + + See Also + -------- + ADMML2: ADMM with L2 misfit function + LinearizedADMM: Linearized ADMM + + Notes + ----- + The ADMM algorithm can be expressed by the following recursion [1]_: + + .. math:: + + \mathbf{x}^{k+1} = \prox_{\tau f}(\mathbf{z}^{k} - \mathbf{u}^{k})\\ + \mathbf{z}^{k+1} = \prox_{\tau g}(\mathbf{x}^{k+1} + \mathbf{u}^{k})\\ + \mathbf{u}^{k+1} = \mathbf{u}^{k} + \mathbf{x}^{k+1} - \mathbf{z}^{k+1} + + Note that ``x`` and ``z`` converge to each other, however if iterations are + stopped too early ``x`` is guaranteed to belong to the domain of ``f`` + while ``z`` is guaranteed to belong to the domain of ``g``. Depending on + the problem either of the two may be the best solution. + + .. [1] S. Boyd, N. Parikh, E. Chu, B. Peleato, and J. Eckstein. 2011. + Distributed optimization and statistical learning via the alternating + direction method of multipliers. Foundations and Trends in Machine + Learning, 3 (1), 1-122. https://doi.org/10.1561/2200000016. + + """ + + def _print_setup(self, xcomplex: bool = False) -> None: + self._print_solver(nbar=65) + + strpar = ( + f"Proximal operator (f): {type(self.proxf).__name__}\n" + f"Proximal operator (g): {type(self.proxg).__name__}\n" + ) + strpar1 = f"tau = {self.tau:6e}\tniter = {self.niter}" + strpar2 = f"gfirst = {self.gfirst}\t\ttol = {self.tol}" + print(strpar) + print(strpar1) + print(strpar2) + + print("-" * 65 + "\n") + if not xcomplex: + head1 = " Itn x[0] f g J=f+g" + else: + head1 = ( + " Itn x[0] f g J=f+g" + ) + print(head1) + + def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: + if self.tol is None: + pf, pg = self.proxf(x), self.proxg(x) + self.pfg = pf + pg + x0 = to_numpy(x[0]) if x.ndim == 1 else to_numpy(x[0, 0]) + strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " + msg = ( + f"{self.iiter:6g} " + + strx + + f"{pf:10.3e} {pg:10.3e} {self.pfg:10.3e}" + ) + print(msg) + + def memory_usage( + self, + show: bool = False, + unit: Tmemunit = "B", + ) -> None: + pass + + def setup( # type: ignore[override] + self, + proxf: ProxOperator, + proxg: ProxOperator, + x0: NDArray, + tau: float | NDArray, + z0: NDArray | None = None, + niter: int = 10, + gfirst: bool = False, + tol: float | None = None, + callbackz: bool = False, + show: bool = False, + ) -> tuple[NDArray, NDArray]: + r"""Setup solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + x0 : :obj:`numpy.ndarray` + Initial vector (not required when ``gfirst=False``, can pass ``None``) + tau : :obj:`float` + Positive scalar weight, which should satisfy the following condition + to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is + the Lipschitz constant of :math:`\nabla f`. + z0 : :obj:`numpy.ndarray`, optional + Initial z vector (not required when ``gfirst=True``) + niter : :obj:`int` + Number of iterations of iterative scheme + gfirst : :obj:`bool`, optional + Apply Proximal of operator ``g`` first (``True``) or Proximal of + operator ``f`` first (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + callbackz : :obj:`bool`, optional + Modify callback signature to (``callback(x, z)``) when ``callbackz=True`` + show : :obj:`bool`, optional + Display iterations log + + Returns + ------- + x : :obj:`numpy.ndarray` + Initial guess + z : :obj:`numpy.ndarray` + Initial guess for the auxiliary variable + + Raises + ------ + ValueError + If both ``x0`` and ``z0`` are set to ``None`` + + """ + self.proxf = proxf + self.proxg = proxg + self.tau = tau + self.niter = niter + self.gfirst = gfirst + self.tol = tol + self.callbackz = callbackz + + self.ncp = get_array_module(x0) + + # initialize solver + x, z = _x0z0_init(x0, z0) + self.u = self.ncp.zeros_like(x) + + # create variables to track the objective function and iterations + self.pfg, self.pfgold = np.inf, np.inf + self.cost: list[float] = [] + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(np.iscomplexobj(x0)) + return x, z + + def step( + self, x: NDArray, z: NDArray, show: bool = False + ) -> tuple[NDArray, NDArray]: + r"""Run one step of solver Parameters ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by a step of the + proximal gradient algorithm + z : :obj:`numpy.ndarray` + Additional model vector to be updated by a step of the + proximal gradient algorithm show : :obj:`bool`, optional - Display finalize log + Display iteration log + + Returns + ------- + x : :obj:`numpy.ndarray` + Updated model vector + z : :obj:`numpy.ndarray` + Updated additional model vector """ - self.tend = time.time() - self.telapsed = self.tend - self.tstart + # proximal steps + if self.gfirst: + z = self.proxg.prox(x + self.u, self.tau) + x = self.proxf.prox(z - self.u, self.tau) + else: + x = self.proxf.prox(z - self.u, self.tau) + z = self.proxg.prox(x + self.u, self.tau) + self.u = self.u + x - z + + # tolerance check: break iterations if overall + # objective does not decrease below tolerance + if self.tol is not None: + self.pfgold = self.pfg + pf = self.proxf(x) + pg = self.proxg(x) + self.pfg = pf + pg + if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + self.tolbreak = True + else: + pf, pg = 0.0, 0.0 + self.iiter += 1 if show: - self._print_finalize(nbar=65) + self._print_step(x, pf, pg) + if self.tol is not None or show: + self.cost.append(float(self.pfg)) + return x, z + + def run( + self, + x: NDArray, + z: NDArray, + niter: int | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray]: + r"""Run solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by multiple steps of + the proximal gradient algorithm + z : :obj:`numpy.ndarray` + Additional model vector to be updated by multiple steps of + the proximal gradient algorithm + niter : :obj:`int`, optional + Number of iterations. Can be set to ``None`` if already + provided in the setup call + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + z : :obj:`numpy.ndarray` + Additional estimated model + + """ + niter = self.niter if niter is None else niter + if niter is None: + msg = "`niter` must not be None" + raise ValueError(msg) + while self.iiter < niter and not self.tolbreak: + showstep = ( + True + if show + and ( + self.iiter < itershow[0] + or niter - self.iiter < itershow[1] + or self.iiter % itershow[2] == 0 + ) + else False + ) + x, z = self.step(x, z, showstep) + if self.callbackz: + self.callback(x, z) + else: + self.callback(x) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x, z def solve( # type: ignore[override] self, @@ -1995,8 +2291,9 @@ def solve( # type: ignore[override] tau: float | NDArray, z0: NDArray | None = None, niter: int = 10, - gfirst: bool = True, + gfirst: bool = False, tol: float | None = None, + callbackz: bool = False, show: bool = False, itershow: tuple[int, int, int] = (10, 10, 10), ) -> tuple[NDArray, NDArray, int, NDArray]: @@ -2010,13 +2307,10 @@ def solve( # type: ignore[override] Proximal operator of g function x0 : :obj:`numpy.ndarray` Initial vector (not required when ``gfirst=False``, can pass ``None``) - tau : :obj:`float` or :obj:`numpy.ndarray` + tau : :obj:`float` Positive scalar weight, which should satisfy the following condition to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is - the Lipschitz constant of :math:`\nabla f`. Finally note that - :math:`\tau` can be chosen to be a vector of size ``niter`` such that - different :math:`\tau` is used at different iterations (i.e., continuation - strategy) + the Lipschitz constant of :math:`\nabla f`. z0 : :obj:`numpy.ndarray`, optional Initial z vector (not required when ``gfirst=True``) niter : :obj:`int` @@ -2027,6 +2321,8 @@ def solve( # type: ignore[override] tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached + callbackz : :obj:`bool`, optional + Modify callback signature to (``callback(x, z)``) when ``callbackz=True`` show : :obj:`bool`, optional Display logs itershow : :obj:`tuple`, optional @@ -2038,7 +2334,7 @@ def solve( # type: ignore[override] ------- x : :obj:`numpy.ndarray` Estimated model - y : :obj:`numpy.ndarray` + z : :obj:`numpy.ndarray` Additional estimated model iiter : :obj:`int` Number of executed iterations @@ -2059,5 +2355,5 @@ def solve( # type: ignore[override] ) x, z = self.run(x, z, niter, show=show, itershow=itershow) - self.finalize(show) + self.finalize(65, show) return x, z, self.iiter, self.cost From a12c7250e504de9ffac55686620f5997c41f60b5 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sun, 10 May 2026 16:57:49 +0100 Subject: [PATCH 10/73] doc: improved PnP tutorial --- tutorials/plugandplay.py | 122 ++++++++++++++++++++++++++++++++++----- 1 file changed, 106 insertions(+), 16 deletions(-) diff --git a/tutorials/plugandplay.py b/tutorials/plugandplay.py index 02db7817..b0186d71 100644 --- a/tutorials/plugandplay.py +++ b/tutorials/plugandplay.py @@ -27,6 +27,7 @@ import numpy as np import pylops from pylops.config import set_ndarray_multiplication +from pylops.utils.metrics import snr import pyproximal @@ -72,7 +73,7 @@ ############################################################################### # At this point we create a denoiser instance using the BM3D algorithm and use -# as Plug-and-Play Prior to the PG and ADMM algorithms +# as Plug-and-Play Prior to the ADMM, PG and HQS algorithms def callback(x, xtrue, errhist): @@ -84,14 +85,31 @@ def callback(x, xtrue, errhist): tau = 1.0 / L sigma = 0.05 -l2 = pyproximal.proximal.L2(Op=Op, b=y.ravel(), niter=50, warm=True) - # BM3D denoiser denoiser = lambda x, tau: bm3d.bm3d( np.real(x), sigma_psd=sigma * tau, stage_arg=bm3d.BM3DStages.HARD_THRESHOLDING ) +# ADMM-PnP +l2 = pyproximal.proximal.L2(Op=Op, b=y.ravel(), niter=50, warm=True) + +errhistadmm = [] +xpnpadmm = pyproximal.optimization.pnp.PlugAndPlay( + l2, + denoiser, + x.shape, + solver=pyproximal.optimization.primal.ADMM, + tau=tau, + x0=np.zeros(x.size), + niter=40, + show=True, + callback=lambda xx: callback(xx, x.ravel(), errhistadmm), +)[0] +xpnpadmm = np.real(xpnpadmm.reshape(x.shape)) + # PG-Pnp +l2 = pyproximal.proximal.L2(Op=Op, b=y.ravel(), niter=50, warm=True) + errhistpg = [] xpnppg = pyproximal.optimization.pnp.PlugAndPlay( l2, @@ -107,39 +125,111 @@ def callback(x, xtrue, errhist): ) xpnppg = np.real(xpnppg.reshape(x.shape)) -# ADMM-PnP -errhistadmm = [] -xpnpadmm = pyproximal.optimization.pnp.PlugAndPlay( +# HQS-PnP +l2 = pyproximal.proximal.L2(Op=Op, b=y.ravel(), niter=50, warm=True) + +tau_hqs = 1.0 / L * 0.99 ** (np.arange(40)) +errhisthqs = [] +xpnphqs = pyproximal.optimization.pnp.PlugAndPlay( l2, denoiser, x.shape, + solver=pyproximal.optimization.primal.HQS, + tau=tau_hqs, + x0=np.zeros(x.size), + niter=40, + show=True, + callback=lambda xx: callback(xx, x.ravel(), errhisthqs), +)[0] +xpnphqs = np.real(xpnphqs.reshape(x.shape)) + +fig, axs = plt.subplots(1, 4, sharey=True, figsize=(15, 5)) +axs[0].imshow(x, vmin=0, vmax=1, cmap="gray") +axs[0].set_title("Model") +axs[0].axis("tight") +axs[1].imshow(xpnpadmm, vmin=0, vmax=1, cmap="gray") +axs[1].set_title(f"ADMM-PnP (SNR={snr(x, xpnpadmm):.2f} dB)") +axs[1].axis("tight") +axs[2].imshow(xpnppg, vmin=0, vmax=1, cmap="gray") +axs[2].set_title(f"PG-PnP (SNR={snr(x, xpnppg):.2f} dB)") +axs[2].axis("tight") +axs[3].imshow(xpnphqs, vmin=0, vmax=1, cmap="gray") +axs[3].set_title(f"HQS-PnP (SNR={snr(x, xpnphqs):.2f} dB)") +axs[3].axis("tight") +plt.tight_layout() + +############################################################################### +# Finally, the attentive reader may have noticed that in the HQS server a +# continuation strategy was used for the `tau` parameter; whilst this is +# strictly needed for HQS to converge, there is a consensus in the literature +# that also other solvers should benefit from adopting the same strategy +# when used with a PnP prior. This can be in fact interpreted as reducing +# the strength of the denoiser as iterations progress and the estimate comes +# closer to the true solution. +# +# While our :func:`pyproximal.optimization.primal.ADMM` solver does currently +# not offer relaxation out-of-the-box, this can be achieved pretty easily +# by creating an auxiliary `Denoiser` class with a `decay` parameter as +# shown below. + + +class Denoiser: + def __init__(self, sigma, decay): + self.sigma = sigma + self.decay = decay + self.iiter = 0 + + def denoise(self, x, tau): + xden = bm3d.bm3d( + np.real(x), + sigma_psd=self.decay[self.iiter] * self.sigma * tau, + stage_arg=bm3d.BM3DStages.HARD_THRESHOLDING, + ) + self.iiter += 1 + return xden + + +# ADMM-PnP with relaxation +denoiser = Denoiser(sigma, decay=0.99 ** (np.arange(40))) +l2 = pyproximal.proximal.L2(Op=Op, b=y.ravel(), niter=50, warm=True) + +errhistadmm1 = [] +xpnpadmm1 = pyproximal.optimization.pnp.PlugAndPlay( + l2, + denoiser.denoise, + x.shape, solver=pyproximal.optimization.primal.ADMM, tau=tau, x0=np.zeros(x.size), niter=40, show=True, - callback=lambda xx: callback(xx, x.ravel(), errhistadmm), + callback=lambda xx: callback(xx, x.ravel(), errhistadmm1), )[0] -xpnpadmm = np.real(xpnpadmm.reshape(x.shape)) +xpnpadmm1 = np.real(xpnpadmm1.reshape(x.shape)) -fig, axs = plt.subplots(1, 3, figsize=(14, 5)) +fig, axs = plt.subplots(1, 3, sharey=True, figsize=(15, 5)) axs[0].imshow(x, vmin=0, vmax=1, cmap="gray") axs[0].set_title("Model") axs[0].axis("tight") -axs[1].imshow(xpnppg, vmin=0, vmax=1, cmap="gray") -axs[1].set_title("PG-PnP Inversion") +axs[1].imshow(xpnpadmm, vmin=0, vmax=1, cmap="gray") +axs[1].set_title(f"ADMM-PnP (SNR={snr(x, xpnpadmm):.2f} dB)") axs[1].axis("tight") -axs[2].imshow(xpnpadmm, vmin=0, vmax=1, cmap="gray") -axs[2].set_title("ADMM-PnP Inversion") +axs[2].imshow(xpnpadmm1, vmin=0, vmax=1, cmap="gray") +axs[2].set_title(f"ADMM-PnP with rel. (SNR={snr(x, xpnpadmm1):.2f} dB)") axs[2].axis("tight") plt.tight_layout() ############################################################################### -# Finally, let's compare the error convergence of the two variations of PnP +# Let's finally compare the error convergence of the four variations of PnP plt.figure(figsize=(12, 3)) -plt.plot(errhistpg, "k", lw=2, label="PG") -plt.plot(errhistadmm, "r", lw=2, label="ADMM") +plt.semilogy(errhistadmm, "k", lw=2, label="ADMM") +plt.semilogy(errhistpg, "r", lw=2, label="PG") +plt.semilogy(errhisthqs, "b", lw=2, label="HQS") +plt.semilogy(errhistadmm1, "--b", lw=2, label="ADMM with rel.") plt.title("Error norm") plt.legend() plt.tight_layout() + +############################################################################### +# This final results clearly shows the importance of relaxation also for ADMM. From 81a1365a485a5c183be028e63d4107100a44ea87 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 11 May 2026 23:02:49 +0100 Subject: [PATCH 11/73] feat: added RED operator --- docs/source/api/index.rst | 1 + pyproximal/optimization/pnp.py | 3 +- pyproximal/proximal/RED.py | 187 ++++++++++++++++++++++++++++++++ pyproximal/proximal/__init__.py | 3 + 4 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 pyproximal/proximal/RED.py diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index cb1a60e5..11960d47 100755 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -100,6 +100,7 @@ Non-Convex Log1 QuadraticEnvelopeCard QuadraticEnvelopeCardIndicator + RED RelaxedMumfordShah SCAD diff --git a/pyproximal/optimization/pnp.py b/pyproximal/optimization/pnp.py index d40bf266..966a66fa 100644 --- a/pyproximal/optimization/pnp.py +++ b/pyproximal/optimization/pnp.py @@ -15,7 +15,8 @@ class _Denoise(ProxOperator): denoiser : :obj:`func` Denoiser (must be a function with two inputs, the first is the signal to be denoised, the second is the `tau` constant of the y-update in - the PnP optimization) + the PnP optimization, which should be interpreted as the strenght of + the denoiser) dims : :obj:`tuple` Dimensions used to reshape the vector ``x`` in the ``prox`` method prior to calling the ``denoiser`` diff --git a/pyproximal/proximal/RED.py b/pyproximal/proximal/RED.py new file mode 100644 index 00000000..932771cd --- /dev/null +++ b/pyproximal/proximal/RED.py @@ -0,0 +1,187 @@ +from collections.abc import Callable +from typing import Any + +from pylops.utils.backend import get_array_module +from pylops.utils.typing import NDArray, ShapeLike +from typing_extensions import Self + +from pyproximal.proximal.L1 import _current_sigma +from pyproximal.ProxOperator import ProxOperator, _check_tau +from pyproximal.utils.typing import FloatCallableLike + + +class _Denoise: + r"""Denoiser of choice + + Parameters + ---------- + denoiser : :obj:`func` + Denoiser (must be a function with two inputs, the first is the signal + to be denoised, the second is the strenght of the denoiser `sigma`) + dims : :obj:`tuple` + Dimensions used to reshape the vector ``x`` in the ``prox`` method + prior to calling the ``denoiser`` + + """ + + def __init__( + self, + denoiser: Callable[[NDArray, float], NDArray], + dims: ShapeLike, + ) -> None: + self.denoiser = denoiser + self.dims = dims + + def __call__(self, x: NDArray, tau: float) -> NDArray: + x = x.reshape(self.dims) + xden = self.denoiser(x, tau) + return xden.ravel() + + +class RED(ProxOperator): + r"""Regularization by Denoising (RED) + + Regularization by Denoising loss: + :math:`RED(\mathbf{x}) = \sigma\mathbf{x}^T (\mathbf{x} - + f_{\sigma_d}(\mathbf{x}))` + + Parameters + ---------- + denoiser : :obj:`func` + Denoiser (must be a function with one input corresponding to + the signal to be denoised) + dims : :obj:`tuple` + Dimensions used to reshape the vector ``x`` in the ``prox`` method + prior to calling the ``denoiser`` + sigma : :obj:`float`, optional + Multiplicative coefficient of RED term + sigmad : :obj:`float` or :obj:`numpy.ndarray` or :obj:`func`, optional + Strenght of the denoiser. This can be a constant number or a function + that is called passing a counter which keeps track of how many + times the ``grad`` or ``prox`` methods has been invoked before and + returns a scalar (or a list of) ``sigma`` to be used + x0 : :obj:`numpy.ndarray`, optional + Initial vector of iterative scheme used to compute the proximal + niter : :obj:`int`, optional + Number of iterations of iterative scheme used to compute the proximal + warm : :obj:`bool`, optional + Warm start (``True``) or not (``False``). Uses estimate from previous + call of ``prox`` method. + call : :obj:`bool`, optional + Evalutate call method (``True``) or not (``False``) + + Notes + ----- + The gradient of the RED loss is defined as: + + .. math:: + + \nabla_\mathbf{x} RED(\mathbf{x}) = + \sigma (\mathbf{x} - f_{\sigma_d}(\mathbf{x})) + + whilst the proximal operator is obtained by solving the + minimization problem + + .. math:: + + prox_{\tau RED} (\mathbf{x}) = \argmin_{\mathbf{y}} RED(\mathbf{y}) + + \frac{1}{2 \tau}||\mathbf{y} - \mathbf{x}||^2_2 + + via the following fixed-point iteration: + + .. math:: + + \mathbf{y}^k = \frac{1}{\beta + \sigma} (\sigma f_{\sigma_d}(\mathbf{y}^{k-1}) + + \beta \mathbf{x}) + + where :math:`\beta=1/\tau`. + + References + ---------- + .. [1] Romano, Y., Elad, M., and Milanfar, P. + "The Little Engine that Could Regularization by + Denoising (RED)", SIAM Journal on Imaging Science. + 2017. + + """ + + def __init__( + self, + denoiser: Callable[[NDArray, float], NDArray], + dims: ShapeLike, + sigma: float = 1.0, + sigmad: FloatCallableLike = 1.0, + x0: NDArray | None = None, + niter: int = 10, + warm: bool = True, + call: bool = True, + ) -> None: + super().__init__(None, False) + + self.denoiser = _Denoise(denoiser, dims=dims) + self.sigma = sigma + self.sigmad = sigmad + self.x0 = x0 + self.niter = niter + self.warm = warm + self.call = call + self.count = 0 + + def __call__(self, x: NDArray) -> bool | float: + """Evaluate RED loss + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Vector + + Returns + ------- + :obj:`float` + - return ``0.0`` immediately if ``call=False`` + - return dot-product of the input and residual + if ``call=True`` + """ + if not self.call: + return 0.0 + else: + ncp = get_array_module(x) + sigmad = _current_sigma(self.sigmad, self.count) + res = self.sigma * (x - self.denoiser(x, sigmad)) + return float(ncp.dot(x, res)) + + def _increment_count(func: Callable[..., Any]) -> Callable[..., Any]: + """Increment counter""" + + def wrapped(self: Self, *args: Any, **kwargs: Any) -> Any: + self.count += 1 + return func(self, *args, **kwargs) + + return wrapped + + @_increment_count + @_check_tau + def prox(self, x: NDArray, tau: float, **kwargs: Any) -> NDArray: + ncp = get_array_module(x) + beta = 1.0 / tau + sigmad = _current_sigma(self.sigmad, self.count) + + # Define starting guess + if self.x0 is None: + sol = ncp.zeros_like(x) + else: + sol = self.x0 + + # Fixed point iterations + for _ in range(self.niter): + den = self.denoiser(sol, sigmad) + sol = (self.sigma * den + beta * x) / (self.sigma + beta) + if self.warm: + self.x0 = sol + return sol + + @_increment_count + def grad(self, x: NDArray) -> NDArray: + sigmad = _current_sigma(self.sigmad, self.count) + res = x - self.denoiser(x, sigmad) + return self.sigma * res diff --git a/pyproximal/proximal/__init__.py b/pyproximal/proximal/__init__.py index 08c53727..05d669d1 100644 --- a/pyproximal/proximal/__init__.py +++ b/pyproximal/proximal/__init__.py @@ -43,6 +43,7 @@ HalfSpace Half space indicator GenericIntersectionProx Indicator of projection onto a union of given sets Sum Proximal operator of the sum of proximable functions + RED Regularization by Denoising """ from .Box import * @@ -73,6 +74,7 @@ from .HalfSpace import * from .GenericIntersection import * from .Sum import * +from .RED import * __all__ = [ @@ -115,4 +117,5 @@ "HalfSpace", "GenericIntersectionProx", "Sum", + "RED", ] From ab187f770066bf1dc1d4153df0c6d56ab5aec6cd Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 11 May 2026 23:13:52 +0100 Subject: [PATCH 12/73] doc: added red tutorial --- pyproximal/proximal/RED.py | 4 +- tutorials/red.py | 221 +++++++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 tutorials/red.py diff --git a/pyproximal/proximal/RED.py b/pyproximal/proximal/RED.py index 932771cd..19e78cfe 100644 --- a/pyproximal/proximal/RED.py +++ b/pyproximal/proximal/RED.py @@ -51,8 +51,8 @@ class RED(ProxOperator): Denoiser (must be a function with one input corresponding to the signal to be denoised) dims : :obj:`tuple` - Dimensions used to reshape the vector ``x`` in the ``prox`` method - prior to calling the ``denoiser`` + Dimensions used to reshape the vector ``x`` in the ``denoiser`` + method prior to applying the denoiser sigma : :obj:`float`, optional Multiplicative coefficient of RED term sigmad : :obj:`float` or :obj:`numpy.ndarray` or :obj:`func`, optional diff --git a/tutorials/red.py b/tutorials/red.py new file mode 100644 index 00000000..923b6548 --- /dev/null +++ b/tutorials/red.py @@ -0,0 +1,221 @@ +r""" +Regularization by Denoising (RED) +================================= +This is a follow up tutorial to the :ref:`sphx_glr_tutorials_plugandplay.py` tutorial, +showcasing an competitive technical of the famous Plug-and-Play method called +Regularization by Denoising (RED). + +The Plug-and-Play algorithm leverges a user-defined denoiser in place of the proximal +operator of the regularization term in the solution of an inverse problem, ultimately +acting as an implicit prior; RED, instead, defines an the following +explicit regularization term + +.. math:: + RED(\mathbf{x}) = \sigma\mathbf{x}^T (\mathbf{x} - f_{\sigma_d}(\mathbf{x})) + +where the dot-product of the sought after model and residual from the action of +the denoiser is minimized. + +Let's consider again a simplified MRI experiment, where the +data is created by appling a 2D Fourier Transform to the input model and +by randomly sampling 60% of its values, and the +`BM3D `_ method as the denoiser of choice. + +Two different solvers will be compared, namely: + +- Gradient descent, which simply uses the gradient of the data misfit term and that + of the (now well defined and differentiable) regularization term; +- ADMM, where the proximal of RED is solved using a fixed-point iteration. +- Fixed-point method. + +""" + +import bm3d +import matplotlib.pyplot as plt +import numpy as np +import pylops +from pylops.config import set_ndarray_multiplication +from pylops.utils.metrics import snr +from scipy.sparse.linalg import lsqr + +import pyproximal + +plt.close("all") +np.random.seed(0) +set_ndarray_multiplication(False) + + +############################################################################### +# Let's first write a simple gradient descent solver and a fixed-point solver +def GradientDescent(f, g, x0, xtrue, alpha=1.0, niter=100): + x = x0.copy() + errhist = [] + for _ in range(niter): + grad = f.grad(x).real + g.grad(x) + x -= alpha * grad + errhist.append(np.linalg.norm(x - xtrue)) + return x, errhist + + +def FixedPoint(Op, y, denoiser, x0, xtrue, sigma, sigmad, niter=100, niter_inner=10): + x = x0.copy() + yy = Op.H @ y + sigmad = sigmad * np.ones(niter) if isinstance(sigmad, float) else sigmad + errhist = [] + for i in range(niter): + xden = denoiser(x, sigmad(i)) + Op1 = Op1 = sigma * pylops.Identity(Op.shape[1], dtype=Op.dtype) + Op.H * Op + y1 = yy + sigma * xden + x = x = lsqr(Op1, y1, iter_lim=niter_inner, x0=x)[0] + errhist.append(np.linalg.norm(x - xtrue)) + return x, errhist + + +############################################################################### +# We start by loading the famous Shepp logan phantom and creating the +# modelling operator +x = np.load("../testdata/shepp_logan_phantom.npy") +x = x / x.max() +ny, nx = x.shape + +perc_subsampling = 0.6 +nxsub = int(np.round(ny * nx * perc_subsampling)) +iava = np.sort(np.random.permutation(np.arange(ny * nx))[:nxsub]) +Rop = pylops.Restriction(ny * nx, iava, dtype=np.complex128) +Fop = pylops.signalprocessing.FFT2D(dims=(ny, nx)) + +############################################################################### +# We now create and display the data alongside the model +y = Rop * Fop * x.ravel() +yfft = Fop * x.ravel() +yfft = np.fft.fftshift(yfft.reshape(ny, nx)) + +ymask = Rop.mask(Fop * x.ravel()) +ymask = ymask.reshape(ny, nx) +ymask.data[:] = np.fft.fftshift(ymask.data) +ymask.mask[:] = np.fft.fftshift(ymask.mask) + +fig, axs = plt.subplots(1, 3, figsize=(14, 5)) +axs[0].imshow(x, vmin=0, vmax=1, cmap="gray") +axs[0].set_title("Model") +axs[0].axis("tight") +axs[1].imshow(np.abs(yfft), vmin=0, vmax=1, cmap="rainbow") +axs[1].set_title("Full data") +axs[1].axis("tight") +axs[2].imshow(np.abs(ymask), vmin=0, vmax=1, cmap="rainbow") +axs[2].set_title("Sampled data") +axs[2].axis("tight") +plt.tight_layout() + +############################################################################### +# At this point we create a denoiser instance using the BM3D algorithm and use +# the gradient descent solver that we wrote at the start + + +def sigmad(iiter): + return 0.1 * 0.99**iiter + + +# BM3D denoiser +denoiser = lambda x, sigma: bm3d.bm3d( + np.real(x), sigma_psd=sigma, stage_arg=bm3d.BM3DStages.HARD_THRESHOLDING +) + +l2 = pyproximal.proximal.L2(Op=Rop * Fop, b=y.ravel()) +red = pyproximal.proximal.RED(denoiser, x.shape, sigma=0.4, sigmad=sigmad, call=False) + +xredgd, errhistgd = GradientDescent( + l2, + red, + x0=np.zeros(x.size), + xtrue=x.ravel(), + alpha=0.5, + niter=50, +) +xredgd = np.real(xredgd.reshape(x.shape)) + +############################################################################### +# And now we use the ADMM solver + + +def callback(x, xtrue, errhist): + errhist.append(np.linalg.norm(x - xtrue)) + + +Op = Rop * Fop +L = np.real((Op.H * Op).eigs(neigs=1, which="LM")[0]) +tau = 1.0 / L + +# BM3D denoiser +denoiser = lambda x, sigma: bm3d.bm3d( + np.real(x), sigma_psd=sigma, stage_arg=bm3d.BM3DStages.HARD_THRESHOLDING +) + +# ADMM-RED +l2 = pyproximal.proximal.L2(Op=Op, b=y.ravel(), niter=10, warm=True) +red = pyproximal.proximal.RED( + denoiser, x.shape, sigma=0.4, sigmad=sigmad, niter=5, warm=True, call=False +) + +errhistadmm = [] +xredadmm = pyproximal.optimization.pnp.ADMM( + l2, + red, + tau=1.0, + x0=np.zeros(x.size), + niter=50, + show=True, + callback=lambda xx: callback(xx, x.ravel(), errhistadmm), +)[0] +xredadmm = np.real(xredadmm.reshape(x.shape)) + +############################################################################### +# And finally we use the Fixed-Point solver + +# BM3D +xshape = x.shape +den = lambda x, sigma: bm3d.bm3d( + x.real.reshape(xshape), sigma_psd=sigma, stage_arg=bm3d.BM3DStages.HARD_THRESHOLDING +).ravel() + +# FP-RED +xredfp, errhistfp = FixedPoint( + Rop * Fop, + y.ravel(), + den, + x0=np.zeros(x.size), + xtrue=x.ravel(), + sigma=0.4, + sigmad=sigmad, + niter=50, + niter_inner=10, +) +xredfp = np.real(xredfp.reshape(x.shape)) + +############################################################################### +# Let's finally compare the results and the error convergence of the three +# variations of RED + +fig, axs = plt.subplots(1, 4, sharey=True, figsize=(15, 5)) +axs[0].imshow(x, vmin=0, vmax=1, cmap="gray") +axs[0].set_title("Model") +axs[0].axis("tight") +axs[1].imshow(xredgd, vmin=0, vmax=1, cmap="gray") +axs[1].set_title(f"GD-RED (SNR={snr(x, xredgd):.2f} dB)") +axs[1].axis("tight") +axs[2].imshow(xredadmm, vmin=0, vmax=1, cmap="gray") +axs[2].set_title(f"ADMM-RED (SNR={snr(x, xredadmm):.2f} dB)") +axs[2].axis("tight") +axs[3].imshow(xredfp, vmin=0, vmax=1, cmap="gray") +axs[3].set_title(f"FP-RED (SNR={snr(x, xredfp):.2f} dB)") +axs[3].axis("tight") +plt.tight_layout() + +plt.figure(figsize=(12, 3)) +plt.semilogy(errhistgd, "k", lw=2, label="GD") +plt.semilogy(errhistadmm, "r", lw=2, label="ADMM") +plt.semilogy(errhistfp, "b", lw=2, label="FP") + +plt.title("Error norm") +plt.legend() +plt.tight_layout() From a32069363aaac02cf23cf55319081055a191d3ff Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 18 May 2026 21:07:56 +0100 Subject: [PATCH 13/73] feat: added RED solver --- pyproximal/optimization/red.py | 293 +++++++++++++++++++++++++++++++++ pyproximal/proximal/RED.py | 4 +- 2 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 pyproximal/optimization/red.py diff --git a/pyproximal/optimization/red.py b/pyproximal/optimization/red.py new file mode 100644 index 00000000..71361c11 --- /dev/null +++ b/pyproximal/optimization/red.py @@ -0,0 +1,293 @@ +import time +from collections.abc import Callable +from typing import Any, Literal, cast + +import numpy as np +from pylops import LinearOperator +from pylops.basicoperators import Identity +from pylops.optimization.basic import lsqr +from pylops.utils.backend import to_numpy +from pylops.utils.typing import NDArray + +from pyproximal.optimization.primal import ADMM +from pyproximal.proximal import L2 +from pyproximal.proximal import RED as pRED +from pyproximal.ProxOperator import ProxOperator + + +def _GradientDescent( + f: ProxOperator, + g: ProxOperator, + x0: NDArray, + alpha: float = 1.0, + niter: int = 100, + callback: Callable[[NDArray], None] | None = None, + show: bool = False, +) -> NDArray: + """Gradient descent solver for composite objective. + + Parameters + ---------- + f: :obj:`pyproximal.ProxOperator` + First proximal operator (must implement ``grad`` and ``call``) + g: :obj:`pyproximal.ProxOperator` + Second proximal operator (must implement ``grad`` and ``call``) + x0 : :obj:`numpy.ndarray` + Initial vector + alpha : :obj:`float`, optional + Step size + niter : :obj:`int`, optional + Number of iterations + callback : :obj:`callable`, optional + Function with signature (``callback(x)``) to call after each iteration + where ``x`` is the current model vector + show: :obj:`bool`, optional + Display iterations log + + Returns + ------- + x : :obj:`numpy.ndarray` + Inverted model + + """ + if show: + tstart = time.time() + print( + "Gradient descent algorithm\n" + "---------------------------------------------------------\n" + "Proximal operator (f): %s\n" + "Proximal operator (g): %s\n" + "alpha = %10e\tniter = %d\n" % (type(f), type(g), alpha, niter) + ) + head = " Itn x[0] f" + print(head) + + x = x0.copy() + for iiter in range(niter): + grad = f.grad(x).real + g.grad(x) + x -= alpha * grad + if callback is not None: + callback(x) + if show: + if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: + msg = "%6g %12.5e %10.3e" % ( + iiter + 1, + np.real(to_numpy(x[0])), + f(x), + ) + print(msg) + if show: + print("\nTotal time (s) = %.2f" % (time.time() - tstart)) + print("---------------------------------------------------------\n") + return x + + +def _FixedPoint( + Op: LinearOperator, + y: NDArray, + denoiser: Callable[[NDArray, float], NDArray], + x0: NDArray, + sigmad: float | Callable[[int], Any], + sigmaOp: float, + sigma: float, + niter: int = 100, + niter_inner: int = 10, + callback: Callable[[NDArray], None] | None = None, + show: bool = False, +) -> NDArray: + """Fixed-point solver for L2 data-misfit term and RED term. + + Parameters + ---------- + Op: :obj:`pylops.LinearOperator` + Linear operator of L2 data-misfit term + y: :obj:`numpy.ndarray` + Data vector + denoiser: :obj:`callable` + Denoising function + x0 : :obj:`numpy.ndarray` + Initial vector + sigmad : :obj:`float`, optional + Strenght of the denoiser + sigmaOp : :obj:`float`, optional + Multiplicative coefficient of L2 data-misfit term + sigma : :obj:`float` or :obj:`func`, optional + Multiplicative coefficient of RED term + niter : :obj:`int`, optional + Number of iterations + niter_inner : :obj:`int`, optional + Number of iterations for the inner solver + callback : :obj:`callable`, optional + Function with signature (``callback(x)``) to call after each iteration + where ``x`` is the current model vector + show: :obj:`bool`, optional + Display iterations log + + Returns + ------- + x : :obj:`numpy.ndarray` + Inverted model + + """ + if show: + tstart = time.time() + print( + "Fixed point algorithm\n" + "---------------------------------------------------------\n" + "Linear Operator: %s\n" + "Denoiser: %s\n" + "sigmad = %s\tsigmaOp = %10e\tsigma = %10e\n" + "niter = %10e\tniter_inner = %d\n" + % ( + type(Op), + type(denoiser), + "multi" if callable(sigmad) else str(sigmad), + sigmaOp, + sigma, + niter, + niter_inner, + ) + ) + head = " Itn x[0] f" + print(head) + + x = x0.copy() + yy = sigmaOp * Op.H @ y + Iop = Identity(Op.shape[1], dtype=Op.dtype) + Op1 = sigma * Iop + sigmaOp * Op.H * Op + for iiter in range(niter): + xden = denoiser(x, sigmad(iiter) if callable(sigmad) else sigmad) + y1 = yy + sigma * xden + x = x = lsqr(Op1, y1, niter=niter_inner, x0=x)[0] + if callback is not None: + callback(x) + if show: + if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: + msg = "%6g %12.5e %10.3e" % ( + iiter + 1, + np.real(to_numpy(x[0])), + sigmaOp / 2.0 * np.linalg.norm(Op * x - y) ** 2, + ) + print(msg) + if show: + print("\nTotal time (s) = %.2f" % (time.time() - tstart)) + print("---------------------------------------------------------\n") + + return x + + +def RED( + proxf: ProxOperator, + red: pRED, + x0: NDArray, + solver: Callable[..., NDArray] | Literal["gradientdescent", "fixedpoint"] = ADMM, + **kwargs_solver: dict[str, Any], +) -> NDArray | tuple[NDArray, ...]: + r"""Regularization by Denoising (RED) solver + + Solves the following minimization problem: + + .. math:: + + \mathbf{x} = \argmin_{\mathbf{x}} + f(\mathbf{x}) + \sigma \mathbf{x}^T (\mathbf{x} - + g_{\sigma_d}(\mathbf{x})) + + using either any of the proximal solvers in + :mod:`pyproximal.optimization.primal` or + :mod:`pyproximal.optimization.primaldual`, or the + gradient descent of fixed point solvers. + + Here :math:`f(\mathbf{x})` is a function that has a known gradient or + proximal operator, whilst :math:`g_{\sigma_d}(\mathbf{x})` is a denoiser + acting on a noisy signal with noise strenght equal to `\sigma_d`. + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function. In the case of `solver="gradientdescent", it must implement + the `grad` method + red : :obj:`pyproximal.proximal.RED` + RED operator + x0 : :obj:`numpy.ndarray` + Initial vector + solver : :func:`pyproximal.optimization.primal` or :func:`pyproximal.optimization.primaldual` or :obj:`str` + Solver of choice or ``"gradientdescent"`` for gradient-descent solver or ``"fixedpoint"`` + for fixed-point solver + kwargs_solver : :obj:`dict` + Additional parameters required by the selected solver. For ``solver="gradientdescent"``, + the parameter ``alpha`` corresponds to the step size; for ``solver="fixedpoint"``, + the parameter ``niter_inner`` corresponds to the number of iterations of the inner loop + + Returns + ------- + out : :obj:`numpy.ndarray` or :obj:`tuple` + Output of the solver of choice. For + + Raises + ------ + ValueError + If ``solver="gradientdescent"`` and ``proxf`` does not implement the ``grad`` method + or ``solver="fixedpoint"`` and ``proxf`` is not of :class:`pyproximal.proximal.L2` type. + + Notes + ----- + The Regularization by Denoising (RED) term can be used with any proximal + solvers, since its proximal operator can be easily evaluated via + fixed-point iterations (see Notes of :class:`pyproximal.proximal.RED` + for more details). + + However, [1]_ presented two additional solvers, namely: + + - gradient descent (for differentiable :math:`f`): + + .. math:: + + \mathbf{x}^{k+1} = \mathbf{x}^k - \alpha + (\nabla_f(\mathbf{x}^k) + \mathbf{x}^k - + g_{\sigma_d}(\mathbf{x}^k)) + + - fixed point (for :math:`f= \frac{\sigma_{\mathbf{Op}}}{2} ||\mathbf{Op}\mathbf{x} - \mathbf{b}||_2^2`): + + .. math:: + + \mathbf{y}^k = g_{\sigma_d}(\mathbf{x}^k)\\ + \mathbf{x}^{k+1} = (\sigma_{\mathbf{Op}} \mathbf{Op}^H\mathbf{Op} + + \sigma \mathbf{I})^{-1} (\sigma_{\mathbf{Op}} \mathbf{Op}^H \mathbf{b} + + \sigma \mathbf{y}^k) + + References + ---------- + .. [1] Romano, Y., Elad, M., and Milanfar, P. + "The Little Engine that Could Regularization by + Denoising (RED)", SIAM Journal on Imaging Science. + 2017. + + """ + + if isinstance(solver, str): + if solver == "gradientdescent": + if hasattr(proxf, "grad") and callable(proxf.grad): + return _GradientDescent( + proxf, red, x0=x0, **cast(dict[str, Any], kwargs_solver) + ) + else: + msg = f"The provided proximal operator ({proxf}) does not implement the grad method..." + raise ValueError(msg) + else: + if isinstance(proxf, L2): + return _FixedPoint( + proxf.Op, + proxf.b, + red.denoiser, + x0, + red.sigmad, + proxf.sigma, + red.sigma, + **cast(dict[str, Any], kwargs_solver), + ) + else: + msg = f"The proximal operator must be of type L2, ({type(proxf).__name__}) provided instead..." + raise ValueError(msg) + else: + return solver(proxf, red, x0=x0, **kwargs_solver) diff --git a/pyproximal/proximal/RED.py b/pyproximal/proximal/RED.py index 19e78cfe..c4cc039c 100644 --- a/pyproximal/proximal/RED.py +++ b/pyproximal/proximal/RED.py @@ -41,7 +41,7 @@ def __call__(self, x: NDArray, tau: float) -> NDArray: class RED(ProxOperator): r"""Regularization by Denoising (RED) - Regularization by Denoising loss: + Regularization by Denoising loss [1]_: :math:`RED(\mathbf{x}) = \sigma\mathbf{x}^T (\mathbf{x} - f_{\sigma_d}(\mathbf{x}))` @@ -55,7 +55,7 @@ class RED(ProxOperator): method prior to applying the denoiser sigma : :obj:`float`, optional Multiplicative coefficient of RED term - sigmad : :obj:`float` or :obj:`numpy.ndarray` or :obj:`func`, optional + sigmad : :obj:`float` or :obj:`func`, optional Strenght of the denoiser. This can be a constant number or a function that is called passing a counter which keeps track of how many times the ``grad`` or ``prox`` methods has been invoked before and From 76fbb0fe8856b5e6aaff094aaa6a0bc4c9034dba Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 18 May 2026 21:08:15 +0100 Subject: [PATCH 14/73] doc: switched red tutorial to use RED solver --- tutorials/red.py | 78 ++++++++++++++++++------------------------------ 1 file changed, 29 insertions(+), 49 deletions(-) diff --git a/tutorials/red.py b/tutorials/red.py index 923b6548..71072a25 100644 --- a/tutorials/red.py +++ b/tutorials/red.py @@ -36,7 +36,6 @@ import pylops from pylops.config import set_ndarray_multiplication from pylops.utils.metrics import snr -from scipy.sparse.linalg import lsqr import pyproximal @@ -44,37 +43,10 @@ np.random.seed(0) set_ndarray_multiplication(False) - -############################################################################### -# Let's first write a simple gradient descent solver and a fixed-point solver -def GradientDescent(f, g, x0, xtrue, alpha=1.0, niter=100): - x = x0.copy() - errhist = [] - for _ in range(niter): - grad = f.grad(x).real + g.grad(x) - x -= alpha * grad - errhist.append(np.linalg.norm(x - xtrue)) - return x, errhist - - -def FixedPoint(Op, y, denoiser, x0, xtrue, sigma, sigmad, niter=100, niter_inner=10): - x = x0.copy() - yy = Op.H @ y - sigmad = sigmad * np.ones(niter) if isinstance(sigmad, float) else sigmad - errhist = [] - for i in range(niter): - xden = denoiser(x, sigmad(i)) - Op1 = Op1 = sigma * pylops.Identity(Op.shape[1], dtype=Op.dtype) + Op.H * Op - y1 = yy + sigma * xden - x = x = lsqr(Op1, y1, iter_lim=niter_inner, x0=x)[0] - errhist.append(np.linalg.norm(x - xtrue)) - return x, errhist - - ############################################################################### # We start by loading the famous Shepp logan phantom and creating the # modelling operator -x = np.load("../testdata/shepp_logan_phantom.npy") +x = np.load("../../pyproximal/testdata/shepp_logan_phantom.npy") x = x / x.max() ny, nx = x.shape @@ -83,6 +55,7 @@ def FixedPoint(Op, y, denoiser, x0, xtrue, sigma, sigmad, niter=100, niter_inner iava = np.sort(np.random.permutation(np.arange(ny * nx))[:nxsub]) Rop = pylops.Restriction(ny * nx, iava, dtype=np.complex128) Fop = pylops.signalprocessing.FFT2D(dims=(ny, nx)) +Op = Rop * Fop ############################################################################### # We now create and display the data alongside the model @@ -112,6 +85,10 @@ def FixedPoint(Op, y, denoiser, x0, xtrue, sigma, sigmad, niter=100, niter_inner # the gradient descent solver that we wrote at the start +def callback(x, xtrue, errhist): + errhist.append(np.linalg.norm(x - xtrue)) + + def sigmad(iiter): return 0.1 * 0.99**iiter @@ -121,28 +98,25 @@ def sigmad(iiter): np.real(x), sigma_psd=sigma, stage_arg=bm3d.BM3DStages.HARD_THRESHOLDING ) -l2 = pyproximal.proximal.L2(Op=Rop * Fop, b=y.ravel()) +l2 = pyproximal.proximal.L2(Op=Op, b=y.ravel()) red = pyproximal.proximal.RED(denoiser, x.shape, sigma=0.4, sigmad=sigmad, call=False) -xredgd, errhistgd = GradientDescent( +errhistgd = [] +xredgd = pyproximal.optimization.red.RED( l2, red, x0=np.zeros(x.size), - xtrue=x.ravel(), + solver="gradientdescent", alpha=0.5, niter=50, + callback=lambda xx: callback(xx, x.ravel(), errhistgd), + show=True, ) xredgd = np.real(xredgd.reshape(x.shape)) -############################################################################### +################################################################################ # And now we use the ADMM solver - -def callback(x, xtrue, errhist): - errhist.append(np.linalg.norm(x - xtrue)) - - -Op = Rop * Fop L = np.real((Op.H * Op).eigs(neigs=1, which="LM")[0]) tau = 1.0 / L @@ -158,11 +132,12 @@ def callback(x, xtrue, errhist): ) errhistadmm = [] -xredadmm = pyproximal.optimization.pnp.ADMM( +xredadmm = pyproximal.optimization.red.RED( l2, red, - tau=1.0, x0=np.zeros(x.size), + solver=pyproximal.optimization.primal.ADMM, + tau=tau, niter=50, show=True, callback=lambda xx: callback(xx, x.ravel(), errhistadmm), @@ -174,21 +149,26 @@ def callback(x, xtrue, errhist): # BM3D xshape = x.shape -den = lambda x, sigma: bm3d.bm3d( +denoiser = lambda x, sigma: bm3d.bm3d( x.real.reshape(xshape), sigma_psd=sigma, stage_arg=bm3d.BM3DStages.HARD_THRESHOLDING ).ravel() # FP-RED -xredfp, errhistfp = FixedPoint( - Rop * Fop, - y.ravel(), - den, +l2 = pyproximal.proximal.L2(Op=Op, b=y.ravel()) +red = pyproximal.proximal.RED( + denoiser, x.shape, sigma=0.4, sigmad=sigmad, niter=5, warm=True, call=False +) + +errhistfp = [] +xredfp = pyproximal.optimization.red.RED( + l2, + red, x0=np.zeros(x.size), - xtrue=x.ravel(), - sigma=0.4, - sigmad=sigmad, + solver="fixedpoint", niter=50, niter_inner=10, + callback=lambda xx: callback(xx, x.ravel(), errhistfp), + show=True, ) xredfp = np.real(xredfp.reshape(x.shape)) From 9c1ad0df06419612e58a1781391540d3a1b96dac Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 18 May 2026 21:08:32 +0100 Subject: [PATCH 15/73] doc: added RED solver to api index --- docs/source/api/index.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index 11960d47..a80ca4ca 100755 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -149,7 +149,6 @@ Utility functions gradtest_bilinear - Solvers ------- @@ -190,6 +189,13 @@ Primal PlugAndPlay +.. currentmodule:: pyproximal.optimization.red + +.. autosummary:: + :toctree: generated/ + + RED + .. currentmodule:: pyproximal.optimization.sr3 .. autosummary:: From 2fc6ecaf0ca6b195ea72a3317ebc3d0f39b9b7c7 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 18 May 2026 21:09:26 +0100 Subject: [PATCH 16/73] minor: added RED to init --- pyproximal/optimization/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pyproximal/optimization/__init__.py b/pyproximal/optimization/__init__.py index 694aac14..748f118d 100644 --- a/pyproximal/optimization/__init__.py +++ b/pyproximal/optimization/__init__.py @@ -18,9 +18,8 @@ ADMML2 ADMM with L2 misfit term LinearizedADMM Linearized ADMM TwIST Two-step Iterative Shrinkage/Threshold - PlugAndPlay Plug-and-Play Prior with ADMM DouglasRachfordSplitting Douglas-Rachford algorithm - PPXA Parallel Proximal Algorithm + PPXA Parallel Proximal algorithm ConsensusADMM Consensus problem with ADMM A list of solvers in ``pyproximal.optimization.proximaldual`` using both proximal @@ -34,7 +33,13 @@ Bregman Bregman iterations -Additional solvers are in ``pyproximal.optimization.sr3`` amd +Similarly, solvers leveraging denoisers are in ``pyproximal.optimization.pnp`` and +``pyproximal.optimization.red``: + + PlugAndPlay Plug-and-Play Prior algorithm + RED Regularization by Denoising algorithm + +Additional solvers are in ``pyproximal.optimization.sr3`` and ``pyproximal.optimization.palm``: SR3 Sparse Relaxed Regularized algorithm @@ -48,4 +53,4 @@ """ -from . import primal, primaldual, bregman, segmentation, sr3, palm, pnp +from . import primal, primaldual, bregman, segmentation, sr3, palm, pnp, red From 4d9be9bb56b11eae45b487310b378f2eaf200c02 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 18 May 2026 21:09:52 +0100 Subject: [PATCH 17/73] minor: docstring improvements --- pyproximal/optimization/pnp.py | 30 ++++++++++++++----------- pyproximal/optimization/segmentation.py | 3 +++ pyproximal/optimization/sr3.py | 8 ++++++- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/pyproximal/optimization/pnp.py b/pyproximal/optimization/pnp.py index 966a66fa..47c760f4 100644 --- a/pyproximal/optimization/pnp.py +++ b/pyproximal/optimization/pnp.py @@ -53,18 +53,19 @@ def PlugAndPlay( ) -> NDArray | tuple[NDArray, ...]: r"""Plug-and-Play Priors with any proximal algorithm of choice - Solves the following minimization problem using any proximal a - lgorithm of choice: + Solves the following minimization problem using any proximal + algorithm of choice: .. math:: - \mathbf{x},\mathbf{z} = \argmin_{\mathbf{x}} - f(\mathbf{x}) + \lambda g(\mathbf{x}) + \mathbf{x} = \argmin_{\mathbf{x}} + f(\mathbf{x}) + \sigma g(\mathbf{x}) where :math:`f(\mathbf{x})` is a function that has a known gradient or - proximal operator and :math:`g(\mathbf{x})` is a function acting as implicit - prior. Implicit means that no explicit function should be defined: instead, - a denoising algorithm of choice is used. See Notes for details. + proximal operator and :math:`g(\mathbf{x})` is a function acting as an + implicit prior. Implicit means that no explicit function should be + defined: instead, a denoising algorithm of choice is used. See Notes + for details. Parameters ---------- @@ -97,25 +98,28 @@ def PlugAndPlay( .. math:: \mathbf{x}^{k+1} = \prox_{\tau f}(\mathbf{z}^{k} - \mathbf{u}^{k})\\ - \mathbf{z}^{k+1} = \operatorname{Denoise}(\mathbf{x}^{k+1} + \mathbf{u}^{k}, \tau \lambda)\\ + \mathbf{z}^{k+1} = \operatorname{Denoise}(\mathbf{x}^{k+1} + \mathbf{u}^{k}, \tau \sigma)\\ \mathbf{u}^{k+1} = \mathbf{u}^{k} + \mathbf{x}^{k+1} - \mathbf{z}^{k+1} where :math:`\operatorname{Denoise}` is a denoising algorithm of choice. This rather peculiar step originates from the intuition that the optimization process associated with the z-update can be interpreted as a denoising inverse problem, or more specifically a MAP denoiser where the noise is gaussian with zero mean and variance - equal to :math:`\tau \lambda`. For this reason any denoising of choice can be used instead of a function with + equal to :math:`\tau \sigma`. For this reason any denoising of choice can be used instead of a function with known proximal operator. - Finally, whilst the :math:`\tau \lambda` denoising parameter should be chosen to + Finally, whilst the :math:`\tau \sigma` denoising parameter should be chosen to represent an estimate of the noise variance (of the denoiser, not the data of the problem we wish to solve!), special care must be taken when setting up the denoiser and calling this optimizer. More specifically, - :math:`\lambda` should not be passed to the optimizer, rather set directly in the denoiser. + :math:`\sigma` should not be passed to the optimizer, rather set directly in the denoiser. On the other hand :math:`\tau` must be passed to the optimizer as it is also affecting the x-update; - when defining the denoiser, ensure that :math:`\tau` is multiplied to :math:`\lambda` as shown in the tutorial. + when defining the denoiser, ensure that :math:`\tau` is multiplied to :math:`\sigma` as shown in the tutorial. - Alternative, as suggested in [2]_, the :math:`\tau` could be set to 1. The parameter :math:`\lambda` can then be set + Alternative, as suggested in [2]_, the :math:`\tau` could be set to 1. The parameter :math:`\sigma` can then be set to maximize the value of the denoiser and a second tuning parameter can be added directly to :math:`f`. + References + ---------- + .. [1] Venkatakrishnan, S. V., Bouman, C. A. and Wohlberg, B. "Plug-and-Play priors for model based reconstruction", IEEE. 2013. diff --git a/pyproximal/optimization/segmentation.py b/pyproximal/optimization/segmentation.py index 5063220a..5f7b7f22 100644 --- a/pyproximal/optimization/segmentation.py +++ b/pyproximal/optimization/segmentation.py @@ -82,6 +82,9 @@ def Segment( :math:`\mathbf{\sigma}=[\sigma_1, ..., \sigma_{N_{cl}}]^T` are vectors representing the optimal mean and standard deviations for each class. + References + ---------- + .. [1] Chambolle, and A., Pock, "A first-order primal-dual algorithm for convex problems with applications to imaging", Journal of Mathematical Imaging and Vision, 40, 8pp. 120–145. 2011. diff --git a/pyproximal/optimization/sr3.py b/pyproximal/optimization/sr3.py index 10f33810..0f79557e 100644 --- a/pyproximal/optimization/sr3.py +++ b/pyproximal/optimization/sr3.py @@ -152,7 +152,7 @@ def SR3( Notes ----- - SR3 uses the following algorithm: + SR3 uses the following algorithm [1]_: .. math:: \mathbf{x}_{k+1} = (\mathbf{A}^T\mathbf{A} + \kappa @@ -161,6 +161,12 @@ def SR3( \mathbf{y}_{k+1} = \prox_{\lambda/\kappa\mathcal{R}} (\mathbf{Lx}_{k+1}) + References + ---------- + .. [1] Zheng, P., Askham, T., Brunton, S. L., Kutz, N., + and Aravkin, A. Y. "A Unified Framework for Sparse Relaxed + Regularized Regression: SR3", https://arxiv.org/abs/1807.05411. + """ (m, n) = Op.shape if x0 is None: From ab9c41b36641e500efe9d0a0ad305ad3bd13d901 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 18 May 2026 21:23:31 +0100 Subject: [PATCH 18/73] fix: fixed path in red tutorial --- tutorials/red.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/red.py b/tutorials/red.py index 71072a25..bc539339 100644 --- a/tutorials/red.py +++ b/tutorials/red.py @@ -46,7 +46,7 @@ ############################################################################### # We start by loading the famous Shepp logan phantom and creating the # modelling operator -x = np.load("../../pyproximal/testdata/shepp_logan_phantom.npy") +x = np.load("../testdata/shepp_logan_phantom.npy") x = x / x.max() ny, nx = x.shape From e6d59d11389853355762939ef1ca3eabaf457902 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sun, 31 May 2026 21:07:49 +0100 Subject: [PATCH 19/73] minor: fix mypy error --- pyproximal/optimization/basesolver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproximal/optimization/basesolver.py b/pyproximal/optimization/basesolver.py index 954b2ab3..b710ab5c 100644 --- a/pyproximal/optimization/basesolver.py +++ b/pyproximal/optimization/basesolver.py @@ -159,8 +159,8 @@ def callback( # noqa: B027 self, x: NDArray, z: NDArray | None = None, - *args, - **kwargs, + *args: Any, + **kwargs: Any, ) -> None: """Callback routine From c3842b2155d5cbb3624273ed74dd7e6c2b56ce82 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 8 Jun 2026 20:50:59 +0100 Subject: [PATCH 20/73] doc: added page for adding solvers --- docs/source/addingsolver.rst | 300 +++++++++++++++++++++++++++++++++++ docs/source/index.rst | 1 + 2 files changed, 301 insertions(+) create mode 100755 docs/source/addingsolver.rst diff --git a/docs/source/addingsolver.rst b/docs/source/addingsolver.rst new file mode 100755 index 00000000..dbafb037 --- /dev/null +++ b/docs/source/addingsolver.rst @@ -0,0 +1,300 @@ +.. _addingsolver: + +|:fire:| Implementing new solvers +################################# + +Users are welcome to create new solvers and add them to the PyProximal library. + +In this tutorial, we will go through the key steps in the definition of a solver, using a +sligthly simplified version of :py:class:`pyproximal.optimization.cls_primal.ProximalGradient` +as an example. + +.. note:: + In case you are already familiar with PyLops and have created a solver before, these guidelines closely + follow those in the PyLops documentation. We aim to highlight every step where the creation + of a PyProximal solver differs from that of a PyLops server or steps that are completely new + for this kind of solvers. + +Creating the solver +------------------- +The first thing we need to do is to locate a file containing solvers in the same family of the solver we plan to +include, or create a new file with the name of the solver we would like to implement (or preferably its family). +Note that as the solver will be a class, we need to follow the UpperCaseCamelCase convention for the class itself +but not for the filename. + +At this point we can start by importing the modules that will be needed by the solver. +This varies from solver to solver, however you will always need to import the +:py:class:`pyproximal.optimization.basesolver.Solver` which will be used as *parent* class for any of our solvers. +Moreover, we always recommend to import :py:func:`pyproximal.utils.backend.get_array_module` as solvers should be written +in such a way that it can work both with ``numpy`` and ``cupy`` arrays. See later for details. + +.. code-block:: python + + import time + + import numpy as np + + from pyproximal.optimization.basesolver import Solver + from pyproximal.utils.backend import get_array_module + + +After that we define our new object: + +.. code-block:: python + + class ProximalGradient(Solver): + +followed by a `numpydoc docstring `__ +(starting with ``r"""`` and ending with ``"""``) containing the documentation of the solver. Such docstring should +contain at least a short description of the solver, a ``Parameters`` section with a description of the +input parameters of the associated ``_init__`` method and a ``Notes`` section providing a reference to the original +solver and possibly a concise mathematical explanation of the solver. Take a look at some of the core solver of PyProximal +to get a feeling of the level of details of the mathematical explanation. + +As for any Python class, our solver will need an ``__init__`` method. In this case, however, we will just rely on that +of the base class. A single input parameter is passed to the ``__init__`` method and saved as members of our class, +namely the :class:`pyproximal.optimization.callback.Callbacks` object that contains any callback we wish to attach +to the solver. Moreover, two additional parameters are created that contains the counter of the iterations (which +will be incremented every time the ``step`` method is called) and the current time (this is used later to report +the execution time of the solver). Here is the ``__init__`` method of the base class: + +.. code-block:: python + + def __init__(self, callbacks=None): + self.callbacks = callbacks + self._registercallbacks() + self.iiter = 0 + self.tstart = time.time() + +We can now move onto writing the *setup* of the solver in the method ``setup``. We will need to write +a piece of code that prepares the solver prior to being able to apply a step. As most solvers in PyProximal are designed +as the sum of two terms (usually defined as ``f`` and ``g``), the setup method usually takes two proximal operators +(generally represented by ``proxf`` and ``proxg``) plus additional parameters as explained below. However, there are some +special cases in which solvers operate on a single proximal, a list of proximals, or a single proximal and some linear +operator plus a data vector; in that case the number and type of the first few inputs of the ``setup`` method may be +different. + +Next the method usually requires the initial guess of the solver ``x0``, alongside various hyperparameters of the solver +--- e.g., step sizes and parameters involved in the stopping criterion. For example in this case we only have nine parameters. +Two of them, ``niter``, which refers to the maximum allowed number of iterations, and ``tol`` to set the +tolerance on the residual norm (the solver will be stopped if this is smaller than the chosen tolerance) are always present. +Moreover, we always have the possibility to decide whether we want to operate the solver (in this case its setup part) in verbose +or silent mode. This is driven by the ``show`` parameter. We will soon discuss how to choose what to print on screen in +case of verbose mode (``show=True``). + +The setup method can be loosely seen as composed of four parts. First, the inputs are stored as members of the class. +Moreover the type of the ``x0`` vector is checked to evaluate whether to use ``numpy`` or ``cupy`` for algebraic operations +(this is done by ``self.ncp = get_array_module(x0)``). As some of the optional parameters hyperparameters may require +some specialized checking and initialization, this is carried out in this second part (see specific solver implementations +for more details). Next, the starting guess or guesses are initialized (see also note below). Finally, a number +of variables are initialized to be used inside the ``step`` method to keep track of the optimization process. Moreover, +note that the ``setup`` method returns the created starting guess ``x`` or guesses (they are not stored as member of the +class). + +.. code-block:: python + + def setup(self, proxf, proxg, x0, epsg, tau, + niter=10, tol=None): + self.proxf = proxf + self.proxg = proxg + self.x0 = x0 + self.epsg = epsg + self.tau = tau + self.niter = niter + self.tol = tol + + self.ncp = get_array_module(x0) + + # more specialized initializations if needed + # ... + # + + # initialize solver + x = x0.copy() + y = x.copy() + + # create variables to track the objective function and iterations + self.pfg, self.pfgold = np.inf, np.inf + self.cost: list[float] = [] + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(epsg_print, np.iscomplexobj(x0)) + return x, y + +.. note:: + For most solvers with multiple proximal operators, more than one initial guess is usually required. + Alongside ``x0``, such solvers may have an additional (optional) input parameter called ``z0``. In + this case, we provide a method called ``_x0z0_init`` which takes ``x0`` and ``z0`` and provides + checks as well as default initialization of ``z0`` when not provided (i.e., ``z0=None``). + +At this point, we need to implement the core of the solver, the ``step`` method. Here, we take the input (or inputs for solvers +with multiple variables) at the previous iterate, update it following the rule of the solver of choice, and return it (or them). +The other input parameter required by this method is ``show`` to choose whether we want to print a report of the step on screen or +not. However, if appropriate, a user can add additional input parameters. A simplified version of the step method for +ProximalGradient is: + +.. code-block:: python + + def step(self, x, y, show=False): + xold = x.copy() + + # proximal step + x = self.proxg.prox( + x - self.tau * self.proxf.grad(x), + self.epsg * self.tau + ) + + # tolerance check + if self.tol is not None: + self.pfgold = self.pfg + pf, pg = self.proxf(x), self.proxg(x) + self.pfg = pf + self.epsg * pg + if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + self.tolbreak = True + else: + pf, pg = 0.0, 0.0 + + self.iiter += 1 + if show: + self._print_step(x, pf, pg) + if self.tol is not None or show: + self.cost.append(float(self.pfg)) + return x, y + +Similarly, we also implement a ``run`` method that is in charge of running a number of iterations by repeatedly +calling the ``step`` method. + +.. code-block:: python + + def run(self, x, y, niter, show, itershow): + while self.iiter < niter and not self.tolbreak: + x, y = self.step(x, y, showstep) + self.callback(x) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x, y + +It is worth noting that any number of callbacks can be attached to the solver; some of these +callbacks can implement a stopping criterion and set the ``stop`` member to True when a given +condition is met. The ``_callback_stop`` method is in change of checking if any of the callbacks +has set ``stop`` to True and in the case break the iterations. + +Finally, it is also usually convenient to implement a ``finalize`` method; this method can do any required post-processing that should +not be applied at the end of each step, rather at the end of the entire optimization process. For ProximalGradient this is not implemented +as the method of the baseclass already performs the needed steps. + +Last but not least, we can wrap it all up in the ``solve`` method. This method takes as input the data, the initial +model and the same hyperparameters of the setup method and runs the entire optimization process. For CG: + +.. code-block:: python + + def solve(self, proxf, proxg, x0, epsg, tau, + niter=10, tol=None, show=False, + itershow=(10, 10, 10)): + x, y = self.setup( + proxf=proxf, + proxg=proxg, + x0=x0, + epsg=epsg, + tau=tau, + niter=niter, + tol=tol, + show=show, + ) + + x, y = self.run(x, y, niter, show=show, itershow=itershow) + self.finalize(81, show) + return x, y, self.iiter, self.cost + +And that's it, we have implemented our first solver operator! + +Although the methods that we just described are enough to implement any solver of choice, we find important to provide +users with feedback during the inversion process. Imagine that the modelling operator is very expensive and can take +minutes (or even hours to run), we don't want to leave a user waiting for hours before they can tell if the solver has +done something meaningful. To avoid such scenario, we can implement so called `_print_*` methods where +``*=solver, setup, step, finalize`` that print on screen some useful information (e.g., first value of the current +estimate, norm of residual, etc.). The ``solver`` and ``finalize`` print are already implemented in the base class, +the other two must be implemented when creating a new solver. When these methods are implemented and a user passes +``show=True`` to the associated method, our solver will provide such information on screen throughout the inverse +process. To better understand how to write such methods, we suggest to look into the source code of the CG method. + +Finally, to be backward compatible with versions of PyProximal, we also want to create a function with the same +name of the class-based solver (but in small letters) which simply instantiates the solver and runs it. This function +is usually placed in the same file of the class-based solver and snake_case should be used for its name. +This function generally takes all the mandatory and optional parameters of the solver as +input and returns some of the most valuable properties of the class-based solver object. An example +for `ProximalGradient` is: + +.. code-block:: python + + def ProximalGradient(proxf, proxg, x0, epsg, tau, niter=10, + tol=None, callback=None, show=False): + pgsolve = cProximalGradient(callbacks=[CostToInitialCallback(rtol), ]) + if callback is not None: + cgsolve.callback = callback + x, _, _, _ = cgsolve.solve( + proxf=proxf, proxg=proxg, x0=x0, epsg=epsg, tau=tau, + niter=niter, tol=tol, show=show, + ) + return x + + +Testing the solver +------------------ +Being able to write a solver is not yet a guarantee of the fact that the solver is correct, or in other words +that the solver can converge to a correct solution. + +We encourage to create a new test within an existing ``test_*.py`` file in the ``pytests`` folder (or in a new file). +We also encourage to test the function-bases solver, as this will implicitly test the underlying class-based solver. + +Generally a test file will start with a number of dictionaries containing different parameters we would like to +use in the testing of one or more solvers. The test itself starts with a *decorator* that contains a list +of all (or some) of dictionaries that will would like to use for our specific operator, followed by +the definition of the test: + +.. code-block:: python + + @pytest.mark.parametrize("par", [(par1),(par2)]) + def test_ProximalGradient(par): + +However, for proximal solvers we often find it difficult to create tests that verify a single solver in isolation +against an expected (or ground-truth) solution due to the convex nature of the objective function. So a strategy +that is commonly used in our test suite is to test two solvers that are capable of solving the same objective +function against each other and verify that they converge to the same solution within some expected tolerance. +We encourage to check our existing tests for inspiration. + + +Documenting the solver +---------------------- +Once the solver has been created, we can add it to the documentation of PyProximal. To do so, simply add the name of +the operator within the ``index.rst`` file in ``docs/source/api`` directory. + +Moreover, in order to facilitate the user of your operator by other users, each test should be included in at least +one of the tutorials as part of the Sphinx-gallery of the documentation of the PyProximal library. The directory +``tutorials`` contains currently available tutorials; if the new solver does not fit any of them, create a new tutorial. + + +Final checklist +--------------- +Before submitting your new solver for review, use the following **checklist** to ensure that your code +adheres to the guidelines of PyProximal: + +- you have added the new solver to a new or existing file in the ``optimization`` directory within the ``pyproximal`` + package. + +- the new class contains at least ``__init__``, ``setup``, ``step``, ``run``, ``finalize``, and ``solve`` methods. + +- each of the above methods have a `numpydoc docstring `__ documenting + at least the input ``Parameters`` and the ``__init__`` method contains also a ``Notes`` section providing a + mathematical explanation of the solver. + +- a new test has been added to an existing ``test_*.py`` file within the ``pytests`` folder. The test should verify + that the new solver converges to the true solution for a well-designed inverse problem (i.e., full rank operator). + +- the new solver is used within at least one *tutorial* (in ``tutorials`` directory). + diff --git a/docs/source/index.rst b/docs/source/index.rst index c5ace64c..f8547092 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -117,6 +117,7 @@ subclassing the :py:class:`pyproximal.ProxOperator` class and by implementing :caption: Getting involved: adding.rst + addingsolver.rst contributing.rst changelog.rst credits.rst From 7a7b01bf8d78aec0b6245a6484db236fff12b2bd Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 8 Jun 2026 20:51:44 +0100 Subject: [PATCH 21/73] minor: moved memory_usage to basesolver --- pyproximal/optimization/basesolver.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pyproximal/optimization/basesolver.py b/pyproximal/optimization/basesolver.py index b710ab5c..1d1b73d8 100644 --- a/pyproximal/optimization/basesolver.py +++ b/pyproximal/optimization/basesolver.py @@ -6,10 +6,13 @@ from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING, Any +import numpy as np from pylops.optimization.basesolver import Solver as pSolver from pylops.optimization.callback import Callbacks from pylops.utils.typing import NDArray +from pyproximal.utils.typing import Tmemunit + if TYPE_CHECKING: from pyproximal.ProxOperator import ProxOperator @@ -84,6 +87,17 @@ def _print_finalize(self, *args: Any, nbar: int = 80, **kwargs: Any) -> None: ) print("-" * nbar + "\n") + def memory_usage( + self, + show: bool = False, + unit: Tmemunit = "B", + ) -> float: + """Compute memory usage of the solver + + Currently not implemented in PyProximal and therefore just overwritten here. + """ + pass + @abstractmethod def setup( self, @@ -92,7 +106,7 @@ def setup( *args: Any, show: bool = False, **kwargs: Any, - ) -> None: + ) -> NDArray | tuple[NDArray, NDArray]: """Setup solver This method is used to setup the solver. Users can change the function signature @@ -151,7 +165,7 @@ def finalize(self, nbar: int = 60, show: bool = False) -> None: """ self.tend = time.time() self.telapsed = self.tend - self.tstart - + self.cost = np.array(self.cost) if show: self._print_finalize(nbar=nbar) From d50866036359272965bc289a58bfbd81e7bc1602 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 8 Jun 2026 21:16:40 +0100 Subject: [PATCH 22/73] test: added more tests for solvers --- pytests/test_solver.py | 156 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 141 insertions(+), 15 deletions(-) diff --git a/pytests/test_solver.py b/pytests/test_solver.py index e416f944..c5f38c69 100644 --- a/pytests/test_solver.py +++ b/pytests/test_solver.py @@ -18,11 +18,13 @@ LinearizedADMM, ProximalGradient, ProximalPoint, + TwIST, ) from pyproximal.proximal import L1, L2, Quadratic -par1 = {"n": 8, "m": 10, "dtype": "float32"} # float64 -par2 = {"n": 8, "m": 10, "dtype": "float64"} # float32 +par1 = {"n": 10, "m": 10, "dtype": "float64"} # square, float64 +par2 = {"n": 8, "m": 10, "dtype": "float64"} # underdetermined, float64 +par3 = {"n": 8, "m": 10, "dtype": "float32"} # underdetermined, float32 def test_ProximalGradient_unknown_acceleration(): @@ -63,7 +65,7 @@ def test_ADMM_noinitial(): def test_ADMML2_noinitial(): """Check that an error is raised if no initial value - is provided to PrimalDual solver + is provided to ADMML2 solver """ with pytest.raises(ValueError, match="Both x0 or"): # Both None @@ -117,7 +119,7 @@ def test_LinearizedADMM_noinitial(): ) -@pytest.mark.parametrize("par", [(par1), (par2)]) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_GPG_weights(par): """Check GPG raises error if weight is not summing to 1""" with pytest.raises(ValueError, match="must be an array of size"): @@ -148,7 +150,7 @@ def test_GPG_weights(par): ) -@pytest.mark.parametrize("par", [(par1), (par2)]) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_ProximalPoint(par): """Check solution of ProximalPoint for quadratic function equals the solution of the associated system of linear equations @@ -171,7 +173,7 @@ def test_ProximalPoint(par): assert_array_almost_equal(xpp, x, decimal=2) -@pytest.mark.parametrize("par", [(par1), (par2)]) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_PG_ISTA(par): """Check equivalency of ProximalGradient and ISTA/FISTA (PyLops)""" np.random.seed(0) @@ -227,7 +229,7 @@ def test_PG_ISTA(par): assert_array_almost_equal(xpg, xista, decimal=2) -@pytest.mark.parametrize("par", [(par1), (par2)]) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_PG_GPG(par): """Check equivalency of ProximalGradient and GeneralizedProximalGradient when using a single regularization term @@ -275,9 +277,55 @@ def test_PG_GPG(par): assert_array_almost_equal(xpg, xgpg, decimal=2) -@pytest.mark.parametrize("par", [(par1), (par2)]) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) +def test_PG_TwiST(par): + """Check that PG/TwiST can be used to solve a sparsity regularized objective function + (note that despite the trajectory will be different, they should converge to the + same solution) + """ + np.random.seed(0) + n, m = par["n"], par["m"] + + # Define sparse model + x = np.zeros(m) + x[2], x[4] = 1, 0.5 + + # Random mixing matrix + R = np.random.normal(0.0, 1.0, (n, m)) + Rop = MatrixMult(R) + + y = Rop @ x + + # Step size + L = (Rop.H * Rop).eigs(1).real + tau = 0.99 / L + + # PG + l2 = L2(Op=Rop, b=y, niter=10, warm=True) + l1 = L1(sigma=5e-1) + xpg = ProximalGradient( + l2, l1, x0=np.zeros(m), tau=tau, niter=100, acceleration="fista" + ) + + # TwiST + l1 = L1(sigma=5e-1) + eigs = np.linalg.eig(R.T @ R)[0] + eigs = (np.abs(eigs[0]), max(1e-1, np.abs(eigs[-1]))) + xtwist = TwIST( + l1, + Rop, + y, + x0=np.zeros(m), + eigs=eigs, + niter=100, + ) + + assert_array_almost_equal(xpg, xtwist, decimal=2) + + +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_HQS_ADMM_L2(par): - """Check that HQS/ADMM can be used to solved a pure L2-based objective function + """Check that HQS/ADMM can be used to solve a pure L2-based objective function (and compare with LSQR - note that despite the trajectory will be different, they should converge to the same solution) """ @@ -326,7 +374,85 @@ def test_HQS_ADMM_L2(par): assert_array_almost_equal(xl2, xadmm, decimal=2) -@pytest.mark.parametrize("par", [(par1), (par2)]) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) +def test_ADMM_ADMML2(par): + """Check equivalency of ADMM and ADMML2 + when the f function is a L2 term + """ + np.random.seed(0) + n, m = par["n"], par["m"] + + # Define sparse model + x = np.random.normal(0.0, 1.0, m).astype(par["dtype"]) + + # Random mixing matrix + R = np.random.normal(0.0, 1.0, (n, m)).astype(par["dtype"]) + Rop = MatrixMult(R, dtype=par["dtype"]) + + y = Rop @ x + + # Step size + Aop = Identity(m) + L = 1.0 # Lipshitz constant of Aop + tau = 0.99 / L + eps = 1e-1 + + # ADMM + l2 = L2(Op=Rop, b=y, niter=10, warm=True) + l2reg = L2(sigma=eps) + xadmm = ADMM(l2, l2reg, x0=np.zeros(m), tau=tau, niter=100)[0] + + # ADMML2 + l2reg = L2(sigma=eps) + xadmml2 = ADMML2( + l2reg, Rop, y, Aop, x0=np.zeros(m), tau=tau, niter=100, iter_lim=10 + )[0] + + assert_array_almost_equal(xadmm, xadmml2, decimal=2) + + +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) +def test_ADMM_LinearizedADMM(par): + """Check equivalency of ADMM and LinearizedADMM + when the f function is a L2 term + """ + np.random.seed(0) + n, m = par["n"], par["m"] + + # Define sparse model + x = np.random.normal(0.0, 1.0, m).astype(par["dtype"]) + + # Random mixing matrix + R = np.random.normal(0.0, 1.0, (n, m)).astype(par["dtype"]) + Rop = MatrixMult(R, dtype=par["dtype"]) + + y = Rop @ x + + # Step size + Aop = Identity(m) + L = 1.0 # Lipshitz constant of Aop + tau = 0.99 / L + mu = 0.99 / L # optimal mu<=tau/maxeig(Dop^H Dop) + + eps = 1e-1 + + # ADMM + l2 = L2(Op=Rop, b=y, niter=10, warm=True) + l2reg = L2(sigma=eps) + xadmm = ADMM(l2, l2reg, x0=np.zeros(m), tau=tau, niter=100)[0] + + # LinearizedADMM + l2 = L2(Op=Rop, b=y, niter=10, warm=True) + l2reg = L2(sigma=eps) + Aop = Identity(m) + xladmm = LinearizedADMM(l2, l2reg, Aop, x0=np.zeros(m), tau=tau, mu=mu, niter=100)[ + 0 + ] + + assert_array_almost_equal(xadmm, xladmm, decimal=2) + + +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_ADMM_DRS(par): """Check equivalency of ADMM and DouglasRachfordSplitting when using a single regularization term @@ -371,7 +497,7 @@ def test_ADMM_DRS(par): assert_array_almost_equal(xadmm, xdrs_f, decimal=2) -@pytest.mark.parametrize("par", [(par1), (par2)]) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_PPXA_with_ADMM(par: dict[str, Any]) -> None: """Check equivalency of PPXA and ADMM when using a single regularization term @@ -413,7 +539,7 @@ def test_PPXA_with_ADMM(par: dict[str, Any]) -> None: assert_array_almost_equal(xppxa, xadmm, decimal=2) -@pytest.mark.parametrize("par", [(par1), (par2)]) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_PPXA_with_GPG(par: dict[str, Any]) -> None: """Check equivalency of PPXA and GeneralizedProximalGradient""" np.random.seed(0) @@ -461,7 +587,7 @@ def test_PPXA_with_GPG(par: dict[str, Any]) -> None: assert_array_almost_equal(xppxa, xgpg, decimal=2) -@pytest.mark.parametrize("par", [(par1), (par2)]) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_ConsensusADMM_with_ADMM(par: dict[str, Any]) -> None: """Check equivalency of ConsensusADMM and ADMM when two proximable functions @@ -503,7 +629,7 @@ def test_ConsensusADMM_with_ADMM(par: dict[str, Any]) -> None: assert_array_almost_equal(xcadmm, xadmm, decimal=2) -@pytest.mark.parametrize("par", [(par1), (par2)]) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_ConsensusADMM_with_ADMM_for_Lasso(par: dict[str, Any]) -> None: """Check equivalency of ConsensusADMM and ADMM when more than two proximable functions for lasso @@ -565,7 +691,7 @@ def test_ConsensusADMM_with_ADMM_for_Lasso(par: dict[str, Any]) -> None: assert_array_almost_equal(xcadmm, xadmm, decimal=2) -@pytest.mark.parametrize("par", [(par1), (par2)]) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_ConsensusADMM_with_GPG(par: dict[str, Any]) -> None: """Check equivalency of ConsensusADMM and GeneralizedProximalGradient""" From c4709a14350b2e9017f3ee3449827078c7b180c6 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 8 Jun 2026 21:24:39 +0100 Subject: [PATCH 23/73] minor: fix mypy issue --- pyproximal/optimization/basesolver.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/pyproximal/optimization/basesolver.py b/pyproximal/optimization/basesolver.py index 1d1b73d8..97739a82 100644 --- a/pyproximal/optimization/basesolver.py +++ b/pyproximal/optimization/basesolver.py @@ -11,8 +11,6 @@ from pylops.optimization.callback import Callbacks from pylops.utils.typing import NDArray -from pyproximal.utils.typing import Tmemunit - if TYPE_CHECKING: from pyproximal.ProxOperator import ProxOperator @@ -87,14 +85,11 @@ def _print_finalize(self, *args: Any, nbar: int = 80, **kwargs: Any) -> None: ) print("-" * nbar + "\n") - def memory_usage( - self, - show: bool = False, - unit: Tmemunit = "B", - ) -> float: + def memory_usage(self) -> None: """Compute memory usage of the solver - Currently not implemented in PyProximal and therefore just overwritten here. + Currently not implemented in PyProximal's solvers + and therefore just overwritten here. """ pass @@ -165,7 +160,7 @@ def finalize(self, nbar: int = 60, show: bool = False) -> None: """ self.tend = time.time() self.telapsed = self.tend - self.tstart - self.cost = np.array(self.cost) + self.cost: NDArray = np.array(self.cost) if show: self._print_finalize(nbar=nbar) From 810a3f859dfcd3887b5b32a5f403e08991aa90f4 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Mon, 8 Jun 2026 21:25:35 +0100 Subject: [PATCH 24/73] feat: added more class solvers --- pyproximal/optimization/cls_primal.py | 1228 +++++++++++++++++++++++-- 1 file changed, 1165 insertions(+), 63 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index 9019eda7..28600599 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -1,20 +1,30 @@ __all__ = [ "ProximalPoint", + "ProximalGradient", + "AndersonProximalGradient", + "GeneralizedProximalGradient", + "HQS", + "ADMM", + "ADMML2", + "LinearizedADMM", + "TwIST", ] from collections.abc import Sequence -from typing import TYPE_CHECKING, Optional, cast +from math import sqrt +from typing import TYPE_CHECKING, Any, Optional, cast import numpy as np import pylops from pylops.optimization.callback import Callbacks +from pylops.optimization.leastsquares import regularized_inversion from pylops.utils.backend import get_array_module, to_numpy from pylops.utils.typing import NDArray from pyproximal.optimization.basesolver import Solver +from pyproximal.proximal import L2 from pyproximal.ProxOperator import ProxOperator from pyproximal.utils.bilinear import BilinearOperator -from pyproximal.utils.typing import Tmemunit if TYPE_CHECKING: from pylops.linearoperator import LinearOperator @@ -167,13 +177,6 @@ def _print_step(self, x: NDArray) -> None: msg = f"{self.iiter:6g} " + strx + f"{self.pf:11.4e}" print(msg) - def memory_usage( - self, - show: bool = False, - unit: Tmemunit = "B", - ) -> None: - pass - def setup( # type: ignore[override] self, prox: "ProxOperator", @@ -454,13 +457,6 @@ def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: ) print(msg) - def memory_usage( - self, - show: bool = False, - unit: Tmemunit = "B", - ) -> None: - pass - def setup( # type: ignore[override] self, proxf: ProxOperator, @@ -897,13 +893,6 @@ def _print_step(self, x: NDArray, pg: float | None) -> None: ) print(msg) - def memory_usage( - self, - show: bool = False, - unit: Tmemunit = "B", - ) -> None: - pass - def setup( # type: ignore[override] self, proxf: ProxOperator, @@ -1013,10 +1002,10 @@ def step( ---------- x : :obj:`numpy.ndarray` Current model vector to be updated by a step of the - proximal gradient algorithm + Andersonproximal gradient algorithm y : :obj:`numpy.ndarray` Additional model vector to be updated by a step of the - proximal gradient algorithm + Anderson proximal gradient algorithm show : :obj:`bool`, optional Display iteration log @@ -1109,7 +1098,7 @@ def run( the Anderson proximal gradient algorithm y : :obj:`numpy.ndarray` Additional model vector to be updated by multiple steps of - the proximal gradient algorithm + the Anderson proximal gradient algorithm niter : :obj:`int`, optional Number of iterations. Can be set to ``None`` if already provided in the setup call @@ -1306,13 +1295,6 @@ def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: ) print(msg) - def memory_usage( - self, - show: bool = False, - unit: Tmemunit = "B", - ) -> None: - pass - def setup( # type: ignore[override] self, proxfs: list[ProxOperator], @@ -1427,10 +1409,10 @@ def step( ---------- x : :obj:`numpy.ndarray` Current model vector to be updated by a step of the - proximal gradient algorithm + generalized proximal gradient algorithm y : :obj:`numpy.ndarray` Additional model vector to be updated by a step of the - proximal gradient algorithm + generalized proximal gradient algorithm show : :obj:`bool`, optional Display iteration log @@ -1506,10 +1488,10 @@ def run( ---------- x : :obj:`numpy.ndarray` Current model vector to be updated by multiple steps of - the proximal gradient algorithm + the generalized proximal gradient algorithm y : :obj:`numpy.ndarray` Additional model vector to be updated by multiple steps of - the proximal gradient algorithm + the generalized proximal gradient algorithm niter : :obj:`int`, optional Number of iterations. Can be set to ``None`` if already provided in the setup call @@ -1699,7 +1681,7 @@ def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: if self.tol is None: pf, pg = self.proxf(x), self.proxg(x) self.pfg = pf + pg - x0 = to_numpy(x[0]) if x.ndim == 1 else to_numpy(x[0, 0]) + x0 = to_numpy(x[0]) strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " msg = ( f"{self.iiter:6g} " @@ -1708,13 +1690,6 @@ def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: ) print(msg) - def memory_usage( - self, - show: bool = False, - unit: Tmemunit = "B", - ) -> None: - pass - def setup( # type: ignore[override] self, proxf: ProxOperator, @@ -1812,10 +1787,10 @@ def step( ---------- x : :obj:`numpy.ndarray` Current model vector to be updated by a step of the - proximal gradient algorithm + HQS algorithm z : :obj:`numpy.ndarray` Additional model vector to be updated by a step of the - proximal gradient algorithm + HQS algorithm show : :obj:`bool`, optional Display iteration log @@ -1868,10 +1843,10 @@ def run( ---------- x : :obj:`numpy.ndarray` Current model vector to be updated by multiple steps of - the proximal gradient algorithm + the HQS algorithm z : :obj:`numpy.ndarray` Additional model vector to be updated by multiple steps of - the proximal gradient algorithm + the HQS algorithm niter : :obj:`int`, optional Number of iterations. Can be set to ``None`` if already provided in the setup call @@ -2073,7 +2048,7 @@ def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: if self.tol is None: pf, pg = self.proxf(x), self.proxg(x) self.pfg = pf + pg - x0 = to_numpy(x[0]) if x.ndim == 1 else to_numpy(x[0, 0]) + x0 = to_numpy(x[0]) strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " msg = ( f"{self.iiter:6g} " @@ -2082,19 +2057,12 @@ def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: ) print(msg) - def memory_usage( - self, - show: bool = False, - unit: Tmemunit = "B", - ) -> None: - pass - def setup( # type: ignore[override] self, proxf: ProxOperator, proxg: ProxOperator, x0: NDArray, - tau: float | NDArray, + tau: float, z0: NDArray | None = None, niter: int = 10, gfirst: bool = False, @@ -2178,10 +2146,10 @@ def step( ---------- x : :obj:`numpy.ndarray` Current model vector to be updated by a step of the - proximal gradient algorithm + ADDM algorithm z : :obj:`numpy.ndarray` Additional model vector to be updated by a step of the - proximal gradient algorithm + ADDM algorithm show : :obj:`bool`, optional Display iteration log @@ -2235,10 +2203,10 @@ def run( ---------- x : :obj:`numpy.ndarray` Current model vector to be updated by multiple steps of - the proximal gradient algorithm + the ADDM algorithm z : :obj:`numpy.ndarray` Additional model vector to be updated by multiple steps of - the proximal gradient algorithm + the ADDM algorithm niter : :obj:`int`, optional Number of iterations. Can be set to ``None`` if already provided in the setup call @@ -2288,7 +2256,7 @@ def solve( # type: ignore[override] proxf: ProxOperator, proxg: ProxOperator, x0: NDArray, - tau: float | NDArray, + tau: float, z0: NDArray | None = None, niter: int = 10, gfirst: bool = False, @@ -2351,9 +2319,1143 @@ def solve( # type: ignore[override] niter=niter, gfirst=gfirst, tol=tol, + callbackz=callbackz, show=show, ) x, z = self.run(x, z, niter, show=show, itershow=itershow) self.finalize(65, show) return x, z, self.iiter, self.cost + + +class ADMML2(Solver): + r"""Alternating Direction Method of Multipliers for L2 misfit term + + Solves the following minimization problem using Alternating Direction + Method of Multipliers: + + .. math:: + + \mathbf{x},\mathbf{z} = \argmin_{\mathbf{x},\mathbf{z}} + \frac{1}{2}||\mathbf{Op}\mathbf{x} - \mathbf{b}||_2^2 + g(\mathbf{z}) \\ + s.t. \; \mathbf{Ax}=\mathbf{z} + + where :math:`g(\mathbf{z})` is any convex function that has a known proximal operator. + + See Also + -------- + ADMM: ADMM + LinearizedADMM: Linearized ADMM + + Notes + ----- + The ADMM algorithm with L2 misfit term can be expressed by the following recursion: + + .. math:: + + \mathbf{x}^{k+1} = \argmin_{\mathbf{x}} \frac{1}{2}||\mathbf{Op}\mathbf{x} + - \mathbf{b}||_2^2 + \frac{1}{2\tau} ||\mathbf{Ax} - \mathbf{z}^k + \mathbf{u}^k||_2^2\\ + \mathbf{z}^{k+1} = \prox_{\tau g}(\mathbf{Ax}^{k+1} + \mathbf{u}^{k})\\ + \mathbf{u}^{k+1} = \mathbf{u}^{k} + \mathbf{Ax}^{k+1} - \mathbf{z}^{k+1} + + .. [1] S. Boyd, N. Parikh, E. Chu, B. Peleato, and J. Eckstein. 2011. + Distributed optimization and statistical learning via the alternating + direction method of multipliers. Foundations and Trends in Machine + Learning, 3 (1), 1-122. https://doi.org/10.1561/2200000016. + + """ + + def _print_setup(self, xcomplex: bool = False) -> None: + self._print_solver(nbar=65) + + strpar = ( + f"Proximal operator (g): {type(self.proxg).__name__}\n" + f"Linear operator (Op): {type(self.Op).__name__}\n" + f"Linear operator (A): {type(self.A).__name__}\n" + ) + strpar1 = f"tau = {self.tau:6e}\tniter = {self.niter}\ttol = {self.tol}" + print(strpar) + print(strpar1) + + print("-" * 65 + "\n") + if not xcomplex: + head1 = " Itn x[0] f g J=f+g" + else: + head1 = ( + " Itn x[0] f g J=f+g" + ) + print(head1) + + def _print_step( + self, x: NDArray, Ax: NDArray, pf: float | None, pg: float | None + ) -> None: + if self.tol is None: + pf, pg = ( + 0.5 * self.ncp.linalg.norm(self.Op @ x - self.b) ** 2, + self.proxg(Ax), + ) + self.pfg = pf + pg + x0 = to_numpy(x[0]) + strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " + msg = ( + f"{self.iiter:6g} " + + strx + + f"{pf:10.3e} {pg:10.3e} {self.pfg:10.3e}" + ) + print(msg) + + def setup( # type: ignore[override] + self, + proxg: ProxOperator, + Op: "LinearOperator", + b: NDArray, + A: "LinearOperator", + x0: NDArray, + tau: float, + z0: NDArray | None = None, + niter: int = 10, + gfirst: bool = False, + tol: float | None = None, + callbackz: bool = False, + show: bool = False, + ) -> tuple[NDArray, NDArray]: + r"""Setup solver + + Parameters + ---------- + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + Op : :obj:`pylops.LinearOperator` + Linear operator of data misfit term + b : :obj:`numpy.ndarray` + Data + A : :obj:`pylops.LinearOperator` + Linear operator of regularization term + x0 : :obj:`numpy.ndarray` + Initial vector + tau : :obj:`float` + Positive scalar weight, which should satisfy the following condition + to guarantees convergence: :math:`\tau \in (0, 1/\lambda_{max}(\mathbf{A}^H\mathbf{A})]`. + z0 : :obj:`numpy.ndarray` + Initial auxiliary vector. If ``None``, initialized to ``A @ x0``. + niter : :obj:`int`, optional + Number of iterations of iterative scheme + gfirst : :obj:`bool`, optional + Apply Proximal of operator ``g`` first (``True``) or Proximal of + operator ``f`` first (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + callbackz : :obj:`bool`, optional + Modify callback signature to (``callback(x, z)``) when ``callbackz=True`` + show : :obj:`bool`, optional + Display iterations log + + Returns + ------- + x : :obj:`numpy.ndarray` + Initial guess + z : :obj:`numpy.ndarray` + Initial guess for the auxiliary variable + + Raises + ------ + ValueError + If both ``x0`` and ``z0`` are set to ``None`` or ``x0`` is set to None + + """ + self.proxg = proxg + self.Op = Op + self.b = b + self.A = A + self.tau = tau + self.niter = niter + self.gfirst = gfirst + self.tol = tol + self.callbackz = callbackz + + self.ncp = get_array_module(x0) + + # initialize solver + x, z = _x0z0_init(x0, z0, A, Opname="A") + self.u = self.ncp.zeros_like(x) + + # other parameters + self.sqrttau = 1.0 / sqrt(self.tau) + + # create variables to track the objective function and iterations + self.pfg, self.pfgold = np.inf, np.inf + self.cost: list[float] = [] + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(np.iscomplexobj(x0)) + return x, z + + def step( + self, + x: NDArray, + z: NDArray, + show: bool = False, + **kwargs_solver: dict[str, Any], + ) -> tuple[NDArray, NDArray]: + r"""Run one step of solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by a step of the + ADMML2 algorithm + z : :obj:`numpy.ndarray` + Additional model vector to be updated by a step of the + ADMML2 algorithm + show : :obj:`bool`, optional + Display iteration log + **kwargs_solver + Arbitrary keyword arguments for :py:func:`scipy.sparse.linalg.lsqr` used + to solve the x-update + + Returns + ------- + x : :obj:`numpy.ndarray` + Updated model vector + z : :obj:`numpy.ndarray` + Updated additional model vector + + """ + if self.gfirst: + Ax = self.A @ x + z = self.proxg.prox(Ax + self.u, self.tau) + + # solve augumented system + x = regularized_inversion( + self.Op, + self.b, + [ + self.A, + ], + x0=x, + dataregs=[ + z - self.u, + ], + epsRs=[ + self.sqrttau, + ], + **kwargs_solver, + )[0] + else: + # solve augumented system + x = regularized_inversion( + self.Op, + self.b, + [ + self.A, + ], + x0=x, + dataregs=[ + z - self.u, + ], + epsRs=[ + self.sqrttau, + ], + **kwargs_solver, + )[0] + Ax = self.A @ x + z = self.proxg.prox(Ax + self.u, self.tau) + self.u = self.u + Ax - z + + # tolerance check: break iterations if overall + # objective does not decrease below tolerance + if self.tol is not None: + self.pfgold = self.pfg + pf = 0.5 * self.ncp.linalg.norm(self.Op @ x - self.b) ** 2 + pg = self.proxg(Ax) + self.pfg = pf + pg + if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + self.tolbreak = True + else: + pf, pg = 0.0, 0.0 + + self.iiter += 1 + if show: + self._print_step(x, Ax, pf, pg) + if self.tol is not None or show: + self.cost.append(float(self.pfg)) + return x, z + + def run( + self, + x: NDArray, + z: NDArray, + niter: int | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + **kwargs_solver: dict[str, Any], + ) -> tuple[NDArray, NDArray]: + r"""Run solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by multiple steps of + the ADDML2 algorithm + z : :obj:`numpy.ndarray` + Additional model vector to be updated by multiple steps of + the ADDML2 algorithm + niter : :obj:`int`, optional + Number of iterations. Can be set to ``None`` if already + provided in the setup call + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + **kwargs_solver + Arbitrary keyword arguments for :py:func:`scipy.sparse.linalg.lsqr` used + to solve the x-update + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + z : :obj:`numpy.ndarray` + Additional estimated model + + """ + niter = self.niter if niter is None else niter + if niter is None: + msg = "`niter` must not be None" + raise ValueError(msg) + while self.iiter < niter and not self.tolbreak: + showstep = ( + True + if show + and ( + self.iiter < itershow[0] + or niter - self.iiter < itershow[1] + or self.iiter % itershow[2] == 0 + ) + else False + ) + x, z = self.step(x, z, showstep, **kwargs_solver) + if self.callbackz: + self.callback(x, z) + else: + self.callback(x) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x, z + + def solve( # type: ignore[override] + self, + proxg: ProxOperator, + Op: "LinearOperator", + b: NDArray, + A: "LinearOperator", + x0: NDArray, + tau: float, + z0: NDArray | None = None, + niter: int = 10, + gfirst: bool = False, + tol: float | None = None, + callbackz: bool = False, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + **kwargs_solver: dict[str, Any], + ) -> tuple[NDArray, NDArray, int, NDArray]: + r"""Run entire solver + + Parameters + ---------- + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + Op : :obj:`pylops.LinearOperator` + Linear operator of data misfit term + b : :obj:`numpy.ndarray` + Data + A : :obj:`pylops.LinearOperator` + Linear operator of regularization term + x0 : :obj:`numpy.ndarray` + Initial vector (not required when ``gfirst=False``, can pass ``None``) + tau : :obj:`float` + Positive scalar weight, which should satisfy the following condition + to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is + the Lipschitz constant of :math:`\nabla f`. + z0 : :obj:`numpy.ndarray`, optional + Initial z vector (not required when ``gfirst=True``) + niter : :obj:`int` + Number of iterations of iterative scheme + gfirst : :obj:`bool`, optional + Apply Proximal of operator ``g`` first (``True``) or Proximal of + operator ``f`` first (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping + criterion). If ``tol=None``, run until ``niter`` is reached + callbackz : :obj:`bool`, optional + Modify callback signature to (``callback(x, z)``) when ``callbackz=True`` + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + **kwargs_solver + Arbitrary keyword arguments for :py:func:`scipy.sparse.linalg.lsqr` used + to solve the x-update + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + z : :obj:`numpy.ndarray` + Additional estimated model + iiter : :obj:`int` + Number of executed iterations + cost : :obj:`list` + History of the objective function + + """ + x, z = self.setup( + proxg=proxg, + Op=Op, + b=b, + A=A, + x0=x0, + tau=tau, + z0=z0, + niter=niter, + gfirst=gfirst, + tol=tol, + callbackz=callbackz, + show=show, + ) + + x, z = self.run( + x, + z, + niter, + show=show, + itershow=itershow, + **kwargs_solver, + ) + self.finalize(65, show) + return x, z, self.iiter, self.cost + + +class LinearizedADMM(Solver): + r"""Linearized Alternating Direction Method of Multipliers + + Solves the following minimization problem using Linearized Alternating + Direction Method of Multipliers: + + .. math:: + + \mathbf{x} = \argmin_\mathbf{x} f(\mathbf{x}) + g(\mathbf{A}\mathbf{x}) + + where :math:`f(\mathbf{x})` and :math:`g(\mathbf{x})` are any convex + function that has a known proximal operator and :math:`\mathbf{A}` is a + linear operator. + + See Also + -------- + ADMM: ADMM + ADMML2: ADMM with L2 misfit function + + Notes + ----- + The Linearized-ADMM algorithm can be expressed by the following recursion [1]_: + + .. math:: + + \mathbf{x}^{k+1} = \prox_{\mu f}(\mathbf{x}^{k} - \frac{\mu}{\tau} + \mathbf{A}^H(\mathbf{A} \mathbf{x}^k - \mathbf{z}^k + \mathbf{u}^k))\\ + \mathbf{z}^{k+1} = \prox_{\tau g}(\mathbf{A} \mathbf{x}^{k+1} + + \mathbf{u}^k)\\ + \mathbf{u}^{k+1} = \mathbf{u}^{k} + \mathbf{A}\mathbf{x}^{k+1} - + \mathbf{z}^{k+1} + + .. [1] N., Parikh, "Proximal Algorithms", Foundations and Trends + in Optimization. 2013. + + """ + + def _print_setup(self, xcomplex: bool = False) -> None: + self._print_solver(nbar=65) + + strpar = ( + f"Proximal operator (f): {type(self.proxf).__name__}\n" + f"Proximal operator (g): {type(self.proxg).__name__}\n" + f"Linear operator (A): {type(self.A).__name__}\n" + ) + strpar1 = f"tau = {self.tau:6e}\tmu = {self.mu:6e}" + strpar2 = f"niter = {self.niter}\ttol = {self.tol}" + print(strpar) + print(strpar1) + print(strpar2) + + print("-" * 65 + "\n") + if not xcomplex: + head1 = " Itn x[0] f g J=f+g" + else: + head1 = ( + " Itn x[0] f g J=f+g" + ) + print(head1) + + def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: + if self.tol is None: + pf, pg = self.proxf(x), self.proxg(self.Ax) + self.pfg = pf + pg + x0 = to_numpy(x[0]) + strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " + msg = ( + f"{self.iiter:6g} " + + strx + + f"{pf:10.3e} {pg:10.3e} {self.pfg:10.3e}" + ) + print(msg) + + def setup( # type: ignore[override] + self, + proxf: ProxOperator, + proxg: ProxOperator, + A: "LinearOperator", + x0: NDArray, + tau: float, + mu: float, + z0: NDArray | None = None, + niter: int = 10, + tol: float | None = None, + show: bool = False, + ) -> tuple[NDArray, NDArray]: + r"""Setup solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + A : :obj:`pylops.LinearOperator` + Linear operator + x0 : :obj:`numpy.ndarray` + Initial vector + tau : :obj:`float`, optional + Positive scalar weight, which should satisfy the following + condition to guarantee convergence: :math:`\mu \in (0, + \tau/\lambda_{max}(\mathbf{A}^H\mathbf{A})]`. + mu : :obj:`float`, optional + Second positive scalar weight, which should satisfy the following + condition to guarantees convergence: :math:`\mu \in (0, + \tau/\lambda_{max}(\mathbf{A}^H\mathbf{A})]`. + z0 : :obj:`numpy.ndarray` + Initial auxiliary vector. If ``None``, initialized to ``A @ x0``. + niter : :obj:`int`, optional + Number of iterations of iterative scheme + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display iterations log + + Returns + ------- + x : :obj:`numpy.ndarray` + Initial guess + z : :obj:`numpy.ndarray` + Initial guess for the auxiliary variable + + Raises + ------ + ValueError + If both ``x0`` and ``z0`` are set to ``None`` or ``x0`` is set to None + + """ + self.proxf = proxf + self.proxg = proxg + self.A = A + self.tau = tau + self.mu = mu + + self.niter = niter + self.tol = tol + + self.ncp = get_array_module(x0) + + # initialize solver + x, z = _x0z0_init(x0, z0, A, Opname="A") + self.Ax = A.matvec(x) if z0 is None else z + self.u = self.ncp.zeros_like(x) + + # create variables to track the objective function and iterations + self.pfg, self.pfgold = np.inf, np.inf + self.cost: list[float] = [] + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(np.iscomplexobj(x0)) + return x, z + + def step( + self, + x: NDArray, + z: NDArray, + show: bool = False, + ) -> tuple[NDArray, NDArray]: + r"""Run one step of solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by a step of the + LinearizedADMM algorithm + z : :obj:`numpy.ndarray` + Additional model vector to be updated by a step of the + LinearizedADMM algorithm + show : :obj:`bool`, optional + Display iteration log + + Returns + ------- + x : :obj:`numpy.ndarray` + Updated model vector + z : :obj:`numpy.ndarray` + Updated additional model vector + + """ + x = self.proxf.prox( + x - self.mu / self.tau * self.A.rmatvec(self.Ax - z + self.u), self.mu + ) + + self.Ax = self.A.matvec(x) + z = self.proxg.prox(self.Ax + self.u, self.tau) + self.u = self.u + self.Ax - z + + # tolerance check: break iterations if overall + # objective does not decrease below tolerance + if self.tol is not None: + self.pfgold = self.pfg + pf = self.proxf(x) + pg = self.proxg(self.Ax) + self.pfg = pf + pg + if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + self.tolbreak = True + else: + pf, pg = 0.0, 0.0 + + self.iiter += 1 + if show: + self._print_step(x, pf, pg) + if self.tol is not None or show: + self.cost.append(float(self.pfg)) + return x, z + + def run( + self, + x: NDArray, + z: NDArray, + niter: int | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray]: + r"""Run solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by multiple steps of + the LinearizedADDM algorithm + z : :obj:`numpy.ndarray` + Additional model vector to be updated by multiple steps of + the LinearizedADDM algorithm + niter : :obj:`int`, optional + Number of iterations. Can be set to ``None`` if already + provided in the setup call + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + z : :obj:`numpy.ndarray` + Additional estimated model + + """ + niter = self.niter if niter is None else niter + if niter is None: + msg = "`niter` must not be None" + raise ValueError(msg) + while self.iiter < niter and not self.tolbreak: + showstep = ( + True + if show + and ( + self.iiter < itershow[0] + or niter - self.iiter < itershow[1] + or self.iiter % itershow[2] == 0 + ) + else False + ) + x, z = self.step(x, z, showstep) + + self.callback(x) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x, z + + def solve( # type: ignore[override] + self, + proxf: ProxOperator, + proxg: ProxOperator, + A: "LinearOperator", + x0: NDArray, + tau: float, + mu: float, + z0: NDArray | None = None, + niter: int = 10, + tol: float | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray, int, NDArray]: + r"""Run entire solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + A : :obj:`pylops.LinearOperator` + Linear operator + x0 : :obj:`numpy.ndarray` + Initial vector + tau : :obj:`float`, optional + Positive scalar weight, which should satisfy the following + condition to guarantee convergence: :math:`\mu \in (0, + \tau/\lambda_{max}(\mathbf{A}^H\mathbf{A})]`. + mu : :obj:`float`, optional + Second positive scalar weight, which should satisfy the following + condition to guarantees convergence: :math:`\mu \in (0, + \tau/\lambda_{max}(\mathbf{A}^H\mathbf{A})]`. + z0 : :obj:`numpy.ndarray` + Initial auxiliary vector. If ``None``, initialized to ``A @ x0``. + niter : :obj:`int`, optional + Number of iterations of iterative scheme + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + z : :obj:`numpy.ndarray` + Additional estimated model + iiter : :obj:`int` + Number of executed iterations + cost : :obj:`list` + History of the objective function + + """ + x, z = self.setup( + proxf=proxf, + proxg=proxg, + A=A, + x0=x0, + tau=tau, + mu=mu, + z0=z0, + niter=niter, + tol=tol, + show=show, + ) + + x, z = self.run( + x, + z, + niter, + show=show, + itershow=itershow, + ) + self.finalize(65, show) + return x, z, self.iiter, self.cost + + +class TwIST(Solver): + r"""Two-step Iterative Shrinkage/Threshold + + Solves the following minimization problem using Two-step Iterative + Shrinkage/Threshold: + + .. math:: + + \mathbf{x} = \argmin_\mathbf{x} \frac{1}{2} + ||\mathbf{b} - \mathbf{Ax}||_2^2 + g(\mathbf{x}) + + where :math:`\mathbf{A}` is a linear operator and :math:`g(\mathbf{x})` + is any convex function that has a known proximal operator. + + Notes + ----- + The TwIST algorithm can be expressed by the following recursion: + + .. math:: + + \mathbf{x}^{k+1} = (1-\alpha) \mathbf{x}^{k-1} + + (\alpha-\beta) \mathbf{x}^k + + \beta \prox_{g} (\mathbf{x}^k + \mathbf{A}^H + (\mathbf{b} - \mathbf{A}\mathbf{x}^k)). + + where :math:`\mathbf{x}^{1} = \prox_{g} (\mathbf{x}^0 + \mathbf{A}^T + (\mathbf{b} - \mathbf{A}\mathbf{x}^0))`. + + The optimal weighting parameters :math:`\alpha` and :math:`\beta` are + linked to the smallest and largest eigenvalues of + :math:`\mathbf{A}^H\mathbf{A}` as follows: + + .. math:: + + \alpha = 1 + \rho^2 \\ + \beta =\frac{2 \alpha}{\Lambda_{max} + \lambda_{min}} + + where :math:`\rho=\frac{1-\sqrt{k}}{1+\sqrt{k}}` with + :math:`k=\frac{\lambda_{min}}{\Lambda_{max}}` and + :math:`\Lambda_{max}=max(1, \lambda_{max})`. + + Experimentally, it has been observed that TwIST is robust to the + choice of such parameters. Finally, note that in the case of + :math:`\alpha=1` and :math:`\beta=1`, TwIST is identical to IST. + + """ + + def _print_setup(self, xcomplex: bool = False) -> None: + self._print_solver(nbar=65) + + strpar = ( + f"Proximal operator (f): {type(self.proxf).__name__}\n" + f"Linear operator (A): {type(self.A).__name__}\n" + ) + strpar1 = f"alpha = {self.alpha:6e}\tbeta = {self.beta:6e}" + strpar2 = f"niter = {self.niter}\ttol = {self.tol}" + print(strpar) + print(strpar1) + print(strpar2) + + print("-" * 65 + "\n") + if not xcomplex: + head1 = " Itn x[0] f g J=f+g" + else: + head1 = ( + " Itn x[0] f g J=f+g" + ) + print(head1) + + def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: + if self.tol is None: + pf, pg = self.proxf(x), self.proxg(x) + self.pfg = pf + pg + x0 = to_numpy(x[0]) + strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " + msg = ( + f"{self.iiter:6g} " + + strx + + f"{pf:10.3e} {pg:10.3e} {self.pfg:10.3e}" + ) + print(msg) + + def setup( # type: ignore[override] + self, + proxg: ProxOperator, + A: "LinearOperator", + b: NDArray, + x0: NDArray, + alpha: float | None = None, + beta: float | None = None, + eigs: tuple[float, float] | None = None, + niter: int = 10, + tol: float | None = None, + show: bool = False, + ) -> NDArray: + r"""Setup solver + + Parameters + ---------- + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + A : :obj:`pylops.LinearOperator` + Linear operator + b : :obj:`numpy.ndarray` + Data + x0 : :obj:`numpy.ndarray` + Initial vector + alpha : :obj:`float`, optional + Positive scalar weight (if ``None``, estimated based on the + eigenvalues of :math:`\mathbf{A}`, see Notes for details) + beta : :obj:`float`, optional + Positive scalar weight (if ``None``, estimated based on the + eigenvalues of :math:`\mathbf{A}`, see Notes for details) + eigs : :obj:`tuple`, optional + Largest and smallest eigenvalues of :math:`\mathbf{A}^H \mathbf{A}`. + If passed, computes `alpha` and `beta` based on them. + niter : :obj:`int`, optional + Number of iterations of iterative scheme + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display iterations log + + Returns + ------- + x : :obj:`numpy.ndarray` + Initial guess + + """ + self.proxf = L2(Op=A, b=b) + self.proxg = proxg + self.A = A + self.niter = niter + self.tol = tol + + self.ncp = get_array_module(x0) + + # find alpha and beta based on the eigenvalues of A if not provided + if alpha is None or beta is None: + if eigs is None: + emin = A.eigs(neigs=1, which="SM") + emax = max([1, A.eigs(neigs=1, which="LM")]) + else: + emax, emin = eigs + k = emin / emax + rho = (1 - sqrt(k)) / (1 + sqrt(k)) + self.alpha = 1 + rho**2 + self.beta = 2 * self.alpha / (emax + emin) + else: + self.alpha, self.beta = alpha, beta + + # compute proximal of g on initial guess (x_1) + x = self.proxg.prox(x0 - self.proxf.grad(x0), 1.0) + + # create variables to track the objective function and iterations + self.pfg, self.pfgold = np.inf, np.inf + self.cost: list[float] = [] + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(np.iscomplexobj(x0)) + return x + + def step( + self, + x: NDArray, + xold: NDArray, + show: bool = False, + ) -> tuple[NDArray, NDArray]: + r"""Run one step of solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by a step of the + TwisT algorithm + xold : :obj:`numpy.ndarray` + Previous model vector + show : :obj:`bool`, optional + Display iteration log + + Returns + ------- + x : :obj:`numpy.ndarray` + Updated model vector + xold : :obj:`numpy.ndarray` + Previous model vector + + """ + # compute new x + xnew = ( + (1 - self.alpha) * xold + + (self.alpha - self.beta) * x + + self.beta * self.proxg.prox(x - self.proxf.grad(x), 1.0) + ) + # save current x as old (x_i -> x_i-1) + xold = x.copy() + # save new x as current (x_i+1 -> x_i) + x = xnew.copy() + + # tolerance check: break iterations if overall + # objective does not decrease below tolerance + if self.tol is not None: + self.pfgold = self.pfg + pf = self.proxf(x) + pg = self.proxg(self.Ax) + self.pfg = pf + pg + if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + self.tolbreak = True + else: + pf, pg = 0.0, 0.0 + + self.iiter += 1 + if show: + self._print_step(x, pf, pg) + if self.tol is not None or show: + self.cost.append(float(self.pfg)) + return x, xold + + def run( + self, + x: NDArray, + xold: NDArray, + niter: int | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> NDArray: + r"""Run solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by multiple steps of + the TwisT algorithm + xold : :obj:`numpy.ndarray` + Previous estimated model + niter : :obj:`int`, optional + Number of iterations. Can be set to ``None`` if already + provided in the setup call + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + + """ + niter = self.niter if niter is None else niter + if niter is None: + msg = "`niter` must not be None" + raise ValueError(msg) + while self.iiter < niter and not self.tolbreak: + showstep = ( + True + if show + and ( + self.iiter < itershow[0] + or niter - self.iiter < itershow[1] + or self.iiter % itershow[2] == 0 + ) + else False + ) + x, xold = self.step(x, xold, showstep) + + self.callback(x) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x + + def solve( # type: ignore[override] + self, + proxg: ProxOperator, + A: "LinearOperator", + b: NDArray, + x0: NDArray, + alpha: float | None = None, + beta: float | None = None, + eigs: tuple[float, float] | None = None, + niter: int = 10, + tol: float | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, int, NDArray]: + r"""Run entire solver + + Parameters + ---------- + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + A : :obj:`pylops.LinearOperator` + Linear operator + b : :obj:`numpy.ndarray` + Data + x0 : :obj:`numpy.ndarray` + Initial vector + alpha : :obj:`float`, optional + Positive scalar weight (if ``None``, estimated based on the + eigenvalues of :math:`\mathbf{A}`, see Notes for details) + beta : :obj:`float`, optional + Positive scalar weight (if ``None``, estimated based on the + eigenvalues of :math:`\mathbf{A}`, see Notes for details) + eigs : :obj:`tuple`, optional + Largest and smallest eigenvalues of :math:`\mathbf{A}^H \mathbf{A}`. + If passed, computes `alpha` and `beta` based on them. + niter : :obj:`int`, optional + Number of iterations of iterative scheme + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + iiter : :obj:`int` + Number of executed iterations + cost : :obj:`list` + History of the objective function + + """ + x = self.setup( + proxg=proxg, + A=A, + b=b, + x0=x0, + alpha=alpha, + beta=beta, + eigs=eigs, + niter=niter, + tol=tol, + show=show, + ) + + x = self.run( + x, + x0, + niter, + show=show, + itershow=itershow, + ) + self.finalize(65, show) + return x, self.iiter, self.cost From 4c6940a104a54cbe3b944173bbe83ed5b24376b4 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 17 Jun 2026 23:09:23 +0100 Subject: [PATCH 25/73] feat: added class-based DouglasRachfordSplitting and PPXA --- pyproximal/optimization/cls_primal.py | 730 +++++++++++++++++++++++++- 1 file changed, 723 insertions(+), 7 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index 28600599..dc7f1c43 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -8,6 +8,7 @@ "ADMML2", "LinearizedADMM", "TwIST", + "DouglasRachfordSplitting", ] from collections.abc import Sequence @@ -197,8 +198,8 @@ def setup( # type: ignore[override] tau : :obj:`float` Positive scalar weight niter : :obj:`int`, optional - Number of iterations (default to ``None`` in case a user wants to - manually step over the solver) + Number of iterations of iterative scheme (default to ``None`` + in case a user wants to manually step over the solver) tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -428,10 +429,14 @@ def _print_setup(self, epsg_print: str, xcomplex: bool = False) -> None: f"Proximal operator (f): {type(self.proxf).__name__}\n" f"Proximal operator (g): {type(self.proxg).__name__}\n" ) - strpar1 = f"tau = {self.tau:4.2e}\t\tbacktrack = {self.backtracking}\tbeta = {self.beta}" - strpar2 = ( - f"epsg = {epsg_print}\t\tniter = {self.niter}\t\ttol = {str(self.tol)}" - ) + + strpar1 = f"tau = {self.tau:4.2e}\t\tbacktrack = {self.backtracking}" + if self.niter is not None: + strpar2 = f"beta = {self.beta}\tepsg = {epsg_print}\t\tniter = {self.niter}\t\ttol = {str(self.tol)}" + else: + strpar2 = ( + f"beta = {self.beta}\t epsg = {epsg_print}\t\ttol = {str(self.tol)}" + ) strpar3 = f"niterback = {self.niterback}\t\tacceleration = {self.acceleration}" print(strpar) print(strpar1) @@ -535,7 +540,7 @@ def setup( # type: ignore[override] # check if epgs is a vector self.epsg = np.asarray(epsg, dtype=float) if self.epsg.size == 1: - self.epsg = self.epsg * np.ones(niter) + self.epsg = epsg * np.ones(niter) epsg_print = str(self.epsg[0]) else: epsg_print = "Multi" @@ -3459,3 +3464,714 @@ def solve( # type: ignore[override] ) self.finalize(65, show) return x, self.iiter, self.cost + + +class DouglasRachfordSplitting(Solver): + r"""Douglas-Rachford Splitting + + Solves the following minimization problem using Douglas-Rachford Splitting + algorithm: + + .. math:: + + \mathbf{x} = \argmin_\mathbf{x} f(\mathbf{x}) + g(\mathbf{x}) + + where :math:`f(\mathbf{x})` and :math:`g(\mathbf{x})` are any convex + functions that has known proximal operators. + + Notes + ----- + The Douglas-Rachford Splitting algorithm can be expressed by the following + recursion [1]_, [2]_, [3]_, [4]_: + + .. math:: + + \mathbf{x}^{k} &= \prox_{\tau g}(\mathbf{y}^k) \\ + \mathbf{y}^{k+1} &= \mathbf{y}^{k} + + \eta (\prox_{\tau f}(2 \mathbf{x}^{k} - \mathbf{y}^{k}) + - \mathbf{x}^{k}) + + .. [1] Patrick L. Combettes and Jean-Christophe Pesquet. 2011. Proximal + Splitting Methods in Signal Processing. In Fixed-Point Algorithms for + Inverse Problems in Science and Engineering, Springer, pp. 185-212. + Algorithm 10.15. + https://doi.org/10.1007/978-1-4419-9569-8_10 + .. [2] Scott B. Lindstrom and Brailey Sims. 2021. Survey: Sixty Years of + Douglas-Rachford. Journal of the Australian Mathematical Society, 110, + 3, 333-370. Eq.(15). https://doi.org/10.1017/S1446788719000570 + https://arxiv.org/abs/1809.07181 + .. [3] Ryu, E.K., Yin, W., 2022. Large-Scale Convex Optimization: Algorithms + & Analyses via Monotone Operators. Cambridge University Press, + Cambridge. Eq.(2.18). https://doi.org/10.1017/9781009160865 + https://large-scale-book.mathopt.com/ + .. [4] Combettes, P.L., Pesquet, J.-C., 2008. A proximal decomposition + method for solving convex variational inverse problems. Inverse Problems + 24, 065014. Proposition 3.2. https://doi.org/10.1088/0266-5611/24/6/065014 + https://arxiv.org/abs/0807.2617 + + """ + + def _print_setup(self, xcomplex: bool = False) -> None: + self._print_solver(nbar=65) + + strpar = ( + f"Proximal operator (f): {type(self.proxf).__name__}\n" + f"Proximal operator (g): {type(self.proxg).__name__}\n" + ) + strpar1 = f"tau = {self.tau:6e}\teta = {self.eta:6e}\tniter = {self.niter}" + strpar2 = f"gfirst = {self.gfirst}\ttol = {self.tol}" + print(strpar) + print(strpar1) + print(strpar2) + + print("-" * 65 + "\n") + if not xcomplex: + head1 = " Itn x[0] f g J=f+g" + else: + head1 = ( + " Itn x[0] f g J=f+g" + ) + print(head1) + + def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: + if self.tol is None: + pf, pg = self.proxf(x), self.proxg(x) + self.pfg = pf + pg + x0 = to_numpy(x[0]) + strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " + msg = ( + f"{self.iiter:6g} " + + strx + + f"{pf:10.3e} {pg:10.3e} {self.pfg:10.3e}" + ) + print(msg) + + def setup( # type: ignore[override] + self, + proxf: ProxOperator, + proxg: ProxOperator, + x0: NDArray, + tau: float, + eta: float = 1.0, + niter: int = 10, + gfirst: bool = True, + tol: float | None = None, + callbacky: bool = False, + show: bool = False, + ) -> tuple[NDArray, NDArray]: + r"""Setup solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + x0 : :obj:`numpy.ndarray` + Initial vector + tau : :obj:`float` + Positive scalar weight + eta : :obj:`float`, optional + Relaxation parameter (must be between 0 and 2, 0 excluded). + niter : :obj:`int`, optional + Number of iterations of iterative scheme + gfirst : :obj:`bool`, optional + Apply Proximal of operator ``g`` first (``True``) or Proximal of + operator ``f`` first (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + callbacky : :obj:`bool`, optional + Modify callback signature to (``callback(x, y)``) + when ``callbacky=True`` + show : :obj:`bool`, optional + Display iterations log + + Returns + ------- + x : :obj:`numpy.ndarray` + Initial guess + y : :obj:`numpy.ndarray` + Initial guess for the auxiliary variable + + """ + self.proxf = proxf + self.proxg = proxg + self.tau = tau + self.eta = eta + self.niter = niter + self.gfirst = gfirst + self.tol = tol + self.callbacky = callbacky + + self.ncp = get_array_module(x0) + + # initialize solver + x = x0.copy() + y = x0.copy() + + # create variables to track the objective function and iterations + self.pfg, self.pfgold = np.inf, np.inf + self.cost: list[float] = [] + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(np.iscomplexobj(x0)) + return x, y + + def step( + self, x: NDArray, y: NDArray, show: bool = False + ) -> tuple[NDArray, NDArray]: + r"""Run one step of solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by a step of the + DouglasRachfordSplitting algorithm + y : :obj:`numpy.ndarray` + Additional model vector to be updated by a step of the + DouglasRachfordSplitting algorithm + show : :obj:`bool`, optional + Display iteration log + + Returns + ------- + x : :obj:`numpy.ndarray` + Updated model vector + y : :obj:`numpy.ndarray` + Updated additional model vector + + """ + # proximal steps + if self.gfirst: + x = self.proxg.prox(y, self.tau) + y = y + self.eta * (self.proxf.prox(2 * x - y, self.tau) - x) + else: + x = self.proxf.prox(y, self.tau) + y = y + self.eta * (self.proxg.prox(2 * x - y, self.tau) - x) + + # tolerance check: break iterations if overall + # objective does not decrease below tolerance + if self.tol is not None: + self.pfgold = self.pfg + pf = self.proxf(x) + pg = self.proxg(x) + self.pfg = pf + pg + if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + self.tolbreak = True + else: + pf, pg = 0.0, 0.0 + + self.iiter += 1 + if show: + self._print_step(x, pf, pg) + if self.tol is not None or show: + self.cost.append(float(self.pfg)) + return x, y + + def run( + self, + x: NDArray, + y: NDArray, + niter: int | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray]: + r"""Run solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by multiple steps of + the DouglasRachfordSplitting algorithm + y : :obj:`numpy.ndarray` + Additional model vector to be updated by multiple steps of + the DouglasRachfordSplitting algorithm + niter : :obj:`int`, optional + Number of iterations. Can be set to ``None`` if already + provided in the setup call + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + y : :obj:`numpy.ndarray` + Additional estimated model + + """ + niter = self.niter if niter is None else niter + if niter is None: + msg = "`niter` must not be None" + raise ValueError(msg) + while self.iiter < niter and not self.tolbreak: + showstep = ( + True + if show + and ( + self.iiter < itershow[0] + or niter - self.iiter < itershow[1] + or self.iiter % itershow[2] == 0 + ) + else False + ) + x, y = self.step(x, y, showstep) + if self.callbacky: + self.callback(x, y) + else: + self.callback(x) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x, y + + def solve( # type: ignore[override] + self, + proxf: ProxOperator, + proxg: ProxOperator, + x0: NDArray, + tau: float, + eta: float = 1.0, + niter: int = 10, + gfirst: bool = True, + tol: float | None = None, + callbacky: bool = False, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray, int, NDArray]: + r"""Run entire solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + x0 : :obj:`numpy.ndarray` + Initial vector + tau : :obj:`float` + Positive scalar weight + eta : :obj:`float`, optional + Relaxation parameter (must be between 0 and 2, 0 excluded). + niter : :obj:`int`, optional + Number of iterations of iterative scheme + gfirst : :obj:`bool`, optional + Apply Proximal of operator ``g`` first (``True``) or Proximal of + operator ``f`` first (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + callbacky : :obj:`bool`, optional + Modify callback signature to (``callback(x, y)``) + when ``callbacky=True`` + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + y : :obj:`numpy.ndarray` + Additional estimated model + iiter : :obj:`int` + Number of executed iterations + cost : :obj:`list` + History of the objective function + + """ + x, y = self.setup( + proxf=proxf, + proxg=proxg, + x0=x0, + tau=tau, + eta=eta, + niter=niter, + gfirst=gfirst, + tol=tol, + callbacky=callbacky, + show=show, + ) + + x, y = self.run(x, y, niter, show=show, itershow=itershow) + self.finalize(65, show) + return x, y, self.iiter, self.cost + + +class PPXA(Solver): + r"""Parallel Proximal Algorithm (PPXA) + + Solves the following minimization problem using + Parallel Proximal Algorithm (PPXA): + + .. math:: + + \mathbf{x} = \argmin_\mathbf{x} \sum_{i=1}^m f_i(\mathbf{x}) + + where :math:`f_i(\mathbf{x})` are any convex + functions that has known proximal operators. + + See Also + -------- + ConsensusADMM: Consensus ADMM + + Notes + ----- + The Parallel Proximal Algorithm (PPXA) can be expressed by the following + recursion [1]_, [2]_, [3]_, [4]_: + + * :math:`\mathbf{y}_{i}^{0} = \mathbf{x}` or :math:`\mathbf{y}_{i}^{0} = \mathbf{x}_{i}` for :math:`i=1,\ldots,m` + * :math:`\mathbf{x}^{0} = \sum_{i=1}^m w_i \mathbf{y}_{i}^{0}` + * for :math:`k = 1, \ldots` + + * for :math:`i = 1, \ldots, m` + + * :math:`\mathbf{p}_{i}^{k} = \prox_{\frac{\tau}{w_i} f_i} (\mathbf{y}_{i}^{k})` + + * :math:`\mathbf{p}^{k} = \sum_{i=1}^{m} w_i \mathbf{p}_{i}^{k}` + * for :math:`i = 1, \ldots, m` + + * :math:`\mathbf{y}_{i}^{k+1} = \mathbf{y}_{i}^{k} + \eta (2 \mathbf{p}^{k} - \mathbf{x}^{k} - \mathbf{p}_i^{k})` + + * :math:`\mathbf{x}^{k+1} = \mathbf{x}^{k} + \eta (\mathbf{p}^{k} - \mathbf{x}^{k})` + + where :math:`0 < \eta < 2` and + :math:`\sum_{i=1}^m w_i = 1, \ 0 < w_i < 1`. + In the current implementation, :math:`w_i = 1 / m` when not provided. + + References + ---------- + .. [1] Combettes, P.L., Pesquet, J.-C., 2008. A proximal decomposition + method for solving convex variational inverse problems. Inverse Problems + 24, 065014. Algorithm 3.1. https://doi.org/10.1088/0266-5611/24/6/065014 + https://arxiv.org/abs/0807.2617 + .. [2] Combettes, P.L., Pesquet, J.-C., 2011. Proximal Splitting Methods in + Signal Processing, in Fixed-Point Algorithms for Inverse Problems in + Science and Engineering, Springer, pp. 185-212. Algorithm 10.27. + https://doi.org/10.1007/978-1-4419-9569-8_10 + .. [3] Bauschke, H.H., Combettes, P.L., 2011. Convex Analysis and Monotone + Operator Theory in Hilbert Spaces, 1st ed, CMS Books in Mathematics. + Springer, New York, NY. Proposition 27.8. + https://doi.org/10.1007/978-1-4419-9467-7 + .. [4] Ryu, E.K., Yin, W., 2022. Large-Scale Convex Optimization: Algorithms + & Analyses via Monotone Operators. Cambridge University Press, + Cambridge. Exercise 2.38 https://doi.org/10.1017/9781009160865 + https://large-scale-book.mathopt.com/ + + """ + + def _print_setup(self, xcomplex: bool = False) -> None: + self._print_solver(nbar=65) + + strpar = "\n".join( + [ + f"Proximal operator (f{i}): {type(proxf).__name__}" + for i, proxf in enumerate(self.proxfs) + ] + ) + strpar1 = f"tau = {self.tau:6e}\teta = {self.eta:6e}" + strpar2 = f"weights = {self.weights}" + strpar3 = f"niter = {self.niter}\ttol = {self.tol}" + print(strpar) + print(strpar1) + print(strpar2) + print(strpar3) + + print("-" * 65 + "\n") + if not xcomplex: + head1 = " Itn x[0] J=sum_i f_i" + else: + head1 = " Itn x[0] J=sum_i f_i" + print(head1) + + def _print_step(self, x: NDArray) -> None: + if self.tol is None: + self.pf = self.ncp.sum([self.proxfs[i](x) for i in range(self.nprox)]) + self.pf = self.ncp.sum([self.proxfs[i](x) for i in range(self.nprox)]) + x0 = to_numpy(x[0]) + strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " + msg = f"{self.iiter:6g} " + strx + f"{self.pf:10.3e}" + print(msg) + + def setup( # type: ignore[override] + self, + proxfs: list[ProxOperator], + x0: NDArray | list[NDArray], + tau: float, + eta: float = 1.0, + weights: NDArray | list[float] | None = None, + niter: int = 1000, + tol: float | None = 1e-7, + show: bool = False, + ) -> tuple[NDArray, NDArray]: + r"""Setup solver + + Parameters + ---------- + proxfs : :obj:`list` + A list of proximable functions :math:`f_1, \ldots, f_m`. + x0 : :obj:`numpy.ndarray` or :obj:`list` + Initial vector :math:`\mathbf{x}` for all :math:`f_i` if 1-dimensional array + is provided, or initial vectors :math:`\mathbf{x}_{i}` for each :math:`f_i` + for :math:`i=1,\ldots,m` if a :obj:`list` of 1-dimensional arrays or a 2-dimensional + array of size ``(m, d)`` is provided, where ``d`` is the dimension of :math:`\mathbf{x}_{i}`. + tau : :obj:`float` + Positive scalar weight + eta : :obj:`float`, optional + Relaxation parameter (must be between 0 and 2, 0 excluded). + weights : :obj:`numpy.ndarray` or :obj:`list` or :obj:`None`, optional + Weights :math:`\sum_{i=1}^m w_i = 1, \ 0 < w_i < 1`, + Defaults to None, which means :math:`w_1 = \cdots = w_m = \frac{1}{m}.` + niter : :obj:`int`, optional + Number of iterations of iterative scheme. + tol : :obj:`float`, optional + Tolerance on change of the solution (used as stopping criterion). + If ``tol=0``, run until ``niter`` is reached. + show : :obj:`bool`, optional + Display iterations log + + Returns + ------- + x : :obj:`numpy.ndarray` + Initial guess + y : :obj:`numpy.ndarray` + Initial guess for the auxiliary variable(s) + + """ + self.proxfs = proxfs + self.tau = tau + self.eta = eta + self.weights = weights + self.niter = niter + self.tol = tol + + self.ncp = get_array_module(x0) + + # initialize solver + self.nprox = len(proxfs) + if weights is None: + self.w = self.ncp.full(self.nprox, 1.0 / self.nprox) + else: + self.w = self.ncp.asarray(weights) + + if isinstance(x0, list) or x0.ndim == 2: + y = self.ncp.asarray(x0) # yi_0 = xi_0, for i = 1, ..., m + else: + y = self.ncp.full( + (self.nprox, x0.size), x0 + ) # y1_0 = y2_0 = ... = ym_0 = x0 + + x = self.ncp.mean(y, axis=0) + + # create variables to track the objective function and iterations + self.pf, self.pfold = np.inf, np.inf + self.cost: list[float] = [] + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(np.iscomplexobj(x0)) + return x, y + + def step( + self, x: NDArray, y: NDArray, show: bool = False + ) -> tuple[NDArray, NDArray]: + r"""Run one step of solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by a step of the + PPXA algorithm + y : :obj:`numpy.ndarray` + Additional model vector to be updated by a step of the + PPXA algorithm + show : :obj:`bool`, optional + Display iteration log + + Returns + ------- + x : :obj:`numpy.ndarray` + Updated model vector + y : :obj:`numpy.ndarray` + Updated additional model vector + + """ + x_old = x.copy() + + # proximal steps + p = self.ncp.stack( + [self.proxfs[i].prox(y[i], self.tau / self.w[i]) for i in range(self.nprox)] + ) + pn = self.ncp.sum(self.w[:, None] * p, axis=0) + y = y + self.eta * (2 * pn - x - p) + x = x + self.eta * (pn - x) + + # tolerance check: break iterations if overall + # objective does not decrease below tolerance + # if self.tol is not None: + # self.pfold = self.pf + # self.pf = self.ncp.sum([self.proxfs[i](x) for i in range(self.nprox)]) + # if np.abs(1.0 - self.pf / self.pfold) < self.tol: + # self.tolbreak = True + # else: + # self.pf = 0.0 + + # tolerance check: break iterations if solution does + # not changeabove tolerance + if self.tol is not None: + if self.ncp.abs(x - x_old).max() < self.tol: + self.tolbreak = True + + self.iiter += 1 + if show: + self._print_step(x) + if self.tol is not None or show: + self.cost.append(float(self.pf)) + return x, y + + def run( + self, + x: NDArray, + y: NDArray, + niter: int | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray]: + r"""Run solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by multiple steps of + the PPXA algorithm + y : :obj:`numpy.ndarray` + Additional model vector to be updated by multiple steps of + the PPXA algorithm + niter : :obj:`int`, optional + Number of iterations. Can be set to ``None`` if already + provided in the setup call + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + y : :obj:`numpy.ndarray` + Additional estimated model(s) + + """ + niter = self.niter if niter is None else niter + if niter is None: + msg = "`niter` must not be None" + raise ValueError(msg) + while self.iiter < niter and not self.tolbreak: + showstep = ( + True + if show + and ( + self.iiter < itershow[0] + or niter - self.iiter < itershow[1] + or self.iiter % itershow[2] == 0 + ) + else False + ) + x, y = self.step(x, y, showstep) + self.callback(x, y) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x, y + + def solve( # type: ignore[override] + self, + proxfs: list[ProxOperator], + x0: NDArray | list[NDArray], + tau: float, + eta: float = 1.0, + weights: NDArray | list[float] | None = None, + niter: int = 1000, + tol: float | None = 1e-7, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray, int, NDArray]: + r"""Run entire solver + + Parameters + ---------- + proxfs : :obj:`list` + A list of proximable functions :math:`f_1, \ldots, f_m`. + x0 : :obj:`numpy.ndarray` or :obj:`list` + Initial vector :math:`\mathbf{x}` for all :math:`f_i` if 1-dimensional array + is provided, or initial vectors :math:`\mathbf{x}_{i}` for each :math:`f_i` + for :math:`i=1,\ldots,m` if a :obj:`list` of 1-dimensional arrays or a 2-dimensional + array of size ``(m, d)`` is provided, where ``d`` is the dimension of :math:`\mathbf{x}_{i}`. + tau : :obj:`float` + Positive scalar weight + eta : :obj:`float`, optional + Relaxation parameter (must be between 0 and 2, 0 excluded). + weights : :obj:`numpy.ndarray` or :obj:`list` or :obj:`None`, optional + Weights :math:`\sum_{i=1}^m w_i = 1, \ 0 < w_i < 1`, + Defaults to None, which means :math:`w_1 = \cdots = w_m = \frac{1}{m}.` + niter : :obj:`int`, optional + Number of iterations of iterative scheme. + tol : :obj:`float`, optional + Tolerance on change of the solution (used as stopping criterion). + If ``tol=0``, run until ``niter`` is reached. + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + y : :obj:`numpy.ndarray` + Additional estimated model(s) + iiter : :obj:`int` + Number of executed iterations + cost : :obj:`list` + History of the objective function + + """ + x, y = self.setup( + proxfs=proxfs, + x0=x0, + tau=tau, + eta=eta, + weights=weights, + niter=niter, + tol=tol, + show=show, + ) + + x, y = self.run(x, y, niter, show=show, itershow=itershow) + self.finalize(65, show) + return x, y, self.iiter, self.cost + + +# TOD0: +# - make niter optional in setup and fix and prints +# in _print_setup to not include if not provided From d66b485ee06c7d435631174e36337de51fc4cc30 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sat, 20 Jun 2026 14:55:30 +0100 Subject: [PATCH 26/73] test: improved test coverage of primal solvers --- pytests/test_solver.py | 241 +++++++++++++++++++++++++++++++++++------ 1 file changed, 210 insertions(+), 31 deletions(-) diff --git a/pytests/test_solver.py b/pytests/test_solver.py index c5f38c69..408ac5ae 100644 --- a/pytests/test_solver.py +++ b/pytests/test_solver.py @@ -1,5 +1,3 @@ -from typing import Any - import numpy as np import pytest from numpy.testing import assert_array_almost_equal @@ -12,6 +10,8 @@ ADMML2, HQS, PPXA, + AcceleratedProximalGradient, + AndersonProximalGradient, ConsensusADMM, DouglasRachfordSplitting, GeneralizedProximalGradient, @@ -168,7 +168,9 @@ def test_ProximalPoint(par): # Proximal point algorithm with quadatic function quad = Quadratic(Op=MatrixMult(A), b=-y, niter=2) - xpp = ProximalPoint(quad, x0=np.zeros_like(x), tau=0.1, niter=1000, tol=0) + xpp = ProximalPoint( + quad, x0=np.zeros_like(x), tau=0.1, niter=1000, tol=0, show=True + ) assert_array_almost_equal(xpp, x, decimal=2) @@ -201,14 +203,7 @@ def test_PG_ISTA(par): # ISTA/FISTA eps = 5e-1 xista = solver( - Rop, - y, - niter=100, - alpha=tau, - eps=eps, - tol=1e-8, - monitorres=False, - show=False, + Rop, y, niter=100, alpha=tau, eps=eps, tol=1e-8, monitorres=False )[0] # PG @@ -224,6 +219,7 @@ def test_PG_ISTA(par): acceleration=acceleration, niter=100, tol=1e-8, + show=True, ) assert_array_almost_equal(xpg, xista, decimal=2) @@ -255,7 +251,13 @@ def test_PG_GPG(par): l2 = L2(Op=Rop, b=y, niter=10, warm=True) l1 = L1(sigma=5e-1) xpg = ProximalGradient( - l2, l1, x0=np.zeros(m), tau=tau, niter=100, acceleration="fista" + l2, + l1, + x0=np.zeros(m), + tau=tau, + niter=100, + acceleration="fista", + show=True, ) # GPG @@ -272,11 +274,162 @@ def test_PG_GPG(par): tau=tau, niter=100, acceleration="fista", + show=True, ) assert_array_almost_equal(xpg, xgpg, decimal=2) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) +@pytest.mark.parametrize("eta", [1, 0.8]) +@pytest.mark.parametrize("acceleration", [None, "fista", "vandenberghe"]) +def test_PG_backtracking(par, eta, acceleration): + """Check equivalency of ProximalGradient with and without tau (aka backtracking)""" + np.random.seed(0) + n, m = par["n"], par["m"] + + # Define sparse model + x = np.zeros(m) + x[2], x[4] = 1, 0.5 + + # Random mixing matrix + R = np.random.normal(0.0, 1.0, (n, m)) + Rop = MatrixMult(R) + + y = Rop @ x + + # Step size + + # PG with tau + L = (Rop.H * Rop).eigs(1).real + tau = 0.99 / L + + l2 = L2(Op=Rop, b=y, niter=10, warm=True) + l1 = L1(sigma=5e-1) + xpg = ProximalGradient( + l2, + l1, + x0=np.zeros(m), + tau=tau, + eta=eta, + niter=100, + acceleration=acceleration, + show=True, + ) + + # PG without tau + l2 = L2(Op=Rop, b=y, niter=10, warm=True) + l1 = L1(sigma=5e-1) + xpgback = ProximalGradient( + l2, + l1, + x0=np.zeros(m), + backtracking=True, + eta=eta, + niter=100, + acceleration=acceleration, + show=True, + ) + + assert_array_almost_equal(xpg, xpgback, decimal=2) + + +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) +def test_PG_AcceleratedPG(par): + """Check equivalency of ProximalGradient and AcceleratedProximalGradient""" + np.random.seed(0) + n, m = par["n"], par["m"] + + # Define sparse model + x = np.zeros(m) + x[2], x[4] = 1, 0.5 + + # Random mixing matrix + R = np.random.normal(0.0, 1.0, (n, m)) + Rop = MatrixMult(R) + + y = Rop @ x + + # Step size + L = (Rop.H * Rop).eigs(1).real + tau = 0.99 / L + + # PG + l2 = L2(Op=Rop, b=y, niter=10, warm=True) + l1 = L1(sigma=5e-1) + xpg = ProximalGradient( + l2, + l1, + x0=np.zeros(m), + tau=tau, + niter=100, + acceleration="fista", + show=True, + ) + + # APG + l2 = L2(Op=Rop, b=y, niter=10, warm=True) + l1 = L1(sigma=5e-1) + xapg = AcceleratedProximalGradient( + l2, + l1, + x0=np.zeros(m), + tau=tau, + niter=100, + ) + + assert_array_almost_equal(xpg, xapg, decimal=2) + + +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) +def test_PG_AndersonPG(par): + """Check equivalency of ProximalGradient and AndersonProximalGradient""" + np.random.seed(0) + n, m = par["n"], par["m"] + + # Define sparse model + x = np.zeros(m) + x[2], x[4] = 1, 0.5 + + # Random mixing matrix + R = np.random.normal(0.0, 1.0, (n, m)) + Rop = MatrixMult(R) + + y = Rop @ x + + # Step size + L = (Rop.H * Rop).eigs(1).real + tau = 0.99 / L + + # PG + l2 = L2(Op=Rop, b=y, niter=10, warm=True) + l1 = L1(sigma=5e-1) + xpg = ProximalGradient( + l2, + l1, + x0=np.zeros(m), + tau=tau, + niter=100, + acceleration="fista", + show=True, + ) + + # AndersonPG + l2 = L2(Op=Rop, b=y, niter=10, warm=True) + l1 = L1(sigma=5e-1) + xapg = AndersonProximalGradient( + l2, + l1, + x0=np.zeros(m), + tau=tau, + niter=100, + nhistory=5, + show=True, + ) + + assert_array_almost_equal(xpg, xapg, decimal=2) + + @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_PG_TwiST(par): """Check that PG/TwiST can be used to solve a sparsity regularized objective function @@ -304,7 +457,7 @@ def test_PG_TwiST(par): l2 = L2(Op=Rop, b=y, niter=10, warm=True) l1 = L1(sigma=5e-1) xpg = ProximalGradient( - l2, l1, x0=np.zeros(m), tau=tau, niter=100, acceleration="fista" + l2, l1, x0=np.zeros(m), tau=tau, niter=100, acceleration="fista", show=True ) # TwiST @@ -318,13 +471,15 @@ def test_PG_TwiST(par): x0=np.zeros(m), eigs=eigs, niter=100, + show=True, ) assert_array_almost_equal(xpg, xtwist, decimal=2) @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) -def test_HQS_ADMM_L2(par): +@pytest.mark.parametrize("gfirst", [False, True]) +def test_HQS_ADMM_L2(par, gfirst): """Check that HQS/ADMM can be used to solve a pure L2-based objective function (and compare with LSQR - note that despite the trajectory will be different, they should converge to the same solution) @@ -363,19 +518,22 @@ def test_HQS_ADMM_L2(par): # HQS l2 = L2(Op=Rop, b=y, niter=10, warm=True) l2reg = L2(sigma=eps) - xhqs = HQS(l2, l2reg, x0=np.zeros(m), tau=tau, niter=1000)[0] + xhqs = HQS( + l2, l2reg, x0=np.zeros(m), tau=tau, gfirst=gfirst, niter=1000, show=True + )[0] # ADMM l2 = L2(Op=Rop, b=y, niter=10, warm=True) l2reg = L2(sigma=eps) - xadmm = ADMM(l2, l2reg, x0=np.zeros(m), tau=tau, niter=1000)[0] + xadmm = ADMM(l2, l2reg, x0=np.zeros(m), tau=tau, niter=1000, show=True)[0] assert_array_almost_equal(xl2, xhqs, decimal=2) assert_array_almost_equal(xl2, xadmm, decimal=2) @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) -def test_ADMM_ADMML2(par): +@pytest.mark.parametrize("gfirst", [False, True]) +def test_ADMM_ADMML2(par, gfirst): """Check equivalency of ADMM and ADMML2 when the f function is a L2 term """ @@ -400,12 +558,21 @@ def test_ADMM_ADMML2(par): # ADMM l2 = L2(Op=Rop, b=y, niter=10, warm=True) l2reg = L2(sigma=eps) - xadmm = ADMM(l2, l2reg, x0=np.zeros(m), tau=tau, niter=100)[0] + xadmm = ADMM(l2, l2reg, x0=np.zeros(m), tau=tau, niter=100, show=True)[0] # ADMML2 l2reg = L2(sigma=eps) xadmml2 = ADMML2( - l2reg, Rop, y, Aop, x0=np.zeros(m), tau=tau, niter=100, iter_lim=10 + l2reg, + Rop, + y, + Aop, + x0=np.zeros(m), + tau=tau, + gfirst=gfirst, + niter=100, + iter_lim=10, + show=True, )[0] assert_array_almost_equal(xadmm, xadmml2, decimal=2) @@ -439,15 +606,15 @@ def test_ADMM_LinearizedADMM(par): # ADMM l2 = L2(Op=Rop, b=y, niter=10, warm=True) l2reg = L2(sigma=eps) - xadmm = ADMM(l2, l2reg, x0=np.zeros(m), tau=tau, niter=100)[0] + xadmm = ADMM(l2, l2reg, x0=np.zeros(m), tau=tau, niter=100, show=True)[0] # LinearizedADMM l2 = L2(Op=Rop, b=y, niter=10, warm=True) l2reg = L2(sigma=eps) Aop = Identity(m) - xladmm = LinearizedADMM(l2, l2reg, Aop, x0=np.zeros(m), tau=tau, mu=mu, niter=100)[ - 0 - ] + xladmm = LinearizedADMM( + l2, l2reg, Aop, x0=np.zeros(m), tau=tau, mu=mu, niter=100, show=True + )[0] assert_array_almost_equal(xadmm, xladmm, decimal=2) @@ -477,20 +644,20 @@ def test_ADMM_DRS(par): # ADMM l2 = L2(Op=Rop, b=y, niter=10, warm=True) l1 = L1(sigma=5e-1) - xadmm, zadmm = ADMM(l2, l1, x0=np.zeros(m), tau=tau, niter=100) + xadmm, zadmm = ADMM(l2, l1, x0=np.zeros(m), tau=tau, niter=100, show=True) # DRS with g first l2 = L2(Op=Rop, b=y, niter=10, warm=True) l1 = L1(sigma=5e-1) xdrs_g, ydrs_g = DouglasRachfordSplitting( - l2, l1, x0=np.zeros(m), tau=tau, niter=100, gfirst=True + l2, l1, x0=np.zeros(m), tau=tau, niter=100, gfirst=True, show=True ) # DRS with f first l2 = L2(Op=Rop, b=y, niter=10, warm=True) l1 = L1(sigma=5e-1) xdrs_f, ydrs_f = DouglasRachfordSplitting( - l2, l1, x0=np.zeros(m), tau=tau, niter=100, gfirst=False + l2, l1, x0=np.zeros(m), tau=tau, niter=100, gfirst=False, show=True ) assert_array_almost_equal(xadmm, xdrs_g, decimal=2) @@ -498,7 +665,8 @@ def test_ADMM_DRS(par): @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) -def test_PPXA_with_ADMM(par: dict[str, Any]) -> None: +@pytest.mark.parametrize("weights", [None, (0.5, 0.5)]) +def test_PPXA_with_ADMM(par, weights) -> None: """Check equivalency of PPXA and ADMM when using a single regularization term """ @@ -529,18 +697,21 @@ def test_PPXA_with_ADMM(par: dict[str, Any]) -> None: x0=np.zeros(m), tau=tau, niter=2000, # niter=1500 makes this test fail for seeds 0 to 499 + show=True, ) xppxa = PPXA( [l2, l1], x0=np.zeros(m), tau=np.random.uniform(3 * tau, 5 * tau), + weights=weights, + show=True, ) assert_array_almost_equal(xppxa, xadmm, decimal=2) @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) -def test_PPXA_with_GPG(par: dict[str, Any]) -> None: +def test_PPXA_with_GPG(par) -> None: """Check equivalency of PPXA and GeneralizedProximalGradient""" np.random.seed(0) @@ -577,18 +748,20 @@ def test_PPXA_with_GPG(par: dict[str, Any]) -> None: x0=np.zeros(m), tau=tau, niter=200, # niter=150 makes this test fail for seeds 0 to 499 + show=True, ) xppxa = PPXA( [l2_1, l2_2, l1_1, l1_2], x0=np.zeros(m), tau=np.random.uniform(3 * tau, 5 * tau), + show=True, ) assert_array_almost_equal(xppxa, xgpg, decimal=2) @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) -def test_ConsensusADMM_with_ADMM(par: dict[str, Any]) -> None: +def test_ConsensusADMM_with_ADMM(par) -> None: """Check equivalency of ConsensusADMM and ADMM when two proximable functions """ @@ -619,18 +792,20 @@ def test_ConsensusADMM_with_ADMM(par: dict[str, Any]) -> None: x0=np.zeros(m), tau=tau, niter=2000, # niter=1500 makes this test fail for seeds 0 to 499 + show=True, ) xcadmm = ConsensusADMM( [l2, l1], x0=np.random.normal(0.0, 1.0, m), # x0=np.zeros(m), tau=np.random.uniform(3 * tau, 5 * tau), + show=True, ) assert_array_almost_equal(xcadmm, xadmm, decimal=2) @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) -def test_ConsensusADMM_with_ADMM_for_Lasso(par: dict[str, Any]) -> None: +def test_ConsensusADMM_with_ADMM_for_Lasso(par) -> None: """Check equivalency of ConsensusADMM and ADMM when more than two proximable functions for lasso """ @@ -677,6 +852,7 @@ def test_ConsensusADMM_with_ADMM_for_Lasso(par: dict[str, Any]) -> None: x0=np.random.normal(0.0, 1.0, m), # x0=np.zeros(m), tau=np.random.uniform(3 * tau, 5 * tau), niter=20000, # niter=15000 makes this test fail for seeds 0 to 499 + show=True, ) # 1/2 || [R1; R2; R3] ||_2^2 + ||x||_1 @@ -686,13 +862,14 @@ def test_ConsensusADMM_with_ADMM_for_Lasso(par: dict[str, Any]) -> None: x0=np.zeros(m), tau=tau, niter=15000, # niter=10000 makes this test fail for seeds 0 to 499 + show=True, ) assert_array_almost_equal(xcadmm, xadmm, decimal=2) @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) -def test_ConsensusADMM_with_GPG(par: dict[str, Any]) -> None: +def test_ConsensusADMM_with_GPG(par) -> None: """Check equivalency of ConsensusADMM and GeneralizedProximalGradient""" np.random.seed(0) @@ -730,11 +907,13 @@ def test_ConsensusADMM_with_GPG(par: dict[str, Any]) -> None: x0=np.zeros(m), tau=tau, niter=200, # niter=150 makes this test fail for seeds 0 to 499 + show=True, ) xppxa = ConsensusADMM( [l2_1, l2_2, l1_1, l1_2], x0=np.zeros(m), tau=np.random.uniform(3 * tau, 5 * tau), + show=True, ) assert_array_almost_equal(xppxa, xgpg, decimal=2) From 6464eaed3156cd8a49f9453df8897141cf972e35 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sat, 20 Jun 2026 14:59:07 +0100 Subject: [PATCH 27/73] feat: added ConsensusADMM and improved overall handling of tol --- pyproximal/optimization/cls_primal.py | 369 ++++++++++++++++++++++++-- 1 file changed, 341 insertions(+), 28 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index dc7f1c43..b44b8d7c 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -9,6 +9,8 @@ "LinearizedADMM", "TwIST", "DouglasRachfordSplitting", + "PPXA", + "ConsensusADMM", ] from collections.abc import Sequence @@ -184,7 +186,7 @@ def setup( # type: ignore[override] x0: NDArray, tau: float, niter: int | None = None, - tol: float = 1e-4, + tol: float | None = None, show: bool = False, ) -> NDArray: r"""Setup solver @@ -324,7 +326,7 @@ def solve( # type: ignore[override] x0: NDArray, tau: float, niter: int = 10, - tol: float = 1e-4, + tol: float | None = None, show: bool = False, itershow: tuple[int, int, int] = (10, 10, 10), ) -> tuple[NDArray, int, NDArray]: @@ -431,13 +433,11 @@ def _print_setup(self, epsg_print: str, xcomplex: bool = False) -> None: ) strpar1 = f"tau = {self.tau:4.2e}\t\tbacktrack = {self.backtracking}" + strpar2 = f"beta = {self.beta}\t\tepsg = {epsg_print}\t\tacceleration = {self.acceleration}" if self.niter is not None: - strpar2 = f"beta = {self.beta}\tepsg = {epsg_print}\t\tniter = {self.niter}\t\ttol = {str(self.tol)}" + strpar3 = f"niter = {self.niter}\t\tniterback = {self.niterback}\t\ttol = {str(self.tol)}" else: - strpar2 = ( - f"beta = {self.beta}\t epsg = {epsg_print}\t\ttol = {str(self.tol)}" - ) - strpar3 = f"niterback = {self.niterback}\t\tacceleration = {self.acceleration}" + strpar3 = f"niterback = {self.niterback}\t\ttol = {str(self.tol)}" print(strpar) print(strpar1) print(strpar2) @@ -3899,7 +3899,6 @@ def _print_setup(self, xcomplex: bool = False) -> None: def _print_step(self, x: NDArray) -> None: if self.tol is None: self.pf = self.ncp.sum([self.proxfs[i](x) for i in range(self.nprox)]) - self.pf = self.ncp.sum([self.proxfs[i](x) for i in range(self.nprox)]) x0 = to_numpy(x[0]) strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " msg = f"{self.iiter:6g} " + strx + f"{self.pf:10.3e}" @@ -3913,7 +3912,7 @@ def setup( # type: ignore[override] eta: float = 1.0, weights: NDArray | list[float] | None = None, niter: int = 1000, - tol: float | None = 1e-7, + tol: float | None = None, show: bool = False, ) -> tuple[NDArray, NDArray]: r"""Setup solver @@ -3937,8 +3936,8 @@ def setup( # type: ignore[override] niter : :obj:`int`, optional Number of iterations of iterative scheme. tol : :obj:`float`, optional - Tolerance on change of the solution (used as stopping criterion). - If ``tol=0``, run until ``niter`` is reached. + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached show : :obj:`bool`, optional Display iterations log @@ -4010,8 +4009,6 @@ def step( Updated additional model vector """ - x_old = x.copy() - # proximal steps p = self.ncp.stack( [self.proxfs[i].prox(y[i], self.tau / self.w[i]) for i in range(self.nprox)] @@ -4022,19 +4019,13 @@ def step( # tolerance check: break iterations if overall # objective does not decrease below tolerance - # if self.tol is not None: - # self.pfold = self.pf - # self.pf = self.ncp.sum([self.proxfs[i](x) for i in range(self.nprox)]) - # if np.abs(1.0 - self.pf / self.pfold) < self.tol: - # self.tolbreak = True - # else: - # self.pf = 0.0 - - # tolerance check: break iterations if solution does - # not changeabove tolerance if self.tol is not None: - if self.ncp.abs(x - x_old).max() < self.tol: + self.pfold = self.pf + self.pf = self.ncp.sum([self.proxfs[i](x) for i in range(self.nprox)]) + if np.abs(1.0 - self.pf / self.pfold) < self.tol: self.tolbreak = True + else: + self.pf = 0.0 self.iiter += 1 if show: @@ -4095,7 +4086,7 @@ def run( else False ) x, y = self.step(x, y, showstep) - self.callback(x, y) + self.callback(x) # check if any callback has raised a stop flag stop = _callback_stop(self.callbacks) if stop: @@ -4110,7 +4101,7 @@ def solve( # type: ignore[override] eta: float = 1.0, weights: NDArray | list[float] | None = None, niter: int = 1000, - tol: float | None = 1e-7, + tol: float | None = None, show: bool = False, itershow: tuple[int, int, int] = (10, 10, 10), ) -> tuple[NDArray, NDArray, int, NDArray]: @@ -4135,8 +4126,8 @@ def solve( # type: ignore[override] niter : :obj:`int`, optional Number of iterations of iterative scheme. tol : :obj:`float`, optional - Tolerance on change of the solution (used as stopping criterion). - If ``tol=0``, run until ``niter`` is reached. + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached show : :obj:`bool`, optional Display logs itershow : :obj:`tuple`, optional @@ -4172,6 +4163,328 @@ def solve( # type: ignore[override] return x, y, self.iiter, self.cost +class ConsensusADMM(Solver): + r"""Consensus ADMM + + Solves the following global consensus problem using ADMM: + + .. math:: + + \argmin_{\mathbf{x_1}, \mathbf{x_2}, \ldots, \mathbf{x_m}} + \sum_{i=1}^m f_i(\mathbf{x}_i) \quad \text{s.t.} + \quad \mathbf{x_1} = \mathbf{x_2} = \cdots = \mathbf{x_m} + + where :math:`f_i(\mathbf{x})` are any convex + functions that has known proximal operators. + + See Also + -------- + ADMM: Alternating Direction Method of Multipliers + PPXA: Parallel Proximal Algorithm + + Notes + ----- + The ADMM for the consensus problem can be expressed by the following + recursion [1]_, [2]_: + + * :math:`\bar{\mathbf{x}}^{0} = \mathbf{x}` + * for :math:`k = 1, \ldots` + + * for :math:`i = 1, \ldots, m` + + * :math:`\mathbf{x}_i^{k+1} = \mathrm{prox}_{\tau f_i} \left(\bar{\mathbf{x}}^{k} - \mathbf{y}_i^{k}\right)` + + * :math:`\bar{\mathbf{x}}^{k+1} = \frac{1}{m} \sum_{i=1}^m \mathbf{x}_i^{k}` + + * for :math:`i = 1, \ldots, m` + + * :math:`\mathbf{y}_i^{k+1} = \mathbf{y}_i^{k} + \mathbf{x}_i^{k+1} - \bar{\mathbf{x}}^{k+1}` + + The current implementation returns :math:`\bar{\mathbf{x}}`. + + References + ---------- + .. [1] Boyd, S., Parikh, N., Chu, E., Peleato, B., Eckstein, J., 2011. + Distributed Optimization and Statistical Learning via the Alternating + Direction Method of Multipliers. Foundations and Trends in Machine Learning, + Vol. 3, No. 1, pp 1-122. Section 7.1. https://doi.org/10.1561/2200000016 + https://stanford.edu/~boyd/papers/pdf/admm_distr_stats.pdf + .. [2] Parikh, N., Boyd, S., 2014. Proximal Algorithms. Foundations and + Trends in Optimization, Vol. 1, No. 3, pp 127-239. + Section 5.2.1. https://doi.org/10.1561/2400000003 + https://web.stanford.edu/~boyd/papers/pdf/prox_algs.pdf + + """ + + def _print_setup(self, xcomplex: bool = False) -> None: + self._print_solver(nbar=65) + + strpar = "\n".join( + [ + f"Proximal operator (f{i}): {type(proxf).__name__}" + for i, proxf in enumerate(self.proxfs) + ] + ) + strpar1 = f"tau = {self.tau:6e}\tniter = {self.niter}\ttol = {self.tol}" + print(strpar) + print(strpar1) + + print("-" * 65 + "\n") + if not xcomplex: + head1 = " Itn x[0] J=sum_i f_i" + else: + head1 = " Itn x[0] J=sum_i f_i" + print(head1) + + def _print_step(self, x: NDArray) -> None: + if self.tol is None: + self.pf = self.ncp.sum([self.proxfs[i](x) for i in range(self.nprox)]) + x0 = to_numpy(x[0]) + strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " + msg = f"{self.iiter:6g} " + strx + f"{self.pf:10.3e}" + print(msg) + + def setup( # type: ignore[override] + self, + proxfs: list[ProxOperator], + x0: NDArray, + tau: float, + niter: int = 1000, + tol: float | None = None, + show: bool = False, + ) -> tuple[NDArray, NDArray, NDArray]: + r"""Setup solver + + Parameters + ---------- + proxfs : :obj:`list` + A list of proximable functions :math:`f_1, \ldots, f_m`. + x0 : :obj:`numpy.ndarray` + Initial vector + tau : :obj:`float` + Positive scalar weight + niter : :obj:`int`, optional + Number of iterations of iterative scheme. + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display iterations log + + Returns + ------- + x : :obj:`numpy.ndarray` + Initial guess + x_bar : :obj:`numpy.ndarray` + Initial guess for averaged model vector + y : :obj:`numpy.ndarray` + Initial guess for additional model vector + + """ + self.proxfs = proxfs + self.tau = tau + self.niter = niter + self.tol = tol + + self.ncp = get_array_module(x0) + + # initialize solver + self.nprox = len(proxfs) + x = x0.copy() + x_bar = x0.copy() + y = self.ncp.zeros((self.nprox, x0.size), dtype=x0.dtype) + + # create variables to track the objective function and iterations + self.pf, self.pfold = np.inf, np.inf + self.cost: list[float] = [] + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(np.iscomplexobj(x0)) + return x, x_bar, y + + def step( + self, x: NDArray, x_bar: NDArray, y: NDArray, show: bool = False + ) -> tuple[NDArray, NDArray, NDArray]: + r"""Run one step of solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by a step of the + ConsensusADMM algorithm + x_bar : :obj:`numpy.ndarray` + Current averaged model vector to be updated by a step of the + ConsensusADMM algorithm + y : :obj:`numpy.ndarray` + Additional model vector to be updated by multiple steps of + the ConsensusADMM algorithm + show : :obj:`bool`, optional + Display iteration log + + Returns + ------- + x : :obj:`numpy.ndarray` + Updated model vectors + x_bar : :obj:`numpy.ndarray` + Updated averaged model vector + y : :obj:`numpy.ndarray` + Updated additional model vector + + """ + # proximal steps + x = self.ncp.stack( + [self.proxfs[i].prox(x_bar - y[i], self.tau) for i in range(self.nprox)] + ) + x_bar = self.ncp.mean(x, axis=0) + y = y + x - x_bar + + # tolerance check: break iterations if overall + # objective does not decrease below tolerance + if self.tol is not None: + self.pfold = self.pf + self.pf = self.ncp.sum([self.proxfs[i](x_bar) for i in range(self.nprox)]) + if np.abs(1.0 - self.pf / self.pfold) < self.tol: + self.tolbreak = True + else: + self.pf = 0.0 + + self.iiter += 1 + if show: + self._print_step(x_bar) + if self.tol is not None or show: + self.cost.append(float(self.pf)) + return x, x_bar, y + + def run( + self, + x: NDArray, + x_bar: NDArray, + y: NDArray, + niter: int | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray, NDArray]: + r"""Run solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by multiple steps of + the PPXA algorithm + x_bar : :obj:`numpy.ndarray` + Current averaged model vector to be updated by multiple steps of + the PPXA algorithm + y : :obj:`numpy.ndarray` + Additional model vector to be updated by multiple steps of + the PPXA algorithm + niter : :obj:`int`, optional + Number of iterations. Can be set to ``None`` if already + provided in the setup call + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + x_bar : :obj:`numpy.ndarray` + Estimated averaged model + y : :obj:`numpy.ndarray` + Additional estimated model + + """ + niter = self.niter if niter is None else niter + if niter is None: + msg = "`niter` must not be None" + raise ValueError(msg) + while self.iiter < niter and not self.tolbreak: + showstep = ( + True + if show + and ( + self.iiter < itershow[0] + or niter - self.iiter < itershow[1] + or self.iiter % itershow[2] == 0 + ) + else False + ) + x, x_bar, y = self.step(x, x_bar, y, showstep) + self.callback(x_bar) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x, x_bar, y + + def solve( # type: ignore[override] + self, + proxfs: list[ProxOperator], + x0: NDArray, + tau: float, + niter: int = 1000, + tol: float | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray, NDArray, int, NDArray]: + r"""Run entire solver + + Parameters + ---------- + proxfs : :obj:`list` + A list of proximable functions :math:`f_1, \ldots, f_m`. + x0 : :obj:`numpy.ndarray` + Initial vector + tau : :obj:`float` + Positive scalar weight + niter : :obj:`int`, optional + Number of iterations of iterative scheme. + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + x_bar : :obj:`numpy.ndarray` + Estimated averaged model + y : :obj:`numpy.ndarray` + Additional estimated model + iiter : :obj:`int` + Number of executed iterations + cost : :obj:`list` + History of the objective function + + """ + x, x_bar, y = self.setup( + proxfs=proxfs, + x0=x0, + tau=tau, + niter=niter, + tol=tol, + show=show, + ) + + x, x_bar, y = self.run(x, x_bar, y, niter, show=show, itershow=itershow) + self.finalize(65, show) + return x, x_bar, y, self.iiter, self.cost + + # TOD0: # - make niter optional in setup and fix and prints # in _print_setup to not include if not provided +# - add the tolerance check on the solution for PPXA and +# ConsensunADMM for the function based solvers via callbacks From 8de09003819a4759f91129eecdbddf557d1165b7 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 26 Jun 2026 10:50:24 +0100 Subject: [PATCH 28/73] minor: removed unused callback method from Solver --- pyproximal/optimization/basesolver.py | 41 --------------------------- 1 file changed, 41 deletions(-) diff --git a/pyproximal/optimization/basesolver.py b/pyproximal/optimization/basesolver.py index 97739a82..008b6cdb 100644 --- a/pyproximal/optimization/basesolver.py +++ b/pyproximal/optimization/basesolver.py @@ -14,8 +14,6 @@ if TYPE_CHECKING: from pyproximal.ProxOperator import ProxOperator -_units = {"B": 1, "KB": 1024, "MB": 1024**2, "GB": 1024**3} - class Solver(pSolver, metaclass=ABCMeta): # type: ignore[misc] r"""Solver @@ -163,42 +161,3 @@ def finalize(self, nbar: int = 60, show: bool = False) -> None: self.cost: NDArray = np.array(self.cost) if show: self._print_finalize(nbar=nbar) - - def callback( # noqa: B027 - self, - x: NDArray, - z: NDArray | None = None, - *args: Any, - **kwargs: Any, - ) -> None: - """Callback routine - - This routine must be passed by the user. Its function signature must contain - either a single input that contains the current solution or two inputs - that contain the current solutions for methods that apply splitting - (when using the `solve` method it will be automatically invoked after - each step of the solve) - - Parameters - ---------- - x : :obj:`numpy.ndarray` - Current solution - z : :obj:`numpy.ndarray` - Current additional solution - - Examples - -------- - >>> import numpy as np - >>> from pyproximal.optimization.cls_primal import ADMM - >>> def callback(x, z): - ... print(f"Running callback, current solutions {x} - {z}") - ... - >>> admmsolve.callback = callback - - >>> x = np.ones(2) - >>> z = np.zeros(2) - >>> admmsolve.callback(x, z) - Running callback, current solutions [1. 1.] - [0. 0.] - - """ - pass From 2de1345eccb9542a5760b94598d4148f27c26e7f Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 26 Jun 2026 10:52:06 +0100 Subject: [PATCH 29/73] feat: various improvements to class-based solvers to match old functional behaviour --- pyproximal/optimization/cls_primal.py | 272 ++++++++++++++++---------- 1 file changed, 170 insertions(+), 102 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index b44b8d7c..0a39745c 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -427,12 +427,17 @@ class ProximalGradient(Solver): def _print_setup(self, epsg_print: str, xcomplex: bool = False) -> None: self._print_solver(nbar=81) + tau_str = ( + f"{self.tau[0]:4.2e}" + if self.tau.size == 1 + else ", ".join(f"{tau:4.2e}" for tau in self.tau) + ) + strpar = ( f"Proximal operator (f): {type(self.proxf).__name__}\n" f"Proximal operator (g): {type(self.proxg).__name__}\n" ) - - strpar1 = f"tau = {self.tau:4.2e}\t\tbacktrack = {self.backtracking}" + strpar1 = f"tau = {tau_str}\t\tbacktrack = {self.backtracking}" strpar2 = f"beta = {self.beta}\t\tepsg = {epsg_print}\t\tacceleration = {self.acceleration}" if self.niter is not None: strpar3 = f"niter = {self.niter}\t\tniterback = {self.niterback}\t\ttol = {str(self.tol)}" @@ -455,10 +460,15 @@ def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: self.pfg = pf + np.sum(self.epsg[self.iiter - 1] * pg) x0 = to_numpy(x[0]) if x.ndim == 1 else to_numpy(x[0, 0]) strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " + tau_str = ( + f"{self.tau[0]:4.2e}" + if self.tau.size == 1 + else ", ".join(f"{tau:4.2e}" for tau in self.tau) + ) msg = ( f"{self.iiter:6g} " + strx - + f"{pf:10.3e} {pg:10.3e} {self.pfg:10.3e} {self.tau:11.2e}" + + f"{pf:10.3e} {pg:10.3e} {self.pfg:10.3e} {tau_str}" ) print(msg) @@ -472,9 +482,9 @@ def setup( # type: ignore[override] backtracking: bool = False, beta: float = 0.5, eta: float = 1.0, - niter: int = 10, - niterback: int = 100, acceleration: str | None = None, + niterback: int = 100, + niter: int = 10, tol: float | None = None, show: bool = False, ) -> tuple[NDArray, NDArray]: @@ -505,12 +515,12 @@ def setup( # type: ignore[override] Backtracking parameter (must be between 0 and 1) eta : :obj:`float`, optional Relaxation parameter (must be between 0 and 1, 0 excluded). - niter : :obj:`int`, optional - Number of iterations of iterative scheme - niterback : :obj:`int`, optional - Max number of iterations of backtracking acceleration : :obj:`str`, optional Acceleration (``None``, ``vandenberghe`` or ``fista``) + niterback : :obj:`int`, optional + Max number of iterations of backtracking + niter : :obj:`int`, optional + Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -531,25 +541,25 @@ def setup( # type: ignore[override] self.backtracking = backtracking self.beta = beta self.eta = eta - self.niter = niter self.niterback = niterback + self.niter = niter self.tol = tol self.ncp = get_array_module(x0) # check if epgs is a vector - self.epsg = np.asarray(epsg, dtype=float) + self.epsg = self.ncp.asarray(epsg, dtype=np.float32) if self.epsg.size == 1: - self.epsg = epsg * np.ones(niter) + self.epsg = epsg * self.ncp.ones(niter, dtype=np.float32) epsg_print = str(self.epsg[0]) else: epsg_print = "Multi" # set tau - self.tau = tau if tau is None: self.backtracking = True - self.tau = 1.0 + tau = 1.0 + self.tau = self.ncp.atleast_1d(self.ncp.asarray(tau, dtype=np.float32)) # check acceleration if acceleration in [None, "None", "vandenberghe", "fista"]: @@ -566,8 +576,12 @@ def setup( # type: ignore[override] self.t = 1.0 # create variables to track the objective function and iterations - self.pfg, self.pfgold = np.inf, np.inf + pf, pg = self.proxf(x), self.proxg(x) + pfg = pf + np.sum(self.epsg[self.iiter] * pg) + self.pfg, self.pfgold = pfg, pfg + self.cost: list[float] = [] + self.cost.append(float(self.pfg)) self.tolbreak = False self.iiter = 0 @@ -577,7 +591,10 @@ def setup( # type: ignore[override] return x, y def step( - self, x: NDArray, y: NDArray, show: bool = False + self, + x: NDArray, + y: NDArray, + show: bool = False, ) -> tuple[NDArray, NDArray]: r"""Run one step of solver @@ -737,9 +754,9 @@ def solve( # type: ignore[override] backtracking: bool = False, beta: float = 0.5, eta: float = 1.0, - niter: int = 10, - niterback: int = 100, acceleration: str | None = None, + niterback: int = 100, + niter: int = 10, tol: float | None = None, show: bool = False, itershow: tuple[int, int, int] = (10, 10, 10), @@ -771,12 +788,12 @@ def solve( # type: ignore[override] Backtracking parameter (must be between 0 and 1) eta : :obj:`float`, optional Relaxation parameter (must be between 0 and 1, 0 excluded). - niter : :obj:`int`, optional - Number of iterations of iterative scheme - niterback : :obj:`int`, optional - Max number of iterations of backtracking acceleration : :obj:`str`, optional Acceleration (``None``, ``vandenberghe`` or ``fista``) + niterback : :obj:`int`, optional + Max number of iterations of backtracking + niter : :obj:`int`, optional + Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -808,9 +825,9 @@ def solve( # type: ignore[override] backtracking=backtracking, beta=beta, eta=eta, - niter=niter, - niterback=niterback, acceleration=acceleration, + niterback=niterback, + niter=niter, tol=tol, show=show, ) @@ -865,13 +882,17 @@ class AndersonProximalGradient(Solver): def _print_setup(self, epsg_print: str, xcomplex: bool = False) -> None: self._print_solver(nbar=81) + tau_str = ( + f"{self.tau[0]:4.2e}" + if self.tau.size == 1 + else ", ".join(f"{tau:4.2e}" for tau in self.tau) + ) + strpar = ( f"Proximal operator (f): {type(self.proxf).__name__}\n" f"Proximal operator (g): {type(self.proxg).__name__}\n" ) - strpar1 = ( - f"tau = {self.tau:4.2e}\t\tepsg = {epsg_print}\t\tniter = {self.niter}" - ) + strpar1 = f"tau = {tau_str}\t\tepsg = {epsg_print}\t\tniter = {self.niter}" strpar2 = f"nhist = {self.nhistory}\t\tepsr = {self.epsr:4.2e}" strpar3 = f"guard = {str(self.safeguard)}\t\ttol = {str(self.tol)}" print(strpar) @@ -891,10 +912,15 @@ def _print_step(self, x: NDArray, pg: float | None) -> None: self.pfg = self.pf + np.sum(self.epsg[self.iiter - 1] * pg) x0 = to_numpy(x[0]) if x.ndim == 1 else to_numpy(x[0, 0]) strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " + tau_str = ( + f"{self.tau[0]:4.2e}" + if self.tau.size == 1 + else ", ".join(f"{tau:4.2e}" for tau in self.tau) + ) msg = ( f"{self.iiter:6g} " + strx - + f"{self.pf:10.3e} {pg:10.3e} {self.pfg:10.3e} {self.tau:11.2e}" + + f"{self.pf:10.3e} {pg:10.3e} {self.pfg:10.3e} {tau_str}" ) print(msg) @@ -905,10 +931,10 @@ def setup( # type: ignore[override] x0: NDArray, epsg: float | NDArray = 1.0, tau: float | NDArray = 1.0, - niter: int = 10, - nhistory: int = 10, epsr: float = 1e-10, safeguard: bool = False, + nhistory: int = 10, + niter: int = 10, tol: float | None = None, show: bool = False, ) -> tuple[NDArray, NDArray]: @@ -927,17 +953,17 @@ def setup( # type: ignore[override] tau : :obj:`float` or :obj:`numpy.ndarray`, optional Positive scalar weight, which should satisfy the following condition to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is - the Lipschitz constant of :math:`\nabla f`. N ote that :math:`\tau` + the Lipschitz constant of :math:`\nabla f`. Note that :math:`\tau` can be chosen to be a vector when dealing with problems with multiple right-hand-sides - niter : :obj:`int`, optional - Number of iterations of iterative scheme - nhistory : :obj:`int`, optional - Number of previous iterates to be kept in memory (to compute the scaling factors) epsr : :obj:`float`, optional Scaling factor for regularization added to the inverse of :math:\mathbf{R}^T \mathbf{R}` safeguard : :obj:`bool`, optional Apply safeguarding strategy to the update (``True``) or not (``False``) + nhistory : :obj:`int`, optional + Number of previous iterates to be kept in memory (to compute the scaling factors) + niter : :obj:`int`, optional + Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -953,23 +979,25 @@ def setup( # type: ignore[override] self.proxf = proxf self.proxg = proxg self.x0 = x0 - self.tau = tau - self.niter = niter - self.nhistory = nhistory self.epsr = epsr self.safeguard = safeguard + self.nhistory = nhistory + self.niter = niter self.tol = tol self.ncp = get_array_module(x0) # check if epgs is a vector - self.epsg = np.asarray(epsg, dtype=float) + self.epsg = self.ncp.asarray(epsg, dtype=np.float32) if self.epsg.size == 1: - self.epsg = self.epsg * np.ones(niter) + self.epsg = self.epsg * self.ncp.ones(niter, dtype=np.float32) epsg_print = str(self.epsg[0]) else: epsg_print = "Multi" + # set tau + self.tau = self.ncp.atleast_1d(self.ncp.asarray(tau, dtype=np.float32)) + # initialize solver y = x0 - self.tau * proxf.grad(x0) x = self.proxg.prox(y, self.epsg[0] * self.tau) @@ -999,7 +1027,10 @@ def setup( # type: ignore[override] return x, y def step( - self, x: NDArray, y: NDArray, show: bool = False + self, + x: NDArray, + y: NDArray, + show: bool = False, ) -> tuple[NDArray, NDArray]: r"""Run one step of solver @@ -1152,10 +1183,10 @@ def solve( # type: ignore[override] x0: NDArray, epsg: float | NDArray = 1.0, tau: float | NDArray = 1.0, - niter: int = 10, - nhistory: int = 10, epsr: float = 1e-10, safeguard: bool = False, + nhistory: int = 10, + niter: int = 10, tol: float | None = None, show: bool = False, itershow: tuple[int, int, int] = (10, 10, 10), @@ -1175,17 +1206,17 @@ def solve( # type: ignore[override] tau : :obj:`float` or :obj:`numpy.ndarray`, optional Positive scalar weight, which should satisfy the following condition to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is - the Lipschitz constant of :math:`\nabla f`. N ote that :math:`\tau` + the Lipschitz constant of :math:`\nabla f`. Note that :math:`\tau` can be chosen to be a vector when dealing with problems with multiple right-hand-sides - niter : :obj:`int`, optional - Number of iterations of iterative scheme - nhistory : :obj:`int`, optional - Number of previous iterates to be kept in memory (to compute the scaling factors) epsr : :obj:`float`, optional Scaling factor for regularization added to the inverse of :math:\mathbf{R}^T \mathbf{R}` safeguard : :obj:`bool`, optional Apply safeguarding strategy to the update (``True``) or not (``False``) + nhistory : :obj:`int`, optional + Number of previous iterates to be kept in memory (to compute the scaling factors) + niter : :obj:`int`, optional + Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -1214,10 +1245,10 @@ def solve( # type: ignore[override] x0=x0, epsg=epsg, tau=tau, - niter=niter, - nhistory=nhistory, epsr=epsr, safeguard=safeguard, + nhistory=nhistory, + niter=niter, tol=tol, show=show, ) @@ -1269,7 +1300,9 @@ def _print_setup(self, epsg_print: str, xcomplex: bool = False) -> None: f"Proximal operators (f): {[type(proxf).__name__ for proxf in self.proxfs]}\n" f"Proximal operators (g): {[type(proxg).__name__ for proxg in self.proxgs]}\n" ) - strpar1 = f"tau = {self.tau:4.2e}\tepsg = {epsg_print}\tniter = {self.niter}" + strpar1 = ( + f"tau = {float(self.tau):4.2e}\tepsg = {epsg_print}\tniter = {self.niter}" + ) print(strpar) print(strpar1) print("-" * 65 + "\n") @@ -1309,8 +1342,8 @@ def setup( # type: ignore[override] epsg: float | NDArray = 1.0, weights: NDArray | None = None, eta: float = 1.0, - niter: int = 10, acceleration: str | None = None, + niter: int = 10, tol: float | None = None, show: bool = False, ) -> tuple[NDArray, NDArray]: @@ -1335,10 +1368,10 @@ def setup( # type: ignore[override] eta : :obj:`float`, optional Relaxation parameter (must be between 0 and 1, 0 excluded). Note that this will be only used when ``acceleration=None``. - niter : :obj:`int`, optional - Number of iterations of iterative scheme acceleration: :obj:`str`, optional Acceleration (``None``, ``vandenberghe`` or ``fista``) + niter : :obj:`int`, optional + Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -1372,9 +1405,9 @@ def setup( # type: ignore[override] raise ValueError(msg) # check if epgs is a vector - self.epsg = np.asarray(epsg, dtype=float) + self.epsg = np.asarray(epsg, dtype=np.float32) if self.epsg.size == 1: - self.epsg = epsg * np.ones(len(proxgs)) + self.epsg = epsg * self.ncp.ones(len(proxgs), dtype=np.float32) epsg_print = str(self.epsg[0]) else: epsg_print = "Multi" @@ -1406,7 +1439,10 @@ def setup( # type: ignore[override] return x, y def step( - self, x: NDArray, y: NDArray, show: bool = False + self, + x: NDArray, + y: NDArray, + show: bool = False, ) -> tuple[NDArray, NDArray]: r"""Run one step of solver @@ -1547,8 +1583,8 @@ def solve( # type: ignore[override] epsg: float | NDArray = 1.0, weights: NDArray | None = None, eta: float = 1.0, - niter: int = 10, acceleration: str | None = None, + niter: int = 10, tol: float | None = None, show: bool = False, itershow: tuple[int, int, int] = (10, 10, 10), @@ -1574,10 +1610,10 @@ def solve( # type: ignore[override] eta : :obj:`float`, optional Relaxation parameter (must be between 0 and 1, 0 excluded). Note that this will be only used when ``acceleration=None``. - niter : :obj:`int`, optional - Number of iterations of iterative scheme acceleration: :obj:`str`, optional Acceleration (``None``, ``vandenberghe`` or ``fista``) + niter : :obj:`int`, optional + Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -1608,8 +1644,8 @@ def solve( # type: ignore[override] epsg=epsg, weights=weights, eta=eta, - niter=niter, acceleration=acceleration, + niter=niter, tol=tol, show=show, ) @@ -1702,9 +1738,10 @@ def setup( # type: ignore[override] x0: NDArray, tau: float | NDArray, z0: NDArray | None = None, - niter: int = 10, gfirst: bool = True, + niter: int = 10, tol: float | None = None, + callbackz: bool = False, show: bool = False, ) -> tuple[NDArray, NDArray]: r"""Setup solver @@ -1726,14 +1763,16 @@ def setup( # type: ignore[override] strategy) z0 : :obj:`numpy.ndarray`, optional Initial z vector (not required when ``gfirst=True``) - niter : :obj:`int` - Number of iterations of iterative scheme gfirst : :obj:`bool`, optional Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) + niter : :obj:`int`, optional + Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached + callbackz : :obj:`bool`, optional + Modify callback signature to (``callback(x, z)``) when ``callbackz=True`` show : :obj:`bool`, optional Display iterations log @@ -1752,17 +1791,18 @@ def setup( # type: ignore[override] """ self.proxf = proxf self.proxg = proxg - self.niter = niter self.gfirst = gfirst + self.niter = niter self.tol = tol + self.callbackz = callbackz self.ncp = get_array_module(x0) # check if tau is a vector - self.tau = self.ncp.asarray(tau, dtype=float) + self.tau = self.ncp.asarray(tau, dtype=np.float32) if self.tau.size == 1: tau_print = str(np.round(self.tau, 6)) - self.tau = self.tau * np.ones(niter) + self.tau = self.tau * self.ncp.ones(niter, dtype=np.float32) else: tau_print = "Variable" @@ -1784,7 +1824,10 @@ def setup( # type: ignore[override] return x, z def step( - self, x: NDArray, z: NDArray, show: bool = False + self, + x: NDArray, + z: NDArray, + show: bool = False, ) -> tuple[NDArray, NDArray]: r"""Run one step of solver @@ -1886,7 +1929,10 @@ def run( else False ) x, z = self.step(x, z, showstep) - self.callback(x) + if self.callbackz: + self.callback(x, z) + else: + self.callback(x) # check if any callback has raised a stop flag stop = _callback_stop(self.callbacks) if stop: @@ -1900,9 +1946,10 @@ def solve( # type: ignore[override] x0: NDArray, tau: float | NDArray, z0: NDArray | None = None, - niter: int = 10, gfirst: bool = True, + niter: int = 10, tol: float | None = None, + callbackz: bool = False, show: bool = False, itershow: tuple[int, int, int] = (10, 10, 10), ) -> tuple[NDArray, NDArray, int, NDArray]: @@ -1925,14 +1972,16 @@ def solve( # type: ignore[override] strategy) z0 : :obj:`numpy.ndarray`, optional Initial z vector (not required when ``gfirst=True``) - niter : :obj:`int` - Number of iterations of iterative scheme gfirst : :obj:`bool`, optional Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) + niter : :obj:`int`, optional + Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached + callbackz : :obj:`bool`, optional + Modify callback signature to (``callback(x, z)``) when ``callbackz=True`` show : :obj:`bool`, optional Display logs itershow : :obj:`tuple`, optional @@ -1944,7 +1993,7 @@ def solve( # type: ignore[override] ------- x : :obj:`numpy.ndarray` Estimated model - y : :obj:`numpy.ndarray` + z : :obj:`numpy.ndarray` Additional estimated model iiter : :obj:`int` Number of executed iterations @@ -1958,9 +2007,10 @@ def solve( # type: ignore[override] x0=x0, tau=tau, z0=z0, - niter=niter, gfirst=gfirst, + niter=niter, tol=tol, + callbackz=callbackz, show=show, ) @@ -2034,7 +2084,7 @@ def _print_setup(self, xcomplex: bool = False) -> None: f"Proximal operator (f): {type(self.proxf).__name__}\n" f"Proximal operator (g): {type(self.proxg).__name__}\n" ) - strpar1 = f"tau = {self.tau:6e}\tniter = {self.niter}" + strpar1 = f"tau = {float(self.tau):6e}\tniter = {self.niter}" strpar2 = f"gfirst = {self.gfirst}\t\ttol = {self.tol}" print(strpar) print(strpar1) @@ -2069,8 +2119,8 @@ def setup( # type: ignore[override] x0: NDArray, tau: float, z0: NDArray | None = None, - niter: int = 10, gfirst: bool = False, + niter: int = 10, tol: float | None = None, callbackz: bool = False, show: bool = False, @@ -2091,11 +2141,11 @@ def setup( # type: ignore[override] the Lipschitz constant of :math:`\nabla f`. z0 : :obj:`numpy.ndarray`, optional Initial z vector (not required when ``gfirst=True``) - niter : :obj:`int` - Number of iterations of iterative scheme gfirst : :obj:`bool`, optional Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) + niter : :obj:`int`, optional + Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -2143,7 +2193,10 @@ def setup( # type: ignore[override] return x, z def step( - self, x: NDArray, z: NDArray, show: bool = False + self, + x: NDArray, + z: NDArray, + show: bool = False, ) -> tuple[NDArray, NDArray]: r"""Run one step of solver @@ -2263,8 +2316,8 @@ def solve( # type: ignore[override] x0: NDArray, tau: float, z0: NDArray | None = None, - niter: int = 10, gfirst: bool = False, + niter: int = 10, tol: float | None = None, callbackz: bool = False, show: bool = False, @@ -2286,11 +2339,11 @@ def solve( # type: ignore[override] the Lipschitz constant of :math:`\nabla f`. z0 : :obj:`numpy.ndarray`, optional Initial z vector (not required when ``gfirst=True``) - niter : :obj:`int` - Number of iterations of iterative scheme gfirst : :obj:`bool`, optional Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) + niter : :obj:`int`, optional + Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -2321,8 +2374,8 @@ def solve( # type: ignore[override] x0=x0, tau=tau, z0=z0, - niter=niter, gfirst=gfirst, + niter=niter, tol=tol, callbackz=callbackz, show=show, @@ -2418,8 +2471,8 @@ def setup( # type: ignore[override] x0: NDArray, tau: float, z0: NDArray | None = None, - niter: int = 10, gfirst: bool = False, + niter: int = 10, tol: float | None = None, callbackz: bool = False, show: bool = False, @@ -2443,11 +2496,11 @@ def setup( # type: ignore[override] to guarantees convergence: :math:`\tau \in (0, 1/\lambda_{max}(\mathbf{A}^H\mathbf{A})]`. z0 : :obj:`numpy.ndarray` Initial auxiliary vector. If ``None``, initialized to ``A @ x0``. - niter : :obj:`int`, optional - Number of iterations of iterative scheme gfirst : :obj:`bool`, optional Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) + niter : :obj:`int`, optional + Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -2474,8 +2527,8 @@ def setup( # type: ignore[override] self.b = b self.A = A self.tau = tau - self.niter = niter self.gfirst = gfirst + self.niter = niter self.tol = tol self.callbackz = callbackz @@ -2665,8 +2718,8 @@ def solve( # type: ignore[override] x0: NDArray, tau: float, z0: NDArray | None = None, - niter: int = 10, gfirst: bool = False, + niter: int = 10, tol: float | None = None, callbackz: bool = False, show: bool = False, @@ -2693,11 +2746,11 @@ def solve( # type: ignore[override] the Lipschitz constant of :math:`\nabla f`. z0 : :obj:`numpy.ndarray`, optional Initial z vector (not required when ``gfirst=True``) - niter : :obj:`int` - Number of iterations of iterative scheme gfirst : :obj:`bool`, optional Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) + niter : :obj:`int`, optional + Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -2733,8 +2786,8 @@ def solve( # type: ignore[override] x0=x0, tau=tau, z0=z0, - niter=niter, gfirst=gfirst, + niter=niter, tol=tol, callbackz=callbackz, show=show, @@ -3314,7 +3367,7 @@ def step( if self.tol is not None: self.pfgold = self.pfg pf = self.proxf(x) - pg = self.proxg(self.Ax) + pg = self.proxg(x) self.pfg = pf + pg if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: self.tolbreak = True @@ -3553,8 +3606,8 @@ def setup( # type: ignore[override] x0: NDArray, tau: float, eta: float = 1.0, - niter: int = 10, gfirst: bool = True, + niter: int = 10, tol: float | None = None, callbacky: bool = False, show: bool = False, @@ -3573,11 +3626,11 @@ def setup( # type: ignore[override] Positive scalar weight eta : :obj:`float`, optional Relaxation parameter (must be between 0 and 2, 0 excluded). - niter : :obj:`int`, optional - Number of iterations of iterative scheme gfirst : :obj:`bool`, optional Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) + niter : :obj:`int`, optional + Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -3599,8 +3652,8 @@ def setup( # type: ignore[override] self.proxg = proxg self.tau = tau self.eta = eta - self.niter = niter self.gfirst = gfirst + self.niter = niter self.tol = tol self.callbacky = callbacky @@ -3622,7 +3675,10 @@ def setup( # type: ignore[override] return x, y def step( - self, x: NDArray, y: NDArray, show: bool = False + self, + x: NDArray, + y: NDArray, + show: bool = False, ) -> tuple[NDArray, NDArray]: r"""Run one step of solver @@ -3741,8 +3797,8 @@ def solve( # type: ignore[override] x0: NDArray, tau: float, eta: float = 1.0, - niter: int = 10, gfirst: bool = True, + niter: int = 10, tol: float | None = None, callbacky: bool = False, show: bool = False, @@ -3762,11 +3818,11 @@ def solve( # type: ignore[override] Positive scalar weight eta : :obj:`float`, optional Relaxation parameter (must be between 0 and 2, 0 excluded). - niter : :obj:`int`, optional - Number of iterations of iterative scheme gfirst : :obj:`bool`, optional Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) + niter : :obj:`int`, optional + Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -3798,8 +3854,8 @@ def solve( # type: ignore[override] x0=x0, tau=tau, eta=eta, - niter=niter, gfirst=gfirst, + niter=niter, tol=tol, callbacky=callbacky, show=show, @@ -4009,6 +4065,10 @@ def step( Updated additional model vector """ + # store current solution prior to update + # for tolerance check + self.xold = x + # proximal steps p = self.ncp.stack( [self.proxfs[i].prox(y[i], self.tau / self.w[i]) for i in range(self.nprox)] @@ -4306,7 +4366,11 @@ def setup( # type: ignore[override] return x, x_bar, y def step( - self, x: NDArray, x_bar: NDArray, y: NDArray, show: bool = False + self, + x: NDArray, + x_bar: NDArray, + y: NDArray, + show: bool = False, ) -> tuple[NDArray, NDArray, NDArray]: r"""Run one step of solver @@ -4334,6 +4398,10 @@ def step( Updated additional model vector """ + # store current solution prior to update + # for tolerance check + self.xold = x + # proximal steps x = self.ncp.stack( [self.proxfs[i].prox(x_bar - y[i], self.tau) for i in range(self.nprox)] From 355d62686d84dff2093dbd42c5798537d426c58e Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 26 Jun 2026 10:52:31 +0100 Subject: [PATCH 30/73] feat: switched to calling class-based solvers in functional api --- pyproximal/optimization/primal.py | 1769 +++++++++-------------------- 1 file changed, 544 insertions(+), 1225 deletions(-) diff --git a/pyproximal/optimization/primal.py b/pyproximal/optimization/primal.py index 12ad1e41..342bfc7f 100644 --- a/pyproximal/optimization/primal.py +++ b/pyproximal/optimization/primal.py @@ -1,116 +1,61 @@ -import time +__all__ = [ + "ProximalPoint", + "ProximalGradient", + "AcceleratedProximalGradient", + "AndersonProximalGradient", + "GeneralizedProximalGradient", + "HQS", + "ADMM", + "ADMML2", + "LinearizedADMM", + "TwIST", + "DouglasRachfordSplitting", + "PPXA", + "ConsensusADMM", +] + import warnings from collections.abc import Callable -from math import sqrt -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any -import numpy as np -from pylops.optimization.leastsquares import regularized_inversion -from pylops.utils.backend import get_array_module, to_numpy +from pylops.optimization.callback import CostNanInfCallback, CostToInitialCallback from pylops.utils.typing import NDArray -from pyproximal.proximal import L2 -from pyproximal.ProxOperator import ProxOperator -from pyproximal.utils.bilinear import BilinearOperator +from pyproximal.optimization.cls_primal import ADMM as cADMM +from pyproximal.optimization.cls_primal import ADMML2 as cADMML2 +from pyproximal.optimization.cls_primal import HQS as cHQS +from pyproximal.optimization.cls_primal import PPXA as cPPXA +from pyproximal.optimization.cls_primal import ( + AndersonProximalGradient as cAndersonProximalGradient, +) +from pyproximal.optimization.cls_primal import ConsensusADMM as cConsensusADMM +from pyproximal.optimization.cls_primal import ( + DouglasRachfordSplitting as cDouglasRachfordSplitting, +) +from pyproximal.optimization.cls_primal import ( + GeneralizedProximalGradient as cGeneralizedProximalGradient, +) +from pyproximal.optimization.cls_primal import LinearizedADMM as cLinearizedADMM +from pyproximal.optimization.cls_primal import ProximalGradient as cProximalGradient +from pyproximal.optimization.cls_primal import ProximalPoint as cProximalPoint +from pyproximal.optimization.cls_primal import TwIST as cTwIST if TYPE_CHECKING: from pylops.linearoperator import LinearOperator - -def _backtracking( - x: NDArray, - tau: float, - proxf: ProxOperator, - proxg: ProxOperator, - epsg: float, - beta: float = 0.5, - niterback: int = 10, -) -> tuple[NDArray, float]: - r"""Backtracking - - Line-search algorithm for finding step sizes in proximal algorithms when - the Lipschitz constant of the operator is unknown (or expensive to - estimate). - - """ - - def ftilde(x: NDArray, y: NDArray, f: ProxOperator, tau: float) -> float: - xy = x - y - return float( - f(y) + np.dot(f.grad(y), xy) + (1.0 / (2.0 * tau)) * np.linalg.norm(xy) ** 2 - ) - - iiterback = 0 - while iiterback < niterback: - z = proxg.prox(x - tau * proxf.grad(x), epsg * tau) - ft = ftilde(z, x, proxf, tau) - if proxf(z) <= ft: - break - tau *= beta - iiterback += 1 - return z, tau - - -def _x0z0_init( - x0: NDArray | None, - z0: NDArray | None, - Op: Optional["LinearOperator"] = None, - z0name: str | None = "z0", - Opname: str | None = "Op", -) -> tuple[NDArray, NDArray]: - r"""Initialize x0 and z0 - - Initialize x0 and z0 using the following convention. - - For ``Op=None``: - - if both are provided, they are simply returned; - - if only one is provided (the other is ``None``), the one provided - is copied to the other one. - - For ``Op!=None``, ``x0`` must be provided, and: - - if both are provided, they are simply returned; - - if ``z0`` is not provided, set to ``Op @ x0``. - - Parameters - ---------- - x0 : :obj:`numpy.ndarray` - Initial vector - z0 : :obj:`numpy.ndarray` - Initial auxiliary vector - Op : :obj:`pylops.LinearOperator`, optional - Linear Operator to apply to ``x0`` - z0name : :obj:`str`, optional - Name to display in error message instead of ``z0`` - Opname : :obj:`str`, optional - Name to display in error message instead of ``Op`` - - """ - if x0 is None and z0 is None: - msg = f"Both x0 or {z0name} are None, provide either of them or both" - raise ValueError(msg) - - if Op is None: - if x0 is None: - x0 = z0.copy() # type: ignore[union-attr] - elif z0 is None: - z0 = x0.copy() - else: - if x0 is None: - msg = f"x0 must be provided when {Opname} is also provided" - raise ValueError(msg) - elif z0 is None: - z0 = Op @ x0 - return x0, z0 + from pyproximal.ProxOperator import ProxOperator def ProximalPoint( - prox: ProxOperator, + prox: "ProxOperator", x0: NDArray, tau: float, niter: int = 10, tol: float | None = None, + rtol: float | None = None, callback: Callable[[NDArray], None] | None = None, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), ) -> NDArray: r"""Proximal point algorithm @@ -135,12 +80,23 @@ def ProximalPoint( Number of iterations of iterative scheme tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If - ``tol=None``, run until ``niter`` is reached + ``tol=None``, run until ``niter`` is reached or the other tolerance + criterion is met + rtol : :obj:`float`, optional + Relative tolerance on objective function wrt initial value. Stops + the solver when the ratio of the current objective function to the + initial objective function is below this value. If ``rtol=None``, + run until ``niter`` is reached or the other tolerance criterion is + met callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector show : :obj:`bool`, optional Display iterations log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. Returns ------- @@ -149,66 +105,35 @@ def ProximalPoint( Notes ----- - The Proximal point algorithm can be expressed by the following recursion: - - .. math:: - - \mathbf{x}^{k+1} = \prox_{\tau f}(\mathbf{x}^k) + See :class:`pyproximal.optimization.cls_primal.ProximalPoint` """ - if show: - tstart = time.time() - print( - "Proximal point algorithm\n" - "---------------------------------------------------------\n" - "Proximal operator: %s\n" - "tau = %10e\tniter = %d\ttol = %s\n" % (type(prox), tau, niter, str(tol)) - ) - head = " Itn x[0] f" - print(head) - - # initialize model - x = x0.copy() - pf = np.inf - tolbreak = False - - # iterate - for iiter in range(niter): - x = prox.prox(x, tau) - - # run callback - if callback is not None: - callback(x) - - # tolerance check: break iterations if overall - # objective does not decrease below tolerance - if tol is not None: - pfold = pf - pf = prox(x) - if np.abs(1.0 - pf / pfold) < tol: - tolbreak = True - - # show iteration logger - if show: - if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: - if tol is None: - pf = prox(x) - msg = "%6g %12.5e %10.3e" % (iiter + 1, x[0], pf) - print(msg) - - # break if tolerance condition is met - if tolbreak: - break - - if show: - print("\nTotal time (s) = %.2f" % (time.time() - tstart)) - print("---------------------------------------------------------\n") + callbacks = [] + if tol is not None or rtol is not None: + callbacks.append(CostNanInfCallback()) + if rtol is not None: + callbacks.append(CostToInitialCallback(rtol)) + + proxpsolve = cProximalPoint( + callbacks=callbacks if len(callbacks) > 0 else None, + ) + if callback is not None: + proxpsolve.callback = callback + x, _, _ = proxpsolve.solve( + prox=prox, + x0=x0, + tau=tau, + niter=niter, + tol=tol or (0.0 if rtol else None), + show=show, + itershow=itershow, + ) return x def ProximalGradient( - proxf: ProxOperator, - proxg: ProxOperator, + proxf: "ProxOperator", + proxg: "ProxOperator", x0: NDArray, epsg: float | NDArray = 1.0, tau: float | None = None, @@ -219,8 +144,10 @@ def ProximalGradient( niterback: int = 100, acceleration: str | None = None, tol: float | None = None, + rtol: float | None = None, callback: Callable[[NDArray], None] | None = None, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), ) -> NDArray: r"""Proximal gradient (optionally accelerated) @@ -268,12 +195,23 @@ def ProximalGradient( Acceleration (``None``, ``vandenberghe`` or ``fista``) tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If - ``tol=None``, run until ``niter`` is reached + ``tol=None``, run until ``niter`` is reached or the other tolerance + criterion is met + rtol : :obj:`float`, optional + Relative tolerance on objective function wrt initial value. Stops + the solver when the ratio of the current objective function to the + initial objective function is below this value. If ``rtol=None``, + run until ``niter`` is reached or the other tolerance criterion is + met callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector show : :obj:`bool`, optional Display iterations log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. Returns ------- @@ -282,177 +220,42 @@ def ProximalGradient( Notes ----- - The Proximal gradient algorithm can be expressed by the following recursion: - - .. math:: - - \mathbf{x}^{k+1} = \mathbf{y}^k + \eta (\prox_{\tau^k \epsilon g}(\mathbf{y}^k - - \tau^k \nabla f(\mathbf{y}^k)) - \mathbf{y}^k) \\ - \mathbf{y}^{k+1} = \mathbf{x}^k + \omega^k - (\mathbf{x}^k - \mathbf{x}^{k-1}) - - where at each iteration :math:`\tau^k` can be estimated by back-tracking - as follows: - - .. math:: - - \begin{aligned} - &\tau = \tau^{k-1} &\\ - &repeat \; \mathbf{z} = \prox_{\tau \epsilon g}(\mathbf{x}^k - - \tau \nabla f(\mathbf{x}^k)), \tau = \beta \tau \quad if \; - f(\mathbf{z}) \leq \tilde{f}_\tau(\mathbf{z}, \mathbf{x}^k) \\ - &\tau^k = \tau, \quad \mathbf{x}^{k+1} = \mathbf{z} &\\ - \end{aligned} - - where :math:`\tilde{f}_\tau(\mathbf{x}, \mathbf{y}) = f(\mathbf{y}) + - \nabla f(\mathbf{y})^T (\mathbf{x} - \mathbf{y}) + - 1/(2\tau)||\mathbf{x} - \mathbf{y}||_2^2`. - - Different accelerations are provided: - - - ``acceleration=None``: :math:`\omega^k = 0`; - - ``acceleration=vandenberghe`` [1]_: :math:`\omega^k = k / (k + 3)` for ` - - ``acceleration=fista``: :math:`\omega^k = (t_{k-1}-1)/t_k` where - :math:`t_k = (1 + \sqrt{1+4t_{k-1}^{2}}) / 2` [2]_ - - .. [1] Vandenberghe, L., "Fast proximal gradient methods", 2010. - .. [2] Beck, A., and Teboulle, M. "A Fast Iterative Shrinkage-Thresholding - Algorithm for Linear Inverse Problems", SIAM Journal on - Imaging Sciences, vol. 2, pp. 183-202. 2009. + See :class:`pyproximal.optimization.cls_primal.ProximalGradient` """ - # check if epgs is a vector - epsg = np.asarray(epsg, dtype=float) - if epsg.size == 1: - epsg = epsg * np.ones(niter) - epsg_print = str(epsg[0]) - else: - epsg_print = "Multi" - - if acceleration not in [None, "None", "vandenberghe", "fista"]: - msg = "Acceleration should be None, vandenberghe or fista" - raise NotImplementedError(msg) - - if show: - tstart = time.time() - print( - "Accelerated Proximal Gradient\n" - "---------------------------------------------------------\n" - "Proximal operator (f): %s\n" - "Proximal operator (g): %s\n" - "tau = %s\tbacktrack = %s\tbeta = %10e\n" - "epsg = %s\tniter = %d\ttol = %s\n" - "" - "niterback = %d\tacceleration = %s\n" - % ( - type(proxf), - type(proxg), - str(tau), - backtracking, - beta, - epsg_print, - niter, - str(tol), - niterback, - acceleration, - ) - ) - head = " Itn x[0] f g J=f+eps*g tau" - print(head) - - if tau is None: - backtracking = True - tau = 1.0 - - # initialize model - t = 1.0 - x = x0.copy() - y = x.copy() - pfg = np.inf - tolbreak = False - - # iterate - for iiter in range(niter): - xold = x.copy() - - # proximal step - if not backtracking: - if eta == 1.0: - x = proxg.prox(y - tau * proxf.grad(y), epsg[iiter] * tau) - else: - x = x + eta * ( - proxg.prox(x - tau * proxf.grad(x), epsg[iiter] * tau) - x - ) - else: - x, tau = _backtracking( - y, tau, proxf, proxg, epsg[iiter], beta=beta, niterback=niterback - ) - if eta != 1.0: - x = x + eta * ( - proxg.prox(x - tau * proxf.grad(x), epsg[iiter] * tau) - x - ) - - # update internal parameters for bilinear operator - if isinstance(proxf, BilinearOperator): - proxf.updatexy(x) - - # update y - if acceleration == "vandenberghe": - omega = iiter / (iiter + 3) - elif acceleration == "fista": - told = t - t = (1.0 + np.sqrt(1.0 + 4.0 * t**2)) / 2.0 - omega = (told - 1.0) / t - else: - omega = 0 - y = x + omega * (x - xold) - - # run callback - if callback is not None: - callback(x) - - # tolerance check: break iterations if overall - # objective does not decrease below tolerance - if tol is not None: - pfgold = pfg - pf, pg = proxf(x), proxg(x) - pfg = pf + np.sum(epsg[iiter] * pg) - if np.abs(1.0 - pfg / pfgold) < tol: - tolbreak = True - - # show iteration logger - if show: - if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: - if tol is None: - pf, pg = proxf(x), proxg(x) - pfg = pf + np.sum(epsg[iiter] * pg) - msg = "%6g %12.5e %10.3e %10.3e %10.3e %10.3e" % ( - iiter + 1, - ( - np.real(to_numpy(x[0])) - if x.ndim == 1 - else np.real(to_numpy(x[0, 0])) - ), - pf, - pg, - pfg, - tau, - ) - print(msg) - - # break if tolerance condition is met - if tolbreak: - break - - if show: - print("\nTotal time (s) = %.2f" % (time.time() - tstart)) - print("---------------------------------------------------------\n") + callbacks = [] + if tol is not None or rtol is not None: + callbacks.append(CostNanInfCallback()) + if rtol is not None: + callbacks.append(CostToInitialCallback(rtol)) + + proxgsolve = cProximalGradient( + callbacks=callbacks if len(callbacks) > 0 else None, + ) + if callback is not None: + proxgsolve.callback = callback + x, _, _, _ = proxgsolve.solve( + proxf=proxf, + proxg=proxg, + x0=x0, + epsg=epsg, + tau=tau, + backtracking=backtracking, + beta=beta, + eta=eta, + acceleration=acceleration, + niterback=niterback, + niter=niter, + tol=tol or (0.0 if rtol else None), + show=show, + itershow=itershow, + ) return x def AcceleratedProximalGradient( - proxf: ProxOperator, - proxg: ProxOperator, + proxf: "ProxOperator", + proxg: "ProxOperator", x0: NDArray, tau: float | None = None, beta: float = 0.5, @@ -461,8 +264,10 @@ def AcceleratedProximalGradient( niterback: int = 100, acceleration: str = "vandenberghe", tol: float | None = None, + rtol: float | None = None, callback: Callable[[NDArray], None] | None = None, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), ) -> NDArray: r"""Accelerated Proximal gradient @@ -490,14 +295,16 @@ def AcceleratedProximalGradient( niterback=niterback, acceleration=acceleration, tol=tol, + rtol=rtol, callback=callback, show=show, + itershow=itershow, ) def AndersonProximalGradient( - proxf: ProxOperator, - proxg: ProxOperator, + proxf: "ProxOperator", + proxg: "ProxOperator", x0: NDArray, epsg: float | NDArray = 1.0, tau: float | NDArray = 1.0, @@ -506,8 +313,10 @@ def AndersonProximalGradient( epsr: float = 1e-10, safeguard: bool = False, tol: float | None = None, + rtol: float | None = None, callback: Callable[[NDArray], None] | None = None, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), ) -> NDArray: r"""Proximal gradient with Anderson acceleration @@ -549,11 +358,21 @@ def AndersonProximalGradient( tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached + rtol : :obj:`float`, optional + Relative tolerance on objective function wrt initial value. Stops + the solver when the ratio of the current objective function to the + initial objective function is below this value. If ``rtol=None``, + run until ``niter`` is reached or the other tolerance criterion is + met callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector show : :obj:`bool`, optional Display iterations log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. Returns ------- @@ -562,170 +381,40 @@ def AndersonProximalGradient( Notes ----- - The Proximal gradient algorithm with Anderson acceleration can be expressed by the - following recursion [1]_: - - .. math:: - m_k = min(m, k)\\ - \mathbf{g}^{k} = \mathbf{x}^{k} - \tau^k \nabla f(\mathbf{x}^k)\\ - \mathbf{r}^{k} = \mathbf{g}^{k} - \mathbf{g}^{k}\\ - \mathbf{G}^{k} = [\mathbf{g}^{k},..., \mathbf{g}^{k-m_k}]\\ - \mathbf{R}^{k} = [\mathbf{r}^{k},..., \mathbf{r}^{k-m_k}]\\ - \alpha_k = (\mathbf{R}^{kT} \mathbf{R}^{k})^{-1} \mathbf{1} / \mathbf{1}^T - (\mathbf{R}^{kT} \mathbf{R}^{k})^{-1} \mathbf{1}\\ - \mathbf{y}^{k+1} = \mathbf{G}^{k} \alpha_k\\ - \mathbf{x}^{k+1} = \prox_{\tau^{k+1} g}(\mathbf{y}^{k+1}) - - where :math:`m` equals ``nhistory``, :math:`k=1,2,...,n_{iter}`, :math:`\mathbf{y}^{0}=\mathbf{x}^{0}`, - :math:`\mathbf{y}^{1}=\mathbf{x}^{0} - \tau^0 \nabla f(\mathbf{x}^0)`, - :math:`\mathbf{x}^{1}=\prox_{\tau^k g}(\mathbf{y}^{1})`, and - :math:`\mathbf{g}^{0}=\mathbf{y}^{1}`. - - Refer to [1]_ for the guarded version of the algorithm (when ``safeguard=True``). - - .. [1] Mai, V., and Johansson, M. "Anderson Acceleration of Proximal Gradient - Methods", 2020. + See :class:`pyproximal.optimization.cls_primal.AndersonProximalGradient` """ - # check if epgs is a vector - epsg = np.asarray(epsg, dtype=float) - if epsg.size == 1: - epsg = epsg * np.ones(niter) - epsg_print = str(epsg[0]) - else: - epsg_print = "Multi" - - if show: - tstart = time.time() - print( - "Proximal Gradient with Anderson Acceleration \n" - "---------------------------------------------------------\n" - "Proximal operator (f): %s\n" - "Proximal operator (g): %s\n" - "tau = %s\t\tepsg = %s\tniter = %d\n" - "nhist = %d\tepsr = %s\n" - "guard = %s\ttol = %s\n" - % ( - type(proxf), - type(proxg), - str(tau), - epsg_print, - niter, - nhistory, - str(epsr), - str(safeguard), - str(tol), - ) - ) - head = " Itn x[0] f g J=f+eps*g tau" - print(head) - - # initialize model - y = x0 - tau * proxf.grad(x0) - x = proxg.prox(y, epsg[0] * tau) - g = y.copy() - r = g - x0 - R, G = ( - [ - g, - ], - [ - r, - ], + callbacks = [] + if tol is not None or rtol is not None: + callbacks.append(CostNanInfCallback()) + if rtol is not None: + callbacks.append(CostToInitialCallback(rtol)) + + aproxgsolve = cAndersonProximalGradient( + callbacks=callbacks if len(callbacks) > 0 else None, + ) + if callback is not None: + aproxgsolve.callback = callback + x, _, _, _ = aproxgsolve.solve( + proxf=proxf, + proxg=proxg, + x0=x0, + epsg=epsg, + tau=tau, + niter=niter, + nhistory=nhistory, + epsr=epsr, + safeguard=safeguard, + tol=tol or (0.0 if rtol else None), + show=show, + itershow=itershow, ) - pf = proxf(x) - pfg = np.inf - tolbreak = False - - # iterate - for iiter in range(niter): - # update fix point - g = x - tau * proxf.grad(x) - r = g - y - - # update history vectors - R.insert(0, r) - G.insert(0, g) - if iiter >= nhistory - 1: - R.pop(-1) - G.pop(-1) - - # solve for alpha coefficients - Rstack = np.vstack(R) - Rinv = np.linalg.pinv(Rstack @ Rstack.T + epsr * np.linalg.norm(Rstack) ** 2) - ones = np.ones(min(nhistory, iiter + 2)) - Rinvones = Rinv @ ones - alpha = Rinvones / (ones[None] @ Rinvones) - - if not safeguard: - # update auxiliary variable - y = np.vstack(G).T @ alpha - - # update main variable - x = proxg.prox(y, epsg[iiter] * tau) - - else: - # update auxiliary variable - ytest = np.vstack(G).T @ alpha - - # update main variable - xtest = proxg.prox(ytest, epsg[iiter] * tau) - - # check if function is decreased, otherwise do basic PG step - pfold, pf = pf, proxf(xtest) - if pf <= pfold - tau * np.linalg.norm(proxf.grad(x)) ** 2 / 2: - y = ytest - x = xtest - else: - x = proxg.prox(g, epsg[iiter] * tau) - y = g - - # run callback - if callback is not None: - callback(x) - - # tolerance check: break iterations if overall - # objective does not decrease below tolerance - if tol is not None: - pfgold = pfg - pf, pg = proxf(x), proxg(x) - pfg = pf + np.sum(epsg[iiter] * pg) - if np.abs(1.0 - pfg / pfgold) < tol: - tolbreak = True - - # show iteration logger - if show: - if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: - if tol is None: - pf, pg = proxf(x), proxg(x) - pfg = pf + np.sum(epsg[iiter] * pg) - msg = "%6g %12.5e %10.3e %10.3e %10.3e %10.3e" % ( - iiter + 1, - ( - np.real(to_numpy(x[0])) - if x.ndim == 1 - else np.real(to_numpy(x[0, 0])) - ), - pf, - pg, - pfg, - tau, - ) - print(msg) - - # break if tolerance condition is met - if tolbreak: - break - - if show: - print("\nTotal time (s) = %.2f" % (time.time() - tstart)) - print("---------------------------------------------------------\n") return x def GeneralizedProximalGradient( - proxfs: list[ProxOperator], - proxgs: list[ProxOperator], + proxfs: list["ProxOperator"], + proxgs: list["ProxOperator"], x0: NDArray, tau: float | None, epsg: float | NDArray = 1.0, @@ -733,8 +422,11 @@ def GeneralizedProximalGradient( eta: float = 1.0, niter: int = 10, acceleration: str | None = None, + tol: float | None = None, + rtol: float | None = None, callback: Callable[[NDArray], None] | None = None, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), ) -> NDArray: r"""Generalized Proximal gradient @@ -773,11 +465,25 @@ def GeneralizedProximalGradient( Number of iterations of iterative scheme acceleration: :obj:`str`, optional Acceleration (``None``, ``vandenberghe`` or ``fista``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached or the other tolerance + criterion is met + rtol : :obj:`float`, optional + Relative tolerance on objective function wrt initial value. Stops + the solver when the ratio of the current objective function to the + initial objective function is below this value. If ``rtol=None``, + run until ``niter`` is reached or the other tolerance criterion is + met callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector show : :obj:`bool`, optional Display iterations log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. Returns ------- @@ -786,134 +492,51 @@ def GeneralizedProximalGradient( Notes ----- - The Generalized Proximal gradient algorithm can be expressed by the - following recursion [1]_: - - .. math:: - \text{for } j=1,\cdots,n, \\ - ~~~~\mathbf z_j^{k+1} = \mathbf z_j^{k} + \eta - \left[prox_{\frac{\tau^k \epsilon_j}{w_j} g_j}\left(2 \mathbf{x}^{k} - \mathbf{z}_j^{k} - - \tau^k \sum_{i=1}^n \nabla f_i(\mathbf{x}^{k})\right) - \mathbf{x}^{k} \right] \\ - \mathbf{x}^{k+1} = \sum_{j=1}^n w_j \mathbf z_j^{k+1} \\ - - where :math:`\sum_{j=1}^n w_j=1`. In the current implementation, :math:`w_j=1/n` when - not provided. - - .. [1] Raguet, H., Fadili, J. and PeyrΓ©, G. "Generalized Forward-Backward Splitting", - arXiv, 2012. + See :class:`pyproximal.optimization.cls_primal.GeneralizedProximalGradient` """ - # check if weights sum to 1 - if weights is None: - weights = np.ones(len(proxgs)) / len(proxgs) - if len(weights) != len(proxgs) or np.sum(weights) != 1.0: - msg = f"omega={weights} must be an array of size {len(proxgs)} summing to 1" - raise ValueError(msg) - - # check if epgs is a vector - epsg = np.asarray(epsg, dtype=float) - if epsg.size == 1: - epsg_print = str(epsg) - epsg = epsg * np.ones(len(proxgs)) - else: - epsg_print = "Multi" - - if acceleration not in [None, "None", "vandenberghe", "fista"]: - msg = "Acceleration should be None, vandenberghe or fista" - raise NotImplementedError(msg) - if show: - tstart = time.time() - print( - "Generalized Proximal Gradient\n" - "---------------------------------------------------------\n" - "Proximal operators (f): %s\n" - "Proximal operators (g): %s\n" - "tau = %10e\nepsg = %s\tniter = %d\n" - % ( - [type(proxf) for proxf in proxfs], - [type(proxg) for proxg in proxgs], - 0 if tau is None else tau, - epsg_print, - niter, - ) - ) - head = " Itn x[0] f g J=f+g" - print(head) - - if tau is None: - tau = 1.0 - - # initialize model - t = 1.0 - x = x0.copy() - y = x.copy() - zs = [x.copy() for _ in range(len(proxgs))] - - # iterate - for iiter in range(niter): - xold = x.copy() - - # gradient - grad = np.zeros_like(x) - for _, proxf in enumerate(proxfs): - grad += proxf.grad(x) - - # proximal step - x = np.zeros_like(x) - for i, proxg in enumerate(proxgs): - ztmp = 2 * y - zs[i] - tau * grad - ztmp = proxg.prox(ztmp, tau * epsg[i] / weights[i]) - zs[i] += eta * (ztmp - y) - x += weights[i] * zs[i] - - # update y - if acceleration == "vandenberghe": - omega = iiter / (iiter + 3) - elif acceleration == "fista": - told = t - t = (1.0 + np.sqrt(1.0 + 4.0 * t**2)) / 2.0 - omega = (told - 1.0) / t - else: - omega = 0 - y = x + omega * (x - xold) - - # run callback - if callback is not None: - callback(x) - - if show: - if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: - pf: float = np.sum([proxf(x) for proxf in proxfs]) - pg: float = np.sum( - [eg * proxg(x) for proxg, eg in zip(proxgs, epsg, strict=True)] - ) - msg = "%6g %12.5e %10.3e %10.3e %10.3e" % ( - iiter + 1, - np.real(to_numpy(x[0])) - if x.ndim == 1 - else np.real(to_numpy(x[0, 0])), - pf, - pg, - pf + pg, - ) - print(msg) - if show: - print("\nTotal time (s) = %.2f" % (time.time() - tstart)) - print("---------------------------------------------------------\n") + callbacks = [] + if tol is not None or rtol is not None: + callbacks.append(CostNanInfCallback()) + if rtol is not None: + callbacks.append(CostToInitialCallback(rtol)) + + proxgsolve = cGeneralizedProximalGradient( + callbacks=callbacks if len(callbacks) > 0 else None, + ) + if callback is not None: + proxgsolve.callback = callback + x, _, _, _ = proxgsolve.solve( + proxfs=proxfs, + proxgs=proxgs, + x0=x0, + epsg=epsg, + weights=weights, + tau=1.0 if tau is None else tau, + eta=eta, + niter=niter, + acceleration=acceleration, + tol=tol or (0.0 if rtol else None), + show=show, + itershow=itershow, + ) return x def HQS( - proxf: ProxOperator, - proxg: ProxOperator, + proxf: "ProxOperator", + proxg: "ProxOperator", x0: NDArray, tau: float | NDArray, niter: int = 10, z0: NDArray | None = None, gfirst: bool = True, + tol: float | None = None, + rtol: float | None = None, callback: Callable[..., None] | None = None, callbackz: bool = False, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), ) -> tuple[NDArray, NDArray]: r"""Half Quadratic splitting @@ -951,6 +574,15 @@ def HQS( gfirst : :obj:`bool`, optional Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + rtol : :obj:`float`, optional + Relative tolerance on objective function wrt initial value. Stops + the solver when the ratio of the current objective function to the + initial objective function is below this value. If ``rtol=None``, + run until ``niter`` is reached or the other tolerance criterion is + met callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector @@ -958,6 +590,10 @@ def HQS( Modify callback signature to (``callback(x, z)``) when ``callbackz=True`` show : :obj:`bool`, optional Display iterations log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. Returns ------- @@ -973,98 +609,50 @@ def HQS( Notes ----- - The HQS algorithm can be expressed by the following recursion [1]_: - - .. math:: - - \mathbf{z}^{k+1} = \prox_{\tau g}(\mathbf{x}^{k}) \\ - \mathbf{x}^{k+1} = \prox_{\tau f}(\mathbf{z}^{k+1}) - - for ``gfirst=False``, or - - .. math:: - - \mathbf{x}^{k+1} = \prox_{\tau f}(\mathbf{z}^{k}) \\ - \mathbf{z}^{k+1} = \prox_{\tau g}(\mathbf{x}^{k+1}) - - for ``gfirst=False``. Note that ``x`` and ``z`` converge to each other, - however if iterations are stopped too early ``x`` is guaranteed to belong to - the domain of ``f`` while ``z`` is guaranteed to belong to the domain of ``g``. - Depending on the problem either of the two may be the best solution. - - .. [1] D., Geman, and C., Yang, "Nonlinear image recovery with halfquadratic - regularization", IEEE Transactions on Image Processing, - 4, 7, pp. 932-946, 1995. + See :class:`pyproximal.optimization.cls_primal.HQS` """ - # initialize variables - x, z = _x0z0_init(x0, z0) - ncp = get_array_module(x) - - # check if tau is a vector - tau = ncp.asarray(tau, dtype=float) - if tau.size == 1: - tau_print = str(tau) - tau = tau * np.ones(niter) - else: - tau_print = "Variable" - - if show: - tstart = time.time() - print( - "HQS\n" - "---------------------------------------------------------\n" - "Proximal operator (f): %s\n" - "Proximal operator (g): %s\n" - "tau = %s\tniter = %d\n" % (type(proxf), type(proxg), tau_print, niter) - ) - head = " Itn x[0] f g J = f + g" - print(head) - - # run iterations - for iiter in range(niter): - if gfirst: - z = proxg.prox(x, tau[iiter]) - x = proxf.prox(z, tau[iiter]) - else: - x = proxf.prox(z, tau[iiter]) - z = proxg.prox(x, tau[iiter]) - - # run callback - if callback is not None: - if callbackz: - callback(x, z) - else: - callback(x) - - if show: - if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: - pf, pg = proxf(x), proxg(x) - msg = "%6g %12.5e %10.3e %10.3e %10.3e" % ( - iiter + 1, - np.real(to_numpy(x[0])), - pf, - pg, - pf + pg, - ) - print(msg) - if show: - print("\nTotal time (s) = %.2f" % (time.time() - tstart)) - print("---------------------------------------------------------\n") + callbacks = [] + if tol is not None or rtol is not None: + callbacks.append(CostNanInfCallback()) + if rtol is not None: + callbacks.append(CostToInitialCallback(rtol)) + + hqssolve = cHQS( + callbacks=callbacks if len(callbacks) > 0 else None, + ) + if callback is not None: + hqssolve.callback = callback + x, z, _, _ = hqssolve.solve( + proxf=proxf, + proxg=proxg, + x0=x0, + tau=tau, + z0=z0, + niter=niter, + gfirst=gfirst, + tol=tol or (0.0 if rtol else None), + callbackz=callbackz, + show=show, + itershow=itershow, + ) return x, z def ADMM( - proxf: ProxOperator, - proxg: ProxOperator, + proxf: "ProxOperator", + proxg: "ProxOperator", x0: NDArray, tau: float, niter: int = 10, z0: NDArray | None = None, gfirst: bool = False, + tol: float | None = None, + rtol: float | None = None, callback: Callable[..., None] | None = None, callbackz: bool = False, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), ) -> tuple[NDArray, NDArray]: r"""Alternating Direction Method of Multipliers @@ -1115,6 +703,15 @@ def ADMM( gfirst : :obj:`bool`, optional Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + rtol : :obj:`float`, optional + Relative tolerance on objective function wrt initial value. Stops + the solver when the ratio of the current objective function to the + initial objective function is below this value. If ``rtol=None``, + run until ``niter`` is reached or the other tolerance criterion is + met callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector @@ -1122,6 +719,10 @@ def ADMM( Modify callback signature to (``callback(x, z)``) when ``callbackz=True`` show : :obj:`bool`, optional Display iterations log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. Returns ------- @@ -1142,77 +743,38 @@ def ADMM( Notes ----- - The ADMM algorithm can be expressed by the following recursion [1]_: - - .. math:: - - \mathbf{x}^{k+1} = \prox_{\tau f}(\mathbf{z}^{k} - \mathbf{u}^{k})\\ - \mathbf{z}^{k+1} = \prox_{\tau g}(\mathbf{x}^{k+1} + \mathbf{u}^{k})\\ - \mathbf{u}^{k+1} = \mathbf{u}^{k} + \mathbf{x}^{k+1} - \mathbf{z}^{k+1} - - Note that ``x`` and ``z`` converge to each other, however if iterations are - stopped too early ``x`` is guaranteed to belong to the domain of ``f`` - while ``z`` is guaranteed to belong to the domain of ``g``. Depending on - the problem either of the two may be the best solution. - - .. [1] S. Boyd, N. Parikh, E. Chu, B. Peleato, and J. Eckstein. 2011. - Distributed optimization and statistical learning via the alternating - direction method of multipliers. Foundations and Trends in Machine - Learning, 3 (1), 1-122. https://doi.org/10.1561/2200000016. + See :class:`pyproximal.optimization.cls_primal.ADMM` """ - # initialize variables - x, z = _x0z0_init(x0, z0) - ncp = get_array_module(x) - u = ncp.zeros_like(x) - - if show: - tstart = time.time() - print( - "ADMM\n" - "---------------------------------------------------------\n" - "Proximal operator (f): %s\n" - "Proximal operator (g): %s\n" - "tau = %10e\tniter = %d\n" % (type(proxf), type(proxg), tau, niter) - ) - head = " Itn x[0] f g J = f + g" - print(head) - - # run iterations - for iiter in range(niter): - if gfirst: - z = proxg.prox(x + u, tau) - x = proxf.prox(z - u, tau) - else: - x = proxf.prox(z - u, tau) - z = proxg.prox(x + u, tau) - u = u + x - z - - # run callback - if callback is not None: - if callbackz: - callback(x, z) - else: - callback(x) - if show: - if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: - pf, pg = proxf(x), proxg(x) - msg = "%6g %12.5e %10.3e %10.3e %10.3e" % ( - iiter + 1, - np.real(to_numpy(x[0])), - pf, - pg, - pf + pg, - ) - print(msg) - if show: - print("\nTotal time (s) = %.2f" % (time.time() - tstart)) - print("---------------------------------------------------------\n") + callbacks = [] + if tol is not None or rtol is not None: + callbacks.append(CostNanInfCallback()) + if rtol is not None: + callbacks.append(CostToInitialCallback(rtol)) + + admmsolve = cADMM( + callbacks=callbacks if len(callbacks) > 0 else None, + ) + if callback is not None: + admmsolve.callback = callback + x, z, _, _ = admmsolve.solve( + proxf=proxf, + proxg=proxg, + x0=x0, + tau=tau, + z0=z0, + gfirst=gfirst, + niter=niter, + tol=tol or (0.0 if rtol else None), + callbackz=callbackz, + show=show, + itershow=itershow, + ) return x, z def ADMML2( - proxg: ProxOperator, + proxg: "ProxOperator", Op: "LinearOperator", b: NDArray, A: "LinearOperator", @@ -1221,8 +783,12 @@ def ADMML2( niter: int = 10, z0: NDArray | None = None, gfirst: bool = False, + tol: float | None = None, + rtol: float | None = None, callback: Callable[[NDArray], None] | None = None, + callbackz: bool = False, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), **kwargs_solver: dict[str, Any], ) -> tuple[NDArray, NDArray]: r"""Alternating Direction Method of Multipliers for L2 misfit term @@ -1260,11 +826,24 @@ def ADMML2( gfirst : :obj:`bool`, optional Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + rtol : :obj:`float`, optional + Relative tolerance on objective function wrt initial value. Stops + the solver when the ratio of the current objective function to the + initial objective function is below this value. If ``rtol=None``, + run until ``niter`` is reached or the other tolerance criterion is + met callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector show : :obj:`bool`, optional Display iterations log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. **kwargs_solver Arbitrary keyword arguments for :py:func:`scipy.sparse.linalg.lsqr` used to solve the x-update @@ -1288,108 +867,53 @@ def ADMML2( Notes ----- - The ADMM algorithm can be expressed by the following recursion: - - .. math:: - - \mathbf{x}^{k+1} = \argmin_{\mathbf{x}} \frac{1}{2}||\mathbf{Op}\mathbf{x} - - \mathbf{b}||_2^2 + \frac{1}{2\tau} ||\mathbf{Ax} - \mathbf{z}^k + \mathbf{u}^k||_2^2\\ - \mathbf{z}^{k+1} = \prox_{\tau g}(\mathbf{Ax}^{k+1} + \mathbf{u}^{k})\\ - \mathbf{u}^{k+1} = \mathbf{u}^{k} + \mathbf{Ax}^{k+1} - \mathbf{z}^{k+1} + See :class:`pyproximal.optimization.cls_primal.ADMML2` """ - # initialize variables - x, z = _x0z0_init(x0, z0, A, Opname="A") - ncp = get_array_module(x) - u = ncp.zeros_like(z) - - if show: - tstart = time.time() - print( - "ADMM\n" - "---------------------------------------------------------\n" - "Proximal operator (g): %s\n" - "tau = %10e\tniter = %d\n" % (type(proxg), tau, niter) - ) - head = " Itn x[0] f g J = f + g" - print(head) - - # run iterations - sqrttau = 1.0 / sqrt(tau) - for iiter in range(niter): - if gfirst: - Ax = A @ x - z = proxg.prox(Ax + u, tau) - - # solve augumented system - x = regularized_inversion( - Op, - b, - [ - A, - ], - x0=x, - dataregs=[ - z - u, - ], - epsRs=[ - sqrttau, - ], - **kwargs_solver, - )[0] - else: - # solve augumented system - x = regularized_inversion( - Op, - b, - [ - A, - ], - x0=x, - dataregs=[ - z - u, - ], - epsRs=[ - sqrttau, - ], - **kwargs_solver, - )[0] - Ax = A @ x - z = proxg.prox(Ax + u, tau) - u = u + Ax - z - - # run callback - if callback is not None: - callback(x) - - if show: - if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: - pf, pg = 0.5 * np.linalg.norm(Op @ x - b) ** 2, proxg(Ax) - msg = "%6g %12.5e %10.3e %10.3e %10.3e" % ( - iiter + 1, - np.real(to_numpy(x[0])), - pf, - pg, - pf + pg, - ) - print(msg) - if show: - print("\nTotal time (s) = %.2f" % (time.time() - tstart)) - print("---------------------------------------------------------\n") + callbacks = [] + if tol is not None or rtol is not None: + callbacks.append(CostNanInfCallback()) + if rtol is not None: + callbacks.append(CostToInitialCallback(rtol)) + + admml2solve = cADMML2( + callbacks=callbacks if len(callbacks) > 0 else None, + ) + if callback is not None: + admml2solve.callback = callback + x, z, _, _ = admml2solve.solve( + proxg=proxg, + Op=Op, + b=b, + A=A, + x0=x0, + tau=tau, + z0=z0, + gfirst=gfirst, + niter=niter, + tol=tol or (0.0 if rtol else None), + callbackz=callbackz, + show=show, + itershow=itershow, + **kwargs_solver, + ) return x, z def LinearizedADMM( - proxf: ProxOperator, - proxg: ProxOperator, + proxf: "ProxOperator", + proxg: "ProxOperator", A: "LinearOperator", x0: NDArray, tau: float, mu: float, niter: int = 10, z0: NDArray | None = None, + tol: float | None = None, + rtol: float | None = None, callback: Callable[[NDArray], None] | None = None, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), ) -> tuple[NDArray, NDArray]: r"""Linearized Alternating Direction Method of Multipliers @@ -1426,11 +950,24 @@ def LinearizedADMM( Number of iterations of iterative scheme z0 : :obj:`numpy.ndarray` Initial auxiliary vector. If ``None``, initialized to ``A @ x0``. + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + rtol : :obj:`float`, optional + Relative tolerance on objective function wrt initial value. Stops + the solver when the ratio of the current objective function to the + initial objective function is below this value. If ``rtol=None``, + run until ``niter`` is reached or the other tolerance criterion is + met callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector show : :obj:`bool`, optional Display iterations log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. Returns ------- @@ -1451,71 +988,38 @@ def LinearizedADMM( Notes ----- - The Linearized-ADMM algorithm can be expressed by the following recursion [1]_: - - .. math:: - - \mathbf{x}^{k+1} = \prox_{\mu f}(\mathbf{x}^{k} - \frac{\mu}{\tau} - \mathbf{A}^H(\mathbf{A} \mathbf{x}^k - \mathbf{z}^k + \mathbf{u}^k))\\ - \mathbf{z}^{k+1} = \prox_{\tau g}(\mathbf{A} \mathbf{x}^{k+1} + - \mathbf{u}^k)\\ - \mathbf{u}^{k+1} = \mathbf{u}^{k} + \mathbf{A}\mathbf{x}^{k+1} - - \mathbf{z}^{k+1} - - .. [1] N., Parikh, "Proximal Algorithms", Foundations and Trends - in Optimization. 2013. + See :class:`pyproximal.optimization.cls_primal.LinearizedADMM` """ - # initialize variables - x, z = _x0z0_init(x0, z0, A, Opname="A") - Ax = A.matvec(x) if z0 is None else z - ncp = get_array_module(x) - u = ncp.zeros_like(z) - - if show: - tstart = time.time() - print( - "Linearized-ADMM\n" - "---------------------------------------------------------\n" - "Proximal operator (f): %s\n" - "Proximal operator (g): %s\n" - "Linear operator (A): %s\n" - "tau = %10e\tmu = %10e\tniter = %d\n" - % (type(proxf), type(proxg), type(A), tau, mu, niter) - ) - head = " Itn x[0] f g J = f + g" - print(head) - - # run iterations - for iiter in range(niter): - x = proxf.prox(x - mu / tau * A.rmatvec(Ax - z + u), mu) - Ax = A.matvec(x) - z = proxg.prox(Ax + u, tau) - u = u + Ax - z - - # run callback - if callback is not None: - callback(x) - - if show: - if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: - pf, pg = proxf(x), proxg(Ax) - msg = "%6g %12.5e %10.3e %10.3e %10.3e" % ( - iiter + 1, - np.real(to_numpy(x[0])), - pf, - pg, - pf + pg, - ) - print(msg) - if show: - print("\nTotal time (s) = %.2f" % (time.time() - tstart)) - print("---------------------------------------------------------\n") + callbacks = [] + if tol is not None or rtol is not None: + callbacks.append(CostNanInfCallback()) + if rtol is not None: + callbacks.append(CostToInitialCallback(rtol)) + + ladmmsolve = cLinearizedADMM( + callbacks=callbacks if len(callbacks) > 0 else None, + ) + if callback is not None: + ladmmsolve.callback = callback + x, z, _, _ = ladmmsolve.solve( + proxf=proxf, + proxg=proxg, + A=A, + x0=x0, + tau=tau, + mu=mu, + z0=z0, + niter=niter, + tol=tol or (0.0 if rtol else None), + show=show, + itershow=itershow, + ) return x, z def TwIST( - proxg: ProxOperator, + proxg: "ProxOperator", A: "LinearOperator", b: NDArray, x0: NDArray, @@ -1523,8 +1027,11 @@ def TwIST( beta: float | None = None, eigs: tuple[float, float] | None = None, niter: int = 10, + tol: float | None = None, + rtol: float | None = None, callback: Callable[[NDArray], None] | None = None, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), returncost: bool = False, ) -> NDArray | tuple[NDArray, NDArray]: r"""Two-step Iterative Shrinkage/Threshold @@ -1561,11 +1068,24 @@ def TwIST( If passed, computes `alpha` and `beta` based on them. niter : :obj:`int`, optional Number of iterations of iterative scheme + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + rtol : :obj:`float`, optional + Relative tolerance on objective function wrt initial value. Stops + the solver when the ratio of the current objective function to the + initial objective function is below this value. If ``rtol=None``, + run until ``niter`` is reached or the other tolerance criterion is + met callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector show : :obj:`bool`, optional Display iterations log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. returncost : :obj:`bool`, optional Return cost function @@ -1573,110 +1093,38 @@ def TwIST( ------- x : :obj:`numpy.ndarray` Inverted model - j : :obj:`numpy.ndarray` + j : :obj:`numpy.ndarray`, optional Cost function Notes ----- - The TwIST algorithm can be expressed by the following recursion: - - .. math:: - - \mathbf{x}^{k+1} = (1-\alpha) \mathbf{x}^{k-1} + - (\alpha-\beta) \mathbf{x}^k + - \beta \prox_{g} (\mathbf{x}^k + \mathbf{A}^H - (\mathbf{b} - \mathbf{A}\mathbf{x}^k)). - - where :math:`\mathbf{x}^{1} = \prox_{g} (\mathbf{x}^0 + \mathbf{A}^T - (\mathbf{b} - \mathbf{A}\mathbf{x}^0))`. - - The optimal weighting parameters :math:`\alpha` and :math:`\beta` are - linked to the smallest and largest eigenvalues of - :math:`\mathbf{A}^H\mathbf{A}` as follows: - - .. math:: - - \alpha = 1 + \rho^2 \\ - \beta =\frac{2 \alpha}{\Lambda_{max} + \lambda_{min}} - - where :math:`\rho=\frac{1-\sqrt{k}}{1+\sqrt{k}}` with - :math:`k=\frac{\lambda_{min}}{\Lambda_{max}}` and - :math:`\Lambda_{max}=max(1, \lambda_{max})`. - - Experimentally, it has been observed that TwIST is robust to the - choice of such parameters. Finally, note that in the case of - :math:`\alpha=1` and :math:`\beta=1`, TwIST is identical to IST. + See :class:`pyproximal.optimization.cls_primal.TwIST` """ - # define proxf as L2 proximal - proxf = L2(Op=A, b=b) - - # find alpha and beta - if alpha is None or beta is None: - if eigs is None: - emin = A.eigs(neigs=1, which="SM") - emax = max([1, A.eigs(neigs=1, which="LM")]) - else: - emax, emin = eigs - k = emin / emax - rho = (1 - sqrt(k)) / (1 + sqrt(k)) - alpha = 1 + rho**2 - beta = 2 * alpha / (emax + emin) - - # compute proximal of g on initial guess (x_1) - xold = x0.copy() - x = proxg.prox(xold - proxf.grad(xold), 1.0) - - if show: - tstart = time.time() - print( - "TwIST\n" - "---------------------------------------------------------\n" - "Proximal operator (g): %s\n" - "Linear operator (A): %s\n" - "alpha = %10e\tbeta = %10e\tniter = %d\n" - % (type(proxg), type(A), alpha, beta, niter) - ) - head = " Itn x[0] f g J = f + g" - print(head) - - # iterate - if returncost: - j = np.zeros(niter) - for iiter in range(niter): - # compute new x - xnew = ( - (1 - alpha) * xold - + (alpha - beta) * x - + beta * proxg.prox(x - proxf.grad(x), 1.0) - ) - # save current x as old (x_i -> x_i-1) - xold = x.copy() - # save new x as current (x_i+1 -> x_i) - x = xnew.copy() - - # compute cost function - if returncost: - j[iiter] = proxf(x) + proxg(x) - - # run callback - if callback is not None: - callback(x) - - if show: - if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: - pf, pg = proxf(x), proxg(x) - msg = "%6g %12.5e %10.3e %10.3e %10.3e" % ( - iiter + 1, - np.real(to_numpy(x[0])), - pf, - pg, - pf + pg, - ) - print(msg) - if show: - print("\nTotal time (s) = %.2f" % (time.time() - tstart)) - print("---------------------------------------------------------\n") + callbacks = [] + if tol is not None or rtol is not None: + callbacks.append(CostNanInfCallback()) + if rtol is not None: + callbacks.append(CostToInitialCallback(rtol)) + + twistsolve = cTwIST( + callbacks=callbacks if len(callbacks) > 0 else None, + ) + if callback is not None: + twistsolve.callback = callback + x, _, j = twistsolve.solve( + proxg=proxg, + A=A, + b=b, + x0=x0, + alpha=alpha, + beta=beta, + eigs=eigs, + niter=niter, + tol=tol or (0.0 if rtol or returncost else None), + show=show, + itershow=itershow, + ) if returncost: return x, j else: @@ -1684,16 +1132,19 @@ def TwIST( def DouglasRachfordSplitting( - proxf: ProxOperator, - proxg: ProxOperator, + proxf: "ProxOperator", + proxg: "ProxOperator", x0: NDArray, tau: float, eta: float = 1.0, niter: int = 10, gfirst: bool = True, + tol: float | None = None, + rtol: float | None = None, callback: Callable[..., None] | None = None, callbacky: bool = False, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), ) -> tuple[NDArray, NDArray]: r"""Douglas-Rachford Splitting @@ -1724,6 +1175,15 @@ def DouglasRachfordSplitting( gfirst : :obj:`bool`, optional Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + rtol : :obj:`float`, optional + Relative tolerance on objective function wrt initial value. Stops + the solver when the ratio of the current objective function to the + initial objective function is below this value. If ``rtol=None``, + run until ``niter`` is reached or the other tolerance criterion is + met callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector @@ -1732,6 +1192,10 @@ def DouglasRachfordSplitting( when ``callbacky=True`` show : :obj:`bool`, optional Display iterations log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. Returns ------- @@ -1742,86 +1206,47 @@ def DouglasRachfordSplitting( Notes ----- - The Douglas-Rachford Splitting algorithm can be expressed by the following - recursion [1]_, [2]_, [3]_, [4]_: - - .. math:: - - \mathbf{x}^{k} &= \prox_{\tau g}(\mathbf{y}^k) \\ - \mathbf{y}^{k+1} &= \mathbf{y}^{k} + - \eta (\prox_{\tau f}(2 \mathbf{x}^{k} - \mathbf{y}^{k}) - - \mathbf{x}^{k}) - - .. [1] Patrick L. Combettes and Jean-Christophe Pesquet. 2011. Proximal - Splitting Methods in Signal Processing. In Fixed-Point Algorithms for - Inverse Problems in Science and Engineering, Springer, pp. 185-212. - Algorithm 10.15. - https://doi.org/10.1007/978-1-4419-9569-8_10 - .. [2] Scott B. Lindstrom and Brailey Sims. 2021. Survey: Sixty Years of - Douglas-Rachford. Journal of the Australian Mathematical Society, 110, - 3, 333-370. Eq.(15). https://doi.org/10.1017/S1446788719000570 - https://arxiv.org/abs/1809.07181 - .. [3] Ryu, E.K., Yin, W., 2022. Large-Scale Convex Optimization: Algorithms - & Analyses via Monotone Operators. Cambridge University Press, - Cambridge. Eq.(2.18). https://doi.org/10.1017/9781009160865 - https://large-scale-book.mathopt.com/ - .. [4] Combettes, P.L., Pesquet, J.-C., 2008. A proximal decomposition - method for solving convex variational inverse problems. Inverse Problems - 24, 065014. Proposition 3.2. https://doi.org/10.1088/0266-5611/24/6/065014 - https://arxiv.org/abs/0807.2617 + See :class:`pyproximal.optimization.cls_primal.DouglasRachfordSplitting` """ - if show: - tstart = time.time() - print( - "Douglas-Rachford Splitting\n" - "---------------------------------------------------------\n" - f"Proximal operator (f): {type(proxf)}\n" - f"Proximal operator (g): {type(proxg)}\n" - f"tau = {tau:10e}\tniter = {niter:d}\n" - ) - head = " Itn x[0] f g J = f + g" - print(head) - - y = x0.copy() - for iiter in range(niter): - if gfirst: - x = proxg.prox(y, tau) - y = y + eta * (proxf.prox(2 * x - y, tau) - x) - else: - x = proxf.prox(y, tau) - y = y + eta * (proxg.prox(2 * x - y, tau) - x) - - # run callback - if callback is not None: - if callbacky: - callback(x, y) - else: - callback(x) - if show: - if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: - pf, pg = proxf(x), proxg(x) - print( - f"{iiter + 1:6d} {np.real(to_numpy(x[0])):12.5e} " - f"{pf:10.3e} {pg:10.3e} {pf + pg:10.3e}" - ) - - if show: - print(f"\nTotal time (s) = {time.time() - tstart:.2f}") - print("---------------------------------------------------------\n") + callbacks = [] + if tol is not None or rtol is not None: + callbacks.append(CostNanInfCallback()) + if rtol is not None: + callbacks.append(CostToInitialCallback(rtol)) + + drssolve = cDouglasRachfordSplitting( + callbacks=callbacks if len(callbacks) > 0 else None, + ) + if callback is not None: + drssolve.callback = callback + x, y, _, _ = drssolve.solve( + proxf=proxf, + proxg=proxg, + x0=x0, + tau=tau, + eta=eta, + gfirst=gfirst, + niter=niter, + tol=tol or (0.0 if rtol else None), + show=show, + itershow=itershow, + ) return x, y def PPXA( # pylint: disable=invalid-name - proxfs: list[ProxOperator], + proxfs: list["ProxOperator"], x0: NDArray | list[NDArray], tau: float, eta: float = 1.0, weights: NDArray | list[float] | None = None, niter: int = 1000, - tol: float | None = 1e-7, + tol: float | None = None, + rtol: float | None = None, callback: Callable[..., None] | None = None, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), ) -> NDArray: r"""Parallel Proximal Algorithm (PPXA) @@ -1854,13 +1279,23 @@ def PPXA( # pylint: disable=invalid-name niter : :obj:`int`, optional Number of iterations of iterative scheme. tol : :obj:`float`, optional - Tolerance on change of the solution (used as stopping criterion). - If ``tol=0``, run until ``niter`` is reached. + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + rtol : :obj:`float`, optional + Relative tolerance on objective function wrt initial value. Stops + the solver when the ratio of the current objective function to the + initial objective function is below this value. If ``rtol=None``, + run until ``niter`` is reached or the other tolerance criterion is + met callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector show : :obj:`bool`, optional Display iterations log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. Returns ------- @@ -1873,115 +1308,44 @@ def PPXA( # pylint: disable=invalid-name Notes ----- - The Parallel Proximal Algorithm (PPXA) can be expressed by the following - recursion [1]_, [2]_, [3]_, [4]_: - - * :math:`\mathbf{y}_{i}^{0} = \mathbf{x}` or :math:`\mathbf{y}_{i}^{0} = \mathbf{x}_{i}` for :math:`i=1,\ldots,m` - * :math:`\mathbf{x}^{0} = \sum_{i=1}^m w_i \mathbf{y}_{i}^{0}` - * for :math:`k = 1, \ldots` - - * for :math:`i = 1, \ldots, m` - - * :math:`\mathbf{p}_{i}^{k} = \prox_{\frac{\tau}{w_i} f_i} (\mathbf{y}_{i}^{k})` - - * :math:`\mathbf{p}^{k} = \sum_{i=1}^{m} w_i \mathbf{p}_{i}^{k}` - * for :math:`i = 1, \ldots, m` - - * :math:`\mathbf{y}_{i}^{k+1} = \mathbf{y}_{i}^{k} + \eta (2 \mathbf{p}^{k} - \mathbf{x}^{k} - \mathbf{p}_i^{k})` - - * :math:`\mathbf{x}^{k+1} = \mathbf{x}^{k} + \eta (\mathbf{p}^{k} - \mathbf{x}^{k})` - - where :math:`0 < \eta < 2` and - :math:`\sum_{i=1}^m w_i = 1, \ 0 < w_i < 1`. - In the current implementation, :math:`w_i = 1 / m` when not provided. - - References - ---------- - .. [1] Combettes, P.L., Pesquet, J.-C., 2008. A proximal decomposition - method for solving convex variational inverse problems. Inverse Problems - 24, 065014. Algorithm 3.1. https://doi.org/10.1088/0266-5611/24/6/065014 - https://arxiv.org/abs/0807.2617 - .. [2] Combettes, P.L., Pesquet, J.-C., 2011. Proximal Splitting Methods in - Signal Processing, in Fixed-Point Algorithms for Inverse Problems in - Science and Engineering, Springer, pp. 185-212. Algorithm 10.27. - https://doi.org/10.1007/978-1-4419-9569-8_10 - .. [3] Bauschke, H.H., Combettes, P.L., 2011. Convex Analysis and Monotone - Operator Theory in Hilbert Spaces, 1st ed, CMS Books in Mathematics. - Springer, New York, NY. Proposition 27.8. - https://doi.org/10.1007/978-1-4419-9467-7 - .. [4] Ryu, E.K., Yin, W., 2022. Large-Scale Convex Optimization: Algorithms - & Analyses via Monotone Operators. Cambridge University Press, - Cambridge. Exercise 2.38 https://doi.org/10.1017/9781009160865 - https://large-scale-book.mathopt.com/ + See :class:`pyproximal.optimization.cls_primal.PPXA` """ - if show: - tstart = time.time() - print( - "Parallel Proximal Algorithm\n" - "---------------------------------------------------------" - ) - for i, proxf in enumerate(proxfs): - print(f"Proximal operator (f{i}): {type(proxf)}") - print(f"tau = {tau:10e}\tniter = {niter:d}\n") - head = " Itn x[0] J=sum_i f_i" - print(head) - - ncp = get_array_module(x0) - - # initialize model - m = len(proxfs) - if weights is None: - w = ncp.full(m, 1.0 / m) - else: - w = ncp.asarray(weights) - - if isinstance(x0, list) or x0.ndim == 2: - y = ncp.asarray(x0) # yi_0 = xi_0, for i = 1, ..., m - else: - y = ncp.full((m, x0.size), x0) # y1_0 = y2_0 = ... = ym_0 = x0 - - x = ncp.mean(y, axis=0) - x_old = x.copy() - - # iterate - for iiter in range(niter): - p = ncp.stack([proxfs[i].prox(y[i], tau / w[i]) for i in range(m)]) - pn = ncp.sum(w[:, None] * p, axis=0) - y = y + eta * (2 * pn - x - p) - x = x + eta * (pn - x) - - # run callback - if callback is not None: - callback(x) - - # show iteration logger - if show: - if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: - pf = ncp.sum([proxfs[i](x) for i in range(m)]) - print(f"{iiter + 1:6d} {ncp.real(to_numpy(x[0])):12.5e} {pf:10.3e}") - - # break if tolerance condition is met - if ncp.abs(x - x_old).max() < tol: - break - - x_old = x - - if show: - print(f"\nTotal time (s) = {time.time() - tstart:.2f}") - print("---------------------------------------------------------\n") - + callbacks = [] + if tol is not None or rtol is not None: + callbacks.append(CostNanInfCallback()) + if rtol is not None: + callbacks.append(CostToInitialCallback(rtol)) + + ppxasolve = cPPXA( + callbacks=callbacks if len(callbacks) > 0 else None, + ) + if callback is not None: + ppxasolve.callback = callback + x, _, _, _ = ppxasolve.solve( + proxfs=proxfs, + x0=x0, + tau=tau, + eta=eta, + weights=weights, + niter=niter, + tol=tol or (0.0 if rtol else None), + show=show, + itershow=itershow, + ) return x def ConsensusADMM( # pylint: disable=invalid-name - proxfs: list[ProxOperator], + proxfs: list["ProxOperator"], x0: NDArray, tau: float, niter: int = 1000, - tol: float | None = 1e-7, + tol: float | None = None, + rtol: float | None = None, callback: Callable[..., None] | None = None, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), ) -> NDArray: r"""Consensus ADMM @@ -2007,13 +1371,23 @@ def ConsensusADMM( # pylint: disable=invalid-name niter : :obj:`int`, optional Number of iterations of iterative scheme. tol : :obj:`float`, optional - Tolerance on change of the solution (used as stopping criterion). - If ``tol=0``, run until ``niter`` is reached. + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + rtol : :obj:`float`, optional + Relative tolerance on objective function wrt initial value. Stops + the solver when the ratio of the current objective function to the + initial objective function is below this value. If ``rtol=None``, + run until ``niter`` is reached or the other tolerance criterion is + met callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector show : :obj:`bool`, optional Display iterations log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. Returns ------- @@ -2027,82 +1401,27 @@ def ConsensusADMM( # pylint: disable=invalid-name Notes ----- - The ADMM for the consensus problem can be expressed by the following - recursion [1]_, [2]_: - - * :math:`\bar{\mathbf{x}}^{0} = \mathbf{x}` - * for :math:`k = 1, \ldots` - - * for :math:`i = 1, \ldots, m` - - * :math:`\mathbf{x}_i^{k+1} = \mathrm{prox}_{\tau f_i} \left(\bar{\mathbf{x}}^{k} - \mathbf{y}_i^{k}\right)` - - * :math:`\bar{\mathbf{x}}^{k+1} = \frac{1}{m} \sum_{i=1}^m \mathbf{x}_i^{k}` - - * for :math:`i = 1, \ldots, m` - - * :math:`\mathbf{y}_i^{k+1} = \mathbf{y}_i^{k} + \mathbf{x}_i^{k+1} - \bar{\mathbf{x}}^{k+1}` - - The current implementation returns :math:`\bar{\mathbf{x}}`. - - References - ---------- - .. [1] Boyd, S., Parikh, N., Chu, E., Peleato, B., Eckstein, J., 2011. - Distributed Optimization and Statistical Learning via the Alternating - Direction Method of Multipliers. Foundations and Trends in Machine Learning, - Vol. 3, No. 1, pp 1-122. Section 7.1. https://doi.org/10.1561/2200000016 - https://stanford.edu/~boyd/papers/pdf/admm_distr_stats.pdf - .. [2] Parikh, N., Boyd, S., 2014. Proximal Algorithms. Foundations and - Trends in Optimization, Vol. 1, No. 3, pp 127-239. - Section 5.2.1. https://doi.org/10.1561/2400000003 - https://web.stanford.edu/~boyd/papers/pdf/prox_algs.pdf + See :class:`pyproximal.optimization.cls_primal.ConsensusADMM` """ - if show: - tstart = time.time() - print( - "Consensus ADMM\n---------------------------------------------------------" - ) - for i, proxf in enumerate(proxfs): - print(f"Proximal operator (f{i}): {type(proxf)}") - print(f"tau = {tau:10e}\tniter = {niter:d}\n") - head = " Itn x[0] J=sum_i f_i" - print(head) - - ncp = get_array_module(x0) - - # initialize model - m = len(proxfs) - x_bar = x0.copy() - x_bar_old = x0.copy() - y = ncp.zeros((m, x0.size), dtype=x0.dtype) - - # iterate - for iiter in range(niter): - x = ncp.stack([proxfs[i].prox(x_bar - y[i], tau) for i in range(m)]) - x_bar = ncp.mean(x, axis=0) - y = y + x - x_bar - - # run callback - if callback is not None: - callback(x_bar) - - # show iteration logger - if show: - if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: - pf = ncp.sum([proxfs[i](x_bar) for i in range(m)]) - print( - f"{iiter + 1:6d} {ncp.real(to_numpy(x_bar[0])):12.5e} {pf:10.3e}" - ) - - # break if tolerance condition is met - if ncp.abs(x_bar - x_bar_old).max() < tol: - break - - x_bar_old = x_bar - - if show: - print(f"\nTotal time (s) = {time.time() - tstart:.2f}") - print("---------------------------------------------------------\n") - - return x_bar + callbacks = [] + if tol is not None or rtol is not None: + callbacks.append(CostNanInfCallback()) + if rtol is not None: + callbacks.append(CostToInitialCallback(rtol)) + + ccadmmsolve = cConsensusADMM( + callbacks=callbacks if len(callbacks) > 0 else None, + ) + if callback is not None: + ccadmmsolve.callback = callback + _, x, _, _, _ = ccadmmsolve.solve( + proxfs=proxfs, + x0=x0, + tau=tau, + niter=niter, + tol=tol or (0.0 if rtol else None), + show=show, + itershow=itershow, + ) + return x From b819bea1e4f5689a53ef81692e1983a4650d49cc Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 26 Jun 2026 10:52:49 +0100 Subject: [PATCH 31/73] feat: added custom callbacks --- pyproximal/optimization/callback.py | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 pyproximal/optimization/callback.py diff --git a/pyproximal/optimization/callback.py b/pyproximal/optimization/callback.py new file mode 100644 index 00000000..91fdc50f --- /dev/null +++ b/pyproximal/optimization/callback.py @@ -0,0 +1,39 @@ +__all__ = [ + "ModuleUpdateCallback", +] + +from typing import TYPE_CHECKING + +from pylops.optimization.callback import Callbacks +from pylops.utils.typing import NDArray + +if TYPE_CHECKING: + from pyproximal.optimization.basesolver import Solver + + +class ModuleUpdateCallback(Callbacks): # type: ignore[misc] + """Module update callback + + This callback can be used to stop the solver when each element of + the model (i.e, solution) is updated below a certain threshold. + + It requires the solver to store the model of the previous iteration + in a variable named ``xold``. + + Parameters + ---------- + tol : :obj:`float` + Absolute value of model update below which the solver + will stop iterating. For example, if ``tol`` is 0.1, the solver + will stop when the absolute value of each element of the difference + between the current model is below below 0.1. + + """ + + def __init__(self, tol: float) -> None: + self.tol = tol + self.stop = False + + def on_step_end(self, solver: "Solver", x: NDArray) -> None: + if solver.ncp.abs(x - solver.xold).max() < self.tol: + self.stop = True From b76dbedc7eeac717d4fc2359bf0a577ef9400596 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 26 Jun 2026 11:15:16 +0100 Subject: [PATCH 32/73] feat: allow niter=None in ProximalGradient setup --- pyproximal/optimization/cls_primal.py | 45 +++++++++++++++++---------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index 0a39745c..21b303de 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -454,10 +454,12 @@ def _print_setup(self, epsg_print: str, xcomplex: bool = False) -> None: head1 = " Itn x[0] f g J=f+eps*g tau" print(head1) - def _print_step(self, x: NDArray, pf: float | None, pg: float | None) -> None: + def _print_step( + self, x: NDArray, pf: float | None, pg: float | None, epsg_prev: int + ) -> None: if self.tol is None: pf, pg = self.proxf(x), self.proxg(x) - self.pfg = pf + np.sum(self.epsg[self.iiter - 1] * pg) + self.pfg = pf + np.sum(epsg_prev * pg) x0 = to_numpy(x[0]) if x.ndim == 1 else to_numpy(x[0, 0]) strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " tau_str = ( @@ -484,7 +486,7 @@ def setup( # type: ignore[override] eta: float = 1.0, acceleration: str | None = None, niterback: int = 100, - niter: int = 10, + niter: int | None = None, tol: float | None = None, show: bool = False, ) -> tuple[NDArray, NDArray]: @@ -520,7 +522,8 @@ def setup( # type: ignore[override] niterback : :obj:`int`, optional Max number of iterations of backtracking niter : :obj:`int`, optional - Number of iterations of iterative scheme + Number of iterations (default to ``None`` in case a user wants to + manually step over the solver) tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -550,8 +553,11 @@ def setup( # type: ignore[override] # check if epgs is a vector self.epsg = self.ncp.asarray(epsg, dtype=np.float32) if self.epsg.size == 1: - self.epsg = epsg * self.ncp.ones(niter, dtype=np.float32) - epsg_print = str(self.epsg[0]) + if niter is None: + epsg_print = str(self.epsg) + else: + self.epsg = epsg * self.ncp.ones(niter, dtype=np.float32) + epsg_print = str(self.epsg[0]) else: epsg_print = "Multi" @@ -576,8 +582,9 @@ def setup( # type: ignore[override] self.t = 1.0 # create variables to track the objective function and iterations + epsg_ = self.epsg if niter is None else self.epsg[0] pf, pg = self.proxf(x), self.proxg(x) - pfg = pf + np.sum(self.epsg[self.iiter] * pg) + pfg = pf + np.sum(epsg_ * pg) self.pfg, self.pfgold = pfg, pfg self.cost: list[float] = [] @@ -619,17 +626,23 @@ def step( """ xold = x.copy() + # define epsg for current iteration + if self.epsg.ndim == 0: + epsg = self.epsg + epsg_prev = self.epsg + else: + epsg = self.epsg[self.iiter] + epsg_prev = self.epsg[self.iiter - 1] + # proximal step if not self.backtracking: if self.eta == 1.0: - x = self.proxg.prox( - y - self.tau * self.proxf.grad(y), self.epsg[self.iiter] * self.tau - ) + x = self.proxg.prox(y - self.tau * self.proxf.grad(y), epsg * self.tau) else: x = x + self.eta * ( self.proxg.prox( x - self.tau * self.proxf.grad(x), - self.epsg[self.iiter] * self.tau, + epsg * self.tau, ) - x ) @@ -639,7 +652,7 @@ def step( cast(float, self.tau), self.proxf, self.proxg, - self.epsg[self.iiter], + epsg, beta=self.beta, niterback=self.niterback, ) @@ -647,7 +660,7 @@ def step( x = x + self.eta * ( self.proxg.prox( x - self.tau * self.proxf.grad(x), - self.epsg[self.iiter] * self.tau, + epsg * self.tau, ) - x ) @@ -672,7 +685,7 @@ def step( if self.tol is not None: self.pfgold = self.pfg pf, pg = self.proxf(x), self.proxg(x) - self.pfg = pf + np.sum(self.epsg[self.iiter] * pg) + self.pfg = pf + np.sum(epsg * pg) if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: self.tolbreak = True else: @@ -680,7 +693,7 @@ def step( self.iiter += 1 if show: - self._print_step(x, pf, pg) + self._print_step(x, pf, pg, epsg_prev) if self.tol is not None or show: self.cost.append(float(self.pfg)) return x, y @@ -4554,5 +4567,3 @@ def solve( # type: ignore[override] # TOD0: # - make niter optional in setup and fix and prints # in _print_setup to not include if not provided -# - add the tolerance check on the solution for PPXA and -# ConsensunADMM for the function based solvers via callbacks From 0a46108ae8bcec0e819ce7be44958d5670480975 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 26 Jun 2026 11:17:24 +0100 Subject: [PATCH 33/73] fix: fixed initialization of u in ADMM solvers --- pyproximal/optimization/cls_primal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index 21b303de..a7453f2f 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -2549,7 +2549,7 @@ def setup( # type: ignore[override] # initialize solver x, z = _x0z0_init(x0, z0, A, Opname="A") - self.u = self.ncp.zeros_like(x) + self.u = self.ncp.zeros_like(z) # other parameters self.sqrttau = 1.0 / sqrt(self.tau) @@ -2961,7 +2961,7 @@ def setup( # type: ignore[override] # initialize solver x, z = _x0z0_init(x0, z0, A, Opname="A") self.Ax = A.matvec(x) if z0 is None else z - self.u = self.ncp.zeros_like(x) + self.u = self.ncp.zeros_like(z) # create variables to track the objective function and iterations self.pfg, self.pfgold = np.inf, np.inf From 08e060e133feaaed50a239ae466b1321ae839356 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sat, 27 Jun 2026 16:03:56 +0100 Subject: [PATCH 34/73] feat: allow solvers not to set niter in setup --- pyproximal/optimization/cls_primal.py | 136 ++++++++++++++++++-------- pyproximal/optimization/primal.py | 11 ++- pytests/test_solver.py | 37 ++++++- 3 files changed, 138 insertions(+), 46 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index a7453f2f..dbc6455e 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -501,7 +501,9 @@ def setup( # type: ignore[override] x0 : :obj:`numpy.ndarray` Initial vector epsg : :obj:`float` or :obj:`numpy.ndarray`, optional - Scaling factor of g function + Scaling factor of g function. Can be a scalar + for iteration-independent scaling or a 1d vector for + iteration-dependent scaling tau : :obj:`float` or :obj:`numpy.ndarray`, optional Positive scalar weight, which should satisfy the following condition to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is @@ -550,7 +552,7 @@ def setup( # type: ignore[override] self.ncp = get_array_module(x0) - # check if epgs is a vector + # check if epsg is a vector self.epsg = self.ncp.asarray(epsg, dtype=np.float32) if self.epsg.size == 1: if niter is None: @@ -785,7 +787,9 @@ def solve( # type: ignore[override] x0 : :obj:`numpy.ndarray` Initial vector epsg : :obj:`float` or :obj:`numpy.ndarray`, optional - Scaling factor of g function + Scaling factor of g function. Can be a scalar + for iteration-independent scaling or a 1d vector for + iteration-dependent scaling tau : :obj:`float` or :obj:`numpy.ndarray`, optional Positive scalar weight, which should satisfy the following condition to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is @@ -919,10 +923,10 @@ def _print_setup(self, epsg_print: str, xcomplex: bool = False) -> None: head1 = " Itn x[0] f g J=f+eps*g tau" print(head1) - def _print_step(self, x: NDArray, pg: float | None) -> None: + def _print_step(self, x: NDArray, pg: float | None, epsg_prev: int) -> None: if self.tol is None: self.pf, pg = self.proxf(x), self.proxg(x) - self.pfg = self.pf + np.sum(self.epsg[self.iiter - 1] * pg) + self.pfg = self.pf + np.sum(epsg_prev * pg) x0 = to_numpy(x[0]) if x.ndim == 1 else to_numpy(x[0, 0]) strx = f"{x0:1.2e} " if np.iscomplexobj(x) else f"{x0:11.4e} " tau_str = ( @@ -947,7 +951,7 @@ def setup( # type: ignore[override] epsr: float = 1e-10, safeguard: bool = False, nhistory: int = 10, - niter: int = 10, + niter: int | None = None, tol: float | None = None, show: bool = False, ) -> tuple[NDArray, NDArray]: @@ -962,7 +966,9 @@ def setup( # type: ignore[override] x0 : :obj:`numpy.ndarray` Initial vector epsg : :obj:`float` or :obj:`numpy.ndarray`, optional - Scaling factor of g function + Scaling factor of g function. Can be a scalar + for iteration-independent scaling or a 1d vector for + iteration-dependent scaling tau : :obj:`float` or :obj:`numpy.ndarray`, optional Positive scalar weight, which should satisfy the following condition to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is @@ -976,7 +982,8 @@ def setup( # type: ignore[override] nhistory : :obj:`int`, optional Number of previous iterates to be kept in memory (to compute the scaling factors) niter : :obj:`int`, optional - Number of iterations of iterative scheme + Number of iterations (default to ``None`` in case a user wants to + manually step over the solver) tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -1000,11 +1007,14 @@ def setup( # type: ignore[override] self.ncp = get_array_module(x0) - # check if epgs is a vector + # check if epsg is a vector self.epsg = self.ncp.asarray(epsg, dtype=np.float32) if self.epsg.size == 1: - self.epsg = self.epsg * self.ncp.ones(niter, dtype=np.float32) - epsg_print = str(self.epsg[0]) + if niter is None: + epsg_print = str(self.epsg) + else: + self.epsg = self.epsg * self.ncp.ones(niter, dtype=np.float32) + epsg_print = str(self.epsg[0]) else: epsg_print = "Multi" @@ -1012,8 +1022,9 @@ def setup( # type: ignore[override] self.tau = self.ncp.atleast_1d(self.ncp.asarray(tau, dtype=np.float32)) # initialize solver + epsg_ = self.epsg if niter is None else self.epsg[0] y = x0 - self.tau * proxf.grad(x0) - x = self.proxg.prox(y, self.epsg[0] * self.tau) + x = self.proxg.prox(y, epsg_ * self.tau) # set history of iterates for Anderson acceleration g = y.copy() @@ -1029,8 +1040,11 @@ def setup( # type: ignore[override] # create variables to track the objective function and iterations self.pf = self.proxf(x) - self.pfg, self.pfgold = np.inf, np.inf + pg = self.proxg(x) + pfg = self.pf + np.sum(epsg_ * pg) + self.pfg, self.pfgold = pfg, pfg self.cost: list[float] = [] + self.cost.append(float(self.pfg)) self.tolbreak = False self.iiter = 0 @@ -1066,6 +1080,13 @@ def step( Updated additional model vector """ + # define epsg for current iteration + if self.epsg.ndim == 0: + epsg = self.epsg + epsg_prev = self.epsg + else: + epsg = self.epsg[self.iiter] + epsg_prev = self.epsg[self.iiter - 1] # update fix point g = x - self.tau * self.proxf.grad(x) @@ -1092,13 +1113,13 @@ def step( y = np.vstack(self.G).T @ alpha # update main variable - x = self.proxg.prox(y, self.epsg[self.iiter] * self.tau) + x = self.proxg.prox(y, epsg * self.tau) else: # update auxiliary variable ytest = np.vstack(self.G).T @ alpha # update main variable - xtest = self.proxg.prox(ytest, self.epsg[self.iiter] * self.tau) + xtest = self.proxg.prox(ytest, epsg * self.tau) # check if function is decreased, otherwise do basic PG step pfold, self.pf = self.pf, self.proxf(xtest) @@ -1109,7 +1130,7 @@ def step( y = ytest x = xtest else: - x = self.proxg.prox(g, self.epsg[self.iiter] * self.tau) + x = self.proxg.prox(g, epsg * self.tau) y = g # tolerance check: break iterations if overall @@ -1117,7 +1138,7 @@ def step( if self.tol is not None: self.pfgold = self.pfg self.pf, pg = self.proxf(x), self.proxg(x) - self.pfg = self.pf + np.sum(self.epsg[self.iiter] * pg) + self.pfg = self.pf + np.sum(epsg * pg) if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: self.tolbreak = True else: @@ -1125,7 +1146,7 @@ def step( self.iiter += 1 if show: - self._print_step(x, pg) + self._print_step(x, pg, epsg_prev) if self.tol is not None or show: self.cost.append(float(self.pfg)) return x, y @@ -1215,7 +1236,9 @@ def solve( # type: ignore[override] x0 : :obj:`numpy.ndarray` Initial vector epsg : :obj:`float` or :obj:`numpy.ndarray`, optional - Scaling factor of g function + Scaling factor of g function. Can be a scalar + for iteration-independent scaling or a 1d vector for + iteration-dependent scaling tau : :obj:`float` or :obj:`numpy.ndarray`, optional Positive scalar weight, which should satisfy the following condition to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is @@ -1356,7 +1379,7 @@ def setup( # type: ignore[override] weights: NDArray | None = None, eta: float = 1.0, acceleration: str | None = None, - niter: int = 10, + niter: int | None = None, tol: float | None = None, show: bool = False, ) -> tuple[NDArray, NDArray]: @@ -1375,7 +1398,8 @@ def setup( # type: ignore[override] to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is the Lipschitz constant of :math:`\sum_{i=1}^n \nabla f_i`. epsg : :obj:`float` or :obj:`numpy.ndarray`, optional - Scaling factor(s) of ``g`` function(s) + Scaling factor(s) of ``g`` function(s). If a scalar is provided + the same scaling factor is applied to every ``g`` function. weights : :obj:`float`, optional Weighting factors of ``g`` functions. Must sum to 1. eta : :obj:`float`, optional @@ -1384,7 +1408,8 @@ def setup( # type: ignore[override] acceleration: :obj:`str`, optional Acceleration (``None``, ``vandenberghe`` or ``fista``) niter : :obj:`int`, optional - Number of iterations of iterative scheme + Number of iterations (default to ``None`` in case a user wants to + manually step over the solver) tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -1417,12 +1442,21 @@ def setup( # type: ignore[override] msg = f"weights={self.weights} must be an array of size {len(self.proxgs)} summing to 1" raise ValueError(msg) - # check if epgs is a vector - self.epsg = np.asarray(epsg, dtype=np.float32) + # check if epsg is a vector and set same value for all gs otherwise + print("epsg", epsg) + self.epsg = self.ncp.asarray(epsg, dtype=np.float32) + print("self.epsg.size", self.epsg.size) if self.epsg.size == 1: self.epsg = epsg * self.ncp.ones(len(proxgs), dtype=np.float32) epsg_print = str(self.epsg[0]) else: + if self.epsg.size != len(proxgs): + msg = ( + "epgs must be a scalar or a vector of size " + "equal to the number of g functions, " + f"provided {self.epsg.size} instead" + ) + raise ValueError(msg) epsg_print = "Multi" # check acceleration @@ -1617,7 +1651,8 @@ def solve( # type: ignore[override] to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is the Lipschitz constant of :math:`\sum_{i=1}^n \nabla f_i`. epsg : :obj:`float` or :obj:`numpy.ndarray`, optional - Scaling factor(s) of ``g`` function(s) + Scaling factor(s) of ``g`` function(s). If a scalar is provided + the same scaling factor is applied to every ``g`` function. weights : :obj:`float`, optional Weighting factors of ``g`` functions. Must sum to 1. eta : :obj:`float`, optional @@ -1752,7 +1787,7 @@ def setup( # type: ignore[override] tau: float | NDArray, z0: NDArray | None = None, gfirst: bool = True, - niter: int = 10, + niter: int | None = None, tol: float | None = None, callbackz: bool = False, show: bool = False, @@ -1780,7 +1815,8 @@ def setup( # type: ignore[override] Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) niter : :obj:`int`, optional - Number of iterations of iterative scheme + Number of iterations (default to ``None`` in case a user wants to + manually step over the solver) tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -1814,8 +1850,11 @@ def setup( # type: ignore[override] # check if tau is a vector self.tau = self.ncp.asarray(tau, dtype=np.float32) if self.tau.size == 1: - tau_print = str(np.round(self.tau, 6)) - self.tau = self.tau * self.ncp.ones(niter, dtype=np.float32) + if niter is None: + tau_print = str(np.round(self.tau, 6)) + else: + self.tau = self.tau * self.ncp.ones(niter, dtype=np.float32) + tau_print = str(np.round(self.tau[0], 6)) else: tau_print = "Variable" @@ -1826,8 +1865,11 @@ def setup( # type: ignore[override] self.t = 1.0 # create variables to track the objective function and iterations - self.pfg, self.pfgold = np.inf, np.inf + pf, pg = self.proxf(x), self.proxg(x) + pfg = pf + pg + self.pfg, self.pfgold = pfg, pfg self.cost: list[float] = [] + self.cost.append(float(self.pfg)) self.tolbreak = False self.iiter = 0 @@ -1863,13 +1905,19 @@ def step( Updated additional model vector """ + # define tau for current iteration + if self.tau.ndim == 0: + tau = self.tau + else: + tau = self.tau[self.iiter] + # proximal steps if self.gfirst: - z = self.proxg.prox(x, self.tau[self.iiter]) - x = self.proxf.prox(z, self.tau[self.iiter]) + z = self.proxg.prox(x, tau) + x = self.proxf.prox(z, tau) else: - x = self.proxf.prox(z, self.tau[self.iiter]) - z = self.proxg.prox(x, self.tau[self.iiter]) + x = self.proxf.prox(z, tau) + z = self.proxg.prox(x, tau) # tolerance check: break iterations if overall # objective does not decrease below tolerance @@ -1960,7 +2008,7 @@ def solve( # type: ignore[override] tau: float | NDArray, z0: NDArray | None = None, gfirst: bool = True, - niter: int = 10, + niter: int | None = None, tol: float | None = None, callbackz: bool = False, show: bool = False, @@ -1989,7 +2037,8 @@ def solve( # type: ignore[override] Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) niter : :obj:`int`, optional - Number of iterations of iterative scheme + Number of iterations (default to ``None`` in case a user wants to + manually step over the solver) tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -2133,7 +2182,7 @@ def setup( # type: ignore[override] tau: float, z0: NDArray | None = None, gfirst: bool = False, - niter: int = 10, + niter: int | None = None, tol: float | None = None, callbackz: bool = False, show: bool = False, @@ -2158,7 +2207,8 @@ def setup( # type: ignore[override] Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) niter : :obj:`int`, optional - Number of iterations of iterative scheme + Number of iterations (default to ``None`` in case a user wants to + manually step over the solver) tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -2485,7 +2535,7 @@ def setup( # type: ignore[override] tau: float, z0: NDArray | None = None, gfirst: bool = False, - niter: int = 10, + niter: int | None = None, tol: float | None = None, callbackz: bool = False, show: bool = False, @@ -2513,7 +2563,8 @@ def setup( # type: ignore[override] Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) niter : :obj:`int`, optional - Number of iterations of iterative scheme + Number of iterations (default to ``None`` in case a user wants to + manually step over the solver) tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -2900,7 +2951,7 @@ def setup( # type: ignore[override] tau: float, mu: float, z0: NDArray | None = None, - niter: int = 10, + niter: int | None = None, tol: float | None = None, show: bool = False, ) -> tuple[NDArray, NDArray]: @@ -2927,7 +2978,8 @@ def setup( # type: ignore[override] z0 : :obj:`numpy.ndarray` Initial auxiliary vector. If ``None``, initialized to ``A @ x0``. niter : :obj:`int`, optional - Number of iterations of iterative scheme + Number of iterations (default to ``None`` in case a user wants to + manually step over the solver) tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached diff --git a/pyproximal/optimization/primal.py b/pyproximal/optimization/primal.py index 342bfc7f..a7c6f07d 100644 --- a/pyproximal/optimization/primal.py +++ b/pyproximal/optimization/primal.py @@ -171,7 +171,9 @@ def ProximalGradient( x0 : :obj:`numpy.ndarray` Initial vector epsg : :obj:`float` or :obj:`numpy.ndarray`, optional - Scaling factor of g function + Scaling factor of g function. Can be a scalar + for iteration-independent scaling or a a 1d vector for + iteration-dependent scaling tau : :obj:`float` or :obj:`numpy.ndarray`, optional Positive scalar weight, which should satisfy the following condition to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is @@ -340,7 +342,9 @@ def AndersonProximalGradient( x0 : :obj:`numpy.ndarray` Initial vector epsg : :obj:`float` or :obj:`numpy.ndarray`, optional - Scaling factor of g function + Scaling factor of g function. Can be a scalar + for iteration-independent scaling or a a 1d vector for + iteration-dependent scaling tau : :obj:`float` or :obj:`numpy.ndarray`, optional Positive scalar weight, which should satisfy the following condition to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is @@ -455,7 +459,8 @@ def GeneralizedProximalGradient( to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is the Lipschitz constant of :math:`\sum_{i=1}^n \nabla f_i`. epsg : :obj:`float` or :obj:`numpy.ndarray`, optional - Scaling factor(s) of ``g`` function(s) + Scaling factor(s) of ``g`` function(s). If a scalar is provided + the same scaling factor is applied to every ``g`` function. weights : :obj:`float`, optional Weighting factors of ``g`` functions. Must sum to 1. eta : :obj:`float`, optional diff --git a/pytests/test_solver.py b/pytests/test_solver.py index 408ac5ae..0eb354ca 100644 --- a/pytests/test_solver.py +++ b/pytests/test_solver.py @@ -20,7 +20,7 @@ ProximalPoint, TwIST, ) -from pyproximal.proximal import L1, L2, Quadratic +from pyproximal.proximal import L1, L2, Box, Quadratic par1 = {"n": 10, "m": 10, "dtype": "float64"} # square, float64 par2 = {"n": 8, "m": 10, "dtype": "float64"} # underdetermined, float64 @@ -150,6 +150,41 @@ def test_GPG_weights(par): ) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) +def test_GPG_epsg(par): + """Check GPG raises error if epsg is a vector with number + of elements differring from the number of g functions + passed to proxgs""" + with pytest.raises(ValueError, match="must be a scalar or a vector"): + np.random.seed(0) + n, m = par["n"], par["m"] + + # Random mixing matrix + R = np.random.normal(0.0, 1.0, (n, m)) + Rop = MatrixMult(R) + + # Model and data + x = np.zeros(m) + y = Rop @ x + + # Operators + l2 = L2(Op=Rop, b=y, niter=10, warm=True) + l1 = L1(sigma=5e-1) + box = Box() + _ = GeneralizedProximalGradient( + [ + l2, + ], + [ + l1, + box, + ], + x0=np.zeros(m), + tau=1.0, + epsg=np.ones(5), + ) + + @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_ProximalPoint(par): """Check solution of ProximalPoint for quadratic function equals the solution of the From f456176928a7ccd51a06473216203ed5856d2da6 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sat, 27 Jun 2026 17:31:49 +0100 Subject: [PATCH 35/73] feat: allow more solvers not to set niter in setup --- pyproximal/optimization/cls_primal.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index dbc6455e..d85942c7 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -3315,7 +3315,7 @@ def setup( # type: ignore[override] alpha: float | None = None, beta: float | None = None, eigs: tuple[float, float] | None = None, - niter: int = 10, + niter: int | None = None, tol: float | None = None, show: bool = False, ) -> NDArray: @@ -3341,7 +3341,8 @@ def setup( # type: ignore[override] Largest and smallest eigenvalues of :math:`\mathbf{A}^H \mathbf{A}`. If passed, computes `alpha` and `beta` based on them. niter : :obj:`int`, optional - Number of iterations of iterative scheme + Number of iterations (default to ``None`` in case a user wants to + manually step over the solver) tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -3672,7 +3673,7 @@ def setup( # type: ignore[override] tau: float, eta: float = 1.0, gfirst: bool = True, - niter: int = 10, + niter: int | None = None, tol: float | None = None, callbacky: bool = False, show: bool = False, @@ -3695,7 +3696,8 @@ def setup( # type: ignore[override] Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) niter : :obj:`int`, optional - Number of iterations of iterative scheme + Number of iterations (default to ``None`` in case a user wants to + manually step over the solver) tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -4032,7 +4034,7 @@ def setup( # type: ignore[override] tau: float, eta: float = 1.0, weights: NDArray | list[float] | None = None, - niter: int = 1000, + niter: int | None = None, tol: float | None = None, show: bool = False, ) -> tuple[NDArray, NDArray]: @@ -4055,7 +4057,8 @@ def setup( # type: ignore[override] Weights :math:`\sum_{i=1}^m w_i = 1, \ 0 < w_i < 1`, Defaults to None, which means :math:`w_1 = \cdots = w_m = \frac{1}{m}.` niter : :obj:`int`, optional - Number of iterations of iterative scheme. + Number of iterations (default to ``None`` in case a user wants to + manually step over the solver) tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached @@ -4374,7 +4377,7 @@ def setup( # type: ignore[override] proxfs: list[ProxOperator], x0: NDArray, tau: float, - niter: int = 1000, + niter: int | None = None, tol: float | None = None, show: bool = False, ) -> tuple[NDArray, NDArray, NDArray]: @@ -4389,7 +4392,8 @@ def setup( # type: ignore[override] tau : :obj:`float` Positive scalar weight niter : :obj:`int`, optional - Number of iterations of iterative scheme. + Number of iterations (default to ``None`` in case a user wants to + manually step over the solver) tol : :obj:`float`, optional Tolerance on change of objective function (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached From 5bd57c1df795cd22c14c16a2d59106335cb5c713 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Tue, 30 Jun 2026 22:02:15 +0100 Subject: [PATCH 36/73] minor: improve print in ProximalPoint solver --- pyproximal/optimization/cls_primal.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index d85942c7..fce9f84e 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -158,12 +158,7 @@ def _print_setup(self, xcomplex: bool = False) -> None: self._print_solver(nbar=60) strpar = f"Proximal operator: {type(self.prox).__name__}" - if self.niter is not None: - strpar1 = ( - f"tau = {self.tau:6e}\ttol = {str(self.tol)}\tniter = {self.niter}" - ) - else: - strpar1 = f"tau = {self.tau:6e}\ttol = {str(self.tol)}" + strpar1 = f"tau = {self.tau:6e}\ttol = {str(self.tol)}\tniter = {self.niter}" print(strpar) print(strpar1) print("-" * 60 + "\n") @@ -215,7 +210,6 @@ def setup( # type: ignore[override] """ self.prox = prox - self.x0 = x0 self.tau = tau self.niter = niter self.tol = tol @@ -439,10 +433,7 @@ def _print_setup(self, epsg_print: str, xcomplex: bool = False) -> None: ) strpar1 = f"tau = {tau_str}\t\tbacktrack = {self.backtracking}" strpar2 = f"beta = {self.beta}\t\tepsg = {epsg_print}\t\tacceleration = {self.acceleration}" - if self.niter is not None: - strpar3 = f"niter = {self.niter}\t\tniterback = {self.niterback}\t\ttol = {str(self.tol)}" - else: - strpar3 = f"niterback = {self.niterback}\t\ttol = {str(self.tol)}" + strpar3 = f"niter = {self.niter}\t\tniterback = {self.niterback}\t\ttol = {str(self.tol)}" print(strpar) print(strpar1) print(strpar2) @@ -542,7 +533,6 @@ def setup( # type: ignore[override] """ self.proxf = proxf self.proxg = proxg - self.x0 = x0 self.backtracking = backtracking self.beta = beta self.eta = eta @@ -998,7 +988,6 @@ def setup( # type: ignore[override] """ self.proxf = proxf self.proxg = proxg - self.x0 = x0 self.epsr = epsr self.safeguard = safeguard self.nhistory = nhistory @@ -1426,7 +1415,6 @@ def setup( # type: ignore[override] """ self.proxfs = proxfs self.proxgs = proxgs - self.x0 = x0 self.tau = tau self.eta = eta self.niter = niter @@ -4510,13 +4498,13 @@ def run( ---------- x : :obj:`numpy.ndarray` Current model vector to be updated by multiple steps of - the PPXA algorithm + the ConsensusADMM algorithm x_bar : :obj:`numpy.ndarray` Current averaged model vector to be updated by multiple steps of - the PPXA algorithm + the ConsensusADMM algorithm y : :obj:`numpy.ndarray` Additional model vector to be updated by multiple steps of - the PPXA algorithm + the ConsensusADMM algorithm niter : :obj:`int`, optional Number of iterations. Can be set to ``None`` if already provided in the setup call From 48c122531d64b0b052d95e706050b606977a9eae Mon Sep 17 00:00:00 2001 From: mrava87 Date: Tue, 30 Jun 2026 22:02:44 +0100 Subject: [PATCH 37/73] doc: added class-based solvers to api doc --- docs/source/api/index.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index a80ca4ca..c22ae98b 100755 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -155,6 +155,32 @@ Solvers Primal ^^^^^^ +Class-based +~~~~~~~~~~~ + +.. currentmodule:: pyproximal.optimization.cls_primal + +.. autosummary:: + :toctree: generated/ + + AcceleratedProximalGradient + ADMM + ADMML2 + AndersonProximalGradient + GeneralizedProximalGradient + HQS + LinearizedADMM + ProximalGradient + ProximalPoint + TwIST + DouglasRachfordSplitting + PPXA + ConsensusADMM + + +Function-based +~~~~~~~~~~~~~~ + .. currentmodule:: pyproximal.optimization.primal .. autosummary:: From 28109eb043ca51f5a9d33a6d02dfb776c9b04a96 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Tue, 30 Jun 2026 22:12:04 +0100 Subject: [PATCH 38/73] feat: added primaldual class-based solvers --- pyproximal/optimization/__init__.py | 3 +- pyproximal/optimization/cls_primal.py | 24 +- pyproximal/optimization/cls_primaldual.py | 941 ++++++++++++++++++++++ 3 files changed, 955 insertions(+), 13 deletions(-) create mode 100644 pyproximal/optimization/cls_primaldual.py diff --git a/pyproximal/optimization/__init__.py b/pyproximal/optimization/__init__.py index 7308842d..01d6c4cd 100644 --- a/pyproximal/optimization/__init__.py +++ b/pyproximal/optimization/__init__.py @@ -54,4 +54,5 @@ """ from . import cls_primal, primal -from . import primaldual, bregman, segmentation, sr3, palm, pnp, red +from . import cls_primaldual, primaldual +from . import bregman, segmentation, sr3, palm, pnp, red diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index fce9f84e..5f20504a 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -352,7 +352,7 @@ def solve( # type: ignore[override] Estimated model iiter : :obj:`int` Number of executed iterations - cost : :obj:`list` + cost : :obj:`numpy.ndarray` History of the objective function """ @@ -819,7 +819,7 @@ def solve( # type: ignore[override] Additional estimated model iiter : :obj:`int` Number of executed iterations - cost : :obj:`list` + cost : :obj:`numpy.ndarray` History of the objective function """ @@ -1260,7 +1260,7 @@ def solve( # type: ignore[override] Additional estimated model iiter : :obj:`int` Number of executed iterations - cost : :obj:`list` + cost : :obj:`numpy.ndarray` History of the objective function """ @@ -1668,7 +1668,7 @@ def solve( # type: ignore[override] Additional estimated model iiter : :obj:`int` Number of executed iterations - cost : :obj:`list` + cost : :obj:`numpy.ndarray` History of the objective function """ @@ -2047,7 +2047,7 @@ def solve( # type: ignore[override] Additional estimated model iiter : :obj:`int` Number of executed iterations - cost : :obj:`list` + cost : :obj:`numpy.ndarray` History of the objective function """ @@ -2415,7 +2415,7 @@ def solve( # type: ignore[override] Additional estimated model iiter : :obj:`int` Number of executed iterations - cost : :obj:`list` + cost : :obj:`numpy.ndarray` History of the objective function """ @@ -2826,7 +2826,7 @@ def solve( # type: ignore[override] Additional estimated model iiter : :obj:`int` Number of executed iterations - cost : :obj:`list` + cost : :obj:`numpy.ndarray` History of the objective function """ @@ -3184,7 +3184,7 @@ def solve( # type: ignore[override] Additional estimated model iiter : :obj:`int` Number of executed iterations - cost : :obj:`list` + cost : :obj:`numpy.ndarray` History of the objective function """ @@ -3545,7 +3545,7 @@ def solve( # type: ignore[override] Estimated model iiter : :obj:`int` Number of executed iterations - cost : :obj:`list` + cost : :obj:`numpy.ndarray` History of the objective function """ @@ -3899,7 +3899,7 @@ def solve( # type: ignore[override] Additional estimated model iiter : :obj:`int` Number of executed iterations - cost : :obj:`list` + cost : :obj:`numpy.ndarray` History of the objective function """ @@ -4259,7 +4259,7 @@ def solve( # type: ignore[override] Additional estimated model(s) iiter : :obj:`int` Number of executed iterations - cost : :obj:`list` + cost : :obj:`numpy.ndarray` History of the objective function """ @@ -4590,7 +4590,7 @@ def solve( # type: ignore[override] Additional estimated model iiter : :obj:`int` Number of executed iterations - cost : :obj:`list` + cost : :obj:`numpy.ndarray` History of the objective function """ diff --git a/pyproximal/optimization/cls_primaldual.py b/pyproximal/optimization/cls_primaldual.py new file mode 100644 index 00000000..1115b9f7 --- /dev/null +++ b/pyproximal/optimization/cls_primaldual.py @@ -0,0 +1,941 @@ +__all__ = [ + "PrimalDual", + "AdaptivePrimalDual", +] + +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import numpy as np +import pylops +from pylops.optimization.callback import Callbacks +from pylops.utils.backend import get_array_module +from pylops.utils.typing import NDArray + +from pyproximal.optimization.basesolver import Solver +from pyproximal.ProxOperator import ProxOperator + +if TYPE_CHECKING: + from pylops.linearoperator import LinearOperator + + +# need to check pylops version since _callback_stop +# is only available in pylops>=2.6.0 +sp_version = pylops.__version__.split(".") +if int(sp_version[0]) < 2 or (int(sp_version[0]) == 2 and int(sp_version[1]) < 6): + + def _callback_stop(callbacks: Sequence[Callbacks]) -> bool: + return False +else: + from pylops.optimization.callback import _callback_stop # type: ignore[no-redef] + + +class PrimalDual(Solver): + r"""Primal-dual algorithm + + Solves the following (possibly) nonlinear minimization problem using + the general version of the first-order primal-dual algorithm of [1]_: + + .. math:: + + \min_{\mathbf{x} \in X} g(\mathbf{Ax}) + f(\mathbf{x}) + + \mathbf{z}^T \mathbf{x} + + where :math:`\mathbf{A}` is a linear operator, :math:`f` + and :math:`g` can be any convex functions that have a known proximal + operator. + + This functional is effectively minimized by solving its equivalent + primal-dual problem (primal in :math:`f`, dual in :math:`g`): + + .. math:: + + \min_{\mathbf{x} \in X} \max_{\mathbf{y} \in Y} + \mathbf{y}^T(\mathbf{Ax}) + \mathbf{z}^T \mathbf{x} + + f(\mathbf{x}) - g^*(\mathbf{y}) + + where :math:`\mathbf{y}` is the so-called dual variable. + + Notes + ----- + The Primal-dual algorithm can be expressed by the following recursion + (``gfirst=True``): + + .. math:: + + \mathbf{y}^{k+1} = \prox_{\mu g^*}(\mathbf{y}^{k} + + \mu \mathbf{A}\bar{\mathbf{x}}^{k})\\ + \mathbf{x}^{k+1} = \prox_{\tau f}(\mathbf{x}^{k} - + \tau (\mathbf{A}^H \mathbf{y}^{k+1} + \mathbf{z})) \\ + \bar{\mathbf{x}}^{k+1} = \mathbf{x}^{k+1} + + \theta (\mathbf{x}^{k+1} - \mathbf{x}^k) + + where :math:`\tau \mu \lambda_{max}(\mathbf{A}^H\mathbf{A}) < 1`. + + Alternatively for ``gfirst=False`` the scheme becomes: + + .. math:: + + \mathbf{x}^{k+1} = \prox_{\tau f}(\mathbf{x}^{k} - + \tau (\mathbf{A}^H \mathbf{y}^{k} + \mathbf{z})) \\ + \bar{\mathbf{x}}^{k+1} = \mathbf{x}^{k+1} + + \theta (\mathbf{x}^{k+1} - \mathbf{x}^k) \\ + \mathbf{y}^{k+1} = \prox_{\mu g^*}(\mathbf{y}^{k} + + \mu \mathbf{A}\bar{\mathbf{x}}^{k+1}) + + .. [1] A., Chambolle, and T., Pock, "A first-order primal-dual algorithm for + convex problems with applications to imaging", Journal of Mathematical + Imaging and Vision, 40, 8pp. 120-145. 2011. + + """ + + def _print_setup( + self, tau_print: str, mu_print: str, xcomplex: bool = False + ) -> None: + self._print_solver(nbar=85) + + strpar = ( + f"Proximal operator (f): {type(self.proxf).__name__}\n" + f"Proximal operator (g): {type(self.proxg).__name__}\n" + f"Linear operator (A): {type(self.A).__name__}\n" + f"Additional vector (z): {None if self.z is None else 'vector'}\n" + ) + strpar1 = f"tau = {tau_print}\tmu = {mu_print}\ttheta = {self.theta:6e}" + strpar2 = f"tol = {str(self.tol)}\t\tniter = {self.niter}" + print(strpar) + print(strpar1) + print(strpar2) + print("-" * 85 + "\n") + if not xcomplex: + head1 = " Itn x[0] f g z^x J=f+g+z^x" + else: + head1 = " Itn x[0] f g z^x J=f+g+z^x" + print(head1) + + def _print_step(self, x: NDArray) -> None: + if self.tol is None: + self.pf = self.proxf(x) + self.pg = self.proxg(self.A.matvec(x)) + self.zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) + pf = 0.0 if isinstance(self.pf, bool) else self.pf + pg = 0.0 if isinstance(self.pg, bool) else self.pg + self.pfg = pf + pg + self.zx + strx = f"{x[0]:1.2e} " if np.iscomplexobj(x) else f"{x[0]:11.4e} " + msg = ( + f"{self.iiter:6g} " + + strx + + f" {self.pf:11.4e} " + + f"{self.pg:11.4e} " + + f" {self.zx:11.4e} " + + f" {self.pfg:11.4e} " + ) + print(msg) + + def setup( # type: ignore[override] + self, + proxf: "ProxOperator", + proxg: "ProxOperator", + A: "LinearOperator", + x0: NDArray, + tau: float | NDArray, + mu: float | NDArray, + y0: NDArray | None = None, + z: NDArray | None = None, + theta: float = 1.0, + gfirst: bool = True, + niter: int | None = None, + tol: float | None = None, + callbacky: bool = False, + show: bool = False, + ) -> tuple[NDArray, NDArray, NDArray]: + r"""Setup solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + A : :obj:`pylops.LinearOperator` + Linear operator of g + x0 : :obj:`numpy.ndarray` + Initial vector + tau : :obj:`float` or :obj:`numpy.ndarray` + Stepsize of subgradient of :math:`f`. This can be constant + or function of iterations (in the latter cases provided as np.ndarray) + mu : :obj:`float` or :obj:`numpy.ndarray` + Stepsize of subgradient of :math:`g^*`. This can be constant + or function of iterations (in the latter cases provided as np.ndarray) + y0 : :obj:`numpy.ndarray` + Initial auxiliary vector. If ``None``, set to zero + z : :obj:`numpy.ndarray`, optional + Additional vector + theta : :obj:`float` + Scalar between 0 and 1 that defines the update of the + :math:`\bar{\mathbf{x}}` variable - note that ``theta=0`` is a + special case that represents the semi-implicit classical Arrow-Hurwicz + algorithm + gfirst : :obj:`bool`, optional + Apply Proximal of operator ``g`` first (``True``) or Proximal of + operator ``f`` first (``False``) + niter : :obj:`int`, optional + Number of iterations of iterative scheme (default to ``None`` + in case a user wants to manually step over the solver) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + callbacky : :obj:`bool`, optional + Modify callback signature to (``callback(x, y)``) when ``callbacky=True`` + show : :obj:`bool`, optional + Display setup log + + Returns + ------- + x : :obj:`numpy.ndarray` + Initial guess of model vector + xhat : :obj:`numpy.ndarray` + Initial guess of extrapolated model vector + y : :obj:`numpy.ndarray` + Initial guess of additional model vector + + """ + self.proxf = proxf + self.proxg = proxg + self.A = A + self.z = z + self.theta = theta + self.gfirst = gfirst + self.niter = niter + self.tol = tol + self.callbacky = callbacky + + self.ncp = get_array_module(x0) + + # check if tau is a vector + self.tau = self.ncp.asarray(tau, dtype=np.float32) + if self.tau.size == 1: + if niter is None: + tau_print = str(np.round(self.tau, 6)) + else: + self.tau = self.tau * self.ncp.ones(niter, dtype=np.float32) + tau_print = str(np.round(self.tau[0], 6)) + else: + tau_print = "Variable" + + # check if tau is a vector + self.mu = self.ncp.asarray(mu, dtype=np.float32) + if self.mu.size == 1: + if niter is None: + mu_print = str(np.round(self.mu, 6)) + else: + self.mu = self.mu * self.ncp.ones(niter, dtype=np.float32) + mu_print = str(np.round(self.mu[0], 6)) + else: + mu_print = "Variable" + + # initialize solver + x = x0.copy() + y = ( + y0.copy() + if y0 is not None + else self.ncp.zeros(self.A.shape[0], dtype=x.dtype) + ) + xhat = x.copy() + + # create variables to track the objective function and iterations + pf = self.proxf(x) + pg = self.proxg(self.A.matvec(x)) + zx = 0.0 if z is None else self.ncp.dot(self.z, x) + pfg = pf + pg + zx + self.pfg, self.pfgold = pfg, pfg + self.cost: list[float] = [] + self.cost.append(float(self.pfg)) + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(tau_print, mu_print, np.iscomplexobj(x0)) + return x, xhat, y + + def step( + self, + x: NDArray, + xhat: NDArray, + y: NDArray, + show: bool = False, + ) -> tuple[NDArray, NDArray, NDArray]: + r"""Run one step of solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by a step of the + Primal-dual algorithm + xhat : :obj:`numpy.ndarray` + Current extrapolated model vector to be updated by a step of the + Primal-dual algorithm + y : :obj:`numpy.ndarray` + Additional model vector to be updated by a step of the + Primal-dual algorithm + show : :obj:`bool`, optional + Display iteration log + + Returns + ------- + x : :obj:`numpy.ndarray` + Updated model vector + xhat : :obj:`numpy.ndarray` + Updated extrapolated model vector + y : :obj:`numpy.ndarray` + Updated additional model vector + + """ + xold = x.copy() + + # define tau for current iteration + if self.tau.ndim == 0: + tau = self.tau + else: + tau = self.tau[self.iiter] + + # define mu for current iteration + if self.mu.ndim == 0: + mu = self.mu + else: + mu = self.mu[self.iiter] + + if self.gfirst: + y = self.proxg.proxdual(y + mu * self.A.matvec(xhat), mu) + ATy = self.A.rmatvec(y) + if self.z is not None: + ATy += self.z + x = self.proxf.prox(x - tau * ATy, tau) + xhat = x + self.theta * (x - xold) + else: + ATy = self.A.rmatvec(y) + if self.z is not None: + ATy += self.z + x = self.proxf.prox(x - tau * ATy, tau) + xhat = x + self.theta * (x - xold) + y = self.proxg.proxdual(y + mu * self.A.matvec(xhat), mu) + + # tolerance check: break iterations if overall + # objective does not decrease below tolerance + if self.tol is not None: + self.pfgold = self.pfg + self.pf = self.proxf(x) + self.pg = self.proxg(self.A.matvec(x)) + self.zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) + self.pfg = self.pf + self.pg + self.zx + if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + self.tolbreak = True + + self.iiter += 1 + if show: + self._print_step(x) + if self.tol is not None or show: + self.cost.append(float(self.pfg)) + return x, xhat, y + + def run( + self, + x: NDArray, + xhat: NDArray, + y: NDArray, + niter: int | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray, NDArray]: + r"""Run solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by multiple steps of + the Primal-dual algorithm + xhat : :obj:`numpy.ndarray` + Current extrapolated model vector to be updated by multiple steps of + the Primal-dual algorithm + y : :obj:`numpy.ndarray` + Current additional model vector to be updated by multiple steps of + the Primal-dual algorithm + niter : :obj:`int`, optional + Number of iterations. Can be set to ``None`` if already + provided in the setup call + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + xhat : :obj:`numpy.ndarray` + Estimated extrapolated model + y : :obj:`numpy.ndarray` + Estimated additional model + + """ + niter = self.niter if niter is None else niter + if niter is None: + msg = "`niter` must not be None" + raise ValueError(msg) + while self.iiter < niter and not self.tolbreak: + showstep = ( + True + if show + and ( + self.iiter < itershow[0] + or niter - self.iiter < itershow[1] + or self.iiter % itershow[2] == 0 + ) + else False + ) + x, xhat, y = self.step(x, xhat, y, showstep) + if self.callbacky: + self.callback(x, y) + else: + self.callback(x) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x, xhat, y + + def solve( # type: ignore[override] + self, + proxf: "ProxOperator", + proxg: "ProxOperator", + A: "LinearOperator", + x0: NDArray, + tau: float | NDArray, + mu: float | NDArray, + y0: NDArray | None = None, + z: NDArray | None = None, + theta: float = 1.0, + gfirst: bool = True, + niter: int = 10, + tol: float | None = None, + callbacky: bool = False, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray, NDArray, int, NDArray]: + r"""Run entire solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + A : :obj:`pylops.LinearOperator` + Linear operator of g + x0 : :obj:`numpy.ndarray` + Initial vector + tau : :obj:`float` or :obj:`numpy.ndarray` + Stepsize of subgradient of :math:`f`. This can be constant + or function of iterations (in the latter cases provided as np.ndarray) + mu : :obj:`float` or :obj:`numpy.ndarray` + Stepsize of subgradient of :math:`g^*`. This can be constant + or function of iterations (in the latter cases provided as np.ndarray) + y0 : :obj:`numpy.ndarray` + Initial auxiliary vector. If ``None``, set to zero + z : :obj:`numpy.ndarray`, optional + Additional vector + theta : :obj:`float` + Scalar between 0 and 1 that defines the update of the + :math:`\bar{\mathbf{x}}` variable - note that ``theta=0`` is a + special case that represents the semi-implicit classical Arrow-Hurwicz + algorithm + gfirst : :obj:`bool`, optional + Apply Proximal of operator ``g`` first (``True``) or Proximal of + operator ``f`` first (``False``) + niter : :obj:`int`, optional + Number of iterations of iterative scheme + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + callbacky : :obj:`bool`, optional + Modify callback signature to (``callback(x, y)``) when ``callbacky=True`` + show : :obj:`bool`, optional + Display setup log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + xhat : :obj:`numpy.ndarray` + Estimated extrapolated model + y : :obj:`numpy.ndarray` + Estimated additional model + iiter : :obj:`int` + Number of executed iterations + cost : :obj:`numpy.ndarray` + History of the objective function + + """ + x, xhat, y = self.setup( + proxf=proxf, + proxg=proxg, + A=A, + x0=x0, + tau=tau, + mu=mu, + y0=y0, + z=z, + theta=theta, + gfirst=gfirst, + niter=niter, + tol=tol, + callbacky=callbacky, + show=show, + ) + x, xhat, y = self.run(x, xhat, y, niter, show=show, itershow=itershow) + self.finalize(85, show) + return x, xhat, y, self.iiter, self.cost + + +class AdaptivePrimalDual(Solver): + r"""Adaptive Primal-dual algorithm + + Solves the minimization problem in + :func:`pyproximal.optimization.primaldual.PrimalDual` + using an adaptive version of the first-order primal-dual algorithm of [1]_. + The main advantage of this method is that step sizes :math:`\tau` and + :math:`\mu` are changing through iterations, improving the overall speed + of convergence of the algorithm. + + Notes + ----- + The Adative Primal-dual algorithm shares the the same iterations of the + original :func:`pyproximal.optimization.cls_primaldual.PrimalDual` solver. + The main difference lies in the fact that the step sizes ``tau`` and ``mu`` + are adaptively changed at each iteration leading to faster converge. + + Changes are applied by tracking the norm of the primal and dual + residuals. When their mutual ratio increases beyond a certain treshold + ``delta`` the step lenghts are updated to balance the minimization and + maximization part of the overall optimization process. + + .. [1] T., Goldstein, M., Li, X., Yuan, E., Esser, R., Baraniuk, "Adaptive + Primal-Dual Hybrid Gradient Methods for Saddle-Point Problems", + ArXiv, 2013. + + """ + + def _print_setup(self, xcomplex: bool = False) -> None: + self._print_solver(nbar=85) + + strpar = ( + f"Proximal operator (f): {type(self.proxf).__name__}\n" + f"Proximal operator (g): {type(self.proxg).__name__}\n" + f"Linear operator (A): {type(self.A).__name__}\n" + f"Additional vector (z): {None if self.z is None else 'vector'}\n" + ) + strpar1 = f"tau0 = {self.tau}\tmu0 = {self.mu}" + strpar2 = f"alpha0 = {str(self.alpha)}\t\teta0 = {self.niter}" + strpar3 = f"s = {str(self.s)}\t\tdelta = {self.delta}" + strpar4 = f"tol = {str(self.tol)}\t\tniter = {self.niter}" + print(strpar) + print(strpar1) + print(strpar2) + print(strpar3) + print(strpar4) + print("-" * 85 + "\n") + if not xcomplex: + head1 = " Itn x[0] f g z^x J=f+g+z^x" + else: + head1 = " Itn x[0] f g z^x J=f+g+z^x" + print(head1) + + def _print_step(self, x: NDArray) -> None: + if self.tol is None: + self.pf = self.proxf(x) + self.pg = self.proxg(self.A.matvec(x)) + self.zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) + pf = 0.0 if isinstance(self.pf, bool) else self.pf + pg = 0.0 if isinstance(self.pg, bool) else self.pg + self.pfg = pf + pg + self.zx + strx = f"{x[0]:1.2e} " if np.iscomplexobj(x) else f"{x[0]:11.4e} " + msg = ( + f"{self.iiter:6g} " + + strx + + f" {self.pf:11.4e} " + + f"{self.pg:11.4e} " + + f" {self.zx:11.4e} " + + f" {self.pfg:11.4e} " + ) + print(msg) + + def setup( # type: ignore[override] + self, + proxf: "ProxOperator", + proxg: "ProxOperator", + A: "LinearOperator", + x0: NDArray, + tau: float, + mu: float, + alpha: float = 0.5, + eta: float = 0.95, + s: float = 1.0, + delta: float = 1.5, + z: NDArray | None = None, + niter: int | None = None, + tol: float | None = None, + show: bool = False, + ) -> tuple[NDArray, NDArray]: + r"""Setup solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + A : :obj:`pylops.LinearOperator` + Linear operator of g + x0 : :obj:`numpy.ndarray` + Initial vector + tau : :obj:`float` + Stepsize of subgradient of :math:`f` + mu : :obj:`float` + Stepsize of subgradient of :math:`g^*` + alpha : :obj:`float`, optional + Initial adaptivity level (must be between 0 and 1) + eta : :obj:`float`, optional + Scaling of adaptivity level to be multipled to the current alpha every + time the norm of the two residuals start to diverge (must be between + 0 and 1) + s : :obj:`float`, optional + Scaling of residual balancing principle + delta : :obj:`float`, optional + Balancing factor. Step sizes are updated only when their ratio exceeds + this value. + z : :obj:`numpy.ndarray`, optional + Additional vector + niter : :obj:`int`, optional + Number of iterations of iterative scheme (default to ``None`` + in case a user wants to manually step over the solver) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display setup log + + Returns + ------- + x : :obj:`numpy.ndarray` + Initial guess + + """ + self.proxf = proxf + self.proxg = proxg + self.A = A + self.tau = tau + self.mu = mu + self.alpha = alpha + self.eta = eta + self.s = s + self.delta = delta + self.z = z + self.niter = niter + self.tol = tol + + self.ncp = get_array_module(x0) + + # initialize solver + x = x0.copy() + y = self.ncp.zeros(self.A.shape[0], dtype=x.dtype) + + # initialize additional variables + self.Ax = self.ncp.zeros(self.A.shape[0], dtype=x.dtype) + self.ATy = self.ncp.zeros(self.A.shape[1], dtype=x.dtype) + + # initialize arrays to store history of parameters + self.taus = [] + self.mus = [] + self.alphas = [] + self.taus.append(tau) + self.mus.append(mu) + self.alphas.append(alpha) + if self.tol is None: + self.p = self.d = None + else: + self.p = self.d = 0.0 + + # create variables to track the objective function and iterations + pf = self.proxf(x) + pg = self.proxg(self.A.matvec(x)) + zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) + pfg = pf + pg + zx + self.pfg, self.pfgold = pfg, pfg + self.cost: list[float] = [] + self.cost.append(float(self.pfg)) + self.tolbreak = False + self.iiter = 0 + + # print setup + if show: + self._print_setup(np.iscomplexobj(x0)) + return x, y + + def step( + self, + x: NDArray, + y: NDArray, + show: bool = False, + ) -> tuple[NDArray, NDArray]: + r"""Run one step of solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by a step of the + Adaptive Primal-dual algorithm + y : :obj:`numpy.ndarray` + Additional model vector to be updated by a step of the + Adaptive Primal-dual algorithm + show : :obj:`bool`, optional + Display iteration log + + Returns + ------- + x : :obj:`numpy.ndarray` + Updated model vector + y : :obj:`numpy.ndarray` + Updated additional model vector + + """ + # store old values + xold = x.copy() + yold = y.copy() + Axold = self.Ax.copy() + ATyold = self.ATy.copy() + + # proxf + if self.z is not None: + self.ATy += self.z + x = self.proxf.prox(x - self.tau * self.ATy, self.tau) + self.Ax = self.A.matvec(x) + Axhat = 2 * self.Ax - Axold + + # proxg + y = self.proxg.proxdual(y + self.mu * Axhat, self.mu) + self.ATy = self.A.rmatvec(y) + + # update steps + if self.z is not None: + self.p = float( + np.linalg.norm( + (xold - x) / self.tau + - (ATyold - self.ATy) + - self.A.rmatvec(self.z) + + self.z + ) + ) + else: + self.p = float(np.linalg.norm((xold - x) / self.tau - (ATyold - self.ATy))) + self.d = float(np.linalg.norm((yold - y) / self.mu - (Axold - self.Ax))) + + if self.p > self.s * self.d * self.delta: + self.tau /= 1 - self.alpha + self.mu *= 1 - self.alpha + self.alpha *= self.eta + elif self.p < self.s * self.d / self.delta: + self.tau *= 1 - self.alpha + self.mu /= 1 - self.alpha + self.alpha *= self.eta + + # save history of steps + self.taus.append(self.tau) + self.mus.append(self.mu) + self.alphas.append(self.alpha) + + # tolerance check: break iterations if overall + # objective or x/y updates do not decrease + # below tolerance + if self.tol is not None: + self.pfgold = self.pfg + self.pf = self.proxf(x) + self.pg = self.proxg(self.A.matvec(x)) + self.zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) + self.pfg = self.pf + self.pg + self.zx + if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + self.tolbreak = True + + if self.p <= self.tol or self.d <= self.tol: + self.tolbreak = True + + self.iiter += 1 + if show: + self._print_step(x) + if self.tol is not None or show: + self.cost.append(float(self.pfg)) + return x, y + + def run( + self, + x: NDArray, + y: NDArray, + niter: int | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> NDArray: + r"""Run solver + + Parameters + ---------- + x : :obj:`numpy.ndarray` + Current model vector to be updated by multiple steps of + the Adaptive Primal-dual algorithm + y : :obj:`numpy.ndarray` + Current additional model vector to be updated by multiple steps of + the Adaptive Primal-dual algorithm + niter : :obj:`int`, optional + Number of iterations. Can be set to ``None`` if already + provided in the setup call + show : :obj:`bool`, optional + Display logs + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + y : :obj:`numpy.ndarray` + Estimated additional model + + """ + niter = self.niter if niter is None else niter + if niter is None: + msg = "`niter` must not be None" + raise ValueError(msg) + while self.iiter < niter and not self.tolbreak: + showstep = ( + True + if show + and ( + self.iiter < itershow[0] + or niter - self.iiter < itershow[1] + or self.iiter % itershow[2] == 0 + ) + else False + ) + x, y = self.step(x, y, showstep) + self.callback(x) + # check if any callback has raised a stop flag + stop = _callback_stop(self.callbacks) + if stop: + break + return x, y + + def solve( # type: ignore[override] + self, + proxf: "ProxOperator", + proxg: "ProxOperator", + A: "LinearOperator", + x0: NDArray, + tau: float, + mu: float, + alpha: float = 0.5, + eta: float = 0.95, + s: float = 1.0, + delta: float = 1.5, + z: NDArray | None = None, + niter: int | None = None, + tol: float | None = None, + show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), + ) -> tuple[NDArray, NDArray, int, NDArray, tuple[NDArray, NDArray, NDArray]]: + r"""Run entire solver + + Parameters + ---------- + proxf : :obj:`pyproximal.ProxOperator` + Proximal operator of f function + proxg : :obj:`pyproximal.ProxOperator` + Proximal operator of g function + A : :obj:`pylops.LinearOperator` + Linear operator of g + x0 : :obj:`numpy.ndarray` + Initial vector + tau : :obj:`float` + Stepsize of subgradient of :math:`f` + mu : :obj:`float` + Stepsize of subgradient of :math:`g^*` + alpha : :obj:`float`, optional + Initial adaptivity level (must be between 0 and 1) + eta : :obj:`float`, optional + Scaling of adaptivity level to be multipled to the current alpha every + time the norm of the two residuals start to diverge (must be between + 0 and 1) + s : :obj:`float`, optional + Scaling of residual balancing principle + delta : :obj:`float`, optional + Balancing factor. Step sizes are updated only when their ratio exceeds + this value. + z : :obj:`numpy.ndarray`, optional + Additional vector + niter : :obj:`int`, optional + Number of iterations of iterative scheme (default to ``None`` + in case a user wants to manually step over the solver) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + show : :obj:`bool`, optional + Display setup log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. + + Returns + ------- + x : :obj:`numpy.ndarray` + Estimated model + y : :obj:`numpy.ndarray` + Estimated additional model + iiter : :obj:`int` + Number of executed iterations + cost : :obj:`numpy.ndarray` + History of the objective function + steps : :obj:`tuple` + Tau, mu and alpha evolution through iterations + + """ + x, y = self.setup( + proxf=proxf, + proxg=proxg, + A=A, + x0=x0, + tau=tau, + mu=mu, + alpha=alpha, + eta=eta, + s=s, + delta=delta, + z=z, + niter=niter, + tol=tol, + show=show, + ) + x, y = self.run(x, y, niter, show=show, itershow=itershow) + steps = ( + np.array(self.taus), + np.array(self.mus), + np.array(self.alphas), + ) + + self.finalize(85, show) + return x, y, self.iiter, self.cost, steps From 2a45f00583518d92f73e0db2e1ed9304a3bcc20f Mon Sep 17 00:00:00 2001 From: mrava87 Date: Tue, 30 Jun 2026 22:33:53 +0100 Subject: [PATCH 39/73] test: added tests for primaldual solvers --- pytests/test_solver.py | 124 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/pytests/test_solver.py b/pytests/test_solver.py index 0eb354ca..625c0c90 100644 --- a/pytests/test_solver.py +++ b/pytests/test_solver.py @@ -1,7 +1,7 @@ import numpy as np import pytest from numpy.testing import assert_array_almost_equal -from pylops.basicoperators import Identity, MatrixMult +from pylops.basicoperators import FirstDerivative, Identity, MatrixMult from pylops.optimization.leastsquares import regularized_inversion from pylops.optimization.sparsity import fista, ista @@ -20,6 +20,7 @@ ProximalPoint, TwIST, ) +from pyproximal.optimization.primaldual import AdaptivePrimalDual, PrimalDual from pyproximal.proximal import L1, L2, Box, Quadratic par1 = {"n": 10, "m": 10, "dtype": "float64"} # square, float64 @@ -952,3 +953,124 @@ def test_ConsensusADMM_with_GPG(par) -> None: ) assert_array_almost_equal(xppxa, xgpg, decimal=2) + + +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) +@pytest.mark.parametrize("gfirst", [False, True]) +def test_ADMML2_PrimalDual(par, gfirst): + """Check equivalency of ADMML2 and Primal-Dual + (note that despite the trajectory will be different, they + should converge to the same solution) + """ + np.random.seed(0) + n, m = par["n"], par["m"] + + # Define sparse model + x = np.zeros(m, dtype=par["dtype"]) + x[m // 2 :] = 1.0 + + # Random mixing matrix + R = np.random.normal(0.0, 1.0, (n, m)).astype(par["dtype"]) + Rop = MatrixMult(R, dtype=par["dtype"]) + + y = Rop @ x + + # Step size + Dop = FirstDerivative(m) + L = 4.0 # Lipshitz constant of Dop + eps = 1e-1 + + # ADMML2 + l1 = L1(sigma=eps) + tau = 0.99 / L + xadmml2 = ADMML2( + l1, + Rop, + y, + Dop, + x0=np.zeros(m), + tau=tau, + gfirst=gfirst, + niter=200, + iter_lim=10, + show=True, + )[0] + + # PD + l2 = L2(Rop, y, niter=10, warm=True) + l1 = L1(sigma=eps) + tau = 0.99 / np.sqrt(L) + mu = 0.99 / np.sqrt(L) + xpd = PrimalDual( + l2, + l1, + Dop, + x0=np.zeros(m), + tau=tau, + mu=mu, + gfirst=gfirst, + niter=200, + show=True, + ) + + assert_array_almost_equal(xadmml2, xpd, decimal=2) + + +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) +def test_PrimalDual_AdaptivePrimalDual(par): + """Check equivalency of Primal-Dual and + Adaptive Primal-Dual (note that despite the + trajectory will be different, they should + converge to the same solution) + """ + np.random.seed(0) + n, m = par["n"], par["m"] + + # Define sparse model + x = np.zeros(m, dtype=par["dtype"]) + x[m // 2 :] = 1.0 + + # Random mixing matrix + R = np.random.normal(0.0, 1.0, (n, m)).astype(par["dtype"]) + Rop = MatrixMult(R, dtype=par["dtype"]) + + y = Rop @ x + + # Step size + Dop = FirstDerivative(m) + L = 4.0 # Lipshitz constant of Dop + eps = 1e-1 + + # PD + l2 = L2(Rop, y, niter=10, warm=True) + l1 = L1(sigma=eps) + tau = 0.99 / np.sqrt(L) + mu = 0.99 / np.sqrt(L) + xpd = PrimalDual( + l2, + l1, + Dop, + x0=np.zeros(m), + tau=tau, + mu=mu, + niter=200, + show=True, + ) + + # Adaptive PD + l2 = L2(Rop, y, niter=10, warm=True) + l1 = L1(sigma=eps) + tau = 0.99 / np.sqrt(L) + mu = 0.99 / np.sqrt(L) + xapd = AdaptivePrimalDual( + l2, + l1, + Dop, + x0=np.zeros(m), + tau=tau, + mu=mu, + niter=200, + show=True, + )[0] + + assert_array_almost_equal(xpd, xapd, decimal=2) From 30c68b701c47d073d7e0445b0f4fa314d7f5076f Mon Sep 17 00:00:00 2001 From: mrava87 Date: Tue, 30 Jun 2026 22:59:36 +0100 Subject: [PATCH 40/73] feat: modified function-based primaldual solvers to use class-based --- docs/source/api/index.rst | 54 ++-- pyproximal/optimization/primaldual.py | 357 ++++++++------------------ 2 files changed, 146 insertions(+), 265 deletions(-) diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index c22ae98b..b6167bc5 100755 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -177,7 +177,6 @@ Class-based PPXA ConsensusADMM - Function-based ~~~~~~~~~~~~~~ @@ -200,56 +199,71 @@ Function-based PPXA ConsensusADMM -.. currentmodule:: pyproximal.optimization.palm + +Primal-dual +^^^^^^^^^^^ + +Class-based +~~~~~~~~~~~ + +.. currentmodule:: pyproximal.optimization.cls_primaldual .. autosummary:: :toctree: generated/ - PALM - iPALM + AdaptivePrimalDual + PrimalDual -.. currentmodule:: pyproximal.optimization.pnp +Function-based +~~~~~~~~~~~~~~ + +.. currentmodule:: pyproximal.optimization.primaldual .. autosummary:: :toctree: generated/ - PlugAndPlay + AdaptivePrimalDual + PrimalDual -.. currentmodule:: pyproximal.optimization.red + +Other +^^^^^ + +.. currentmodule:: pyproximal.optimization.bregman .. autosummary:: :toctree: generated/ - RED + Bregman -.. currentmodule:: pyproximal.optimization.sr3 +.. currentmodule:: pyproximal.optimization.palm .. autosummary:: :toctree: generated/ - SR3 + PALM + iPALM +.. currentmodule:: pyproximal.optimization.pnp -Primal-dual -^^^^^^^^^^^ +.. autosummary:: + :toctree: generated/ -.. currentmodule:: pyproximal.optimization.primaldual + PlugAndPlay + +.. currentmodule:: pyproximal.optimization.red .. autosummary:: :toctree: generated/ - AdaptivePrimalDual - PrimalDual - -Other -^^^^^ + RED -.. currentmodule:: pyproximal.optimization.bregman +.. currentmodule:: pyproximal.optimization.sr3 .. autosummary:: :toctree: generated/ - Bregman + SR3 .. currentmodule:: pyproximal.optimization.segmentation diff --git a/pyproximal/optimization/primaldual.py b/pyproximal/optimization/primaldual.py index d98491bb..4391b2eb 100644 --- a/pyproximal/optimization/primaldual.py +++ b/pyproximal/optimization/primaldual.py @@ -1,20 +1,28 @@ -import time +__all__ = [ + "PrimalDual", + "AdaptivePrimalDual", +] + from collections.abc import Callable from typing import TYPE_CHECKING -import numpy as np -from pylops.utils.backend import get_array_module, to_numpy +from pylops.optimization.callback import CostNanInfCallback, CostToInitialCallback from pylops.utils.typing import NDArray -from pyproximal.ProxOperator import ProxOperator +from pyproximal.optimization.cls_primaldual import ( + AdaptivePrimalDual as cAdaptivePrimalDual, +) +from pyproximal.optimization.cls_primaldual import PrimalDual as cPrimalDual if TYPE_CHECKING: from pylops.linearoperator import LinearOperator + from pyproximal.ProxOperator import ProxOperator + def PrimalDual( - proxf: ProxOperator, - proxg: ProxOperator, + proxf: "ProxOperator", + proxg: "ProxOperator", A: "LinearOperator", x0: NDArray, tau: float | NDArray, @@ -24,10 +32,13 @@ def PrimalDual( theta: float = 1.0, niter: int = 10, gfirst: bool = True, + tol: float | None = None, + rtol: float | None = None, callback: Callable[..., None] | None = None, callbacky: bool = False, returny: bool = False, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), ) -> NDArray | tuple[NDArray, NDArray]: r"""Primal-dual algorithm @@ -66,10 +77,12 @@ def PrimalDual( Initial vector tau : :obj:`float` or :obj:`numpy.ndarray` Stepsize of subgradient of :math:`f`. This can be constant - or function of iterations (in the latter cases provided as np.ndarray) + or function of iterations (in the latter cases provided + as numpy.ndarray) mu : :obj:`float` or :obj:`numpy.ndarray` Stepsize of subgradient of :math:`g^*`. This can be constant - or function of iterations (in the latter cases provided as np.ndarray) + or function of iterations (in the latter cases provided as + numpy.ndarray) y0 : :obj:`numpy.ndarray` Initial auxiliary vector. If ``None``, set to zero z : :obj:`numpy.ndarray`, optional @@ -84,6 +97,16 @@ def PrimalDual( gfirst : :obj:`bool`, optional Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached or the other tolerance + criterion is met + rtol : :obj:`float`, optional + Relative tolerance on objective function wrt initial value. Stops + the solver when the ratio of the current objective function to the + initial objective function is below this value. If ``rtol=None``, + run until ``niter`` is reached or the other tolerance criterion is + met callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector @@ -93,6 +116,10 @@ def PrimalDual( Return also ``y`` show : :obj:`bool`, optional Display iterations log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. Returns ------- @@ -103,118 +130,37 @@ def PrimalDual( Notes ----- - The Primal-dual algorithm can be expressed by the following recursion - (``gfirst=True``): - - .. math:: - - \mathbf{y}^{k+1} = \prox_{\mu g^*}(\mathbf{y}^{k} + - \mu \mathbf{A}\bar{\mathbf{x}}^{k})\\ - \mathbf{x}^{k+1} = \prox_{\tau f}(\mathbf{x}^{k} - - \tau (\mathbf{A}^H \mathbf{y}^{k+1} + \mathbf{z})) \\ - \bar{\mathbf{x}}^{k+1} = \mathbf{x}^{k+1} + - \theta (\mathbf{x}^{k+1} - \mathbf{x}^k) - - where :math:`\tau \mu \lambda_{max}(\mathbf{A}^H\mathbf{A}) < 1`. - - Alternatively for ``gfirst=False`` the scheme becomes: - - .. math:: - - \mathbf{x}^{k+1} = \prox_{\tau f}(\mathbf{x}^{k} - - \tau (\mathbf{A}^H \mathbf{y}^{k} + \mathbf{z})) \\ - \bar{\mathbf{x}}^{k+1} = \mathbf{x}^{k+1} + - \theta (\mathbf{x}^{k+1} - \mathbf{x}^k) \\ - \mathbf{y}^{k+1} = \prox_{\mu g^*}(\mathbf{y}^{k} + - \mu \mathbf{A}\bar{\mathbf{x}}^{k+1}) - - .. [1] A., Chambolle, and T., Pock, "A first-order primal-dual algorithm for - convex problems with applications to imaging", Journal of Mathematical - Imaging and Vision, 40, 8pp. 120-145. 2011. + See :class:`pyproximal.optimization.cls_primaldual.PrimalDual` """ - ncp = get_array_module(x0) - - # check if tau and mu are scalars or arrays - fixedtau = fixedmu = False - if isinstance(tau, (int, float)): - tau = tau * ncp.ones(niter, dtype=x0.dtype) - fixedtau = True - if isinstance(mu, (int, float)): - mu = mu * ncp.ones(niter, dtype=x0.dtype) - fixedmu = True - - if show: - tstart = time.time() - print( - "Primal-dual: min_x f(Ax) + x^T z + g(x)\n" - "---------------------------------------------------------\n" - "Proximal operator (f): %s\n" - "Proximal operator (g): %s\n" - "Linear operator (A): %s\n" - "Additional vector (z): %s\n" - "tau = %s\t\tmu = %s\ntheta = %.2f\t\tniter = %d\n" - % ( - type(proxf), - type(proxg), - type(A), - None if z is None else "vector", - str(tau[0]) if fixedtau else "Variable", - str(mu[0]) if fixedmu else "Variable", - theta, - niter, - ) - ) - head = " Itn x[0] f g z^x J = f + g + z^x" - print(head) - - # initialize variables - x = x0.copy() - y = y0.copy() if y0 is not None else ncp.zeros(A.shape[0], dtype=x.dtype) - xhat = x.copy() - - # run iterations - for iiter in range(niter): - xold = x.copy() - if gfirst: - y = proxg.proxdual(y + mu[iiter] * A.matvec(xhat), mu[iiter]) - ATy = A.rmatvec(y) - if z is not None: - ATy += z - x = proxf.prox(x - tau[iiter] * ATy, tau[iiter]) - xhat = x + theta * (x - xold) - else: - ATy = A.rmatvec(y) - if z is not None: - ATy += z - x = proxf.prox(x - tau[iiter] * ATy, tau[iiter]) - xhat = x + theta * (x - xold) - y = proxg.proxdual(y + mu[iiter] * A.matvec(xhat), mu[iiter]) - - # run callback - if callback is not None: - if callbacky: - callback(x, y) - else: - callback(x) - if show: - if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: - pf, pg = proxf(x), proxg(A.matvec(x)) - pf = 0.0 if isinstance(pf, bool) else pf - pg = 0.0 if isinstance(pg, bool) else pg - zx = 0.0 if z is None else np.dot(z, x) - msg = "%6g %12.5e %10.3e %10.3e %10.3e %10.3e" % ( - iiter + 1, - np.real(to_numpy(x[0])), - pf, - pg, - zx, - pf + pg + zx, - ) - print(msg) - if show: - print("\nTotal time (s) = %.2f" % (time.time() - tstart)) - print("---------------------------------------------------------\n") + callbacks = [] + if tol is not None or rtol is not None: + callbacks.append(CostNanInfCallback()) + if rtol is not None: + callbacks.append(CostToInitialCallback(rtol)) + + pdsolve = cPrimalDual( + callbacks=callbacks if len(callbacks) > 0 else None, + ) + if callback is not None: + pdsolve.callback = callback + x, _, y, _, _ = pdsolve.solve( + proxf=proxf, + proxg=proxg, + A=A, + x0=x0, + tau=tau, + mu=mu, + y0=y0, + z=z, + theta=theta, + gfirst=gfirst, + niter=niter, + tol=tol or (0.0 if rtol else None), + callbacky=callbacky, + show=show, + itershow=itershow, + ) if not returny: return x else: @@ -222,8 +168,8 @@ def PrimalDual( def AdaptivePrimalDual( - proxf: ProxOperator, - proxg: ProxOperator, + proxf: "ProxOperator", + proxg: "ProxOperator", A: "LinearOperator", x0: NDArray, tau: float, @@ -234,9 +180,11 @@ def AdaptivePrimalDual( delta: float = 1.5, z: NDArray | None = None, niter: int = 10, - tol: float = 1e-10, + tol: float | None = 1e-10, + rtol: float | None = None, callback: Callable[[NDArray], None] | None = None, show: bool = False, + itershow: tuple[int, int, int] = (10, 10, 10), ) -> tuple[NDArray, tuple[NDArray, NDArray, NDArray]]: r"""Adaptive Primal-dual algorithm @@ -276,13 +224,25 @@ def AdaptivePrimalDual( Additional vector niter : :obj:`int`, optional Number of iterations of iterative scheme - tol : :obj:`int`, optional - Tolerance on residual norms + tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached or the other tolerance + criterion is met + rtol : :obj:`float`, optional + Relative tolerance on objective function wrt initial value. Stops + the solver when the ratio of the current objective function to the + initial objective function is below this value. If ``rtol=None``, + run until ``niter`` is reached or the other tolerance criterion is + met callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector show : :obj:`bool`, optional Display iterations log + itershow : :obj:`tuple`, optional + Display set log for the first N1 steps, last N2 steps, + and every N3 steps in between where N1, N2, N3 are the + three element of the list. Returns ------- @@ -293,128 +253,35 @@ def AdaptivePrimalDual( Notes ----- - The Adative Primal-dual algorithm share the the same iterations of the - original :func:`pyproximal.optimization.primaldual.PrimalDual` solver. - The main difference lies in the fact that the step sizes ``tau`` and ``mu`` - are adaptively changed at each iteration leading to faster converge. - - Changes are applied by tracking the norm of the primal and dual - residuals. When their mutual ratio increases beyond a certain treshold - ``delta`` the step lenghts are updated to balance the minimization and - maximization part of the overall optimization process. - - .. [1] T., Goldstein, M., Li, X., Yuan, E., Esser, R., Baraniuk, "Adaptive - Primal-Dual Hybrid Gradient Methods for Saddle-Point Problems", - ArXiv, 2013. + See :class:`pyproximal.optimization.cls_primaldual.AdaptivePrimalDual` """ - if show: - tstart = time.time() - print( - "Adaptive Primal-dual: min_x f(Ax) + x^T z + g(x)\n" - "---------------------------------------------------------\n" - "Proximal operator (f): %s\n" - "Proximal operator (g): %s\n" - "Linear operator (A): %s\n" - "Additional vector (z): %s\n" - "tau0 = %10e\tmu0 = %10e\n" - "alpha0 = %10e\teta = %10e\n" - "s = %10e\tdelta = %10e\n" - "niter = %d\t\ttol = %10e\n" - % ( - type(proxf), - type(proxg), - type(A), - None if z is None else "vector", - tau, - mu, - alpha, - eta, - s, - delta, - niter, - tol, - ) - ) - head = " Itn x[0] f g z^x J = f + g + z^x" - print(head) - - # initialization - x = x0.copy() - y = np.zeros(A.shape[0], dtype=x.dtype) - Ax = np.zeros(A.shape[0], dtype=x.dtype) - ATy = np.zeros(A.shape[1], dtype=x.dtype) - taus = np.zeros(niter + 1) - mus = np.zeros(niter + 1) - alphas = np.zeros(niter + 1) - taus[0], mus[0], alphas[0] = tau, mu, alpha - p = d = tol + 1.0 - - iiter = 0 - while iiter < niter and p > tol and d > tol: - # store old values - xold = x.copy() - yold = y.copy() - Axold = Ax.copy() - ATyold = ATy.copy() - - # proxf - if z is not None: - ATy += z - x = proxf.prox(x - tau * ATy, tau) - Ax = A.matvec(x) - Axhat = 2 * Ax - Axold - - # proxg - y = proxg.proxdual(y + mu * Axhat, mu) - ATy = A.rmatvec(y) - - # update steps - if z is not None: - p = float( - np.linalg.norm((xold - x) / tau - (ATyold - ATy) - A.rmatvec(z) + z) - ) - else: - p = float(np.linalg.norm((xold - x) / tau - (ATyold - ATy))) - d = float(np.linalg.norm((yold - y) / mu - (Axold - Ax))) - - if p > s * d * delta: - tau /= 1 - alpha - mu *= 1 - alpha - alpha *= eta - elif p < s * d / delta: - tau *= 1 - alpha - mu /= 1 - alpha - alpha *= eta - - # save history of steps - taus[iiter + 1] = tau - mus[iiter + 1] = mu - alphas[iiter + 1] = alpha - iiter += 1 - - # run callback - if callback is not None: - callback(x) - - if show: - if iiter < 10 or niter - iiter < 10 or iiter % (niter // 10) == 0: - pf, pg = proxf(x), proxg(A.matvec(x)) - pf = 0.0 if isinstance(pf, bool) else pf - pg = 0.0 if isinstance(pg, bool) else pg - zx = 0.0 if z is None else np.dot(z, x) - msg = "%6g %12.5e %10.3e %10.3e %10.3e %10.3e" % ( - iiter + 1, - np.real(to_numpy(x[0])), - pf, - pg, - zx, - pf + pg + zx, - ) - print(msg) - - steps = (taus[: iiter + 1], mus[: iiter + 1], alphas[: iiter + 1]) - if show: - print("\nTotal time (s) = %.2f" % (time.time() - tstart)) - + callbacks = [] + if tol is not None or rtol is not None: + callbacks.append(CostNanInfCallback()) + if rtol is not None: + callbacks.append(CostToInitialCallback(rtol)) + + apdsolve = cAdaptivePrimalDual( + callbacks=callbacks if len(callbacks) > 0 else None, + ) + if callback is not None: + apdsolve.callback = callback + x, _, _, _, steps = apdsolve.solve( + proxf=proxf, + proxg=proxg, + A=A, + x0=x0, + tau=tau, + mu=mu, + alpha=alpha, + eta=eta, + s=s, + delta=delta, + z=z, + niter=niter, + tol=tol or (0.0 if rtol else None), + show=show, + itershow=itershow, + ) return x, steps From c54bc38270bc6ad661ae7cddae2be53d45135e98 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 1 Jul 2026 21:41:28 +0100 Subject: [PATCH 41/73] doc: fix dtype of x0 in deblending tutorial --- tutorials/deblending.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tutorials/deblending.py b/tutorials/deblending.py index af8c1169..4053fe22 100644 --- a/tutorials/deblending.py +++ b/tutorials/deblending.py @@ -130,7 +130,13 @@ def sigma(iiter): lort = pyproximal.proximal.Orthogonal(pyproximal.proximal.L0(sigma=sigma), Sop.H) data_inv = pyproximal.optimization.primal.HQS( - laff, lort, x0=np.zeros(Bop.shape[1]), tau=tau, gfirst=False, niter=niter, show=True + laff, + lort, + x0=np.zeros(Bop.shape[1], dtype=np.complex128), + tau=tau, + gfirst=False, + niter=niter, + show=True, )[0] data_inv = data_inv.reshape(ns, nt) snr_inv = pylops.utils.metrics.snr(data, data_inv) From d33c24b136227c81e02b5df3dd63c6cb8bcced8d Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 1 Jul 2026 21:41:49 +0100 Subject: [PATCH 42/73] minor: remved entry from doc api --- docs/source/api/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index b6167bc5..2810fbed 100755 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -163,7 +163,6 @@ Class-based .. autosummary:: :toctree: generated/ - AcceleratedProximalGradient ADMM ADMML2 AndersonProximalGradient From bdadd4f2b087ea9bb4420c0bfd221fafb65c88fb Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 1 Jul 2026 22:26:10 +0100 Subject: [PATCH 43/73] doc: fixed complex dtypes in tutorials --- tutorials/brainmri.py | 2 +- tutorials/nonlinearconstrained.py | 1 + tutorials/plugandplay.py | 8 ++++---- tutorials/red.py | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/tutorials/brainmri.py b/tutorials/brainmri.py index 3866c513..d5c239e5 100644 --- a/tutorials/brainmri.py +++ b/tutorials/brainmri.py @@ -110,7 +110,7 @@ l2, l1, Gop, - x0=np.zeros(gt.size), + x0=np.zeros(gt.size, dtype=np.complex128), tau=tau, mu=mu, theta=1.0, diff --git a/tutorials/nonlinearconstrained.py b/tutorials/nonlinearconstrained.py index c2689bca..3c1878ce 100644 --- a/tutorials/nonlinearconstrained.py +++ b/tutorials/nonlinearconstrained.py @@ -168,6 +168,7 @@ def optimize(self): epsg=1.0, niter=5000, show=True, + itershow=(10, 500, 10), callback=lambda x: callback(x, xhist), ) xhist_pg = np.array(xhist) diff --git a/tutorials/plugandplay.py b/tutorials/plugandplay.py index b0186d71..5d1df383 100644 --- a/tutorials/plugandplay.py +++ b/tutorials/plugandplay.py @@ -100,7 +100,7 @@ def callback(x, xtrue, errhist): x.shape, solver=pyproximal.optimization.primal.ADMM, tau=tau, - x0=np.zeros(x.size), + x0=np.zeros(x.size, dtype=np.complex128), niter=40, show=True, callback=lambda xx: callback(xx, x.ravel(), errhistadmm), @@ -117,7 +117,7 @@ def callback(x, xtrue, errhist): x.shape, solver=pyproximal.optimization.primal.ProximalGradient, tau=tau, - x0=np.zeros(x.size), + x0=np.zeros(x.size, dtype=np.complex128), niter=40, acceleration="fista", show=True, @@ -136,7 +136,7 @@ def callback(x, xtrue, errhist): x.shape, solver=pyproximal.optimization.primal.HQS, tau=tau_hqs, - x0=np.zeros(x.size), + x0=np.zeros(x.size, dtype=np.complex128), niter=40, show=True, callback=lambda xx: callback(xx, x.ravel(), errhisthqs), @@ -200,7 +200,7 @@ def denoise(self, x, tau): x.shape, solver=pyproximal.optimization.primal.ADMM, tau=tau, - x0=np.zeros(x.size), + x0=np.zeros(x.size, dtype=np.complex128), niter=40, show=True, callback=lambda xx: callback(xx, x.ravel(), errhistadmm1), diff --git a/tutorials/red.py b/tutorials/red.py index bc539339..1bef7073 100644 --- a/tutorials/red.py +++ b/tutorials/red.py @@ -105,7 +105,7 @@ def sigmad(iiter): xredgd = pyproximal.optimization.red.RED( l2, red, - x0=np.zeros(x.size), + x0=np.zeros(x.size, dtype=np.complex128), solver="gradientdescent", alpha=0.5, niter=50, @@ -135,7 +135,7 @@ def sigmad(iiter): xredadmm = pyproximal.optimization.red.RED( l2, red, - x0=np.zeros(x.size), + x0=np.zeros(x.size, dtype=np.complex128), solver=pyproximal.optimization.primal.ADMM, tau=tau, niter=50, From b451556111920a434cb772aadfa800716ed6db1a Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 1 Jul 2026 22:33:52 +0100 Subject: [PATCH 44/73] minor: removed links to missing references --- pyproximal/optimization/primaldual.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproximal/optimization/primaldual.py b/pyproximal/optimization/primaldual.py index 4391b2eb..2c6a0835 100644 --- a/pyproximal/optimization/primaldual.py +++ b/pyproximal/optimization/primaldual.py @@ -43,7 +43,7 @@ def PrimalDual( r"""Primal-dual algorithm Solves the following (possibly) nonlinear minimization problem using - the general version of the first-order primal-dual algorithm of [1]_: + the general version of the first-order primal-dual algorithm: .. math:: @@ -98,7 +98,7 @@ def PrimalDual( Apply Proximal of operator ``g`` first (``True``) or Proximal of operator ``f`` first (``False``) tol : :obj:`float`, optional - Tolerance on change of objective function (used as stopping criterion). If + Tolerance on x/y updates (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached or the other tolerance criterion is met rtol : :obj:`float`, optional @@ -190,7 +190,7 @@ def AdaptivePrimalDual( Solves the minimization problem in :func:`pyproximal.optimization.primaldual.PrimalDual` - using an adaptive version of the first-order primal-dual algorithm of [1]_. + using an adaptive version of the first-order primal-dual algorithm. The main advantage of this method is that step sizes :math:`\tau` and :math:`\mu` are changing through iterations, improving the overall speed of convergence of the algorithm. From 52c2f26737f40587047d58c080c3182a80544cd6 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 1 Jul 2026 22:34:14 +0100 Subject: [PATCH 45/73] feat: improved printing alignments --- pyproximal/optimization/cls_primal.py | 10 ++--- pyproximal/optimization/cls_primaldual.py | 54 +++++++++-------------- 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index 5f20504a..db90021c 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -440,9 +440,9 @@ def _print_setup(self, epsg_print: str, xcomplex: bool = False) -> None: print(strpar3) print("-" * 81 + "\n") if not xcomplex: - head1 = " Itn x[0] f g J=f+eps*g tau" + head1 = " Itn x[0] f g J=f+eps*g tau" else: - head1 = " Itn x[0] f g J=f+eps*g tau" + head1 = " Itn x[0] f g J=f+eps*g tau" print(head1) def _print_step( @@ -2145,7 +2145,7 @@ def _print_setup(self, xcomplex: bool = False) -> None: head1 = " Itn x[0] f g J=f+g" else: head1 = ( - " Itn x[0] f g J=f+g" + " Itn x[0] f g J=f+g" ) print(head1) @@ -2458,7 +2458,7 @@ class ADMML2(Solver): Notes ----- - The ADMM algorithm with L2 misfit term can be expressed by the following recursion: + The ADMM algorithm with L2 misfit term can be expressed by the following recursion [1]_: .. math:: @@ -3626,7 +3626,7 @@ def _print_setup(self, xcomplex: bool = False) -> None: f"Proximal operator (g): {type(self.proxg).__name__}\n" ) strpar1 = f"tau = {self.tau:6e}\teta = {self.eta:6e}\tniter = {self.niter}" - strpar2 = f"gfirst = {self.gfirst}\ttol = {self.tol}" + strpar2 = f"gfirst = {self.gfirst}\t\ttol = {self.tol}" print(strpar) print(strpar1) print(strpar2) diff --git a/pyproximal/optimization/cls_primaldual.py b/pyproximal/optimization/cls_primaldual.py index 1115b9f7..fd2c9a67 100644 --- a/pyproximal/optimization/cls_primaldual.py +++ b/pyproximal/optimization/cls_primaldual.py @@ -100,16 +100,16 @@ def _print_setup( f"Linear operator (A): {type(self.A).__name__}\n" f"Additional vector (z): {None if self.z is None else 'vector'}\n" ) - strpar1 = f"tau = {tau_print}\tmu = {mu_print}\ttheta = {self.theta:6e}" - strpar2 = f"tol = {str(self.tol)}\t\tniter = {self.niter}" + strpar1 = f"tau = {tau_print}\tmu = {mu_print}\ttheta = {self.theta:6.2e}" + strpar2 = f"tol = {str(self.tol)}\tniter = {self.niter}" print(strpar) print(strpar1) print(strpar2) print("-" * 85 + "\n") if not xcomplex: - head1 = " Itn x[0] f g z^x J=f+g+z^x" + head1 = " Itn x[0] f g z^x J=f+g+z^x" else: - head1 = " Itn x[0] f g z^x J=f+g+z^x" + head1 = " Itn x[0] f g z^x J=f+g+z^x" print(head1) def _print_step(self, x: NDArray) -> None: @@ -540,10 +540,10 @@ def _print_setup(self, xcomplex: bool = False) -> None: f"Linear operator (A): {type(self.A).__name__}\n" f"Additional vector (z): {None if self.z is None else 'vector'}\n" ) - strpar1 = f"tau0 = {self.tau}\tmu0 = {self.mu}" - strpar2 = f"alpha0 = {str(self.alpha)}\t\teta0 = {self.niter}" + strpar1 = f"tau0 = {self.tau:6.2e}\tmu0 = {self.mu:6.2e}" + strpar2 = f"alpha0 = {str(self.alpha)}\teta0 = {self.niter}" strpar3 = f"s = {str(self.s)}\t\tdelta = {self.delta}" - strpar4 = f"tol = {str(self.tol)}\t\tniter = {self.niter}" + strpar4 = f"tol = {str(self.tol)}\tniter = {self.niter}" print(strpar) print(strpar1) print(strpar2) @@ -551,16 +551,15 @@ def _print_setup(self, xcomplex: bool = False) -> None: print(strpar4) print("-" * 85 + "\n") if not xcomplex: - head1 = " Itn x[0] f g z^x J=f+g+z^x" + head1 = " Itn x[0] f g z^x J=f+g+z^x" else: head1 = " Itn x[0] f g z^x J=f+g+z^x" print(head1) def _print_step(self, x: NDArray) -> None: - if self.tol is None: - self.pf = self.proxf(x) - self.pg = self.proxg(self.A.matvec(x)) - self.zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) + self.pf = self.proxf(x) + self.pg = self.proxg(self.A.matvec(x)) + self.zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) pf = 0.0 if isinstance(self.pf, bool) else self.pf pg = 0.0 if isinstance(self.pg, bool) else self.pg self.pfg = pf + pg + self.zx @@ -625,7 +624,7 @@ def setup( # type: ignore[override] Number of iterations of iterative scheme (default to ``None`` in case a user wants to manually step over the solver) tol : :obj:`float`, optional - Tolerance on change of objective function (used as stopping criterion). If + Tolerance on x/y updates (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached show : :obj:`bool`, optional Display setup log @@ -672,10 +671,10 @@ def setup( # type: ignore[override] self.p = self.d = 0.0 # create variables to track the objective function and iterations - pf = self.proxf(x) - pg = self.proxg(self.A.matvec(x)) - zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) - pfg = pf + pg + zx + self.pf = self.proxf(x) + self.pg = self.proxg(self.A.matvec(x)) + self.zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) + pfg = self.pf + self.pg + self.zx self.pfg, self.pfgold = pfg, pfg self.cost: list[float] = [] self.cost.append(float(self.pfg)) @@ -759,25 +758,16 @@ def step( self.mus.append(self.mu) self.alphas.append(self.alpha) - # tolerance check: break iterations if overall - # objective or x/y updates do not decrease + # tolerance check: break iterations if + # x/y updates do not decrease # below tolerance - if self.tol is not None: - self.pfgold = self.pfg - self.pf = self.proxf(x) - self.pg = self.proxg(self.A.matvec(x)) - self.zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) - self.pfg = self.pf + self.pg + self.zx - if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: - self.tolbreak = True - - if self.p <= self.tol or self.d <= self.tol: - self.tolbreak = True + if self.p <= self.tol or self.d <= self.tol: + self.tolbreak = True self.iiter += 1 if show: self._print_step(x) - if self.tol is not None or show: + if show: self.cost.append(float(self.pfg)) return x, y @@ -891,7 +881,7 @@ def solve( # type: ignore[override] Number of iterations of iterative scheme (default to ``None`` in case a user wants to manually step over the solver) tol : :obj:`float`, optional - Tolerance on change of objective function (used as stopping criterion). If + Tolerance on x/y updates (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached show : :obj:`bool`, optional Display setup log From 32afd77f520b29752755776d086e11de90bc3a1d Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 1 Jul 2026 22:39:35 +0100 Subject: [PATCH 46/73] minor: fix mypy error --- pyproximal/optimization/cls_primaldual.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproximal/optimization/cls_primaldual.py b/pyproximal/optimization/cls_primaldual.py index fd2c9a67..67b05e47 100644 --- a/pyproximal/optimization/cls_primaldual.py +++ b/pyproximal/optimization/cls_primaldual.py @@ -761,8 +761,9 @@ def step( # tolerance check: break iterations if # x/y updates do not decrease # below tolerance - if self.p <= self.tol or self.d <= self.tol: - self.tolbreak = True + if self.tol is not None: + if self.p <= self.tol or self.d <= self.tol: + self.tolbreak = True self.iiter += 1 if show: From 1874b0259056f060f78a3c51b49cc8563828f7e2 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 1 Jul 2026 23:06:09 +0100 Subject: [PATCH 47/73] feat: added xytol to AdaptivePrimalDual --- pyproximal/optimization/cls_primaldual.py | 48 ++++++++++++++++------- pyproximal/optimization/primaldual.py | 7 +++- tutorials/adaptivepd.py | 2 +- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/pyproximal/optimization/cls_primaldual.py b/pyproximal/optimization/cls_primaldual.py index 67b05e47..ca9c76ba 100644 --- a/pyproximal/optimization/cls_primaldual.py +++ b/pyproximal/optimization/cls_primaldual.py @@ -543,7 +543,9 @@ def _print_setup(self, xcomplex: bool = False) -> None: strpar1 = f"tau0 = {self.tau:6.2e}\tmu0 = {self.mu:6.2e}" strpar2 = f"alpha0 = {str(self.alpha)}\teta0 = {self.niter}" strpar3 = f"s = {str(self.s)}\t\tdelta = {self.delta}" - strpar4 = f"tol = {str(self.tol)}\tniter = {self.niter}" + strpar4 = ( + f"tol = {str(self.tol)}\txytol = {str(self.xytol)}\tniter = {self.niter}" + ) print(strpar) print(strpar1) print(strpar2) @@ -551,15 +553,16 @@ def _print_setup(self, xcomplex: bool = False) -> None: print(strpar4) print("-" * 85 + "\n") if not xcomplex: - head1 = " Itn x[0] f g z^x J=f+g+z^x" + head1 = " Itn x[0] f g z^x J=f+g+z^x" else: head1 = " Itn x[0] f g z^x J=f+g+z^x" print(head1) def _print_step(self, x: NDArray) -> None: - self.pf = self.proxf(x) - self.pg = self.proxg(self.A.matvec(x)) - self.zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) + if self.tol is None: + self.pf = self.proxf(x) + self.pg = self.proxg(self.A.matvec(x)) + self.zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) pf = 0.0 if isinstance(self.pf, bool) else self.pf pg = 0.0 if isinstance(self.pg, bool) else self.pg self.pfg = pf + pg + self.zx @@ -567,10 +570,10 @@ def _print_step(self, x: NDArray) -> None: msg = ( f"{self.iiter:6g} " + strx - + f" {self.pf:11.4e} " - + f"{self.pg:11.4e} " - + f" {self.zx:11.4e} " - + f" {self.pfg:11.4e} " + + f"{pf:11.4e} " + + f"{pg:11.4e} " + + f"{self.zx:11.4e} " + + f"{self.pfg:11.4e} " ) print(msg) @@ -589,6 +592,7 @@ def setup( # type: ignore[override] z: NDArray | None = None, niter: int | None = None, tol: float | None = None, + xytol: float | None = None, show: bool = False, ) -> tuple[NDArray, NDArray]: r"""Setup solver @@ -624,6 +628,9 @@ def setup( # type: ignore[override] Number of iterations of iterative scheme (default to ``None`` in case a user wants to manually step over the solver) tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + xytol : :obj:`float`, optional Tolerance on x/y updates (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached show : :obj:`bool`, optional @@ -647,7 +654,7 @@ def setup( # type: ignore[override] self.z = z self.niter = niter self.tol = tol - + self.xytol = xytol self.ncp = get_array_module(x0) # initialize solver @@ -758,11 +765,19 @@ def step( self.mus.append(self.mu) self.alphas.append(self.alpha) - # tolerance check: break iterations if - # x/y updates do not decrease - # below tolerance + # tolerance checks: break iterations if overall + # objective or x/y updates do not decrease + # below respective tolerances if self.tol is not None: - if self.p <= self.tol or self.d <= self.tol: + self.pfgold = self.pfg + self.pf = self.proxf(x) + self.pg = self.proxg(self.A.matvec(x)) + self.zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) + self.pfg = self.pf + self.pg + self.zx + if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + self.tolbreak = True + if self.xytol is not None: + if self.p <= self.xytol or self.d <= self.xytol: self.tolbreak = True self.iiter += 1 @@ -846,6 +861,7 @@ def solve( # type: ignore[override] z: NDArray | None = None, niter: int | None = None, tol: float | None = None, + xytol: float | None = None, show: bool = False, itershow: tuple[int, int, int] = (10, 10, 10), ) -> tuple[NDArray, NDArray, int, NDArray, tuple[NDArray, NDArray, NDArray]]: @@ -882,6 +898,9 @@ def solve( # type: ignore[override] Number of iterations of iterative scheme (default to ``None`` in case a user wants to manually step over the solver) tol : :obj:`float`, optional + Tolerance on change of objective function (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached + xytol : :obj:`float`, optional Tolerance on x/y updates (used as stopping criterion). If ``tol=None``, run until ``niter`` is reached show : :obj:`bool`, optional @@ -919,6 +938,7 @@ def solve( # type: ignore[override] z=z, niter=niter, tol=tol, + xytol=xytol, show=show, ) x, y = self.run(x, y, niter, show=show, itershow=itershow) diff --git a/pyproximal/optimization/primaldual.py b/pyproximal/optimization/primaldual.py index 2c6a0835..9faebea0 100644 --- a/pyproximal/optimization/primaldual.py +++ b/pyproximal/optimization/primaldual.py @@ -180,8 +180,9 @@ def AdaptivePrimalDual( delta: float = 1.5, z: NDArray | None = None, niter: int = 10, - tol: float | None = 1e-10, + tol: float | None = None, rtol: float | None = None, + xytol: float | None = None, callback: Callable[[NDArray], None] | None = None, show: bool = False, itershow: tuple[int, int, int] = (10, 10, 10), @@ -234,6 +235,9 @@ def AdaptivePrimalDual( initial objective function is below this value. If ``rtol=None``, run until ``niter`` is reached or the other tolerance criterion is met + xytol : :obj:`float`, optional + Tolerance on x/y updates (used as stopping criterion). If + ``tol=None``, run until ``niter`` is reached callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector @@ -281,6 +285,7 @@ def AdaptivePrimalDual( z=z, niter=niter, tol=tol or (0.0 if rtol else None), + xytol=xytol, show=show, itershow=itershow, ) diff --git a/tutorials/adaptivepd.py b/tutorials/adaptivepd.py index fa29d79d..b6dd7734 100644 --- a/tutorials/adaptivepd.py +++ b/tutorials/adaptivepd.py @@ -98,7 +98,7 @@ def callback(x, f, g, K, cost, xtrue, err): x0=np.zeros_like(img.ravel()), niter=45, show=True, - tol=0.05, + xytol=0.05, callback=lambda x: callback(x, l2, l1iso, Gop, cost_ada, img.ravel(), err_ada), ) iml12_ada = iml12_ada.reshape(img.shape) From 35ce5ea1d5cf28951bb57476095ce5594687a5d6 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sun, 5 Jul 2026 21:07:54 +0100 Subject: [PATCH 48/73] feat: adde cost at setup --- pyproximal/optimization/cls_primal.py | 120 ++++++++++++++++------ pyproximal/optimization/cls_primaldual.py | 10 +- 2 files changed, 98 insertions(+), 32 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index db90021c..6c8ba991 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -217,8 +217,11 @@ def setup( # type: ignore[override] self.ncp = get_array_module(x0) # create variables to track the objective function and iterations - self.pf, self.pfold = np.inf, np.inf + pf = self.prox(x0) + self.pf, self.pfold = pf, pf + self.cost: list[float] = [] + self.cost.append(float(self.pf)) self.tolbreak = False self.iiter = 0 @@ -251,7 +254,7 @@ def step(self, x: NDArray, show: bool = False) -> NDArray: if self.tol is not None: self.pfold = self.pf self.pf = self.prox(x) - if np.abs(1.0 - self.pf / self.pfold) < self.tol: + if self.pfold != 0 and self.ncp.abs(1.0 - self.pf / self.pfold) < self.tol: self.tolbreak = True self.iiter += 1 @@ -678,7 +681,10 @@ def step( self.pfgold = self.pfg pf, pg = self.proxf(x), self.proxg(x) self.pfg = pf + np.sum(epsg * pg) - if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + if ( + self.pfgold != 0 + and self.ncp.abs(1.0 - self.pfg / self.pfgold) < self.tol + ): self.tolbreak = True else: pf, pg = 0.0, 0.0 @@ -1128,7 +1134,10 @@ def step( self.pfgold = self.pfg self.pf, pg = self.proxf(x), self.proxg(x) self.pfg = self.pf + np.sum(epsg * pg) - if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + if ( + self.pfgold != 0 + and self.ncp.abs(1.0 - self.pfg / self.pfgold) < self.tol + ): self.tolbreak = True else: pg = 0.0 @@ -1431,9 +1440,7 @@ def setup( # type: ignore[override] raise ValueError(msg) # check if epsg is a vector and set same value for all gs otherwise - print("epsg", epsg) self.epsg = self.ncp.asarray(epsg, dtype=np.float32) - print("self.epsg.size", self.epsg.size) if self.epsg.size == 1: self.epsg = epsg * self.ncp.ones(len(proxgs), dtype=np.float32) epsg_print = str(self.epsg[0]) @@ -1463,8 +1470,15 @@ def setup( # type: ignore[override] self.t = 1.0 # create variables to track the objective function and iterations - self.pfg, self.pfgold = np.inf, np.inf + pf = np.sum([proxf(x) for proxf in self.proxfs]) + pg = np.sum( + [eg * proxg(x) for proxg, eg in zip(self.proxgs, self.epsg, strict=True)] + ) + pfg = pf + pg + self.pfg, self.pfgold = pfg, pfg + self.cost: list[float] = [] + self.cost.append(float(self.pfg)) self.tolbreak = False self.iiter = 0 @@ -1538,7 +1552,10 @@ def step( ] ) self.pfg = pf + pg - if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + if ( + self.pfgold != 0 + and self.ncp.abs(1.0 - self.pfg / self.pfgold) < self.tol + ): self.tolbreak = True else: pf, pg = 0.0, 0.0 @@ -1856,6 +1873,7 @@ def setup( # type: ignore[override] pf, pg = self.proxf(x), self.proxg(x) pfg = pf + pg self.pfg, self.pfgold = pfg, pfg + self.cost: list[float] = [] self.cost.append(float(self.pfg)) self.tolbreak = False @@ -1914,7 +1932,10 @@ def step( pf = self.proxf(x) pg = self.proxg(x) self.pfg = pf + pg - if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + if ( + self.pfgold != 0 + and self.ncp.abs(1.0 - self.pfg / self.pfgold) < self.tol + ): self.tolbreak = True else: pf, pg = 0.0, 0.0 @@ -2139,7 +2160,6 @@ def _print_setup(self, xcomplex: bool = False) -> None: print(strpar) print(strpar1) print(strpar2) - print("-" * 65 + "\n") if not xcomplex: head1 = " Itn x[0] f g J=f+g" @@ -2233,8 +2253,13 @@ def setup( # type: ignore[override] self.u = self.ncp.zeros_like(x) # create variables to track the objective function and iterations - self.pfg, self.pfgold = np.inf, np.inf + pf = self.proxf(x) + pg = self.proxg(x) + pfg = pf + pg + self.pfg, self.pfgold = pfg, pfg + self.cost: list[float] = [] + self.cost.append(float(self.pfg)) self.tolbreak = False self.iiter = 0 @@ -2286,7 +2311,10 @@ def step( pf = self.proxf(x) pg = self.proxg(x) self.pfg = pf + pg - if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + if ( + self.pfgold != 0 + and self.ncp.abs(1.0 - self.pfg / self.pfgold) < self.tol + ): self.tolbreak = True else: pf, pg = 0.0, 0.0 @@ -2485,7 +2513,6 @@ def _print_setup(self, xcomplex: bool = False) -> None: strpar1 = f"tau = {self.tau:6e}\tniter = {self.niter}\ttol = {self.tol}" print(strpar) print(strpar1) - print("-" * 65 + "\n") if not xcomplex: head1 = " Itn x[0] f g J=f+g" @@ -2594,8 +2621,13 @@ def setup( # type: ignore[override] self.sqrttau = 1.0 / sqrt(self.tau) # create variables to track the objective function and iterations - self.pfg, self.pfgold = np.inf, np.inf + pf = 0.5 * self.ncp.linalg.norm(self.Op @ x - self.b) ** 2 + pg = self.proxg(self.A @ x) + pfg = pf + pg + self.pfg, self.pfgold = pfg, pfg + self.cost: list[float] = [] + self.cost.append(float(self.pfg)) self.tolbreak = False self.iiter = 0 @@ -2683,7 +2715,10 @@ def step( pf = 0.5 * self.ncp.linalg.norm(self.Op @ x - self.b) ** 2 pg = self.proxg(Ax) self.pfg = pf + pg - if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + if ( + self.pfgold != 0 + and self.ncp.abs(1.0 - self.pfg / self.pfgold) < self.tol + ): self.tolbreak = True else: pf, pg = 0.0, 0.0 @@ -2907,7 +2942,6 @@ def _print_setup(self, xcomplex: bool = False) -> None: print(strpar) print(strpar1) print(strpar2) - print("-" * 65 + "\n") if not xcomplex: head1 = " Itn x[0] f g J=f+g" @@ -3004,8 +3038,13 @@ def setup( # type: ignore[override] self.u = self.ncp.zeros_like(z) # create variables to track the objective function and iterations - self.pfg, self.pfgold = np.inf, np.inf + pf = self.proxf(x) + pg = self.proxg(self.Ax) + pfg = pf + pg + self.pfg, self.pfgold = pfg, pfg + self.cost: list[float] = [] + self.cost.append(float(self.pfg)) self.tolbreak = False self.iiter = 0 @@ -3056,7 +3095,10 @@ def step( pf = self.proxf(x) pg = self.proxg(self.Ax) self.pfg = pf + pg - if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + if ( + self.pfgold != 0 + and self.ncp.abs(1.0 - self.pfg / self.pfgold) < self.tol + ): self.tolbreak = True else: pf, pg = 0.0, 0.0 @@ -3271,7 +3313,6 @@ def _print_setup(self, xcomplex: bool = False) -> None: print(strpar) print(strpar1) print(strpar2) - print("-" * 65 + "\n") if not xcomplex: head1 = " Itn x[0] f g J=f+g" @@ -3369,8 +3410,13 @@ def setup( # type: ignore[override] x = self.proxg.prox(x0 - self.proxf.grad(x0), 1.0) # create variables to track the objective function and iterations - self.pfg, self.pfgold = np.inf, np.inf + pf = self.proxf(x) + pg = self.proxg(x) + pfg = pf + pg + self.pfg, self.pfgold = pfg, pfg + self.cost: list[float] = [] + self.cost.append(float(self.pfg)) self.tolbreak = False self.iiter = 0 @@ -3423,7 +3469,10 @@ def step( pf = self.proxf(x) pg = self.proxg(x) self.pfg = pf + pg - if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + if ( + self.pfgold != 0 + and self.ncp.abs(1.0 - self.pfg / self.pfgold) < self.tol + ): self.tolbreak = True else: pf, pg = 0.0, 0.0 @@ -3630,7 +3679,6 @@ def _print_setup(self, xcomplex: bool = False) -> None: print(strpar) print(strpar1) print(strpar2) - print("-" * 65 + "\n") if not xcomplex: head1 = " Itn x[0] f g J=f+g" @@ -3719,8 +3767,13 @@ def setup( # type: ignore[override] y = x0.copy() # create variables to track the objective function and iterations - self.pfg, self.pfgold = np.inf, np.inf + pf = self.proxf(x) + pg = self.proxg(x) + pfg = pf + pg + self.pfg, self.pfgold = pfg, pfg + self.cost: list[float] = [] + self.cost.append(float(self.pfg)) self.tolbreak = False self.iiter = 0 @@ -3771,7 +3824,10 @@ def step( pf = self.proxf(x) pg = self.proxg(x) self.pfg = pf + pg - if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + if ( + self.pfgold != 0 + and self.ncp.abs(1.0 - self.pfg / self.pfgold) < self.tol + ): self.tolbreak = True else: pf, pg = 0.0, 0.0 @@ -3999,7 +4055,6 @@ def _print_setup(self, xcomplex: bool = False) -> None: print(strpar1) print(strpar2) print(strpar3) - print("-" * 65 + "\n") if not xcomplex: head1 = " Itn x[0] J=sum_i f_i" @@ -4087,8 +4142,11 @@ def setup( # type: ignore[override] x = self.ncp.mean(y, axis=0) # create variables to track the objective function and iterations - self.pf, self.pfold = np.inf, np.inf + pf = self.ncp.sum([self.proxfs[i](x) for i in range(self.nprox)]) + self.pf, self.pfold = pf, pf + self.cost: list[float] = [] + self.cost.append(float(self.pf)) self.tolbreak = False self.iiter = 0 @@ -4138,7 +4196,7 @@ def step( if self.tol is not None: self.pfold = self.pf self.pf = self.ncp.sum([self.proxfs[i](x) for i in range(self.nprox)]) - if np.abs(1.0 - self.pf / self.pfold) < self.tol: + if self.pfold != 0 and self.ncp.abs(1.0 - self.pf / self.pfold) < self.tol: self.tolbreak = True else: self.pf = 0.0 @@ -4344,7 +4402,6 @@ def _print_setup(self, xcomplex: bool = False) -> None: strpar1 = f"tau = {self.tau:6e}\tniter = {self.niter}\ttol = {self.tol}" print(strpar) print(strpar1) - print("-" * 65 + "\n") if not xcomplex: head1 = " Itn x[0] J=sum_i f_i" @@ -4412,8 +4469,11 @@ def setup( # type: ignore[override] y = self.ncp.zeros((self.nprox, x0.size), dtype=x0.dtype) # create variables to track the objective function and iterations - self.pf, self.pfold = np.inf, np.inf + pf = self.ncp.sum([self.proxfs[i](x_bar) for i in range(self.nprox)]) + self.pf, self.pfold = pf, pf + self.cost: list[float] = [] + self.cost.append(float(self.pf)) self.tolbreak = False self.iiter = 0 @@ -4471,7 +4531,7 @@ def step( if self.tol is not None: self.pfold = self.pf self.pf = self.ncp.sum([self.proxfs[i](x_bar) for i in range(self.nprox)]) - if np.abs(1.0 - self.pf / self.pfold) < self.tol: + if self.pfold != 0 and self.ncp.abs(1.0 - self.pf / self.pfold) < self.tol: self.tolbreak = True else: self.pf = 0.0 diff --git a/pyproximal/optimization/cls_primaldual.py b/pyproximal/optimization/cls_primaldual.py index ca9c76ba..83bd039b 100644 --- a/pyproximal/optimization/cls_primaldual.py +++ b/pyproximal/optimization/cls_primaldual.py @@ -328,7 +328,10 @@ def step( self.pg = self.proxg(self.A.matvec(x)) self.zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) self.pfg = self.pf + self.pg + self.zx - if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + if ( + self.pfgold != 0 + and self.ncp.abs(1.0 - self.pfg / self.pfgold) < self.tol + ): self.tolbreak = True self.iiter += 1 @@ -774,7 +777,10 @@ def step( self.pg = self.proxg(self.A.matvec(x)) self.zx = 0.0 if self.z is None else self.ncp.dot(self.z, x) self.pfg = self.pf + self.pg + self.zx - if np.abs(1.0 - self.pfg / self.pfgold) < self.tol: + if ( + self.pfgold != 0 + and self.ncp.abs(1.0 - self.pfg / self.pfgold) < self.tol + ): self.tolbreak = True if self.xytol is not None: if self.p <= self.xytol or self.d <= self.xytol: From caa028815c0449a1f64af7c5515e5b9248ea4d18 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sun, 5 Jul 2026 21:36:04 +0100 Subject: [PATCH 49/73] doc: fix call of BlockDiagFrobenius in nrsfm tutorial --- tutorials/nrsfm.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tutorials/nrsfm.py b/tutorials/nrsfm.py index 5b70684d..1763e420 100644 --- a/tutorials/nrsfm.py +++ b/tutorials/nrsfm.py @@ -231,8 +231,11 @@ def __init__(self, dim, R, M): self.M = M def __call__(self, x): - X = x.reshape(self.dim) - return 0.5 * np.linalg.norm(self.R @ X - self.M, "fro") ** 2 + X = x.reshape(self.dim).reshape(self.dim[0] // 3, 3, self.dim[1]) + return ( + 0.5 + * np.linalg.norm((self.R @ X).reshape(self.M.shape) - self.M, "fro") ** 2 + ) @_check_tau def prox(self, x, tau): From acdcdccd4521464922ad63252205d8cec0df8ed6 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sun, 5 Jul 2026 21:54:22 +0100 Subject: [PATCH 50/73] minor: fix name of callback --- pyproximal/optimization/callback.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproximal/optimization/callback.py b/pyproximal/optimization/callback.py index 91fdc50f..5da4b1dc 100644 --- a/pyproximal/optimization/callback.py +++ b/pyproximal/optimization/callback.py @@ -1,5 +1,5 @@ __all__ = [ - "ModuleUpdateCallback", + "ModelUpdateCallback", ] from typing import TYPE_CHECKING @@ -11,8 +11,8 @@ from pyproximal.optimization.basesolver import Solver -class ModuleUpdateCallback(Callbacks): # type: ignore[misc] - """Module update callback +class ModelUpdateCallback(Callbacks): # type: ignore[misc] + """Model update callback This callback can be used to stop the solver when each element of the model (i.e, solution) is updated below a certain threshold. From b562aafa29e84f77b00674798f81c83a5ad5659a Mon Sep 17 00:00:00 2001 From: mrava87 Date: Tue, 11 Aug 2026 23:30:55 +0100 Subject: [PATCH 51/73] fix: correct L21 for complex valued inputs --- pyproximal/ProxOperator.py | 2 +- pyproximal/proximal/L21.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproximal/ProxOperator.py b/pyproximal/ProxOperator.py index f32d07ce..0a34c6ff 100644 --- a/pyproximal/ProxOperator.py +++ b/pyproximal/ProxOperator.py @@ -407,7 +407,7 @@ def __init__(self, f: ProxOperator, a: float, b: float | NDArray) -> None: self.f, self.a, self.b = f, a, b super().__init__(None, f.hasgrad) - def __call__(self, x: NDArray) -> NDArray: + def __call__(self, x: NDArray) -> bool | float | int: return self.f(self.a * x + self.b) @_check_tau diff --git a/pyproximal/proximal/L21.py b/pyproximal/proximal/L21.py index bf59462a..e8348bc9 100644 --- a/pyproximal/proximal/L21.py +++ b/pyproximal/proximal/L21.py @@ -65,13 +65,14 @@ def __init__(self, ndim: int, sigma: float = 1.0) -> None: def __call__(self, x: NDArray) -> float: x = x.reshape(self.ndim, len(x) // self.ndim) - f = self.sigma * np.sum(np.sqrt(np.sum(x**2, axis=0))) + f = self.sigma * np.sum(np.sqrt(np.sum(np.abs(x) ** 2, axis=0))) + print(f"f = {f}") return float(f) @_check_tau def prox(self, x: NDArray, tau: float) -> NDArray: x = x.reshape(self.ndim, len(x) // self.ndim) - aux = np.sqrt(np.sum(x**2, axis=0)) + aux = np.sqrt(np.sum(np.abs(x) ** 2, axis=0)) aux = np.vstack([aux] * self.ndim).ravel() x = (1 - (tau * self.sigma) / np.maximum(aux, tau * self.sigma)) * x.ravel() return x @@ -79,7 +80,7 @@ def prox(self, x: NDArray, tau: float) -> NDArray: @_check_tau def proxdual(self, x: NDArray, tau: float) -> NDArray: x = x.reshape(self.ndim, len(x) // self.ndim) - aux = np.sqrt(np.sum(x**2, axis=0)) + aux = np.sqrt(np.sum(np.abs(x) ** 2, axis=0)) aux = np.vstack([aux] * self.ndim).ravel() x = self.sigma * x.ravel() / np.maximum(aux, self.sigma) return x From f5b1950bf56f4821f646c6f05ab70cdc42b3f7fb Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 12 Aug 2026 20:13:52 +0100 Subject: [PATCH 52/73] fix: correct L21_plus_L1 for complex valued input --- pyproximal/proximal/L21.py | 2 +- pyproximal/proximal/L21_plus_L1.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproximal/proximal/L21.py b/pyproximal/proximal/L21.py index e8348bc9..03ad8f21 100644 --- a/pyproximal/proximal/L21.py +++ b/pyproximal/proximal/L21.py @@ -28,7 +28,7 @@ class L21(ProxOperator): .. math:: \sigma \|\mathbf{X}\|_{2,1} = \sigma \sum_{j=0}^{N'_x} \|\mathbf{x}_j\|_2 = - \sigma \sum_{j=0}^{N'_x} \sqrt{\sum_{i=0}^{N_{dim}}} |x_{ij}|^2 + \sigma \sum_{j=0}^{N'_x} \sqrt{\sum_{i=0}^{N_{dim}} |x_{ij}|^2} the proximal operator is: diff --git a/pyproximal/proximal/L21_plus_L1.py b/pyproximal/proximal/L21_plus_L1.py index 10dcbd45..fe6c90dc 100644 --- a/pyproximal/proximal/L21_plus_L1.py +++ b/pyproximal/proximal/L21_plus_L1.py @@ -38,7 +38,9 @@ def __init__(self, sigma: float = 1.0, rho: float = 0.8) -> None: def __call__(self, x: NDArray) -> float: return float( self.rho * self.sigma * np.sum(np.abs(x)) - + (1 - self.rho) * self.sigma * np.sum(np.sqrt(np.sum(x**2, axis=0))) + + (1 - self.rho) + * self.sigma + * np.sum(np.sqrt(np.sum(np.abs(x) ** 2, axis=0))) ) @_check_tau From 17e0640fdaaebad5973922f0f13aa09b862b9c9e Mon Sep 17 00:00:00 2001 From: mrava87 Date: Wed, 12 Aug 2026 23:50:41 +0100 Subject: [PATCH 53/73] fix: remove print from L21 --- pyproximal/proximal/L21.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproximal/proximal/L21.py b/pyproximal/proximal/L21.py index 03ad8f21..478b96a1 100644 --- a/pyproximal/proximal/L21.py +++ b/pyproximal/proximal/L21.py @@ -66,7 +66,6 @@ def __init__(self, ndim: int, sigma: float = 1.0) -> None: def __call__(self, x: NDArray) -> float: x = x.reshape(self.ndim, len(x) // self.ndim) f = self.sigma * np.sum(np.sqrt(np.sum(np.abs(x) ** 2, axis=0))) - print(f"f = {f}") return float(f) @_check_tau From f8d7c0994a4c1daa75b7dc8c2f08026b18488cc2 Mon Sep 17 00:00:00 2001 From: Matteo Ravasi Date: Sat, 15 Aug 2026 07:56:50 +0100 Subject: [PATCH 54/73] "Claude PR Assistant workflow" --- .github/workflows/claude.yml | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 00000000..6b15fac7 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr *)' + From 787a50e8785ad538d5ee67f79530fe6c5e199adf Mon Sep 17 00:00:00 2001 From: mrava87 Date: Thu, 27 Aug 2026 18:03:35 +0100 Subject: [PATCH 55/73] build: added numba-cuda --- .github/workflows/build.yaml | 2 +- docs/source/installation.rst | 2 +- noxfile.py | 4 +- pyproject.toml | 16 +- uv.lock | 1202 +++++++++++++--------------------- 5 files changed, 462 insertions(+), 764 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5702c85e..63d5b248 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -13,7 +13,7 @@ jobs: strategy: matrix: platform: [ ubuntu-latest, macos-latest ] - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14"] runs-on: ${{ matrix.platform }} diff --git a/docs/source/installation.rst b/docs/source/installation.rst index a16d6d9c..8fe9ccda 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -7,7 +7,7 @@ Dependencies ************ The mandatory dependencies of PyProximal are limited to: -* Python 3.10 or greater +* Python 3.11 or greater * `NumPy `_ * `SciPy `_ * `PyLops `_ diff --git a/noxfile.py b/noxfile.py index 7e403a72..b6b5796c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -26,7 +26,7 @@ def lint(session): ) -@nox.session(python=["3.10", "3.11", "3.12", "3.13", "3.14"]) +@nox.session(python=["3.11", "3.12", "3.13", "3.14"]) def tests(session: nox.Session) -> None: """ Run unit tests. @@ -36,7 +36,7 @@ def tests(session: nox.Session) -> None: session.run("pytest", *session.posargs) -@nox.session(python=["3.10", "3.11", "3.12", "3.13", "3.14"]) +@nox.session(python=["3.11", "3.12", "3.13", "3.14"]) def build(session: nox.Session) -> None: """ Build an SDist and wheel. diff --git a/pyproject.toml b/pyproject.toml index e1ba5e9c..9053a8f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,12 +23,13 @@ classifiers = [ "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Mathematics", ] -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ "numpy >= 1.21.0", "scipy >= 1.11.0", @@ -42,6 +43,8 @@ advanced = [ "llvmlite", "numba", ] +gpu-cu12 = ["cupy-cuda12x", "numba-cuda[cu12]>=0.30.0"] +gpu-cu13 = ["cupy-cuda13x", "numba-cuda[cu13]>=0.30.0"] [dependency-groups] dev = [ @@ -85,6 +88,15 @@ packages = ["pyproximal"] [tool.hatch.build.targets.sdist] exclude = ["/.github", "/testdata"] + +[tool.uv] +conflicts = [ + [ + { extra = "gpu-cu12" }, + { extra = "gpu-cu13" }, + ], +] + [tool.pytest.ini_options] addopts = "--verbose" python_files = ["pytests/*.py"] diff --git a/uv.lock b/uv.lock index 15a7fedf..b74d368e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,11 +1,14 @@ version = 1 revision = 3 -requires-python = ">=3.10" +requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version < '3.11'", + "python_full_version < '3.12'", ] +conflicts = [[ + { package = "pyproximal", extra = "gpu-cu12" }, + { package = "pyproximal", extra = "gpu-cu13" }, +]] [[package]] name = "alabaster" @@ -20,9 +23,6 @@ wheels = [ name = "asgiref" version = "3.11.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, @@ -43,7 +43,6 @@ version = "2.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycodestyle" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/d8/30873d2b7b57dee9263e53d142da044c4600a46f2d28374b3e38b023df16/autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758", size = 92210, upload-time = "2025-01-14T14:46:18.454Z" } wheels = [ @@ -106,12 +105,9 @@ name = "bm4d" version = "4.2.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pywavelets", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pywavelets", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, + { name = "pywavelets" }, + { name = "scipy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/20/33/d85ecd508ad6d0782002c21264d5d67f43d430d9e2304841bd5c7fb25753/bm4d-4.2.5.tar.gz", hash = "sha256:5a7ca4d85ecb570e8eb20213d035fc801da58227772fd077b0a8798f5342c9b4", size = 3891186, upload-time = "2024-09-06T15:40:13.357Z" } wheels = [ @@ -132,22 +128,10 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, @@ -224,22 +208,6 @@ version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, - { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, - { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, - { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, - { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, - { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, - { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, - { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, - { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, - { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, @@ -316,86 +284,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "contourpy" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, - { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, - { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, - { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, - { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, - { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, - { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, - { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, - { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, - { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, - { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, - { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, - { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, - { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, - { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, - { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, - { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, - { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, - { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, - { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, - { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, - { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, - { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, - { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, - { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, - { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, - { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, - { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, - { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, - { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, - { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, - { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, - { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, - { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, - { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, - { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, - { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, -] - [[package]] name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] dependencies = [ - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -478,18 +372,6 @@ version = "7.13.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/07/1c8099563a8a6c389a31c2d0aa1497cee86d6248bb4b9ba5e779215db9f9/coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", size = 219143, upload-time = "2026-02-03T13:59:40.459Z" }, - { url = "https://files.pythonhosted.org/packages/69/39/a892d44af7aa092cab70e0cc5cdbba18eeccfe1d6930695dab1742eef9e9/coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b", size = 219663, upload-time = "2026-02-03T13:59:41.951Z" }, - { url = "https://files.pythonhosted.org/packages/9a/25/9669dcf4c2bb4c3861469e6db20e52e8c11908cf53c14ec9b12e9fd4d602/coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8", size = 246424, upload-time = "2026-02-03T13:59:43.418Z" }, - { url = "https://files.pythonhosted.org/packages/f3/68/d9766c4e298aca62ea5d9543e1dd1e4e1439d7284815244d8b7db1840bfb/coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0", size = 248228, upload-time = "2026-02-03T13:59:44.816Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e2/eea6cb4a4bd443741adf008d4cccec83a1f75401df59b6559aca2bdd9710/coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6", size = 250103, upload-time = "2026-02-03T13:59:46.271Z" }, - { url = "https://files.pythonhosted.org/packages/db/77/664280ecd666c2191610842177e2fab9e5dbdeef97178e2078fed46a3d2c/coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f", size = 247107, upload-time = "2026-02-03T13:59:48.53Z" }, - { url = "https://files.pythonhosted.org/packages/2b/df/2a672eab99e0d0eba52d8a63e47dc92245eee26954d1b2d3c8f7d372151f/coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e", size = 248143, upload-time = "2026-02-03T13:59:50.027Z" }, - { url = "https://files.pythonhosted.org/packages/a5/dc/a104e7a87c13e57a358b8b9199a8955676e1703bb372d79722b54978ae45/coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56", size = 246148, upload-time = "2026-02-03T13:59:52.025Z" }, - { url = "https://files.pythonhosted.org/packages/2b/89/e113d3a58dc20b03b7e59aed1e53ebc9ca6167f961876443e002b10e3ae9/coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f", size = 246414, upload-time = "2026-02-03T13:59:53.859Z" }, - { url = "https://files.pythonhosted.org/packages/3f/60/a3fd0a6e8d89b488396019a2268b6a1f25ab56d6d18f3be50f35d77b47dc/coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a", size = 247023, upload-time = "2026-02-03T13:59:55.454Z" }, - { url = "https://files.pythonhosted.org/packages/19/fa/de4840bb939dbb22ba0648a6d8069fa91c9cf3b3fca8b0d1df461e885b3d/coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be", size = 221751, upload-time = "2026-02-03T13:59:57.383Z" }, - { url = "https://files.pythonhosted.org/packages/de/87/233ff8b7ef62fb63f58c78623b50bef69681111e0c4d43504f422d88cda4/coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b", size = 222686, upload-time = "2026-02-03T13:59:58.825Z" }, { url = "https://files.pythonhosted.org/packages/ec/09/1ac74e37cf45f17eb41e11a21854f7f92a4c2d6c6098ef4a1becb0c6d8d3/coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", size = 219276, upload-time = "2026-02-03T14:00:00.296Z" }, { url = "https://files.pythonhosted.org/packages/2e/cb/71908b08b21beb2c437d0d5870c4ec129c570ca1b386a8427fcdb11cf89c/coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", size = 219776, upload-time = "2026-02-03T14:00:02.414Z" }, { url = "https://files.pythonhosted.org/packages/09/85/c4f3dd69232887666a2c0394d4be21c60ea934d404db068e6c96aa59cd87/coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", size = 250196, upload-time = "2026-02-03T14:00:04.197Z" }, @@ -571,6 +453,205 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" }, ] +[[package]] +name = "cuda-bindings" +version = "12.9.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version < '3.12'", +] +dependencies = [ + { name = "cuda-pathfinder", marker = "extra == 'extra-10-pyproximal-gpu-cu12'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/f3/f9d1095f90d2a4df24cfcafe7487fd9444c6dacb94e3722be6fedd8ac26c/cuda_bindings-12.9.7-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16043ef5b15ab88fe9954c5c2061b1d8007591b27f2c916331056de0ebc6187e", size = 7114834, upload-time = "2026-05-27T18:44:07.746Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8a/1251e1794b69865aacd5629936006b18ea0816a495de4ecea9a825556eb3/cuda_bindings-12.9.7-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6496a88d84b1209d6651b0370c19c26319e157c22f6d018bf9a358cd8049041", size = 7647147, upload-time = "2026-05-27T18:44:09.4Z" }, + { url = "https://files.pythonhosted.org/packages/1e/39/158392f6572e6e0def70ca39029c46b75e02ea4a43c63ff7320b3d180a29/cuda_bindings-12.9.7-cp311-cp311-win_amd64.whl", hash = "sha256:c392ffa5010ef4073bfd9dfff4d1ae56032094ed52d3d732014f8e41a73e6b59", size = 7218081, upload-time = "2026-05-27T18:44:11.104Z" }, + { url = "https://files.pythonhosted.org/packages/32/45/557d4ed1fa54f0c7db8aee083229f624990d69f7d00f55477eed5c7e169a/cuda_bindings-12.9.7-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0666d3c082ef8f4b2d670950589373550e9f3bf564d635dd883f24a0b40402ff", size = 7071026, upload-time = "2026-05-27T18:44:13.356Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/e3c6e58ece26a053419ba0a18444b5443cfc64451bbf37f84e8143b8bdca/cuda_bindings-12.9.7-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c7ef48c5e13ae90f3b2ecfb72f8e99ac43c8f4c43e67e1325b8aae331453687", size = 7611059, upload-time = "2026-05-27T18:44:15.252Z" }, + { url = "https://files.pythonhosted.org/packages/6d/39/afaa3de4d491a55af8961081e0b69c08d51bfbe471c359a7bddb4a28ca41/cuda_bindings-12.9.7-cp312-cp312-win_amd64.whl", hash = "sha256:3c089aaf4f5f570ec50244c68f5a2b00a2c9a8e01e04219fd2e36e340be0d88b", size = 7400841, upload-time = "2026-05-27T18:44:17.164Z" }, + { url = "https://files.pythonhosted.org/packages/eb/7b/f1575e41e1a17dc2f2a408b2e8e864c9324e41e3e23f6401e5efc54c152a/cuda_bindings-12.9.7-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:266379e4942051f544a8e7ea1a30ead8d7e8199b6b30fcdc8917cae2bf614e61", size = 6978549, upload-time = "2026-05-27T18:44:18.839Z" }, + { url = "https://files.pythonhosted.org/packages/9d/dc/62d62eb4f91eb721bcf46da51b13e9872ccd8fa7e60eb8ba7b7baeac72c6/cuda_bindings-12.9.7-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59cf4a37b0d662ba15037c9ceebe1a306ebf2c01a8235a09be13cd07094fdb74", size = 7457675, upload-time = "2026-05-27T18:44:20.637Z" }, + { url = "https://files.pythonhosted.org/packages/43/b2/753fe88151001d0dc23f56a8e119fe06b991b0d1a885fa02f9852b12f523/cuda_bindings-12.9.7-cp313-cp313-win_amd64.whl", hash = "sha256:5bd89dcb78475a6d8a4620ea94b74edf0cbbeacee6d1622d8f94452c1e8d3f15", size = 7360097, upload-time = "2026-05-27T18:44:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/f9/77/94d9b85f26add6fe9c9cb7c4ec3b96bc598f7ea5cfbd7490cc0a36adf5be/cuda_bindings-12.9.7-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2dbcd4801954eb3508f4dc2fa0d0c8eb93eb3f45326fd61be2731418c371e7a0", size = 6870886, upload-time = "2026-05-27T18:44:24.164Z" }, + { url = "https://files.pythonhosted.org/packages/04/dd/3ec34b569e1b990b11276feba306bf8f446656cc38e8ed0f49b5facfeffa/cuda_bindings-12.9.7-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3747ea132642416786a8e31bf229032df3a7856911ae5426a7be53d032df183d", size = 7345663, upload-time = "2026-05-27T18:44:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c8/d79a20ba396e7ab2dfdd4b72b62356972b25b88aee2ded49a70c797ddea1/cuda_bindings-12.9.7-cp313-cp313t-win_amd64.whl", hash = "sha256:64f7ade7a7a3b69001489753acc21706d9dbda32db8deb68a767a0a0aab30b68", size = 7780136, upload-time = "2026-05-27T18:44:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/68/e4/075052d42872cf8162da53f14447a4b8abc004c3750e4b724ee502428da0/cuda_bindings-12.9.7-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:775960ac9e530717f3b48e165cc6f68684fa9a4141764fd923e4c1a9820acc73", size = 7060090, upload-time = "2026-05-27T18:44:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/ec/cd/3289c810a4d45e5364a3387a74b4c9b6f6f57ee96ae0e5b537cc61dec242/cuda_bindings-12.9.7-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c47ec1a7a441d91aab32339951df7a1be53451121a12c094bba51467717a35a", size = 7504419, upload-time = "2026-05-27T18:44:31.992Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a0/c429fdcfa5aae181415504c5085ea5944f782b417dd16a7f2a14be0da80d/cuda_bindings-12.9.7-cp314-cp314-win_amd64.whl", hash = "sha256:1e2a4f2ec5b67408c04bb4fbed45d214b66de1f00ee2e972865cacb8708d4e1e", size = 7493876, upload-time = "2026-05-27T18:44:33.618Z" }, + { url = "https://files.pythonhosted.org/packages/11/43/472a6281c3d94e71687e27c657a8f60718d3579b4d94c41deea503165f8a/cuda_bindings-12.9.7-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00a833d399b31071fab4cf3de2929840ae462dc4848116eeff033d09219e7116", size = 6899146, upload-time = "2026-05-27T18:44:35.556Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/10c1d0b32a9da65142d213e0733d748457fb3fd066aee4317335266f15c6/cuda_bindings-12.9.7-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11aeafa2b33995f890086b3fb0f062075176d956e9b6a6fe1a699dddc413f6ad", size = 7369087, upload-time = "2026-05-27T18:44:37.359Z" }, + { url = "https://files.pythonhosted.org/packages/33/10/c71a07cd2a1d4db119bada1848b4752a874ccfe4927d419bfdd05f250920/cuda_bindings-12.9.7-cp314-cp314t-win_amd64.whl", hash = "sha256:ece8dfbc22e6de96a26940ab9887eb3cfe1fc1bc3966169391cdb866bb82bb64", size = 8208198, upload-time = "2026-05-27T18:44:39.053Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version < '3.12'", +] +dependencies = [ + { name = "cuda-pathfinder", marker = "extra == 'extra-10-pyproximal-gpu-cu13'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/95/7a/c5e3c34a409b148f5c0f5a4ea374158f95d488862c1dffedf9aa5c639df9/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04436a9364059c84b8f9636f359eccda1cf814341f5b670c71d80d2f79dbc708", size = 6674166, upload-time = "2026-05-29T23:11:45.478Z" }, + { url = "https://files.pythonhosted.org/packages/93/f7/0e35987a21914f84068061dcf4b61466ccbce1c62ddc9727596d5ed0c26f/cuda_bindings-13.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:507b0e19e7f934c5e30f30f0244ad70a75812619a7d3a0d742543caae1bd50f1", size = 5664286, upload-time = "2026-05-29T23:11:47.719Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/7c/95/872a0392122f1fb43fcb06869790ef3171f37beee9f7db8f441739113570/cuda_bindings-13.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:b134dd8c5c66ae4c4ad814f7aee88fd215353c077010cbc47e3b55ed35ec9eff", size = 5875099, upload-time = "2026-05-29T23:11:54.635Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2732904099e0a4d4db774a5fc6d91ee95fae065b4d2ecabb4968c5fe2406c9d7", size = 6476660, upload-time = "2026-05-29T23:11:59.188Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2f/6a0dd496550c6fafbf6aeb1bf40242eeabb2fd138a43892aabb4be8224c2/cuda_bindings-13.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:18c8c167c8907b8f02531ca810534315c458dabef31f7965095619bf647b9202", size = 5830027, upload-time = "2026-05-29T23:12:01.205Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/bff68ce829999c1e4209c761bbf903b1c06ec570416ddb25020864ad5907/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ab2f74ed65bfef4163ba07a8db16f1085e0729291db12a2423aff84ee8278b8", size = 6013639, upload-time = "2026-05-29T23:12:03.509Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e0/c8a1f0c8f9ffdea4f5fe6dbab89b326cef4d85caf489dad39e209da89416/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd4c814d311ec08c981f6dded1dbe7d4b371067ee4f6c14cccec4bde9590f80", size = 6534419, upload-time = "2026-05-29T23:12:05.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/2734be44dbc80ac082ec23a86b41c8294992dcb90033645ed1bc50aafe4c/cuda_bindings-13.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:8de12ef60bf40756852cb62bbb40460609269f6ece522903d1cc93d73a3ececb", size = 5961055, upload-time = "2026-05-29T23:12:07.971Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/83b1f563925b290f2d11a01a77a84013ba56052fe3653a5bef3ccfbb43d6/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3c772dfff49681541d59630c90f858e173ac926b9c593a2b7123f2a1043cc76", size = 5809771, upload-time = "2026-05-29T23:12:10.422Z" }, + { url = "https://files.pythonhosted.org/packages/12/20/e79b4bfe98f075195afb6343d41c498f9dbd2d161d7021d4d28bceb83581/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36febb7c1079d68a981dbbd8d5a67235b399802b82075c9388624719607e52b9", size = 6358584, upload-time = "2026-05-29T23:12:12.767Z" }, + { url = "https://files.pythonhosted.org/packages/27/2a/b59bcac016ab9985d6b48a5d05b0d698461a159ca03ee11c4abd54da2ac4/cuda_bindings-13.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61120b5e4f4a63f67efd7e7396914cb9ef871bb1f0021e990fb70277be240a4d", size = 6740329, upload-time = "2026-05-29T23:12:15.153Z" }, +] + +[[package]] +name = "cuda-core" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/b9/99e461e485ffa94631b996338a8bc726425914a4aa4b7e4c22899d0f68e6/cuda_core-1.1.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43361914aa9d337555a6ec1f7eaf8580a95689255b074dc0e6d0769b4d737d17", size = 5803221, upload-time = "2026-07-29T23:05:25Z" }, + { url = "https://files.pythonhosted.org/packages/96/f6/e8a5d1242ee7ed146e56c7c31179383443f6fa576cb94f06cec5810dbd38/cuda_core-1.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:169dded199c76e85ab3d7d474454ab784215d856a233ec8cf21adcfa906b499c", size = 6109115, upload-time = "2026-07-29T23:05:26.878Z" }, + { url = "https://files.pythonhosted.org/packages/30/89/8d446413a39ce7c5124cab5c3b0f285c30df74498a8214b42e9646fe3169/cuda_core-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:9410c8845f8c9ab769668c8bfdf21584c7c98a765f56ae37681de82bff3281ab", size = 5679821, upload-time = "2026-07-29T23:05:28.518Z" }, + { url = "https://files.pythonhosted.org/packages/01/24/22c25c350f08529b37bf03a79edb3e66f9b853d7f433a3add15db129f67f/cuda_core-1.1.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95cb3836321a8539e3e199d9e0d1959ebf83e7ce9813cbbc8ac4aef00ea03b6f", size = 5827475, upload-time = "2026-07-29T23:05:30.17Z" }, + { url = "https://files.pythonhosted.org/packages/05/fe/9434d5f1ccc299d30cf9e49522e0f59c641d5700b23c2bd2eb0868b6f0ff/cuda_core-1.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7dbabe18157e819b4df3834014b23481012c1d3cae0b835eb39b3df86438668", size = 6192814, upload-time = "2026-07-29T23:05:32.152Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c7/d576bc9b1fc8d8cd47c8d276483544164abb544110f81480bd16af952c43/cuda_core-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d9fa9d92d1608bf004790347e10df9ade7280d56da1e5dd92b3139b809f4464", size = 5608228, upload-time = "2026-07-29T23:05:34.067Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ad/8a6f9a17290ca6b1f0d1cda17eb5361ffe707e9e118365cb1069caf4124a/cuda_core-1.1.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d9b29e1bd65d0aced26feb785fb8b44bf7c22ca27245e90a69c58a7e48a9d77", size = 5763490, upload-time = "2026-07-29T23:05:35.745Z" }, + { url = "https://files.pythonhosted.org/packages/54/e7/cfe12e5f1423a8eca98bca52fdc982c962745cf3d75c6dddcdd39f0d78c1/cuda_core-1.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91311ffa905d72b0a53599f41f747565b9baed7e1b62eea54904c11e48f87e81", size = 6138302, upload-time = "2026-07-29T23:05:37.496Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/efc25f86f280ba658b5955f91ea9ca26523d9f05b29d2d69d561c513ec08/cuda_core-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:ff1585f687e4081c4731e24076b0af9dd0a55eb5e4a90c608289effa708718fb", size = 5579371, upload-time = "2026-07-29T23:05:40.016Z" }, + { url = "https://files.pythonhosted.org/packages/ae/7c/3fcfbc1e1b09fd389ad8ef809b513e407e5eb71db2558962b0910ee13f04/cuda_core-1.1.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74881deb1d39f9f202b48532a62af4b53d524f2ea71a6a8c9caac2a66cac7d48", size = 5836547, upload-time = "2026-07-29T23:05:41.94Z" }, + { url = "https://files.pythonhosted.org/packages/eb/be/62c73ba4d00aa69687328edcbfed0183923be193f6df97a000ab77518ae5/cuda_core-1.1.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be538c4d2c1c6a994408e2665feb1fc7a0a2605eb4ee202ac50201816e62ab8a", size = 6171860, upload-time = "2026-07-29T23:05:43.882Z" }, + { url = "https://files.pythonhosted.org/packages/78/ca/41432e7990934a15c581acf82049df73f1d328f4f8a484bfa424f868f829/cuda_core-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:30b6ff62f8a68b968c6abe293b07bf89a704b2e8e4fb7948588295867abf3353", size = 5703729, upload-time = "2026-07-29T23:05:45.714Z" }, + { url = "https://files.pythonhosted.org/packages/4a/95/b38fa8ae508fc73bfc3e7e7938a10ffa85e89abf8f58be4f7983972d4911/cuda_core-1.1.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688a5ec8fe1a8acbe2453b170e99db1def74ce6a265a484d4dd49a32eb49de9c", size = 6037546, upload-time = "2026-07-29T23:05:47.757Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ab/ec42016657510301aeebacffaf7a2ed0e20a534b1e8caf33c892e388e94d/cuda_core-1.1.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d09196bddd111f96e043c1fd5e729e1cabe4d69e30a3524bae253d62cdfa504a", size = 6290309, upload-time = "2026-07-29T23:05:49.725Z" }, + { url = "https://files.pythonhosted.org/packages/05/d2/5087a3e69603d4d9ac76346a3535ffc7f9e90350ee957886772367270757/cuda_core-1.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:34fa2e70c2666a1ce7f5c8df156b28f58d717df0786eb2f52be580205b8f377a", size = 6584630, upload-time = "2026-07-29T23:05:51.73Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/01/a7171c5e2e8755597bd8f1c1eb228a0876f502afdf25f936061f5dbe2880/cuda_pathfinder-1.7.0-py3-none-any.whl", hash = "sha256:e9d67e950f3d5992b854dfd25917c3719d0c21d3057b11abe86ba6feec526138", size = 63091, upload-time = "2026-08-24T04:13:56.054Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "12.9.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version < '3.12'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/7e/f61db087382a2114a93abc0b38f7872f86f2c7c36839da7f9403c9044b47/cuda_toolkit-12.9.2.0-py2.py3-none-any.whl", hash = "sha256:b54b8f2fd4200090b44843d48fc5411198e8b3e47a3db56e2518ab588c0ef1e0", size = 2431, upload-time = "2026-04-14T00:40:05.972Z" }, +] + +[package.optional-dependencies] +cccl = [ + { name = "nvidia-cuda-cccl-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +cudart = [ + { name = "nvidia-cuda-runtime-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +nvcc = [ + { name = "nvidia-cuda-nvcc-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version < '3.12'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/a1/54c1e9498ba0df91ca15a46f41af6320cb9faed6ec2dbb30b6cbff8887c4/cuda_toolkit-13.3.1-py2.py3-none-any.whl", hash = "sha256:2ceda460a540323d52469bcfde48b48c1861f6482e4b5ea3cb5bdac00a1b11bd", size = 2656, upload-time = "2026-06-29T17:23:23.848Z" }, +] + +[package.optional-dependencies] +cccl = [ + { name = "nvidia-cuda-cccl", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +nvvm = [ + { name = "nvidia-nvvm", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] + +[[package]] +name = "cupy-cuda12x" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/98/ac56fb7a285e264a0f29ea71d64b5c2eacd23c1f4ed9b4a8f99b16db3881/cupy_cuda12x-14.2.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:1c775069f0af34662a8d4ae90848e29afcaf4ba63762d556ff22b6011683e571", size = 148189237, upload-time = "2026-08-20T02:39:47.011Z" }, + { url = "https://files.pythonhosted.org/packages/d3/49/a83b7664151a7bdfb5d7ca7f29cef4eb5574a4cb8e1f9dfbae7fea372e4f/cupy_cuda12x-14.2.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:5fe2366cc5c61a7ee4a527ce1e8951cb89092d0fb0b5830623cf114d1942c585", size = 136632369, upload-time = "2026-08-20T02:39:51.562Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d0/a3f4c7b7c4d642c7c8cf8ae6128ccd70cb05592f35b89d76281456e3de00/cupy_cuda12x-14.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:eceffbf02a5833c8ba1c94615da07c374284db76a60f8c8b217b0d9d2667162a", size = 98876640, upload-time = "2026-08-20T02:39:55.735Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8c/5fe3f6719c2d4560c79c62ef6d9b7d6c34d145879ddc0c1a41f8153ad0a6/cupy_cuda12x-14.2.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:b74340aa7271f0f081f77e2e5107bac75af19b86df29213db7ada90e14428efe", size = 147490427, upload-time = "2026-08-20T02:40:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5b/65124de2dbaf2e85109f611a41947e39acd6dd938751c04b4c4d7bf6fc82/cupy_cuda12x-14.2.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:f82141761f2c81905d49387464ae29438887956d99063381c93a1d5d1b7d32e8", size = 136111459, upload-time = "2026-08-20T02:40:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/e9/18/ddea819204701024bef7fa748730702245d803847c841b737723b94fd091/cupy_cuda12x-14.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:c9571d3b5f2e65758137e210f7fb3c3b34767f0af6b6ca04035a244b6141ee12", size = 98780018, upload-time = "2026-08-20T02:40:09.465Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/dce7be227a845943d14baef3b58be49c74a465e5d9251f38840b5b1fd89a/cupy_cuda12x-14.2.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:cfe673f73599ee0b9c2c9de5c0bb2395d98c9238c24deafa2ddcc69cacbd6af6", size = 147070561, upload-time = "2026-08-20T02:40:14.556Z" }, + { url = "https://files.pythonhosted.org/packages/c9/02/520f7b9f92114b4df7d88aa77c36db0d556caf76a362537687e3a2e42833/cupy_cuda12x-14.2.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:efc1da23505e88d9834a3ddd3c00352c34e58e301f512d9dd593cc4bfbbdf7dc", size = 135714387, upload-time = "2026-08-20T02:40:19.077Z" }, + { url = "https://files.pythonhosted.org/packages/29/94/2dfb330afc6756ab9a8d16e955c0458e82e769930eab01e6c491e411363d/cupy_cuda12x-14.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:dcea9f2b1887ac631a9275a61577e09d1eea26bf5f95491501c3b7528cebc592", size = 98760068, upload-time = "2026-08-20T02:40:23.43Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d3/f6639af54f5872d1ef0c523601c7fe76d28783e71a3e8533e096c9ca1d43/cupy_cuda12x-14.2.0-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:ed317136439af4780f217eda0b82f25180084eb16c44854e1bc9e055f96fd429", size = 146942115, upload-time = "2026-08-20T02:40:28.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5e/e6134253265fefc0a35356adcebc4e3ffa81f6c9a2a74f8f9e2de32b3018/cupy_cuda12x-14.2.0-cp314-cp314-manylinux2014_x86_64.whl", hash = "sha256:db802e4b9a85ed84fd3e84790586c06e808ee45e0214cd4e80734c09fcf93073", size = 135091580, upload-time = "2026-08-20T02:40:33.351Z" }, + { url = "https://files.pythonhosted.org/packages/0a/98/4d3215440b7a0d8661295050653760b57f32c933f1ef1c81841b7329209e/cupy_cuda12x-14.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:5f08fc1d651d2446c1d18ad94f1a710224fab36d46634d4aa356423926964591", size = 99344061, upload-time = "2026-08-20T02:40:37.459Z" }, + { url = "https://files.pythonhosted.org/packages/3a/e9/8ed4adeb8c64f188b9ea6fba3be62fb7999584308bdf7ec6c5e17f77b99c/cupy_cuda12x-14.2.0-cp314-cp314t-manylinux2014_aarch64.whl", hash = "sha256:9dd33f9cfc7aefbd935879bf50e95db539721a0702bdb05be3c74bd46a85ba29", size = 147438535, upload-time = "2026-08-20T02:40:42.11Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c9/73227968a5b01ac31eaf1d5c58b4318e4b83654ed6dac3c310c7b2075c36/cupy_cuda12x-14.2.0-cp314-cp314t-manylinux2014_x86_64.whl", hash = "sha256:8cbbd48c9cfd6b78d0a833ebbafda3e1b057c38d6acc3c6e54de0735a7364e27", size = 135455541, upload-time = "2026-08-20T02:40:46.804Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/26127dd01e08ed645a70b4084ef6dde93e6a75b0a84fddc3ac6b11b05bf7/cupy_cuda12x-14.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d14b651ed835079f8a5e273936e02eda690be7d30f2658e5f48f328322fd9d7b", size = 100242434, upload-time = "2026-08-20T02:40:51.04Z" }, +] + +[[package]] +name = "cupy-cuda13x" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3c/4716e285f8459a5707ef39d84e9a7547bf16f87c583ac4789c0763d6d986/cupy_cuda13x-14.2.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:a22e2f17c97940cd566f351f186d8fe04285280d052ca1512ff223febbe9979c", size = 74074375, upload-time = "2026-08-20T02:41:04.327Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cb/41b6d8f1880e6f3128f8084b7622b1a3ca2d615ab77af9ef313b4aafed3d/cupy_cuda13x-14.2.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:f94843e54500a9c8c169135f79572bfaba946efe4f7178e834c227280c1b0c12", size = 69969199, upload-time = "2026-08-20T02:41:07.842Z" }, + { url = "https://files.pythonhosted.org/packages/f8/eb/25b8c1a415ade6479ed684c5cbff1e6f5002a92cbf26ef826df56a40cf69/cupy_cuda13x-14.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:3893acd8d4dc78d113736045eaa306e496ee7bd8253bd959513ba28c91ed396d", size = 36082966, upload-time = "2026-08-20T02:41:10.723Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c8/68e07ff959f6a9f93169e37a64a4c24223487102b8625191e4a566cab465/cupy_cuda13x-14.2.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:0a9b6538079c5151dcd99cf44b94cc3f1242315b7de0498b4619cef9b2a7c2c8", size = 73382974, upload-time = "2026-08-20T02:41:13.648Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/c4c25e97552320451afdcd459d4b6e0b715459594e998b23051c06db64c3/cupy_cuda13x-14.2.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:9aafadbe29db3d4285026044aafeb28f2a3de4bd0df70718ec6892b7d27a4ded", size = 69439247, upload-time = "2026-08-20T02:41:17.375Z" }, + { url = "https://files.pythonhosted.org/packages/5e/88/0a4d2d075485c4a82bfcd4725300ff87d17de36dce27b36f720d286f0f70/cupy_cuda13x-14.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:9e8bc8a7693c5c212cd1450d728f47e1f05aa07ae003de30415893d0daf7d243", size = 35986921, upload-time = "2026-08-20T02:41:20.253Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3cb39774c71a16275c50b5d1e92ddcefa948ff1e69f464249b70a79b0abe/cupy_cuda13x-14.2.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:449af7f608dc70ce64e8c45cf5a6ea0bbef51623e7c806b1a43445ae3c76e1e0", size = 72969322, upload-time = "2026-08-20T02:41:23.311Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/216370ae8c39fa9835645be3c27513ab72f52cf4f51faf58d799fba981b6/cupy_cuda13x-14.2.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:5b79a606f6639d9bed74ef7072f36756a95a804c86d7adb367b39307ac771600", size = 69048238, upload-time = "2026-08-20T02:41:26.831Z" }, + { url = "https://files.pythonhosted.org/packages/5d/75/9df3c545baad4ff2e384db1ff6c8005fa4040492c4ee5672ca8b8711d29d/cupy_cuda13x-14.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9a2a55abf889f6df68b5f62cd5f3ec4a2f305ac57ed09becd52e16201edcd86", size = 35965490, upload-time = "2026-08-20T02:41:29.943Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/8c80c9e7010193afed8ccb7110d6173aa23bf3a6ce2fc2450244b0fa50a1/cupy_cuda13x-14.2.0-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:7ac57c4e16c62f265b5ecc2b45d6bbafe9dc70a99b849ed6cfb8d16f32204589", size = 72840193, upload-time = "2026-08-20T02:41:33.146Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b8/4f4c4f34fc31ab8d136ed965a919505297974629d9852c50e15bfd616281/cupy_cuda13x-14.2.0-cp314-cp314-manylinux2014_x86_64.whl", hash = "sha256:ff0bdebd1b43c0c6db53095784c787c4e4eae671356cf521c4b7482ed78a1a7e", size = 68422828, upload-time = "2026-08-20T02:41:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5c/58941ece5ed0b2cc8304f8ba81b5e8334759d409306475d53c52ac99809b/cupy_cuda13x-14.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:b99a8bf9d5391954c0c08675a8cb39219923fe2ee249faa3546ebbf75783a360", size = 36110882, upload-time = "2026-08-20T02:41:39.837Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/a582ac70e9e75d3fad7ae073051bcd50dd128a1f66dd275291dc5f48d997/cupy_cuda13x-14.2.0-cp314-cp314t-manylinux2014_aarch64.whl", hash = "sha256:e8bb453551deced08ec5c4576da61ebf1e2b4a45aeff48e09f47ec332f7cad3d", size = 73336756, upload-time = "2026-08-20T02:41:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/f9/aa/222151ba5061d7a01efe4009efad9f9a7164e15bb2a494d9aa86efc97d4f/cupy_cuda13x-14.2.0-cp314-cp314t-manylinux2014_x86_64.whl", hash = "sha256:313500c5d415f65d388e84a9d2eb7f07f97c23c85dc5785f986e75d46d9c9693", size = 68792663, upload-time = "2026-08-20T02:41:46.079Z" }, + { url = "https://files.pythonhosted.org/packages/d4/20/13dd37dbeaec879c8348fde2bfca67d96daa56a6e63e4a5083ff61660e53/cupy_cuda13x-14.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c517fc55502a7e3d1a8b9310abad95cec61dded5a38d328fe0220bcf7b036268", size = 37004845, upload-time = "2026-08-20T02:41:48.963Z" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -603,13 +684,12 @@ name = "django" version = "5.2.10" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.11.*'", - "python_full_version < '3.11'", + "python_full_version < '3.12'", ] dependencies = [ - { name = "asgiref", marker = "python_full_version < '3.12'" }, - { name = "sqlparse", marker = "python_full_version < '3.12'" }, - { name = "tzdata", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "asgiref", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sqlparse", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "tzdata", marker = "(python_full_version < '3.12' and sys_platform == 'win32') or (python_full_version >= '3.12' and extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13') or (sys_platform != 'win32' and extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e6/e5/2671df24bf0ded831768ef79532e5a7922485411a5696f6d979568591a37/django-5.2.10.tar.gz", hash = "sha256:74df100784c288c50a2b5cad59631d71214f40f72051d5af3fdf220c20bdbbbe", size = 10880754, upload-time = "2026-01-06T18:55:26.817Z" } wheels = [ @@ -624,52 +704,24 @@ resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ - { name = "asgiref", marker = "python_full_version >= '3.12'" }, - { name = "sqlparse", marker = "python_full_version >= '3.12'" }, - { name = "tzdata", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "asgiref", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sqlparse", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "tzdata", marker = "(python_full_version >= '3.12' and sys_platform == 'win32') or (python_full_version < '3.12' and extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13') or (sys_platform != 'win32' and extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b5/9b/016f7e55e855ee738a352b05139d4f8b278d0b451bd01ebef07456ef3b0e/django-6.0.1.tar.gz", hash = "sha256:ed76a7af4da21551573b3d9dfc1f53e20dd2e6c7d70a3adc93eedb6338130a5f", size = 11069565, upload-time = "2026-01-06T18:55:53.069Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/95/b5/814ed98bd21235c116fd3436a7ed44d47560329a6d694ec8aac2982dbb93/django-6.0.1-py3-none-any.whl", hash = "sha256:a92a4ff14f664a896f9849009cb8afaca7abe0d6fc53325f3d1895a15253433d", size = 8338791, upload-time = "2026-01-06T18:55:46.175Z" }, ] -[[package]] -name = "docutils" -version = "0.21.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, -] - [[package]] name = "docutils" version = "0.22.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - [[package]] name = "fastjsonschema" version = "2.21.2" @@ -694,14 +746,6 @@ version = "4.61.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" }, - { url = "https://files.pythonhosted.org/packages/94/93/c2e682faaa5ee92034818d8f8a8145ae73eb83619600495dcf8503fa7771/fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958", size = 2403032, upload-time = "2025-12-12T17:29:30.115Z" }, - { url = "https://files.pythonhosted.org/packages/f1/62/1748f7e7e1ee41aa52279fd2e3a6d0733dc42a673b16932bad8e5d0c8b28/fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da", size = 4897863, upload-time = "2025-12-12T17:29:32.535Z" }, - { url = "https://files.pythonhosted.org/packages/69/69/4ca02ee367d2c98edcaeb83fc278d20972502ee071214ad9d8ca85e06080/fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6", size = 4859076, upload-time = "2025-12-12T17:29:34.907Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f5/660f9e3cefa078861a7f099107c6d203b568a6227eef163dd173bfc56bdc/fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1", size = 4875623, upload-time = "2025-12-12T17:29:37.33Z" }, - { url = "https://files.pythonhosted.org/packages/63/d1/9d7c5091d2276ed47795c131c1bf9316c3c1ab2789c22e2f59e0572ccd38/fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881", size = 4993327, upload-time = "2025-12-12T17:29:39.781Z" }, - { url = "https://files.pythonhosted.org/packages/6f/2d/28def73837885ae32260d07660a052b99f0aa00454867d33745dfe49dbf0/fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47", size = 1502180, upload-time = "2025-12-12T17:29:42.217Z" }, - { url = "https://files.pythonhosted.org/packages/63/fa/bfdc98abb4dd2bd491033e85e3ba69a2313c850e759a6daa014bc9433b0f/fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6", size = 1550654, upload-time = "2025-12-12T17:29:44.564Z" }, { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, @@ -768,8 +812,8 @@ name = "image" version = "1.5.33" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", version = "5.2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "django", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "django", version = "5.2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "django", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, { name = "pillow" }, { name = "six" }, ] @@ -780,8 +824,7 @@ name = "imageio" version = "2.37.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "pillow" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } @@ -890,19 +933,6 @@ version = "1.4.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, - { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" }, - { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" }, - { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" }, - { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" }, - { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" }, - { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" }, - { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" }, - { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" }, - { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" }, - { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" }, { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, @@ -980,11 +1010,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, - { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" }, - { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" }, - { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" }, { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, @@ -1010,16 +1035,6 @@ version = "0.7.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, - { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, - { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, - { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, - { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, - { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, - { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, - { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, @@ -1083,10 +1098,6 @@ version = "0.46.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/a4/3959e1c61c5ca9db7921e5fd115b344c29b9d57a5dadd87bef97963ca1a5/llvmlite-0.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4323177e936d61ae0f73e653e2e614284d97d14d5dd12579adc92b6c2b0597b0", size = 37232766, upload-time = "2025-12-08T18:14:34.765Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a5/a4d916f1015106e1da876028606a8e87fd5d5c840f98c87bc2d5153b6a2f/llvmlite-0.46.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a2d461cb89537b7c20feb04c46c32e12d5ad4f0896c9dfc0f60336219ff248e", size = 56275176, upload-time = "2025-12-08T18:14:37.944Z" }, - { url = "https://files.pythonhosted.org/packages/79/7f/a7f2028805dac8c1a6fae7bda4e739b7ebbcd45b29e15bf6d21556fcd3d5/llvmlite-0.46.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1f6595a35b7b39c3518b85a28bf18f45e075264e4b2dce3f0c2a4f232b4a910", size = 55128629, upload-time = "2025-12-08T18:14:41.674Z" }, - { url = "https://files.pythonhosted.org/packages/b2/bc/4689e1ba0c073c196b594471eb21be0aa51d9e64b911728aa13cd85ef0ae/llvmlite-0.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:e7a34d4aa6f9a97ee006b504be6d2b8cb7f755b80ab2f344dda1ef992f828559", size = 38138651, upload-time = "2025-12-08T18:14:45.845Z" }, { url = "https://files.pythonhosted.org/packages/7a/a1/2ad4b2367915faeebe8447f0a057861f646dbf5fbbb3561db42c65659cf3/llvmlite-0.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82f3d39b16f19aa1a56d5fe625883a6ab600d5cc9ea8906cca70ce94cabba067", size = 37232766, upload-time = "2025-12-08T18:14:48.836Z" }, { url = "https://files.pythonhosted.org/packages/12/b5/99cf8772fdd846c07da4fd70f07812a3c8fd17ea2409522c946bb0f2b277/llvmlite-0.46.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3df43900119803bbc52720e758c76f316a9a0f34612a886862dfe0a5591a17e", size = 56275175, upload-time = "2025-12-08T18:14:51.604Z" }, { url = "https://files.pythonhosted.org/packages/38/f2/ed806f9c003563732da156139c45d970ee435bd0bfa5ed8de87ba972b452/llvmlite-0.46.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de183fefc8022d21b0aa37fc3e90410bc3524aed8617f0ff76732fc6c3af5361", size = 55128630, upload-time = "2025-12-08T18:14:55.107Z" }, @@ -1111,17 +1122,6 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, @@ -1195,13 +1195,11 @@ name = "matplotlib" version = "3.10.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "contourpy" }, { name = "cycler" }, { name = "fonttools" }, { name = "kiwisolver" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "packaging" }, { name = "pillow" }, { name = "pyparsing" }, @@ -1209,12 +1207,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, - { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, - { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, @@ -1257,9 +1249,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, - { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, @@ -1269,9 +1258,6 @@ wheels = [ name = "mistune" version = "3.2.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, @@ -1282,20 +1268,13 @@ name = "mypy" version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, @@ -1392,14 +1371,12 @@ name = "nbsphinx" version = "0.9.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "docutils" }, { name = "jinja2" }, { name = "nbconvert" }, { name = "nbformat" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e7/d1/82081750f8a78ad0399c6ed831d42623b891904e8e7b8a75878225cf1dce/nbsphinx-0.9.8.tar.gz", hash = "sha256:d0765908399a8ee2b57be7ae881cf2ea58d66db3af7bbf33e6eb48f83bea5495", size = 417469, upload-time = "2025-11-28T17:41:02.336Z" } @@ -1407,26 +1384,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl", hash = "sha256:92d95ee91784e56bc633b60b767a6b6f23a0445f891e24641ce3c3f004759ccf", size = 31961, upload-time = "2025-11-28T17:41:00.796Z" }, ] -[[package]] -name = "networkx" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, -] - [[package]] name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, @@ -1447,15 +1408,10 @@ version = "0.63.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "llvmlite" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dc/60/0145d479b2209bd8fdae5f44201eceb8ce5a23e0ed54c71f57db24618665/numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b", size = 2761666, upload-time = "2025-12-10T02:57:39.002Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/ce/5283d4ffa568f795bb0fd61ee1f0efc0c6094b94209259167fc8d4276bde/numba-0.63.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6d6bf5bf00f7db629305caaec82a2ffb8abe2bf45eaad0d0738dc7de4113779", size = 2680810, upload-time = "2025-12-10T02:56:55.269Z" }, - { url = "https://files.pythonhosted.org/packages/0f/72/a8bda517e26d912633b32626333339b7c769ea73a5c688365ea5f88fd07e/numba-0.63.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08653d0dfc9cc9c4c9a8fba29ceb1f2d5340c3b86c4a7e5e07e42b643bc6a2f4", size = 3739735, upload-time = "2025-12-10T02:56:57.922Z" }, - { url = "https://files.pythonhosted.org/packages/ca/17/1913b7c1173b2db30fb7a9696892a7c4c59aeee777a9af6859e9e01bac51/numba-0.63.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09eebf5650246ce2a4e9a8d38270e2d4b0b0ae978103bafb38ed7adc5ea906e", size = 3446707, upload-time = "2025-12-10T02:56:59.837Z" }, - { url = "https://files.pythonhosted.org/packages/b4/77/703db56c3061e9fdad5e79c91452947fdeb2ec0bdfe4affe9b144e7025e0/numba-0.63.1-cp310-cp310-win_amd64.whl", hash = "sha256:f8bba17421d865d8c0f7be2142754ebce53e009daba41c44cf6909207d1a8d7d", size = 2747374, upload-time = "2025-12-10T02:57:07.908Z" }, { url = "https://files.pythonhosted.org/packages/70/90/5f8614c165d2e256fbc6c57028519db6f32e4982475a372bbe550ea0454c/numba-0.63.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b33db00f18ccc790ee9911ce03fcdfe9d5124637d1ecc266f5ae0df06e02fec3", size = 2680501, upload-time = "2025-12-10T02:57:09.797Z" }, { url = "https://files.pythonhosted.org/packages/dc/9d/d0afc4cf915edd8eadd9b2ab5b696242886ee4f97720d9322650d66a88c6/numba-0.63.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d31ea186a78a7c0f6b1b2a3fe68057fdb291b045c52d86232b5383b6cf4fc25", size = 3744945, upload-time = "2025-12-10T02:57:11.697Z" }, { url = "https://files.pythonhosted.org/packages/05/a9/d82f38f2ab73f3be6f838a826b545b80339762ee8969c16a8bf1d39395a8/numba-0.63.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed3bb2fbdb651d6aac394388130a7001aab6f4541837123a4b4ab8b02716530c", size = 3450827, upload-time = "2025-12-10T02:57:13.709Z" }, @@ -1475,78 +1431,48 @@ wheels = [ ] [[package]] -name = "numpy" -version = "2.2.6" +name = "numba-cuda" +version = "0.30.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, - { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, - { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, - { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, - { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, - { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, - { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, - { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, - { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, - { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, - { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, - { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, - { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, - { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, - { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, - { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, - { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +dependencies = [ + { name = "cuda-bindings", version = "12.9.7", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-pyproximal-gpu-cu12'" }, + { name = "cuda-bindings", version = "13.3.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-pyproximal-gpu-cu13'" }, + { name = "cuda-core" }, + { name = "cuda-pathfinder" }, + { name = "numba" }, + { name = "packaging" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d8/7c3d78188e7e3aff630634fbb7e60ed778349f3aee2f4f5c1f02d7edeedd/numba_cuda-0.30.4-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:757d1c725083602f51c56fb43be3d94593d04769dace306a0aa055c6ac317380", size = 1861431, upload-time = "2026-07-03T03:49:59.827Z" }, + { url = "https://files.pythonhosted.org/packages/63/b0/6672fc36cf6604b73cd7faf608c3a70c47922c6782dcc682e8978fb12cf5/numba_cuda-0.30.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59b0e895a4884a26323b9581ac6966b7b063b6f0ce87ac23d1192c95158f4d1e", size = 1858159, upload-time = "2026-07-03T03:50:01.745Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e7/487379fc377c990afe703c3579a516374f818b58911adb616cb09433d303/numba_cuda-0.30.4-cp311-cp311-win_amd64.whl", hash = "sha256:4b98cb4242b5ef62f704bb5a19ef6db3ec7ab8c9f9a6d5b732e489180cfc5947", size = 1878926, upload-time = "2026-07-03T03:50:03.858Z" }, + { url = "https://files.pythonhosted.org/packages/7c/14/47b329af0047e27e1f0fe2a2f5544f5c6660a01e1a1e0fe8445dddd5747f/numba_cuda-0.30.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d363e80f0e98d3e6616ced13f226b0e70295085ad97f682c2259eac3ecd1d052", size = 1902336, upload-time = "2026-07-03T03:50:05.839Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b6/1db1a2f1164b82e5fd867e880dfd964516f1382fb7d35a916c0c2929fa55/numba_cuda-0.30.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f229a9314c98a6084e43c3a29ae1568a182bdcab7e87d8fc628a651ea2336ca", size = 1899900, upload-time = "2026-07-03T03:50:07.778Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1e/d873bd274561eb1484be3bba59a2016c30b46edb9a0fa49014b2a5bc06f8/numba_cuda-0.30.4-cp312-cp312-win_amd64.whl", hash = "sha256:478ce786d562eb04e73087442967e0f49cbdc7f5d24c316eff3ea390e657ee38", size = 1879110, upload-time = "2026-07-03T03:50:09.493Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/dc09092e69115eca1b49a2edb1112b424fb2b105e2cb0b2d0497b7da09ec/numba_cuda-0.30.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c667006d2331b3dcdc98863cfe16d125bbc3852b9cbd5454cfd4d7fe866acce0", size = 1909491, upload-time = "2026-07-03T03:50:11.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/8550fb0dd4ae4e8ef58ea6dc1465395f1f9bfa5220ef2ff857fb20a2aa52/numba_cuda-0.30.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ed7f67afaf92dd9973c3985d8323ddd3f53a81aa9cf96b4f3275f3bbfb7182d3", size = 1907244, upload-time = "2026-07-03T03:50:13.082Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/a2619a7c14688d0ed4fc11cf24bc71b66795619b7db9f45664dff79f5bd2/numba_cuda-0.30.4-cp313-cp313-win_amd64.whl", hash = "sha256:efebb025ef48475414b66c4f4973b11b8fdf6bd6eb8c73be32edcbd66503355e", size = 1879200, upload-time = "2026-07-03T03:50:14.984Z" }, + { url = "https://files.pythonhosted.org/packages/bc/35/3c2a05375b01159632ed90384a04a08725b83b49d2e511f44158c5ec2c43/numba_cuda-0.30.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b153d7f59e9a7834dc5866681b607317c72f0d49f6bbfed06eef82b396cbee84", size = 1878752, upload-time = "2026-07-03T03:50:17.092Z" }, + { url = "https://files.pythonhosted.org/packages/df/73/8934508d27efd9a930489059b980bc1e75336574be31bffb284a9e782fa7/numba_cuda-0.30.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec8e14ab7d4ecacaa1ff8d662efe5ab3c268e2f1cdad0330d28804b0d19a78c1", size = 1876463, upload-time = "2026-07-03T03:50:19.237Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e3/613f6fc457f430775d124bea2e38033802fe97ab412a032191ac9b117d3a/numba_cuda-0.30.4-cp314-cp314-win_amd64.whl", hash = "sha256:a99227a6a7bc9f953a7abb61917d5ff767b9667f9c01e28d0b6b1def8f0fd991", size = 1884556, upload-time = "2026-07-03T03:50:21.483Z" }, +] + +[package.optional-dependencies] +cu12 = [ + { name = "cuda-bindings", version = "12.9.7", source = { registry = "https://pypi.org/simple" } }, + { name = "cuda-toolkit", version = "12.9.2.0", source = { registry = "https://pypi.org/simple" }, extra = ["cccl", "cudart", "nvcc", "nvrtc"], marker = "extra == 'extra-10-pyproximal-gpu-cu12'" }, + { name = "nvidia-nvjitlink-cu12" }, +] +cu13 = [ + { name = "cuda-bindings", version = "13.3.1", source = { registry = "https://pypi.org/simple" } }, + { name = "cuda-toolkit", version = "13.3.1", source = { registry = "https://pypi.org/simple" }, extra = ["cccl", "cudart", "nvrtc", "nvvm"], marker = "extra == 'extra-10-pyproximal-gpu-cu13'" }, + { name = "nvidia-nvjitlink" }, ] [[package]] name = "numpy" version = "2.3.5" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, @@ -1629,16 +1555,114 @@ name = "numpydoc" version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/3c/dfccc9e7dee357fb2aa13c3890d952a370dd0ed071e0f7ed62ed0df567c1/numpydoc-1.10.0.tar.gz", hash = "sha256:3f7970f6eee30912260a6b31ac72bba2432830cd6722569ec17ee8d3ef5ffa01", size = 94027, upload-time = "2025-12-02T16:39:12.937Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl", hash = "sha256:3149da9874af890bcc2a82ef7aae5484e5aa81cb2778f08e3c307ba6d963721b", size = 69255, upload-time = "2025-12-02T16:39:11.561Z" }, ] +[[package]] +name = "nvidia-cuda-cccl" +version = "13.3.3.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/bd/572971ffc14bd36676c821fc15d991b08fe6179cb09368250147475f954d/nvidia_cuda_cccl-13.3.3.4.1-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:067d19b4b3c9d0f2ebec9f29a311b2863db96bf98e058bbc331597d51ce818cf", size = 3454030, upload-time = "2026-06-29T16:41:49.092Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ab/049726d90147865a3ea53bae6cb7c35b98bf1fdf96cdb967101329625f83/nvidia_cuda_cccl-13.3.3.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc0adc188d570b09f4d606c7dc05a42aa3d8aa082e0d60f7bbfc5b6435f627c6", size = 3454034, upload-time = "2026-06-29T16:42:07.435Z" }, + { url = "https://files.pythonhosted.org/packages/24/d3/b1afcd9c40ceca72022579215fcaf5318cd747fd896cb928d4a1de924ff8/nvidia_cuda_cccl-13.3.3.4.1-py3-none-win_amd64.whl", hash = "sha256:d7c92cc03047031fa7af30866636d35ce4af409c28fc7dd8f69cb17053741399", size = 3454014, upload-time = "2026-06-29T17:09:09.012Z" }, +] + +[[package]] +name = "nvidia-cuda-cccl-cu12" +version = "12.9.27" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/7e/82e49956b046bdc506c789235c587d9b3ef58b8bc1782258c1e247229647/nvidia_cuda_cccl_cu12-12.9.27-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7898b38aa68beaa234d48f0868273702342a196d6e2e9d0ef058dca2390ebea", size = 3152245, upload-time = "2025-05-01T19:32:04.802Z" }, + { url = "https://files.pythonhosted.org/packages/18/2a/d4cd8506d2044e082f8cd921be57392e6a9b5ccd3ffdf050362430a3d5d5/nvidia_cuda_cccl_cu12-12.9.27-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37869e17ce2e1ecec6eddf1927cca0f8c34e64fd848d40453df559091e2d7117", size = 3152243, upload-time = "2025-05-01T19:32:13.955Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9b/1daf405620c7ac371b76b823c6336dd742673d41a150d9a04eec2c690379/nvidia_cuda_cccl_cu12-12.9.27-py3-none-win_amd64.whl", hash = "sha256:72106f95a9bb3be18472806b4f663ebf0f9248a86d14b4ae3305725b855d9d92", size = 3152175, upload-time = "2025-05-01T19:45:11.372Z" }, +] + +[[package]] +name = "nvidia-cuda-nvcc-cu12" +version = "12.9.86" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/48/b54a06168a2190572a312bfe4ce443687773eb61367ced31e064953dd2f7/nvidia_cuda_nvcc_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:5d6a0d32fdc7ea39917c20065614ae93add6f577d840233237ff08e9a38f58f0", size = 40546229, upload-time = "2025-06-05T20:01:53.357Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/8cc072436787104bbbcbde1f76ab4a0d89e68f7cebc758dd2ad7913a43d0/nvidia_cuda_nvcc_cu12-12.9.86-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44e1eca4d08926193a558d2434b1bf83d57b4d5743e0c431c0c83d51da1df62b", size = 39411138, upload-time = "2025-06-05T20:01:43.182Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9e/c71c53655a65d7531c89421c282359e2f626838762f1ce6180ea0bbebd29/nvidia_cuda_nvcc_cu12-12.9.86-py3-none-win_amd64.whl", hash = "sha256:8ed7f0b17dea662755395be029376db3b94fed5cbb17c2d35cc866c5b1b84099", size = 34669845, upload-time = "2025-06-05T20:11:56.308Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.3.33" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/2c/86916c8a34dcdb0c3ddd1c0e30545041bd781184e437b9cb76fcda70560b/nvidia_cuda_nvrtc-13.3.33-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:82530788b8c6164a54d3fd9ae8bcca8893d397c4aeb998861982a03bbe41e204", size = 51110910, upload-time = "2026-05-26T16:38:16.116Z" }, + { url = "https://files.pythonhosted.org/packages/e7/b6/60a3641111d39ebfcfcd8b8bfd0290d7623c4b8b5f90952c2d84776f8ca4/nvidia_cuda_nvrtc-13.3.33-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7b05ecda494c6dabc44231a608b060a71008a730d9dfda932cc508e6d29159e0", size = 49260054, upload-time = "2026-05-26T16:37:51.177Z" }, + { url = "https://files.pythonhosted.org/packages/a1/42/edce72f2c5a0f587168109c867f25f4a9a6cd7289ecf0d68ed2b1070f273/nvidia_cuda_nvrtc-13.3.33-py3-none-win_amd64.whl", hash = "sha256:7d2af818851c0c224d5f92221e9226e51ee23c236df4b51f9194563979c888be", size = 45319163, upload-time = "2026-05-26T17:02:49.217Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.9.86" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/85/e4af82cc9202023862090bfca4ea827d533329e925c758f0cde964cb54b7/nvidia_cuda_nvrtc_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:210cf05005a447e29214e9ce50851e83fc5f4358df8b453155d5e1918094dcb4", size = 89568129, upload-time = "2025-06-05T20:02:41.973Z" }, + { url = "https://files.pythonhosted.org/packages/64/eb/c2295044b8f3b3b08860e2f6a912b702fc92568a167259df5dddb78f325e/nvidia_cuda_nvrtc_cu12-12.9.86-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:096d4de6bda726415dfaf3198d4f5c522b8e70139c97feef5cd2ca6d4cd9cead", size = 44528905, upload-time = "2025-06-05T20:02:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/52/de/823919be3b9d0ccbf1f784035423c5f18f4267fb0123558d58b813c6ec86/nvidia_cuda_nvrtc_cu12-12.9.86-py3-none-win_amd64.whl", hash = "sha256:72972ebdcf504d69462d3bcd67e7b81edd25d0fb85a2c46d3ea3517666636349", size = 76408187, upload-time = "2025-06-05T20:12:27.819Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.3.29" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/e5/c1a221c8e6fecd071b80ea44c20fc253ae24f56e15e3f77cfbc3fb76e724/nvidia_cuda_runtime-13.3.29-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:73291e19c9dd919c140c91bda2f80b0eca487da5ee30a086ef7bc4918ecb90ea", size = 2356574, upload-time = "2026-05-26T16:29:56.333Z" }, + { url = "https://files.pythonhosted.org/packages/97/be/5699b6e642b372f7d24c59c2f41383e2696825e20bab85f7399c7c6a56f7/nvidia_cuda_runtime-13.3.29-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e04420616e72f563167a7733272992d7e6df6dc5cb54b2f94f9f1520ea9e30c1", size = 2339786, upload-time = "2026-05-26T16:30:21.584Z" }, + { url = "https://files.pythonhosted.org/packages/d2/27/b53a5e0397842a5c11f0e1a39d4e5b2f22638a4126e83b3c4e196f62c969/nvidia_cuda_runtime-13.3.29-py3-none-win_amd64.whl", hash = "sha256:0667ec61c3d897388efa305ed4f7609ace88849a753ba9c6311d06dca55fff4f", size = 2630354, upload-time = "2026-05-26T17:00:05.389Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.9.79" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/e0/0279bd94539fda525e0c8538db29b72a5a8495b0c12173113471d28bce78/nvidia_cuda_runtime_cu12-12.9.79-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83469a846206f2a733db0c42e223589ab62fd2fabac4432d2f8802de4bded0a4", size = 3515012, upload-time = "2025-06-05T20:00:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/bc/46/a92db19b8309581092a3add7e6fceb4c301a3fd233969856a8cbf042cd3c/nvidia_cuda_runtime_cu12-12.9.79-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25bba2dfb01d48a9b59ca474a1ac43c6ebf7011f1b0b8cc44f54eb6ac48a96c3", size = 3493179, upload-time = "2025-06-05T20:00:53.735Z" }, + { url = "https://files.pythonhosted.org/packages/59/df/e7c3a360be4f7b93cee39271b792669baeb3846c58a4df6dfcf187a7ffab/nvidia_cuda_runtime_cu12-12.9.79-py3-none-win_amd64.whl", hash = "sha256:8e018af8fa02363876860388bd10ccb89eb9ab8fb0aa749aaf58430a9f7c4891", size = 3591604, upload-time = "2025-06-05T20:11:17.036Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.3.33" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/ee/580ca6f29dcab0221db8706badca1bbbb084f1975c4d4e83329c3a7e31f0/nvidia_nvjitlink-13.3.33-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:26a6de7fb4c8fdaa7703d3dad720d6d427ddfea5c48a528fd97c11733ad830e5", size = 40742423, upload-time = "2026-05-26T16:54:51.613Z" }, + { url = "https://files.pythonhosted.org/packages/69/30/45414e35ff2eee7db3da037e5707037ccf9d2b5218ffbdb055ea4d5aa98a/nvidia_nvjitlink-13.3.33-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce48b37dfeb3cb1eae4cf85adacb47d7a6539ea2272870c9a3628ce275c2037e", size = 39168635, upload-time = "2026-05-26T16:54:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/67/f2/ec9c05a108095828dfc58840978c627b3c313fdf2a567c6de9ffbbb46901/nvidia_nvjitlink-13.3.33-py3-none-win_amd64.whl", hash = "sha256:4297ee49639b4f2e07255a1d69b3acc7ab2d011bb892b403e91ac98368962e3b", size = 37766359, upload-time = "2026-05-26T17:11:28.96Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.9.86" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/0c/c75bbfb967457a0b7670b8ad267bfc4fffdf341c074e0a80db06c24ccfd4/nvidia_nvjitlink_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:e3f1171dbdc83c5932a45f0f4c99180a70de9bd2718c1ab77d14104f6d7147f9", size = 39748338, upload-time = "2025-06-05T20:10:25.613Z" }, + { url = "https://files.pythonhosted.org/packages/97/bc/2dcba8e70cf3115b400fef54f213bcd6715a3195eba000f8330f11e40c45/nvidia_nvjitlink_cu12-12.9.86-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:994a05ef08ef4b0b299829cde613a424382aff7efb08a7172c1fa616cc3af2ca", size = 39514880, upload-time = "2025-06-05T20:10:04.89Z" }, + { url = "https://files.pythonhosted.org/packages/dd/7e/2eecb277d8a98184d881fb98a738363fd4f14577a4d2d7f8264266e82623/nvidia_nvjitlink_cu12-12.9.86-py3-none-win_amd64.whl", hash = "sha256:cc6fcec260ca843c10e34c936921a1c426b351753587fdd638e8cff7b16bb9db", size = 35584936, upload-time = "2025-06-05T20:16:08.525Z" }, +] + +[[package]] +name = "nvidia-nvvm" +version = "13.3.73" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/e7/ff646aa6015c7e6d12aad234e68925c87b6681d8d18c3ac40535994a3b0d/nvidia_nvvm-13.3.73-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:0e28e0858a3475e11ac67d35301cd5bf82666a1c0dc4ec4e80ceaf3a5fd1dea8", size = 69250424, upload-time = "2026-06-29T17:08:07.453Z" }, + { url = "https://files.pythonhosted.org/packages/2f/05/35754a7105563fd9b496e5ee8e1acd986aef8258760c3cbccf419aee861a/nvidia_nvvm-13.3.73-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2bcdd5783b5481445f1f0e7170cb836cc0d72999839ba850bbba6dc97b76bb8", size = 66984478, upload-time = "2026-06-29T17:07:43.765Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6b/d5756f485012b920475cbc01457c1b9a7d0485bfb04b92598c5e1ef3e9ab/nvidia_nvvm-13.3.73-py3-none-win_amd64.whl", hash = "sha256:b5c91dfa59ee4cee90b2dfb19c6203f31c914b9c9b5ca10726c2da7cf8ed401d", size = 59981103, upload-time = "2026-06-29T17:21:43.334Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -1672,17 +1696,6 @@ version = "12.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, - { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" }, - { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" }, - { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" }, - { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" }, - { url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568, upload-time = "2026-01-02T09:10:41.035Z" }, - { url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367, upload-time = "2026-01-02T09:10:43.09Z" }, - { url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345, upload-time = "2026-01-02T09:10:44.795Z" }, { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, @@ -1856,10 +1869,8 @@ name = "pylops" version = "2.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, + { name = "scipy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/81/e0/28d300177adf19f28fef2b95b3aabe01d39cedd79a739a3139dfb63c04be/pylops-2.6.0.tar.gz", hash = "sha256:4fd0fd66fa2eb0270a3c044a04b113d4636be029888066dabb6f403fc98ac40f", size = 269730, upload-time = "2025-11-24T20:34:07.709Z" } wheels = [ @@ -1879,11 +1890,9 @@ wheels = [ name = "pyproximal" source = { editable = "." } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "pylops" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy" }, { name = "typing-extensions" }, ] @@ -1892,6 +1901,14 @@ advanced = [ { name = "llvmlite" }, { name = "numba" }, ] +gpu-cu12 = [ + { name = "cupy-cuda12x" }, + { name = "numba-cuda", extra = ["cu12"], marker = "extra == 'extra-10-pyproximal-gpu-cu12'" }, +] +gpu-cu13 = [ + { name = "cupy-cuda13x" }, + { name = "numba-cuda", extra = ["cu13"], marker = "extra == 'extra-10-pyproximal-gpu-cu13'" }, +] [package.dev-dependencies] dev = [ @@ -1910,14 +1927,11 @@ doc = [ { name = "nbsphinx" }, { name = "numpydoc" }, { name = "pooch" }, - { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scikit-image" }, { name = "shibuya" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "sphinx-design", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx-design", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinx-design" }, { name = "sphinx-gallery" }, { name = "sphinx-iconify" }, { name = "sphinxemoji" }, @@ -1925,14 +1939,18 @@ doc = [ [package.metadata] requires-dist = [ + { name = "cupy-cuda12x", marker = "extra == 'gpu-cu12'" }, + { name = "cupy-cuda13x", marker = "extra == 'gpu-cu13'" }, { name = "llvmlite", marker = "extra == 'advanced'" }, { name = "numba", marker = "extra == 'advanced'" }, + { name = "numba-cuda", extras = ["cu12"], marker = "extra == 'gpu-cu12'", specifier = ">=0.30.0" }, + { name = "numba-cuda", extras = ["cu13"], marker = "extra == 'gpu-cu13'", specifier = ">=0.30.0" }, { name = "numpy", specifier = ">=1.21.0" }, { name = "pylops", specifier = ">=2.4.0" }, { name = "scipy", specifier = ">=1.11.0" }, { name = "typing-extensions", specifier = ">=4.0.0" }, ] -provides-extras = ["advanced"] +provides-extras = ["advanced", "gpu-cu12", "gpu-cu13"] [package.metadata.requires-dev] dev = [ @@ -1965,13 +1983,11 @@ name = "pytest" version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ @@ -1990,67 +2006,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] -[[package]] -name = "pywavelets" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/45/bfaaab38545a33a9f06c61211fc3bea2e23e8a8e00fedeb8e57feda722ff/pywavelets-1.8.0.tar.gz", hash = "sha256:f3800245754840adc143cbc29534a1b8fc4b8cff6e9d403326bd52b7bb5c35aa", size = 3935274, upload-time = "2024-12-04T19:54:20.593Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/7e/c5e398f25c70558ca195dd4144ee004666401f6167084c1e76059d7e68d8/pywavelets-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f5c86fcb203c8e61d1f3d4afbfc08d626c64e4e3708207315577264c724632bf", size = 4323291, upload-time = "2024-12-04T19:53:01.836Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d7/2fc8067c3520ce25f7632b0f47b89d1b75653cab804a42700e95126f2679/pywavelets-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fafb5fa126277e1690c3d6329287122fc08e4d25a262ce126e3d81b1f5709308", size = 4291864, upload-time = "2024-12-04T19:53:04.659Z" }, - { url = "https://files.pythonhosted.org/packages/2f/17/a868aa26e45c104613d9069f9d8ec0123687cb6945062d274f20a3992436/pywavelets-1.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec23dfe6d5a3f4312b12456b8c546aa90a11c1138e425a885987505f0658ae0", size = 4447532, upload-time = "2024-12-04T19:53:06.383Z" }, - { url = "https://files.pythonhosted.org/packages/53/7a/7f5889a57177e2b1182080fc2c52236d1e03a0fad5e0b3d7c5312070c0be/pywavelets-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:880a0197e9fa108939af50a95e97c1bf9b7d3e148e0fad92ea60a9ed8c8947c0", size = 4487695, upload-time = "2024-12-04T19:53:08.84Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e6/04d76d93c158919ef0d8e1d40d1d453168305031eca6733fdc844f7cbb07/pywavelets-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bfa833d08b60d0bf53a7939fbbf3d98015dd34efe89cbe4e53ced880d085fc1", size = 4473752, upload-time = "2024-12-04T19:53:10.848Z" }, - { url = "https://files.pythonhosted.org/packages/3b/a7/42ea5bbb6055abd312e45b27d931200fd6eed5414a87ec5d62020a4c651b/pywavelets-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e10c3fc7f4a796e94da4bca9871be2186a7bb7a3b3536a0ca9376d84263140f0", size = 4504191, upload-time = "2024-12-04T19:53:13.912Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7e/52df87a9e77adfb12c1b8be79a2053f2eb4c2507dec96ebfd2333b15ff03/pywavelets-1.8.0-cp310-cp310-win32.whl", hash = "sha256:31baf4be6940fde72cc85663154360857ac1b93c251822deaf72bb804da95031", size = 4143794, upload-time = "2024-12-04T19:53:16.296Z" }, - { url = "https://files.pythonhosted.org/packages/01/e2/06e08230c26049740b2773952fbb12cc7186e5df655a73b1c30ba493e864/pywavelets-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:560c39f1ff8cb37f8b8ea4b7b6eb8a14f6926c11f5cf8c09f013a58f895ed5bc", size = 4214262, upload-time = "2024-12-04T19:53:17.998Z" }, - { url = "https://files.pythonhosted.org/packages/6c/8a/9f8e794120b55caa1c4ae8d72696111bc408251615f351a8e54a5d8c4d4e/pywavelets-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e8dd5be4faed994581a8a4b3c0169be20567a9346e523f0b57f903c8f6722bce", size = 4324170, upload-time = "2024-12-04T19:53:19.66Z" }, - { url = "https://files.pythonhosted.org/packages/3e/b8/f6246be5c78e9fa73fcbba9ab4cbfe0d4dcb79ea5491f28d673a53466134/pywavelets-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d8abaf7c120b151ef309c9ff57e0a44ba9febf49045056dbc1577526ecec6c8", size = 4294254, upload-time = "2024-12-04T19:53:21.767Z" }, - { url = "https://files.pythonhosted.org/packages/2c/dc/ba1f212e9b43117ed28e0fd092e72e817790427400f88937ea742d260153/pywavelets-1.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b43a4c58707b1e8d941bec7f1d83e67c482278575ff0db3189d5c0dfae23a57", size = 4447178, upload-time = "2024-12-04T19:53:23.525Z" }, - { url = "https://files.pythonhosted.org/packages/58/10/e59c162a11d2fedb4454abbf7b74a52390aba5edc9605bf829bfa8708dac/pywavelets-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1aad0b97714e3079a2bfe48e4fb8ccd60778d0427e9ee5e0a9ff922e6c61e4", size = 4486799, upload-time = "2024-12-04T19:53:25.238Z" }, - { url = "https://files.pythonhosted.org/packages/03/ee/90c3d0a0a3bda74e6e097e4c06bff9446ff2a4c90b8617aaf4902c46966b/pywavelets-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0e1db96dcf3ce08156859df8b359e9ff66fa15061a1b90e70e020bf4cd077a0", size = 4486403, upload-time = "2024-12-04T19:53:26.954Z" }, - { url = "https://files.pythonhosted.org/packages/05/54/58b87f8b636a9f044f3f9814d2ec696cf25f3b33af97c11811f13c364085/pywavelets-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e62c8fb52ab0e8ff212fff9acae681a8f12d68b76c36fe24cc48809d5b6825ba", size = 4515011, upload-time = "2024-12-04T19:53:28.832Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d0/f755cee11ff20668114942d0e777e2b502a8e4665e1fdb2553b587aac637/pywavelets-1.8.0-cp311-cp311-win32.whl", hash = "sha256:bf327528d10de471b04bb725c4e10677fac5a49e13d41bf0d0b3a1f6d7097abf", size = 4139934, upload-time = "2024-12-04T19:53:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0b/f4b92d4f00565280ea3e62a8e3dc81a667d67ed7bd59232f2f18d55f9aff/pywavelets-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3814d354dd109e244ffaac3d480d29a5202212fe24570c920268237c8d276f95", size = 4214321, upload-time = "2024-12-04T19:53:33.183Z" }, - { url = "https://files.pythonhosted.org/packages/2d/8b/4870f11559307416470158a5aa6f61e5c2a910f1645a7a836ffae580b7ad/pywavelets-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3f431c9e2aff1a2240765eff5e804975d0fcc24c82d6f3d4271243f228e5963b", size = 4326187, upload-time = "2024-12-04T19:53:35.19Z" }, - { url = "https://files.pythonhosted.org/packages/c4/35/66835d889fd7fbf3119c7a9bd9d9bd567fc0bb603dfba408e9226db7cb44/pywavelets-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e39b0e2314e928cb850ee89b9042733a10ea044176a495a54dc84d2c98407a51", size = 4295428, upload-time = "2024-12-04T19:53:36.962Z" }, - { url = "https://files.pythonhosted.org/packages/63/1c/42e5130226538c70d4bbbaee00eb1bc06ec3287f7ea43d5fcf85bfc761ce/pywavelets-1.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cae701117f5c7244b7c8d48b9e92a0289637cdc02a9c205e8be83361f0c11fae", size = 4421259, upload-time = "2024-12-04T19:53:39.119Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c5/1ce93657432e22a5debc21e8b52ec6980f819ecb7fa727bb86744224d967/pywavelets-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649936baee933e80083788e0adc4d8bc2da7cdd8b10464d3b113475be2cc5308", size = 4447650, upload-time = "2024-12-04T19:53:41.589Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d6/b54ef30daca71824f811f9d2322a978b0a58d27674b8e3af6520f67e9ec6/pywavelets-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8c68e9d072c536bc646e8bdce443bb1826eeb9aa21b2cb2479a43954dea692a3", size = 4448538, upload-time = "2024-12-04T19:53:44.308Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/1688b790e55674667ad644262f174405c2c9873cb13e773432e78b1b33e4/pywavelets-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:63f67fa2ee1610445de64f746fb9c1df31980ad13d896ea2331fc3755f49b3ae", size = 4485228, upload-time = "2024-12-04T19:53:46.778Z" }, - { url = "https://files.pythonhosted.org/packages/c9/9b/69de31c3b663dadd76d1da6bf8af68d8cefff55df8e880fe96a94bb8c9ac/pywavelets-1.8.0-cp312-cp312-win32.whl", hash = "sha256:4b3c2ab669c91e3474fd63294355487b7dd23f0b51d32f811327ddf3546f4f3d", size = 4134850, upload-time = "2024-12-04T19:53:49.101Z" }, - { url = "https://files.pythonhosted.org/packages/1c/88/9e2aa9d5fde08bfc0fb18ffb1b5307c1ed49c24930b4147e5f48571a7251/pywavelets-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:810a23a631da596fef7196ddec49b345b1aab13525bb58547eeebe1769edbbc1", size = 4210786, upload-time = "2024-12-04T19:53:51.546Z" }, - { url = "https://files.pythonhosted.org/packages/94/73/7ff347d77c6bda11330565050c3346c54bc210086380abeb84e402c1c9cd/pywavelets-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:441ba45c8dff8c6916dbe706958d0d7f91da675695ca0c0d75e483f6f52d0a12", size = 4321474, upload-time = "2024-12-04T19:53:53.369Z" }, - { url = "https://files.pythonhosted.org/packages/b0/70/c58937ebbca1aba3475ca5ee63c7bcebf09f3c93891ae5942eaec7e95707/pywavelets-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:24bb282bab09349d9d128ed0536fa50fff5c2147891971a69c2c36155dfeeeac", size = 4291502, upload-time = "2024-12-04T19:53:55.396Z" }, - { url = "https://files.pythonhosted.org/packages/da/55/87b4ad6128b2e85985908e958e856e0b680cdcc03cc490e2cc995164b13a/pywavelets-1.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:426ff3799446cb4da1db04c2084e6e58edfe24225596805665fd39c14f53dece", size = 4412669, upload-time = "2024-12-04T19:53:57.393Z" }, - { url = "https://files.pythonhosted.org/packages/bf/1a/bfca9eab23bd7b27843b0ce95c47796033a7b2c93048315f5fc5d6ac6428/pywavelets-1.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0607a9c085b8285bc0d04e33d461a6c80f8c325389221ffb1a45141861138e", size = 4454604, upload-time = "2024-12-04T19:53:59.105Z" }, - { url = "https://files.pythonhosted.org/packages/c3/23/9ce38829f57159e812c469c4f9d7b5a16c1ba922c1802985e8c504468206/pywavelets-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d31c36a39110e8fcc7b1a4a11cfed7d22b610c285d3e7f4fe73ec777aa49fa39", size = 4445507, upload-time = "2024-12-04T19:54:00.78Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d2/e78a976b0600a6ef7a70f4430122d6ad11b3e1cbda3c8b3565661d094678/pywavelets-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa7c68ed1e5bab23b1bafe60ccbcf709b878652d03de59e961baefa5210fcd0a", size = 4479078, upload-time = "2024-12-04T19:54:02.46Z" }, - { url = "https://files.pythonhosted.org/packages/85/4d/1c4f870010368f3aeb0bdd72929376a1988e4a122e76545bd8c56e549c96/pywavelets-1.8.0-cp313-cp313-win32.whl", hash = "sha256:2c6b359b55d713ef683e9da1529181b865a80d759881ceb9adc1c5742e4da4d8", size = 4133763, upload-time = "2024-12-04T19:54:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/c9/4f/0a709a5732e6cf9297fc87bf545cb879997cde204115f8c0cbc296c5bdd3/pywavelets-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dbebcfd55ea8a85b7fc8802d411e75337170422abf6e96019d7e46c394e80e5", size = 4209548, upload-time = "2024-12-04T19:54:06.61Z" }, - { url = "https://files.pythonhosted.org/packages/de/2a/4cac0bba67d3bc0ad697d0680539864db0a6964c7ad953d8d9d887f360b3/pywavelets-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:2e1c79784bebeafd3715c1bea6621daa2e2e6ed37b687719322e2078fb35bb70", size = 4335183, upload-time = "2024-12-04T19:54:08.349Z" }, - { url = "https://files.pythonhosted.org/packages/58/d1/3abe4cf34a35b09ad847f0e9a85f340c1988611222926d295fa8710659e7/pywavelets-1.8.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f489380c95013cc8fb3ef338f6d8c1a907125db453cc4dc739e2cca06fcd8b6", size = 4454723, upload-time = "2024-12-04T19:54:11.007Z" }, - { url = "https://files.pythonhosted.org/packages/d5/62/f05dd191232ae94e0b48509bb0ee65c9d45abf5e8f3612b09fd309b41384/pywavelets-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:06786201a91b5e74540f4f3c115c49a29190de2eb424823abbd3a1fd75ea3e28", size = 4472192, upload-time = "2024-12-04T19:54:12.754Z" }, - { url = "https://files.pythonhosted.org/packages/20/6a/257c95ad1e0fd395cbccd4ffec0d01cc9b51a3bb91e67d8fa10ffebc9c72/pywavelets-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:f2877fb7b58c94211257dcf364b204d6ed259146fc87d5a90bf9d93c97af6226", size = 4183968, upload-time = "2024-12-04T19:54:15.099Z" }, - { url = "https://files.pythonhosted.org/packages/6c/58/7179fd6f87153f2e339171e8cfe9bf901398a89045eefd7a3911bb9b47ad/pywavelets-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ec5d723c3335ff8aa630fd4b14097077f12cc02893c91cafd60dd7b1730e780f", size = 4265431, upload-time = "2024-12-04T19:54:16.928Z" }, -] - [[package]] name = "pywavelets" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] dependencies = [ - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/75/50581633d199812205ea8cdd0f6d52f12a624886b74bf1486335b67f01ff/pywavelets-1.9.0.tar.gz", hash = "sha256:148d12203377772bea452a59211d98649c8ee4a05eff019a9021853a36babdc8", size = 3938340, upload-time = "2025-08-04T16:20:04.978Z" } wheels = [ @@ -2110,15 +2071,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, @@ -2173,20 +2125,10 @@ name = "pyzmq" version = "27.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "implementation_name == 'pypy'" }, + { name = "cffi", marker = "implementation_name == 'pypy' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, - { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, - { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, - { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, - { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, - { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, - { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, - { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, @@ -2229,11 +2171,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, - { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, - { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, - { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, - { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, @@ -2248,7 +2185,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ @@ -2285,20 +2222,6 @@ version = "0.30.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, - { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, - { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, - { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, - { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, @@ -2427,65 +2350,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] -[[package]] -name = "scikit-image" -version = "0.25.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "imageio", marker = "python_full_version < '3.11'" }, - { name = "lazy-loader", marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pillow", marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/cb/016c63f16065c2d333c8ed0337e18a5cdf9bc32d402e4f26b0db362eb0e2/scikit_image-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78", size = 13988922, upload-time = "2025-02-18T18:04:11.069Z" }, - { url = "https://files.pythonhosted.org/packages/30/ca/ff4731289cbed63c94a0c9a5b672976603118de78ed21910d9060c82e859/scikit_image-0.25.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063", size = 13192698, upload-time = "2025-02-18T18:04:15.362Z" }, - { url = "https://files.pythonhosted.org/packages/39/6d/a2aadb1be6d8e149199bb9b540ccde9e9622826e1ab42fe01de4c35ab918/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99", size = 14153634, upload-time = "2025-02-18T18:04:18.496Z" }, - { url = "https://files.pythonhosted.org/packages/96/08/916e7d9ee4721031b2f625db54b11d8379bd51707afaa3e5a29aecf10bc4/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09", size = 14767545, upload-time = "2025-02-18T18:04:22.556Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ee/c53a009e3997dda9d285402f19226fbd17b5b3cb215da391c4ed084a1424/scikit_image-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054", size = 12812908, upload-time = "2025-02-18T18:04:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" }, - { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" }, - { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" }, - { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" }, - { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" }, - { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" }, - { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" }, - { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, - { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, - { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, - { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, - { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, -] - [[package]] name = "scikit-image" version = "0.26.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] dependencies = [ - { name = "imageio", marker = "python_full_version >= '3.11'" }, - { name = "lazy-loader", marker = "python_full_version >= '3.11'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pillow", marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "tifffile", version = "2026.1.28", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "imageio" }, + { name = "lazy-loader" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "scipy" }, + { name = "tifffile" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" } wheels = [ @@ -2539,75 +2416,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/cd/4da01329b5a8d47ff7ec3c99a2b02465a8017b186027590dc7425cee0b56/scikit_image-0.26.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf", size = 11769501, upload-time = "2025-12-20T17:12:19.339Z" }, ] -[[package]] -name = "scipy" -version = "1.15.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, - { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, - { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, - { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, - { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, - { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, - { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, - { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, - { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, - { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, - { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, - { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, - { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, - { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, - { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, - { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, - { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, - { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, - { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, - { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, - { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, - { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, - { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, - { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, - { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, - { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, - { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, - { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, - { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, - { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, - { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, - { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, -] - [[package]] name = "scipy" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] dependencies = [ - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } wheels = [ @@ -2679,9 +2493,8 @@ version = "2026.1.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments-styles" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/b94cb04adbb984973fe83fd670dd066514610241d829723f678366e691d2/shibuya-2026.1.9.tar.gz", hash = "sha256:b389f10fd9c07b048e940f32d1e1ac096a2d49736389173ac771b37a10b51fdf", size = 86002, upload-time = "2026-01-09T02:19:14.365Z" } wheels = [ @@ -2715,62 +2528,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] -[[package]] -name = "sphinx" -version = "8.1.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "alabaster", marker = "python_full_version < '3.11'" }, - { name = "babel", marker = "python_full_version < '3.11'" }, - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "imagesize", marker = "python_full_version < '3.11'" }, - { name = "jinja2", marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "requests", marker = "python_full_version < '3.11'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, -] - [[package]] name = "sphinx" version = "9.0.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.11.*'", + "python_full_version < '3.12'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version == '3.11.*'" }, - { name = "babel", marker = "python_full_version == '3.11.*'" }, - { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "imagesize", marker = "python_full_version == '3.11.*'" }, - { name = "jinja2", marker = "python_full_version == '3.11.*'" }, - { name = "packaging", marker = "python_full_version == '3.11.*'" }, - { name = "pygments", marker = "python_full_version == '3.11.*'" }, - { name = "requests", marker = "python_full_version == '3.11.*'" }, - { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, - { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, + { name = "alabaster", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "babel", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "colorama", marker = "(python_full_version < '3.12' and sys_platform == 'win32') or (python_full_version >= '3.12' and extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13') or (sys_platform != 'win32' and extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "docutils", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "imagesize", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "jinja2", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "packaging", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "pygments", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "requests", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "roman-numerals", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "snowballstemmer", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ @@ -2785,55 +2567,36 @@ resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version >= '3.12'" }, - { name = "babel", marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "imagesize", marker = "python_full_version >= '3.12'" }, - { name = "jinja2", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "requests", marker = "python_full_version >= '3.12'" }, - { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, + { name = "alabaster", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "babel", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "colorama", marker = "(python_full_version >= '3.12' and sys_platform == 'win32') or (python_full_version < '3.12' and extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13') or (sys_platform != 'win32' and extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "docutils", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "imagesize", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "jinja2", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "packaging", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "pygments", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "requests", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, ] -[[package]] -name = "sphinx-design" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2b/69/b34e0cb5336f09c6866d53b4a19d76c227cdec1bbc7ac4de63ca7d58c9c7/sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632", size = 2193689, upload-time = "2024-08-02T13:48:44.277Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/43/65c0acbd8cc6f50195a3a1fc195c404988b15c67090e73c7a41a9f57d6bd/sphinx_design-0.6.1-py3-none-any.whl", hash = "sha256:b11f37db1a802a183d61b159d9a202314d4d2fe29c163437001324fe2f19549c", size = 2215338, upload-time = "2024-08-02T13:48:42.106Z" }, -] - [[package]] name = "sphinx-design" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] dependencies = [ - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/7b/804f311da4663a4aecc6cf7abd83443f3d4ded970826d0c958edc77d4527/sphinx_design-0.7.0.tar.gz", hash = "sha256:d2a3f5b19c24b916adb52f97c5f00efab4009ca337812001109084a740ec9b7a", size = 2203582, upload-time = "2026-01-19T13:12:53.297Z" } wheels = [ @@ -2846,9 +2609,8 @@ version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pillow" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5f/14/9238ac61932299b38c20c7c37dbfe60348c0348ea4d400f9ef25875b3bf7/sphinx_gallery-0.20.0.tar.gz", hash = "sha256:70281510c6183d812d3595957005ccf555c5a793f207410f6cd16a25bf08d735", size = 473502, upload-time = "2025-12-02T15:51:37.277Z" } wheels = [ @@ -2860,9 +2622,8 @@ name = "sphinx-iconify" version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/68/4e/498964c2a6025e489b255015d3b5644aa93eb1dd1035324de18179af8abc/sphinx_iconify-0.3.0.tar.gz", hash = "sha256:7824ca694472d6babbe811e72cdcaf52b6e0ff4240e8cfda721f84f867605611", size = 3701, upload-time = "2025-12-18T05:19:03.683Z" } wheels = [ @@ -2928,9 +2689,8 @@ name = "sphinxemoji" version = "0.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ad/05/d531d8ce28eeb364e900dab98b8e236cf664a1b7f5fa93c212a5e87313aa/sphinxemoji-0.3.2.tar.gz", hash = "sha256:814da89a9a7603b7e4d85c487c7381656fa83632f20065e5ed782ab1dbbb5083", size = 45999, upload-time = "2025-12-15T12:01:56.885Z" } wheels = [ @@ -2946,31 +2706,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, ] -[[package]] -name = "tifffile" -version = "2025.5.10" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/44/d0/18fed0fc0916578a4463f775b0fbd9c5fed2392152d039df2fb533bfdd5d/tifffile-2025.5.10.tar.gz", hash = "sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103", size = 365290, upload-time = "2025-05-10T19:22:34.386Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/06/bd0a6097da704a7a7c34a94cfd771c3ea3c2f405dd214e790d22c93f6be1/tifffile-2025.5.10-py3-none-any.whl", hash = "sha256:e37147123c0542d67bc37ba5cdd67e12ea6fbe6e86c52bee037a9eb6a064e5ad", size = 226533, upload-time = "2025-05-10T19:22:27.279Z" }, -] - [[package]] name = "tifffile" version = "2026.1.28" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] dependencies = [ - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/94/32/38498d2a1a5d70f33f6c3909bbad48557c9a54b0e33a9307ff06b6d416ba/tifffile-2026.1.28.tar.gz", hash = "sha256:537ae6466a8bb555c336108bb1878d8319d52c9c738041d3349454dea6956e1c", size = 374675, upload-time = "2026-01-29T05:17:24.992Z" } wheels = [ @@ -2989,60 +2730,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, ] -[[package]] -name = "tomli" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, - { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, - { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, - { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, - { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, - { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, - { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, - { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, - { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, - { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, -] - [[package]] name = "tornado" version = "6.5.4" @@ -3106,7 +2793,6 @@ dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ From f9491631ce5a59666a15d9ab11f8064a79ea33d6 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Thu, 27 Aug 2026 18:14:24 +0100 Subject: [PATCH 56/73] minor: fix Github actions with new extra deps --- .github/workflows/build.yaml | 2 +- .github/workflows/codacy-coverage-reporter.yaml | 2 +- .github/workflows/mypy.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 63d5b248..dc134e3d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -27,6 +27,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install dependencies and pyproximal - run: uv sync --locked --all-extras --all-groups + run: uv sync --locked --extra advanced --all-groups - name: Test with pytest run: uv run pytest --color=yes pytests/ diff --git a/.github/workflows/codacy-coverage-reporter.yaml b/.github/workflows/codacy-coverage-reporter.yaml index 856a9462..4dcfcd7e 100644 --- a/.github/workflows/codacy-coverage-reporter.yaml +++ b/.github/workflows/codacy-coverage-reporter.yaml @@ -20,7 +20,7 @@ jobs: with: python-version: "3.11" - name: Install dependencies and pyproximal - run: uv sync --locked --all-extras --all-groups + run: uv sync --locked --extra advanced --all-groups - name: Coverage with pytest run: | uv run coverage run -m pytest diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml index 1c40c80d..1f7df24f 100644 --- a/.github/workflows/mypy.yaml +++ b/.github/workflows/mypy.yaml @@ -18,7 +18,7 @@ jobs: python-version: "3.11" enable-cache: true - name: Install dependencies and pyproximal - run: uv sync --locked --all-extras --all-groups + run: uv sync --locked --extra advanced --all-groups - name: Run Mypy run: | uv run mypy pyproximal/ From 19820c3dfaded742a3ef5e9274555699bac6c6ae Mon Sep 17 00:00:00 2001 From: mrava87 Date: Thu, 27 Aug 2026 20:31:53 +0100 Subject: [PATCH 57/73] minor: fix RTD and Azure pipelines --- .readthedocs.yaml | 2 +- azure-pipelines.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 1adbefea..64249678 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -22,4 +22,4 @@ build: create_environment: - uv venv "${READTHEDOCS_VIRTUALENV_PATH}" install: - - UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --locked --all-extras --all-groups + - UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --locked --extra advanced --all-groups diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 369e5b85..4bde86f8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,7 +34,7 @@ jobs: displayName: 'Install uv' - script: | - uv sync --locked --all-extras --all-groups + uv sync --locked --extra advanced --all-groups displayName: 'Install dependencies and pyproximal' - script: | From 19c2f35652a64acbc2a63ad8a68451433f9a0697 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Thu, 27 Aug 2026 20:44:13 +0100 Subject: [PATCH 58/73] minor: fix Linux Azure pipelines --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4bde86f8..31382d5f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -66,7 +66,7 @@ jobs: displayName: 'Install uv' - script: | - uv sync --locked --all-extras --all-groups + uv sync --locked --extra advanced --all-groups displayName: 'Install dependencies and pyproximal' - script: | From 3b54fda201c678b7eb8914f362cc1d2e8bfb9521 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 28 Aug 2026 22:12:30 +0100 Subject: [PATCH 59/73] fix: ensure step sizes and tau are passed as scalars --- pyproximal/optimization/cls_primal.py | 10 +++++----- pyproximal/optimization/cls_primaldual.py | 4 ++-- pyproximal/proximal/L2.py | 7 ++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index 6c8ba991..287680e8 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -626,8 +626,8 @@ def step( epsg = self.epsg epsg_prev = self.epsg else: - epsg = self.epsg[self.iiter] - epsg_prev = self.epsg[self.iiter - 1] + epsg = self.epsg[self.iiter].item() + epsg_prev = self.epsg[self.iiter - 1].item() # proximal step if not self.backtracking: @@ -1080,8 +1080,8 @@ def step( epsg = self.epsg epsg_prev = self.epsg else: - epsg = self.epsg[self.iiter] - epsg_prev = self.epsg[self.iiter - 1] + epsg = self.epsg[self.iiter].item() + epsg_prev = self.epsg[self.iiter - 1].item() # update fix point g = x - self.tau * self.proxf.grad(x) @@ -1915,7 +1915,7 @@ def step( if self.tau.ndim == 0: tau = self.tau else: - tau = self.tau[self.iiter] + tau = self.tau[self.iiter].item() # proximal steps if self.gfirst: diff --git a/pyproximal/optimization/cls_primaldual.py b/pyproximal/optimization/cls_primaldual.py index 83bd039b..412776d0 100644 --- a/pyproximal/optimization/cls_primaldual.py +++ b/pyproximal/optimization/cls_primaldual.py @@ -297,13 +297,13 @@ def step( if self.tau.ndim == 0: tau = self.tau else: - tau = self.tau[self.iiter] + tau = self.tau[self.iiter].item() # define mu for current iteration if self.mu.ndim == 0: mu = self.mu else: - mu = self.mu[self.iiter] + mu = self.mu[self.iiter].item() if self.gfirst: y = self.proxg.proxdual(y + mu * self.A.matvec(xhat), mu) diff --git a/pyproximal/proximal/L2.py b/pyproximal/proximal/L2.py index f38f53b0..d63bd719 100644 --- a/pyproximal/proximal/L2.py +++ b/pyproximal/proximal/L2.py @@ -1,4 +1,5 @@ from collections.abc import Callable +from math import sqrt from typing import TYPE_CHECKING, Any, Optional import numpy as np @@ -37,7 +38,7 @@ class L2(ProxOperator): Data vector q : :obj:`numpy.ndarray`, optional Dot vector - sigma : :obj:`int`, optional + sigma : :obj:`float`, optional Multiplicative coefficient of L2 norm alpha : :obj:`float`, optional Multiplicative coefficient of dot product @@ -267,8 +268,8 @@ def prox(self, x: NDArray, tau: float) -> NDArray: if self.q is not None: y -= tau * self.alpha * self.q x = regularized_inversion( - np.sqrt(tau * self.sigma) * self.Op, - np.sqrt(tau * self.sigma) * self.b, + sqrt(tau * self.sigma) * self.Op, + sqrt(tau * self.sigma) * self.b, [ Identity(self.Op.shape[1], dtype=self.Op.dtype), ], From ab2e7af63b4a72e9b2a08de4e19d92b08d46e75f Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 28 Aug 2026 22:31:02 +0100 Subject: [PATCH 60/73] minor: added more .item() --- pyproximal/optimization/cls_primal.py | 16 +++++++++------- pyproximal/optimization/cls_primaldual.py | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index 287680e8..1d91df01 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -623,8 +623,8 @@ def step( # define epsg for current iteration if self.epsg.ndim == 0: - epsg = self.epsg - epsg_prev = self.epsg + epsg = self.epsg.item() + epsg_prev = self.epsg.item() else: epsg = self.epsg[self.iiter].item() epsg_prev = self.epsg[self.iiter - 1].item() @@ -1077,8 +1077,8 @@ def step( """ # define epsg for current iteration if self.epsg.ndim == 0: - epsg = self.epsg - epsg_prev = self.epsg + epsg = self.epsg.item() + epsg_prev = self.epsg.item() else: epsg = self.epsg[self.iiter].item() epsg_prev = self.epsg[self.iiter - 1].item() @@ -1525,9 +1525,11 @@ def step( x = np.zeros_like(x) for i, proxg in enumerate(self.proxgs): ztmp = 2 * y - self.zs[i] - self.tau * grad - ztmp = proxg.prox(ztmp, self.tau * self.epsg[i] / self.weights[i]) + ztmp = proxg.prox( + ztmp, self.tau * self.epsg[i].item() / self.weights[i].item() + ) self.zs[i] += self.eta * (ztmp - y) - x += self.weights[i] * self.zs[i] + x += self.weights[i].item() * self.zs[i] # update y if self.acceleration == "vandenberghe": @@ -1913,7 +1915,7 @@ def step( """ # define tau for current iteration if self.tau.ndim == 0: - tau = self.tau + tau = self.tau.item() else: tau = self.tau[self.iiter].item() diff --git a/pyproximal/optimization/cls_primaldual.py b/pyproximal/optimization/cls_primaldual.py index 412776d0..3717cbfd 100644 --- a/pyproximal/optimization/cls_primaldual.py +++ b/pyproximal/optimization/cls_primaldual.py @@ -295,13 +295,13 @@ def step( # define tau for current iteration if self.tau.ndim == 0: - tau = self.tau + tau = self.tau.item() else: tau = self.tau[self.iiter].item() # define mu for current iteration if self.mu.ndim == 0: - mu = self.mu + mu = self.mu.item() else: mu = self.mu[self.iiter].item() From f9540500a8269ba814ebd95e1949e947e12453aa Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 28 Aug 2026 22:57:50 +0100 Subject: [PATCH 61/73] fix: fixed .item() issue also for proximal gradient solvers --- pyproximal/optimization/cls_primal.py | 42 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index 1d91df01..5f12e5b3 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -15,7 +15,7 @@ from collections.abc import Sequence from math import sqrt -from typing import TYPE_CHECKING, Any, Optional, cast +from typing import TYPE_CHECKING, Any, Optional import numpy as np import pylops @@ -621,6 +621,12 @@ def step( """ xold = x.copy() + # define tau for current iteration + if self.tau.size == 1: + tau = self.tau.item() + else: + tau = self.tau + # define epsg for current iteration if self.epsg.ndim == 0: epsg = self.epsg.item() @@ -632,30 +638,31 @@ def step( # proximal step if not self.backtracking: if self.eta == 1.0: - x = self.proxg.prox(y - self.tau * self.proxf.grad(y), epsg * self.tau) + x = self.proxg.prox(y - tau * self.proxf.grad(y), epsg * tau) else: x = x + self.eta * ( self.proxg.prox( - x - self.tau * self.proxf.grad(x), - epsg * self.tau, + x - tau * self.proxf.grad(x), + epsg * tau, ) - x ) else: - x, self.tau = _backtracking( + x, tau = _backtracking( y, - cast(float, self.tau), + tau, self.proxf, self.proxg, epsg, beta=self.beta, niterback=self.niterback, ) + self.tau = self.ncp.atleast_1d(self.ncp.asarray(tau, dtype=np.float32)) if self.eta != 1.0: x = x + self.eta * ( self.proxg.prox( - x - self.tau * self.proxf.grad(x), - epsg * self.tau, + x - tau * self.proxf.grad(x), + epsg * tau, ) - x ) @@ -1075,6 +1082,12 @@ def step( Updated additional model vector """ + # define tau for current iteration + if self.tau.size == 1: + tau = self.tau.item() + else: + tau = self.tau + # define epsg for current iteration if self.epsg.ndim == 0: epsg = self.epsg.item() @@ -1084,7 +1097,7 @@ def step( epsg_prev = self.epsg[self.iiter - 1].item() # update fix point - g = x - self.tau * self.proxf.grad(x) + g = x - tau * self.proxf.grad(x) r = g - y # update history vectors @@ -1108,24 +1121,21 @@ def step( y = np.vstack(self.G).T @ alpha # update main variable - x = self.proxg.prox(y, epsg * self.tau) + x = self.proxg.prox(y, epsg * tau) else: # update auxiliary variable ytest = np.vstack(self.G).T @ alpha # update main variable - xtest = self.proxg.prox(ytest, epsg * self.tau) + xtest = self.proxg.prox(ytest, epsg * tau) # check if function is decreased, otherwise do basic PG step pfold, self.pf = self.pf, self.proxf(xtest) - if ( - self.pf - <= pfold - self.tau * np.linalg.norm(self.proxf.grad(x)) ** 2 / 2 - ): + if self.pf <= pfold - tau * np.linalg.norm(self.proxf.grad(x)) ** 2 / 2: y = ytest x = xtest else: - x = self.proxg.prox(g, epsg * self.tau) + x = self.proxg.prox(g, epsg * tau) y = g # tolerance check: break iterations if overall From d634983d1d1efad88b2f6d8cc291ed7372bf0038 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 28 Aug 2026 23:11:38 +0100 Subject: [PATCH 62/73] doc: clarify type of weights in GeneralizedProximalGradient --- pyproximal/optimization/cls_primal.py | 2 +- pyproximal/optimization/primal.py | 2 +- pytests/test_solver.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py index 5f12e5b3..9a21e83a 100644 --- a/pyproximal/optimization/cls_primal.py +++ b/pyproximal/optimization/cls_primal.py @@ -1408,7 +1408,7 @@ def setup( # type: ignore[override] epsg : :obj:`float` or :obj:`numpy.ndarray`, optional Scaling factor(s) of ``g`` function(s). If a scalar is provided the same scaling factor is applied to every ``g`` function. - weights : :obj:`float`, optional + weights : :obj:`numpy.ndarray`, optional Weighting factors of ``g`` functions. Must sum to 1. eta : :obj:`float`, optional Relaxation parameter (must be between 0 and 1, 0 excluded). Note that diff --git a/pyproximal/optimization/primal.py b/pyproximal/optimization/primal.py index a7c6f07d..36abf037 100644 --- a/pyproximal/optimization/primal.py +++ b/pyproximal/optimization/primal.py @@ -461,7 +461,7 @@ def GeneralizedProximalGradient( epsg : :obj:`float` or :obj:`numpy.ndarray`, optional Scaling factor(s) of ``g`` function(s). If a scalar is provided the same scaling factor is applied to every ``g`` function. - weights : :obj:`float`, optional + weights : :obj:`numpy.ndarray`, optional Weighting factors of ``g`` functions. Must sum to 1. eta : :obj:`float`, optional Relaxation parameter (must be between 0 and 1, 0 excluded). Note that diff --git a/pytests/test_solver.py b/pytests/test_solver.py index 625c0c90..7200a248 100644 --- a/pytests/test_solver.py +++ b/pytests/test_solver.py @@ -147,7 +147,7 @@ def test_GPG_weights(par): ], x0=np.zeros(m), tau=1.0, - weights=[1.0, 1.0], + weights=np.array([1.0, 1.0]), ) @@ -701,7 +701,7 @@ def test_ADMM_DRS(par): @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) -@pytest.mark.parametrize("weights", [None, (0.5, 0.5)]) +@pytest.mark.parametrize("weights", [None, np.array([0.5, 0.5])]) def test_PPXA_with_ADMM(par, weights) -> None: """Check equivalency of PPXA and ADMM when using a single regularization term From d806a866acf9e6af0b85f755755a7b5bec001c6a Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sun, 30 Aug 2026 13:32:19 +0100 Subject: [PATCH 63/73] build: added initial agent support --- AGENTS.md | 40 ++++++++++++++++++++++++++++++++++++++++ AIPOLICY.md | 37 +++++++++++++++++++++++++++++++++++++ CLAUDE.md | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 AGENTS.md create mode 100644 AIPOLICY.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..99826aec --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,40 @@ +# Agent Guide for PyProximal + +## What This Project Is +PyProximal implements **proximal operators** and **proximal algorithms** for non-smooth, constrained convex optimization. It deliberately does *not* implement linear operators: those come from [PyLops](https://pylops.readthedocs.io) (`pylops.LinearOperator` is a hard dependency, `>= 2.4.0`). Any change that would add a linear-operator implementation here is out of scope by design. + +## Where Things Live +- `pyproximal/`: library code. + - `proximal/`: proximal operators (one file per operator family, `UpperCaseCamelCase` filename matching the class). + - `projection/`: orthogonal projections onto sets, usually wrapped by the indicator-function proximal operators. + - `optimization/`: solvers. + - `utils/`: `moreau` and `gradtest_proximal` test helpers, `BilinearOperator`, backend/typing helpers. + - `ProxOperator.py`: base class of every proximal operator. +- `pytests/`: pytest suite. +- `docs/`, `examples/`, `tutorials/`, `testdata/`: docs, examples, tutorial assets, and test data. +- `pyproject.toml`: build, test, lint, and packaging config. +- `Makefile`: preferred entry point for local work. + +## Working Here +- Prefer `make` targets. Use the `*_uv` variants when working in a `uv` environment. +- Common commands: `make dev-install_uv`, `make tests` or `make tests_uv`, `make lint` or `make lint_uv`, `make typeannot` or `make typeannot_uv`, `make docupdate` or `make docupdate_uv`. +- Packaging uses `hatchling`; keep build and version settings in `pyproject.toml`. + +## Core Design +- `ProxOperator` implements `prox` and `proxdual` each in terms of the other via the Moreau decomposition, so a subclass needs only one of them; implement both when closed forms exist. +- Decorate every `prox`/`proxdual` with `@_check_tau`. +- `grad` on the base class is the gradient of the Moreau envelope, not of the function; pass `hasgrad=True` and override `grad` when a true gradient is known. +- Solvers exist twice: class-based implementations in `optimization/cls_primal.py` and `cls_primaldual.py` hold the logic; `primal.py` and `primaldual.py` are thin functional wrappers over them. Change behavior in the `cls_*` files. +- Solvers should support both `numpy` and `cupy` arrays (`get_array_module(x0)`); numba/CUDA paths are optional and selected through an `engine` argument with a runtime fallback (see `proximal/Simplex.py`). + +## Style And Tests +- Follow the `ruff` rules in `pyproject.toml`; compliance is enforced in CI, as is `mypy` in `strict` mode over `pyproximal/`. +- Keep imports tidy and follow PEP 8 rules. +- Follow `numpydoc` style for docstrings, with a `Notes` section giving the maths and a reference. +- Export new operators in the subpackage `__init__.py`: docstring table, star import, and `__all__`, and list them in `docs/source/api/index.rst`. +- Add or update tests in `pytests/` and examples in `examples/` and/or `tutorials/` when changing behavior or public APIs. Validate a new operator with `moreau` when both prox and dual prox are available, otherwise with ad-hoc edge cases. + +## Contribution Flow +- Use `docs/source/contributing.rst` as the source of truth for longer contribution workflows, and `docs/source/adding.rst` / `docs/source/addingsolver.rst` when implementing a new operator or solver. +- If functionality changes, update docs and run the relevant tests before handing off. +- Avoid editing generated artifacts or build output unless the task explicitly requires it. diff --git a/AIPOLICY.md b/AIPOLICY.md new file mode 100644 index 00000000..2c406ae8 --- /dev/null +++ b/AIPOLICY.md @@ -0,0 +1,37 @@ +# PyProximal AI Policy + +**Table of Contents** + +- [The Short Version](#the-short-version) +- [The Longer Version](#the-longer-version) + - [Philosophy](#philosophy) + - [Coding Agents](#coding-agents) + + +## The Short Version + +Use AI like you used Google and StackOverflow. Own the final solution like you owned it before. + +However, since AI still behaves differently from humans (it is undoubtedly better at certain tasks and worse at others), always acknowledge directly - e.g., let Claude Code author a PR that was strongly driven by it - or indirectly - e.g., explain in the PR where and how AI was used. This helps reviewers and maintainers to pay attention in different ways when reviewing a human-driven code vs an AI-driven code. + +## The Longer Version + +### Philosophy + +PyLops has always been a forward-thinking and inclusive project. At a time when Python was largely regarded as a scripting language for mundane data manipulation and for stitching together HPC software written in lower-level programming languages, we believed that solving large-scale inverse problems efficiently and scalably didn’t necessarily require reaching for those languages. Instead, we focused on achieving greater expressivity, stronger abstractions, and ease of useβ€”all while retaining the power and performance needed for demanding applications. + +With the emergence of Coding Agents, the way we approach software development is changing, and we do not intend to be one of those communities that buries its head in the sand and carries on with business as usual. We instead encourage everyone to experiment with AI and Coding Agents and benefit from them in all stages of development. + +This however does not mean that we encourage our developers (especially newcomers) to vibe-code complex solutions with little to no control on the physical outcome - lines of code! AI and Coding Agents should be treated as colleagues during pair-coding sessions: they can help in the ideation phase, during development, and in later stages to ensure consistency and act as additional attentive reviewers. + +The only **strong recommendation** that we provide to anyone contributing code to PyProximal is to be transparent about their use of AI/Coding Agents. We must recognize that AI still behaves differently from humans - it is undoubtedly better than us in certain tasks but it is still worse in other tasks. If we, reviewers and maintainers, know how a piece of code was generated, we can approach the review process slightly differently whether we review a human-driven code vs an AI-driven code. + +### Coding Agents + +In order to help our developers, we are committed to provide some of basic ingredients that allow Coding Agent to perform at their best. We aim to be as much as possible vendor-agnostic, and therefore we will provide equivalent versions of *.md* files, skills/commands, etc. that are suitable for one or another Coding Agent. + +More specifically, we currently provide: + +- ``AGENTS.md / CLAUDE.md``: basic set of instructions that tell coding agents how to work with our specific software project. + +πŸ€–πŸ€– **This Policy was written by humans and polished by AI** πŸ€–πŸ€– diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..99826aec --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,40 @@ +# Agent Guide for PyProximal + +## What This Project Is +PyProximal implements **proximal operators** and **proximal algorithms** for non-smooth, constrained convex optimization. It deliberately does *not* implement linear operators: those come from [PyLops](https://pylops.readthedocs.io) (`pylops.LinearOperator` is a hard dependency, `>= 2.4.0`). Any change that would add a linear-operator implementation here is out of scope by design. + +## Where Things Live +- `pyproximal/`: library code. + - `proximal/`: proximal operators (one file per operator family, `UpperCaseCamelCase` filename matching the class). + - `projection/`: orthogonal projections onto sets, usually wrapped by the indicator-function proximal operators. + - `optimization/`: solvers. + - `utils/`: `moreau` and `gradtest_proximal` test helpers, `BilinearOperator`, backend/typing helpers. + - `ProxOperator.py`: base class of every proximal operator. +- `pytests/`: pytest suite. +- `docs/`, `examples/`, `tutorials/`, `testdata/`: docs, examples, tutorial assets, and test data. +- `pyproject.toml`: build, test, lint, and packaging config. +- `Makefile`: preferred entry point for local work. + +## Working Here +- Prefer `make` targets. Use the `*_uv` variants when working in a `uv` environment. +- Common commands: `make dev-install_uv`, `make tests` or `make tests_uv`, `make lint` or `make lint_uv`, `make typeannot` or `make typeannot_uv`, `make docupdate` or `make docupdate_uv`. +- Packaging uses `hatchling`; keep build and version settings in `pyproject.toml`. + +## Core Design +- `ProxOperator` implements `prox` and `proxdual` each in terms of the other via the Moreau decomposition, so a subclass needs only one of them; implement both when closed forms exist. +- Decorate every `prox`/`proxdual` with `@_check_tau`. +- `grad` on the base class is the gradient of the Moreau envelope, not of the function; pass `hasgrad=True` and override `grad` when a true gradient is known. +- Solvers exist twice: class-based implementations in `optimization/cls_primal.py` and `cls_primaldual.py` hold the logic; `primal.py` and `primaldual.py` are thin functional wrappers over them. Change behavior in the `cls_*` files. +- Solvers should support both `numpy` and `cupy` arrays (`get_array_module(x0)`); numba/CUDA paths are optional and selected through an `engine` argument with a runtime fallback (see `proximal/Simplex.py`). + +## Style And Tests +- Follow the `ruff` rules in `pyproject.toml`; compliance is enforced in CI, as is `mypy` in `strict` mode over `pyproximal/`. +- Keep imports tidy and follow PEP 8 rules. +- Follow `numpydoc` style for docstrings, with a `Notes` section giving the maths and a reference. +- Export new operators in the subpackage `__init__.py`: docstring table, star import, and `__all__`, and list them in `docs/source/api/index.rst`. +- Add or update tests in `pytests/` and examples in `examples/` and/or `tutorials/` when changing behavior or public APIs. Validate a new operator with `moreau` when both prox and dual prox are available, otherwise with ad-hoc edge cases. + +## Contribution Flow +- Use `docs/source/contributing.rst` as the source of truth for longer contribution workflows, and `docs/source/adding.rst` / `docs/source/addingsolver.rst` when implementing a new operator or solver. +- If functionality changes, update docs and run the relevant tests before handing off. +- Avoid editing generated artifacts or build output unless the task explicitly requires it. From 933ef4be652f3d0a311d7ac03aa5fbe14fe1c3cd Mon Sep 17 00:00:00 2001 From: mrava87 Date: Thu, 3 Sep 2026 21:33:58 +0100 Subject: [PATCH 64/73] feat: added optional tau/mu to Segment --- pyproximal/optimization/segmentation.py | 36 ++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/pyproximal/optimization/segmentation.py b/pyproximal/optimization/segmentation.py index 5f7b7f22..4eb61b5d 100644 --- a/pyproximal/optimization/segmentation.py +++ b/pyproximal/optimization/segmentation.py @@ -3,6 +3,7 @@ import numpy as np from pylops import BlockDiag, Gradient +from pylops.utils.backend import get_array_module from pylops.utils.typing import NDArray from pyproximal import L21, Simplex, VStack @@ -14,6 +15,8 @@ def Segment( cl: NDArray, sigma: float, alpha: float, + tau: float | None = 1.0, + mu: float | None = None, clsigmas: NDArray | None = None, z: NDArray | None = None, niter: int = 10, @@ -37,6 +40,14 @@ def Segment( Positive scalar weight of the misfit term alpha : :obj:`float` Positive scalar weight of the regularization term + tau : :obj:`float`, optional + Stepsize of subgradient of segmentation term. + If ``None``, it is set from ``mu`` and the + Lipschitz constant of the gradient operator. + mu : :obj:`float`, optional + Stepsize of subgradient of regularization term. + If ``None``, it is set from ``tau`` and the + Lipschitz constant of the gradient operator. clsigmas : :obj:`numpy.ndarray`, optional Classes standard deviations z : :obj:`numpy.ndarray`, optional @@ -64,6 +75,11 @@ def Segment( Estimated classes. This is a vector of the same size of the input data ``y`` with the selected classes at each pixel. + Raises + ------ + ValueError + If both ``tau`` and ``mu`` are ``None`` + Notes ----- This solver performs image segmentation over :math:`N_{cl}` classes solving @@ -90,6 +106,11 @@ def Segment( Imaging and Vision, 40, 8pp. 120–145. 2011. """ + if tau is None and mu is None: + msg = "Either tau or mu must be provided." + raise ValueError(msg) + + ncp = get_array_module(y) kwargs_simplex = {} if kwargs_simplex is None else kwargs_simplex dims = y.shape @@ -104,10 +125,7 @@ def Segment( g = g.ravel() # Gradient operator - sampling = 1.0 - Gop = Gradient( - dims=dims, sampling=sampling, edge=False, kind="forward", dtype="float64" - ) + Gop = Gradient(dims=dims, sampling=1.0, edge=False, kind="forward", dtype="float64") Gop = BlockDiag([Gop] * ncl) # Simplex and L21 proximal operators @@ -119,9 +137,9 @@ def Segment( ) # Steps - L = 8.0 / sampling**2 - tau = 1.0 - mu = 1.0 / (tau * L) + L = 4 * ndims + tau = 1.0 / (mu * L) if tau is None else tau + mu = 1.0 / (tau * L) if mu is None else mu # Inversion x: NDArray = PrimalDual( @@ -132,14 +150,14 @@ def Segment( mu=mu, z=g if z is None else g + z, theta=1.0, - x0=np.zeros_like(g) if x0 is None else x0, + x0=ncp.zeros_like(g) if x0 is None else x0, niter=niter, callback=callback, show=show, returny=False, ) x = x.reshape(ncl, dimsprod).T - cl = np.argmax(x, axis=1) + cl = ncp.argmax(x, axis=1) cl = cl.reshape(dims) return x, cl From f62c90e92fd26e69535270e7a84eef8e829f6ca9 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 4 Sep 2026 11:34:29 +0100 Subject: [PATCH 65/73] minor: fix mypy --- pyproximal/optimization/segmentation.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pyproximal/optimization/segmentation.py b/pyproximal/optimization/segmentation.py index 4eb61b5d..33986519 100644 --- a/pyproximal/optimization/segmentation.py +++ b/pyproximal/optimization/segmentation.py @@ -106,10 +106,6 @@ def Segment( Imaging and Vision, 40, 8pp. 120–145. 2011. """ - if tau is None and mu is None: - msg = "Either tau or mu must be provided." - raise ValueError(msg) - ncp = get_array_module(y) kwargs_simplex = {} if kwargs_simplex is None else kwargs_simplex @@ -118,6 +114,16 @@ def Segment( dimsprod = np.prod(np.array(dims)) ncl = len(cl) + # Set step sizes + L = 4 * ndims + if tau is None: + if mu is None: + msg = "Either tau or mu must be provided." + raise ValueError(msg) + tau = 1.0 / (mu * L) + elif mu is None: + mu = 1.0 / (tau * L) + # Data (difference between image and center of classes) g = sigma / 2.0 * (y.reshape(1, dimsprod) - cl[:, np.newaxis]) ** 2 if clsigmas is not None: @@ -136,11 +142,6 @@ def Segment( [L21(ndim=ndims, sigma=0.5 * alpha)] * ncl, nn=[ndims * dimsprod] * ncl ) - # Steps - L = 4 * ndims - tau = 1.0 / (mu * L) if tau is None else tau - mu = 1.0 / (tau * L) if mu is None else mu - # Inversion x: NDArray = PrimalDual( simp, From 876597e377ab710d33d40f5411a8012be8cfc4e7 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 4 Sep 2026 11:34:48 +0100 Subject: [PATCH 66/73] test: added test for Segment raise --- pytests/test_solver.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pytests/test_solver.py b/pytests/test_solver.py index 7200a248..9292b2e9 100644 --- a/pytests/test_solver.py +++ b/pytests/test_solver.py @@ -21,6 +21,7 @@ TwIST, ) from pyproximal.optimization.primaldual import AdaptivePrimalDual, PrimalDual +from pyproximal.optimization.segmentation import Segment from pyproximal.proximal import L1, L2, Box, Quadratic par1 = {"n": 10, "m": 10, "dtype": "float64"} # square, float64 @@ -186,6 +187,19 @@ def test_GPG_epsg(par): ) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) +def test_Segment_taumu(par): + """Check Segment raises error if neither tau or mu are passed""" + with pytest.raises(ValueError, match="tau or mu must be"): + np.random.seed(0) + + # Model and classes + y = np.zeros((par["n"], par["m"])) + cl = np.array([-1, 0, 1]) + + _ = Segment(y, cl, 1.0, 1.0, tau=None, mu=None) + + @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_ProximalPoint(par): """Check solution of ProximalPoint for quadratic function equals the solution of the From 02c851a87d001313102dfb0dcf1e07dd3e0347d9 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 25 Sep 2026 21:42:12 +0100 Subject: [PATCH 67/73] agent: added newop skill --- .claude/skills/newop/SKILL.md | 148 ++++++++++++++++++ .../newop/reference/operator_template.py | 84 ++++++++++ .../skills/newop/reference/test_template.py | 66 ++++++++ 3 files changed, 298 insertions(+) create mode 100644 .claude/skills/newop/SKILL.md create mode 100644 .claude/skills/newop/reference/operator_template.py create mode 100644 .claude/skills/newop/reference/test_template.py diff --git a/.claude/skills/newop/SKILL.md b/.claude/skills/newop/SKILL.md new file mode 100644 index 00000000..10eb88cc --- /dev/null +++ b/.claude/skills/newop/SKILL.md @@ -0,0 +1,148 @@ +--- +name: newop +description: Create a new PyProximal proximal operator following docs/source/adding.rst - class file, docstring, registration, Moreau/edge-case tests, docs entry and example. Use when the user asks to add/implement/port a proximal operator, penalty, norm, or indicator/projection into PyProximal, including porting an existing prox implementation from a paper, a URL or a local file (e.g. "add a Foo proximal operator", "implement the prox of this penalty", "port the prox at "). +--- + +Goal: add a new, PyProximal-compliant `ProxOperator` to the library, complete with +docstring, registration, tests, docs entry and a gallery example, following +`docs/source/adding.rst` (the authoritative guide - read it if unsure). + +The operator may be written from scratch (from a mathematical description or a +paper) or **ported** from an existing implementation supplied as a **web link** or a +**local file**. Ask for the operator name and the source only if neither is +inferable from the invocation. + +## 0. Get the source material + +- **Web link**: fetch it with `WebFetch` (or the browser tools if the page needs + JS). Extract the actual prox/dual prox code or formulas, not the prose. +- **Local file**: read it in full. +- **Neither**: work from the user's mathematical description, and state the + assumed definition of the function before writing code. + +Then write down explicitly, before touching `pyproximal/`: + +- the function \(f(\mathbf{x})\), its parameters and its domain; +- whether a closed form exists for `prox`, for `proxdual`, or both; +- whether the source's "prox" really is \(\prox_{\tau f}\) and not \(\prox_f\) - + **a missing or misplaced `tau` is the most common porting bug**; +- whether \(f\) is differentiable (`hasgrad=True` and a `grad` override); +- whether \(f\) is convex or non-convex (decides the docs section); +- whether \(f\) is an indicator function (then a projection class is needed); +- whether it acts on vectors or on matrices (e.g. singular-value penalties); +- which source parameters become `__init__` arguments, which become derived + members, and which are irrelevant (plotting, I/O, CLI args). + +## 1. Place the file + +- File `pyproximal/proximal/.py` in **UpperCamelCase**, matching the + class (`HalfSpace.py` holds `HalfSpace`). Closely related variants share a file + (`L1`/`L1Ball` in `L1.py`, `Log`/`Log1` in `Log.py`). +- For an **indicator function**, first add `pyproximal/projection/.py` with a + `Proj` class exposing `__call__(x)` (pattern: `projection/HalfSpace.py`), + register it in `projection/__init__.py` (docstring table, star import, `__all__`), + then wrap it in the proximal operator (pattern: `proximal/HalfSpace.py`). +- Reuse existing helpers instead of re-deriving them, e.g. `_softthreshold` and + `_current_sigma` (callable `sigma`) in `proximal/L1.py`, and the projections in + `pyproximal.projection` (`BoxProj`, `L1BallProj`, ...). + +## 2. Write the class + +Use `reference/operator_template.py` as the skeleton. Key rules: + +- Inherit from `pyproximal.ProxOperator.ProxOperator` and call + `super().__init__(Op, hasgrad)`: pass a PyLops `LinearOperator` as `Op` only if + the function uses one (else `None`), and `hasgrad=True` only if you override + `grad` with the true gradient (else `False`, and the base `grad` returns the + gradient of the Moreau envelope). +- `__call__(x)` returns the function value (`float`), or membership (`bool`) for an + indicator. +- Implement `prox(self, x, tau)` and/or `proxdual(self, x, tau)`, **each decorated + with `@_check_tau`**. At least one is required: the base class derives the other + through the Moreau identity. Implement both when closed forms exist. +- Keep it backend-agnostic: use `ncp = get_array_module(x)` from + `pylops.utils.backend` for array operations inside `prox`/`proxdual`, so CuPy + inputs work. Numba/CUDA paths are optional and go through an `engine` argument + with a runtime fallback (see `proximal/Simplex.py`); do not add them unless asked. +- Type-annotate everything (`NDArray` from `pylops.utils.typing`, + `FloatCallableLike` from `pyproximal.utils.typing`): `mypy --strict` runs over + `pyproximal/`. +- Validate inputs in `__init__` as `msg = "..."; raise ValueError(msg)` (ruff `EM`). +- Write the `numpydoc` docstring (`r"""`) with, at minimum: one-line summary, the + definition of the function in maths, `.. versionadded:: X.Y.Z` (next release after + `git describe --tags`), `Parameters`, `Raises` (when `__init__` validates), and a + `Notes` section giving the prox (and dual prox, if implemented) in `.. math::` + blocks with a `.. [1]` reference. Match the detail of neighbouring operators. + +## 3. Register + +In `pyproximal/proximal/__init__.py` add: a row in the module docstring table, +`from . import *`, and the class name in `__all__`. The top-level +`pyproximal/__init__.py` re-exports with `from .proximal import *`, so the operator +is then available as `pyproximal.`. + +## 4. Add tests + +Add to the matching existing file (`test_norms.py`, `test_proximal.py`, +`test_concave_penalties.py`, `test_projection.py`) or create a new one. Follow +`reference/test_template.py`: + +- module-level `par*` dicts (float32 even size / float64 odd size) with + `@pytest.mark.parametrize("par", [(par1), (par2)])` and `np.random.seed(10)`; +- a `__call__` check against an independently computed value; +- **both `prox` and `proxdual` in closed form**: `assert moreau(op, x, tau)`; +- **only one in closed form**: ad-hoc edge cases with known answers (x=0, points + inside/outside the set, `tau`β†’0 returns `x`, large `tau`), and the defining + property where cheap - for indicators the output lies in the set and projecting + twice is idempotent; in general, compare against a brute-force + `scipy.optimize.minimize` of \(f(\mathbf{y}) + \|\mathbf{y}-\mathbf{x}\|_2^2/(2\tau)\) + on a small vector; +- with `hasgrad=True`, add a `gradtest_proximal` test in `test_grads.py`; +- an error-path test for every `raise` in `__init__` (see + `test_Quadratic_nonsquare` in `test_proximal.py`). + +Never loosen the `moreau` tolerance to make a wrong dual pass: fix the maths. + +## 5. Run + +Always use `uv`: + +```bash +uv run pytest pytests/test_.py -k -q +make lint_uv +make typeannot_uv +``` + +Iterate until all tests pass and lint/mypy are clean. + +## 6. Document + +- Add the class name (alphabetically) to the right `autosummary` block in + `docs/source/api/index.rst`: *Vector β†’ Convex* / *Vector β†’ Non-Convex*, + *Matrix-only*, or *Other*; a new projection goes under *Orthogonal projections*. +- Use the operator in at least one example: extend an existing gallery script when + it fits (`examples/plot_norms.py`, `plot_indicators.py`, + `plot_concave_penalties.py`), or add `examples/plot_.py` in sphinx-gallery + format (`r"""` title/underline/description header, `###...` comment blocks between + narrative and code, a matplotlib figure of `x` vs `prox(x)` vs `proxdual(x)`). + Prefer a script in `tutorials/` when the operator is best shown inside a solver. + +## 7. Final checklist (from `docs/source/adding.rst`) + +Report back confirming each item: + +- [ ] class in its own UpperCamelCase file in `pyproximal/proximal/` (plus a + projection in `pyproximal/projection/` for indicators) +- [ ] `__init__`, `__call__`, and `prox` and/or `proxdual` (both with `@_check_tau`) + implemented; `grad` only with `hasgrad=True` +- [ ] exported in `pyproximal/proximal/__init__.py` (table, import, `__all__`) +- [ ] numpydoc docstring with `Parameters` and a mathematical `Notes` section with + a reference +- [ ] tests added (Moreau or edge cases, `__call__`, raises) and passing +- [ ] listed in `docs/source/api/index.rst` +- [ ] used in at least one `examples/` or `tutorials/` script +- [ ] `make lint_uv` and `make typeannot_uv` clean + +When porting, close with a short note on what differed between the source +implementation and the PyProximal version (`tau` scaling, dual in closed form vs +derived via Moreau, shape/dtype handling, removed I/O). diff --git a/.claude/skills/newop/reference/operator_template.py b/.claude/skills/newop/reference/operator_template.py new file mode 100644 index 00000000..9e649bfc --- /dev/null +++ b/.claude/skills/newop/reference/operator_template.py @@ -0,0 +1,84 @@ +"""Skeleton for a new PyProximal proximal operator. + +Copy into ``pyproximal/proximal/.py`` (UpperCamelCase, matching the class), +rename, and fill in. The bodies below implement a scaled L1 norm only so that the +skeleton runs and passes ``moreau`` as-is: replace them with the maths of the new +operator. Delete anything that does not apply. +""" + +import numpy as np +from pylops.utils.backend import get_array_module +from pylops.utils.typing import NDArray + +from pyproximal.ProxOperator import ProxOperator, _check_tau + + +class MyProx(ProxOperator): + r"""One-line summary of the proximal operator. + + Proximal operator of the function + :math:`f(\mathbf{x}) = \sigma \|\mathbf{x}\|_1` (replace with the + definition of the new function). + + .. versionadded:: X.Y.Z + + Parameters + ---------- + sigma : :obj:`float`, optional + Multiplicative coefficient of the function. + + Raises + ------ + ValueError + If ``sigma`` is not positive. + + Notes + ----- + The proximal operator of :math:`f` is defined as [1]_: + + .. math:: + + \prox_{\tau f}(\mathbf{x}) = \ldots + + and, when a closed form is available, its dual proximal operator as: + + .. math:: + + \prox_{\tau f^*}(\mathbf{x}) = \ldots + + .. [1] Author, A., and Author, B., "Title of the paper", Journal, + volume, pp. xx-yy. Year. + + """ + + def __init__(self, sigma: float = 1.0) -> None: + # Op: PyLops linear operator used inside the function (None if not needed) + # hasgrad: True only if f is differentiable and ``grad`` is overridden + super().__init__(None, False) + if sigma <= 0: + msg = "sigma must be positive" + raise ValueError(msg) + self.sigma = sigma + + def __call__(self, x: NDArray) -> float: + # function value (return ``bool`` membership for indicator functions) + return float(self.sigma * np.sum(np.abs(x))) + + @_check_tau + def prox(self, x: NDArray, tau: float) -> NDArray: + # use ``ncp`` (not ``np``) for array operations so that CuPy inputs work + ncp = get_array_module(x) + thresh = tau * self.sigma + return ncp.maximum(ncp.abs(x) - thresh, 0.0) * ncp.sign(x) + + @_check_tau + def proxdual(self, x: NDArray, tau: float) -> NDArray: + # implement only if a closed form exists; otherwise delete this method + # and the base class derives it from ``prox`` via the Moreau identity + ncp = get_array_module(x) + return ncp.clip(x, -self.sigma, self.sigma) + + # def grad(self, x: NDArray) -> NDArray: + # # only with ``hasgrad=True``: true gradient of f (otherwise the base + # # class returns the gradient of the Moreau envelope) + # return ... diff --git a/.claude/skills/newop/reference/test_template.py b/.claude/skills/newop/reference/test_template.py new file mode 100644 index 00000000..fec27111 --- /dev/null +++ b/.claude/skills/newop/reference/test_template.py @@ -0,0 +1,66 @@ +"""Skeleton tests for a new PyProximal proximal operator. + +Merge into the ``pytests/test_*.py`` file that matches the operator +(``test_norms.py``, ``test_proximal.py``, ``test_concave_penalties.py``, +``test_projection.py``) and reuse its existing ``par*`` dicts. Keep the Moreau +test when both ``prox`` and ``proxdual`` have closed forms; otherwise rely on +the edge-case and brute-force tests. +""" + +import numpy as np +import pytest +from numpy.testing import assert_array_almost_equal +from scipy.optimize import minimize + +from pyproximal.proximal import MyProx # adjust import +from pyproximal.utils import moreau + +par1 = {"nx": 10, "sigma": 1.0, "dtype": "float32"} # even float32 +par2 = {"nx": 11, "sigma": 2.0, "dtype": "float64"} # odd float64 + + +@pytest.mark.parametrize("par", [(par1), (par2)]) +def test_MyProx(par): + """MyProx function and proximal/dual proximal""" + np.random.seed(10) + myprox = MyProx(sigma=par["sigma"]) + + # function value, checked against an independent computation + x = np.random.normal(0.0, 1.0, par["nx"]).astype(par["dtype"]) + assert myprox(x) == pytest.approx(par["sigma"] * np.sum(np.abs(x)), rel=1e-5) + + # prox / dualprox (only when both have a closed form) + tau = 2.0 + assert moreau(myprox, x, tau) + + +@pytest.mark.parametrize("par", [(par1), (par2)]) +def test_MyProx_edgecases(par): + """MyProx proximal on cases with a known answer""" + np.random.seed(10) + myprox = MyProx(sigma=par["sigma"]) + + # prox of zero (replace with the known answer for the new function) + x = np.zeros(par["nx"], dtype=par["dtype"]) + assert_array_almost_equal(myprox.prox(x, 1.0), x) + + # tau -> 0 leaves x unchanged + x = np.random.normal(0.0, 1.0, par["nx"]).astype(par["dtype"]) + assert_array_almost_equal(myprox.prox(x, 1e-10), x, decimal=5) + + # brute-force minimization of f(y) + ||y - x||^2 / (2 tau) on a small vector + tau = 0.5 + x = np.random.normal(0.0, 1.0, 5) + y = minimize( + lambda y: myprox(y) + np.sum((y - x) ** 2) / (2 * tau), + x, + method="Powell", + options={"xtol": 1e-8, "ftol": 1e-10}, + ).x + assert_array_almost_equal(myprox.prox(x, tau), y, decimal=4) + + +def test_MyProx_raises(): + """Check input validation of MyProx""" + with pytest.raises(ValueError, match="sigma must be positive"): + _ = MyProx(sigma=-1.0) From 79b49c84e8e3f5a1529a0614e9842027690c5e61 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 25 Sep 2026 22:20:31 +0100 Subject: [PATCH 68/73] doc: added deblurring tutorial and pywavelets doc dependency --- environment-dev-arm.yml | 1 + environment-dev.yml | 1 + pyproject.toml | 1 + tutorials/deblurring.py | 166 ++++++++++++++++++++++++++++++++++++++++ uv.lock | 7 ++ 5 files changed, 176 insertions(+) create mode 100644 tutorials/deblurring.py diff --git a/environment-dev-arm.yml b/environment-dev-arm.yml index 5198b2e9..24a3b2ed 100644 --- a/environment-dev-arm.yml +++ b/environment-dev-arm.yml @@ -9,6 +9,7 @@ dependencies: - scipy>=1.14.0 - pylops>=2.4.0 - scikit-image + - pywavelets - matplotlib - typing_extensions>=4.0.0 - ipython diff --git a/environment-dev.yml b/environment-dev.yml index 0eb2569c..5848f4ef 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -9,6 +9,7 @@ dependencies: - scipy>=1.14.0 - pylops>=2.4.0 - scikit-image + - pywavelets - matplotlib - typing_extensions>=4.0.0 - ipython diff --git a/pyproject.toml b/pyproject.toml index 9053a8f0..f2bb575a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ doc = [ "scikit-image", "bm3d", "bm4d", + "pywavelets", "pooch", "sphinx", "sphinx-design", diff --git a/tutorials/deblurring.py b/tutorials/deblurring.py new file mode 100644 index 00000000..7e600899 --- /dev/null +++ b/tutorials/deblurring.py @@ -0,0 +1,166 @@ +r""" +Deblurring with multiple wavelet priors +======================================= +This tutorial considers the problem of deblurring an image that has been +convolved with a 2D Gaussian kernel and contaminated by random noise: + +.. math:: + \mathbf{y} = \mathbf{B} \mathbf{x} + \mathbf{n} + +where :math:`\mathbf{B}` is a 2D convolution operator and +:math:`\mathbf{n} \sim \mathcal{N}(0, \sigma^2)`. + +Natural images are rarely well represented by a single sparsifying transform: +piecewise-constant regions with sharp edges are compactly described by the +Haar wavelet, whilst smoother, textured regions are better captured by +higher-order wavelets such as the Daubechies-4 (db4). We will therefore +promote sparsity in both domains at the same time by solving: + +.. math:: + \argmin_\mathbf{x} \frac{1}{2}\|\mathbf{B}\mathbf{x}-\mathbf{y}\|_2^2 + + \lambda_1 \|\mathbf{W}_1 \mathbf{x}\|_1 + + \lambda_2 \|\mathbf{W}_2 \mathbf{x}\|_1 + +where :math:`\mathbf{W}_1` and :math:`\mathbf{W}_2` are the Haar and db4 +2D wavelet transforms, respectively. + +By stacking the two wavelet transforms into a single operator +:math:`\mathbf{K} = [\mathbf{W}_1^T, \mathbf{W}_2^T]^T`, the regularization +term becomes a separable function of :math:`\mathbf{K}\mathbf{x}`, whose +proximal operator is simply the stack of the proximal operators of the two +L1 norms. This problem can therefore be solved with the +:func:`pyproximal.optimization.primaldual.PrimalDual` solver +(Chambolle-Pock algorithm). + +""" + +import matplotlib.pyplot as plt +import numpy as np +import pylops +from pylops.utils.metrics import psnr + +import pyproximal + +plt.close("all") +np.random.seed(0) + +############################################################################### +# Let's start by loading an image, converting it to grayscale, and cropping +# it to a square of size power of two (so that the wavelet transforms do not +# require any padding). +im = plt.imread("../testdata/butterfly.png") +im = np.mean(im[..., :3], axis=-1) + +n = 256 +h, w = im.shape +im = im[(h - n) // 2 : (h - n) // 2 + n, (w - n) // 2 : (w - n) // 2 + n] +ny, nx = im.shape + +############################################################################### +# We now create a Gaussian blurring kernel and the associated +# :class:`pylops.signalprocessing.Convolve2D` operator. This is used to +# create the blurred data, to which we add white Gaussian noise with a +# signal-to-noise ratio of 30 dB. +nh = 11 +sigh = 2.0 +t = np.arange(-(nh // 2), nh // 2 + 1) +hy, hx = np.meshgrid(t, t, indexing="ij") +h = np.exp(-(hx**2 + hy**2) / (2 * sigh**2)) +h /= h.sum() + +Bop = pylops.signalprocessing.Convolve2D( + dims=(ny, nx), h=h, offset=(nh // 2, nh // 2), dtype="float64" +) + +yclean = Bop @ im.ravel() +snr_db = 30.0 +sigman = np.linalg.norm(yclean) / np.sqrt(yclean.size) * 10 ** (-snr_db / 20) +y = yclean + sigman * np.random.randn(yclean.size) + +fig, axs = plt.subplots(1, 3, figsize=(12, 4)) +axs[0].imshow(im, cmap="gray", vmin=0, vmax=1) +axs[0].set_title("True image") +axs[0].axis("off") +axs[1].imshow(y.reshape(ny, nx), cmap="gray", vmin=0, vmax=1) +axs[1].set_title(f"Blurred+noisy (PSNR={psnr(im, y.reshape(ny, nx), 1.0):.1f} dB)") +axs[1].axis("off") +axs[2].imshow(h, cmap="viridis") +axs[2].set_title(rf"Blurring kernel ($\sigma$={sigh})") +axs[2].axis("off") +plt.tight_layout() + +############################################################################### +# Next, we define the two wavelet transforms and stack them vertically into +# the operator :math:`\mathbf{K}`. +W1op = pylops.signalprocessing.DWT2D(dims=(ny, nx), wavelet="haar", level=3) +W2op = pylops.signalprocessing.DWT2D(dims=(ny, nx), wavelet="db4", level=3) +Kop = pylops.VStack([W1op, W2op]) +n1, n2 = W1op.shape[0], W2op.shape[0] + +############################################################################### +# We are now ready to define the proximal operators. The data term is a +# :class:`pyproximal.L2` norm with the blurring operator, whilst the +# regularization term is a :class:`pyproximal.VStack` of two +# :class:`pyproximal.L1` norms, each acting on the portion of the vector +# :math:`\mathbf{K}\mathbf{x}` associated with one of the two wavelet +# transforms. +lam1 = 1e-3 +lam2 = 1e-3 + +f = pyproximal.L2(Op=Bop, b=y) +g = pyproximal.VStack( + [pyproximal.L1(sigma=lam1), pyproximal.L1(sigma=lam2)], nn=[n1, n2] +) + +############################################################################### +# The Primal-Dual algorithm converges provided that +# :math:`\tau \mu \|\mathbf{K}\|_2^2 < 1`. Since each wavelet transform is +# (close to) orthogonal, we have :math:`\|\mathbf{K}\|_2^2 \le 2`, and choose +# the step lengths accordingly (with some safety margin). To appreciate the +# benefit of combining the two priors, we also solve the problem using a +# single wavelet transform at a time (for which +# :math:`\|\mathbf{W}_i\|_2^2 \le 1`). +tau = 0.4 +mu = 1.0 / (tau * 2.5) +mu1 = 1.0 / (tau * 1.25) +niter = 400 +x0 = np.zeros(ny * nx) + +# Haar only +xhaar = pyproximal.optimization.primaldual.PrimalDual( + f, pyproximal.L1(sigma=lam1), W1op, tau=tau, mu=mu1, x0=x0, niter=niter +) +xhaar = xhaar.reshape(ny, nx) + +# db4 only +xdb4 = pyproximal.optimization.primaldual.PrimalDual( + f, pyproximal.L1(sigma=lam2), W2op, tau=tau, mu=mu1, x0=x0, niter=niter +) +xdb4 = xdb4.reshape(ny, nx) + +# Haar + db4 +xdual = pyproximal.optimization.primaldual.PrimalDual( + f, g, Kop, tau=tau, mu=mu, x0=x0, niter=niter, show=True +) +xdual = xdual.reshape(ny, nx) + +############################################################################### +# Finally, we compare the three reconstructions. +fig, axs = plt.subplots(2, 2, figsize=(9, 9)) +for ax, img, title in zip( + axs.ravel(), + [y.reshape(ny, nx), xhaar, xdb4, xdual], + ["Blurred+noisy", "Haar prior", "db4 prior", "Haar + db4 priors"], + strict=True, +): + ax.imshow(img, cmap="gray", vmin=0, vmax=1) + ax.set_title(f"{title} (PSNR={psnr(im, img, 1.0):.1f} dB)") + ax.axis("off") +plt.tight_layout() + +############################################################################### +# Both single-prior reconstructions already remove most of the blur. The +# Haar prior favours piecewise-constant solutions and is therefore prone +# to blocky artefacts, whilst the db4 prior produces smoother images. By +# combining the two priors, we obtain a reconstruction that benefits from +# the strengths of both transforms. diff --git a/uv.lock b/uv.lock index b74d368e..212e2219 100644 --- a/uv.lock +++ b/uv.lock @@ -638,18 +638,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/3c/4716e285f8459a5707ef39d84e9a7547bf16f87c583ac4789c0763d6d986/cupy_cuda13x-14.2.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:a22e2f17c97940cd566f351f186d8fe04285280d052ca1512ff223febbe9979c", size = 74074375, upload-time = "2026-08-20T02:41:04.327Z" }, { url = "https://files.pythonhosted.org/packages/ac/cb/41b6d8f1880e6f3128f8084b7622b1a3ca2d615ab77af9ef313b4aafed3d/cupy_cuda13x-14.2.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:f94843e54500a9c8c169135f79572bfaba946efe4f7178e834c227280c1b0c12", size = 69969199, upload-time = "2026-08-20T02:41:07.842Z" }, { url = "https://files.pythonhosted.org/packages/f8/eb/25b8c1a415ade6479ed684c5cbff1e6f5002a92cbf26ef826df56a40cf69/cupy_cuda13x-14.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:3893acd8d4dc78d113736045eaa306e496ee7bd8253bd959513ba28c91ed396d", size = 36082966, upload-time = "2026-08-20T02:41:10.723Z" }, + { url = "https://files.pythonhosted.org/packages/10/72/ad885cc21a21208b94044b34f50a840394b2e9dbb6111ed43959366aa6eb/cupy_cuda13x-14.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:e0ad42a1ef35549282e2087a7c1e6ec8b7aa624a95e8fc0a60da777ca87e71de", size = 35136070, upload-time = "2026-09-01T06:41:38.284Z" }, { url = "https://files.pythonhosted.org/packages/dd/c8/68e07ff959f6a9f93169e37a64a4c24223487102b8625191e4a566cab465/cupy_cuda13x-14.2.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:0a9b6538079c5151dcd99cf44b94cc3f1242315b7de0498b4619cef9b2a7c2c8", size = 73382974, upload-time = "2026-08-20T02:41:13.648Z" }, { url = "https://files.pythonhosted.org/packages/7e/bf/c4c25e97552320451afdcd459d4b6e0b715459594e998b23051c06db64c3/cupy_cuda13x-14.2.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:9aafadbe29db3d4285026044aafeb28f2a3de4bd0df70718ec6892b7d27a4ded", size = 69439247, upload-time = "2026-08-20T02:41:17.375Z" }, { url = "https://files.pythonhosted.org/packages/5e/88/0a4d2d075485c4a82bfcd4725300ff87d17de36dce27b36f720d286f0f70/cupy_cuda13x-14.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:9e8bc8a7693c5c212cd1450d728f47e1f05aa07ae003de30415893d0daf7d243", size = 35986921, upload-time = "2026-08-20T02:41:20.253Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ab/8a756f553397b8509906d4a1d8083d92d3d21cbde115504e7a5db97de61f/cupy_cuda13x-14.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:0dae6d788752c987c1cd87adb903640620820feb895339e8bc51a01fba0fb804", size = 35049803, upload-time = "2026-09-01T06:41:43.442Z" }, { url = "https://files.pythonhosted.org/packages/3c/02/3cb39774c71a16275c50b5d1e92ddcefa948ff1e69f464249b70a79b0abe/cupy_cuda13x-14.2.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:449af7f608dc70ce64e8c45cf5a6ea0bbef51623e7c806b1a43445ae3c76e1e0", size = 72969322, upload-time = "2026-08-20T02:41:23.311Z" }, { url = "https://files.pythonhosted.org/packages/4e/27/216370ae8c39fa9835645be3c27513ab72f52cf4f51faf58d799fba981b6/cupy_cuda13x-14.2.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:5b79a606f6639d9bed74ef7072f36756a95a804c86d7adb367b39307ac771600", size = 69048238, upload-time = "2026-08-20T02:41:26.831Z" }, { url = "https://files.pythonhosted.org/packages/5d/75/9df3c545baad4ff2e384db1ff6c8005fa4040492c4ee5672ca8b8711d29d/cupy_cuda13x-14.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9a2a55abf889f6df68b5f62cd5f3ec4a2f305ac57ed09becd52e16201edcd86", size = 35965490, upload-time = "2026-08-20T02:41:29.943Z" }, + { url = "https://files.pythonhosted.org/packages/5c/3d/ee85ce39c2f32e8561c81c507df458a914b45d9a0eb1a54e5396c2cfd632/cupy_cuda13x-14.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:5ddaf38291f60ec20e8c7889b28e339bf17ad1b4df768b8bb5e6c9f9e445efbb", size = 35029087, upload-time = "2026-09-01T06:41:48.593Z" }, { url = "https://files.pythonhosted.org/packages/e5/70/8c80c9e7010193afed8ccb7110d6173aa23bf3a6ce2fc2450244b0fa50a1/cupy_cuda13x-14.2.0-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:7ac57c4e16c62f265b5ecc2b45d6bbafe9dc70a99b849ed6cfb8d16f32204589", size = 72840193, upload-time = "2026-08-20T02:41:33.146Z" }, { url = "https://files.pythonhosted.org/packages/c2/b8/4f4c4f34fc31ab8d136ed965a919505297974629d9852c50e15bfd616281/cupy_cuda13x-14.2.0-cp314-cp314-manylinux2014_x86_64.whl", hash = "sha256:ff0bdebd1b43c0c6db53095784c787c4e4eae671356cf521c4b7482ed78a1a7e", size = 68422828, upload-time = "2026-08-20T02:41:36.527Z" }, { url = "https://files.pythonhosted.org/packages/7d/5c/58941ece5ed0b2cc8304f8ba81b5e8334759d409306475d53c52ac99809b/cupy_cuda13x-14.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:b99a8bf9d5391954c0c08675a8cb39219923fe2ee249faa3546ebbf75783a360", size = 36110882, upload-time = "2026-08-20T02:41:39.837Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c5/cc7d1a9b764b3f828e09b48a73d0fdeaad15ecd2c7e50507f6d5fcf5fd1a/cupy_cuda13x-14.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c61c59ba665948c0a855926997ee96026420b628816b2e406998ec797caddb22", size = 35184806, upload-time = "2026-09-01T06:41:58.317Z" }, { url = "https://files.pythonhosted.org/packages/6f/54/a582ac70e9e75d3fad7ae073051bcd50dd128a1f66dd275291dc5f48d997/cupy_cuda13x-14.2.0-cp314-cp314t-manylinux2014_aarch64.whl", hash = "sha256:e8bb453551deced08ec5c4576da61ebf1e2b4a45aeff48e09f47ec332f7cad3d", size = 73336756, upload-time = "2026-08-20T02:41:42.718Z" }, { url = "https://files.pythonhosted.org/packages/f9/aa/222151ba5061d7a01efe4009efad9f9a7164e15bb2a494d9aa86efc97d4f/cupy_cuda13x-14.2.0-cp314-cp314t-manylinux2014_x86_64.whl", hash = "sha256:313500c5d415f65d388e84a9d2eb7f07f97c23c85dc5785f986e75d46d9c9693", size = 68792663, upload-time = "2026-08-20T02:41:46.079Z" }, { url = "https://files.pythonhosted.org/packages/d4/20/13dd37dbeaec879c8348fde2bfca67d96daa56a6e63e4a5083ff61660e53/cupy_cuda13x-14.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c517fc55502a7e3d1a8b9310abad95cec61dded5a38d328fe0220bcf7b036268", size = 37004845, upload-time = "2026-08-20T02:41:48.963Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5b/b1367f79abd90a259dbb7f0603dd162c03c3a76ec1d02c2527b0c79d46ba/cupy_cuda13x-14.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:83885db963eb0b95a0757b4907a37115a1d2aebfa9cf43b99ebac8db35713d37", size = 35485280, upload-time = "2026-09-01T06:41:53.618Z" }, ] [[package]] @@ -1927,6 +1932,7 @@ doc = [ { name = "nbsphinx" }, { name = "numpydoc" }, { name = "pooch" }, + { name = "pywavelets" }, { name = "scikit-image" }, { name = "shibuya" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or (extra == 'extra-10-pyproximal-gpu-cu12' and extra == 'extra-10-pyproximal-gpu-cu13')" }, @@ -1969,6 +1975,7 @@ doc = [ { name = "nbsphinx" }, { name = "numpydoc" }, { name = "pooch" }, + { name = "pywavelets" }, { name = "scikit-image" }, { name = "shibuya" }, { name = "sphinx" }, From 1d0249f35acfa8685c032d3ffb7fa581674d4b04 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 25 Sep 2026 22:20:57 +0100 Subject: [PATCH 69/73] feat: added support for vector sigma in L1 --- pyproximal/proximal/L1.py | 27 ++++++++------ pytests/test_norms.py | 77 +++++++++++++++++++++++++++++---------- 2 files changed, 73 insertions(+), 31 deletions(-) diff --git a/pyproximal/proximal/L1.py b/pyproximal/proximal/L1.py index 42129ae2..6427d555 100644 --- a/pyproximal/proximal/L1.py +++ b/pyproximal/proximal/L1.py @@ -54,7 +54,8 @@ class L1(ProxOperator): Proximal operator of the :math:`\ell_1` norm: - :math:`\sigma\|\mathbf{x} - \mathbf{g}\|_1 = \sigma \sum_i |x_i - g_i|` for - 1-dimensional arrays; + 1-dimensional arrays (or :math:`\sum_i \sigma_i |x_i - g_i|` when + :math:`\sigma` is a vector of the same size as :math:`\mathbf{x}`); - :math:`\sum_j \sigma_j \|\mathbf{X}_j - \mathbf{g}\|_1 = \sum_j \sigma_j \sum_i |x_{i,j} - g_i|` for 2-dimensional arrays. @@ -62,10 +63,10 @@ class L1(ProxOperator): ---------- sigma : :obj:`float` or :obj:`numpy.ndarray` or :obj:`func`, optional Multiplicative coefficient of L1 norm. This can be a constant number, a list - of values (for 2-dimensional inputs, acting on the second dimension) or - a function that is called passing a counter which keeps track of how many - times the ``prox`` method has been invoked before and returns a scalar (or a list of) - ``sigma`` to be used. + of values (for 1-dimensional inputs, acting element-wise; for 2-dimensional + inputs, acting on the second dimension) or a function that is called passing + a counter which keeps track of how many times the ``prox`` method has been + invoked before and returns a scalar (or a list of) ``sigma`` to be used. g : :obj:`numpy.ndarray`, optional Vector to be subtracted @@ -98,6 +99,12 @@ class L1(ProxOperator): \sigma, & x_i > \sigma\\ \end{cases} + When :math:`\mathbf{g} \neq \mathbf{0}`, the convex conjugate becomes + :math:`f^*(\mathbf{x}) = \mathbf{x}^T\mathbf{g} + + \mathcal{I}_{\|\cdot\|_{\infty} <=\sigma}(\mathbf{x})`, and the dual + operator is obtained by applying the same projection to + :math:`\mathbf{x} - \tau \mathbf{g}`. + .. [1] Chambolle, and A., Pock, "A first-order primal-dual algorithm for convex problems with applications to imaging", Journal of Mathematical Imaging and Vision, 40, 8pp. 120–145. 2011. @@ -122,8 +129,8 @@ def __init__( def __call__(self, x: NDArray) -> float: sigma = _current_sigma(self.sigma, self.count) if self.g is None: - return float(np.sum(sigma * np.sum(np.abs(x), axis=0))) - return float(np.sum(sigma * np.sum(np.abs(x - self.g), axis=0))) + return float(np.sum(sigma * np.abs(x))) + return float(np.sum(sigma * np.abs(x - self.g))) def _increment_count(func: Callable[..., Any]) -> Callable[..., Any]: """Increment counter""" @@ -147,11 +154,7 @@ def prox(self, x: NDArray, tau: float) -> NDArray: @_check_tau def proxdual(self, x: NDArray, tau: float) -> NDArray: - if not isinstance(self.gdual, np.ndarray): - x = self.box(x) - else: - x = self._proxdual_moreau(x, tau) - return x + return self.box(x - tau * self.gdual) class L1Ball(ProxOperator): diff --git a/pytests/test_norms.py b/pytests/test_norms.py index 65c2ebb8..1a3ace13 100644 --- a/pytests/test_norms.py +++ b/pytests/test_norms.py @@ -314,23 +314,48 @@ def test_L2Convolve(par): @pytest.mark.parametrize("par", [(par1), (par2)]) -def test_L1(par): - """L1 norm and grad and proximal/dual proximal""" +def test_L1_scalar(par): + """L1 norm (with and without g) and grad and proximal/dual proximal + with sigma as scalar""" np.random.seed(10) - l1 = L1(sigma=par["sigma"]) + g = np.random.normal(0.0, 1.0, par["nx"]).astype(par["dtype"]) + x = np.random.normal(0.0, 1.0, par["nx"]).astype(par["dtype"]) + tau = 2.0 + for gg in [None, g]: + l1 = L1(sigma=par["sigma"], g=gg) + xg = x if gg is None else x - gg + + # norm + assert l1(x) == pytest.approx(par["sigma"] * np.sum(np.abs(xg))) + + # grad (note that since this norm is non-smooth, the + # gradient method is not implemented; as such the gradient + # of the Moreau envelope is called instead + _ = l1.grad(x) + + # prox / dualprox + assert moreau(l1, x, tau) + + +@pytest.mark.parametrize("par", [(par1), (par2)]) +def test_L1_vector(par): + """L1 norm and proximal/dual proximal with sigma as vector""" + np.random.seed(10) + + sigma = np.random.uniform(0.1, 2.0, par["nx"]).astype(par["dtype"]) + l1 = L1(sigma=sigma) # norm x = np.random.normal(0.0, 1.0, par["nx"]).astype(par["dtype"]) - assert l1(x) == par["sigma"] * np.sum(np.abs(x)) + assert l1(x) == pytest.approx(np.sum(sigma * np.abs(x))) - # grad (note that since this norm is non-smooth, the - # gradient method is not implemented; as such the gradient - # of the Moreau envelope is called instead - _ = l1.grad(x) + # prox + tau = 2.0 + xprox = np.sign(x) * np.maximum(np.abs(x) - tau * sigma, 0.0) + assert_array_almost_equal(l1.prox(x, tau), xprox) # prox / dualprox - tau = 2.0 assert moreau(l1, x, tau) @@ -351,20 +376,34 @@ def test_L1_func(par): @pytest.mark.parametrize("par", [(par1), (par2)]) -def test_L1_diff(par): - """L1 norm of difference and proximal/dual proximal""" +def test_L1_2d(par): + """L1 norm and proximal/dual proximal of 2-dimensional input with + sigma as vector acting on the second dimension""" np.random.seed(10) - g = np.random.normal(0.0, 1.0, par["nx"]).astype(par["dtype"]) - l1 = L1(sigma=par["sigma"], g=g) - - # norm - x = np.random.normal(0.0, 1.0, par["nx"]).astype(par["dtype"]) - assert l1(x) == par["sigma"] * np.sum(np.abs(x - g)) + ncol = 3 + sigma = np.random.uniform(0.1, 2.0, ncol).astype(par["dtype"]) + g = np.random.normal(0.0, 1.0, (par["nx"], 1)).astype(par["dtype"]) - # prox / dualprox + x = np.random.normal(0.0, 1.0, (par["nx"], ncol)).astype(par["dtype"]) tau = 2.0 - assert moreau(l1, x, tau) + for gg in [None, g]: + l1 = L1(sigma=sigma, g=gg) + xg = x if gg is None else x - gg + + # norm + norm = sum(sigma[j] * np.sum(np.abs(xg[:, j])) for j in range(ncol)) + assert l1(x) == pytest.approx(norm) + + # prox + xprox = np.sign(xg) * np.maximum(np.abs(xg) - tau * sigma, 0.0) + if gg is not None: + xprox += gg + assert_array_almost_equal(l1.prox(x, tau), xprox) + + # dualprox (conjugate of sigma||x - g||_1 is + i_{|x|<=sigma}) + xdual = x if gg is None else x - tau * gg + assert_array_almost_equal(l1.proxdual(x, tau), np.clip(xdual, -sigma, sigma)) @pytest.mark.parametrize("par", [(par1), (par2)]) From c9322debbc76f18ae57330180e4ff56a69ca93da Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 25 Sep 2026 22:34:54 +0100 Subject: [PATCH 70/73] minor: updated steps in deblurring tutorial --- tutorials/deblurring.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tutorials/deblurring.py b/tutorials/deblurring.py index 7e600899..ac972813 100644 --- a/tutorials/deblurring.py +++ b/tutorials/deblurring.py @@ -120,21 +120,25 @@ # benefit of combining the two priors, we also solve the problem using a # single wavelet transform at a time (for which # :math:`\|\mathbf{W}_i\|_2^2 \le 1`). + +Lw = 1.0 # maxeig(W_i^H W_i) +L = 2.0 # maxeig(K^H K) + tau = 0.4 -mu = 1.0 / (tau * 2.5) -mu1 = 1.0 / (tau * 1.25) +muw = 0.8 / (tau * Lw) +mu = 0.8 / (tau * L) niter = 400 x0 = np.zeros(ny * nx) # Haar only xhaar = pyproximal.optimization.primaldual.PrimalDual( - f, pyproximal.L1(sigma=lam1), W1op, tau=tau, mu=mu1, x0=x0, niter=niter + f, pyproximal.L1(sigma=lam1), W1op, tau=tau, mu=muw, x0=x0, niter=niter ) xhaar = xhaar.reshape(ny, nx) # db4 only xdb4 = pyproximal.optimization.primaldual.PrimalDual( - f, pyproximal.L1(sigma=lam2), W2op, tau=tau, mu=mu1, x0=x0, niter=niter + f, pyproximal.L1(sigma=lam2), W2op, tau=tau, mu=muw, x0=x0, niter=niter ) xdb4 = xdb4.reshape(ny, nx) From fa7898c55df0b3e0b35342183fd923f982435fe9 Mon Sep 17 00:00:00 2001 From: mika <211269698+mikamikasuki@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:13:09 -0700 Subject: [PATCH 71/73] fix: preserve feasible inputs in L1 ball projection --- pyproximal/projection/L1.py | 9 ++++++++- pytests/test_projection.py | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/pyproximal/projection/L1.py b/pyproximal/projection/L1.py index e6991fd1..6b3672cd 100644 --- a/pyproximal/projection/L1.py +++ b/pyproximal/projection/L1.py @@ -30,7 +30,12 @@ class L1BallProj: .. math:: - P_{L1_{r}} (\mathbf{x}) = sign(\mathbf{x}) P_{\operatorname{Simplex}(r)}(\mathbf{x}) + P_{L1_{r}} (\mathbf{x}) = + \begin{cases} + \mathbf{x}, & \|\mathbf{x}\|_1 \leq r \\ + sign(\mathbf{x}) P_{\operatorname{Simplex}(r)}(|\mathbf{x}|), + & \text{otherwise} + \end{cases} Note that this is the proximal operator of the corresponding indicator function :math:`\mathcal{I}_{L1_{r}}`. @@ -49,6 +54,8 @@ def __init__( self.simplex = SimplexProj(n, radius, maxiter, xtol) def __call__(self, x: NDArray) -> NDArray: + if np.sum(np.abs(x)) <= self.radius: + return x if np.iscomplexobj(x): return np.exp(1j * np.angle(x)) * self.simplex(np.abs(x)) else: diff --git a/pytests/test_projection.py b/pytests/test_projection.py index 2473d8a5..b48d96a9 100644 --- a/pytests/test_projection.py +++ b/pytests/test_projection.py @@ -1,8 +1,9 @@ import numpy as np import pytest -from numpy.testing import assert_array_almost_equal +from numpy.testing import assert_array_almost_equal, assert_array_equal from pylops.basicoperators import Identity +from pyproximal.projection import L1BallProj from pyproximal.proximal import ( AffineSet, Box, @@ -157,6 +158,38 @@ def test_L1Ball(par): assert moreau(l1, x, tau) +@pytest.mark.parametrize("dtype", ["float32", "float64", "complex64", "complex128"]) +@pytest.mark.parametrize("values", [[0.125, -0.25], [1.0, 0.0], [0.0, 0.0]]) +def test_L1Ball_feasible_inputs(dtype, values): + """The projection fixes every point already in the ball.""" + x = np.array(values, dtype=dtype) + original = x.copy() + projection = L1BallProj(x.size, radius=1.0) + ball = L1Ball(x.size, radius=1.0) + + assert_array_equal(projection(x), original) + assert_array_equal(ball.prox(x, 0.5), original) + assert_array_equal(ball.prox(x, 2.0), original) + assert_array_equal(x, original) + + +@pytest.mark.parametrize("dtype", ["complex64", "complex128"]) +def test_L1Ball_complex_interior(dtype): + x = np.array([0.125 + 0.125j, 0.25 - 0.125j], dtype=dtype) + ball = L1Ball(x.size, radius=1.0) + assert_array_equal(ball.prox(x, 1.0), x) + + +@pytest.mark.parametrize("dtype", ["float32", "float64", "complex64", "complex128"]) +@pytest.mark.parametrize("tau", [0.5, 2.0]) +def test_L1Ball_dual_inside_scaled_ball(dtype, tau): + """The conjugate prox is zero when x/tau lies inside the ball.""" + x = np.array([0.125, -0.25], dtype=dtype) + ball = L1Ball(x.size, radius=1.0) + assert_array_equal(ball.proxdual(x, tau), np.zeros_like(x)) + assert moreau(ball, x, tau) + + @pytest.mark.parametrize("par", [(par1), (par2)]) def test_NuclBall(par): """Nuclear Ball projection and proximal/dual proximal of related indicator""" From 9f48cf60ddb99f8cace0b61b9917589cebfde207 Mon Sep 17 00:00:00 2001 From: mika <211269698+mikamikasuki@users.noreply.github.com> Date: Fri, 2 Oct 2026 15:48:01 -0700 Subject: [PATCH 72/73] test: clarify feasible L1 projection cases --- pytests/test_projection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytests/test_projection.py b/pytests/test_projection.py index b48d96a9..38382890 100644 --- a/pytests/test_projection.py +++ b/pytests/test_projection.py @@ -161,7 +161,7 @@ def test_L1Ball(par): @pytest.mark.parametrize("dtype", ["float32", "float64", "complex64", "complex128"]) @pytest.mark.parametrize("values", [[0.125, -0.25], [1.0, 0.0], [0.0, 0.0]]) def test_L1Ball_feasible_inputs(dtype, values): - """The projection fixes every point already in the ball.""" + """Check that the projection returns the input as already in the ball.""" x = np.array(values, dtype=dtype) original = x.copy() projection = L1BallProj(x.size, radius=1.0) @@ -175,6 +175,7 @@ def test_L1Ball_feasible_inputs(dtype, values): @pytest.mark.parametrize("dtype", ["complex64", "complex128"]) def test_L1Ball_complex_interior(dtype): + """Check that complex inputs already in the ball are returned unchanged.""" x = np.array([0.125 + 0.125j, 0.25 - 0.125j], dtype=dtype) ball = L1Ball(x.size, radius=1.0) assert_array_equal(ball.prox(x, 1.0), x) From 54ea79cb90c382399506123e9b1b412e6a54a2f8 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Sat, 3 Oct 2026 16:35:47 +0100 Subject: [PATCH 73/73] doc: prepare for v0.13.0 --- CHANGELOG.md | 17 +++++++++++++++-- docs/source/changelog.rst | 21 ++++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a4d705c..3fc0126d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,26 @@ Changelog ========= +# 0.13.0 + +* Added ``pyproximal.optimization.red.RED`` solver. +* Added ``pyproximal.proximal.RED`` operator. +* Added class-based solvers in ``pyproximal.optimization.cls_primal`` and ``pyproximal.optimization.cls_primaldual``. +* Added ``tau`` and ``mu`` optional parameters to ``pyproximal.optimization.segmentation.Segment`` solver. +* Added support for ``sigma`` vector in ``pyproximal.proximal.L1``. +* Added ``numba-cuda`` to optional dependencies. +* Fixed ``pyproximal.projection.L1BallProj`` to preserve feasible inputs . + preserve feasible inputs in L1 ball projection +* Fixed handling of complex-valued inputs in ``pyproximal.proximal.L21`` and ``pyproximal.proximal.L21_plus_L1``. + + # 0.12.0 * Added ``pyproximal.optimization.primal.ConsensusADMM`` and ``pyproximal.optimization.primal.PPXA`` solvers * Modified role of x and y in ``pyproximal.optimization.primal.DouglasRachfordSplitting`` -* Fix norm computation in ``pyproximal.proximal.L1`` for +* Fixed norm computation in ``pyproximal.proximal.L1`` for 2D-arrays # 0.11.1 @@ -113,7 +126,7 @@ Changelog * Added classes standard deviation in ``pyproximal.optimization.segmentation.Segment`` solver * Added `chain` method ``pyproximal.ProxOperator`` -* Fix ``pyproximal.proximal.Orthogonal`` by introducing `alpha` +* Fixed ``pyproximal.proximal.Orthogonal`` by introducing `alpha` in the proximal evaluation # 0.1.0 diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 6c31058f..452533b5 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -3,7 +3,26 @@ |:newspaper_roll:| Changelog ############################ -Version 0.12.1 + +Version 0.13.0 +-------------- +*Released on: 30/10/2026* + +* Added :py:class:`pyproximal.optimization.red.RED` solver. +* Added :py:class:`pyproximal.proximal.RED` operator. +* Added class-based solvers in :py:mod:`pyproximal.optimization.cls_primal` + and :py:mod:`pyproximal.optimization.cls_primaldual`. +* Added ``tau`` and ``mu`` optional parameters to + :py:class:`pyproximal.optimization.segmentation.Segment` solver. +* Added support for ``sigma`` vector in :py:class:`pyproximal.proximal.L1`. +* Added ``numba-cuda`` to optional dependencies. +* Fixed :py:class:`pyproximal.projection.L1BallProj` to preserve feasible inputs . + preserve feasible inputs in L1 ball projection +* Fixed handling of complex-valued inputs in :py:class:`pyproximal.proximal.L21` + and :py:class:`pyproximal.proximal.L21_plus_L1`. + + +Version 0.12.0 -------------- *Released on: 18/04/2026*