Concepts Intermediate Free 60/60 in series 20 min read By

Quantum Computing in Julia with Yao.jl

Write your first quantum programs in Julia with Yao.jl: build a Bell state, apply gates and rotations, measure, and compute expectation values. Every snippet is run on Yao 0.9.

What you'll learn

  • julia
  • yao.jl
  • quantum circuits
  • bell state
  • quantum programming

Prerequisites

  • Basic quantum concepts: qubits, gates, and the Bell state
  • Some programming experience in any language

Almost every quantum computing tutorial reaches for Python. Julia is the alternative worth knowing about: it runs close to C speed while reading like a high-level scripting language. That combination stops mattering when you are writing a two-gate circuit, and starts mattering a lot once you are optimizing the parameters of a variational circuit over thousands of iterations, which is where a fast simulator earns its keep.

Yao.jl is the best-known quantum framework in the Julia ecosystem. It is built around two ideas: simulate circuits quickly, and differentiate through them automatically so that gradient-based algorithms like VQE and QAOA are first-class. This tutorial stays on the fundamentals. You will install Julia and Yao, build a Bell state, apply single-qubit gates and rotations, measure a circuit, and compute an expectation value. Every code block below was run on Yao 0.9, and the outputs shown are the actual results.

Why Julia and Yao.jl

If you already know Python and Qiskit, the honest answer to “why switch” is: usually you do not have to. The core ideas (qubits, gates, superposition, entanglement, measurement) are identical, and skills transfer in an afternoon. Julia becomes worth the move in two situations. The first is raw simulation performance on larger circuits, where Julia’s compiled speed shows up directly. The second is differentiable programming: Yao can compute exact gradients of a circuit’s output with respect to its gate parameters, which is the engine underneath variational quantum algorithms. If your work is quantum machine learning or variational optimization, that is a real advantage rather than a preference.

Setting Up Julia and Yao

Install Julia with juliaup, the official version manager. On macOS or Linux:

curl -fsSL https://install.julialang.org | sh

On Windows, install juliaup from the Microsoft Store, or download an installer from julialang.org. Once julia is on your path, start the REPL by typing julia, then add the two packages this tutorial uses:

using Pkg
Pkg.add(["Yao", "StatsBase"])

The first time you run this, Julia downloads and precompiles the packages, which takes a couple of minutes. After that, loading them is fast. Yao is the framework itself; StatsBase gives us countmap for tallying measurement results.

Example 1: A Bell State

The Bell state Φ+=12(00+11)|\Phi^+\rangle = \frac{1}{\sqrt{2}}(|00\rangle + |11\rangle) is the classic two-qubit entangled state: measure it and you always get 00 or 11, each with probability one half, never 01 or 10. You build it with a Hadamard on the first qubit followed by a CNOT.

using Yao, StatsBase

# Build the circuit: H on qubit 1, then CNOT (control 1, target 2)
bell = chain(2, put(1 => H), control(1, 2 => X))
println(bell)

# Apply the circuit to the two-qubit zero state |00>
reg = zero_state(2) |> bell

# Inspect the amplitudes, then the probabilities
println(round.(statevec(reg), digits = 4))
println(round.(probs(reg), digits = 4))

# Sample 1000 measurements and tally the outcomes
counts = countmap(measure(reg; nshots = 1000))
println(counts)

Running this prints the circuit as a tree, the state vector, the probabilities, and the measurement tally:

nqubits: 2
chain
├─ put on (1)
│  └─ H
└─ control(1)
   └─ (2,) X
ComplexF64[0.7071 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.7071 + 0.0im]
[0.5, 0.0, 0.0, 0.5]
Dict{DitStr{2, 2, Int64}, Int64}(00 ₍₂₎ => 486, 11 ₍₂₎ => 514)

The amplitudes are 120.7071\frac{1}{\sqrt{2}} \approx 0.7071 on 00|00\rangle and 11|11\rangle and zero elsewhere, exactly the Bell state. The measurement tally shows only 00 and 11. The exact split changes from run to run because measurement is random, but it will always be close to 500 and 500, and you will never see 01 or 10.

What Each Piece Does

  • chain(2, ...) declares a circuit on two qubits and runs the blocks inside it left to right.
  • put(1 => H) places a single-qubit gate, here a Hadamard, on qubit 1. The => syntax reads as “put H on wire 1”.
  • control(1, 2 => X) is a controlled gate: qubit 1 is the control, and an X (NOT) is applied to qubit 2 when the control is set. Together with the Hadamard, this is a CNOT that produces entanglement.
  • zero_state(2) creates a fresh two-qubit register in the 00|00\rangle state, and the pipe operator |> feeds it through the circuit. Julia programmers use |> constantly; here it reads as “take the zero state and apply bell to it”.
  • statevec returns the raw complex amplitudes, probs returns their squared magnitudes, and measure(reg; nshots = 1000) samples outcomes without collapsing reg, so you can keep using it afterward. Outcomes come back as DitStr bit strings, printed as 00 ₍₂₎ where the subscript 2 marks them as base-2.

Example 2: Single-Qubit Gates and Rotations

You do not need a full circuit to experiment with one qubit. Apply a gate directly to a one-qubit register and read off the amplitudes.

# A Hadamard on |0> creates an equal superposition
println(round.(statevec(zero_state(1) |> H), digits = 4))

# An Rx rotation of pi/2 about the X axis
println(round.(statevec(zero_state(1) |> Rx(pi/2)), digits = 4))
ComplexF64[0.7071 + 0.0im, 0.7071 + 0.0im]
ComplexF64[0.7071 + 0.0im, 0.0 - 0.7071im]

The Hadamard sends 0|0\rangle to 12(0+1)\frac{1}{\sqrt{2}}(|0\rangle + |1\rangle), both amplitudes real and equal. Rx(pi/2) rotates halfway around the X axis of the Bloch sphere and produces a complex amplitude on 1|1\rangle: 12(0i1)\frac{1}{\sqrt{2}}(|0\rangle - i|1\rangle). Parameterized rotations like Rx, Ry, and Rz are the tunable knobs that variational algorithms adjust, which is exactly what Yao is built to differentiate through.

Example 3: Expectation Values

In practice you rarely read out a full state vector, because on real hardware you cannot. What you measure are expectation values of observables, such as the average of the Pauli ZZ operator. Yao computes these directly with expect.

# <Z> is +1 for |0>, and 0 for an equal superposition
println(real(expect(Z, zero_state(1))))
println(round(real(expect(Z, zero_state(1) |> H)), digits = 6))
1.0
0.0

For 0|0\rangle, a ZZ measurement always returns +1+1, so the expectation value is 1.01.0. For the equal superposition H0H|0\rangle, the two outcomes +1+1 and 1-1 are equally likely and average to 0.00.0. An expectation value like this is the quantity a variational algorithm minimizes: define an observable whose lowest expectation value encodes your answer, then let a classical optimizer tune the circuit’s rotation angles until it gets there.

Why Yao Stands Out

Two things set Yao apart from wrapping a Python framework. It is fast, because Julia compiles to native code and Yao’s register representation is built for it, so simulating dozens of qubits stays practical on a laptop. And it is differentiable: because the whole stack is Julia, you can take gradients of a circuit’s output through Julia’s automatic differentiation, which makes gradient-based training of quantum circuits direct rather than bolted on. If you are building variational or quantum machine learning models, those two properties are the reason to reach for it.

Where to Go Next

You now have a working Julia and Yao setup and have built and measured a real entangled state. Good next steps:

Was this tutorial helpful?