Skip to content

Building and Simulating Circuits

FlagQuantum edited this page Oct 20, 2026 · 6 revisions

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.

1. Create a Circuit and Add Gates

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 call circuit.h(0) and circuit.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.

Common Single-qubit Gates

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.

Common Two-qubit Gates

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.

2. Run a Local Statevector Simulation

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(...) and fq.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.

Example 1: Single-qubit Gates, Rotations, and Interference

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.

3. Build an Entangled Circuit and Read Several Outputs

Create a Bell state by applying H to qubit 0, followed by CX from qubit 0 to qubit 1. Its state is:

$$|\psi\rangle = \frac{|00\rangle + |11\rangle}{\sqrt{2}}.$$
Formula / mathematical operation Corresponding code excerpt Check / interpretation
$\lvert\psi\rangle=(\lvert00\rangle+\lvert11\rangle)/\sqrt2$ circuit.h(0); circuit.cx(0, 1) simulate_bell.py executes H before controlled-X.
$\psi=(1,0,0,1)/\sqrt2$ state = result.statevector()[0] Compare all amplitudes with expected, with atol=1e-6.
$p_k=\lvert\psi_k\rvert^2$ state.abs().square() Compare against result.probabilities[0].

Example 2: Bell-state Amplitudes, Probabilities, and Expectations

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.

4. Understand Controlled Gates, SWAP, and Phase

The following small circuits make two-qubit gate behavior visible without relying on a random measurement split.

Example 3: Compare CX, SWAP, and CZ

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, producing 11.
  • SWAP instead moves the excitation from 10 to 01.
  • H on each qubit prepares four equal positive amplitudes. CZ changes the 11 amplitude 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.

5. Sample a Circuit and Read Counts

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:

$$|\mathrm{GHZ}\rangle = \frac{|000\rangle + |111\rangle}{\sqrt{2}}.$$
Formula / mathematical operation Corresponding code excerpt Check / interpretation
$\lvert\mathrm{GHZ}\rangle=(\lvert000\rangle+\lvert111\rangle)/\sqrt2$ 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.
$p_{000}=p_{111}=1/2$ 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_x N_x=S$ 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.

Example 4: GHZ Probabilities, Samples, and Counts

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.

6. Choose the Right Result Accessor

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.

7. Practical Limits and Troubleshooting

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.

Continue Learning and References

Clone this wiki locally