Entropy (partial trace, von Neumann entropy, mutual information)¶
A qubit entangled with another one has no well-defined state of its own -- only the
pair has a pure state. Measuring "how mixed" that single qubit looks on its own is
exactly how entanglement shows up numerically: a qubit maximally entangled with its
partner looks maximally random in isolation, even though the two-qubit system as a
whole is perfectly pure. This module measures that: reducing a multi-qubit state down
to a subsystem (partial_trace), quantifying how mixed the result is
(von_neumann_entropy), and how much two subsystems know about each other
(mutual_information).
Step 1. Reduce a Bell state down to one qubit¶
import numpy as np
import dense_evolution as de
from dense_evolution.physics.entropy import partial_trace
qasm = 'OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; h q[0]; cx q[0],q[1];'
circuit = de.QASMParser().parse(qasm)
sim = de.DenseSVSimulator(2)
sim.run_circuit_jit(circuit.to_tuples())
sv = sim.get_statevector()
rho0 = partial_trace(sv, n_qubits=2, keep_qubits=[0])
np.round(rho0, 4)
sv is the same Bell state built on the Simulator page. partial_trace
"forgets" qubit 1 and returns qubit 0's own 2x2 density matrix -- I/2, maximally
mixed. That's the signature of entanglement: qubit 0 alone looks like a fair coin flip,
even though the full 2-qubit state is perfectly pure and deterministic.
Step 2. How mixed is that, exactly?¶
von_neumann_entropy is -Tr(rho * log(rho)), the quantum generalization of Shannon
entropy -- 0 for a pure state, and ln(2) = 0.6931... (matching the printed value)
for a single qubit's maximum possible mixedness. Step 1's rho0 hit that ceiling
exactly, which is only possible when qubit 0 is maximally entangled with the rest of
the system.
Step 3. Do the two qubits know about each other?¶
from dense_evolution.physics.entropy import mutual_information
mutual_information(sv, n_qubits=2, qubits_a=[0], qubits_b=[1])
mutual_information(state, n_qubits, qubits_a, qubits_b) measures correlation between
two subsystems directly from the full state -- no need to call partial_trace on each
side yourself first. 2*ln(2) = 1.3862... is the maximum two qubits can share, and a
Bell pair hits it exactly. This is the tool for a case a single-qubit expectation value
structurally cannot catch: a qubit entangled in a Bell pair (or any subsystem
maximally mixed on its own) has <Z> = 0 regardless of what happened to its partner --
the no-signaling theorem, not a measurement limitation -- but its mutual information
with that partner is very much nonzero.
Step 4. Fitting a whole entropy curve to CFT theory¶
from dense_evolution.physics.entropy import central_charge
N = 20
Ls = [2, 4, 6, 8, 10]
S = [(0.5 / 6.0) * np.log((2 * N / np.pi) * np.sin(np.pi * L / N)) + 0.3 for L in Ls]
central_charge(Ls, S, n_qubits=N)
central_charge(Ls, S, n_qubits) fits an already-measured entanglement-entropy curve
S(L) (entropy of the first L qubits, at several subsystem sizes L) to the
Calabrese-Cardy CFT prediction and returns (c, r_squared) -- backend-agnostic, it
doesn't compute entropies itself, only fits a curve you already measured (e.g. via
Steps 1-2 above, repeated at each L). S here is built directly from the
Calabrese-Cardy formula at the known value c=0.5, so a perfect round-trip
(r_squared=1.0, recovered c matching to 1e-15) confirms the fit itself is
correct -- a real critical spin chain's measured entropies won't be this clean, but the
fit machinery is the same either way.
Details¶
Indexing convention: qubit 0 is the most significant bit of the basis-state index
throughout this module, matching observables/
pauli_hamiltonian_to_matrix -- not the little-endian convention some other libraries
use. The only prior partial trace in this package before this module existed
(dashboard_core/state_visuals.py's private, single-qubit-only _reduced_density_matrix)
used the opposite convention -- do not reuse that helper here, it would silently
transpose which qubits get traced out.
A high r_squared alone doesn't mean the extracted c is trustworthy:
Dense-Evolution-Discovery Experiment 36
found that fitting a real critical Ising chain's entropy curve at a finite-size
susceptibility-peak pseudo-critical point (instead of the true self-dual CFT point)
gives a deceptively clean fit (r_squared=0.999997) to a wrong answer -- c off by
roughly 2x from the known Ising value c=1/2. Fitting at the correct critical point
recovered c=0.565, much closer to the true 0.5.
entropy ¶
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.
partial_trace ¶
Reduced density matrix on keep_qubits, tracing out the rest.
MSB-first (qubit 0 = most significant bit), this package's own convention everywhere else -- NOT the same as dashboard_core. state_visuals._reduced_density_matrix, which is deliberately little-endian (Qiskit's convention) for its own Bloch-sphere/Q-sphere display consumers. Two genuinely different conventions for two different consumers, not an accidental divergence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
ndarray
|
A pure statevector of length 2**n_qubits. |
required |
n_qubits
|
int
|
|
required |
keep_qubits
|
list[int]
|
Qubit indices (this package's MSB-first convention) to keep. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Density matrix of shape (2len(keep_qubits), 2len(keep_qubits)). |
Source code in dense_evolution/physics/entropy.py
von_neumann_entropy ¶
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.
Natural log (nats), NOT log2 (bits) -- unlike this package's other entropy-family quantities (magic_entropy, kl_divergence, sandwiched_renyi_divergence, stabilizer_renyi_entropy), which all use log2 and say so explicitly. mutual_information/central_charge below inherit this same nats convention.
Source code in dense_evolution/physics/entropy.py
mutual_information ¶
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. In nats (natural log), same as von_neumann_entropy above -- see its docstring for how this differs from this package's other, log2-based entropy quantities.
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
Source code in dense_evolution/physics/entropy.py
central_charge ¶
Fit an open-chain entanglement entropy curve S(L) to the Calabrese-
Cardy CFT prediction S(L) = (c/6)ln[(2N/pi)sin(pi*L/N)] + const
(Calabrese & Cardy, J. Stat. Mech. 2004, P06002, eq. 4/19 combined via
the standard open-chain doubling trick) and return (c, r_squared).
The fit itself is in the natural-log (nats) convention shown above --
S must be too (von_neumann_entropy's own convention) for the fitted
c to come out right; a log2-based S would scale it off by ln(2).
Backend-agnostic: S can come from any source (exact diagonalization
via partial_trace/von_neumann_entropy on this package's own
DenseSVSimulator, MPSSimulator, Chunk, or elsewhere) -- this
doesn't compute the entropy itself, only fits an already-measured
curve. Meant as a benchmark diagnostic: does a given backend/
truncation scheme preserve genuine critical CFT scaling, and with
what effective central charge?
A high r_squared alone does NOT mean the extracted c is trustworthy -- Dense-Evolution-Discovery Experiment 36 found fitting at a finite-size pseudo-critical point (a susceptibility peak, not the true CFT point) gives a deceptively clean fit (r_squared=0.999997) to a wrong answer (c off by 2x). Only trust this near a genuine, independently-verified critical point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
Ls
|
array-like of int
|
Subsystem sizes, each counted from one physical boundary of an
open chain of |
required |
S
|
array-like of float
|
Entanglement entropy at each L in |
required |
n_qubits
|
int
|
Total open-chain length N. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
c |
float
|
Extracted central charge (theory: 0.5 for Ising, 1.0 for a free boson/XX chain, ...). |
r_squared |
float
|
Fit quality, in [0, 1] for a sane fit (can go negative for a pathological fit worse than the mean). |
Source code in dense_evolution/physics/entropy.py
See also: fermions and trotter, the other two
modules promoted alongside this one from a real traversable-wormhole-inspired quantum
teleportation reproduction (arXiv:2604.10090) -- see
Dense-Evolution-Discovery
for the real experiments, including a control run confirming mutual_information
correctly returns exactly 0 when two subsystems are structurally disconnected.