Simulation of Clifford Circuits¶
Overview¶
Simulating quantum circuits on a classical computer is fundamentally hard. The memory required to store a quantum state vector for \(n\) qubits grows as \(2^n\), an exponential scaling that quickly becomes intractable. However, a special and highly important subclass of quantum circuits, known as Clifford circuits, can be simulated efficiently on a classical computer.
The method used for this is the stabilizer formalism. Instead of tracking the \(2^n\) amplitudes of the state vector, we track a small set of operators—the stabilizers—that leave the quantum state unchanged. For an \(n\)-qubit system, we only need to track \(n\) such operators, and the updates for each Clifford gate can be performed in polynomial time.
This tutorial will introduce how to use tensorcircuit.StabilizerCircuit, which leverages the powerful stim library as its backend, to perform efficient Clifford circuit simulations. We will cover:
Creating and Manipulating a Stabilizer Circuit: How to build a circuit, apply gates, and inspect its state.
Understanding the Stabilizer Tableau: Using
StabilizerCircuitmethods to view the underlying tableau representation.Handling Measurements and Post-selection: Demonstrating how
StabilizerCircuitmanages these complex operations.Application: We will then apply these concepts to our main problem: calculating the entanglement entropy of a Clifford circuit with mid-circuit measurements, comparing its performance and results with standard state-vector simulation.
Setup¶
[1]:
import numpy as np
import tensorcircuit as tc
# Set a random seed for reproducibility
np.random.seed(0)
Creating and Manipulating a Stabilizer Circuit¶
The tc.StabilizerCircuit class provides an interface very similar to the standard tc.Circuit, but it is restricted to Clifford gates and uses a stim.TableauSimulator internally. This allows for polynomial-time simulation.
Let’s start by creating a simple StabilizerCircuit.
[4]:
n = 2
# Initialize a stabilizer circuit for 2 qubits
sc = tc.StabilizerCircuit(n)
print(f"Number of qubits: {sc._nqubits}")
Number of qubits: 2
We can apply Clifford gates just like with a normal circuit. Let’s create a Bell state \(|\Phi^+\rangle = \frac{1}{\sqrt{2}}(|00\rangle + |11\rangle)\).
[5]:
# Apply a Hadamard gate to the first qubit
sc.h(0)
# Apply a CNOT gate with qubit 0 as control and qubit 1 as target
sc.cnot(0, 1)
print("Circuit constructed. Let's inspect its properties.")
Circuit constructed. Let's inspect its properties.
Understanding the Stabilizer Tableau¶
The core of a stabilizer simulation is the stabilizer tableau. We can access this directly from our StabilizerCircuit object.
[8]:
# Get the current stabilizer tableau
# This represents the final state of the circuit
final_tableau = sc.current_tableau()
print("Tableau for the Bell state |Φ+>:")
print(final_tableau)
Tableau for the Bell state |Φ+>:
+-xz-xz-
| ++ ++
| ZX _Z
| _X XZ
The z_output(i) method of a stim.Tableau object tells us what the initial \(Z_i\) operator evolves into under the circuit’s action. These evolved operators are the stabilizers of the final state.
[9]:
print("Stabilizers for the Bell state |Φ+>:")
for i in range(n):
# For a state |psi> = U|0...0>, the stabilizers are U Z_i U_dag
# which is what tableau.z_output(i) returns.
print(f"S_{i+1}: {final_tableau.z_output(i)}")
Stabilizers for the Bell state |Φ+>:
S_1: +XX
S_2: +ZZ
As expected, the stabilizers for the Bell state are correctly identified as X_0 X_1 and Z_0 Z_1.
We can also get the full state vector, though this operation is computationally expensive and defeats the purpose of stabilizer simulation for large systems. It is, however, useful for verifying results on small circuits.
[11]:
# Get the state vector from the tableau
state_vector = sc.state()
print("State vector for the Bell state:")
print(np.round(state_vector, 3))
State vector for the Bell state:
[0.707+0.j 0. +0.j 0. +0.j 0.707+0.j]
The result is \(\frac{1}{\sqrt{2}}(|00\rangle + |11\rangle)\), which is correct.
Handling Measurements and Post-selection¶
StabilizerCircuit provides methods to handle both probabilistic and deterministic (post-selected) measurements.
Probabilistic Measurement¶
The measure method returns a random outcome based on the state’s probabilities. For a stabilizer state, if a measurement operator anti-commutes with a stabilizer, the outcome is random (0 or 1 with 50% probability). If it commutes, the outcome is deterministic.
[2]:
sc_plus = tc.StabilizerCircuit(2)
sc_plus.h(0)
# The stabilizer is X_0. Z_0 anti-commutes with X_0.
print(
"The first stabilizer for |+0> state:", sc_plus.current_tableau().z_output(0)
) # This should show an X
# Measure multiple times to see the randomness
print("Measuring a qubit in the |+> state:")
outcomes = [sc_plus.measure(0) for _ in range(10)]
print(f"10 measurement outcomes: {outcomes}")
# Note: Since measure does not collapse the state in StabilizerCircuit, each measurement is independent.
# For a collapsing measurement, use cond_measure.
outcomes = [sc_plus.measure(0, 1, with_prob=True) for _ in range(10)]
print(outcomes)
The first stabilizer for |+0> state: +X
Measuring a qubit in the |+> state:
10 measurement outcomes: [[True], [False], [False], [False], [False], [False], [False], [True], [False], [True]]
[([False, False], 0.5), ([False, False], 0.5), ([False, False], 0.5), ([False, False], 0.5), ([True, False], 0.5), ([True, False], 0.5), ([True, False], 0.5), ([False, False], 0.5), ([False, False], 0.5), ([True, False], 0.5)]
Post-selection¶
The mid_measurement (or post_selection) method allows us to project the state onto a specific measurement outcome. This is a non-unitary operation that deterministically collapses the state and updates the stabilizer tableau accordingly. This is crucial for our main application.
[4]:
# Create a Bell state
sc_bell = tc.StabilizerCircuit(2)
sc_bell.h(0)
sc_bell.cnot(0, 1)
print("Original Bell state stabilizers:")
for i in range(2):
print(sc_bell.current_tableau().z_output(i))
# Now, perform a mid-measurement on qubit 0 and keep the '0' outcome
sc_bell.post_select(0, keep=0)
print("\nStabilizers after post-selecting qubit 0 to be |0>:")
for i in range(2):
print(sc_bell.current_tableau().z_output(i))
# Let's check the final state vector
final_state = sc_bell.state()
print(f"\nFinal state vector: {np.round(final_state, 3)}")
Original Bell state stabilizers:
+XX
+ZZ
Stabilizers after post-selecting qubit 0 to be |0>:
+Z_
+ZZ
Final state vector: [1.+0.j 0.+0.j 0.+0.j 0.+0.j]
Application: Entanglement Entropy with Mid-Circuit Measurements¶
Now, let’s use tc.StabilizerCircuit to solve the problem efficiently. We will create a random Clifford circuit with mid-circuit measurements and then calculate the entanglement entropy directly using a built-in method.
1. Generating the Circuit¶
We’ll write a function that builds a random Clifford circuit using tc.StabilizerCircuit.
[12]:
def random_stabilizer_circuit_with_mid_measurement(num_qubits, depth):
"""Generates a random Clifford circuit using tc.StabilizerCircuit."""
sc = tc.StabilizerCircuit(num_qubits)
for _ in range(depth):
# Apply random gates to random pairs
for j in range(num_qubits - 1):
sc.random_gate(j, j + 1)
# With 20% probability, perform a mid-circuit measurement
for j in range(num_qubits - 1):
if np.random.uniform() < 0.2:
sc.cond_measure(j)
return sc
[13]:
num_qubits = 12
depth = 12
cut = [i for i in range(num_qubits // 2)]
# Generate the stabilizer circuit
stabilizer_circuit = random_stabilizer_circuit_with_mid_measurement(num_qubits, depth)
2. Calculating Entropy from the Stabilizer Circuit¶
With the StabilizerCircuit object, calculating the entanglement entropy is a one-liner. The entanglement_entropy method implements the rank-based formula, providing a highly efficient calculation.
[15]:
entropy = stabilizer_circuit.entanglement_entropy(cut)
print(f"Entanglement Entropy (from StabilizerCircuit): {entropy}")
Entanglement Entropy (from StabilizerCircuit): 2.0794415416798357
Final Conclusion¶
This tutorial demonstrates the power and convenience of tensorcircuit.StabilizerCircuit. By providing a user-friendly interface that mirrors tc.Circuit while using the highly optimized stim library as its backend, it enables efficient simulation of large-scale Clifford circuits. Operations that are computationally prohibitive with state-vector methods, like calculating entanglement entropy in circuits with dozens or hundreds of qubits, become feasible. This makes
StabilizerCircuit an essential tool for research in quantum error correction, fault tolerance, and any domain involving Clifford dynamics.