spacecore-optimize 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.
@@ -0,0 +1,193 @@
1
+ """
2
+ Closed-form proximal steps in the space metric.
3
+
4
+ For diagonal weights, divide each scalar proximal objective by ``w_i`` to obtain
5
+ the Euclidean problem with step ``t / w_i``; other metrics raise.
6
+ Beck, *First-Order Methods in Optimization*, Thm 6.6 and Lemma 6.5.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from spacecore import WeightedInnerProduct
13
+ from spacecore.functional._prox import _soft_threshold, validate_step
14
+
15
+
16
+ def _diagonal_weights_or_raise(X: Any, fname: str) -> Any:
17
+ """Return diagonal weights (``None`` for Euclidean) or raise on a non-diagonal metric."""
18
+ # Euclidean gives None, diagonal gives its weights, anything else cannot separate.
19
+ geometry = getattr(X, "geometry", None)
20
+ if geometry is None:
21
+ raise TypeError(
22
+ f"{fname} requires an inner-product space with a geometry; "
23
+ f"got {type(X).__name__}."
24
+ )
25
+ if geometry.is_euclidean:
26
+ return None
27
+ if isinstance(geometry, WeightedInnerProduct):
28
+ return X.ops.real(geometry.weights)
29
+ raise ValueError(
30
+ f"{fname} is a separable closed form valid only for Euclidean or diagonal "
31
+ f"(weighted) metrics; the metric {type(geometry).__name__} is not diagonal, "
32
+ f"so the subproblem does not separate. Refusing to return a wrong "
33
+ f"(separable) answer."
34
+ )
35
+
36
+
37
+ def _require_member_shape(X: Any, arr: Any, name: str, fname: str) -> None:
38
+ """Raise when ``arr`` does not have the coordinate shape of ``X``."""
39
+ shape = tuple(getattr(arr, "shape", ()))
40
+ if shape != tuple(X.shape):
41
+ raise ValueError(
42
+ f"{fname} {name} must have the space shape {tuple(X.shape)}, got {shape}."
43
+ )
44
+
45
+
46
+ def generalized_shrinkage(
47
+ X: Any,
48
+ *,
49
+ c: Any,
50
+ x0: Any,
51
+ eps: float,
52
+ lam: float = 0.0,
53
+ nonneg: bool = False,
54
+ ) -> Any:
55
+ r"""
56
+ Solve ``min_x Re<c, x>_X + eps ||x - x0||_X^2 + lam ||x||_1`` coordinatewise, optionally with ``x >= 0``.
57
+
58
+ The smooth minimizer is ``v = x0 - c / (2 eps)``; the l1 term soft-thresholds it
59
+ with width ``lam / (2 eps w_i)`` for diagonal metric weights ``w``: complete
60
+ the square, then divide by ``2 eps w_i``. Beck, *First-Order Methods in
61
+ Optimization*, Sec. 6.2.2 and Lemma 6.5; for complex coordinates, minimizing
62
+ over phase aligns ``x_i`` with ``v_i`` and leaves the same scalar radius problem.
63
+
64
+ Parameters
65
+ ----------
66
+ X : Space
67
+ Euclidean or diagonal-metric space; other metrics raise.
68
+ c : array-like
69
+ Metric gradient in ``X``.
70
+ x0 : array-like
71
+ Proximal center in ``X``.
72
+ eps : float
73
+ Positive quadratic weight.
74
+ lam : float, optional
75
+ Non-negative l1 weight; ``0`` disables thresholding.
76
+ nonneg : bool, optional
77
+ Constrain to ``x >= 0`` (real spaces only).
78
+
79
+ Returns
80
+ -------
81
+ Element
82
+ The minimizer.
83
+ """
84
+ # Validate eps, lam, the metric, the shapes, and nonneg on real spaces only.
85
+ validate_step(eps, X, "generalized_shrinkage eps")
86
+ validate_step(lam, X, "generalized_shrinkage lam", positive=False)
87
+
88
+ weights = _diagonal_weights_or_raise(X, "generalized_shrinkage")
89
+
90
+ ops = X.ops
91
+ c = X.ctx.asarray(c)
92
+ x0 = X.ctx.asarray(x0)
93
+ _require_member_shape(X, c, "c", "generalized_shrinkage")
94
+ _require_member_shape(X, x0, "x0", "generalized_shrinkage")
95
+ if nonneg and ops.is_complex_dtype(ops.get_dtype(x0)):
96
+ raise ValueError("generalized_shrinkage(nonneg=True) is defined for real spaces only.")
97
+
98
+ # Data step: minimizer of <c, x>_X + eps ||x - x0||^2_X. The metric weight
99
+ # cancels here precisely because c is a metric gradient.
100
+ v = x0 - c / (2.0 * eps)
101
+
102
+ # Threshold width lam / (2 eps w_i), with w_i = 1 on Euclidean spaces.
103
+ if weights is None:
104
+ tau = lam / (2.0 * eps)
105
+ else:
106
+ tau = lam / (2.0 * eps * weights)
107
+
108
+ # On x >= 0 the l1 term is linear, so the step is a one-sided shift.
109
+ if nonneg:
110
+ return ops.maximum(v - tau, ops.zeros_like(v))
111
+ # Soft-threshold; tau == 0 collapses to the identity v.
112
+ return _soft_threshold(ops, v, tau)
113
+
114
+
115
+ def prox_l1(v: Any, t: Any, X: Any, *, nonneg: bool = False) -> Any:
116
+ r"""
117
+ Prox of ``t ||.||_1`` in the space metric: soft-threshold with width ``t / w_i``.
118
+
119
+ Parameters
120
+ ----------
121
+ v : array-like
122
+ Point in ``X``.
123
+ t : float
124
+ Non-negative threshold.
125
+ X : Space
126
+ Euclidean or diagonal-metric space.
127
+ nonneg : bool, optional
128
+ Constrain to ``x >= 0``, giving ``max(v_i - t / w_i, 0)``; at ``t = 0`` this is
129
+ :func:`project_nonneg` (Beck, *First-Order Methods in Optimization*, Lemma 6.5).
130
+
131
+ Returns
132
+ -------
133
+ Element
134
+ The thresholded point; divide each scalar objective by ``w_i`` to apply
135
+ Beck, *First-Order Methods in Optimization*, Example 6.8.
136
+ """
137
+ # prox of t ||.||_1 = shrinkage with c = 0, x0 = v, eps = 1/2.
138
+ validate_step(t, X, "prox_l1", positive=False)
139
+ v = X.ctx.asarray(v)
140
+ _require_member_shape(X, v, "v", "prox_l1")
141
+ return generalized_shrinkage(X, c=X.zeros(), x0=v, eps=0.5, lam=t, nonneg=nonneg)
142
+
143
+
144
+ def prox_l2sq(v: Any, t: Any, X: Any, *, nonneg: bool = False) -> Any:
145
+ r"""
146
+ Prox of ``(t / 2) ||.||_X^2``: the shrinkage ``v / (1 + t)``.
147
+
148
+ Parameters
149
+ ----------
150
+ v : array-like
151
+ Point in ``X``.
152
+ t : float
153
+ Non-negative step size.
154
+ X : Space
155
+ Euclidean or diagonal-metric space.
156
+ nonneg : bool, optional
157
+ Constrain to ``x >= 0``, giving ``max(v, 0) / (1 + t)``.
158
+
159
+ Returns
160
+ -------
161
+ Element
162
+ The shrunk point; cancel ``w_i`` in the scalar quadratic optimality equation.
163
+ Beck, *First-Order Methods in Optimization*, Sec. 6.2.3.
164
+ """
165
+ # prox of (t/2) ||.||^2 = shrinkage with c = -v, x0 = 0, eps = (1 + t) / 2.
166
+ validate_step(t, X, "prox_l2sq", positive=False)
167
+ v = X.ctx.asarray(v)
168
+ _require_member_shape(X, v, "v", "prox_l2sq")
169
+ return generalized_shrinkage(
170
+ X, c=-v, x0=X.zeros(), eps=0.5 * (1.0 + t), lam=0.0, nonneg=nonneg
171
+ )
172
+
173
+
174
+ def project_nonneg(v: Any, X: Any) -> Any:
175
+ r"""
176
+ Project onto ``x >= 0`` in the space metric: coordinatewise ``max(v, 0)``.
177
+
178
+ Parameters
179
+ ----------
180
+ v : array-like
181
+ Point in ``X``.
182
+ X : Space
183
+ Euclidean or diagonal-metric space.
184
+
185
+ Returns
186
+ -------
187
+ Element
188
+ ``max(v, 0)``; positive diagonal weights do not change each scalar minimizer.
189
+ Beck, *First-Order Methods in Optimization*, Lemma 6.5 (``mu = 0``).
190
+ """
191
+ v = X.ctx.asarray(v)
192
+ _require_member_shape(X, v, "v", "project_nonneg")
193
+ return generalized_shrinkage(X, c=X.zeros(), x0=v, eps=0.5, lam=0.0, nonneg=True)
@@ -0,0 +1,166 @@
1
+ r"""
2
+ Real-coordinate view of a complex-domain functional.
3
+
4
+ For real-valued ``F`` with metric gradient ``g`` and ``v = a + i b``, the real
5
+ gradient is ``(Re g_v, Im g_v)`` where ``g_v = flatten(X.riesz(g))``. The view
6
+ writes that bookkeeping once: ``DF[h] = Re(g_v^H flatten(h))`` gives the stacked
7
+ partials. Kreutz-Delgado, *The Complex Gradient Operator and the CR-Calculus*,
8
+ arXiv:0906.4835, Eqs. (51), (55)-(56); SpaceCore's ``g`` is twice that paper's
9
+ complex gradient. Stacking is an isometry to this Euclidean view when ``X`` is
10
+ Euclidean; a non-Euclidean ``X`` requires the displayed Riesz conversion.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from typing import Any, Tuple
15
+
16
+ from spacecore import (
17
+ Context,
18
+ CoordinateSpace,
19
+ DenseCoordinateSpace,
20
+ Functional,
21
+ checked_method,
22
+ )
23
+
24
+
25
+ class RealifiedFunctional(Functional):
26
+ r"""
27
+ View of a complex-domain functional over stacked real coordinates ``(Re v, Im v)``.
28
+
29
+ The real gradient is :math:`(\operatorname{Re} g_v, \operatorname{Im} g_v)` with
30
+ :math:`g_v = \operatorname{flatten}(X.\operatorname{riesz}(g))`.
31
+
32
+ Parameters
33
+ ----------
34
+ base : Functional
35
+ Functional on a complex :class:`~spacecore.space.CoordinateSpace`.
36
+
37
+ Raises
38
+ ------
39
+ TypeError
40
+ If the base domain is not a coordinate space.
41
+ ValueError
42
+ If the base domain is already real.
43
+
44
+ Notes
45
+ -----
46
+ :class:`~spacecore.HermitianSpace` stores complex entries but is a real space of
47
+ dimension :math:`n^2` (``n`` real diagonal entries and two real coordinates per
48
+ off-diagonal pair). Its stacked representation still has Hermitian constraints;
49
+ this view does not supply an unconstrained extension of the functional.
50
+
51
+ Examples
52
+ --------
53
+ >>> import numpy as np
54
+ >>> import spacecore as sc
55
+ >>> import spacecore_optimize as spo
56
+ >>> ctx = sc.Context(sc.NumpyOps(), dtype=np.complex128)
57
+ >>> X = sc.DenseCoordinateSpace((2,), ctx=ctx)
58
+ >>> F = sc.SquaredL2NormFunctional(X)
59
+ >>> R = spo.realify(F)
60
+ >>> R.domain.shape
61
+ (4,)
62
+ >>> float(R.value(R.to_real(ctx.asarray([3.0 + 4.0j, 0.0]))))
63
+ 12.5
64
+ """
65
+
66
+ _fields = ("base",)
67
+ _dynamic = ("base",)
68
+
69
+ def __init__(self, base: Functional) -> None:
70
+ # Only complex coordinate spaces can be split into real and imaginary parts.
71
+ X = base.domain
72
+ if not isinstance(X, CoordinateSpace):
73
+ raise TypeError(
74
+ "RealifiedFunctional requires a CoordinateSpace domain; "
75
+ f"got {type(X).__name__}."
76
+ )
77
+ if X.field == "real":
78
+ raise ValueError(
79
+ "The base functional already has a real domain; use it directly."
80
+ )
81
+ ops = X.ops
82
+ # check_level is a property of the bound object, not of the Context, so
83
+ # it is threaded through the constructors rather than the context.
84
+ # The view's domain: real coordinates of twice the size.
85
+ real_ctx = Context(ops, dtype=ops.real_dtype(X.dtype))
86
+ dom = DenseCoordinateSpace(
87
+ (2 * X.size,), real_ctx, check_level=X.check_level
88
+ )
89
+ super().__init__(dom, real_ctx, check_level=X.check_level)
90
+ self.base = base
91
+ self.complex_space = X
92
+
93
+ def to_real(self, y: Any) -> Any:
94
+ """Flatten a base-domain element into stacked real coordinates."""
95
+ ops = self.ops
96
+ v = self.complex_space.flatten(y)
97
+ return ops.concatenate([ops.real(v), ops.imag(v)])
98
+
99
+ def from_real(self, w: Any) -> Any:
100
+ """Rebuild a base-domain element from stacked real coordinates."""
101
+ m = self.complex_space.size
102
+ v = w[:m] + 1j * w[m:]
103
+ return self.complex_space.unflatten(self.complex_space.ctx.asarray(v))
104
+
105
+ # Output-only: the input is validated by ``base.value`` against the *complex*
106
+ # domain after ``from_real``, so an ``in_space`` here would check ``w``
107
+ # against the wrong space.
108
+ @checked_method(out_space="codomain")
109
+ def value(self, w: Any, *args: Any, **kwargs: Any) -> Any:
110
+ """Return ``base.value`` at the complex element ``w`` encodes."""
111
+ return self.base.value(self.from_real(w), *args, **kwargs)
112
+
113
+ def grad(self, w: Any, *args: Any, **kwargs: Any) -> Any:
114
+ """Return the stacked real gradient ``(Re g_v, Im g_v)``."""
115
+ return self.value_and_grad(w, *args, **kwargs)[1]
116
+
117
+ def value_and_grad(self, w: Any, *args: Any, **kwargs: Any) -> Tuple[Any, Any]:
118
+ """Return ``(value, stacked real gradient)`` from one fused base evaluation.
119
+
120
+ ``riesz`` maps the base's metric gradient back to coordinates before the
121
+ real/imaginary split; skipping it would silently return the wrong vector
122
+ on any non-Euclidean geometry.
123
+ """
124
+ X, ops = self.complex_space, self.ops
125
+ # Evaluate the base at v = a + i b, then stack Re and Im of its coordinate gradient.
126
+ val, g = self.base.value_and_grad(self.from_real(w), *args, **kwargs)
127
+ gv = X.flatten(X.riesz(g))
128
+ return val, ops.concatenate([ops.real(gv), ops.imag(gv)])
129
+
130
+ def _convert(self, new_ctx: Context) -> "RealifiedFunctional":
131
+ """Convert the wrapped functional to ``new_ctx`` and re-derive the view.
132
+
133
+ ``new_ctx`` describes the *real view*, so the base must be converted to
134
+ the matching **complex** dtype — passing the real context straight
135
+ through would make the base's domain real and the view impossible to
136
+ rebuild. The pairing comes from ``ops.complex_dtype`` because it is not
137
+ portable to derive: NumPy promotes ``float32`` against a Python complex
138
+ to ``complex128``, JAX and Torch to ``complex64``.
139
+ """
140
+ ops = new_ctx.ops
141
+ base_ctx = Context(ops, dtype=ops.complex_dtype(new_ctx.dtype))
142
+ return RealifiedFunctional(self.base.convert(base_ctx))
143
+
144
+ def realify(F: Functional) -> Functional:
145
+ """
146
+ Return ``F`` unchanged on a real domain, realified on a complex one.
147
+
148
+ The idempotent entry point: calling it on an already-real functional is a
149
+ no-op rather than an error, so a caller preparing an objective for a
150
+ real-only optimizer need not branch on the field.
151
+
152
+ Parameters
153
+ ----------
154
+ F : Functional
155
+ Functional on a real or complex coordinate space.
156
+
157
+ Returns
158
+ -------
159
+ Functional
160
+ ``F`` itself when its domain is real, else a :class:`RealifiedFunctional`
161
+ over stacked real coordinates.
162
+ """
163
+ if F.domain.field == "real":
164
+ return F
165
+ return RealifiedFunctional(F)
166
+
@@ -0,0 +1,226 @@
1
+ """
2
+ Spectral lift ``F(X) = f(lambda(X))`` of a symmetric coordinate functional.
3
+
4
+ For real-valued symmetric ``f`` differentiable at the spectrum of a Hermitian ``X``,
5
+ the Frobenius gradient is ``from_spectrum(grad f(lambda), frame)``.
6
+ Lewis, "Derivatives of Spectral Functions", 1996, Thm 1.1 (convex subgradients: Thm 1.3).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, cast
11
+
12
+ from spacecore import (
13
+ CheckLevel,
14
+ Context,
15
+ DenseCoordinateSpace,
16
+ Functional,
17
+ JordanAlgebraSpace,
18
+ LpNormFunctional,
19
+ checked_method,
20
+ )
21
+ from spacecore.functional._base import Domain
22
+ from spacecore.functional.tools._coordinate import _CoordinateFunctional
23
+
24
+
25
+ def eigenvalue_space(dom: Any, check_level: CheckLevel | None = None) -> Any:
26
+ r"""
27
+ Return the real Euclidean space of shape ``(rank,)`` that the spectrum of ``dom`` lives in.
28
+
29
+ Hermitian eigenvalues are real even with complex entries (Horn & Johnson,
30
+ *Matrix Analysis*, 2nd ed., Thm 4.1.5). Other domains must supply a real
31
+ spectrum for this helper; the rank is read off the zero element.
32
+
33
+ Parameters
34
+ ----------
35
+ dom : JordanAlgebraSpace
36
+ Domain whose spectrum is modelled.
37
+ check_level : {"none", "cheap", "standard", "strict"}, optional
38
+ Validation level; ``dom``'s by default.
39
+
40
+ Returns
41
+ -------
42
+ DenseCoordinateSpace
43
+ The eigenvalue space.
44
+ """
45
+ # The rank is the length of the spectrum of the zero element.
46
+ if not isinstance(dom, JordanAlgebraSpace):
47
+ raise TypeError(
48
+ "eigenvalue_space requires a Jordan-algebra domain with a spectral "
49
+ f"decomposition; got {type(dom).__name__}."
50
+ )
51
+ ops = dom.ops
52
+ rank = int(dom.spectrum(dom.zeros()).shape[0])
53
+ real_ctx = Context(ops, dtype=ops.real_dtype(dom.dtype))
54
+ level = dom.check_level if check_level is None else check_level
55
+ return DenseCoordinateSpace((rank,), real_ctx, check_level=level)
56
+
57
+
58
+ class SpectralFunctional(_CoordinateFunctional[Domain]):
59
+ r"""
60
+ Lift a symmetric coordinate functional onto a Jordan spectrum: :math:`F(X) = f(\lambda(X))`.
61
+
62
+ ``LpNormFunctional`` lifts to the Schatten norm and ``NegativeEntropyFunctional`` to
63
+ the negative von Neumann entropy on positive trace-one matrices.
64
+ Horn & Johnson, *Matrix Analysis*, 2nd ed., Thm 7.4.7.2; for Hermitian matrices
65
+ the singular values are the absolute eigenvalues, so the gauge is the p-norm.
66
+
67
+ .. warning::
68
+
69
+ The gradient formula requires permutation-invariant ``f`` and differentiability
70
+ at ``lambda(X)`` (or a convex subgradient). Lewis, "Derivatives of Spectral
71
+ Functions", Thms 1.1 and 1.3. Permutation invariance is not checked; a generic
72
+ :class:`~spacecore_optimize.KLDivergenceFunctional` with unequal targets fails it.
73
+ Elementwise spaces reduce to coordinate differentiation; custom Jordan spaces
74
+ must supply a compatible spectral-gradient reconstruction.
75
+
76
+ Parameters
77
+ ----------
78
+ dom : JordanAlgebraSpace
79
+ Domain with a spectral decomposition, e.g. :class:`~spacecore.HermitianSpace`.
80
+ base : Functional
81
+ Symmetric functional on the ``(rank,)`` eigenvalue space; see :func:`eigenvalue_space`.
82
+ ctx : Context, str, or None, optional
83
+ Backend context; resolved from ``dom`` when omitted.
84
+ check_level : {"none", "cheap", "standard", "strict"}, optional
85
+ Validation level.
86
+
87
+ Examples
88
+ --------
89
+ >>> import numpy as np
90
+ >>> import spacecore as sc
91
+ >>> import spacecore_optimize as spo
92
+ >>> ctx = sc.Context(sc.NumpyOps(), dtype=np.float64)
93
+ >>> X = sc.HermitianSpace(2, ctx=ctx)
94
+ >>> S = spo.eigenvalue_space(X)
95
+ >>> vn = spo.SpectralFunctional(X, spo.NegativeEntropyFunctional(S))
96
+ >>> rho = ctx.asarray([[0.5, 0.0], [0.0, 0.5]]) # maximally mixed state
97
+ >>> float(vn.value(rho)) # -log 2, i.e. sum p log p
98
+ -0.6931471805599453
99
+ """
100
+
101
+ _fields = ("dom", "base", "ctx")
102
+ _dynamic = ("base",)
103
+
104
+ def __init__(
105
+ self,
106
+ dom: Domain,
107
+ base: Functional,
108
+ ctx: Context | str | None = None,
109
+ check_level: CheckLevel | None = None,
110
+ ) -> None:
111
+ super().__init__(dom, ctx, check_level=check_level)
112
+ # Check the domain has a spectrum and base lives on its eigenvalue space.
113
+ domain = cast(Any, self.domain)
114
+ if not isinstance(domain, JordanAlgebraSpace):
115
+ raise TypeError(
116
+ "SpectralFunctional requires a Jordan-algebra domain with a "
117
+ f"spectral decomposition (e.g. HermitianSpace); got "
118
+ f"{type(domain).__name__}."
119
+ )
120
+ if not isinstance(base, Functional):
121
+ raise TypeError(f"base must be a Functional, got {type(base).__name__}.")
122
+ expected = eigenvalue_space(domain)
123
+ if base.domain.shape != expected.shape:
124
+ raise ValueError(
125
+ f"base domain shape {base.domain.shape} does not match the "
126
+ f"spectrum of {type(domain).__name__} (rank {expected.shape[0]}); "
127
+ f"build it on eigenvalue_space(dom)."
128
+ )
129
+ # Lewis's formula needs the plain coordinate gradient of f.
130
+ if not getattr(base.domain, "is_euclidean", True):
131
+ # Lewis needs the EUCLIDEAN gradient of f. On a Euclidean eigenvalue
132
+ # space base.grad already is that; under any other metric it would be
133
+ # G^-1 df, silently scaling the reconstructed spectral gradient.
134
+ raise ValueError(
135
+ "SpectralFunctional requires a Euclidean eigenvalue space so that "
136
+ "base.grad is the plain coordinate gradient Lewis's formula needs."
137
+ )
138
+ self.base = base
139
+
140
+ @checked_method(in_space="domain", out_space="codomain")
141
+ def value(self, x: Any) -> Any:
142
+ """Return ``f(lambda(x))``."""
143
+ return self.base.value(cast(Any, self.domain).spectrum(x))
144
+
145
+ def _coordinate_grad(self, x: Any) -> Any:
146
+ """Return the spectral gradient (Lewis, "Derivatives of Spectral Functions", Thm 1.1)."""
147
+ domain = cast(Any, self.domain)
148
+ eigvals, frame = domain.spectral_decompose(x)
149
+ return domain.from_spectrum(self.base.grad(eigvals), frame)
150
+
151
+ def spectralize(
152
+ dom: Domain,
153
+ make_base: Any,
154
+ ctx: Context | str | None = None,
155
+ check_level: CheckLevel | None = None,
156
+ ) -> "SpectralFunctional[Domain]":
157
+ r"""
158
+ Build the spectral lift of ``make_base(eigenvalue_space(dom))`` on ``dom``.
159
+
160
+ Parameters
161
+ ----------
162
+ dom : JordanAlgebraSpace
163
+ Domain with a spectral decomposition.
164
+ make_base : callable
165
+ ``make_base(eigenvalue_space) -> Functional``, symmetric.
166
+ ctx : Context, str, or None, optional
167
+ Backend context.
168
+ check_level : {"none", "cheap", "standard", "strict"}, optional
169
+ Validation level.
170
+
171
+ Returns
172
+ -------
173
+ SpectralFunctional
174
+ ``f(lambda(.))`` on ``dom``.
175
+
176
+ Examples
177
+ --------
178
+ >>> import numpy as np
179
+ >>> import spacecore as sc
180
+ >>> import spacecore_optimize as spo
181
+ >>> ctx = sc.Context(sc.NumpyOps(), dtype=np.float64)
182
+ >>> X = sc.HermitianSpace(2, ctx=ctx)
183
+ >>> vn = spo.spectralize(X, spo.NegativeEntropyFunctional) # negative von Neumann entropy
184
+ >>> float(vn.value(ctx.asarray([[0.5, 0.0], [0.0, 0.5]])))
185
+ -0.6931471805599453
186
+ """
187
+ base = make_base(eigenvalue_space(dom, check_level=check_level))
188
+ return SpectralFunctional(dom, base, ctx, check_level=check_level)
189
+
190
+
191
+ def NuclearNormFunctional(
192
+ dom: Domain,
193
+ ctx: Context | str | None = None,
194
+ check_level: CheckLevel | None = None,
195
+ ) -> "SpectralFunctional[Domain]":
196
+ r"""
197
+ Nuclear norm ``sum_i |lambda_i(X)|``, i.e. the Schatten-1 lift of the 1-norm.
198
+
199
+ Parameters
200
+ ----------
201
+ dom : JordanAlgebraSpace
202
+ Domain with a spectral decomposition.
203
+ ctx : Context, str, or None, optional
204
+ Backend context; resolved from ``dom`` when omitted.
205
+ check_level : {"none", "cheap", "standard", "strict"}, optional
206
+ Validation level.
207
+
208
+ Returns
209
+ -------
210
+ SpectralFunctional
211
+ The Schatten-1 lift.
212
+
213
+ Examples
214
+ --------
215
+ >>> import numpy as np
216
+ >>> import spacecore as sc
217
+ >>> import spacecore_optimize as spo
218
+ >>> ctx = sc.Context(sc.NumpyOps(), dtype=np.float64)
219
+ >>> X = sc.HermitianSpace(2, ctx=ctx)
220
+ >>> f = spo.NuclearNormFunctional(X)
221
+ >>> float(f.value(ctx.asarray([[2.0, 0.0], [0.0, -3.0]])))
222
+ 5.0
223
+ """
224
+ return spectralize(
225
+ dom, lambda s: LpNormFunctional(s, 1.0), ctx, check_level=check_level
226
+ )
File without changes
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: spacecore-optimize
3
+ Version: 0.0.1
4
+ Summary: Optimization on top of SpaceCore: SciPy and optax adapters, and a catalogue of functionals and proximal steps.
5
+ Author: Pavlo Pelikh
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Documentation, https://pavlo3p.github.io/spacecore-optimize/
8
+ Project-URL: Source, https://github.com/Pavlo3P/spacecore-optimize
9
+ Project-URL: SpaceCore, https://github.com/Pavlo3P/SpaceCore
10
+ Project-URL: Changelog, https://github.com/Pavlo3P/spacecore-optimize/blob/main/CHANGELOG.md
11
+ Keywords: optimization,proximal,scipy,optax,spacecore
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: spacecore>=0.5.0
22
+ Requires-Dist: numpy>=2.0.0
23
+ Requires-Dist: scipy>=1.17
24
+ Provides-Extra: optax
25
+ Requires-Dist: jax>=0.9.1; extra == "optax"
26
+ Requires-Dist: optax>=0.2; extra == "optax"
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8.0; extra == "dev"
29
+ Provides-Extra: docs
30
+ Requires-Dist: sphinx>=8.0; extra == "docs"
31
+ Requires-Dist: pydata-sphinx-theme>=0.16; extra == "docs"
32
+ Dynamic: license-file
33
+
34
+ # spacecore-optimize
35
+
36
+ Optimization on top of [SpaceCore](https://github.com/Pavlo3P/SpaceCore). SpaceCore supplies
37
+ spaces, geometry, operators, functionals and proximal steps; this package hands them to
38
+ external optimizers and adds a catalogue of common objectives.
39
+
40
+ The one rule it enforces: an optimizer that works on flat coordinates gets the gradient as
41
+ the covector `X.riesz(F.grad(x))`, not the Riesz gradient `F.grad(x)`. On a Euclidean space
42
+ the two coincide; on a weighted or operator geometry only the covector is the derivative
43
+ with respect to the coordinates SciPy and optax update.
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pip install spacecore-optimize # SciPy adapters and functionals
49
+ pip install "spacecore-optimize[optax]" # plus minimize_optax (JAX backend)
50
+ ```
51
+
52
+ Requires Python 3.11+ and SpaceCore 0.5.0+.
53
+
54
+ ## Example
55
+
56
+ Least squares on a space with a weighted inner product:
57
+
58
+ ```python
59
+ import numpy as np
60
+ import spacecore as sc
61
+ import spacecore_optimize as spo
62
+
63
+ ctx = sc.Context(sc.NumpyOps(), dtype=np.float64)
64
+ X = sc.DenseCoordinateSpace((3,), ctx, geometry=sc.WeightedInnerProduct(ctx.asarray([1.0, 4.0, 9.0])))
65
+ Y = sc.DenseCoordinateSpace((5,), ctx)
66
+ rng = np.random.default_rng(0)
67
+ A = sc.DenseLinOp(ctx.asarray(rng.standard_normal((5, 3))), X, Y, ctx)
68
+ b = ctx.asarray(rng.standard_normal(5))
69
+
70
+ F = spo.least_squares(A, b) # 1/2 ||A x - b||^2
71
+ result = spo.minimize_scipy(F, X.zeros()) # scipy.optimize.minimize, L-BFGS-B
72
+ x = result.x_element # the minimizer, an element of X
73
+ ```
74
+
75
+ ## What is in it
76
+
77
+ | Name | What it does |
78
+ |---|---|
79
+ | `minimize_scipy`, `line_search_scipy` | Run `scipy.optimize.minimize` / `line_search` on a SpaceCore `Functional`. |
80
+ | `minimize_optax`, `OptaxResult` | A compiled optax loop on a JAX-backed domain, stopping on the dual gradient norm. |
81
+ | `least_squares` | `scale * ‖A x − b‖²` as a quadratic form, optionally weighted. |
82
+ | `NegativeEntropyFunctional`, `KLDivergenceFunctional`, `HuberFunctional` | Coordinate objectives whose gradients follow the domain geometry. |
83
+ | `SpectralFunctional`, `spectralize`, `eigenvalue_space`, `NuclearNormFunctional` | Spectral functions on Jordan-algebra spaces such as Hermitian matrices. |
84
+ | `RealifiedFunctional`, `realify` | A complex-domain functional seen on stacked real coordinates. |
85
+ | `generalized_shrinkage`, `prox_l1`, `prox_l2sq`, `project_nonneg` | Closed-form proximal steps in diagonal geometry. |
86
+
87
+ The norms covered by SpaceCore's own prox rules (`LpNormFunctional`,
88
+ `SquaredL2NormFunctional`, `L1NormFunctional`) live in SpaceCore.
89
+
90
+ ## Documentation
91
+
92
+ Published at https://pavlo3p.github.io/spacecore-optimize/. To build locally:
93
+
94
+ ```bash
95
+ pip install -e ".[docs]"
96
+ sphinx-build -W docs docs/_build/html
97
+ ```
98
+
99
+ ## License
100
+
101
+ Apache-2.0.