# Quantum Operations and Gates ```elixir Mix.install([ {:qx, "~> 0.5.1", hex: :qx_sim}, {:kino, "~> 0.12"}, {:vega_lite, "~> 0.1.11"}, {:kino_vega_lite, "~> 0.1.11"} ]) alias Qx.Qubit alias Qx.Register ``` ## Introduction In the previous tutorial we explored quantum state and the qubit using Qx in **calculation mode**, where gates apply immediately and state can be inspected at every step. In this tutorial we introduce **circuit mode** — the standard workflow for building and executing quantum algorithms — and then explore the quantum gates that transform qubit states. **What this tutorial covers:** 1. Circuit mode and how it differs from calculation mode 2. Quantum operators and unitarity 3. The Hadamard gate 4. The inner product 5. Pauli gates and their action on the Bloch sphere 6. Phase gates (S, S†, and T) 7. Rotation gates (RX, RY, RZ) 8. Multi-qubit gates (CX, CZ, CCX) ## Quantum Circuits and Circuit Mode ### The Circuit Model In the circuit model of quantum computation, an algorithm is expressed as a sequence of **quantum gates** applied to a set of qubits, followed by **measurement**. This is analogous to how classical computation is built from logic gates, but with a crucial difference: quantum gates are reversible. A quantum circuit is read left to right: 1. **Initialise** qubits (usually in the $\ket{0}$ state) 2. **Apply** a sequence of gates 3. **Measure** the qubits to extract classical information ### Circuit Mode in Qx In circuit mode, you build a circuit description first, then execute it with `Qx.run/2`. This is the standard workflow for quantum algorithms. ```elixir # Build a circuit: 2 qubits, 2 classical bits for measurement results # This is called a Bell state and we will learn a lot more about them in future tutorials circuit = Qx.create_circuit(2, 2) |> Qx.h(0) # Apply Hadamard to qubit 0 |> Qx.cx(0, 1) # Apply CNOT from qubit 0 to qubit 1 |> Qx.measure(0, 0) # Measure qubit 0, store result in classical bit 0 |> Qx.measure(1, 1) # Measure qubit 1, store result in classical bit 1 # Execute the circuit for 1000 shots result = Qx.run(circuit, 1000) IO.inspect(result.counts, label: "Measurement counts") ``` Each execution ("shot") collapses the quantum state and records a classical outcome. Running many shots builds up a statistical distribution of outcomes. ```elixir # Visualise the measurement distribution Qx.draw_counts(result) ``` We can convieniently also draw a representation of a quntum circuit. This is often usefull in helping visualise the actions of our quntum circuits. ```elixir # Visualise the circuit # We currently don't have an alias for the circuit function so we need to specificy the full module and function name Qx.Draw.circuit(circuit) ``` ### Circuit Mode vs Calculation Mode The two modes serve complementary purposes: | Feature | Calculation Mode | Circuit Mode | | ---------------- | ------------------------- | ------------------------------ | | Gate application | Immediate | Deferred (on `run`) | | State inspection | At any step | Before measurement only | | Measurement | Probabilities only | Full measurement with collapse | | Multiple shots | No | Yes | | Module | `Qx.Qubit`, `Qx.Register` | `Qx.create_circuit`, `Qx.run` | **Calculation mode** is ideal for learning and interactive exploration. **Circuit mode** is the standard for algorithms, benchmarking, and hardware execution. In previous example we used a single qubit in calcualtion mode, when we need calculation mode for more than a single qubit we use the Qx.Register module as below. ```elixir # The same Bell state in calculation mode — for comparison reg = Register.new(2) |> Register.h(0) |> Register.cx(0, 1) Register.show_state(reg) ``` In calculation mode, the state is available immediately after each gate. In circuit mode, the state evolves internally and is only accessible through measurement or explicit state inspection before measurement. ```elixir # Inspecting the state vector before measurement in circuit mode circuit = Qx.create_circuit(2) |> Qx.h(0) |> Qx.cx(0, 1) state = Qx.get_state(circuit) IO.inspect(state, label: "State vector") probs = Qx.get_probabilities(circuit) IO.inspect(probs, label: "Probabilities") ``` Throughout this tutorial, we use both modes. Calculation mode lets us inspect state changes step by step, while circuit mode demonstrates the standard algorithm workflow. ## Quantum Operators and Unitarity ### Operators as Matrices A quantum gate is a mathematical operation that transforms one quantum state into another. For a single qubit, a gate is represented by a $2 \times 2$ matrix. When a gate $U$ acts on a state $\ket{\psi}$, the result is: $$ \ket{\psi'} = U\ket{\psi} $$ For example, applying a matrix $U$ to the state $\ket{\psi} = \begin{pmatrix} \alpha \\ \beta \end{pmatrix}$: $$ U\ket{\psi} = \begin{pmatrix} u_{00} & u_{01} \\ u_{10} & u_{11} \end{pmatrix} \begin{pmatrix} \alpha \\ \beta \end{pmatrix} = \begin{pmatrix} u_{00}\alpha + u_{01}\beta \\ u_{10}\alpha + u_{11}\beta \end{pmatrix} $$ ### Unitarity Not every matrix is a valid quantum gate. Quantum mechanics requires that gates are **unitary**. A matrix $U$ is unitary if: $$ U^\dagger U = U U^\dagger = I $$ where $U^\dagger$ (read "U-dagger") is the **conjugate transpose** of $U$ — obtained by transposing the matrix and taking the complex conjugate of each entry — and $I$ is the identity matrix. Unitarity guarantees two essential properties: 1. **Reversibility:** Every quantum gate has an inverse, $U^{-1} = U^\dagger$. Information is never lost. 2. **Normalisation preservation:** If $\ket{\psi}$ is a valid quantum state (normalised), then $U\ket{\psi}$ is also normalised. Probabilities continue to sum to 1. **Why reversibility matters:** Unlike classical gates (e.g., AND, OR), which can lose information (you cannot determine both inputs from the output), quantum gates are always reversible. This is a fundamental constraint of quantum mechanics that shapes the design of quantum algorithms. ```elixir # Demonstrate reversibility: applying a gate and then its inverse returns to the original state q = Qubit.new() |> Qubit.h() # Apply Hadamard |> Qubit.h() # Apply Hadamard again (H is its own inverse: H† = H) Qubit.show_state(q) # Back to |0⟩ ``` ## The Hadamard Gate The Hadamard gate is one of the most important single-qubit gates. It creates an equal superposition from a basis state: $$ H = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & 1 \\ 1 & -1 \end{pmatrix} $$ **Action on basis states:** $$ H\ket{0} = \frac{1}{\sqrt{2}}(\ket{0} + \ket{1}) = \ket{+} $$ $$ H\ket{1} = \frac{1}{\sqrt{2}}(\ket{0} - \ket{1}) = \ket{-} $$ The Hadamard gate transforms between the Z-basis and the X-basis. It is its own inverse: $H^2 = I$. ```elixir # Hadamard on |0⟩ creates |+⟩ q = Qubit.new() |> Qubit.h() Qubit.show_state(q) ``` ```elixir # Hadamard on |1⟩ creates |−⟩ q = Qubit.one() |> Qubit.h() Qubit.show_state(q) ``` ```elixir # Hadamard is self-inverse: H(H|0⟩) = |0⟩ q = Qubit.new() |> Qubit.h() |> Qubit.h() Qubit.show_state(q) ``` In circuit mode, Hadamard is the standard way to initialise qubits into superposition at the start of an algorithm: ```elixir # Put all 3 qubits into equal superposition circuit = Qx.create_circuit(3) |> Qx.h(0) |> Qx.h(1) |> Qx.h(2) probs = Qx.get_probabilities(circuit) IO.inspect(probs, label: "All 8 basis states equally likely") ``` ## The Inner Product Before continuing with more gates, we introduce the **inner product** — a mathematical tool that is essential for understanding unitarity, measurement, and orthogonality. ### Bras and Kets In Dirac notation, a **ket** $\ket{\psi}$ represents a column vector (a quantum state). Its companion, a **bra** $\bra{\psi}$, is the conjugate transpose — a row vector: $$ \ket{\psi} = \begin{pmatrix} \alpha \\ \beta \end{pmatrix} \quad \Longrightarrow \quad \bra{\psi} = \begin{pmatrix} \alpha^* & \beta^* \end{pmatrix} $$ where $\alpha^*$ denotes the complex conjugate of $\alpha$. ### Definition The **inner product** of two states $\ket{\phi}$ and $\ket{\psi}$ is written $\braket{\phi|\psi}$ (a "bra-ket" — the origin of the notation). It is computed as: $$ \braket{\phi|\psi} = \begin{pmatrix} \phi_0^* & \phi_1^* \end{pmatrix} \begin{pmatrix} \psi_0 \\ \psi_1 \end{pmatrix} = \phi_0^*\psi_0 + \phi_1^*\psi_1 $$ The inner product returns a single complex number and captures the "overlap" between two quantum states. ### Key Properties 1. **Normalisation:** A state is normalised when $\braket{\psi|\psi} = 1$. 2. **Orthogonality:** Two states are orthogonal when $\braket{\phi|\psi} = 0$. Orthogonal states are perfectly distinguishable. 3. **Basis states are orthonormal:** $$ \braket{0|0} = 1, \quad \braket{1|1} = 1, \quad \braket{0|1} = 0, \quad \braket{1|0} = 0 $$ ### Connection to Unitarity The unitarity condition $U^\dagger U = I$ can be restated using inner products: a gate $U$ is unitary if and only if it preserves inner products between all pairs of states: $$ \braket{U\phi|U\psi} = \braket{\phi|\psi} $$ This means unitary operations preserve both the length of state vectors (normalisation) and the angles between them (orthogonality). Two states that are distinguishable before a gate remain distinguishable after. ## Pauli Gates The Pauli gates are fundamental single-qubit gates named after the physicist Wolfgang Pauli. Together with the identity, they form a basis for all single-qubit operations. ### Pauli-X Gate (Bit Flip) The Pauli-X gate flips the computational basis states, making it the quantum analogue of the classical NOT gate: $$ X = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix} $$ **Action:** $$ X\ket{0} = \ket{1} \qquad X\ket{1} = \ket{0} $$ ```elixir # X gate flips |0⟩ to |1⟩ q = Qubit.new() |> Qubit.x() Qubit.show_state(q) ``` ```elixir # In circuit mode circuit = Qx.create_circuit(1) |> Qx.x(0) Qx.get_state(circuit) |> IO.inspect(label: "State after X") ``` ### Pauli-Y Gate The Pauli-Y gate combines a bit flip with a phase flip: $$ Y = \begin{pmatrix} 0 & -i \\ i & 0 \end{pmatrix} $$ **Action:** $$ Y\ket{0} = i\ket{1} \qquad Y\ket{1} = -i\ket{0} $$ ```elixir # Y gate on |0⟩ — note the imaginary amplitude q = Qubit.new() |> Qubit.y() Qubit.show_state(q) ``` ### Pauli-Z Gate (Phase Flip) The Pauli-Z gate leaves $\ket{0}$ unchanged and applies a phase of $-1$ to $\ket{1}$: $$ Z = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix} $$ **Action:** $$ Z\ket{0} = \ket{0} \qquad Z\ket{1} = -\ket{1} $$ The Z gate has no effect on basis states in terms of measurement probabilities. Its effect is on the **phase**, which becomes visible when the qubit is in superposition. ```elixir # Z gate on a superposition state: |+⟩ becomes |−⟩ q = Qubit.plus() |> Qubit.z() Qubit.show_state(q) ``` The measurement probabilities are unchanged (still 50/50), but the relative phase has flipped from positive to negative. As we discussed in the previous tutorial, this phase difference is physically real and detectable in the X-basis. ### Properties of Pauli Gates **Self-inverse:** All Pauli gates are their own inverse: $$ X^2 = Y^2 = Z^2 = I $$ ```elixir # Applying X twice returns to the original state q = Qubit.new() |> Qubit.x() |> Qubit.x() Qubit.show_state(q) ``` **Anti-commutation:** Pauli gates anti-commute with each other: $$ XY = -YX, \qquad YZ = -ZY, \qquad ZX = -XZ $$ **Eigenvalues:** Each Pauli gate has eigenvalues $+1$ and $-1$, with corresponding eigenstates: | Gate | $+1$ eigenstate | $-1$ eigenstate | | ---- | --------------- | --------------- | | X | $\ket{+}$ | $\ket{-}$ | | Y | $\ket{i}$ | $\ket{-i}$ | | Z | $\ket{0}$ | $\ket{1}$ | An **eigenstate** of a gate is a state that the gate leaves unchanged (up to a scalar factor). For example, $X\ket{+} = \ket{+}$ and $X\ket{-} = -\ket{-}$. ```elixir # |+⟩ is an eigenstate of X: applying X leaves it unchanged q = Qubit.plus() |> Qubit.x() Qubit.show_state(q) ``` ## Pauli Gates on the Bloch Sphere In the previous tutorial we introduced the Bloch sphere as a geometric representation of single-qubit states. The Pauli gates have an elegant geometric interpretation: each is a **rotation by $\pi$ radians** ($180°$) about its respective axis. ```elixir # Helper for Bloch sphere rendering defmodule BlochHelper do def render(qubit) do svg = Qubit.draw_bloch(qubit, format: :svg) Kino.Image.new(svg, :svg) end end ``` ### X Gate: $\pi$ Rotation About the X-Axis The X gate rotates the state vector by $180°$ around the X-axis of the Bloch sphere. This swaps the north and south poles ($\ket{0} \leftrightarrow \ket{1}$) while leaving points on the X-axis fixed. ```elixir # |0⟩ (north pole) → |1⟩ (south pole) q = Qubit.new() |> Qubit.x() BlochHelper.render(q) ``` ### Y Gate: $\pi$ Rotation About the Y-Axis The Y gate rotates by $180°$ around the Y-axis. ```elixir # |0⟩ → i|1⟩ (south pole, with a global phase) q = Qubit.new() |> Qubit.y() BlochHelper.render(q) ``` ### Z Gate: $\pi$ Rotation About the Z-Axis The Z gate rotates by $180°$ around the Z-axis. It leaves $\ket{0}$ and $\ket{1}$ fixed (they lie on the Z-axis) but flips points on the equator. ```elixir # |+⟩ (positive X-axis) → |−⟩ (negative X-axis) q = Qubit.plus() |> Qubit.z() BlochHelper.render(q) ``` ### Visualising Multiple Transformations ```elixir # Start at |0⟩, apply H to reach |+⟩, then Z to reach |−⟩ q = Qubit.new() |> Qubit.h() |> Qubit.z() IO.puts("H then Z on |0⟩ produces |−⟩:") Qubit.show_state(q) ``` ```elixir BlochHelper.render(q) ``` ## Phase Gates: S, S†, and T Beyond the Pauli gates, two important phase gates provide finer control over the relative phase. ### The S Gate ($\pi/2$ Phase) The S gate applies a phase of $i$ ($90°$ rotation) to the $\ket{1}$ component: $$ S = \begin{pmatrix} 1 & 0 \\ 0 & i \end{pmatrix} $$ Note that $S^2 = Z$: applying S twice is equivalent to a Z gate. ```elixir # S gate on |+⟩: rotates phase by π/2 on the equator q = Qubit.plus() |> Qubit.s() IO.puts("|+⟩ after S gate:") Qubit.show_state(q) ``` ```elixir BlochHelper.render(q) ``` ### The T Gate ($\pi/4$ Phase) The T gate applies a phase of $e^{i\pi/4}$ ($45°$ rotation) to $\ket{1}$: $$ T = \begin{pmatrix} 1 & 0 \\ 0 & e^{i\pi/4} \end{pmatrix} $$ Note that $T^2 = S$: applying T twice gives S. ```elixir # T gate on |+⟩: rotates phase by π/4 on the equator q = Qubit.plus() |> Qubit.t() IO.puts("|+⟩ after T gate:") Qubit.show_state(q) ``` ```elixir BlochHelper.render(q) ``` ### The S† Gate (Inverse S) Every gate has an inverse (its conjugate transpose, written $S^\dagger$). For the S gate: $$ S^\dagger = \begin{pmatrix} 1 & 0 \\ 0 & -i \end{pmatrix} $$ $S^\dagger$ applies a $-90°$ phase, undoing what S does: $S^\dagger S = I$. This makes it useful wherever you need to rotate *back* from the Y-basis to the X-basis — for example, measuring in the Y-basis. ```elixir # S† undoes S: applying both returns to the original state q = Qubit.plus() |> Qubit.s() |> Qubit.sdg() IO.puts("|+⟩ after S then S†:") Qubit.show_state(q) ``` ```elixir BlochHelper.render(q) ``` ### The Gate Hierarchy These phase gates form a natural hierarchy: $$ T^2 = S, \qquad S^2 = Z, \qquad Z^2 = I $$ Each gate in the sequence doubles the phase angle: $\pi/4 \to \pi/2 \to \pi \to 2\pi$ (identity). Their inverses reverse direction: $S^\dagger$ is the inverse of $S$, and $(S^\dagger)^2 = Z^\dagger = Z$ (Z is self-inverse). ## Rotation Gates The **rotation gates** generalise the Pauli gates to arbitrary angles. While the Pauli gates are fixed $\pi$ rotations, the rotation gates take an angle parameter $\theta$ and rotate the Bloch vector by $\theta$ radians around the specified axis. ### RX: Rotation Around the X-Axis $$ R_X(\theta) = e^{-i\frac{\theta}{2}X} = \begin{pmatrix} \cos\frac{\theta}{2} & -i\sin\frac{\theta}{2} \\ -i\sin\frac{\theta}{2} & \cos\frac{\theta}{2} \end{pmatrix} $$ When $\theta = \pi$, this reduces to the Pauli-X gate (up to a global phase). ```elixir # RX(π/2) on |0⟩: rotates halfway from north pole toward equator along X q = Qubit.new() |> Qubit.rx(:math.pi() / 2) IO.puts("RX(π/2) on |0⟩:") Qubit.show_state(q) ``` ```elixir BlochHelper.render(q) ``` ### RY: Rotation Around the Y-Axis $$ R_Y(\theta) = e^{-i\frac{\theta}{2}Y} = \begin{pmatrix} \cos\frac{\theta}{2} & -\sin\frac{\theta}{2} \\ \sin\frac{\theta}{2} & \cos\frac{\theta}{2} \end{pmatrix} $$ RY is notable because it produces **real-valued** superpositions (no imaginary components). When $\theta = \pi/2$, it creates the same superposition as the Hadamard gate acting on $\ket{0}$: ```elixir # RY(π/2) on |0⟩ creates a superposition similar to H|0⟩ q = Qubit.new() |> Qubit.ry(:math.pi() / 2) IO.puts("RY(π/2) on |0⟩:") Qubit.show_state(q) ``` ```elixir BlochHelper.render(q) ``` ### RZ: Rotation Around the Z-Axis $$ R_Z(\theta) = e^{-i\frac{\theta}{2}Z} = \begin{pmatrix} e^{-i\frac{\theta}{2}} & 0 \\ 0 & e^{i\frac{\theta}{2}} \end{pmatrix} $$ RZ rotations change the **azimuthal angle** $\phi$ on the Bloch sphere without affecting the polar angle $\theta$. They modify the relative phase between $\ket{0}$ and $\ket{1}$. ```elixir # RZ(π/4) on |+⟩: rotates around the equator q = Qubit.plus() |> Qubit.rz(:math.pi() / 4) IO.puts("RZ(π/4) on |+⟩:") Qubit.show_state(q) ``` ```elixir BlochHelper.render(q) ``` ### Reaching Any Point on the Bloch Sphere Any single-qubit state can be reached by combining rotations. In fact, any single-qubit unitary can be decomposed as a sequence of rotations: $$ U = R_Z(\alpha) \cdot R_Y(\beta) \cdot R_Z(\gamma) $$ for some angles $\alpha$, $\beta$, $\gamma$. This is the **ZYZ decomposition**. ```elixir # Combine rotations to reach an arbitrary point on the Bloch sphere q = Qubit.new() |> Qubit.ry(:math.pi() / 3) |> Qubit.rz(:math.pi() / 4) IO.puts("After RY(π/3) then RZ(π/4):") Qubit.show_state(q) ``` ```elixir BlochHelper.render(q) ``` ### Rotation Gates in Circuit Mode ```elixir circuit = Qx.create_circuit(1, 1) |> Qx.ry(0, :math.pi() / 3) |> Qx.rz(0, :math.pi() / 4) |> Qx.measure(0, 0) result = Qx.run(circuit, 1000) IO.inspect(result.counts, label: "Measurement counts") Qx.draw_counts(result) ``` ## Multi-Qubit Gates Single-qubit gates alone cannot create **entanglement** — the distinctly quantum phenomenon where qubits become correlated in ways impossible in classical physics. Multi-qubit gates are essential for this. ### The Controlled-NOT (CNOT / CX) Gate The CNOT gate is the most important two-qubit gate. It has a **control** qubit and a **target** qubit. It flips the target if and only if the control is $\ket{1}$: $$ \text{CNOT}\ket{c, t} = \ket{c, \; t \oplus c} $$ where $\oplus$ is addition modulo 2 (XOR). **Matrix representation** in the computational basis $\{\ket{00}, \ket{01}, \ket{10}, \ket{11}\}$: $$ \text{CNOT} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \end{pmatrix} $$ **Action on each basis state:** | Input | Output | Explanation | | ---------- | ---------- | ------------------------------ | | $\ket{00}$ | $\ket{00}$ | Control is 0, target unchanged | | $\ket{01}$ | $\ket{01}$ | Control is 0, target unchanged | | $\ket{10}$ | $\ket{11}$ | Control is 1, target flipped | | $\ket{11}$ | $\ket{10}$ | Control is 1, target flipped | ```elixir # CNOT with control=0, target=1 # When control qubit is |1⟩, the target flips reg = Register.new(2) |> Register.x(0) # Set qubit 0 to |1⟩ |> Register.cx(0, 1) # CNOT: qubit 0 controls, qubit 1 is target IO.puts("|10⟩ after CNOT:") Register.show_state(reg) ``` ### Creating Entanglement with CNOT The most iconic use of CNOT is creating **Bell states** — maximally entangled two-qubit states. Apply Hadamard to the control qubit (creating a superposition), then CNOT: $$ \text{CNOT} \cdot (H \otimes I)\ket{00} = \text{CNOT} \cdot \frac{1}{\sqrt{2}}(\ket{00} + \ket{10}) = \frac{1}{\sqrt{2}}(\ket{00} + \ket{11}) = \ket{\Phi^+} $$ The result, $\ket{\Phi^+}$, is an entangled state: neither qubit has a definite individual state, but measuring one immediately determines the other. ```elixir # Create a Bell state in calculation mode reg = Register.new(2) |> Register.h(0) |> Register.cx(0, 1) Register.show_state(reg) ``` ```elixir # Create and measure a Bell state in circuit mode circuit = Qx.create_circuit(2, 2) |> Qx.h(0) |> Qx.cx(0, 1) |> Qx.measure(0, 0) |> Qx.measure(1, 1) result = Qx.run(circuit, 1000) IO.inspect(result.counts, label: "Bell state measurements") ``` Only the outcomes "00" and "11" appear — never "01" or "10". The two qubits are perfectly correlated despite each individual measurement being random. ```elixir Qx.draw_counts(result) ``` ### The Controlled-Z (CZ) Gate The CZ gate applies a phase flip ($-1$) when **both** qubits are $\ket{1}$: $$ \text{CZ} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & -1 \end{pmatrix} $$ **Action:** | Input | Output | | ---------- | ----------- | | $\ket{00}$ | $\ket{00}$ | | $\ket{01}$ | $\ket{01}$ | | $\ket{10}$ | $\ket{10}$ | | $\ket{11}$ | $-\ket{11}$ | Unlike CNOT, the CZ gate is **symmetric**: it does not matter which qubit is called the control and which is the target. $\text{CZ}_{0,1} = \text{CZ}_{1,0}$. ```elixir # CZ on a state where both qubits are in superposition reg = Register.new(2) |> Register.h(0) |> Register.h(1) IO.puts("Before CZ (equal superposition of all basis states):") Register.show_state(reg) |> IO.inspect() reg = reg |> Register.cz(0, 1) IO.puts("\nAfter CZ (note the negative phase on |11⟩):") Register.show_state(reg) ``` ### Relationship Between CNOT and CZ CNOT and CZ are closely related through Hadamard gates applied to the target qubit: $$ \text{CZ} = (I \otimes H) \cdot \text{CNOT} \cdot (I \otimes H) $$ This means: apply H to the target, then CNOT, then H again — the result is equivalent to CZ. ```elixir # Verify the equivalence: CZ = H-CNOT-H on target state_cz = Qx.create_circuit(2) |> Qx.h(0) |> Qx.h(1) |> Qx.cz(0, 1) |> Qx.get_state() state_hch = Qx.create_circuit(2) |> Qx.h(0) |> Qx.h(1) |> Qx.h(1) |> Qx.cx(0, 1) |> Qx.h(1) |> Qx.get_state() IO.inspect(state_cz, label: "CZ result ") IO.inspect(state_hch, label: "H-CNOT-H result") ``` ### The Toffoli Gate (CCX) The Toffoli gate (also called CCNOT or CCX) is a three-qubit gate with **two controls** and one target. It flips the target only when **both** controls are $\ket{1}$: $$ \text{CCX}\ket{c_1, c_2, t} = \ket{c_1, c_2, \; t \oplus (c_1 \wedge c_2)} $$ The Toffoli gate is **universal for classical computation** — any classical Boolean function can be built from Toffoli gates. It also plays a key role in quantum error correction and reversible computing. ```elixir # Toffoli: target flips only when both controls are |1⟩ reg = Register.new(3) |> Register.x(0) # Set control 1 to |1⟩ |> Register.x(1) # Set control 2 to |1⟩ IO.puts("Before Toffoli (state is |110⟩):") Register.show_state(reg) |> IO.inspect() reg = reg |> Register.ccx(0, 1, 2) IO.puts("\nAfter Toffoli (target flipped to |1⟩, state is |111⟩):") Register.show_state(reg) ``` ```elixir # When only one control is |1⟩, the target is unchanged reg = Register.new(3) |> Register.x(0) # Only control 1 is |1⟩ |> Register.ccx(0, 1, 2) IO.puts("Only one control set — target unchanged:") Register.show_state(reg) ``` ```elixir # Toffoli in circuit mode with measurement circuit = Qx.create_circuit(3, 3) |> Qx.x(0) |> Qx.x(1) |> Qx.ccx(0, 1, 2) |> Qx.measure(0, 0) |> Qx.measure(1, 1) |> Qx.measure(2, 2) result = Qx.run(circuit, 100) IO.inspect(result.counts, label: "Toffoli result — should be all |111⟩") ``` ## Putting It Together: A Complete Circuit Let us build a complete circuit that demonstrates several gates working together. We will create a 3-qubit GHZ state — a maximally entangled state of three qubits: $$ \ket{\text{GHZ}} = \frac{1}{\sqrt{2}}(\ket{000} + \ket{111}) $$ ```elixir # Build the GHZ circuit ghz_circuit = Qx.create_circuit(3, 3) |> Qx.h(0) # Superposition on qubit 0 |> Qx.cx(0, 1) # Entangle qubit 0 and 1 |> Qx.cx(0, 2) # Entangle qubit 0 and 2 |> Qx.measure(0, 0) |> Qx.measure(1, 1) |> Qx.measure(2, 2) result = Qx.run(ghz_circuit, 1000) IO.inspect(result.counts, label: "GHZ state — only |000⟩ and |111⟩") ``` ```elixir Qx.draw_counts(result) ``` All three qubits are perfectly correlated: they are always all 0 or all 1, never a mixture. We will explore entanglement in much greater depth in a later tutorial. ## Summary In this tutorial, we covered quantum operations and gates: * **Circuit Mode:** Build a circuit description with gates and measurements, then execute with `Qx.run/2` for multi-shot simulation. * **Operators and Unitarity:** Quantum gates are unitary matrices ($U^\dagger U = I$), guaranteeing reversibility and normalisation preservation. * **The Hadamard Gate:** Creates equal superposition, transforms between Z-basis and X-basis, and is self-inverse. * **The Inner Product:** The overlap $\braket{\phi|\psi}$ quantifies the relationship between states. Unitarity preserves inner products. * **Pauli Gates:** X (bit flip), Y (bit + phase flip), Z (phase flip) — each is a $\pi$ rotation on the Bloch sphere and self-inverse. * **Phase Gates:** S ($\pi/2$ phase), S† (inverse of S, $-\pi/2$ phase), and T ($\pi/4$ phase) provide finer phase control, with $T^2 = S$, $S^2 = Z$, and $S^\dagger S = I$. * **Rotation Gates:** $R_X(\theta)$, $R_Y(\theta)$, $R_Z(\theta)$ generalise Pauli gates to arbitrary rotation angles. Any single-qubit unitary decomposes into rotations. * **Multi-Qubit Gates:** CNOT (controlled bit flip), CZ (controlled phase flip), and Toffoli (doubly-controlled flip) enable entanglement and universal computation. ### What's Next In the next tutorial, **Quantum Measurement**, we explore the measurement postulate, the Born rule, the probabilistic nature of quantum outcomes, and wavefunction collapse.