Tatopenn's picture
Sync mirror with GitHub main (v8.1.62): subpackage refactor, mitigation promotions, MCP server refactor
2a87ede verified
Raw History Blame Contribute Delete
3.97 kB
"""
Multi-qubit partial trace, von Neumann entropy, and mutual information.
Nothing like this existed anywhere in the package before: the only prior
partial trace (dashboard_core/state_visuals.py's private
`_reduced_density_matrix`) is single-qubit-only and uses the *opposite*,
little-endian convention (qubit 0 = least significant bit). Everything
here uses this package's own convention instead, matching
dense_evolution.observables/pauli_hamiltonian_to_matrix: qubit 0 is the
*most* significant bit of the basis-state index. Do not mix the two --
reusing dashboard_core's helper here would silently transpose which
qubits get traced out.
Originated in research/wormhole_syk.py (a traversable-wormhole-inspired
quantum teleportation reproduction) -- promoted here because these are
generic quantum-information utilities, not specific to that experiment.
Any state can have a subsystem's reduced density matrix, entropy, or the
mutual information between two subsystems computed with these three
functions; the wormhole work needed all three because the physically
meaningful readout there (a message injected into one system showing up
correlated with a reference qubit) is *not* visible in any single-qubit
expectation value -- see mutual_information's docstring.
"""
import numpy as np
__all__ = ['partial_trace', 'von_neumann_entropy', 'mutual_information']
def partial_trace(state, n_qubits, keep_qubits):
"""Reduced density matrix on `keep_qubits`, tracing out the rest.
Parameters
----------
state : np.ndarray
A pure statevector of length 2**n_qubits.
n_qubits : int
keep_qubits : list[int]
Qubit indices (this package's MSB-first convention) to keep.
Returns
-------
np.ndarray
Density matrix of shape (2**len(keep_qubits), 2**len(keep_qubits)).
"""
keep_qubits = sorted(keep_qubits)
trace_qubits = [q for q in range(n_qubits) if q not in keep_qubits]
psi = np.transpose(np.asarray(state).reshape([2] * n_qubits), keep_qubits + trace_qubits)
keep_dim, trace_dim = 2 ** len(keep_qubits), 2 ** len(trace_qubits)
psi = psi.reshape(keep_dim, trace_dim)
return psi @ psi.conj().T
def von_neumann_entropy(rho):
"""S(rho) = -Tr(rho log rho), computed from rho's eigenvalues. Nearly-
zero eigenvalues (which a numerically pure/near-pure state produces,
and which are mathematically forbidden from being exactly negative
for a real density matrix but can land at a tiny negative float) are
clipped before the log rather than raising or propagating a NaN."""
eigs = np.clip(np.linalg.eigvalsh(rho).real, 1e-14, None)
return float(-np.sum(eigs * np.log(eigs)))
def mutual_information(state, n_qubits, qubits_a, qubits_b):
"""I(A:B) = S(A) + S(B) - S(A union B), the standard quantum mutual
information between two disjoint subsystems of a pure global state.
Why this and not a single-qubit expectation value: a qubit entangled
in a Bell pair (or more generally, maximally mixed on its own) has a
marginal <Z> of exactly 0 regardless of what operation was applied to
its partner -- this is the no-signaling theorem, not a measurement
limitation, and no amount of clever circuit design around a
single-qubit readout can get around it. Mutual information *can*
reveal correlations a marginal expectation value structurally cannot,
because it depends on the *joint* state of A and B, not either one
alone. Verified in tests/unit/test_entropy.py against the exact textbook
value for a Bell pair (I = 2*ln(2), maximal) and a GHZ state.
"""
s_a = von_neumann_entropy(partial_trace(state, n_qubits, qubits_a))
s_b = von_neumann_entropy(partial_trace(state, n_qubits, qubits_b))
s_ab = von_neumann_entropy(partial_trace(state, n_qubits, list(qubits_a) + list(qubits_b)))
return s_a + s_b - s_ab