Simulating Quantum Channels
A quantum-channel calculation should answer two questions: what happens to the state, and is the proposed map physically allowed even when the system is entangled with a reference? This example computes both. It uses small complex matrices so that you can compare every numerical result with an analytic formula.
Helpful background. Kraus operators describe channel action; the Choi matrix tests complete positivity. The code needs Python 3 and NumPy; paste the complete block into a notebook cell or a .py file.
Channels as linear maps on matrices
Section titled “Channels as linear maps on matrices”Use the basis , with the ground state for amplitude damping. A Kraus map acts on any matrix by
It preserves trace when . For damping probability , the operators are
The distinction between a general matrix and a density matrix matters when building a Choi matrix. For example, the linear depolarizing map is
The trace factor cannot be omitted: off-diagonal basis matrices have trace zero. Substituting indiscriminately would produce the wrong Choi matrix.
We use output first, reference second:
For a channel, and . Its trace is , not ; is the associated normalized bipartite state.
Compute an output state and its Choi matrix
Section titled “Compute an output state and its Choi matrix”The input tests complex conjugation as well as populations. At the expected output is
Here @ means matrix multiplication, .conj().T means the adjoint, and np.kron forms a tensor product.
import numpy as np
I = np.eye(2, dtype=complex)tol = 1e-12p = 0.36Ks = [np.diag([1, np.sqrt(1-p)]), np.array([[0, np.sqrt(p)], [0, 0]])]
def apply(Ks, A): return sum((K @ A @ K.conj().T for K in Ks), np.zeros_like(A, dtype=complex))
def choi(phi): J = np.zeros((4, 4), dtype=complex) for i in range(2): for j in range(2): E = np.zeros((2, 2), dtype=complex) E[i, j] = 1 J += np.kron(phi(E), E) return J
def trace_output(J): # Indices are output-row, input-row, output-column, input-column. return np.einsum('aiaj->ij', J.reshape(2, 2, 2, 2))
rho_y = np.array([[0.5, -0.5j], [0.5j, 0.5]])out = apply(Ks, rho_y)J = choi(lambda A: apply(Ks, A))assert np.linalg.norm(sum(K.conj().T @ K for K in Ks)-I) < tolassert np.linalg.norm(J-J.conj().T) < tolassert np.linalg.eigvalsh(J).min() >= -tolassert np.linalg.norm(trace_output(J)-I) < tolassert np.allclose(out, [[0.68, -0.4j], [0.4j, 0.32]])assert np.allclose(np.linalg.eigvalsh(J), [0, 0, p, 2-p])print('output state:', out, sep='\n')print('Choi eigenvalues:', np.linalg.eigvalsh(J))
lam = -0.4bad_J = choi(lambda A: lam*A + (1-lam)*np.trace(A)*I/2)assert np.allclose(np.linalg.eigvalsh(bad_J), [-0.1, 0.7, 0.7, 0.7])print('invalid depolarizing Choi:', np.linalg.eigvalsh(bad_J))
# Apply a Hadamard rotation first, then damping.H = np.array([[1, 1], [1, -1]]) / np.sqrt(2)composed = apply([K @ H for K in Ks], rho_y)sequential = apply(Ks, H @ rho_y @ H.conj().T)assert np.allclose(composed, sequential)assert not np.allclose(composed, H @ out @ H.conj().T)The valid Choi eigenvalues are up to roundoff. The deliberately invalid depolarizing map has eigenvalues . It nevertheless sends every isolated qubit state to a valid state, because it shrinks its Bloch vector by . Only the reference-system test reveals the failure of complete positivity.
Check Hermiticity before calling eigvalsh: that routine assumes a Hermitian matrix and reads only one triangle. A mistaken non-Hermitian matrix must not be silently interpreted as Hermitian. For these small exact formulas, is a reasonable absolute tolerance; a genuine eigenvalue of is not roundoff.
Try other channels
Section titled “Try other channels”Replace Ks by for a unitary channel, or by
for dephasing with . Compare the outputs for , , , , and . The last state distinguishes unital noise from amplitude damping. The four matrices and span the qubit operator space, so agreement on this set determines a linear map, apart from numerical error.
For composition, matrix order follows operation order: if acts first and second, the Kraus operators are . The final code lines check this and demonstrate that reversing the physical order generally changes the answer. Continue with Bloch-vector noise models to see the same maps geometrically.
Exercises
Section titled “Exercises”Completeness. Verify the amplitude-damping completeness relation by hand.
Solution
and . Their sum is .
Unitality. Calculate the image of at .
Solution
The result is , so amplitude damping is not unital. Identity, unitary, dephasing, and depolarizing channels are unital.
Complete-positivity boundary. Derive the allowed depolarizing interval from its Choi eigenvalues.
Solution
The eigenvalue along the maximally entangled vector is ; the three orthogonal eigenvalues are . Requiring both to be nonnegative gives .
A wrong tensor-factor trace. What does tracing the reference factor of the damping Choi matrix give?
Solution
It gives , which is not for . This is a unitality test, not the trace-preservation test. The nonunital example makes a swapped partial trace detectable.
References
Section titled “References”- C. R. Harris et al., “Array programming with NumPy,” Nature 585, 357–362 (2020). doi:10.1038/s41586-020-2649-2.
- K. Kraus, States, Effects, and Operations: Fundamental Notions of Quantum Theory, Springer (1983).
- NumPy Developers, “numpy.linalg.eigvalsh”, NumPy documentation (accessed 13 October 2026).
- J. Watrous, The Theory of Quantum Information, Cambridge University Press (2018). Author’s manuscript and errata.