# Quantum Operations and Gates

```elixir
Mix.install([
  {:qx, "~> 0.8.0", hex: :qx_sim},
  {:kino, "~> 0.12"},
  {:vega_lite, "~> 0.1.11"},
  {:kino_vega_lite, "~> 0.1.11"}
])

alias Qx.Qubit
```

## Learning objectives

By the end of this tutorial you will be able to:

* Explain why every quantum gate is a unitary matrix, and verify unitarity numerically
* Apply the Pauli gates ($X$, $Y$, $Z$) and the Hadamard gate, and predict their effect on any state you met in the previous tutorial
* Use the phase gates ($S$, $T$) and rotation gates ($R_x$, $R_y$, $R_z$) to move a state anywhere on the Bloch sphere
* Demonstrate that quantum gates are reversible by undoing them
* Build and run your first quantum circuit in Qx's **circuit mode**

## Introduction

This is the second tutorial in the series, following **Quantum State and The Qubit**. There we established what a qubit *is*: a normalised vector $\ket{\psi} = \alpha\ket{0} + \beta\ket{1}$, visualised as a point on the Bloch sphere. This tutorial is about what qubits *do* — or rather, what we do to them.

A quantum computation is a sequence of operations applied to qubits. The operations are called **quantum gates**, by analogy with the logic gates of classical computing. The analogy is real but imperfect, and the differences are where quantum computing gets interesting: quantum gates are rotations, they are always reversible, and some of them manipulate information (phase) that no classical bit can hold.

We work in calculation mode for most of this tutorial, inspecting the state after every gate. At the end we introduce **circuit mode**, the build-then-run style used for real quantum programs, which the rest of the series relies on.

**Prerequisites:** the previous tutorial in this series. Everything here builds on state vectors, amplitudes, bases, phase, and the Bloch sphere as covered there.

Run the helper cell below first; we use it throughout to render Bloch spheres.

```elixir
# Helper function for rendering Bloch sphere SVGs
defmodule BlochHelper do
  def render(qubit) do
    svg = Qubit.draw_bloch(qubit, format: :svg)
    Kino.Image.new(svg, :svg)
  end
end
```

## Gates are unitary matrices

In the previous tutorial we represented a qubit state as a column vector. Quantum mechanics says that the evolution of an isolated quantum system is *linear*: the new state is a matrix multiplied by the old state. For a single qubit, that matrix is $2 \times 2$ with complex entries:

$$
\ket{\psi'} = U\ket{\psi}
$$

Not just any matrix qualifies. The output must still be a valid quantum state, so $U$ must preserve the normalisation $|\alpha|^2 + |\beta|^2 = 1$ for *every* input. Matrices that preserve vector length are called **unitary**, and they satisfy:

$$
U^\dagger U = I
$$

where $U^\dagger$ (the *adjoint* or *conjugate transpose*) is obtained by transposing $U$ and conjugating each entry, and $I$ is the identity matrix.

Unitarity has two consequences worth holding onto for the rest of the series:

1. **Gates are rotations.** A length-preserving transformation of the Bloch sphere is a rotation of it. Every single-qubit gate rotates the state vector around some axis, by some angle.
2. **Gates are reversible.** $U^\dagger$ undoes $U$ exactly. No information is ever destroyed by a gate — a sharp contrast with classical gates like AND, which take 2 bits in and give 1 bit out.

Let's check the first claim empirically. A pipeline of gates applied to any state should always leave a valid, normalised qubit:

```elixir
# Run this several times: random state, arbitrary gates, still normalised
Qubit.random()
|> Qubit.h()
|> Qubit.t()
|> Qubit.rx(0.7)
|> Qubit.z()
|> Qubit.valid?()
```

However the qubit starts and whatever we throw at it, the norm survives. Now let's meet the gates individually.

## The Pauli-X gate: bit flip

The $X$ gate is the closest thing quantum computing has to the classical NOT. Its matrix swaps the two amplitudes:

$$
X = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}, \qquad X\begin{pmatrix} \alpha \\ \beta \end{pmatrix} = \begin{pmatrix} \beta \\ \alpha \end{pmatrix}
$$

On basis states it acts exactly like NOT: $X\ket{0} = \ket{1}$ and $X\ket{1} = \ket{0}$.

```elixir
# X flips |0⟩ to |1⟩
Qubit.new() |> Qubit.x() |> Qubit.show_state()
```

On the Bloch sphere, $X$ is a half-turn ($\pi$ radians) around the X-axis — which is why $\ket{0}$ at the north pole lands on $\ket{1}$ at the south pole.

**Predict first:** what does $X$ do to the superposition $\ket{+} = \frac{1}{\sqrt{2}}(\ket{0} + \ket{1})$? Apply the amplitude-swap rule, then run the cell.

```elixir
Qubit.plus() |> Qubit.x() |> Qubit.show_state()
```

Nothing changed. Swapping two equal amplitudes gives the same state back. Geometrically, $\ket{+}$ sits *on* the X-axis, and rotating around an axis leaves the points on that axis fixed. States that a gate leaves unchanged are called **eigenstates** of that gate, and they matter a great deal in later tutorials.

## The Pauli-Z gate: phase flip

The $Z$ gate looks almost trivial:

$$
Z = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix}, \qquad Z\begin{pmatrix} \alpha \\ \beta \end{pmatrix} = \begin{pmatrix} \alpha \\ -\beta \end{pmatrix}
$$

It leaves $\ket{0}$ untouched and merely flips the sign of the $\ket{1}$ amplitude. From the previous tutorial you know exactly what that sign is: the **relative phase**. $Z$ is a half-turn around the Z-axis.

```elixir
# Z turns |+⟩ into |−⟩
q = Qubit.plus() |> Qubit.z()
Qubit.show_state(q)
```

The amplitudes read $0.707$ and $-0.707$: this is $\ket{-}$. And as we established last time, a Z-basis measurement can't see the difference, but an X-basis measurement can:

```elixir
IO.inspect(Qubit.measure_probabilities(q), label: "Z-basis (blind to the flip)")
IO.inspect(Qubit.measure_x(q), label: "X-basis (sees it: this is |−⟩)")
```

```elixir
BlochHelper.render(q)
```

The Bloch vector has swung from the positive X-axis to the negative X-axis: a half-turn about Z, exactly as advertised. $X$ flips bits; $Z$ flips phase. The two flips are equally physical, and quantum algorithms use both.

## The Pauli-Y gate

The third Pauli gate does both flips at once, with complex entries:

$$
Y = \begin{pmatrix} 0 & -i \\ i & 0 \end{pmatrix}
$$

```elixir
Qubit.new() |> Qubit.y() |> Qubit.show_state()
```

The result is $i\ket{1}$: the bit flipped *and* picked up a phase of $i$. Since the factor $i$ here multiplies the entire state, it is a **global phase** with no observable effect — the measurement probabilities are identical to those of $X\ket{0}$. On the Bloch sphere, $Y$ is a half-turn around the Y-axis. Together $X$, $Y$, $Z$ give a half-turn around each of the sphere's three axes.

```elixir
BlochHelper.render(Qubit.new() |> Qubit.y())
```

## The Hadamard gate

The Hadamard gate is the workhorse of quantum computing — it is how nearly every algorithm creates superposition from definite states:

$$
H = \frac{1}{\sqrt{2}}\begin{pmatrix} 1 & 1 \\ 1 & -1 \end{pmatrix}
$$

Its action on the basis states:

$$
H\ket{0} = \frac{\ket{0} + \ket{1}}{\sqrt{2}} = \ket{+} \qquad H\ket{1} = \frac{\ket{0} - \ket{1}}{\sqrt{2}} = \ket{-}
$$

$H$ converts the Z-basis into the X-basis and back. Geometrically it is a half-turn around the diagonal axis that lies midway between X and Z.

```elixir
q = Qubit.new() |> Qubit.h()
Qubit.show_state(q)
```

```elixir
BlochHelper.render(q)
```

**Predict first:** what does applying $H$ twice do? Trace $\ket{0} \to \ket{+} \to \,?$ through the equations above, then run:

```elixir
Qubit.new() |> Qubit.h() |> Qubit.h() |> Qubit.show_state()
```

Back to $\ket{0}$: the Hadamard is its own inverse, $H^2 = I$. We can verify this at the matrix level. $H$ is real and symmetric, so $H^\dagger = H$, and the unitarity condition $H^\dagger H = I$ becomes simply $H \cdot H = I$:

```elixir
h = Nx.divide(Nx.tensor([[1.0, 1.0], [1.0, -1.0]]), :math.sqrt(2))
Nx.dot(h, h)
```

The identity matrix, up to floating-point dust. This is what "unitary" looks like as arithmetic.

One more useful fact — $H$ maps $\ket{1}$ to $\ket{-}$, which we can confirm with an X-basis measurement:

```elixir
Qubit.one() |> Qubit.h() |> Qubit.measure_x()
```

Probability 1 at index 1: definitely $\ket{-}$.

## Phase gates: S and T

$Z$ flips the phase by a half-turn ($\pi$). The $S$ and $T$ gates apply finer phase rotations:

$$
S = \begin{pmatrix} 1 & 0 \\ 0 & i \end{pmatrix} \qquad T = \begin{pmatrix} 1 & 0 \\ 0 & e^{i\pi/4} \end{pmatrix}
$$

$S$ rotates the phase by $\pi/2$ (a quarter-turn around the Z-axis) and $T$ by $\pi/4$ (an eighth-turn). A quarter-turn applied to $\ket{+}$ should carry it from the X-axis to the Y-axis — that is, onto $\ket{i}$. The basis measurements from the previous tutorial let us verify:

```elixir
q = Qubit.plus() |> Qubit.s()

IO.inspect(Qubit.measure_probabilities(q), label: "Z-basis")
IO.inspect(Qubit.measure_x(q), label: "X-basis")
IO.inspect(Qubit.measure_y(q), label: "Y-basis")
```

A 50/50 coin to the Z and X bases, but certainty in the Y-basis: this is $\ket{i}$.

```elixir
BlochHelper.render(q)
```

**Predict first:** if $T$ is an eighth-turn and $S$ is a quarter-turn, what should two $T$ gates equal? Run the cell and compare the state vectors:

```elixir
two_ts = Qubit.plus() |> Qubit.t() |> Qubit.t() |> Qubit.state_vector()
one_s = Qubit.plus() |> Qubit.s() |> Qubit.state_vector()

{two_ts, one_s}
```

Identical: $T^2 = S$, and by the same logic $S^2 = Z$. Phase gates compose by adding their angles.

Unlike the Paulis and $H$, the $S$ gate is *not* its own inverse (a quarter-turn forward twice is a half-turn, not a return). Its inverse is the $S^\dagger$ gate, available as `sdg/1`:

```elixir
Qubit.plus() |> Qubit.s() |> Qubit.sdg() |> Qubit.show_state()
```

The same goes for $T$, and we have already proved it. We saw above that $T^2 = S$, so two $T$ gates land on $S$, not on the identity. $T$ isn't its own inverse. Its turn is just finer, so the full cycle is longer:

$$
T^2 = S, \qquad T^4 = Z, \qquad T^8 = I
$$

Eight eighth-turns add up to a full $2\pi$ trip around the Z-axis, back to where you started. Run all eight on $\ket{+}$ and compare:

```elixir
t_eighth =
  Qubit.plus()
  |> Qubit.t() |> Qubit.t() |> Qubit.t() |> Qubit.t()
  |> Qubit.t() |> Qubit.t() |> Qubit.t() |> Qubit.t()
  |> Qubit.state_vector()

just_plus = Qubit.plus() |> Qubit.state_vector()

{t_eighth, just_plus}
```

Identical: eight $T$s return $\ket{+}$.

To undo a single $T$ you want its $T^\dagger$ gate. Qx ships `sdg/1` for $S^\dagger$ but has no `tdg`, so apply a $-\pi/4$ phase with `phase/2` instead:

```elixir
Qubit.plus() |> Qubit.t() |> Qubit.phase(-:math.pi() / 4) |> Qubit.show_state()
```

The $T$ and the $-\pi/4$ phase cancel, leaving $\ket{+}$ untouched. Same trick works for $S$: `phase/2` with $-\phi$ inverts any phase gate when no dedicated dagger exists.

For arbitrary phase angles, `Qubit.phase/2` takes any $\phi$ in radians.

## Rotation gates

The fixed gates above are special cases of a continuous family. The rotation gates turn the Bloch vector by *any* angle $\theta$ around each axis:

$$
R_y(\theta) = \begin{pmatrix} \cos\frac{\theta}{2} & -\sin\frac{\theta}{2} \\ \sin\frac{\theta}{2} & \cos\frac{\theta}{2} \end{pmatrix}
$$

with analogous $R_x(\theta)$ and $R_z(\theta)$ (their matrices involve $i$; the pattern of half-angles is the same). Note the $\theta/2$ inside the matrix — the same Hilbert-space-vs-sphere factor of 2 we derived in the previous tutorial.

**Predict first:** $R_y(\pi/2)$ turns $\ket{0}$ a quarter of the way around the Y-axis. Starting at the north pole, where does a quarter-turn land? Check the matrix: $\cos(\pi/4) = \sin(\pi/4) = \frac{1}{\sqrt{2}}$.

```elixir
Qubit.new() |> Qubit.ry(:math.pi() / 2) |> Qubit.show_state()
```

It lands on $\ket{+}$, on the equator. A rotation gate with the right angle reaches any point on the sphere, which is why hardware only needs a small set of these to do everything.

A half-turn should reproduce a Pauli gate. Almost:

```elixir
q = Qubit.new() |> Qubit.rx(:math.pi())
Qubit.show_state(q)
```

The amplitude reads $-i$ rather than $1$, yet the measurement probabilities match $X\ket{0}$ exactly. That stray $-i$ multiplies the whole state — another unobservable global phase. $R_x(\pi)$ and $X$ are physically the same operation.

### Drive the rotation yourself

Run the slider cell, then re-run the render cell each time you change the angle:

```elixir
theta_input = Kino.Input.range("θ — rotation angle (0 to 2π)", min: 0.0, max: 6.28318, step: 0.01, default: 0.0)
```

```elixir
theta = Kino.Input.read(theta_input)

Qubit.new()
|> Qubit.ry(theta)
|> Qubit.tap_state(label: "After ry(#{Float.round(theta, 2)}) on |0⟩")
|> BlochHelper.render()
```

Experiments worth running:

* $\theta = \pi/2$ should land on $\ket{+}$ and $\theta = \pi$ on $\ket{1}$, sweeping down the X–Z plane.
* Push $\theta$ all the way to $2\pi$, a full turn, and look closely at the amplitude of $\ket{0}$. It comes back as $-1$, not $+1$. A qubit needs a $4\pi$ rotation to truly return to its original description — a deep quantum fact (physicists call such objects *spinors*). The minus sign is a global phase, so no measurement can detect it here, but it becomes physical when only *part* of a system is rotated. File it away for the entanglement tutorial.

## Reversibility

Unitarity promised that every gate can be undone. The inverses we have met:

* $X$, $Y$, $Z$, $H$ — each is its own inverse
* $S$ — undone by `sdg/1`; $T$ — undone by `phase/2` with $-\pi/4$; arbitrary phases by `phase/2` with the negated angle
* $R_x$, $R_y$, $R_z$ — undone by rotating the same axis with $-\theta$

Watch reversal work on a state we don't even know:

```elixir
# Run this a few times — H followed by H restores any random state
Qubit.random()
|> Qubit.tap_state(label: "Random start")
|> Qubit.h()
|> Qubit.tap_state(label: "After H — somewhere else entirely")
|> Qubit.h()
|> Qubit.show_state()
```

The final state matches the random start every time. Compare this with a classical AND gate: given the output `0`, you cannot say whether the inputs were `00`, `01`, or `10` — information is gone. A quantum computation never loses information between gates. The only irreversible step in quantum computing is measurement, which is the subject of the next tutorial.

## Your first circuit

So far we have applied gates one at a time, peeking at the state after each — invaluable for learning, impossible on real hardware, where the state cannot be inspected mid-computation. Real quantum programs are written as **circuits**: declare the qubits, list the gates, end with measurements, then run the whole thing and collect statistics.

Qx's circuit mode mirrors this workflow. The functions live on the top-level `Qx` module (no alias needed):

```elixir
# 1 qubit, 1 classical bit to hold the measured result
qc =
  Qx.create_circuit(1, 1)
  |> Qx.h(0)
  |> Qx.measure(0, 0)

result = Qx.run(qc, shots: 1024)
Qx.draw_counts(result, title: "H then measure — 1024 shots")
```

Reading the pipeline: `create_circuit(1, 1)` declares one qubit (which starts in $\ket{0}$) and one classical bit, `h(0)` queues a Hadamard on qubit 0, and `measure(0, 0)` records qubit 0's outcome into classical bit 0. Nothing executes until `Qx.run/2`, which simulates the circuit 1024 times — each repetition is called a **shot** — and tallies the outcomes.

The chart shows roughly 512 counts each for `0` and `1`. That's our familiar $\ket{+}$ state, seen the way an experimenter sees it: through repeated measurement statistics rather than direct state inspection.

**Try it:** add `Qx.x(0)` before the Hadamard in the circuit above and predict the counts before re-running. (Recall $H\ket{1} = \ket{-}$, and what $\ket{-}$ gives under Z-basis measurement.)

Why the counts are *roughly* 512 rather than exactly, what measurement does to the state, and how to measure in other bases at circuit level — all of that is the next tutorial's territory.

## Gate cheat sheet

| Gate            | Qx (calculation mode)        | Matrix                                                            | Bloch action                       |
| --------------- | ---------------------------- | ----------------------------------------------------------------- | ---------------------------------- |
| $X$             | `Qubit.x/1`                  | $\begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}$                    | half-turn about X                  |
| $Y$             | `Qubit.y/1`                  | $\begin{pmatrix} 0 & -i \\ i & 0 \end{pmatrix}$                   | half-turn about Y                  |
| $Z$             | `Qubit.z/1`                  | $\begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix}$                   | half-turn about Z                  |
| $H$             | `Qubit.h/1`                  | $\frac{1}{\sqrt{2}}\begin{pmatrix} 1 & 1 \\ 1 & -1 \end{pmatrix}$ | half-turn about X+Z diagonal       |
| $S$             | `Qubit.s/1`                  | $\begin{pmatrix} 1 & 0 \\ 0 & i \end{pmatrix}$                    | quarter-turn about Z               |
| $T$             | `Qubit.t/1`                  | $\begin{pmatrix} 1 & 0 \\ 0 & e^{i\pi/4} \end{pmatrix}$           | eighth-turn about Z                |
| $R_x, R_y, R_z$ | `Qubit.rx/2`, `ry/2`, `rz/2` | half-angle rotation matrices                                      | any angle $\theta$ about that axis |

Every gate in this table also exists in circuit mode as `Qx.x/2`, `Qx.h/2`, `Qx.rx/3`, and so on, taking the circuit and a qubit index.

## Summary

* **Gates are unitary matrices:** $U^\dagger U = I$. Unitarity preserves normalisation, makes every gate a rotation of the Bloch sphere, and guarantees reversibility.
* **The Pauli gates** give half-turns about the three axes: $X$ flips bits, $Z$ flips phase, $Y$ does both.
* **The Hadamard gate** converts between the Z-basis and the X-basis, creating superposition from definite states. It is its own inverse.
* **Phase gates** $S$ and $T$ apply quarter- and eighth-turns about Z; their effects are invisible to Z-basis measurements but show up in the X and Y bases.
* **Rotation gates** reach any point on the Bloch sphere, and expose the global-phase quirk that a $2\pi$ turn returns a qubit with a minus sign.
* **Circuit mode** is the build-then-run workflow of real quantum programs: queue gates with `Qx.create_circuit/2` pipelines, execute with `Qx.run/2`, read statistics from shots.

### What's next

In the next tutorial, **Quantum Measurement**, we look closely at the one operation that is *not* unitary: why measurement is irreversible, what collapse does to a superposition, where the randomness in shot counts comes from, and how to measure in any basis at circuit level.
