sdplab 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.
- sdplab/__init__.py +115 -0
- sdplab/_version.py +17 -0
- sdplab/examples/__init__.py +56 -0
- sdplab/examples/_max_cut.py +168 -0
- sdplab/examples/q_tomography/__init__.py +24 -0
- sdplab/examples/q_tomography/_build.py +130 -0
- sdplab/examples/qot/__init__.py +31 -0
- sdplab/examples/qot/_random.py +132 -0
- sdplab/linalg/__init__.py +22 -0
- sdplab/linalg/dense/__init__.py +23 -0
- sdplab/linalg/dense/_kron.py +66 -0
- sdplab/linalg/dense/_ptrace.py +65 -0
- sdplab/problem/__init__.py +70 -0
- sdplab/problem/_base.py +313 -0
- sdplab/problem/_constraint.py +253 -0
- sdplab/problem/_cost.py +434 -0
- sdplab/regularization/__init__.py +51 -0
- sdplab/regularization/_base.py +295 -0
- sdplab/regularization/_eval_space.py +77 -0
- sdplab/regularization/_functional.py +221 -0
- sdplab/regularization/entropy.py +87 -0
- sdplab/regularization/quadratic.py +170 -0
- sdplab/solvers/__init__.py +30 -0
- sdplab/solvers/_common.py +80 -0
- sdplab/solvers/_cvxpy.py +156 -0
- sdplab/solvers/_regularized.py +234 -0
- sdplab/special/__init__.py +15 -0
- sdplab/special/pauli/__init__.py +25 -0
- sdplab/special/pauli/_operators.py +791 -0
- sdplab/special/pauli/_pauli.py +110 -0
- sdplab/special/pauli/_string.py +48 -0
- sdplab/special/qot/__init__.py +33 -0
- sdplab/special/qot/_constraint_op.py +398 -0
- sdplab/special/qot/_cvxpy.py +119 -0
- sdplab-0.0.1.dist-info/METADATA +167 -0
- sdplab-0.0.1.dist-info/RECORD +39 -0
- sdplab-0.0.1.dist-info/WHEEL +5 -0
- sdplab-0.0.1.dist-info/licenses/LICENSE +201 -0
- sdplab-0.0.1.dist-info/top_level.txt +1 -0
sdplab/__init__.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Copyright 2026 Pavlo Pelikh
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
r"""Semidefinite and conic programs over spacecore Euclidean Jordan algebras.
|
|
16
|
+
|
|
17
|
+
.. math::
|
|
18
|
+
|
|
19
|
+
\min_X \quad \langle C, X\rangle
|
|
20
|
+
\quad \text{s.t.} \quad \mathcal{A}X = b,\quad X \succeq 0
|
|
21
|
+
|
|
22
|
+
``X`` is any Jordan-algebra element -- Hermitian matrix, nonnegative vector, or
|
|
23
|
+
a tree of such blocks -- and :math:`X \succeq 0` means a nonnegative spectrum.
|
|
24
|
+
Build with ``SDPProblem(C, A, b)``; solve with :func:`run_cvxpy_solver`, or
|
|
25
|
+
smooth it: a :class:`Regularizer` through
|
|
26
|
+
:class:`RegularizedSDPDualFunctional`, ``.bind(eps)``, then
|
|
27
|
+
:func:`run_regularized_solver`.
|
|
28
|
+
|
|
29
|
+
Smoothing adds :math:`\varepsilon \operatorname{Tr}[\varphi(X)]` to the primal,
|
|
30
|
+
giving the differentiable dual
|
|
31
|
+
|
|
32
|
+
.. math::
|
|
33
|
+
|
|
34
|
+
D_\varepsilon(y) = \langle b, y\rangle - \varepsilon
|
|
35
|
+
\operatorname{Tr}\!\big[\psi\big(
|
|
36
|
+
(\mathcal{A}^\dagger y - C)/\varepsilon\big)\big],
|
|
37
|
+
|
|
38
|
+
maximized over ``y`` in ``cod``, with :math:`\psi'` of the dual slack
|
|
39
|
+
recovering the primal. :math:`\varepsilon` is per-call, so continuation needs
|
|
40
|
+
no rebuild.
|
|
41
|
+
|
|
42
|
+
Subpackages: :mod:`~sdplab.problem`, :mod:`~sdplab.regularization`,
|
|
43
|
+
:mod:`~sdplab.solvers`, :mod:`~sdplab.examples`, :mod:`~sdplab.linalg`,
|
|
44
|
+
:mod:`~sdplab.special`.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
from typing import TYPE_CHECKING
|
|
50
|
+
|
|
51
|
+
from .problem import (
|
|
52
|
+
ConstraintOp,
|
|
53
|
+
Cost,
|
|
54
|
+
DenseConstraintOp,
|
|
55
|
+
MatrixFreeConstraintOp,
|
|
56
|
+
SDPProblem,
|
|
57
|
+
SparseConstraintOp,
|
|
58
|
+
)
|
|
59
|
+
from .regularization import (
|
|
60
|
+
BoundDualFunctional,
|
|
61
|
+
EntropyReg,
|
|
62
|
+
QuadraticReg,
|
|
63
|
+
RegularizedSDPDualFunctional,
|
|
64
|
+
Regularizer,
|
|
65
|
+
)
|
|
66
|
+
from ._version import __version__
|
|
67
|
+
|
|
68
|
+
# Solvers pull in CVXPY (~1.2s), making an optional backend a hard requirement
|
|
69
|
+
# for anyone who only builds problems. PEP 562 keeps them reachable as
|
|
70
|
+
# ``sdplab.run_*`` while deferring the cost to first use.
|
|
71
|
+
_LAZY = {
|
|
72
|
+
"run_regularized_solver": "sdplab.solvers",
|
|
73
|
+
"run_cvxpy_solver": "sdplab.solvers",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if TYPE_CHECKING: # type checkers and IDEs do not run __getattr__
|
|
77
|
+
from .solvers import run_cvxpy_solver, run_regularized_solver
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def __getattr__(name: str):
|
|
81
|
+
"""Resolve the lazily-exported solver entry points on first access."""
|
|
82
|
+
module = _LAZY.get(name)
|
|
83
|
+
if module is None:
|
|
84
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
85
|
+
import importlib
|
|
86
|
+
|
|
87
|
+
value = getattr(importlib.import_module(module), name)
|
|
88
|
+
globals()[name] = value # cache, so this runs once per name
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def __dir__() -> list[str]:
|
|
93
|
+
"""Include the lazy names in ``dir(sdplab)`` and tab completion."""
|
|
94
|
+
return sorted(set(globals()) | set(_LAZY))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = [
|
|
98
|
+
"__version__",
|
|
99
|
+
# problem data
|
|
100
|
+
"SDPProblem",
|
|
101
|
+
"Cost",
|
|
102
|
+
"ConstraintOp",
|
|
103
|
+
"DenseConstraintOp",
|
|
104
|
+
"SparseConstraintOp",
|
|
105
|
+
"MatrixFreeConstraintOp",
|
|
106
|
+
# regularization and the smoothed dual
|
|
107
|
+
"Regularizer",
|
|
108
|
+
"EntropyReg",
|
|
109
|
+
"QuadraticReg",
|
|
110
|
+
"RegularizedSDPDualFunctional",
|
|
111
|
+
"BoundDualFunctional",
|
|
112
|
+
# solvers (lazy)
|
|
113
|
+
"run_regularized_solver",
|
|
114
|
+
"run_cvxpy_solver",
|
|
115
|
+
]
|
sdplab/_version.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright 2026 Pavlo Pelikh
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Single source of truth for the SDPLab package version."""
|
|
16
|
+
|
|
17
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Copyright 2026 Pavlo Pelikh
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
r"""Ready-made SDP instances for testing and benchmarking.
|
|
16
|
+
|
|
17
|
+
Each builder returns a :class:`~sdplab.problem.SDPProblem`; they differ in the
|
|
18
|
+
structure of :math:`\mathcal{A}`.
|
|
19
|
+
|
|
20
|
+
``generate_max_cut``
|
|
21
|
+
Diagonal extraction, :math:`\operatorname{Herm}(n) \to \mathbb{R}^n`. Real,
|
|
22
|
+
and **gauge-degenerate**: :math:`\mathcal{A}^\dagger \mathbf{1} = \mathbb{1}`,
|
|
23
|
+
so the dual is fixed only modulo :math:`\operatorname{span}\{\mathbf 1\}`.
|
|
24
|
+
``generate_random_qot``
|
|
25
|
+
One-body partial traces,
|
|
26
|
+
:math:`\operatorname{Herm}(d^N) \to \operatorname{Herm}(d)^N`. Complex,
|
|
27
|
+
stacked-block codomain. Returns ``(problem, state)``, ``state`` feasible.
|
|
28
|
+
``generate_qubit_tomography``
|
|
29
|
+
:math:`\operatorname{Tr}[M_i X] = b_i` plus a unit-trace row. Zero cost, so
|
|
30
|
+
pure feasibility.
|
|
31
|
+
|
|
32
|
+
All three domains are :class:`~spacecore.HermitianSpace`, but the interfaces
|
|
33
|
+
are written against the general Euclidean Jordan algebra contract -- orthant
|
|
34
|
+
spaces, block products, trees mixing cone types. Prefer space-level operations
|
|
35
|
+
(``spectrum``, ``flatten``, ``axpy``) over anything assuming a dense block.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from ._max_cut import (
|
|
39
|
+
MaxCutOperator,
|
|
40
|
+
generate_erdos_renyi_graph_laplacian,
|
|
41
|
+
generate_max_cut,
|
|
42
|
+
)
|
|
43
|
+
from .q_tomography import generate_qubit_tomography
|
|
44
|
+
from .qot import QOTConstraintOp, generate_random_qot
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# problem generators
|
|
48
|
+
"generate_max_cut",
|
|
49
|
+
"generate_qubit_tomography",
|
|
50
|
+
"generate_random_qot",
|
|
51
|
+
# constraint operators, for hand-assembled instances
|
|
52
|
+
"MaxCutOperator",
|
|
53
|
+
"QOTConstraintOp",
|
|
54
|
+
# supporting utilities
|
|
55
|
+
"generate_erdos_renyi_graph_laplacian",
|
|
56
|
+
]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Copyright 2026 Pavlo Pelikh
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
from spacecore import (
|
|
19
|
+
Context,
|
|
20
|
+
DenseArray,
|
|
21
|
+
DenseVectorSpace,
|
|
22
|
+
HermitianSpace,
|
|
23
|
+
LinOp,
|
|
24
|
+
NumpyOps,
|
|
25
|
+
checked_method,
|
|
26
|
+
jax_pytree_class,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from sdplab.problem import SDPProblem
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@jax_pytree_class
|
|
33
|
+
class MaxCutOperator(LinOp[HermitianSpace, DenseVectorSpace]):
|
|
34
|
+
r"""Diagonal-extraction operator :math:`\mathcal{A}` for the Max-Cut SDP.
|
|
35
|
+
|
|
36
|
+
The linear map is
|
|
37
|
+
:math:`\mathcal{A}: \operatorname{Herm}(n) \to \mathbb{R}^n` with
|
|
38
|
+
:math:`(\mathcal{A}X)_i = X_{ii}`, so the constraint
|
|
39
|
+
:math:`\mathcal{A}X = \mathbf{1}` fixes every diagonal entry to one. Its
|
|
40
|
+
adjoint sends a vector to the matrix with that diagonal,
|
|
41
|
+
:math:`\mathcal{A}^\dagger y = \operatorname{diag}(y)`, which satisfies
|
|
42
|
+
:math:`\operatorname{Tr}[(\mathcal{A}X)y] = \operatorname{Tr}[X \operatorname{diag}(y)]`.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
@checked_method(in_space="domain", out_space="codomain")
|
|
46
|
+
def apply(self, X: DenseArray) -> DenseArray:
|
|
47
|
+
r"""Return :math:`\mathcal{A}X = \operatorname{diag}(X)`, the diagonal of ``X``."""
|
|
48
|
+
return self.ops.diag(X)
|
|
49
|
+
|
|
50
|
+
@checked_method(in_space="codomain", out_space="domain")
|
|
51
|
+
def rapply(self, y: DenseArray) -> DenseArray:
|
|
52
|
+
r"""Return :math:`\mathcal{A}^\dagger y = \operatorname{diag}(y)`."""
|
|
53
|
+
return self.ops.diag(y)
|
|
54
|
+
|
|
55
|
+
def to_dense(self) -> DenseArray:
|
|
56
|
+
r"""Return the operator tensor of shape ``codomain.shape + domain.shape``.
|
|
57
|
+
|
|
58
|
+
The entry ``T[i, p, q]`` is one iff ``i == p == q``, so that
|
|
59
|
+
``sum_{p,q} T[i, p, q] X[p, q] = X[i, i]``.
|
|
60
|
+
"""
|
|
61
|
+
n = self.cod.shape[0]
|
|
62
|
+
idx = self.ops.arange(n)
|
|
63
|
+
mask = (idx[:, None, None] == idx[None, :, None]) & (
|
|
64
|
+
idx[:, None, None] == idx[None, None, :]
|
|
65
|
+
)
|
|
66
|
+
return self.ops.asarray(mask, dtype=self.dtype)
|
|
67
|
+
|
|
68
|
+
def tree_flatten(self):
|
|
69
|
+
"""The operator carries no arrays; spaces and context are static aux."""
|
|
70
|
+
return (), (self.dom, self.cod, self.ctx)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def tree_unflatten(cls, aux, children):
|
|
74
|
+
"""Rebuild the operator from JAX PyTree data."""
|
|
75
|
+
dom, cod, ctx = aux
|
|
76
|
+
return cls(dom, cod, ctx)
|
|
77
|
+
|
|
78
|
+
def _convert(self, new_ctx: Context) -> MaxCutOperator:
|
|
79
|
+
"""Return an equivalent operator in ``new_ctx``."""
|
|
80
|
+
return MaxCutOperator(self.dom.convert(new_ctx), self.cod.convert(new_ctx), new_ctx)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def generate_erdos_renyi_graph_laplacian(
|
|
84
|
+
n: int,
|
|
85
|
+
p: float = 0.3,
|
|
86
|
+
seed: int | None = None,
|
|
87
|
+
weighted: bool = True,
|
|
88
|
+
weight_low: float = 0.0,
|
|
89
|
+
weight_high: float = 1.0,
|
|
90
|
+
) -> np.ndarray:
|
|
91
|
+
"""
|
|
92
|
+
Generate an undirected Erdős–Rényi graph G(n, p).
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
L:
|
|
97
|
+
Graph Laplacian L = D - W in S^n,
|
|
98
|
+
where D[i, i] = sum_j W[i, j].
|
|
99
|
+
"""
|
|
100
|
+
if n <= 0:
|
|
101
|
+
raise ValueError("n must be positive.")
|
|
102
|
+
if not (0.0 <= p <= 1.0):
|
|
103
|
+
raise ValueError("p must be in [0, 1].")
|
|
104
|
+
if weight_low < 0 or weight_high < weight_low:
|
|
105
|
+
raise ValueError("Require 0 <= weight_low <= weight_high.")
|
|
106
|
+
|
|
107
|
+
rng = np.random.default_rng(seed)
|
|
108
|
+
|
|
109
|
+
# Upper-triangular edge mask, excluding diagonal.
|
|
110
|
+
edge_mask = rng.random((n, n)) < p
|
|
111
|
+
edge_mask = np.triu(edge_mask, k=1)
|
|
112
|
+
|
|
113
|
+
if weighted:
|
|
114
|
+
weights = rng.uniform(weight_low, weight_high, size=(n, n))
|
|
115
|
+
W_upper = edge_mask * weights
|
|
116
|
+
else:
|
|
117
|
+
W_upper = edge_mask.astype(float)
|
|
118
|
+
|
|
119
|
+
W = W_upper + W_upper.T
|
|
120
|
+
|
|
121
|
+
degrees = W.sum(axis=1)
|
|
122
|
+
L = np.diag(degrees) - W
|
|
123
|
+
|
|
124
|
+
return L
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def generate_max_cut(
|
|
128
|
+
n: int,
|
|
129
|
+
p: float = 0.3,
|
|
130
|
+
seed: int | None = None,
|
|
131
|
+
weighted: bool = True,
|
|
132
|
+
weight_low: float = 0.0,
|
|
133
|
+
weight_high: float = 1.0,
|
|
134
|
+
atol: float = 0.0,
|
|
135
|
+
rtol: float = 0.0,
|
|
136
|
+
enforce_herm: bool = True,
|
|
137
|
+
unit_trace: bool = False,
|
|
138
|
+
ctx: Context | str | None = None
|
|
139
|
+
):
|
|
140
|
+
r"""Return the Max-Cut SDP for an Erdős–Rényi graph on ``n`` vertices.
|
|
141
|
+
|
|
142
|
+
The constraint is :math:`\operatorname{diag}(X) = \mathbf{1}`, hence
|
|
143
|
+
:math:`\operatorname{Tr}X = n`. With ``unit_trace=True`` the substitution
|
|
144
|
+
:math:`X = n\tilde X` is applied instead, giving
|
|
145
|
+
:math:`\operatorname{diag}(\tilde X) = \mathbf{1}/n`,
|
|
146
|
+
:math:`\operatorname{Tr}\tilde X = 1`, and cost :math:`nC` -- the same
|
|
147
|
+
problem in a rescaled variable, so the optimal value is unchanged. Use it
|
|
148
|
+
when a unit-trace primal is assumed, as by the fixed-trace (log-partition)
|
|
149
|
+
form of the entropy dual.
|
|
150
|
+
"""
|
|
151
|
+
np_ctx = Context(NumpyOps(), dtype="float64", check_level="none")
|
|
152
|
+
|
|
153
|
+
L = generate_erdos_renyi_graph_laplacian(n, p, seed, weighted, weight_low, weight_high)
|
|
154
|
+
C = -L / 4 # Minus to turn maximization into minimization
|
|
155
|
+
b = np.ones(n)
|
|
156
|
+
if unit_trace:
|
|
157
|
+
C = C * n
|
|
158
|
+
b = b / n
|
|
159
|
+
|
|
160
|
+
dom = HermitianSpace(n, atol=atol, rtol=rtol, enforce_herm=enforce_herm, ctx=np_ctx)
|
|
161
|
+
cod = DenseVectorSpace((n,), ctx=np_ctx)
|
|
162
|
+
A = MaxCutOperator(dom, cod, np_ctx)
|
|
163
|
+
|
|
164
|
+
sdp = SDPProblem(C, A, b, ctx=np_ctx)
|
|
165
|
+
|
|
166
|
+
if ctx is not None:
|
|
167
|
+
sdp = sdp.convert(ctx)
|
|
168
|
+
return sdp
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Copyright 2026 Pavlo Pelikh
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
r"""State tomography as feasibility: find :math:`X \succeq 0` with
|
|
16
|
+
:math:`\operatorname{Tr}[M_i X] = b_i` and :math:`\operatorname{Tr}[X] = 1`.
|
|
17
|
+
|
|
18
|
+
Unit trace is appended as one more measurement row. See :mod:`._build` for the
|
|
19
|
+
transpose the dense operator needs.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from ._build import generate_qubit_tomography
|
|
23
|
+
|
|
24
|
+
__all__ = ["generate_qubit_tomography"]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Copyright 2026 Pavlo Pelikh
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
r"""Builders for qubit tomography SDP feasibility problems.
|
|
16
|
+
|
|
17
|
+
Quantum tomography estimates an unknown density matrix
|
|
18
|
+
:math:`X \in \operatorname{Herm}(d)` from linear measurement constraints.
|
|
19
|
+
This module builds the feasibility SDP
|
|
20
|
+
|
|
21
|
+
.. math::
|
|
22
|
+
|
|
23
|
+
\min_{X \in \operatorname{Herm}(d)}\quad 0
|
|
24
|
+
\quad \text{s.t.} \quad
|
|
25
|
+
\mathcal{A}X = b_{\mathrm{obs}},\quad
|
|
26
|
+
\operatorname{Tr}[X] = 1,\quad
|
|
27
|
+
X \succeq 0.
|
|
28
|
+
|
|
29
|
+
Here :math:`\mathcal{A}: \operatorname{Herm}(d) \to \mathbb{R}^m` is the
|
|
30
|
+
measurement map :math:`(\mathcal{A}X)_i = \operatorname{Tr}[M_i X]` and
|
|
31
|
+
:math:`b_{\mathrm{obs}}` stores the observed expectation values. The unit-trace
|
|
32
|
+
constraint is appended as one extra row measuring the identity, so the returned
|
|
33
|
+
object is a plain :class:`~sdplab.problem.SDPProblem`.
|
|
34
|
+
|
|
35
|
+
The measurement map is a dense tensor contraction and nothing more, so it is a
|
|
36
|
+
:class:`~sdplab.problem.DenseConstraintOp` rather than a hand-written operator
|
|
37
|
+
-- see the transpose note on :func:`generate_qubit_tomography`.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import numpy as np
|
|
43
|
+
from spacecore import Context, DenseArray, DenseVectorSpace, HermitianSpace, NumpyOps
|
|
44
|
+
|
|
45
|
+
from ...problem import DenseConstraintOp, SDPProblem
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def generate_qubit_tomography(
|
|
49
|
+
M: DenseArray,
|
|
50
|
+
b_obs: DenseArray,
|
|
51
|
+
atol: float = 0.0,
|
|
52
|
+
rtol: float = 0.0,
|
|
53
|
+
enforce_herm: bool = True,
|
|
54
|
+
ctx: Context | str | None = None,
|
|
55
|
+
) -> SDPProblem:
|
|
56
|
+
r"""Build the tomography feasibility SDP for observables ``M``.
|
|
57
|
+
|
|
58
|
+
The unknown is a density matrix :math:`X \in \operatorname{Herm}(d)` and
|
|
59
|
+
the returned problem is
|
|
60
|
+
|
|
61
|
+
.. math::
|
|
62
|
+
|
|
63
|
+
\min_{X \in \operatorname{Herm}(d)}\quad 0
|
|
64
|
+
\quad \text{s.t.} \quad
|
|
65
|
+
\operatorname{Tr}[M_i X] = b_i,\quad
|
|
66
|
+
\operatorname{Tr}[X] = 1,\quad
|
|
67
|
+
X \succeq 0.
|
|
68
|
+
|
|
69
|
+
The trace-one constraint is appended as one extra row measuring the
|
|
70
|
+
identity, so the constraint operator has ``m + 1`` rows.
|
|
71
|
+
|
|
72
|
+
**Mind the transpose.** A :class:`~sdplab.problem.DenseConstraintOp` pairs
|
|
73
|
+
its tensor with :math:`X` by Frobenius,
|
|
74
|
+
:math:`(\mathcal{A}X)_i = \sum_{pq} T_{i,pq} X_{pq} =
|
|
75
|
+
\operatorname{Tr}[T_i^{T} X]`. Handing it the observables directly would
|
|
76
|
+
therefore measure :math:`\operatorname{Tr}[M_i^{T} X]` -- for a Hermitian
|
|
77
|
+
observable the *conjugate* of the intended value, silently flipping the
|
|
78
|
+
sign of every measurement with a nonzero imaginary part (the Pauli
|
|
79
|
+
:math:`Y`, say). The stack is transposed here so that
|
|
80
|
+
:math:`(\mathcal{A}X)_i = \operatorname{Tr}[M_i X]`, which also makes the
|
|
81
|
+
operator's cvxpy encoding come out as the :math:`M_i` themselves.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
M: Stack of Hermitian observables, shape ``(m, d, d)``.
|
|
85
|
+
b_obs: Observed expectation values :math:`b_i = \operatorname{Tr}[M_i X]`,
|
|
86
|
+
length ``m``. Taken as real -- an imaginary part would not be
|
|
87
|
+
attainable by a Hermitian observable on a Hermitian state.
|
|
88
|
+
atol: Absolute Hermitian membership tolerance.
|
|
89
|
+
rtol: Relative Hermitian membership tolerance.
|
|
90
|
+
enforce_herm: Whether the primal domain enforces Hermitian matrices.
|
|
91
|
+
ctx: Optional backend context for the returned problem. The problem is
|
|
92
|
+
assembled on NumPy (in a complex dtype when ``M`` is complex) and
|
|
93
|
+
converted, matching :func:`~sdplab.examples.generate_max_cut`.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Feasibility :class:`~sdplab.problem.SDPProblem` with a zero cost.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
ValueError: If ``M`` is not a stack of square matrices, if ``b_obs``
|
|
100
|
+
does not match its length, or if any observable is not Hermitian.
|
|
101
|
+
"""
|
|
102
|
+
M = np.asarray(M)
|
|
103
|
+
b_obs = np.asarray(b_obs).reshape(-1)
|
|
104
|
+
|
|
105
|
+
if M.ndim != 3 or M.shape[-1] != M.shape[-2]:
|
|
106
|
+
raise ValueError(f"M must have shape (m, d, d); got {M.shape}.")
|
|
107
|
+
if b_obs.shape[0] != M.shape[0]:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"b_obs has length {b_obs.shape[0]}, but M has {M.shape[0]} observables."
|
|
110
|
+
)
|
|
111
|
+
if not np.allclose(M, np.conj(np.swapaxes(M, -1, -2)), atol=1e-10):
|
|
112
|
+
raise ValueError("every observable M[i] must be Hermitian.")
|
|
113
|
+
|
|
114
|
+
m, d = M.shape[0], M.shape[-1]
|
|
115
|
+
dtype = "complex128" if np.iscomplexobj(M) else "float64"
|
|
116
|
+
np_ctx = Context(NumpyOps(), dtype=dtype, check_level="none")
|
|
117
|
+
|
|
118
|
+
# Append Tr[X] = 1 as one extra measurement of the identity.
|
|
119
|
+
M_full = np.concatenate([M.astype(dtype), np.eye(d, dtype=dtype)[None]], axis=0)
|
|
120
|
+
b_full = np.concatenate([b_obs.real.astype("float64"), [1.0]])
|
|
121
|
+
|
|
122
|
+
dom = HermitianSpace(d, atol=atol, rtol=rtol, enforce_herm=enforce_herm, ctx=np_ctx)
|
|
123
|
+
cod = DenseVectorSpace((m + 1,), ctx=np_ctx)
|
|
124
|
+
A = DenseConstraintOp(np.swapaxes(M_full, -1, -2), dom, cod, np_ctx)
|
|
125
|
+
|
|
126
|
+
sdp = SDPProblem(dom.zeros(), A, b_full, ctx=np_ctx) # zero cost: pure feasibility
|
|
127
|
+
|
|
128
|
+
if ctx is not None:
|
|
129
|
+
sdp = sdp.convert(ctx)
|
|
130
|
+
return sdp
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Copyright 2026 Pavlo Pelikh
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
r"""Random quantum optimal-transport instances: a coupling
|
|
16
|
+
:math:`\Gamma \in \operatorname{Herm}(d^N)` of minimal cost with prescribed
|
|
17
|
+
one-body marginals :math:`\operatorname{Tr}^k[\Gamma] = \gamma_k`.
|
|
18
|
+
|
|
19
|
+
The codomain is a stack of Hermitian blocks, so the dual is a tuple of matrices
|
|
20
|
+
rather than a vector of scalars. :func:`generate_random_qot` derives the
|
|
21
|
+
marginals from a reference coupling, so the returned ``(problem, state)``
|
|
22
|
+
carries a feasible point.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from ...special.qot import QOTConstraintOp
|
|
26
|
+
from ._random import generate_random_qot
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"QOTConstraintOp",
|
|
30
|
+
"generate_random_qot",
|
|
31
|
+
]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Copyright 2026 Pavlo Pelikh
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
r"""Random QOT problem generator.
|
|
16
|
+
|
|
17
|
+
Use this module when you want a self-contained QOT test problem. It creates a
|
|
18
|
+
random cost matrix, builds a valid primal density matrix, computes the
|
|
19
|
+
marginals from that density matrix, and returns an SDP whose constraints are
|
|
20
|
+
therefore feasible by construction.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from typing import Any, Optional, Tuple
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
from spacecore import Context, NumpyOps
|
|
27
|
+
from ...special.qot import QOTConstraintOp
|
|
28
|
+
from ...problem import SDPProblem
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def generate_random_qot(
|
|
32
|
+
d: int, N: int,
|
|
33
|
+
proportions: tuple[float, ...],
|
|
34
|
+
seed: Optional[int] = 0,
|
|
35
|
+
atol: float = 0.0,
|
|
36
|
+
rtol: float = 0.0,
|
|
37
|
+
enforce_herm: bool = True,
|
|
38
|
+
ctx: Context | str | None = None
|
|
39
|
+
) -> Tuple[SDPProblem, Any]:
|
|
40
|
+
r"""
|
|
41
|
+
Generate a random dense QOT instance together with a feasible primal state.
|
|
42
|
+
|
|
43
|
+
The generated SDP has the form
|
|
44
|
+
|
|
45
|
+
.. math::
|
|
46
|
+
|
|
47
|
+
\min_\Gamma \quad \operatorname{Tr}[C \Gamma]
|
|
48
|
+
\quad \text{s.t.} \quad
|
|
49
|
+
\operatorname{Tr}^k[\Gamma] = \gamma_k,\quad
|
|
50
|
+
k = 0, ..., N - 1,\quad \Gamma \succeq 0.
|
|
51
|
+
|
|
52
|
+
The function first chooses a reference coupling ``Gamma`` and then defines
|
|
53
|
+
:math:`\gamma_k = \operatorname{Tr}^k[\Gamma]`. Because the right-hand side is computed
|
|
54
|
+
from ``Gamma``, the returned ``state`` is guaranteed to satisfy the
|
|
55
|
+
equality constraints.
|
|
56
|
+
|
|
57
|
+
This function samples a random Hermitian cost matrix on
|
|
58
|
+
:math:`(\mathbb{C}^d)^{\otimes N}`,
|
|
59
|
+
builds a reference density matrix as a convex combination of eigenvector projectors,
|
|
60
|
+
computes its one-body marginals through ``QOTConstraintOp``, and returns the
|
|
61
|
+
corresponding dense SDP problem plus the same state as a plain ``dom`` element.
|
|
62
|
+
|
|
63
|
+
The generated primal state is feasible for the constructed constraint data by design,
|
|
64
|
+
since the marginals are obtained by applying the constraint operator to that state.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
d: Local Hilbert space dimension. The full state space is
|
|
68
|
+
:math:`(\mathbb{C}^d)^{\otimes N}`, so the ambient matrix size is
|
|
69
|
+
:math:`D = d^N`.
|
|
70
|
+
N: Number of subsystems.
|
|
71
|
+
proportions: Coefficients used to form the mixed state
|
|
72
|
+
:math:`\Gamma = \sum_i p_i \, |v_i\rangle \langle v_i|`,
|
|
73
|
+
where the :math:`v_i` are eigenvectors of the sampled cost matrix.
|
|
74
|
+
This is intended to be a convex combination, so the entries should
|
|
75
|
+
be nonnegative and sum to :math:`1`.
|
|
76
|
+
seed: Random seed used for NumPy sampling.
|
|
77
|
+
atol: Absolute tolerance passed to ``QOTConstraintOp``.
|
|
78
|
+
rtol: Relative tolerance passed to ``QOTConstraintOp``.
|
|
79
|
+
enforce_herm: Whether the constraint operator should enforce Hermitian outputs.
|
|
80
|
+
ctx: Target context for the returned SDP problem. If provided, the generated
|
|
81
|
+
problem is converted to this context before returning.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
- ``qot`` is an ``SDPProblem`` representing the dense QOT SDP with the
|
|
85
|
+
sampled Hermitian cost matrix and marginals induced by ``Gamma``.
|
|
86
|
+
- ``state`` is the same density matrix used to define the marginals, a
|
|
87
|
+
plain ``dom`` element in the returned problem's context.
|
|
88
|
+
|
|
89
|
+
Notes:
|
|
90
|
+
- The random cost matrix is sampled entrywise in real and imaginary parts and
|
|
91
|
+
then symmetrized to be Hermitian.
|
|
92
|
+
- The density matrix ``Gamma`` is built in the NumPy complex context
|
|
93
|
+
``Context(NumpyOps(), dtype=np.complex128)`` before optional conversion.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
D = d ** N
|
|
97
|
+
|
|
98
|
+
proportions = np.asarray(proportions, dtype=float)
|
|
99
|
+
|
|
100
|
+
if len(proportions) > D:
|
|
101
|
+
raise ValueError("len(proportions) cannot exceed D = d ** N")
|
|
102
|
+
|
|
103
|
+
if np.any(proportions < 0):
|
|
104
|
+
raise ValueError("proportions must be nonnegative")
|
|
105
|
+
|
|
106
|
+
if not np.isclose(np.sum(proportions), 1.0):
|
|
107
|
+
raise ValueError("proportions must sum to 1")
|
|
108
|
+
|
|
109
|
+
rng = np.random.default_rng(seed)
|
|
110
|
+
cost_re_im = rng.normal(size=(2, D, D))
|
|
111
|
+
cost_matrix = cost_re_im[0, :, :] + 1j * cost_re_im[1, :, :]
|
|
112
|
+
cost_matrix = (cost_matrix + cost_matrix.T.conj()) * .5
|
|
113
|
+
|
|
114
|
+
# Compute eigenvalues and eigenvectors
|
|
115
|
+
evals, evecs = np.linalg.eigh(cost_matrix)
|
|
116
|
+
|
|
117
|
+
# Compute ground state energy and density matrix
|
|
118
|
+
np_ctx = Context(NumpyOps(), dtype=np.complex128, check_level="none")
|
|
119
|
+
Gamma = sum(p * np.outer(v, v.conj()) for p, v in zip(proportions, evecs.T))
|
|
120
|
+
Gamma = (Gamma + Gamma.T.conj()) * .5
|
|
121
|
+
qot_op = QOTConstraintOp(d=d, N=N, atol=atol, rtol=rtol, enforce_herm=enforce_herm, ctx=np_ctx)
|
|
122
|
+
marginals = qot_op.apply(Gamma)
|
|
123
|
+
|
|
124
|
+
# Define QOT problem & convert to target ctx
|
|
125
|
+
qot = SDPProblem(cost_matrix, qot_op, marginals, ctx=np_ctx)
|
|
126
|
+
if ctx is not None:
|
|
127
|
+
qot = qot.convert(ctx)
|
|
128
|
+
|
|
129
|
+
# Move the example state onto the problem's context
|
|
130
|
+
state = qot.ctx.asarray(Gamma)
|
|
131
|
+
|
|
132
|
+
return qot, state
|