Repository navigation
Training Parameterized Circuits
Define trainable circuit parameters, construct a scalar loss, optimize with PyTorch, and evaluate the learned circuit.
Version: FlagQuantum 0.3.1. All three complete examples were verified on local CPU with Python 3.12.14, PyTorch 2.14.1, and macOS Apple Silicon. These examples use noiseless PyTorch statevector simulation, not sampled hardware measurements.
Start with Installation and Compatibility, Core Concepts, and Building and Simulating Circuits if you are new to the framework.
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.
Our circuit applies a trainable Y rotation to qubit 0 and, in the two-qubit examples, a controlled-X from qubit 0 to qubit 1. The output we train is the Pauli-Z expectation on qubit 0:
| Formula / mathematical operation | Corresponding code excerpt | Check / interpretation |
|---|---|---|
value = prediction() |
train_parameter.py executes RY and reads the Z expectation. | |
torch.cos(theta.detach()) |
Independent expression used by the gradient assertion. |
We want this expectation to match the target
| Formula / mathematical operation | Corresponding code excerpt | Check / interpretation |
|---|---|---|
target = torch.tensor(0.25) |
This target is an expectation value, not a probability. | |
loss = (value - target).square().mean() |
For the one-element circuit batch, mean returns the squared error. |
The target is an expectation value, not a bit probability. A Pauli-Z expectation lies between −1 and +1. For this circuit, a target of 0.25 corresponds to probability 0.625 for qubit 0 being zero and 0.375 for it being one.
One solution is
Two useful approaches are:
| Approach | Parameter ownership | Optimizer input |
|---|---|---|
| A tensor parameter | Create a torch.nn.Parameter and pass it directly to the gate. |
A list such as [theta]
|
| A FlagQuantum module |
fq.Module owns parameters and passes them to a circuit-builder function. |
model.parameters() |
For a fixed gate, a Python number is sufficient. For a differentiable gate, keep its parameter as a floating-point tensor throughout the forward computation. Do not convert it with .item(), float(...), or .detach() inside the training circuit.
fq.Module(..., n_parameters=1, init=torch.tensor([0.6])) creates one owned trainable parameter initialized to 0.6 radians. You do not need to set requires_grad=True on this initializer: the module registers the parameter for you. In the examples below, next(model.parameters()) retrieves the single owned parameter tensor; models with several parameter groups should use their named parameters explicitly.
For this objective,
| Formula / mathematical operation | Corresponding code excerpt | Check / interpretation |
|---|---|---|
loss.backward() |
Autograd populates theta.grad. | |
expected_gradient = -2 * (torch.cos(theta.detach()) - target) * torch.sin(theta.detach()) |
Compare theta.grad with this analytical derivative before optimizer.step(), with atol=1e-6. |
At an initial angle of zero, this derivative is zero even though the target has not been reached. We use a fixed nonzero initial angle of 0.6 for a reproducible example. A zero gradient can indicate a stationary initialization, not necessarily a broken autograd connection.
A loss must remain a real scalar tensor until backward completes. In PyTorch, (values - target).square().mean() computes the mean squared error and reduces the output to a scalar. For one circuit this is one squared error; for a batch, it averages the elementwise errors. Match target shapes intentionally when extending to batches.
The standard update order is:
- Clear accumulated gradients with
optimizer.zero_grad(). - Execute a fresh forward pass and compute the loss.
- Call
loss.backward()to calculate gradients. - Call
optimizer.step()to update parameters.
Backward calculates gradients; the optimizer applies them. Repeat the forward pass after an update to see its effect.
Save as train_parameter.py:
import torch
import flagquantum as fq
theta = torch.nn.Parameter(torch.tensor(0.6))
optimizer = torch.optim.SGD([theta], lr=0.1)
target = torch.tensor(0.25)
options = fq.ExecutionOptions(mode="statevector", backend="pytorch", device="cpu")
def prediction():
circuit = fq.Circuit(1).ry(0, theta)
return fq.run(circuit, outputs=fq.expectation(fq.Z(0)), options=options).expectation()
optimizer.zero_grad()
value = prediction()
loss = (value - target).square().mean()
before = loss.item()
loss.backward()
expected_gradient = -2 * (torch.cos(theta.detach()) - target) * torch.sin(theta.detach())
assert torch.allclose(theta.grad, expected_gradient, atol=1e-6)
gradient = theta.grad.item()
optimizer.step()
with torch.no_grad():
after = (prediction() - target).square().mean().item()
print("Trainable parameter:", theta.requires_grad)
print(f"Loss before step: {before:.6f}")
print(f"Parameter gradient: {gradient:.6f}")
print(f"Loss after step: {after:.6f}")
assert after < before
print("Parameter and gradient checks passed.")Run python train_parameter.py. Expected output:
Trainable parameter: True
Loss before step: 0.331011
Parameter gradient: -0.649718
Loss after step: 0.288298
Parameter and gradient checks passed.
This uses SGD with learning rate 0.1. The analytical gradient assertion checks the derivative independently of the displayed decimals. The second forward pass runs after the update, so the two printed losses refer to different parameter values.
fq.Module is a torch.nn.Module. Its circuit-builder function receives the current parameter tensor and returns an fq.Circuit. Build the circuit inside that function so later forward passes use updated parameters.
In FlagQuantum 0.3.1, the default module policy uses local PyTorch statevector execution and returns the Z expectation on qubit 0. model() returns a differentiable tensor, unlike fq.run(...), which returns an execution-result container. This module policy is separate from the outputs= requests used with fq.run.
Save as train_module_loop.py:
import torch
import flagquantum as fq
def build_circuit(parameters):
return fq.Circuit(2).ry(0, parameters[0]).cx(0, 1)
model = fq.Module(
build_circuit,
n_parameters=1,
init=torch.tensor([0.6]),
device="cpu",
)
target = torch.tensor(0.25)
optimizer = torch.optim.Adam(model.parameters(), lr=0.05)
losses = []
model.train()
for step in range(1, 201):
optimizer.zero_grad()
values = model()
loss = (values - target).square().mean()
loss.backward()
optimizer.step()
losses.append(loss.detach().item())
if step in (1, 50, 100, 200):
print(f"Step {step:3d}: loss={losses[-1]:.6f}")
model.eval()
with torch.no_grad():
final_prediction = model().mean().item()
final_loss = (model() - target).square().mean().item()
trained_parameters = next(model.parameters()).detach().clone()
result = fq.run(
build_circuit(trained_parameters),
outputs=[fq.probabilities(), fq.expectation(fq.Z(0))],
options=fq.ExecutionOptions(mode="statevector", backend="pytorch", device="cpu"),
)
print("Recorded steps:", len(losses))
print(f"Trained angle: {trained_parameters[0].item():.6f}")
print(f"Final <Z0>: {final_prediction:.6f}")
print(f"Recomputed final loss: {final_loss:.8f}")
print("Final probabilities:", [round(p.item(), 6) for p in result.probabilities[0]])
assert final_loss < 1e-6 and abs(final_prediction - target.item()) < 1e-3
assert abs(result.expectation().item() - final_prediction) < 1e-6
print("Training and final-evaluation checks passed.")Run python train_module_loop.py. Output recorded in the verification environment:
Step 1: loss=0.331011
Step 50: loss=0.001047
Step 100: loss=0.000004
Step 200: loss=0.000000
Recorded steps: 200
Trained angle: 1.318130
Final <Z0>: 0.249987
Recomputed final loss: 0.00000000
Final probabilities: [0.624993, 0.0, 0.0, 0.375007]
Training and final-evaluation checks passed.
The loss is smaller than the printed precision, rather than mathematically zero. The last digits may vary across compatible environments. The script checks a loss below 1e-6, an expectation within 1e-3 of the target, and agreement between the module and a separate evaluation of the learned circuit.
-
Loss history:
lossescontains detached Python numbers for reporting. Detaching the recorded values avoids retaining every training graph. -
Learned parameter:
trained_parametersis a detached clone, so it is an independent snapshot suitable for a fixed-circuit evaluation. Do not replace the optimizer's registered parameter with this snapshot during training. - Final prediction and loss: these come from fresh forward passes after the final optimizer update.
-
Final probabilities: the vector is ordered
00,01,10,11. For the trained two-qubit circuit, only00and11are populated.
The loop appends the loss calculated before each parameter update, even though the append occurs after optimizer.step(). The original loss tensor still represents that earlier forward pass. Recomputing the final loss is therefore the correct way to evaluate the final parameters.
model.train() and model.eval() set PyTorch module mode. model.eval() does not disable gradients. torch.no_grad() disables gradient recording for final evaluation; do not wrap the training forward pass in it.
When you do not need a custom update loop, fq.train handles forward execution, scalar-loss validation, gradient clearing, backward, optimizer updates, and loss recording. You still supply the PyTorch optimizer and objective.
The next script starts a new model with the same initializer and target. It can run independently of Example 2.
Save as train_with_helper.py:
import torch
import flagquantum as fq
def build_circuit(parameters):
return fq.Circuit(2).ry(0, parameters[0]).cx(0, 1)
model = fq.Module(build_circuit, n_parameters=1, init=torch.tensor([0.6]), device="cpu")
target = torch.tensor(0.25)
optimizer = torch.optim.Adam(model.parameters(), lr=0.05)
training = fq.train(
model,
optimizer=optimizer,
objective=lambda values: (values - target).square().mean(),
steps=200,
)
model.eval()
with torch.no_grad():
final_prediction = model().mean().item()
final_loss = (model() - target).square().mean().item()
print("Completed steps:", training.completed_steps)
print("Loss history length:", len(training.losses))
print("Optimizer:", training.optimizer)
print(f"First recorded loss: {training.losses[0]:.6f}")
print(f"Last recorded loss: {training.final_loss:.8f}")
print(f"Post-update <Z0>: {final_prediction:.6f}")
print(f"Post-update loss: {final_loss:.8f}")
assert training.completed_steps == len(training.losses) == 200
assert training.final_loss < training.losses[0]
assert final_loss < 1e-6 and abs(final_prediction - target.item()) < 1e-3
print("Training summary checks passed.")Run python train_with_helper.py. Output recorded in the verification environment:
Completed steps: 200
Loss history length: 200
Optimizer: Adam
First recorded loss: 0.331011
Last recorded loss: 0.00000000
Post-update <Z0>: 0.249987
Post-update loss: 0.00000000
Training summary checks passed.
| Training-result accessor | Meaning in 0.3.1 |
|---|---|
training.completed_steps |
Number of completed optimization steps |
training.losses |
Tuple of recorded scalar losses, one per completed step |
training.final_loss |
Last recorded loss; equivalent to training.losses[-1]
|
training.optimizer |
Optimizer class name |
training.last_execution |
Detached result of the final training forward execution |
training.summary() |
A structured summary of the training run |
The last recorded loss and last_execution correspond to the forward pass before the last optimizer update. They are not an extra post-update evaluation. Use the updated model for a fresh final prediction, as the script does. The returned training summary does not replace the model that owns the learned parameters.
For progress reporting, log_interval=N prints the first, last, and matching interval steps. A callback receives a one-based step number, scalar loss, and detached execution result after each optimizer step; the loss and result still describe that step's forward pass. Logged or detached callback values are for inspection rather than continuing the original autograd graph.
| Goal | Example scalar objective on model output values
|
|---|---|
| Minimize the selected expectation | values.mean() |
| Maximize the selected expectation | -values.mean() |
| Match an expectation target | (values - target).square().mean() |
| Use PyTorch mean squared error |
torch.nn.functional.mse_loss(values, target_tensor) with a deliberately matching target shape |
Changing the objective changes the optimization task. It does not change which observable the module measures. If you need another observable or Hamiltonian, configure a supported module policy and workflow rather than relabeling the default Z output.
PyTorch optimizers such as SGD and Adam operate on the registered tensors. Create the optimizer after creating the model and selecting its device. Learning rate, initialization, circuit structure, and the objective all affect the trajectory; loss need not decrease at every step, and these small examples do not establish convergence for arbitrary circuits.
| Symptom | What to check |
|---|---|
| Parameter gradient is missing | Ensure the tensor is trainable and was used directly in the forward computation. |
| Loss is a Python float | Keep .item() and float(...) for logging after constructing the tensor loss. |
| Gradient is zero at initialization | Check the objective analytically when possible; this example has a stationary point at angle zero. |
| Parameters do not update | Check optimizer ownership, learning rate, and that optimizer.step() runs after backward. |
| Gradients accumulate unexpectedly | Clear them at the start of each independent step unless accumulation is intentional. |
| Repeated backward raises a graph error | Build a fresh forward computation for each iteration rather than reusing a previous loss. |
| Loss oscillates or grows | Inspect values and gradients, check the objective, and consider a smaller learning rate or a different initialization. |
| Final evaluation differs from the last logged loss | Evaluate the final parameters explicitly; the log records the preceding forward pass. |
| CPU and accelerator runs differ slightly | Record versions, precision, device, initialization, and optimizer settings; compare with appropriate tolerances. |
For reproducibility, record the package versions, circuit, initial parameters, target, optimizer, learning rate, step count, precision, device, and any random seeds. The examples use fixed initialization and exact simulator expectations, without measurement-shot randomness.
Serialize model.state_dict() to save model state and include optimizer.state_dict() when optimizer state must also be restored. A scalar loss history alone is not enough to resume training faithfully; initialization and optimizer moments matter. An in-memory state dictionary can share tensor storage with the model, so serialize it or make an independent copy before continuing to train.
These examples establish local CPU differentiability only. Sample counts, remote providers, noisy execution, and alternative simulation methods require their own gradient support. Check the release capability catalog before changing the execution path.
- Distributed Simulation and Training: sharding, communication preflight, and a verified two-worker training workflow.
- 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.
- Building and Simulating Circuits: circuit construction, gates, and result reading.
- Core Concepts: observables, result containers, and autograd.
- Qubit Ordering and Result Conventions: probability indices and measurement bitstrings.
- Quick Start: a shorter expectation-minimization example.
- Wiki Directory: published guides and topics.
- Release module implementation, runtime policy, and training lifecycle and result fields.
- 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.