Write quantum computations with Program¶
Program is what you build and pass to every FatQat
backend. If you are used to a Circuit class, this is FatQat's broader
equivalent: it can contain circuit gates, measurements, classical conditions,
symbolic parameters, qubits and qudits, and direct physical controls. A Program
records its resources and ordered instructions without deciding how a backend
will execute them.
If you want the compiler to map and route a static gate circuit before it is
run, construct a LogicalProgram instead. It keeps
the same register and authoring model while restricting operations to the
device-independent compiler frontend. The compiler workflow
describes its supported gates and source restrictions.
This chapter begins with a small qubit circuit, then introduces registers, feedforward, reusable parameters, mixed local dimensions, drawing, and direct controls.
Declare the program's resources¶
For a small program, pass the number of quantum and classical slots directly:
import fatqat as fq
program = fq.Program(2, 2) # two qubits and two classical bits
As a Program grows, explicit registers give its resources useful names and let it contain more than one quantum or classical register:
>>> import fatqat as fq
>>> import fatqat.operations as ops
>>> data = fq.QuantumRegister(2, name="data")
>>> readout = fq.ClassicalRegister(2, name="readout")
>>> program = fq.Program([data], [readout])
Indexing a register identifies one of its slots. Explicit references such as
data[0] remain unambiguous as the Program grows. Use a bare integer only
when the Program has a single register of that kind.
Need rows and columns?
A GridRegister adds row and column views:
>>> grid = fq.GridRegister(2, 3, name="grid")
>>> grid_program = fq.Program([grid])
>>> grid_program.add(ops.RX(0.2), grid.row(1))
The row view groups several targets for one operation. It describes structure inside the Program, not placement on a physical device. Choose physical placement when executing the Program.
Compose operations in order¶
Add operations in the order they should occur:
>>> program.add(ops.H, data[0])
>>> program.add(ops.RY(0.3), data[1])
>>> program.add(ops.CX, (data[0], data[1]))
Fixed gates such as H and CX can be added directly. Create a
parameterized gate such as RY by passing its angle. For an operation with
several targets, pass one tuple in operand order. For CX, the control comes
first and the target second.
These few operations illustrate the calling pattern. The operations API reference lists the complete operation set and exact definitions.
Use mid-circuit measurement and feedforward¶
A measurement is an ordered Program instruction, just like a gate. It can end a circuit or store a value that controls a later operation:
>>> dynamic = fq.Program(2, 2)
>>> dynamic.add(ops.H, 0)
>>> dynamic.measure(0, 0)
>>> dynamic.add(ops.X, 1, condition=(0, 1))
>>> dynamic.add(ops.Reset, 0)
>>> dynamic.measure(1, 1)
On each shot, the first measurement writes the result from qubit 0 to
classical bit 0. When that value is 1, X flips qubit 1. Reset then returns
qubit 0 to |0> without changing the stored value. The final measurement
writes qubit 1 to classical bit 1, so the two classical bits agree:
>>> counts = (
... fq.simulator.Simulator()
... .run(dynamic, shots=100, simulation_config={"seed": 7})
... .result()
... .get_counts()
... )
>>> sorted(counts)
['00', '11']
>>> sum(counts.values())
100
Not every backend supports this mid-program behavior. If measurement, reset, or classical feedforward is unavailable, the backend rejects the Program rather than silently changing it.
Mid-circuit measurement can make execution dynamic because later operations may depend on a sampled outcome. Estimating the resulting distribution therefore requires repeated shots of the same Program.
Reuse a parameterized Program¶
A Parameter acts as a placeholder for a numeric
operation argument. Calling assign_parameters returns a new Program, so the
original template remains available for other values:
>>> import numpy as np
>>> theta = fq.Parameter("theta")
>>> template = fq.Program(1)
>>> template.add(ops.RY(theta), 0)
>>> quarter_turn = template.assign_parameters({theta: np.pi / 2})
>>> half_turn = template.assign_parameters({theta: np.pi})
>>> template_backend = fq.simulator.Simulator("SV", runtime="numpy")
>>> quarter_state = template_backend.run(quarter_turn).result().get_statevector()
>>> half_state = template_backend.run(half_turn).result().get_statevector()
>>> round(float(abs(quarter_state[1]) ** 2), 3)
0.5
>>> round(float(abs(half_state[1]) ** 2), 3)
1.0
The binding map uses the parameter object, as in {theta: value}, rather
than its display name. Reuse one object in several gates when they should share
a value. Use ParameterVector for an explicitly
ordered group. The simulation chapter shows how
run_sweep evaluates a batch of
parameter values from the same template without rebuilding the Program.
Mix qubits and qutrits¶
A register's dim gives the number of states available to each slot. With
the default dim=2, a quantum slot is a qubit and a classical slot is a bit.
Setting dim=3 creates a qutrit or a classical trit. Registers with different
dimensions can coexist in one Program:
>>> qubit = fq.QuantumRegister(1, name="qubit")
>>> qutrit = fq.QuantumRegister(1, name="qutrit", dim=3)
>>> bit = fq.ClassicalRegister(1, name="bit")
>>> trit = fq.ClassicalRegister(1, name="trit", dim=3)
>>> hybrid = fq.Program([qubit, qutrit], [bit, trit])
>>> hybrid.add(ops.X, qubit[0])
>>> hybrid.add(ops.Shift(2), qutrit[0])
>>> hybrid.measure(
... (qubit[0], qutrit[0]),
... (bit[0], trit[0]),
... )
X prepares the qubit in |1>, while the dimension-generic Shift(2) maps
the qutrit from |0> to |2>. Measurement pairs each quantum slot with a
classical slot of the same dimension:
>>> hybrid_result = (
... fq.simulator.Simulator("SV")
... .run(hybrid, shots=20, simulation_config={"seed": 7})
... .result()
... )
>>> hybrid_result.get_counts_as_tuples()
{(1, 2): 20}
>>> hybrid_result.get_counts()
{'12': 20}
Tuple keys list the flattened classical slots in declaration order, making
the qubit value 1 and qutrit value 2 explicit. String keys use the same
left-to-right order. Each operation determines which local dimensions it
accepts, so consult the qudit operation reference
when you move beyond Shift.
Draw a Program¶
Drawing lets you check the instruction order and classical conditions before execution. Under the hood, FatQat adapts the Program to QuTiP-QIP's circuit-drawing tools. The examples below use this Program:
import fatqat as fq
import fatqat.operations as ops
program = fq.Program(2, 2)
program.add(ops.H, 0)
program.measure(0, 0)
program.add(ops.X, 1, condition=(0, 1))
program.add(ops.Reset, 0)
program.measure(1, 1)
Text renderer¶
Pass "text" to return a terminal-friendly string, then print it:
diagram = program.draw("text")
print(diagram)
┌───────────┐ ┌───┐
q1 :─────────────────┤ X if c0=1 ├───────────┤ M ├───
└─────┬─────┘ └─╥─┘
┌───┐ ┌───┐ ║ ┌─────┐ ║
q0 :───┤ H ├──┤ M ├────────║────────┤ |0> ├────║─────
└───┘ └─╥─┘ ║ └─────┘ ║
║ ║ ║
c1 :════════════║══════════║═══════════════════╩═════
║ ║
║ ║
c0 :════════════╩══════════█═════════════════════════
Reproduce this output
import fatqat as fq
import fatqat.operations as ops
program = fq.Program(2, 2)
program.add(ops.H, 0)
program.measure(0, 0)
program.add(ops.X, 1, condition=(0, 1))
program.add(ops.Reset, 0)
program.measure(1, 1)
print(program.draw("text"), end="")
The condition appears directly on X, and the vertical connector shows that
classical bit 0 controls whether the gate is applied.
Matplotlib renderer¶
With no renderer argument, draw() returns a Matplotlib figure:
import matplotlib.pyplot as plt
figure = program.draw()
plt.show()

Reproduce this figure
import fatqat as fq
import fatqat.operations as ops
import matplotlib.pyplot as plt
program = fq.Program(2, 2)
program.add(ops.H, 0)
program.measure(0, 0)
program.add(ops.X, 1, condition=(0, 1))
program.add(ops.Reset, 0)
program.measure(1, 1)
figure = program.draw()
plt.show()
Both renderers show the instructions recorded in the Program, not the result of executing them.
Important
The circuit drawing uses one wire per quantum or classical slot and does not
display a register's local dimension. Qubit and qutrit wires therefore look
the same. Qudit and custom operations appear as labeled boxes. A
PulseOperation cannot be represented by this
circuit renderer.
A Program can also contain direct physical controls. Add them as
PulseOperation instructions without quantum targets; their control channels
identify the physical resources. The
Hamiltonian-emulation chapter visualizes these
controls as waveforms and timelines instead.
Understand validation¶
Program validates structure as you build it: references must belong to the
Program, each operation must receive the expected number of targets, and
measured quantum and classical dimensions must agree. The selected backend
then checks whether it supports those operations, dimensions, controls, and
classical behavior.
Continue with choose how much physics to model to see how different backends interpret a Program. Use the Program API when you need exact accepted forms or validation behavior.