Education / Quantum Programming

Quantum Programming · Chapter 01

Quantum Programming Languages

How should a programming language express quantum computation and its classical control?

In this chapter · 3 sections
  1. 1.1 Language Families
  2. 1.2 Design Principles
  3. 1.3 Abstraction and Execution
Reading guide · preparation and approach

Starting point: Quantum Computing and Quantum Algorithms, or equivalent knowledge. No Python is assumed: the few Python lines in this chapter are explained where they appear, and the next chapter develops Python further.

Anchor example: Compare descriptions of the same controlled operation and measurement in different programming styles.

This lesson includes explanations, worked examples, and problems with solutions. Advanced results whose proofs are outside the chapter are identified explicitly.

In the earlier courses we described quantum computations as circuits and as matrices. A person reads a circuit drawing and knows what to do: put two qubits in zero, apply H to the first, apply a controlled-X, measure. We never had to say what the name of a qubit refers to, or exactly when a measurement result becomes available.

A computer has to be told all of that. So how should a programming language express a quantum computation and the classical decisions around it? We will write one small computation, the Bell preparation, in several styles and see what the styles share. Some instructions that every classical language allows turn out to be meaningless for qubits, so a quantum language needs rules against them. Once a program obeys those rules, it still has to be rewritten for each machine, and that forces a separation between what a program means and how a device carries it out.

We end by reading our first program in Qiskit, the framework the rest of the course uses.

1.1 Language Families

Suppose we want a computer to prepare the Bell state \((\ket{00}+\ket{11})/\sqrt2\). We know the circuit. How many genuinely different ways are there to write it down for a machine? We first compare two ways of organizing the instructions, and then ask who reads the resulting text.

1.1.1 Imperative and functional approaches

The most direct attempt copies the circuit drawing from left to right. We name the qubits and list the actions in time order:

qubits q0, q1, both starting in |0⟩
apply H to q0
apply CNOT with control q0 and target q1

This is an imperative description: a sequence of commands that change named things, one after another.

We could instead describe the same computation as one transformation built from smaller ones. The two-qubit operation is \(\mathrm{CNOT}\,(H\otimes I)\), and the program is that product applied to \(\ket{00}\). This is a functional description: it says what the result is, as a composition of transformations, rather than listing changes in order.

Does the choice of style change the state we get? It cannot, because both describe the same matrix product. Let us check it. \(H\) on the first qubit gives \((\ket{00}+\ket{10})/\sqrt2\), where the first label is q0. CNOT then flips q1 exactly when q0 is 1, giving \((\ket{00}+\ket{11})/\sqrt2\).

So the styles differ only in what they make easy to see. The imperative list looks like the drawing. The functional product makes composition visible. For example, the inverse of a product of unitaries is the product of their adjoints in reverse order, \((H\otimes I)\,\mathrm{CNOT}\), and a language that stores the product can build that inverse for us.

Bell-pair preparation. A Hadamard followed by CNOT makes (|00⟩+|11⟩)/√2. The first wire is the control.
Bell-pair preparation. A Hadamard followed by CNOT makes (|00⟩+|11⟩)/√2. The first wire is the control.

1.1.2 Standalone languages and embedded frameworks

Whichever style we choose, some program must read the text. Who? One answer is to invent a new language with its own grammar and its own compiler, the program that translates the text into instructions for a machine. That is a standalone language. Here is the Bell preparation, followed by measurement, in OpenQASM 3, a standalone language for circuits:

OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
h q[0];
cx q[0], q[1];
c = measure q;

Each line declares two qubits, declares two classical bits, applies a gate, or measures. Q# is another standalone language, built around operations that can be composed and inverted.

A second answer reuses an existing language, such as Python, and adds a library of quantum objects to it. The existing language is then called the host, and the result is an embedded framework. Qiskit is such a framework: it is a Python software development kit (SDK), not a separate language. We gain everything the host offers, such as loops, functions and files, without writing a new compiler.

What do we pay for that? The host runs on our ordinary computer while it builds the circuit. Suppose we write, in Python, “repeat three times: append H to qubit 0.” The loop runs once, on our laptop, and leaves behind a circuit with three separate H instructions. The quantum device never sees a loop. So in an embedded framework we must keep two moments apart: construction time, when the host builds the circuit, and execution time, when the circuit runs.

Problem 1.1. Why distinguish construction time from execution time?

Worked solution

The host may expand a loop into a fixed gate sequence before any quantum run. A measurement-dependent loop requires controller support and cannot be resolved during ordinary circuit construction.

1.2 Design Principles

So far the styles differ only on the surface. But a classical language also lets us copy a variable, reuse it freely, undo any step we like, and branch on any value. Which of these freedoms survive when the values are qubits? We test copying first, then undoing and branching, and each failure becomes a rule a quantum language should enforce.

1.2.1 Types, ownership, and no-cloning

In a classical program, writing y = x gives us a copy of the value in x. What should q2 = q1 mean when q1 is a qubit?

Suppose it meant “copy the state.” Then some unitary \(U\) would send \(\ket\psi\ket0\) to \(\ket\psi\ket\psi\) for every \(\psi\). On the basis states this forces \(U\ket0\ket0=\ket{00}\) and \(U\ket1\ket0=\ket{11}\). Since \(U\) is linear, \(U\ket+\ket0\) must then be \((\ket{00}+\ket{11})/\sqrt2\). A copy of \(\ket+\) would be \(\ket+\ket+=(\ket{00}+\ket{01}+\ket{10}+\ket{11})/2\), which is a different state. So no such \(U\) exists. This is the no-cloning theorem.

So q2 can only be a second name for the same qubit. A name that refers to a qubit without containing its state is called a handle. Assigning a handle copies the reference, never the quantum state.

Handles bring a new hazard. Suppose we write cx(q, q), a CNOT whose control and target are the same qubit. A CNOT acts on two distinct systems, so this instruction has no meaning. A language can reject it with a type rule, a rule about how values may be used that is checked before the program runs. A linear rule requires each qubit value to be used exactly once. An affine rule requires it to be used at most once, so a qubit may also be discarded. The quantum lambda calculus of Selinger and Valiron is built on a linear rule.

Qiskit enforces neither rule. It refuses a gate whose two arguments name the same qubit, but only when we try to append it. No-cloning still holds physically whether or not the language checks for it.

1.2.2 Reversibility, measurement, and classical control

Next, undoing. A routine built from unitaries \(U_1,\dots,U_k\) has the inverse \(U_1^\dagger\cdots U_k^\dagger\), so a language can generate it automatically, as in our functional description. Does this work for every routine?

Take a reset, which sets a qubit to \(\ket0\). It sends \(\ket0\) to \(\ket0\) and also \(\ket1\) to \(\ket0\). After it, nothing tells us which input we had, so no operation can undo it. A measurement whose result we throw away loses information in the same way. So only the unitary parts of a program have automatic inverses.

Now branching. A measurement produces a classical bit, and we often want a later gate to depend on it. Could we write an ordinary host-language if: “if qubit 0 is 1, apply X to qubit 1”? At construction time no measurement has happened yet. The host is only building a description, so there is no bit to test. The condition must therefore be an instruction inside the circuit that names a recorded bit. The controller evaluates it during execution, when the bit exists. This is called classical control.

Problem 1.2. Can every quantum function have an automatic inverse?

Worked solution

Only operations with a specified reversible action can be inverted this way. Measurement with a discarded outcome and reset lose information in the circuit-level description.

1.3 Abstraction and Execution

We now have a description that a language can check. Two questions remain before we can run it. Real devices do not all offer the same gates, so how can one description serve many machines? And what does the description look like in Qiskit? Answering the first separates meaning from realization, and the second gives us our first program.

1.3.1 Circuit languages and intermediate representations

Our Bell description uses H and CNOT. Suppose a device can apply only a few kinds of rotation and one particular two-qubit gate. The operations a device implements directly are its native gates. Suppose also that each qubit can interact only with certain neighbours. Who turns our description into something this device can run?

A compiler does, and to rewrite a circuit safely it must know which operations act on which qubits and which must happen before which. A form of the program that records exactly this is called an intermediate representation. The compiler can replace each H by native rotations that agree with it up to a global phase. If two qubits that need a CNOT are not neighbours, it can insert swaps that move their states next to each other. That step is called routing.

After such rewriting the gate list looks different, yet the program means the same computation. So a language should keep two things apart: the circuit’s mathematical meaning, and its device-specific realization. Standalone circuit languages such as OpenQASM also serve as an exchange format between tools. Writing a circuit out in that format does not make every destination able to run it, since a device may lack, for example, classical control during execution.

1.3.2 Qiskit as a Python SDK

Qiskit has no grammar of its own. So what does our Bell preparation look like when Python is the host? We read the program below a few lines at a time. It uses only a handful of Python features, and each one is there because the program needs it.

Python starts with a small core. The from … import … lines borrow two definitions from the installed Qiskit package: QuantumCircuit, which describes circuits, and Statevector, which computes amplitudes. Lines beginning with # are comments for human readers.

We want a recipe we can reuse, so we package it as a function. The line def bell(): starts a function named bell. The indented lines below it form its body. Inside, circuit = QuantumCircuit(2) creates an empty circuit on qubits 0 and 1, and the equals sign gives it the name circuit. The dot in circuit.h(0) calls a method, an operation that belongs to that object. Here it appends H on qubit 0, and circuit.cx(0, 1) appends a CNOT with control 0 and target 1. Finally return circuit hands the finished circuit back.

The line circuit = bell() runs the recipe. Notice what it produces: a description, not a quantum run. What should that description contain? Before running anything we predict the amplitudes of \(\ket{00},\ket{01},\ket{10},\ket{11}\): \((1/\sqrt2,0,0,1/\sqrt2)\). In Python, 2**-0.5 is \(2^{-1/2}=1/\sqrt2\).

Statevector.from_instruction(circuit) computes the amplitudes of the circuit’s output by an ordinary classical calculation. The method equiv compares that state with our prediction, ignoring any global phase. The word assert turns the comparison into a check. If the claim is true, nothing happens; if it is false, Python stops with an error. So a silent run means the prediction held. The last line prints a text drawing of the circuit.

"""Runnable companion for Quantum Programming, Chapter 1."""
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector

# A Python function builds a circuit; it does not execute a quantum device.
def bell():
    circuit = QuantumCircuit(2)
    circuit.h(0)
    circuit.cx(0, 1)
    return circuit

circuit = bell()
assert Statevector.from_instruction(circuit).equiv(
    Statevector([2**-0.5, 0, 0, 2**-0.5]))
print(circuit.draw(output="text"))

Companion script: chapter_01.py.

The program shows the two roles we separated. The function builds a description, and Statevector evaluates it on a classical computer. Neither step touches a quantum device.

Problem 1.3. Does assigning other = circuit create an independent circuit?

Worked solution

No. Both Python names refer to the same mutable object. Use circuit.copy() when subsequent edits must be independent. Neither action copies a physical quantum state.

Our program passed its check on this computer. Would it give the same result on someone else’s, with whatever versions of Python and Qiskit they happen to have installed?

Sources and further study: OpenQASM language specification; Selinger and Valiron, quantum lambda calculus.

Summary

Questions and worked problems

These exercises draw on the whole chapter. Attempt each question before opening a hint, explain your reasoning, then compare it with the worked solution.

1. Quick check §

How many instructions does this circuit contain, what is its depth, and what state does it prepare? When does the Python loop run?

from qiskit import QuantumCircuit

circuit = QuantumCircuit(1)
for k in range(3):
    circuit.h(0)
print(dict(circuit.count_ops()), circuit.depth())
Hint 1

The loop executes while the circuit is being built.

Hint 2

\(H^3=H\).

Worked solution

The loop runs at construction time, in Python, and appends three separate H instructions: count_ops reports {'h': 3} and the depth is \(3\). Since \(H^2=I\), the state is \(H\ket0=\ket+\). The quantum device never sees a loop; it receives three gates, which a compiler may simplify to one.

from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector

circuit = QuantumCircuit(1)
for k in range(3):
    circuit.h(0)
assert dict(circuit.count_ops()) == {"h": 3} and circuit.depth() == 3
assert Statevector.from_instruction(circuit).equiv(Statevector.from_label("+"))

Revisit the relevant explanation

2. Quick check §

Classify Q#, OpenQASM, and Qiskit as a standalone quantum programming language, a circuit-level description language, or a framework embedded in a host language. Which of them is a Python library?

Hint 1

Ask whether each has its own syntax and compiler.

Hint 2

Section 1.1.2 names all three.

Worked solution

Q# is a standalone quantum programming language with its own syntax and compiler. OpenQASM is a circuit-level language for describing operations, measurements, and classical control, often used as an exchange format between tools. Qiskit is a Python SDK: a library that uses Python to construct circuit objects, which it can then compile, simulate, export, or submit. Its programs are Python programs that build quantum instructions.

Revisit the relevant explanation

3. Quick check §

What happens when you ask for the inverse of a circuit that ends in a measurement, and why?

from qiskit import QuantumCircuit

circuit = QuantumCircuit(1, 1)
circuit.h(0)
circuit.measure(0, 0)
circuit.inverse()
Hint 1

A measurement is not a unitary.

Hint 2

Try it and read the error message.

Worked solution

Qiskit raises a CircuitError, “inverse() not implemented for measure.” A measurement maps different input states to the same output record and has no unitary inverse, so no automatic adjoint exists. Only purely unitary routines can be inverted gate by gate; a routine containing measurement or reset needs a different treatment, such as removing the measurement before inverting.

Revisit the relevant explanation

4. Worked problem §

Predict the statevector produced by each circuit before running it: (a) H on qubit 1 followed by cx(1, 0); (b) H on qubit 1 followed by cx(0, 1). Qiskit lists amplitudes in the order \(00,01,10,11\) of the labels \(q_1q_0\). Which circuit makes a Bell state?

Hint 1

In (b), the control qubit 0 is still \(\ket0\).

Hint 2

\((\ket{00}+\ket{11})/\sqrt2\) is symmetric under exchanging the qubits.

Worked solution

(a) H puts qubit 1 in \(\ket+\); cx(1, 0) flips qubit 0 when qubit 1 is \(1\), giving \((\ket{00}+\ket{11})/\sqrt2\), the same Bell state as the chapter’s circuit, because that state is symmetric under exchanging the qubits. (b) The control, qubit 0, is \(\ket0\), so the CNOT does nothing: the state is \(\ket+\) on qubit 1 and \(\ket0\) on qubit 0, with amplitudes \((0.707,0,0.707,0)\), a product state. The roles of control and target, not just which gates appear, determine the result.

from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector

a = QuantumCircuit(2)
a.h(1)
a.cx(1, 0)
assert Statevector.from_instruction(a).equiv(Statevector([2**-0.5, 0, 0, 2**-0.5]))

b = QuantumCircuit(2)
b.h(1)
b.cx(0, 1)
assert Statevector.from_instruction(b).equiv(Statevector([2**-0.5, 0, 2**-0.5, 0]))

Revisit the relevant explanation

5. Worked problem §

Build \(U\), which applies H and then S to one qubit. (a) Predict the instructions of U.inverse(). (b) Check that composing U with its inverse gives the identity operator. (c) Why must the inverse reverse the order as well as invert each gate?

Hint 1

\((SH)^{-1}=H^{-1}S^{-1}\).

Hint 2

\(H^{-1}=H\) and \(S^{-1}=S^\dagger\).

Worked solution

(a) The inverse lists sdg, then h. (b) The composition is the identity operator, which Operator equivalence confirms. (c) \(U=SH\) as a matrix (H acts first), so \(U^{-1}=H^{-1}S^{-1}=HS^\dagger\): the last gate applied must be undone first, exactly as when unwinding nested function calls. Inverting each gate without reversing the order would give \(S^\dagger H\), which is not the inverse.

from qiskit import QuantumCircuit
from qiskit.quantum_info import Operator

u = QuantumCircuit(1)
u.h(0)
u.s(0)
inverse = u.inverse()
assert [instruction.operation.name for instruction in inverse.data] == ["sdg", "h"]
assert Operator(u.compose(inverse)).equiv(Operator(QuantumCircuit(1)))

Revisit the relevant explanation

6. Worked problem §

A student wants to apply X to qubit 1 only if a measurement of qubit 0 gives 1, and writes a Python if statement that inspects a variable holding the measurement result. Explain why this cannot work at construction time, and write the version that records the condition as an instruction executed during the run.

Hint 1

When the Python code runs, no measurement has happened.

Hint 2

Qiskit provides a context manager that conditions instructions on a classical bit.

Worked solution

A Python if runs while the circuit is being built, before any shot executes, so no measured value exists to inspect. The condition must become part of the circuit: an instruction that refers to the classical bit and is evaluated by the controller during each run. In Qiskit, the if_test context adds an if_else instruction whose body applies X when the bit is 1.

from qiskit import QuantumCircuit

circuit = QuantumCircuit(2, 1)
circuit.h(0)
circuit.measure(0, 0)
with circuit.if_test((circuit.clbits[0], 1)):
    circuit.x(1)
assert "if_else" in circuit.count_ops()

Revisit the relevant explanation

7. Worked problem §

Export the Bell circuit to OpenQASM 3 with qiskit.qasm3.dumps. Identify which output line declares the qubits and which lines are the gates. What information would an exporter need in order to represent the circuit on a specific device, beyond this text?

Hint 1

Look for the qubit declaration and the gate statements.

Hint 2

Consider native gates, layout, and timing.

Worked solution

The output contains a header (OPENQASM 3.0 and an include of the standard gates), a declaration qubit[2] q, and the gates h q[0] and cx q[0], q[1]. This describes the logical circuit. A device implementation also needs the native gate decomposition, the mapping of logical to physical qubits, any routing, and timing or pulse details. Exporting preserves meaning, not a device-specific realization.

from qiskit import QuantumCircuit, qasm3

bell = QuantumCircuit(2)
bell.h(0)
bell.cx(0, 1)
text = qasm3.dumps(bell)
assert "qubit[2] q;" in text and "h q[0];" in text and "cx q[0], q[1];" in text
print(text)

Revisit the relevant explanation

8. Misconception §

A student reasons: “If r = q refers to the same qubit, then cx(q, r) uses one qubit as control and its copy as target.” What does Qiskit do with this code, and what physical principle does the check reflect?

from qiskit import QuantumCircuit

circuit = QuantumCircuit(2)
q = circuit.qubits[0]
r = q
circuit.cx(q, r)
Hint 1

Assigning a handle does not copy a quantum state.

Hint 2

A two-qubit gate needs two distinct qubits.

Worked solution

Qiskit raises a CircuitError, “duplicate bit arguments.” Assigning r = q creates a second name for the same qubit, not a copy of its state; no-cloning forbids making an independent copy of an unknown state. A CNOT needs two different qubits, and using one wire as both control and target is meaningless. Qiskit catches this when the gate is appended; a language with linear types could reject it before the program runs.

Revisit the relevant explanation

9. Extension §

A linear type system requires every qubit handle to be used exactly once and forbids duplicating it. (a) Which of these would it reject at compile time: a function that returns the same qubit twice; cx(q, q); a function that silently drops a qubit that is entangled with others? (b) What does Qiskit check instead, and when? (c) Give one benefit and one cost of static enforcement.

Hint 1

Duplication and silent discarding are both restricted by linear types.

Hint 2

Qiskit checks arguments when instructions are appended.

Worked solution

(a) A linear type system rejects all three: returning a qubit twice and cx(q, q) both duplicate a handle, and dropping an entangled qubit discards a resource without an explicit operation (an affine system would allow discarding but not duplication). (b) Qiskit is a Python library, so it checks at run time of the construction code: duplicate arguments raise an error when the gate is appended, while silently ignoring a qubit is allowed. (c) Static checks catch mistakes before any execution and document resource use; the cost is a stricter language in which some valid programs, such as those that measure and discard deliberately, need extra annotations.

Revisit the relevant explanation

10. Extension §

A one-qubit circuit applies X with global_phase \(\pi/2\), so it implements \(iX\). (a) Uncontrolled, how does it differ from X? (b) Form its controlled version with control(1) and inspect the matrix. What happened to the phase? (c) Why must a programming framework track global phase for this reason?

Hint 1

Uncontrolled, \(iX\) and \(X\) give identical probabilities for every input.

Hint 2

Controlled, the phase applies only on the control-1 branch.

Worked solution

(a) Uncontrolled, \(iX\) and \(X\) differ by a global phase and are physically indistinguishable. (b) The controlled matrix maps the control-1 states by \(iX\) and leaves the control-0 states alone: the factor \(i\) now distinguishes the two branches, so the controlled version is CNOT followed by S on the control. (c) A routine that is correct “up to global phase” can become wrong when controlled. Qiskit stores global_phase on the circuit and includes it in control(), so routines used inside controlled operations must have their phase specified, not discarded.

import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Operator

ix = QuantumCircuit(1, global_phase=np.pi / 2)
ix.x(0)
controlled = ix.control(1)
expected = QuantumCircuit(2)
expected.cx(0, 1)
expected.s(0)
assert Operator(controlled).equiv(Operator(expected))

Revisit the relevant explanation

Search this book

Search the course.