Repository navigation
Building and Simulating Circuits
Build a quantum circuit, add common gates, execute a local simulation, and read amplitudes, probabilities, expectation values, samples, and counts.
Version: FlagQuantum 0.3.1. All four complete scripts below were verified on macOS Apple Silicon with Python 3.12.14 and PyTorch 2.14.1. They explicitly select local PyTorch CPU statevector simulation and require no GPU, provider credentials, or quantum hardware.
Prepare your environment using Installation and Compatibility. For terminology, see Core Concepts.
Formula/code review — October 20, 2026: The equations are paired with code excerpts, and the local CPU examples were re-run with Python 3.12.14 and PyTorch 2.13.0. See the review record for executed commands, distributed checks, and untested hardware/provider paths. Earlier environment and output records are retained.
fq.Circuit(n) creates an n-qubit circuit. Qubit indices are zero-based: a three-qubit circuit has qubits 0, 1, and 2. Without an explicit input state, every qubit starts in zero.
Add gates with methods such as .h(0) or .cx(0, 1). Calls are applied in the order written. Gate methods modify the circuit builder and return it, so both of these styles are valid:
- Chained calls:
circuit = fq.Circuit(2).h(0).cx(0, 1). - Separate statements: create
circuit = fq.Circuit(2), then callcircuit.h(0)andcircuit.cx(0, 1).
Creating another Python reference to the same builder does not make an independent circuit. Start with a fresh fq.Circuit(...) when comparing different gate sequences, as in the first example.
| Method | Effect |
|---|---|
.x(q) |
Pauli X: flips the computational-basis bit. |
.y(q) |
Pauli Y: flips the basis bit and applies phases. |
.z(q) |
Pauli Z: changes the sign of the amplitude whose bit is 1. |
.h(q) |
Hadamard: creates or recombines a superposition. |
.s(q) / .sdg(q)
|
Adds a phase of +π/2 / −π/2 to the 1 component. |
.t(q) / .tdg(q)
|
Adds a phase of +π/4 / −π/4 to the 1 component. |
.rx(q, theta) |
Rotation around the X axis. |
.ry(q, theta) |
Rotation around the Y axis. |
.rz(q, theta) |
Rotation around the Z axis. |
Rotation angles are in radians. For example, use math.pi, not 180, for a half-turn. A gate's action on phase can matter even when its immediate computational-basis probabilities stay unchanged.
| Method | Effect |
|---|---|
.cx(control, target) |
Flips the target bit when the control bit is 1. |
.cz(control, target) |
Changes the sign of the component where both selected bits are 1. |
.swap(q0, q1) |
Exchanges the states of the two qubits. |
For CX, control and target are different roles: .cx(0, 1) and .cx(1, 0) generally implement different circuits. Do not swap these arguments to compensate for a result-order mismatch.
The scripts select execution explicitly with fq.ExecutionOptions(mode="statevector", backend="pytorch", device="cpu") and pass the options to fq.run(...).
Use outputs= to say what you need:
-
fq.probabilities()requests exact computational-basis probabilities. -
fq.expectation(fq.Z(0))requests a Z expectation value. -
fq.samples(...)andfq.counts(...)request shot-based outcomes and require a shot count.
fq.run returns an execution-result object. Constructing an output request does not execute the circuit by itself.
Save as simulate_single_qubit.py:
import math
import torch
import flagquantum as fq
options = fq.ExecutionOptions(mode="statevector", backend="pytorch", device="cpu")
cases = [
("Initial", fq.Circuit(1), [1.0, 0.0]),
("X", fq.Circuit(1).x(0), [0.0, 1.0]),
("H", fq.Circuit(1).h(0), [0.5, 0.5]),
("H-Z", fq.Circuit(1).h(0).z(0), [0.5, 0.5]),
("H-Z-H", fq.Circuit(1).h(0).z(0).h(0), [0.0, 1.0]),
("Rx(pi)", fq.Circuit(1).rx(0, math.pi), [0.0, 1.0]),
("Ry(pi/3)", fq.Circuit(1).ry(0, math.pi / 3), [0.75, 0.25]),
("H-Rz(pi)-H", fq.Circuit(1).h(0).rz(0, math.pi).h(0), [0.0, 1.0]),
]
for label, circuit, expected in cases:
probabilities = fq.run(circuit, outputs=fq.probabilities(), options=options).probabilities[0]
print(f"{label}: {[round(p.item(), 6) for p in probabilities]}")
assert torch.allclose(probabilities, torch.tensor(expected), atol=1e-6)
print("Single-qubit checks passed.")Run python simulate_single_qubit.py. Expected output:
Initial: [1.0, 0.0]
X: [0.0, 1.0]
H: [0.5, 0.5]
H-Z: [0.5, 0.5]
H-Z-H: [0.0, 1.0]
Rx(pi): [0.0, 1.0]
Ry(pi/3): [0.75, 0.25]
H-Rz(pi)-H: [0.0, 1.0]
Single-qubit checks passed.
Each vector lists the probabilities of 0 and 1. The assertions compare numerical results with the expected distributions using a floating-point tolerance.
H and H–Z produce the same probabilities, but their states have different relative phases. The final H in H–Z–H turns that phase difference into a deterministic outcome of 1. This is why probabilities alone do not capture every effect of a quantum gate.
For a Y rotation of angle theta applied to zero, the probability of 1 is sin(theta / 2)**2. At theta = pi/3, that probability is 0.25.
Create a Bell state by applying H to qubit 0, followed by CX from qubit 0 to qubit 1. Its state is:
| Formula / mathematical operation | Corresponding code excerpt | Check / interpretation |
|---|---|---|
circuit.h(0); circuit.cx(0, 1) |
simulate_bell.py executes H before controlled-X. | |
state = result.statevector()[0] |
Compare all amplitudes with expected, with atol=1e-6. | |
state.abs().square() |
Compare against result.probabilities[0]. |
Save as simulate_bell.py:
import math
import torch
import flagquantum as fq
circuit = fq.Circuit(2)
circuit.h(0)
circuit.cx(0, 1)
result = fq.run(
circuit,
outputs=[
fq.probabilities(),
fq.expectation(fq.Z(0), name="z0"),
fq.expectation(fq.Z(0) @ fq.Z(1), name="zz"),
],
options=fq.ExecutionOptions(mode="statevector", backend="pytorch", device="cpu"),
)
state = result.statevector()[0]
print("Statevector shape:", tuple(result.statevector().shape))
print("Real amplitudes:", [round(a.real.item(), 6) for a in state])
print("Imaginary amplitudes:", [round(a.imag.item(), 6) for a in state])
print("Probabilities:", [round(p.item(), 6) for p in result.probabilities[0]])
print(f"<Z0>: {result.expectation('z0').item():.6f}")
print(f"<Z0 Z1>: {result.expectation('zz').item():.6f}")
expected = torch.tensor([1 / math.sqrt(2), 0.0, 0.0, 1 / math.sqrt(2)], dtype=state.dtype)
assert torch.allclose(state, expected, atol=1e-6)
assert torch.allclose(result.probabilities[0], state.abs().square(), atol=1e-6)
assert abs(result.expectation("z0").item()) < 1e-6
assert abs(result.expectation("zz").item() - 1.0) < 1e-6
print("Bell simulation checks passed.")Run python simulate_bell.py. Expected output:
Statevector shape: (1, 4)
Real amplitudes: [0.707107, 0.0, 0.0, 0.707107]
Imaginary amplitudes: [0.0, 0.0, 0.0, 0.0]
Probabilities: [0.5, 0.0, 0.0, 0.5]
<Z0>: 0.000000
<Z0 Z1>: 1.000000
Bell simulation checks passed.
The basis order is 00, 01, 10, 11. Amplitudes are complex numbers; their squared magnitudes give the probabilities. The first result dimension is the batch dimension, so [0] selects the single circuit's output, not qubit 0.
Qubit 0 has equal probabilities of 0 and 1, giving a Z expectation of zero. The two bits always agree, so their Z correlation is +1. Named outputs let you retrieve each expectation without ambiguity.
The following small circuits make two-qubit gate behavior visible without relying on a random measurement split.
Save as simulate_two_qubit_gates.py:
import torch
import flagquantum as fq
options = fq.ExecutionOptions(mode="statevector", backend="pytorch", device="cpu")
copied = fq.run(fq.Circuit(2).x(0).cx(0, 1), outputs=fq.probabilities(), options=options)
swapped = fq.run(fq.Circuit(2).x(0).swap(0, 1), outputs=fq.probabilities(), options=options)
phased = fq.run(fq.Circuit(2).h(0).h(1).cz(0, 1), outputs=fq.probabilities(), options=options)
print("X(0), CX(0,1):", [round(p.item(), 6) for p in copied.probabilities[0]])
print("X(0), SWAP(0,1):", [round(p.item(), 6) for p in swapped.probabilities[0]])
print("H(0), H(1), CZ(0,1) amplitudes:", [round(a.real.item(), 6) for a in phased.statevector()[0]])
assert torch.allclose(copied.probabilities[0], torch.tensor([0.0, 0.0, 0.0, 1.0]), atol=1e-6)
assert torch.allclose(swapped.probabilities[0], torch.tensor([0.0, 1.0, 0.0, 0.0]), atol=1e-6)
expected = torch.tensor([0.5, 0.5, 0.5, -0.5], dtype=phased.statevector().dtype)
assert torch.allclose(phased.statevector()[0], expected, atol=1e-6)
print("Two-qubit gate checks passed.")Run python simulate_two_qubit_gates.py. Expected output:
X(0), CX(0,1): [0.0, 0.0, 0.0, 1.0]
X(0), SWAP(0,1): [0.0, 1.0, 0.0, 0.0]
H(0), H(1), CZ(0,1) amplitudes: [0.5, 0.5, 0.5, -0.5]
Two-qubit gate checks passed.
- X on qubit 0 prepares
10; CX then flips qubit 1, producing11. - SWAP instead moves the excitation from
10to01. - H on each qubit prepares four equal positive amplitudes. CZ changes the
11amplitude from +0.5 to −0.5. All four probabilities remain 0.25, even though the state has changed.
The real-amplitude display in the CZ example is sufficient for this particular circuit; other circuits may have nonzero imaginary components, as accommodated in Example 2.
To build a three-qubit GHZ state, apply H to qubit 0 and then use qubit 0 to control X on qubits 1 and 2:
| Formula / mathematical operation | Corresponding code excerpt | Check / interpretation |
|---|---|---|
fq.Circuit(3).h(0).cx(0, 1).cx(0, 2) |
The complete simulate_ghz_shots.py circuit has support only on 000 and 111. | |
result.probabilities[0] |
The script checks the marginal on qubits (0, 2): only 00 and 11 have probability 1/2, separately from random full-register samples. | |
sum(counts.values()) |
The sampled counts sum to the requested shots; individual counts fluctuate. |
The next script builds the gate sequence with a Python loop. It requests exact probabilities on a subset of qubits and full-register samples and counts in the same execution.
Save as simulate_ghz_shots.py:
import torch
import flagquantum as fq
circuit = fq.Circuit(3).h(0)
for target in (1, 2):
circuit.cx(0, target)
result = fq.run(
circuit,
outputs=[
fq.probabilities(qubits=(0, 2)),
fq.samples(name="bits"),
fq.counts(),
],
shots=128,
options=fq.ExecutionOptions(mode="statevector", backend="pytorch", device="cpu", seed=7),
)
samples = result.measurement("bits").value
counts = result.counts[0]
valid_samples = bool((samples[..., 0] == samples[..., 1]).all() and (samples[..., 1] == samples[..., 2]).all())
valid_counts = set(counts).issubset({"000", "111"})
print("Qubits [0, 2] probabilities:", [round(p.item(), 6) for p in result.probabilities[0]])
print("Sample shape:", tuple(samples.shape))
print("All three sampled bits agree:", valid_samples)
print("Total counts:", sum(counts.values()))
print("Only 000/111 counted:", valid_counts)
assert torch.allclose(result.probabilities[0], torch.tensor([0.5, 0.0, 0.0, 0.5]), atol=1e-6)
assert valid_samples and valid_counts and sum(counts.values()) == 128
print("Shot-based simulation checks passed.")Run python simulate_ghz_shots.py. Expected output:
Qubits [0, 2] probabilities: [0.5, 0.0, 0.0, 0.5]
Sample shape: (1, 128, 3)
All three sampled bits agree: True
Total counts: 128
Only 000/111 counted: True
Shot-based simulation checks passed.
result.measurement("bits").value retrieves the named sample tensor. Its shape is (batch, shots, measured_qubits). result.counts[0] retrieves the first batch entry's frequency dictionary, whose keys here are full-register bitstrings.
Inspect counts to see the individual frequencies. The count total is exactly 128; the split between 000 and 111 need not be exactly 64 and 64. The output above prints invariants rather than a platform-specific random split. A seed helps reproduce a run in a compatible environment, but does not promise identical draws across devices, backends, or releases.
The probability output selects qubits (0, 2), while the sample and count requests select all qubits by default. Each request defines its own selection. Samples and counts are separate requests, so do not assume they reuse the same random draws. See Qubit Ordering and Result Conventions for subset ordering and indices.
| What you requested | How to read it | Notes |
|---|---|---|
| Exact probabilities | result.probabilities |
Tensor property, not a method; [0] selects one batch entry. |
| One expectation | result.expectation() |
Use only when there is a single expectation output. |
| Several named expectations | result.expectation("name") |
Select a particular observable's result. |
| Named samples | result.measurement("bits").value |
Computational-basis sample entries are bits. |
| Counts | result.counts[0] |
A dictionary for one batch entry; unobserved outcomes may be absent. |
| Statevector | result.statevector() |
Available on the demonstrated local path; not guaranteed for every target. |
Do not assume that requesting probabilities also requests an expectation or counts. An accessor for an unrequested output can raise an error. Raw state access and the supported outputs depend on the chosen simulation method and execution target.
Statevector simulation stores 2**n complex amplitudes per batch entry. Increasing the qubit count by one doubles the amplitude count, so start with small circuits. A successful small CPU example is not evidence of large-circuit capacity, distributed scaling, or quantum advantage.
For other simulation representations, accelerator execution, or noise, consult the release capability catalog before changing execution options. These scripts demonstrate noiseless CPU statevector behavior only.
| Problem | What to check |
|---|---|
| Qubit index outside the circuit | Use indices from 0 through n-1 and distinct qubits for two-qubit gates. |
| Unexpected rotation results | Check angle units: all rotation angles above are radians. |
| An earlier circuit appears to change | Gate methods modify their builder; create independent circuits for independent variants. |
| A phase gate appears to do nothing | Inspect complex amplitudes or use a later interference gate, not just immediate basis probabilities. |
Tensor object is not callable |
Read result.probabilities, without parentheses. |
| Ambiguous expectation output | Give expectation requests distinct names and select the desired name. |
| Sampling fails without a shot count | Supply shots= alongside fq.samples() or fq.counts(). |
| Count frequencies differ from probabilities | Finite-shot frequencies fluctuate; compare totals, valid outcomes, and statistical uncertainty. |
- Choosing a Simulation Method: representations, selection criteria, and verified output and gradient comparisons.
- Hybrid Classical-Quantum Models: combine classical layers and quantum circuits in a jointly trained predictor.
- Training Parameterized Circuits: loss construction, PyTorch training loops, and learned-circuit evaluation.
- Core Concepts: parameters, observables, and automatic differentiation.
- Qubit Ordering and Result Conventions: statevector indices, batch dimensions, and bitstrings.
- Quick Start: a complete parameter-training example.
- Wiki Directory: published guides.
- Release circuit implementation, execution options, output requests, and result accessors.
- Installation and Compatibility
- Quick Start
- Install and Run
- Qubit Ordering and Result Conventions
- Runnable Examples
- Building and Simulating Circuits
- Training Parameterized Circuits
- Hybrid Classical-Quantum Models
- Core Concepts
- From Papers and Equations to FlagQuantum Code
- Reproducing Molecular Hydrogen VQE
- Fundamentals
- Tutorials
- Choosing a Simulation Method
- Accelerator Setup
- Distributed Simulation and Training
- Execution
- Circuit Compilation and Interoperability
- Noise and Open-System Simulation
- Quantum Error Correction Experiments
- Cloud and Quantum Hardware Execution
- QPU Digital Twins
- Advanced Topics
- Support and Contribution
All 21 guides are linked in the navigation sections. The Wiki Directory lists published guides and current repository resources.
- Troubleshooting and FAQ
- Reproducibility and Benchmarks
- Development and Contribution Guide
- Documentation Guide
Documentation, capability, and contribution links in this section follow current main; the guides above target 0.3.1.