Repository navigation
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.
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 |
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:
| Formula / mathematical operation | Corresponding code excerpt | Check / interpretation |
|---|---|---|
fq.Circuit(2).h(0).cx(0, 1) |
Prepare the Bell state in concepts_circuit.py. | |
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 00 and 11, so each has probability
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.
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:
| Formula / mathematical operation | Corresponding code excerpt | Check / interpretation |
|---|---|---|
fq.Circuit(1).ry(0, theta) |
The gate takes radians and includes the half-angle in its amplitudes. | |
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.
An observable represents a measurable quantity. Its expectation value is the average of its measurement outcomes in the current state, written
For Pauli Z, |0> has eigenvalue +1 and |1> has eigenvalue -1. Therefore:
| Formula / mathematical operation | Corresponding code excerpt | Check / interpretation |
|---|---|---|
fq.expectation(fq.Z(0), name="z0") |
Request Z on qubit 0 in concepts_circuit.py. | |
result.expectation('z0') |
The Bell state gives zero; the script independently checks its probabilities. | |
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.
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-6Run 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.
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.
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:
| Formula / mathematical operation | Corresponding code excerpt | Check / interpretation |
|---|---|---|
loss = result.expectation().mean() |
Single-circuit expectation reduced to a real scalar. | |
loss.backward() |
theta.grad is checked against -torch.sin(theta.detach()) with atol=1e-6. |
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.
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.
| 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.
- 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.
- Building and Simulating Circuits: common gates and local simulation workflows.
- Qubit Ordering and Result Conventions: basis indices, selected qubits, samples, and counts.
- Quick Start: train a circuit for multiple optimization steps.
- Installation and Compatibility: supported package versions and platform setup.
- Wiki Directory: the documentation structure and published guides.
- Release capability catalog: supported workflows and limitations.
- Release circuit implementation, output requests, execution results, and module implementation.
- 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.