{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Simulation of Clifford Circuits" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Overview\n", "\n", "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.\n", "\n", "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.\n", "\n", "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:\n", "\n", "1. **Creating and Manipulating a Stabilizer Circuit**: How to build a circuit, apply gates, and inspect its state.\n", "2. **Understanding the Stabilizer Tableau**: Using `StabilizerCircuit` methods to view the underlying tableau representation.\n", "3. **Handling Measurements and Post-selection**: Demonstrating how `StabilizerCircuit` manages these complex operations.\n", "4. **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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Setup" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import tensorcircuit as tc\n", "\n", "# Set a random seed for reproducibility\n", "np.random.seed(0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Creating and Manipulating a Stabilizer Circuit\n", "\n", "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.\n", "\n", "Let's start by creating a simple `StabilizerCircuit`." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Number of qubits: 2\n" ] } ], "source": [ "n = 2\n", "# Initialize a stabilizer circuit for 2 qubits\n", "sc = tc.StabilizerCircuit(n)\n", "print(f\"Number of qubits: {sc._nqubits}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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)$." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Circuit constructed. Let's inspect its properties.\n" ] } ], "source": [ "# Apply a Hadamard gate to the first qubit\n", "sc.h(0)\n", "# Apply a CNOT gate with qubit 0 as control and qubit 1 as target\n", "sc.cnot(0, 1)\n", "\n", "print(\"Circuit constructed. Let's inspect its properties.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Understanding the Stabilizer Tableau\n", "\n", "The core of a stabilizer simulation is the **stabilizer tableau**. We can access this directly from our `StabilizerCircuit` object." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Tableau for the Bell state |Φ+>:\n", "+-xz-xz-\n", "| ++ ++\n", "| ZX _Z\n", "| _X XZ\n" ] } ], "source": [ "# Get the current stabilizer tableau\n", "# This represents the final state of the circuit\n", "final_tableau = sc.current_tableau()\n", "\n", "print(\"Tableau for the Bell state |Φ+>:\")\n", "print(final_tableau)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Stabilizers for the Bell state |Φ+>:\n", "S_1: +XX\n", "S_2: +ZZ\n" ] } ], "source": [ "print(\"Stabilizers for the Bell state |Φ+>:\")\n", "for i in range(n):\n", " # For a state |psi> = U|0...0>, the stabilizers are U Z_i U_dag\n", " # which is what tableau.z_output(i) returns.\n", " print(f\"S_{i+1}: {final_tableau.z_output(i)}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As expected, the stabilizers for the Bell state are correctly identified as `X_0 X_1` and `Z_0 Z_1`.\n", "\n", "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.\n" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "State vector for the Bell state:\n", "[0.707+0.j 0. +0.j 0. +0.j 0.707+0.j]\n" ] } ], "source": [ "# Get the state vector from the tableau\n", "state_vector = sc.state()\n", "\n", "print(\"State vector for the Bell state:\")\n", "print(np.round(state_vector, 3))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The result is $\\frac{1}{\\sqrt{2}}(|00\\rangle + |11\\rangle)$, which is correct.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Handling Measurements and Post-selection\n", "\n", "`StabilizerCircuit` provides methods to handle both probabilistic and deterministic (post-selected) measurements.\n", "\n", "### Probabilistic Measurement\n", "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.\n", "\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The first stabilizer for |+0> state: +X\n", "Measuring a qubit in the |+> state:\n", "10 measurement outcomes: [[True], [False], [False], [False], [False], [False], [False], [True], [False], [True]]\n", "[([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)]\n" ] } ], "source": [ "sc_plus = tc.StabilizerCircuit(2)\n", "sc_plus.h(0)\n", "\n", "# The stabilizer is X_0. Z_0 anti-commutes with X_0.\n", "print(\n", " \"The first stabilizer for |+0> state:\", sc_plus.current_tableau().z_output(0)\n", ") # This should show an X\n", "\n", "# Measure multiple times to see the randomness\n", "print(\"Measuring a qubit in the |+> state:\")\n", "outcomes = [sc_plus.measure(0) for _ in range(10)]\n", "print(f\"10 measurement outcomes: {outcomes}\")\n", "# Note: Since measure does not collapse the state in StabilizerCircuit, each measurement is independent.\n", "# For a collapsing measurement, use cond_measure.\n", "outcomes = [sc_plus.measure(0, 1, with_prob=True) for _ in range(10)]\n", "print(outcomes)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "### Post-selection\n", "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." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Original Bell state stabilizers:\n", "+XX\n", "+ZZ\n", "\n", "Stabilizers after post-selecting qubit 0 to be |0>:\n", "+Z_\n", "+ZZ\n", "\n", "Final state vector: [1.+0.j 0.+0.j 0.+0.j 0.+0.j]\n" ] } ], "source": [ "# Create a Bell state\n", "sc_bell = tc.StabilizerCircuit(2)\n", "sc_bell.h(0)\n", "sc_bell.cnot(0, 1)\n", "\n", "print(\"Original Bell state stabilizers:\")\n", "for i in range(2):\n", " print(sc_bell.current_tableau().z_output(i))\n", "\n", "# Now, perform a mid-measurement on qubit 0 and keep the '0' outcome\n", "sc_bell.post_select(0, keep=0)\n", "\n", "print(\"\\nStabilizers after post-selecting qubit 0 to be |0>:\")\n", "for i in range(2):\n", " print(sc_bell.current_tableau().z_output(i))\n", "\n", "# Let's check the final state vector\n", "final_state = sc_bell.state()\n", "print(f\"\\nFinal state vector: {np.round(final_state, 3)}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Application: Entanglement Entropy with Mid-Circuit Measurements\n", "\n", "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.\n", "\n", "### 1. Generating the Circuit\n", "\n", "We'll write a function that builds a random Clifford circuit using `tc.StabilizerCircuit`.\n" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "def random_stabilizer_circuit_with_mid_measurement(num_qubits, depth):\n", " \"\"\"Generates a random Clifford circuit using tc.StabilizerCircuit.\"\"\"\n", " sc = tc.StabilizerCircuit(num_qubits)\n", "\n", " for _ in range(depth):\n", " # Apply random gates to random pairs\n", " for j in range(num_qubits - 1):\n", " sc.random_gate(j, j + 1)\n", "\n", " # With 20% probability, perform a mid-circuit measurement\n", " for j in range(num_qubits - 1):\n", " if np.random.uniform() < 0.2:\n", " sc.cond_measure(j)\n", " return sc" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "num_qubits = 12\n", "depth = 12\n", "cut = [i for i in range(num_qubits // 2)]\n", "\n", "# Generate the stabilizer circuit\n", "stabilizer_circuit = random_stabilizer_circuit_with_mid_measurement(num_qubits, depth)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Calculating Entropy from the Stabilizer Circuit\n", "\n", "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.\n" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Entanglement Entropy (from StabilizerCircuit): 2.0794415416798357\n" ] } ], "source": [ "entropy = stabilizer_circuit.entanglement_entropy(cut)\n", "\n", "print(f\"Entanglement Entropy (from StabilizerCircuit): {entropy}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Final Conclusion\n", "\n", "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." ] } ], "metadata": { "kernelspec": { "display_name": "2502", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.0" } }, "nbformat": 4, "nbformat_minor": 2 }