Quantum optimal transport#
The partial-trace constraint operator and a dedicated dual solver.
Partial-trace operator \(\mathcal{A}\) for quantum optimal transport. |
|
Solve the QOT dual SDP. |
- class sdplab.special.qot.QOTConstraintOp(*, d, N, atol=0.0, rtol=0.0, enforce_herm=True, ctx=None)[source]#
Bases:
MatrixFreeConstraintOpPartial-trace operator \(\mathcal{A}\) for quantum optimal transport.
This is the linear map
\[\mathcal{A}: \operatorname{Herm}(d^N) \to \operatorname{Herm}(d)^N.\]Its domain \(\operatorname{dom}(\mathcal{A})\) contains global Hermitian matrices on
Ntensor factors. Its codomain \(\operatorname{cod}(\mathcal{A})\) containsNHermitiand x dmatrices, one per subsystem.If \(\Gamma \in \operatorname{dom}(\mathcal{A})\) is a feasible QOT coupling and \(\gamma_k\) is the prescribed marginal for site
k, then the equality constraint is\[(\mathcal{A}\Gamma)_k = \operatorname{Tr}^k[\Gamma] = \gamma_k.\]- Parameters:
d (int)
N (int)
atol (float)
rtol (float)
enforce_herm (bool)
ctx (Context | str | None)
- __init__(*, d, N, atol=0.0, rtol=0.0, enforce_herm=True, ctx=None)[source]#
Create \(\mathcal{A}: \operatorname{Herm}(d^N) \to \operatorname{Herm}(d)^N\).
- Parameters:
d (int) – Local Hilbert-space dimension.
N (int) – Number of tensor factors/subsystems.
atol (float) – Absolute tolerance for Hermitian membership checks.
rtol (float) – Relative tolerance for Hermitian membership checks.
enforce_herm (bool) – Whether domain and codomain require Hermitian input.
ctx (Context | str | None) – Optional backend context.
- apply(X)[source]#
Return \(\mathcal{A}\Gamma\), the one-body marginals of
X.Xis the numerical array representing \(\Gamma \in \operatorname{dom}(\mathcal{A})\). It has shape(d^N, d^N), and the return value lies in \(\operatorname{cod}(\mathcal{A})\) with shape(N, d, d). Thek-th block is \((\mathcal{A}\Gamma)_k = \operatorname{Tr}^k[\Gamma]\).- Parameters:
X (DenseArray)
- Return type:
DenseArray
- rapply(y)[source]#
Apply the adjoint \(\mathcal{A}^\dagger\) as a Kronecker sum.
For \(y = (y_0, \ldots, y_{N-1}) \in \operatorname{cod}(\mathcal{A})\), the adjoint is
\[\mathcal{A}^\dagger y = y_0 \oplus \cdots \oplus y_{N-1} = \sum_k I \otimes \cdots \otimes y_k \otimes \cdots \otimes I,\]as an element of \(\operatorname{dom}(\mathcal{A})\). This identity is characterized by
\[\operatorname{Tr}[(\mathcal{A}\Gamma)y] = \operatorname{Tr}[\Gamma(\mathcal{A}^\dagger y)].\]The decorator already asserts codomain membership on entry and domain membership on exit, so no explicit check is repeated here.
- Parameters:
y (DenseArray)
- Return type:
Any
- to_sparse()[source]#
Materialize \(\mathcal{A}\) as a sparse coordinate matrix.
The shape is
(prod(cod.shape), prod(dom.shape)) = (N d^2, d^{2N}), matchingto_matrix(): row \((k, a, b)\) is the flattened tensor slice reading off \(\operatorname{Tr}^k\) at marginal entry \((a, b)\),\[(\mathcal{A}\Gamma)_{k,ab} = \sum_{l,r} \Gamma_{(l,a,r),(l,b,r)},\]so that row carries exactly \(d^{N-1}\) unit entries – the configurations of the traced-out subsystems. Storage is therefore \(N d^{N+1}\) nonzeros instead of the \(N d^{2N+2}\) of the dense form. The base class raises
NotImplementedErrorhere; the partial trace is a 0/1 incidence matrix, so it is worth providing.- Return type:
SparseArray
- to_cvxpy()[source]#
Return the QOT constraints as a list of per-constraint sparse matrices.
This adapts the dense Kronecker construction of the QOT-to-SDP proof (th. 3.1 of https://arxiv.org/abs/2105.06922) into standard-form constraint matrices for a general SDP solver such as the CVXPY backend, whose equalities read \(\operatorname{Re}\operatorname{Tr}[A_i \Gamma] = b_i\).
Constraint
i = (k, \alpha)is the Hermitian matrix\[A_i = \mathcal{A}^\dagger(H_\alpha^{(k)}) = I \otimes \cdots \otimes H_\alpha \otimes \cdots \otimes I,\]where \(H_\alpha\) ranges over the real-coordinate Hermitian generators of the
k-thd x dblock (see_herm_generators()). Because each \(H_\alpha\) is Hermitian, the matching right-hand side \(b_i = \operatorname{Tr}[H_\alpha^{(k)} \gamma_k]\) is real even though the marginals \(\gamma_k\) are complex Hermitian – naively flattening the marginal entries would instead give a complexb. The matchingbis produced byrhs_to_cvxpy()and the dual is reassembled bydual_from_cvxpy().The returned list has
m = N d(d+1)/2entries for a real context andm = N d^2for a complex one, each a sparse(d^N, d^N)matrix so a solver can formtrace(A_i @ Gamma)directly. Each generator embeds overd^{N-1}configurations of the traced-out subsystems, so storage stays sparse rather than the densem d^{2N}.- Return type:
list[SparseArray]
- dual_from_cvxpy(y)[source]#
Reassemble marginal dual blocks from per-constraint scalar duals.
Inverse of the row layout of
to_cvxpy()/rhs_to_cvxpy(): constrainti = (k, \alpha)reads off generator \(H_\alpha\) of blockk, so the marginal dual is \(U_k = \sum_\alpha y_{(k,\alpha)} H_\alpha\), a Hermitiand x dblock. The result is the stacked(N, d, d)codomain element. The caller suppliesyalready carrying the intended dual sign.- Parameters:
y (DenseArray)
- Return type:
DenseArray
- rhs_to_cvxpy(rhs)[source]#
Return the real right-hand side
bmatchingto_cvxpy().rhsis the stacked codomain array(N, d, d)of Hermitian one-body marginals \(\gamma_k\). The returned real vectorbhas lengthm(the number of matrices fromto_cvxpy()) with \(b_{(k,\alpha)} = \operatorname{Tr}[H_\alpha^{(k)} \gamma_k]\), laid out in the same generator order so that the SDP equality \(\operatorname{Re}\operatorname{Tr}[A_i \Gamma] = b_i\) holds.Equivalently \(b = \operatorname{Re}\langle H_\alpha, \gamma_k\rangle\) read off the generator matrices, which is how it is evaluated: the per-entry
re/imselection of_herm_generators()is exactly what tracing against the generator performs.- Parameters:
rhs (DenseArray)
- Return type:
DenseArray
- property A: Any#
Native numerical representation of this operator.
Concrete subclasses may choose the representation that best matches their storage model: for example, dense operators return a dense array while sparse operators return their sparse matrix. Matrix-free or lazy operators generally do not have such a representation and should leave this property unimplemented. Use
to_dense()when a dense tensor materialization is explicitly required.
- property H: LinOp#
Hermitian-adjoint view of this linear operator.
- Returns:
Adjoint view satisfying \(\langle A x, y\rangle_Y = \langle x, A^* y\rangle_X\).
- Return type:
LinOp
- adjoint()#
Return the Hermitian-adjoint view of this linear operator.
- Return type:
LinOp
- adjoint_apply(y)#
Apply the adjoint of this linear operator to
y.- Parameters:
y (Any)
- Return type:
Any
- assert_codomain(y)#
Raise if
yis not in the codomain.- Parameters:
y (Any)
- Return type:
None
- assert_domain(x)#
Raise if
xis not in the domain.- Parameters:
x (Any)
- Return type:
None
- property check_level: Literal['none', 'cheap', 'standard', 'strict']#
Return this object’s runtime validation level.
- property codomain: Codomain#
Codomain space of this linear operator.
- convert(new_ctx=None)#
Return this object represented in
new_ctx.- Parameters:
new_ctx (Context | BackendFamily | str | None)
- Return type:
Self
- property ctx: Context#
Return the execution context bound to this object.
- property domain: Domain#
Domain space of this linear operator.
- property dtype: Any#
Return the default dtype associated with this object’s context.
- classmethod from_linop(op)#
- Parameters:
op (LinOp)
- Return type:
- fuse(*, materialize=False)#
Return an equivalent operator with fusible sub-expressions multiplied out.
Tier-2 lazy-algebra simplification ([ADR-021](021_lazy_operator_algebra_and_simplification.md)): collapse each maximal subtree of densely-fusible operators into a single materialized operator — for example, a composition of dense operators becomes one
DenseLinOpholding the matrix product \(M_A M_B\) — while leaving matrix-free and other non-materializable leaves intact.This is an explicit, opt-in materialization. The result is mathematically equal to
selfbut only within floating-point rounding: fusing reassociates the arithmetic (multiplying matrices then applying differs from applying in sequence at the ulp level), so equality holds up to tolerance, not bit-for-bit. The fused operator preserves the domain, codomain, context, and scalar-field/dtype identity. A leaf operator returns itself.- Parameters:
materialize (bool, optional) – With the default
False, a matrix-free operand ([ADR-008](008_linop_subclasses.md)) is never densified: it remains a lazy leaf and only breaks a fusible run. WithTruethe caller explicitly accepts giving up the matrix-free contract: a matrix-free operand is densified into aDenseLinOp(via itsto_densebasis probe, which may be expensive), allowing the enclosing expression to collapse to a single dense operator.- Returns:
A fused operator with the same action as
self(up to rounding).- Return type:
LinOp
- is_hermitian()#
Return whether this operator is structurally Hermitian when known.
- Returns:
TrueorFalsewhen the subclass can verify the structure cheaply, otherwiseNonefor unknown or matrix-free operators.- Return type:
bool | None
- property ops: BackendOps#
Return backend operations associated with this object’s context.
- rvapply(ys)#
Apply the adjoint over a leading batch axis. Input must have shape
(N,) + codomain.shape; usemoveaxisfor other layouts.- Parameters:
ys (Any)
- Return type:
Any
- to_dense()#
Materialize this operator as a dense backend array.
The returned array has shape
self.codomain.shape + self.domain.shape. The default implementation is intended for small problems, debugging, and tests. It materializes the full coordinate matrix, so subclasses that already store a dense or sparse matrix should override this method for efficiency.- Return type:
Any
- to_matrix()#
Materialize this operator as a 2D dense coordinate matrix.
The returned array has shape
(prod(self.codomain.shape), prod(self.domain.shape)). The default implementation builds a batch of standard basis vectors and callsvapply()once. If a space cannot batch-flatten or batch-unflatten its representation, it falls back to a safe Python loop. This method is for small/testing use; concrete storage-backed subclasses should override it when they can expose a matrix directly.- Return type:
Any
- vapply(xs)#
Apply over a leading batch axis. Input must have shape
(N,) + domain.shape; usemoveaxisfor other layouts.- Parameters:
xs (Any)
- Return type:
Any
- sdplab.special.qot.solve_qot_dual(qot, solver='MOSEK', verbose=False, *args, **kwargs)[source]#
Solve the QOT dual SDP.
For the QOT constraint operator \(\mathcal{A}\), the primal coupling satisfies \(\Gamma \in \operatorname{dom}(\mathcal{A}) = \operatorname{Herm}(d^N)\) and the marginal data satisfy \(\gamma = (\gamma_0, \ldots, \gamma_{N-1}) \in \operatorname{cod}(\mathcal{A}) = \operatorname{Herm}(d)^N\). The dual problem is
\[\max_{U \in \operatorname{cod}(\mathcal{A})}\quad \sum_k \operatorname{Tr}[U_k \gamma_k] \quad \text{s.t.}\quad \mathcal{A}^\dagger U \preceq C.\]Here \(C \in \operatorname{dom}(\mathcal{A})\) is the cost matrix and \(U = (U_0, \ldots, U_{N-1})\) is the block dual variable. The adjoint constraint is
\[\mathcal{A}^\dagger U = U_0 \oplus \cdots \oplus U_{N-1} = \sum_k I \otimes \cdots \otimes U_k \otimes \cdots \otimes I \preceq C,\]equivalently \(C - \mathcal{A}^\dagger U \succeq 0\).
- Returns:
A pair
(primal, dual). The primal value stores the coupling \(\Gamma\), represented as the positive-semidefinite multiplier for \(C - \mathcal{A}^\dagger U \succeq 0\). The dual value stores the optimized blocks \(U_k\) in \(\operatorname{cod}(\mathcal{A})\).- Parameters:
qot (SDPProblem)
solver (str)
verbose (bool)
- Return type:
tuple[Any, Any]