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)
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 5702c85e..dc134e3d 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 }}
@@ -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/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 *)'
+
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/
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/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/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/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.
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 369e5b85..31382d5f 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: |
@@ -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: |
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/api/index.rst b/docs/source/api/index.rst
index cb1a60e5..2810fbed 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
@@ -148,19 +149,20 @@ Utility functions
gradtest_bilinear
-
Solvers
-------
Primal
^^^^^^
-.. currentmodule:: pyproximal.optimization.primal
+Class-based
+~~~~~~~~~~~
+
+.. currentmodule:: pyproximal.optimization.cls_primal
.. autosummary::
:toctree: generated/
- AcceleratedProximalGradient
ADMM
ADMML2
AndersonProximalGradient
@@ -174,31 +176,45 @@ Primal
PPXA
ConsensusADMM
-.. currentmodule:: pyproximal.optimization.palm
+Function-based
+~~~~~~~~~~~~~~
+
+.. currentmodule:: pyproximal.optimization.primal
.. autosummary::
:toctree: generated/
- PALM
- iPALM
+ AcceleratedProximalGradient
+ ADMM
+ ADMML2
+ AndersonProximalGradient
+ GeneralizedProximalGradient
+ HQS
+ LinearizedADMM
+ ProximalGradient
+ ProximalPoint
+ TwIST
+ DouglasRachfordSplitting
+ PPXA
+ ConsensusADMM
-.. currentmodule:: pyproximal.optimization.pnp
-.. autosummary::
- :toctree: generated/
+Primal-dual
+^^^^^^^^^^^
- PlugAndPlay
+Class-based
+~~~~~~~~~~~
-.. currentmodule:: pyproximal.optimization.sr3
+.. currentmodule:: pyproximal.optimization.cls_primaldual
.. autosummary::
:toctree: generated/
- SR3
-
+ AdaptivePrimalDual
+ PrimalDual
-Primal-dual
-^^^^^^^^^^^
+Function-based
+~~~~~~~~~~~~~~
.. currentmodule:: pyproximal.optimization.primaldual
@@ -208,6 +224,7 @@ Primal-dual
AdaptivePrimalDual
PrimalDual
+
Other
^^^^^
@@ -218,6 +235,35 @@ Other
Bregman
+.. currentmodule:: pyproximal.optimization.palm
+
+.. autosummary::
+ :toctree: generated/
+
+ PALM
+ iPALM
+
+.. currentmodule:: pyproximal.optimization.pnp
+
+.. autosummary::
+ :toctree: generated/
+
+ PlugAndPlay
+
+.. currentmodule:: pyproximal.optimization.red
+
+.. autosummary::
+ :toctree: generated/
+
+ RED
+
+.. currentmodule:: pyproximal.optimization.sr3
+
+.. autosummary::
+ :toctree: generated/
+
+ SR3
+
.. currentmodule:: pyproximal.optimization.segmentation
.. autosummary::
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*
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
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/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/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..f2bb575a 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 = [
@@ -57,6 +60,7 @@ doc = [
"scikit-image",
"bm3d",
"bm4d",
+ "pywavelets",
"pooch",
"sphinx",
"sphinx-design",
@@ -85,6 +89,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/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/optimization/__init__.py b/pyproximal/optimization/__init__.py
index 694aac14..01d6c4cd 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,6 @@
"""
-from . import primal, primaldual, bregman, segmentation, sr3, palm, pnp
+from . import cls_primal, primal
+from . import cls_primaldual, primaldual
+from . import bregman, segmentation, sr3, palm, pnp, red
diff --git a/pyproximal/optimization/basesolver.py b/pyproximal/optimization/basesolver.py
new file mode 100644
index 00000000..008b6cdb
--- /dev/null
+++ b/pyproximal/optimization/basesolver.py
@@ -0,0 +1,163 @@
+from __future__ import annotations
+
+__all__ = ["Solver"]
+
+import time
+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
+
+if TYPE_CHECKING:
+ from pyproximal.ProxOperator import ProxOperator
+
+
+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.
+ 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")
+
+ def memory_usage(self) -> None:
+ """Compute memory usage of the solver
+
+ Currently not implemented in PyProximal's solvers
+ and therefore just overwritten here.
+ """
+ pass
+
+ @abstractmethod
+ def setup(
+ self,
+ proxf: ProxOperator | list[ProxOperator],
+ proxg: ProxOperator | None = None,
+ *args: Any,
+ show: bool = False,
+ **kwargs: Any,
+ ) -> NDArray | tuple[NDArray, NDArray]:
+ """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: Any,
+ show: bool = False,
+ **kwargs: Any,
+ ) -> 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
+
+ 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
+ self.cost: NDArray = np.array(self.cost)
+ if show:
+ self._print_finalize(nbar=nbar)
diff --git a/pyproximal/optimization/callback.py b/pyproximal/optimization/callback.py
new file mode 100644
index 00000000..5da4b1dc
--- /dev/null
+++ b/pyproximal/optimization/callback.py
@@ -0,0 +1,39 @@
+__all__ = [
+ "ModelUpdateCallback",
+]
+
+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 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.
+
+ 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
diff --git a/pyproximal/optimization/cls_primal.py b/pyproximal/optimization/cls_primal.py
new file mode 100644
index 00000000..9a21e83a
--- /dev/null
+++ b/pyproximal/optimization/cls_primal.py
@@ -0,0 +1,4685 @@
+__all__ = [
+ "ProximalPoint",
+ "ProximalGradient",
+ "AndersonProximalGradient",
+ "GeneralizedProximalGradient",
+ "HQS",
+ "ADMM",
+ "ADMML2",
+ "LinearizedADMM",
+ "TwIST",
+ "DouglasRachfordSplitting",
+ "PPXA",
+ "ConsensusADMM",
+]
+
+from collections.abc import Sequence
+from math import sqrt
+from typing import TYPE_CHECKING, Any, Optional
+
+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
+
+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]
+
+
+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)
+
+ """
+
+ pf: float
+
+ def _print_setup(self, xcomplex: bool = False) -> None:
+ self._print_solver(nbar=60)
+
+ strpar = f"Proximal operator: {type(self.prox).__name__}"
+ strpar1 = f"tau = {self.tau:6e}\ttol = {str(self.tol)}\tniter = {self.niter}"
+ 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 setup( # type: ignore[override]
+ self,
+ prox: "ProxOperator",
+ x0: NDArray,
+ tau: float,
+ niter: int | None = None,
+ tol: float | None = None,
+ 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 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.prox = prox
+ self.tau = tau
+ self.niter = niter
+ self.tol = tol
+
+ self.ncp = get_array_module(x0)
+
+ # create variables to track the objective function and iterations
+ 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
+
+ # 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 self.pfold != 0 and self.ncp.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.cost.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 solve( # type: ignore[override]
+ self,
+ prox: "ProxOperator",
+ x0: NDArray,
+ tau: float,
+ 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
+ ----------
+ 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
+ cost : :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(60, show)
+ return x, self.iiter, self.cost
+
+
+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: 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 = {tau_str}\t\tbacktrack = {self.backtracking}"
+ strpar2 = f"beta = {self.beta}\t\tepsg = {epsg_print}\t\tacceleration = {self.acceleration}"
+ strpar3 = f"niter = {self.niter}\t\tniterback = {self.niterback}\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, 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(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 = (
+ 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} {tau_str}"
+ )
+ print(msg)
+
+ def setup( # type: ignore[override]
+ 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,
+ acceleration: str | None = None,
+ niterback: int = 100,
+ 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 (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. 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
+ 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).
+ 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 (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 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.backtracking = backtracking
+ self.beta = beta
+ self.eta = eta
+ self.niterback = niterback
+ self.niter = niter
+ self.tol = tol
+
+ self.ncp = get_array_module(x0)
+
+ # check if epsg is a vector
+ self.epsg = self.ncp.asarray(epsg, dtype=np.float32)
+ if self.epsg.size == 1:
+ 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"
+
+ # set tau
+ if tau is None:
+ self.backtracking = True
+ 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"]:
+ self.acceleration = acceleration
+ else:
+ msg = "Acceleration should be None, vandenberghe or fista"
+ raise NotImplementedError(msg)
+
+ # initialize solver
+ x = x0.copy()
+ y = x.copy()
+
+ # for accelaration
+ 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(epsg_ * pg)
+ 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(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()
+
+ # 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()
+ epsg_prev = self.epsg.item()
+ else:
+ epsg = self.epsg[self.iiter].item()
+ epsg_prev = self.epsg[self.iiter - 1].item()
+
+ # proximal step
+ if not self.backtracking:
+ if self.eta == 1.0:
+ x = self.proxg.prox(y - tau * self.proxf.grad(y), epsg * tau)
+ else:
+ x = x + self.eta * (
+ self.proxg.prox(
+ x - tau * self.proxf.grad(x),
+ epsg * tau,
+ )
+ - x
+ )
+ else:
+ x, tau = _backtracking(
+ y,
+ 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 - tau * self.proxf.grad(x),
+ epsg * 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(epsg * pg)
+ 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
+
+ self.iiter += 1
+ if show:
+ 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
+
+ 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 solve( # type: ignore[override]
+ 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,
+ 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),
+ ) -> 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. 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
+ 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).
+ 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
+ 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:`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,
+ acceleration=acceleration,
+ niterback=niterback,
+ 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
+
+
+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)
+
+ 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 = {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)
+ 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, epsg_prev: int) -> None:
+ if self.tol is None:
+ self.pf, pg = self.proxf(x), self.proxg(x)
+ 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 = (
+ 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} {tau_str}"
+ )
+ print(msg)
+
+ def setup( # type: ignore[override]
+ self,
+ proxf: ProxOperator,
+ proxg: ProxOperator,
+ x0: NDArray,
+ epsg: float | NDArray = 1.0,
+ tau: float | NDArray = 1.0,
+ epsr: float = 1e-10,
+ safeguard: bool = False,
+ nhistory: int = 10,
+ 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 (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. 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
+ 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
+ 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 (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 iterations log
+
+ Returns
+ -------
+ x : :obj:`numpy.ndarray`
+ Initial guess
+
+ """
+ self.proxf = proxf
+ self.proxg = proxg
+ self.epsr = epsr
+ self.safeguard = safeguard
+ self.nhistory = nhistory
+ self.niter = niter
+ self.tol = tol
+
+ self.ncp = get_array_module(x0)
+
+ # check if epsg is a vector
+ self.epsg = self.ncp.asarray(epsg, dtype=np.float32)
+ if self.epsg.size == 1:
+ 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"
+
+ # set tau
+ 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, epsg_ * 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)
+ 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
+
+ # 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
+ Andersonproximal gradient algorithm
+ y : :obj:`numpy.ndarray`
+ Additional model vector to be updated by a step of the
+ Anderson 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
+
+ """
+ # 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()
+ epsg_prev = self.epsg.item()
+ else:
+ epsg = self.epsg[self.iiter].item()
+ epsg_prev = self.epsg[self.iiter - 1].item()
+
+ # update fix point
+ g = x - 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, epsg * tau)
+ else:
+ # update auxiliary variable
+ ytest = np.vstack(self.G).T @ alpha
+
+ # update main variable
+ 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 - tau * np.linalg.norm(self.proxf.grad(x)) ** 2 / 2:
+ y = ytest
+ x = xtest
+ else:
+ x = self.proxg.prox(g, epsg * 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(epsg * pg)
+ if (
+ self.pfgold != 0
+ and self.ncp.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, epsg_prev)
+ 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 Anderson proximal gradient algorithm
+ y : :obj:`numpy.ndarray`
+ Additional model vector to be updated by multiple steps of
+ the Anderson 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 solve( # type: ignore[override]
+ self,
+ proxf: ProxOperator,
+ proxg: ProxOperator,
+ x0: NDArray,
+ epsg: float | NDArray = 1.0,
+ tau: float | NDArray = 1.0,
+ 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),
+ ) -> 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. 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
+ 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
+ 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
+ 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:`numpy.ndarray`
+ History of the objective function
+
+ """
+ x, y = self.setup(
+ proxf=proxf,
+ proxg=proxg,
+ x0=x0,
+ epsg=epsg,
+ tau=tau,
+ epsr=epsr,
+ safeguard=safeguard,
+ nhistory=nhistory,
+ 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
+
+
+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 = {float(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 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,
+ acceleration: str | None = None,
+ niter: int | 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). If a scalar is provided
+ the same scaling factor is applied to every ``g`` function.
+ 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
+ this will be only used when ``acceleration=None``.
+ acceleration: :obj:`str`, optional
+ Acceleration (``None``, ``vandenberghe`` or ``fista``)
+ 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 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.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 epsg is a vector and set same value for all gs otherwise
+ self.epsg = self.ncp.asarray(epsg, dtype=np.float32)
+ 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
+ if acceleration in [None, "None", "vandenberghe", "fista"]:
+ self.acceleration = acceleration
+ else:
+ msg = "Acceleration should be None, vandenberghe or fista"
+ raise NotImplementedError(msg)
+
+ # initialize solver
+ 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
+ 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
+
+ # 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
+ generalized proximal gradient algorithm
+ y : :obj:`numpy.ndarray`
+ Additional model vector to be updated by a step of the
+ generalized 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].item() / self.weights[i].item()
+ )
+ self.zs[i] += self.eta * (ztmp - y)
+ x += self.weights[i].item() * 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 (
+ 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
+
+ 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 generalized proximal gradient algorithm
+ y : :obj:`numpy.ndarray`
+ Additional model vector to be updated by multiple steps of
+ the generalized 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 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,
+ acceleration: str | 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
+ ----------
+ 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). 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
+ Relaxation parameter (must be between 0 and 1, 0 excluded). Note that
+ this will be only used when ``acceleration=None``.
+ 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
+ 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:`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,
+ acceleration=acceleration,
+ 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
+
+
+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}"
+ 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 | NDArray,
+ z0: NDArray | None = None,
+ gfirst: bool = True,
+ niter: int | None = None,
+ 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` 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``)
+ 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 (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
+ 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.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=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"
+
+ # initialize solver
+ x, z = _x0z0_init(x0, z0)
+
+ # for accelaration
+ self.t = 1.0
+
+ # create variables to track the objective function and iterations
+ 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
+
+ # 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
+ HQS algorithm
+ z : :obj:`numpy.ndarray`
+ Additional model vector to be updated by a step of the
+ HQS algorithm
+ show : :obj:`bool`, optional
+ Display iteration log
+
+ Returns
+ -------
+ x : :obj:`numpy.ndarray`
+ Updated model vector
+ z : :obj:`numpy.ndarray`
+ Updated additional model vector
+
+ """
+ # define tau for current iteration
+ if self.tau.ndim == 0:
+ tau = self.tau.item()
+ else:
+ tau = self.tau[self.iiter].item()
+
+ # proximal steps
+ if self.gfirst:
+ z = self.proxg.prox(x, tau)
+ x = self.proxf.prox(z, tau)
+ else:
+ x = self.proxf.prox(z, tau)
+ z = self.proxg.prox(x, tau)
+
+ # 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 (
+ 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
+
+ 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 HQS algorithm
+ z : :obj:`numpy.ndarray`
+ Additional model vector to be updated by multiple steps of
+ the HQS 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,
+ proxf: ProxOperator,
+ proxg: ProxOperator,
+ x0: NDArray,
+ tau: float | NDArray,
+ z0: NDArray | None = None,
+ gfirst: bool = True,
+ niter: int | None = None,
+ tol: float | None = None,
+ callbackz: 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 (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``)
+ 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 (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
+ 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.
+
+ Returns
+ -------
+ x : :obj:`numpy.ndarray`
+ Estimated model
+ z : :obj:`numpy.ndarray`
+ Additional estimated model
+ iiter : :obj:`int`
+ Number of executed iterations
+ cost : :obj:`numpy.ndarray`
+ History of the objective function
+
+ """
+ x, z = self.setup(
+ proxf=proxf,
+ proxg=proxg,
+ x0=x0,
+ tau=tau,
+ z0=z0,
+ gfirst=gfirst,
+ niter=niter,
+ 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 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 = {float(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])
+ 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,
+ z0: NDArray | None = None,
+ gfirst: bool = False,
+ niter: int | None = None,
+ 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``)
+ 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 (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
+ 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
+ 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
+
+ # 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
+ ADDM algorithm
+ z : :obj:`numpy.ndarray`
+ Additional model vector to be updated by a step of the
+ ADDM 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.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 (
+ 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
+
+ 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 ADDM algorithm
+ z : :obj:`numpy.ndarray`
+ Additional model vector to be updated by multiple steps of
+ the ADDM 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,
+ proxf: ProxOperator,
+ proxg: ProxOperator,
+ x0: NDArray,
+ tau: float,
+ z0: NDArray | None = None,
+ gfirst: bool = False,
+ 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]:
+ 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`
+ 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``)
+ 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
+ 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:`numpy.ndarray`
+ History of the objective function
+
+ """
+ x, z = self.setup(
+ proxf=proxf,
+ proxg=proxg,
+ x0=x0,
+ tau=tau,
+ z0=z0,
+ gfirst=gfirst,
+ niter=niter,
+ 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 [1]_:
+
+ .. 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,
+ gfirst: bool = False,
+ niter: int | None = None,
+ 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``.
+ 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 (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
+ 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.gfirst = gfirst
+ self.niter = niter
+ 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(z)
+
+ # other parameters
+ self.sqrttau = 1.0 / sqrt(self.tau)
+
+ # create variables to track the objective function and iterations
+ 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
+
+ # 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 (
+ 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
+
+ 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,
+ gfirst: bool = False,
+ niter: int = 10,
+ 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``)
+ 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
+ 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:`numpy.ndarray`
+ History of the objective function
+
+ """
+ x, z = self.setup(
+ proxg=proxg,
+ Op=Op,
+ b=b,
+ A=A,
+ x0=x0,
+ tau=tau,
+ z0=z0,
+ gfirst=gfirst,
+ niter=niter,
+ 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 | 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
+ 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 (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 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(z)
+
+ # create variables to track the objective function and iterations
+ 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
+
+ # 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 (
+ 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
+
+ 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:`numpy.ndarray`
+ 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 | None = None,
+ 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 (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 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
+ 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
+
+ # 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(x)
+ self.pfg = pf + pg
+ 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
+
+ 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:`numpy.ndarray`
+ 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
+
+
+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}\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])
+ 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,
+ gfirst: bool = True,
+ niter: int | None = None,
+ 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).
+ 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 (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 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.gfirst = gfirst
+ self.niter = niter
+ 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
+ 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
+
+ # 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 (
+ 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
+
+ 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,
+ 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, 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).
+ 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 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:`numpy.ndarray`
+ History of the objective function
+
+ """
+ x, y = self.setup(
+ proxf=proxf,
+ proxg=proxg,
+ x0=x0,
+ tau=tau,
+ eta=eta,
+ gfirst=gfirst,
+ niter=niter,
+ 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)])
+ 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 | None = None,
+ tol: float | None = None,
+ 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 (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 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
+ 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
+
+ # 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
+
+ """
+ # 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)]
+ )
+ 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 self.pfold != 0 and self.ncp.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)
+ 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)
+ # 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 = 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`
+ 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 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(s)
+ iiter : :obj:`int`
+ Number of executed iterations
+ cost : :obj:`numpy.ndarray`
+ 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
+
+
+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 | None = None,
+ 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 (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 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
+ 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
+
+ # 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
+
+ """
+ # 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)]
+ )
+ 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 self.pfold != 0 and self.ncp.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 ConsensusADMM algorithm
+ x_bar : :obj:`numpy.ndarray`
+ Current averaged model vector to be updated by multiple steps of
+ the ConsensusADMM algorithm
+ y : :obj:`numpy.ndarray`
+ Additional model vector to be updated by multiple steps of
+ the ConsensusADMM 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:`numpy.ndarray`
+ 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
diff --git a/pyproximal/optimization/cls_primaldual.py b/pyproximal/optimization/cls_primaldual.py
new file mode 100644
index 00000000..3717cbfd
--- /dev/null
+++ b/pyproximal/optimization/cls_primaldual.py
@@ -0,0 +1,958 @@
+__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: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"
+ 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.item()
+ else:
+ tau = self.tau[self.iiter].item()
+
+ # define mu for current iteration
+ if self.mu.ndim == 0:
+ mu = self.mu.item()
+ else:
+ mu = self.mu[self.iiter].item()
+
+ 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 (
+ self.pfgold != 0
+ and self.ncp.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: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)}\txytol = {str(self.xytol)}\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"{pf:11.4e} "
+ + f"{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,
+ xytol: 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
+ 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
+ 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.xytol = xytol
+ 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
+ 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))
+ 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 checks: break iterations if overall
+ # objective or x/y updates do not decrease
+ # below respective tolerances
+ 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 (
+ 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:
+ self.tolbreak = True
+
+ self.iiter += 1
+ if show:
+ self._print_step(x)
+ if 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,
+ xytol: 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
+ 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
+ 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,
+ xytol=xytol,
+ 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
diff --git a/pyproximal/optimization/pnp.py b/pyproximal/optimization/pnp.py
index d40bf266..47c760f4 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``
@@ -52,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
----------
@@ -96,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/primal.py b/pyproximal/optimization/primal.py
index fad1fe74..36abf037 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)
@@ -244,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
@@ -268,12 +197,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,176 +222,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,
@@ -460,8 +266,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
@@ -489,14 +297,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,
@@ -505,8 +315,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
@@ -530,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
@@ -548,11 +362,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
-------
@@ -561,170 +385,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,
@@ -732,8 +426,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
@@ -762,8 +459,9 @@ 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)
- weights : :obj:`float`, 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:`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
@@ -772,11 +470,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
-------
@@ -785,134 +497,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
@@ -950,6 +579,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
@@ -957,6 +595,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
-------
@@ -972,98 +614,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
@@ -1114,6 +708,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
@@ -1121,6 +724,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
-------
@@ -1141,77 +748,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",
@@ -1220,8 +788,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
@@ -1259,11 +831,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
@@ -1287,108 +872,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
@@ -1425,11 +955,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
-------
@@ -1450,71 +993,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,
@@ -1522,8 +1032,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
@@ -1560,11 +1073,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
@@ -1572,110 +1098,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:
@@ -1683,16 +1137,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
@@ -1723,6 +1180,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
@@ -1731,6 +1197,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
-------
@@ -1741,86 +1211,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)
@@ -1853,13 +1284,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
-------
@@ -1872,115 +1313,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
@@ -2006,13 +1376,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
-------
@@ -2026,82 +1406,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
diff --git a/pyproximal/optimization/primaldual.py b/pyproximal/optimization/primaldual.py
index d98491bb..9faebea0 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,15 +32,18 @@ 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
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::
@@ -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 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
+ 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,15 +180,18 @@ def AdaptivePrimalDual(
delta: float = 1.5,
z: NDArray | None = None,
niter: int = 10,
- tol: float = 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),
) -> tuple[NDArray, tuple[NDArray, NDArray, NDArray]]:
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]_.
+ 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.
@@ -276,13 +225,28 @@ 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
+ 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
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 +257,36 @@ 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),
+ xytol=xytol,
+ show=show,
+ itershow=itershow,
+ )
return x, steps
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/optimization/segmentation.py b/pyproximal/optimization/segmentation.py
index 5063220a..33986519 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
@@ -82,11 +98,15 @@ 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.
"""
+ ncp = get_array_module(y)
kwargs_simplex = {} if kwargs_simplex is None else kwargs_simplex
dims = y.shape
@@ -94,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:
@@ -101,10 +131,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
@@ -115,11 +142,6 @@ def Segment(
[L21(ndim=ndims, sigma=0.5 * alpha)] * ncl, nn=[ndims * dimsprod] * ncl
)
- # Steps
- L = 8.0 / sampling**2
- tau = 1.0
- mu = 1.0 / (tau * L)
-
# Inversion
x: NDArray = PrimalDual(
simp,
@@ -129,14 +151,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
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:
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/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/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),
],
diff --git a/pyproximal/proximal/L21.py b/pyproximal/proximal/L21.py
index bf59462a..478b96a1 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:
@@ -65,13 +65,13 @@ 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)))
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 +79,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
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
diff --git a/pyproximal/proximal/RED.py b/pyproximal/proximal/RED.py
new file mode 100644
index 00000000..c4cc039c
--- /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 [1]_:
+ :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 ``denoiser``
+ method prior to applying the denoiser
+ sigma : :obj:`float`, optional
+ Multiplicative coefficient of RED term
+ 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
+ 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",
]
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"]
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)])
diff --git a/pytests/test_projection.py b/pytests/test_projection.py
index 2473d8a5..38382890 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,39 @@ 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):
+ """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)
+ 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):
+ """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)
+
+
+@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"""
diff --git a/pytests/test_solver.py b/pytests/test_solver.py
index 8480e76f..9292b2e9 100644
--- a/pytests/test_solver.py
+++ b/pytests/test_solver.py
@@ -1,25 +1,40 @@
-from typing import Any
-
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
from pyproximal.optimization.primal import (
ADMM,
ADMML2,
HQS,
PPXA,
+ AcceleratedProximalGradient,
+ AndersonProximalGradient,
ConsensusADMM,
DouglasRachfordSplitting,
GeneralizedProximalGradient,
LinearizedADMM,
ProximalGradient,
+ ProximalPoint,
+ TwIST,
)
-from pyproximal.proximal import L1, L2
+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
+par2 = {"n": 8, "m": 10, "dtype": "float64"} # underdetermined, float64
+par3 = {"n": 8, "m": 10, "dtype": "float32"} # underdetermined, float32
+
-par1 = {"n": 8, "m": 10, "dtype": "float32"} # float64
-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():
@@ -52,7 +67,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
@@ -106,7 +121,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"):
@@ -133,11 +148,134 @@ def test_GPG_weights(par):
],
x0=np.zeros(m),
tau=1.0,
- weights=[1.0, 1.0],
+ weights=np.array([1.0, 1.0]),
+ )
+
+
+@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_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
+ 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, show=True
+ )
+
+ assert_array_almost_equal(xpp, x, decimal=2)
+
+
+@pytest.mark.parametrize("par", [(par1), (par2), (par3)])
+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
+ )[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,
+ show=True,
)
+ 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
@@ -163,7 +301,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
@@ -180,12 +324,352 @@ 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)])
+@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
+ (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", show=True
+ )
+
+ # 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,
+ show=True,
+ )
+
+ assert_array_almost_equal(xpg, xtwist, decimal=2)
+
+
+@pytest.mark.parametrize("par", [(par1), (par2), (par3)])
+@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)
+ """
+ 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, 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, 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)])
+@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
+ """
+ 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, show=True)[0]
+
+ # ADMML2
+ l2reg = L2(sigma=eps)
+ xadmml2 = ADMML2(
+ 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)
+
+
+@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, 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, show=True
+ )[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
@@ -216,22 +700,23 @@ def test_ADMM_DRS(par):
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, 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, show=True, 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)
assert_array_almost_equal(xadmm, xdrs_f, decimal=2)
-@pytest.mark.parametrize("par", [(par1), (par2)])
-def test_PPXA_with_ADMM(par: dict[str, Any]) -> None:
+@pytest.mark.parametrize("par", [(par1), (par2), (par3)])
+@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
"""
@@ -268,14 +753,15 @@ def test_PPXA_with_ADMM(par: dict[str, Any]) -> None:
[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)])
-def test_PPXA_with_GPG(par: dict[str, Any]) -> None:
+@pytest.mark.parametrize("par", [(par1), (par2), (par3)])
+def test_PPXA_with_GPG(par) -> None:
"""Check equivalency of PPXA and GeneralizedProximalGradient"""
np.random.seed(0)
@@ -324,8 +810,8 @@ def test_PPXA_with_GPG(par: dict[str, Any]) -> None:
assert_array_almost_equal(xppxa, xgpg, decimal=2)
-@pytest.mark.parametrize("par", [(par1), (par2)])
-def test_ConsensusADMM_with_ADMM(par: dict[str, Any]) -> None:
+@pytest.mark.parametrize("par", [(par1), (par2), (par3)])
+def test_ConsensusADMM_with_ADMM(par) -> None:
"""Check equivalency of ConsensusADMM and ADMM
when two proximable functions
"""
@@ -368,8 +854,8 @@ def test_ConsensusADMM_with_ADMM(par: dict[str, Any]) -> None:
assert_array_almost_equal(xcadmm, xadmm, decimal=2)
-@pytest.mark.parametrize("par", [(par1), (par2)])
-def test_ConsensusADMM_with_ADMM_for_Lasso(par: dict[str, Any]) -> None:
+@pytest.mark.parametrize("par", [(par1), (par2), (par3)])
+def test_ConsensusADMM_with_ADMM_for_Lasso(par) -> None:
"""Check equivalency of ConsensusADMM and ADMM
when more than two proximable functions for lasso
"""
@@ -432,8 +918,8 @@ 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)])
-def test_ConsensusADMM_with_GPG(par: dict[str, Any]) -> None:
+@pytest.mark.parametrize("par", [(par1), (par2), (par3)])
+def test_ConsensusADMM_with_GPG(par) -> None:
"""Check equivalency of ConsensusADMM and GeneralizedProximalGradient"""
np.random.seed(0)
@@ -481,3 +967,124 @@ def test_ConsensusADMM_with_GPG(par: dict[str, Any]) -> 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)
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)
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/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)
diff --git a/tutorials/deblurring.py b/tutorials/deblurring.py
new file mode 100644
index 00000000..ac972813
--- /dev/null
+++ b/tutorials/deblurring.py
@@ -0,0 +1,170 @@
+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`).
+
+Lw = 1.0 # maxeig(W_i^H W_i)
+L = 2.0 # maxeig(K^H K)
+
+tau = 0.4
+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=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=muw, 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/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/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):
diff --git a/tutorials/plugandplay.py b/tutorials/plugandplay.py
index 02db7817..5d1df383 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, dtype=np.complex128),
+ 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,
@@ -99,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,
@@ -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, dtype=np.complex128),
+ 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),
+ x0=np.zeros(x.size, dtype=np.complex128),
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.
diff --git a/tutorials/red.py b/tutorials/red.py
new file mode 100644
index 00000000..1bef7073
--- /dev/null
+++ b/tutorials/red.py
@@ -0,0 +1,201 @@
+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
+
+import pyproximal
+
+plt.close("all")
+np.random.seed(0)
+set_ndarray_multiplication(False)
+
+###############################################################################
+# 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))
+Op = Rop * Fop
+
+###############################################################################
+# 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 callback(x, xtrue, errhist):
+ errhist.append(np.linalg.norm(x - xtrue))
+
+
+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=Op, b=y.ravel())
+red = pyproximal.proximal.RED(denoiser, x.shape, sigma=0.4, sigmad=sigmad, call=False)
+
+errhistgd = []
+xredgd = pyproximal.optimization.red.RED(
+ l2,
+ red,
+ x0=np.zeros(x.size, dtype=np.complex128),
+ 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
+
+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.red.RED(
+ l2,
+ red,
+ x0=np.zeros(x.size, dtype=np.complex128),
+ solver=pyproximal.optimization.primal.ADMM,
+ tau=tau,
+ 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
+denoiser = lambda x, sigma: bm3d.bm3d(
+ x.real.reshape(xshape), sigma_psd=sigma, stage_arg=bm3d.BM3DStages.HARD_THRESHOLDING
+).ravel()
+
+# FP-RED
+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),
+ solver="fixedpoint",
+ niter=50,
+ niter_inner=10,
+ callback=lambda xx: callback(xx, x.ravel(), errhistfp),
+ show=True,
+)
+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()
diff --git a/uv.lock b/uv.lock
index 15a7fedf..212e2219 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,210 @@ 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/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]]
name = "cycler"
version = "0.12.1"
@@ -603,13 +689,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 +709,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 +751,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 +817,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 +829,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 +938,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 +1015,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 +1040,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 +1103,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 +1127,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 +1200,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 +1212,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 +1254,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 +1263,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 +1273,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 +1376,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 +1389,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 +1413,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 +1436,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 +1560,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 +1701,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 +1874,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 +1895,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 +1906,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 +1932,12 @@ 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 = "pywavelets" },
+ { 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 +1945,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 = [
@@ -1951,6 +1975,7 @@ doc = [
{ name = "nbsphinx" },
{ name = "numpydoc" },
{ name = "pooch" },
+ { name = "pywavelets" },
{ name = "scikit-image" },
{ name = "shibuya" },
{ name = "sphinx" },
@@ -1965,13 +1990,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 +2013,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 +2078,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 +2132,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 +2178,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 +2192,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 +2229,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 +2357,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 +2423,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 +2500,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 +2535,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 +2574,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 +2616,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 +2629,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 +2696,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 +2713,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 +2737,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 +2800,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 = [