proxax 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. proxax/__init__.py +79 -0
  2. proxax/_loop.py +67 -0
  3. proxax/_typing.py +39 -0
  4. proxax/certificates/__init__.py +14 -0
  5. proxax/certificates/stationarity.py +168 -0
  6. proxax/constraints/__init__.py +62 -0
  7. proxax/constraints/linear_op.py +542 -0
  8. proxax/constraints/sets.py +158 -0
  9. proxax/optax_compat/__init__.py +33 -0
  10. proxax/optax_compat/transforms.py +2598 -0
  11. proxax/oracle/__init__.py +39 -0
  12. proxax/oracle/composite.py +390 -0
  13. proxax/oracle/spectral.py +640 -0
  14. proxax/oracle/terms.py +140 -0
  15. proxax/problem.py +102 -0
  16. proxax/prox/__init__.py +42 -0
  17. proxax/prox/base.py +117 -0
  18. proxax/prox/combinators.py +83 -0
  19. proxax/prox/indicators.py +201 -0
  20. proxax/prox/norms.py +191 -0
  21. proxax/py.typed +0 -0
  22. proxax/solve.py +175 -0
  23. proxax/solvers/__init__.py +101 -0
  24. proxax/solvers/base.py +110 -0
  25. proxax/solvers/constrained/__init__.py +33 -0
  26. proxax/solvers/constrained/iaipal.py +654 -0
  27. proxax/solvers/constrained/ialm.py +604 -0
  28. proxax/solvers/constrained/qp_aipp.py +420 -0
  29. proxax/solvers/constrained/qp_apd.py +432 -0
  30. proxax/solvers/constrained/s_prox_alm.py +491 -0
  31. proxax/solvers/inner/__init__.py +34 -0
  32. proxax/solvers/inner/acg.py +335 -0
  33. proxax/solvers/inner/pf_acg.py +359 -0
  34. proxax/solvers/unconstrained/__init__.py +63 -0
  35. proxax/solvers/unconstrained/ac_acg.py +332 -0
  36. proxax/solvers/unconstrained/adap_apg.py +519 -0
  37. proxax/solvers/unconstrained/adap_fista.py +443 -0
  38. proxax/solvers/unconstrained/ag.py +377 -0
  39. proxax/solvers/unconstrained/aipp.py +439 -0
  40. proxax/solvers/unconstrained/apd.py +501 -0
  41. proxax/solvers/unconstrained/da_icg.py +488 -0
  42. proxax/solvers/unconstrained/ecg.py +409 -0
  43. proxax/solvers/unconstrained/ia_icg.py +458 -0
  44. proxax/solvers/unconstrained/nc_fista.py +321 -0
  45. proxax/solvers/unconstrained/upfag.py +589 -0
  46. proxax/tree.py +189 -0
  47. proxax-0.0.1.dist-info/METADATA +95 -0
  48. proxax-0.0.1.dist-info/RECORD +50 -0
  49. proxax-0.0.1.dist-info/WHEEL +4 -0
  50. proxax-0.0.1.dist-info/licenses/LICENSE +21 -0
proxax/__init__.py ADDED
@@ -0,0 +1,79 @@
1
+ """Certified first-order proximal point methods for composite optimization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from proxax import certificates
6
+ from proxax import constraints
7
+ from proxax import optax_compat
8
+ from proxax import oracle
9
+ from proxax import problem as _problem
10
+ from proxax import prox
11
+ from proxax import solve as _solve
12
+ from proxax import solvers
13
+ from proxax import tree
14
+
15
+ __version__ = "0.0.1"
16
+
17
+ ACACG = solvers.ACACG
18
+ AG = solvers.AG
19
+ AIPP = solvers.AIPP
20
+ APD = solvers.APD
21
+ AdapAPG = solvers.AdapAPG
22
+ AdapFISTA = solvers.AdapFISTA
23
+ CompositeProblem = _problem.CompositeProblem
24
+ DAICG = solvers.DAICG
25
+ ECG = solvers.ECG
26
+ IAICG = solvers.IAICG
27
+ IAIPAL = solvers.IAIPAL
28
+ IALM = solvers.IALM
29
+ KKTResiduals = _problem.KKTResiduals
30
+ NCFISTA = solvers.NCFISTA
31
+ QPAIPP = solvers.QPAIPP
32
+ QPAPD = solvers.QPAPD
33
+ SProxALM = solvers.SProxALM
34
+ Solution = solvers.Solution
35
+ SolveStats = solvers.SolveStats
36
+ SolveStatus = solvers.SolveStatus
37
+ Solver = solvers.Solver
38
+ SpectralOracle = oracle.SpectralOracle
39
+ SpectralProblem = oracle.SpectralProblem
40
+ StoppingConfig = solvers.StoppingConfig
41
+ UPFAG = solvers.UPFAG
42
+ solve = _solve.solve
43
+
44
+ __all__ = [
45
+ "ACACG",
46
+ "AG",
47
+ "AIPP",
48
+ "APD",
49
+ "AdapAPG",
50
+ "AdapFISTA",
51
+ "CompositeProblem",
52
+ "DAICG",
53
+ "ECG",
54
+ "IAICG",
55
+ "IAIPAL",
56
+ "IALM",
57
+ "KKTResiduals",
58
+ "NCFISTA",
59
+ "QPAIPP",
60
+ "QPAPD",
61
+ "SProxALM",
62
+ "Solution",
63
+ "SolveStats",
64
+ "SolveStatus",
65
+ "Solver",
66
+ "SpectralOracle",
67
+ "SpectralProblem",
68
+ "StoppingConfig",
69
+ "UPFAG",
70
+ "__version__",
71
+ "certificates",
72
+ "constraints",
73
+ "optax_compat",
74
+ "oracle",
75
+ "prox",
76
+ "solve",
77
+ "solvers",
78
+ "tree",
79
+ ]
proxax/_loop.py ADDED
@@ -0,0 +1,67 @@
1
+ """Dual-mode loop execution supporting both JAX while_loop and eager Python."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ import typing
7
+ from collections import abc
8
+
9
+ import jax
10
+
11
+ from proxax import _typing
12
+
13
+ StateT = typing.TypeVar("StateT")
14
+
15
+
16
+ def while_loop(
17
+ cond_fun: abc.Callable[[StateT], jax.Array],
18
+ body_fun: abc.Callable[[StateT], StateT],
19
+ init_val: StateT,
20
+ *,
21
+ jit: bool = True,
22
+ callback: _typing.CallbackFn[StateT] | None = None,
23
+ time_limit: float | None = None,
24
+ on_time_limit: abc.Callable[[StateT], StateT] | None = None,
25
+ ) -> StateT:
26
+ """Executes a stateful loop via `jax.lax.while_loop` or eager Python.
27
+
28
+ Args:
29
+ cond_fun: Continuation predicate returning a boolean JAX scalar.
30
+ body_fun: Pure state transition function `state -> next_state`.
31
+ init_val: Initial loop carry PyTree.
32
+ jit: If True, executes `jax.lax.while_loop` (XLA-compiled and
33
+ `vmap`-compatible). If False, runs a Python `while` loop supporting
34
+ Python callbacks and wall-clock time limits.
35
+ callback: Optional Python callback run after each iteration when
36
+ `jit=False`. Returning True stops the loop early.
37
+ time_limit: Optional wall-clock limit in seconds (supported when
38
+ `jit=False`).
39
+ on_time_limit: Optional state updater applied when `time_limit` triggers.
40
+
41
+ Returns:
42
+ Final loop state `StateT`.
43
+
44
+ Raises:
45
+ ValueError: If `callback` or `time_limit` is supplied when `jit=True`.
46
+ """
47
+ if jit:
48
+ if callback is not None:
49
+ raise ValueError("Python `callback` requires `jit=False`.")
50
+ if time_limit is not None:
51
+ raise ValueError("Wall-clock `time_limit` requires `jit=False`.")
52
+ return jax.lax.while_loop(cond_fun, body_fun, init_val)
53
+
54
+ state = init_val
55
+ start_time = time.perf_counter()
56
+ while bool(cond_fun(state)):
57
+ if (
58
+ time_limit is not None
59
+ and (time.perf_counter() - start_time) >= time_limit
60
+ ):
61
+ if on_time_limit is not None:
62
+ state = on_time_limit(state)
63
+ break
64
+ state = body_fun(state)
65
+ if callback is not None and callback(state):
66
+ break
67
+ return state
proxax/_typing.py ADDED
@@ -0,0 +1,39 @@
1
+ """Core type aliases and structural protocols for proxax."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typing
6
+
7
+ import jax
8
+
9
+ Array: typing.TypeAlias = jax.Array
10
+ Scalar: typing.TypeAlias = jax.Array | float
11
+ PyTree: typing.TypeAlias = typing.Any
12
+ StateT = typing.TypeVar("StateT")
13
+ StateContraT = typing.TypeVar("StateContraT", contravariant=True)
14
+
15
+
16
+ @typing.runtime_checkable
17
+ class SmoothFn(typing.Protocol):
18
+ """Callable evaluating a scalar smooth objective on a PyTree."""
19
+
20
+ def __call__(self, x: PyTree, /) -> jax.Array:
21
+ """Evaluates the smooth function value at `x`."""
22
+ ...
23
+
24
+
25
+ @typing.runtime_checkable
26
+ class ValueAndGradFn(typing.Protocol):
27
+ """Callable returning both scalar value and PyTree gradient at `x`."""
28
+
29
+ def __call__(self, x: PyTree, /) -> tuple[jax.Array, PyTree]:
30
+ """Evaluates `(f(x), grad_f(x))`."""
31
+ ...
32
+
33
+
34
+ class CallbackFn(typing.Protocol[StateContraT]):
35
+ """Optional eager-mode callback invoked after each outer iteration."""
36
+
37
+ def __call__(self, state: StateContraT, /) -> bool | None:
38
+ """Inspects solver state; returning True requests early termination."""
39
+ ...
@@ -0,0 +1,14 @@
1
+ """Stationarity and KKT certificate refinement routines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from proxax.certificates import stationarity
6
+
7
+ RefinementResult = stationarity.RefinementResult
8
+ refine = stationarity.refine
9
+
10
+ __all__ = [
11
+ "RefinementResult",
12
+ "refine",
13
+ "stationarity",
14
+ ]
@@ -0,0 +1,168 @@
1
+ """Stationarity refinement mapping approximate iterates to certified pairs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+
7
+ import jax
8
+ import jax.numpy as jnp
9
+
10
+ from proxax import _loop
11
+ from proxax import _typing
12
+ from proxax import tree
13
+ from proxax.oracle import composite
14
+
15
+
16
+ @jax.tree_util.register_dataclass
17
+ @dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
18
+ class RefinementResult:
19
+ """Certified approximate stationary pair `(x_hat, v_hat)`.
20
+
21
+ Guarantees exact subdifferential membership
22
+ `v_hat in grad_f(x_hat) + dh(x_hat)` with monotone composite descent
23
+ `phi(x_hat) <= phi(y)`.
24
+ """
25
+
26
+ x_hat: _typing.PyTree
27
+ v_hat: _typing.PyTree
28
+ obj_val: jax.Array
29
+ lipschitz_used: jax.Array
30
+ smooth_evals: jax.Array
31
+ grad_evals: jax.Array
32
+ prox_evals: jax.Array
33
+
34
+
35
+ @jax.tree_util.register_dataclass
36
+ @dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
37
+ class _RefineCarry:
38
+ """Internal backtracking loop state for `refine`."""
39
+
40
+ lipschitz: jax.Array
41
+ x_trial: _typing.PyTree
42
+ f_trial: jax.Array
43
+ accepted: jax.Array
44
+ backtracks: jax.Array
45
+
46
+
47
+ def refine(
48
+ oracle: composite.CompositeOracleProtocol,
49
+ y: _typing.PyTree,
50
+ lipschitz_init: _typing.Scalar,
51
+ *,
52
+ backtrack_factor: float = 2.0,
53
+ max_backtracks: int = 40,
54
+ jit: bool = True,
55
+ ) -> RefinementResult:
56
+ """Computes a certified pair `(x_hat, v_hat)` from an approximate iterate `y`.
57
+
58
+ Performs one adaptive composite gradient step from `y` with smoothness `L`:
59
+
60
+ - `x_hat = prox_{(1 / L) h}(y - (1 / L) grad_f(y))`
61
+ - `v_hat = grad_f(x_hat) - grad_f(y) + L * (y - x_hat)`
62
+
63
+ Because `L * (y - x_hat) - grad_f(y) in dh(x_hat)` holds exactly by the
64
+ optimality condition of `prox`, `v_hat in grad_f(x_hat) + dh(x_hat)` is
65
+ guaranteed without subgradient selection heuristics.
66
+
67
+ Args:
68
+ oracle: Target `CompositeOracleProtocol`.
69
+ y: Approximate domain point `y in dom(h)`.
70
+ lipschitz_init: Initial trial smoothness constant `L_0 > 0`.
71
+ backtrack_factor: Multiplicative step-up factor `> 1` during line search.
72
+ max_backtracks: Maximum number of backtracking doublings.
73
+ jit: Whether to run line search via `jax.lax.while_loop` (`True`) or eager
74
+ Python (`False`).
75
+
76
+ Returns:
77
+ `RefinementResult` containing `(x_hat, v_hat)`, objective value, and counts.
78
+ """
79
+ dtype = tree.infer_dtype(y)
80
+ f_y, grad_y = oracle.smooth_value_and_grad(y)
81
+ num_slack = 10.0 * tree.eps(dtype) * (1.0 + jnp.abs(f_y))
82
+
83
+ if oracle.curvature_upper is not None:
84
+ L_fixed = jnp.maximum(
85
+ jnp.asarray(oracle.curvature_upper, dtype=dtype),
86
+ jnp.asarray(1e-12, dtype=dtype),
87
+ )
88
+ inv_L = 1.0 / L_fixed
89
+ x_hat = oracle.prox(tree.add_scaled(y, -inv_L, grad_y), inv_L)
90
+ f_x_hat, grad_x_hat = oracle.smooth_value_and_grad(x_hat)
91
+ v_hat = tree.add(
92
+ tree.sub(grad_x_hat, grad_y),
93
+ tree.scale(L_fixed, tree.sub(y, x_hat)),
94
+ )
95
+ obj_val = f_x_hat + oracle.proximal_value(x_hat)
96
+ return RefinementResult(
97
+ x_hat=x_hat,
98
+ v_hat=v_hat,
99
+ obj_val=obj_val,
100
+ lipschitz_used=L_fixed,
101
+ smooth_evals=jnp.asarray(2, dtype=jnp.int32),
102
+ grad_evals=jnp.asarray(2, dtype=jnp.int32),
103
+ prox_evals=jnp.asarray(1, dtype=jnp.int32),
104
+ )
105
+
106
+ L0 = jnp.maximum(
107
+ jnp.asarray(lipschitz_init, dtype=dtype),
108
+ jnp.asarray(1e-12, dtype=dtype),
109
+ )
110
+
111
+ def _eval_trial(
112
+ L_val: jax.Array,
113
+ ) -> tuple[_typing.PyTree, jax.Array, jax.Array]:
114
+ step = 1.0 / L_val
115
+ x_t = oracle.prox(tree.add_scaled(y, -step, grad_y), step)
116
+ f_t = oracle.smooth_value(x_t)
117
+ diff = tree.sub(x_t, y)
118
+ quad_upper = (
119
+ f_y + tree.vdot(grad_y, diff) + 0.5 * L_val * tree.norm_sq(diff)
120
+ )
121
+ ok = f_t <= quad_upper + num_slack
122
+ return x_t, f_t, ok
123
+
124
+ x0_trial, f0_trial, ok0 = _eval_trial(L0)
125
+ init_carry = _RefineCarry(
126
+ lipschitz=L0,
127
+ x_trial=x0_trial,
128
+ f_trial=f0_trial,
129
+ accepted=ok0,
130
+ backtracks=jnp.asarray(0, dtype=jnp.int32),
131
+ )
132
+
133
+ def _cond(carry: _RefineCarry) -> jax.Array:
134
+ return jnp.logical_and(
135
+ jnp.logical_not(carry.accepted),
136
+ carry.backtracks < max_backtracks,
137
+ )
138
+
139
+ def _body(carry: _RefineCarry) -> _RefineCarry:
140
+ next_L = carry.lipschitz * backtrack_factor
141
+ x_t, f_t, ok = _eval_trial(next_L)
142
+ return _RefineCarry(
143
+ lipschitz=next_L,
144
+ x_trial=x_t,
145
+ f_trial=f_t,
146
+ accepted=ok,
147
+ backtracks=carry.backtracks + 1,
148
+ )
149
+
150
+ final_carry = _loop.while_loop(_cond, _body, init_carry, jit=jit)
151
+ x_hat = final_carry.x_trial
152
+ L_used = final_carry.lipschitz
153
+ f_x_hat, grad_x_hat = oracle.smooth_value_and_grad(x_hat)
154
+ v_hat = tree.add(
155
+ tree.sub(grad_x_hat, grad_y),
156
+ tree.scale(L_used, tree.sub(y, x_hat)),
157
+ )
158
+ obj_val = f_x_hat + oracle.proximal_value(x_hat)
159
+ trials = 1 + final_carry.backtracks
160
+ return RefinementResult(
161
+ x_hat=x_hat,
162
+ v_hat=v_hat,
163
+ obj_val=obj_val,
164
+ lipschitz_used=L_used,
165
+ smooth_evals=trials + 2,
166
+ grad_evals=jnp.asarray(2, dtype=jnp.int32),
167
+ prox_evals=trials,
168
+ )
@@ -0,0 +1,62 @@
1
+ """Linear operators, convex sets, and set-inclusion constraints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from proxax.constraints import linear_op
6
+ from proxax.constraints import sets
7
+
8
+ BoundCallableLinearOperator = linear_op.BoundCallableLinearOperator
9
+ BoundConstraint = linear_op.BoundConstraint
10
+ BoundLinearOperator = linear_op.BoundLinearOperator
11
+ BoundProductConstraint = linear_op.BoundProductConstraint
12
+ BoundSetConstraint = linear_op.BoundSetConstraint
13
+ BoxSet = sets.BoxSet
14
+ CallableLinearOperator = linear_op.CallableLinearOperator
15
+ Constraint = linear_op.Constraint
16
+ ConvexSet = sets.ConvexSet
17
+ LinearOperator = linear_op.LinearOperator
18
+ LowerBound = sets.LowerBound
19
+ MatrixLinearOperator = linear_op.MatrixLinearOperator
20
+ ProductConstraint = linear_op.ProductConstraint
21
+ ProductSet = sets.ProductSet
22
+ SecondOrderCone = sets.SecondOrderCone
23
+ SetConstraint = linear_op.SetConstraint
24
+ Singleton = sets.Singleton
25
+ UpperBound = sets.UpperBound
26
+ ZeroSet = sets.ZeroSet
27
+ as_linear_operator = linear_op.as_linear_operator
28
+ box = linear_op.box
29
+ equality = linear_op.equality
30
+ greater_equal = linear_op.greater_equal
31
+ in_set = linear_op.in_set
32
+ less_equal = linear_op.less_equal
33
+ normalize_constraints = linear_op.normalize_constraints
34
+
35
+ __all__ = [
36
+ "BoundCallableLinearOperator",
37
+ "BoundConstraint",
38
+ "BoundLinearOperator",
39
+ "BoundProductConstraint",
40
+ "BoundSetConstraint",
41
+ "BoxSet",
42
+ "CallableLinearOperator",
43
+ "Constraint",
44
+ "ConvexSet",
45
+ "LinearOperator",
46
+ "LowerBound",
47
+ "MatrixLinearOperator",
48
+ "ProductConstraint",
49
+ "ProductSet",
50
+ "SecondOrderCone",
51
+ "SetConstraint",
52
+ "Singleton",
53
+ "UpperBound",
54
+ "ZeroSet",
55
+ "as_linear_operator",
56
+ "box",
57
+ "equality",
58
+ "greater_equal",
59
+ "in_set",
60
+ "less_equal",
61
+ "normalize_constraints",
62
+ ]