Skip to content

Core Concepts

FlagQuantum edited this page Oct 20, 2026 · 7 revisions

Core Concepts

Understand how a circuit becomes a numerical result, how parameters make it trainable, and how gradients connect quantum simulation to PyTorch optimization.

Version: FlagQuantum 0.3.1. The three complete examples below were verified on local CPU with Python 3.12.14, PyTorch 2.14.1, and macOS Apple Silicon. See Installation and Compatibility to prepare your environment and Quick Start for a longer training example.

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.

The Basic Workflow

A typical experiment starts by building a circuit, choosing observables or output requests, and executing it with fq.run. The returned execution result contains the requested numerical outputs. For a differentiable execution path, a scalar objective computed from those outputs can flow backward to the circuit's parameters through PyTorch autograd.

Concept Meaning FlagQuantum interface
Circuit Qubits and an ordered sequence of operations fq.Circuit
Parameter A value controlling an operation, such as a rotation angle Python numbers or PyTorch tensors; trainable tensors for autograd
Observable A quantity whose measurement outcomes have a meaningful average fq.Z(0), fq.X(0), or products such as fq.Z(0) @ fq.Z(1)
Output request What an execution should return fq.probabilities(), fq.expectation(observable)
Execution result A container holding requested outputs and execution information The result of fq.run(...)
Trainable model A PyTorch module owning circuit parameters and returning a differentiable tensor fq.Module

1. Circuits: Describe the Computation

fq.Circuit(n) creates a circuit with n qubits. Without a custom input state, the register starts in the all-zero computational-basis state. Gate methods append operations, and chained calls express their order.

For example, fq.Circuit(2).h(0).cx(0, 1) first applies a Hadamard gate to qubit 0, then a controlled-X from qubit 0 to qubit 1. It prepares the Bell state:

$$|\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$ fq.Circuit(2).h(0).cx(0, 1) Prepare the Bell state in concepts_circuit.py.
$p_{00}=p_{11}=1/2$ result.probabilities[0] The script checks [0.5, 0, 0, 0.5], in 00, 01, 10, 11 order.

An amplitude is a complex coefficient in the state; its squared magnitude gives a computational-basis probability. A Bell state has amplitudes of $1/\sqrt{2}$ on 00 and 11, so each has probability $1/2$.

FlagQuantum writes basis states as |q0 q1 ...>, with qubit 0 as the most-significant bit. For two qubits, the probability-vector order is 00, 01, 10, 11. This convention does not change the (control, target) argument order of .cx(...).

The examples use the local PyTorch statevector path. Other execution methods have their own capability and resource limits; a circuit definition alone does not guarantee that every target supports its operations.

2. Parameters: Make the Circuit Adjustable

Parameterized gates depend on numerical values. A Y rotation, written .ry(0, theta), uses an angle in radians. On a qubit initialized to |0>, it produces:

$$R_y(\theta)|0\rangle = \cos(\theta/2)|0\rangle + \sin(\theta/2)|1\rangle.$$
Formula / mathematical operation Corresponding code excerpt Check / interpretation
$R_y(\theta)\lvert0\rangle$ fq.Circuit(1).ry(0, theta) The gate takes radians and includes the half-angle in its amplitudes.
$\theta=0.6$ theta = torch.tensor(0.6, requires_grad=True) The tensor stays attached to the gradient graph in concepts_gradient.py.

A Python number is convenient for a fixed circuit. To differentiate with respect to the angle, use a floating-point PyTorch tensor with requires_grad=True and pass the tensor directly to the gate. Example 2 demonstrates this.

For optimization, fq.Module can own parameters as torch.nn.Parameter objects, making them available through model.parameters(). Its builder function receives the current parameter tensor and constructs the circuit for that evaluation. Rebuilding inside the function lets each forward pass use the updated parameters.

3. Observables: Decide What to Measure

An observable represents a measurable quantity. Its expectation value is the average of its measurement outcomes in the current state, written $\langle O\rangle$.

For Pauli Z, |0> has eigenvalue +1 and |1> has eigenvalue -1. Therefore:

$$\langle Z\rangle = P(0) - P(1).$$
Formula / mathematical operation Corresponding code excerpt Check / interpretation
$\langle Z_0\rangle$ fq.expectation(fq.Z(0), name="z0") Request Z on qubit 0 in concepts_circuit.py.
$\langle Z_0\rangle=p_{00}+p_{01}-p_{10}-p_{11}$ result.expectation('z0') The Bell state gives zero; the script independently checks its probabilities.
$\langle Z_0Z_1\rangle=1$ result.expectation('zz') Every supported Bell outcome has even parity; the script asserts 1.

fq.Z(0) describes Pauli Z on qubit 0. fq.Z(0) @ fq.Z(1) describes the product observable on two different qubits, which measures their Z correlation. fq.expectation(...) wraps the observable into an output request; it does not execute the circuit itself.

fq.probabilities() requests a full computational-basis probability distribution. It is an output request rather than a Pauli observable. For an arbitrary observable such as X, its expectation cannot generally be inferred from computational-basis probabilities alone: phases and the measurement basis also matter.

Example 1: Probabilities and Correlations

Save as concepts_circuit.py:

import torch
import flagquantum as fq

circuit = fq.Circuit(2).h(0).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"),
    ],
)

print("Probability shape:", tuple(result.probabilities.shape))
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}")
assert torch.allclose(result.probabilities[0], torch.tensor([0.5, 0.0, 0.0, 0.5]), atol=1e-6)
assert abs(result.expectation("z0").item()) < 1e-6
assert abs(result.expectation("zz").item() - 1.0) < 1e-6

Run python concepts_circuit.py. Expected output:

Probability shape: (1, 4)
Probabilities: [0.5, 0.0, 0.0, 0.5]
<Z0>: 0.000000
<Z0 Z1>: 1.000000

Qubit 0 is equally likely to be 0 or 1, so its Z expectation is zero. The two bits always agree, so the product of their Z outcomes is always +1. The probabilities in this example are exact simulator probabilities up to floating-point precision, not estimates from finite measurement shots.

4. Execution Results: Read the Requested Outputs

fq.run(...) returns an ExecutionResult, not a bare tensor. Request the outputs you need and use the corresponding accessor:

Accessor Meaning
result.probabilities The unique requested probability tensor; this is a property, with no parentheses
result.expectation() The expectation tensor when exactly one expectation output was requested
result.expectation("z0") A named expectation output, as in Example 1
result.expectations A tuple of expectation tensors in expectation-output order
result.measurements Individual measurement records
result.statevector() A statevector, only when the chosen execution path exposes one

The probability tensor in Example 1 has shape (1, 4): one batch entry and four basis states. [0] selects that batch entry. Shapes depend on the output and batching configuration; inspect them instead of assuming that every result is scalar.

If several expectations are requested, select one by name or output index instead of calling the ambiguous result.expectation(). An accessor for an output that was not requested, or a statevector unavailable from that execution path, can raise an error.

Result containers can also carry execution plans, metrics, accuracy, and provenance information. Their presence and detail depend on the execution path; use the typed output accessors for numerical work.

5. Automatic Differentiation: Connect Outputs to Parameters

PyTorch autograd tracks supported tensor operations from the circuit parameters to the chosen objective. Calling .backward() on a real scalar objective computes gradients; it does not update the parameters. A PyTorch optimizer performs the update separately.

For the single-qubit Y rotation above:

$$\langle Z\rangle = \cos(\theta), \qquad \frac{\partial\langle Z\rangle}{\partial\theta} = -\sin(\theta).$$
Formula / mathematical operation Corresponding code excerpt Check / interpretation
$E(\theta)=\cos\theta$ loss = result.expectation().mean() Single-circuit expectation reduced to a real scalar.
$dE/d\theta=-\sin\theta$ loss.backward() theta.grad is checked against -torch.sin(theta.detach()) with atol=1e-6.

Example 2: Differentiate an Execution Output

Save as concepts_gradient.py:

import torch
import flagquantum as fq

theta = torch.tensor(0.6, requires_grad=True)
circuit = fq.Circuit(1).ry(0, theta)
result = fq.run(circuit, outputs=fq.expectation(fq.Z(0)))
loss = result.expectation().mean()
loss.backward()

print(f"<Z0>: {loss.item():.6f}")
print(f"d<Z0>/dtheta: {theta.grad.item():.6f}")
expected_gradient = -torch.sin(theta.detach())
assert torch.allclose(theta.grad, expected_gradient, atol=1e-6)
print("Gradient check passed.")

Run python concepts_gradient.py. Expected output:

<Z0>: 0.825336
d<Z0>/dtheta: -0.564642
Gradient check passed.

Here .mean() produces a scalar objective from the returned expectation tensor. After .backward(), theta.grad holds the derivative. The assertion compares it with the analytical derivative, providing a correctness check independent of the printed decimal values.

The calls to .item() occur after the objective and gradient have been computed. They convert values to Python numbers for display; they are not part of the differentiable objective.

Example 3: Use a PyTorch Module and Optimizer

Save as concepts_module.py:

import torch
import flagquantum as fq

def build_circuit(parameters):
    return fq.Circuit(1).ry(0, parameters[0])

model = fq.Module(build_circuit, n_parameters=1, init=torch.tensor([0.6]))
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)

optimizer.zero_grad()
values = model()
loss = values.mean()
before = loss.item()
loss.backward()
gradient = next(model.parameters()).grad.item()
optimizer.step()

with torch.no_grad():
    after = model().mean().item()

print("Module output shape:", tuple(values.shape))
print(f"Before step: {before:.6f}")
print(f"Parameter gradient: {gradient:.6f}")
print(f"After step: {after:.6f}")
assert after < before
print("Optimizer step check passed.")

Run python concepts_module.py. Expected output:

Module output shape: (1,)
Before step: 0.825336
Parameter gradient: -0.564642
After step: 0.792155
Optimizer step check passed.

For FlagQuantum 0.3.1, the default fq.Module policy returns the Z expectation on qubit 0. model() returns a tensor directly, while fq.run(...) returns an execution-result container. The module policy is separate from the outputs= requests used with fq.run.

optimizer.zero_grad() clears accumulated gradients, loss.backward() calculates them, and optimizer.step() changes the owned parameter. The loss decreases after this particular step. One successful update does not establish general optimizer convergence.

The final evaluation uses torch.no_grad() because it only reports the updated objective. This evaluation does not need to retain a computation graph.

Common Gradient Mistakes

Mistake Better approach
Passing theta.item() or float(theta) to a gate Pass the tensor directly to preserve its gradient connection.
Calling .detach() while building the training circuit Keep the parameter attached during forward execution; detach only for independent evaluation or reporting.
Building a new tensor from existing differentiable tensor values Preserve the existing tensor operations; use differentiable operations such as torch.stack when combining values.
Calling .backward() on a Python number Keep the objective as a real scalar tensor until backward completes.
Forgetting optimizer.zero_grad() between updates Clear gradients before each independent training step unless accumulation is intentional.
Expecting .backward() to change parameters Call optimizer.step() after backward.
Assuming every output or execution target supports gradients Check the specific execution method, output, and backend in the capability catalog.

These examples demonstrate local, noiseless, differentiable CPU simulation. They do not establish gradient support for sampled counts, remote hardware, noise models, or every alternative backend. A PyTorch interface does not make every execution path differentiable.

Continue Learning

Clone this wiki locally