Skip to content

Circuit Compilation and Interoperability

FlagQuantum edited this page Oct 20, 2026 · 3 revisions

Circuit Compilation and Interoperability

Optimize a circuit, route it onto a declared topology, and exchange supported programs with another framework while checking their numerical meaning.

Scope: FlagQuantum 0.3.1. All five complete scripts below were verified on October 20, 2026 with Python 3.12.14, PyTorch 2.13.0, and macOS ARM64 using local CPU statevector execution. The Qiskit examples use Qiskit 2.5.0 and NumPy 2.5.4. They do not require Qiskit Aer, a GPU, credentials, or quantum hardware. Cirq and PennyLane boundaries below were checked against release source and contracts; those frameworks were not executed for this page.

Start with Building and Simulating Circuits and Qubit Ordering and Result Conventions.

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. Choose the Right Operation

Operation Entry point Purpose and result
Record a program circuit.to_ir() Produce FlagQuantum-owned CircuitIR, containing instructions and relevant metadata.
Optimize independently of a target flagquantum.compiler.optimize(...) Return a transformed IR using the selected optimization level.
Optimize and optionally route flagquantum.compiler.compile(...) Return IR, optionally routed to an explicit CouplingMap.
Select the public compilation workflow fq.compile(program, compiler=..., target=..., target_qubits=...) Use the native path or an explicitly named installed compiler; target/plugin requirements depend on that path.
Prepare execution fq.plan(program, options=..., outputs=...) Build an execution plan; normal execution performs its required compilation internally.
Execute fq.run(...) Run the selected workflow and return an execution result.
Convert framework objects export_qiskit, import_qiskit, and adapter equivalents Translate at the IR boundary and report conversion issues.

The two compile interfaces have different signatures. The topology example imports the expert function from flagquantum.compiler; coupling_map= is not a keyword of the root fq.compile interface.

Compilation transforms a program. It does not execute it, discover a device's connectivity, submit a job, or certify hardware behavior. Converting to a Qiskit circuit also does not automatically invoke Qiskit's transpiler or select a Qiskit backend.

2. Requirements

Use Installation and Compatibility to create a supported environment. Core FlagQuantum 0.3.1 requires Python 3.10–3.12 and PyTorch >=2.10,<2.15.

The optimization, routing, and OpenQASM scripts require only the core CPU environment. For the exact Qiskit conversion examples, install into that environment:

python -m pip install "flagquantum==0.3.1" "qiskit==2.5.0" "numpy>=1.24,<3"
python -m pip check

These examples use Qiskit's built-in quantum_info.Statevector. For the complete optional integration, including Aer and QASM3 import dependencies, install "flagquantum[qiskit]==0.3.1" instead. For other adapters, the release declares:

Optional extra Dependency window Additional Python requirement
qiskit qiskit[qasm3-import]>=2,<3, qiskit-aer>=0.17,<1, sympy>=1.13,<2 Within the core Python range and dependency support.
cirq cirq-core>=1.6.1,<1.8 Python 3.11 or 3.12; the dependency is guarded by a Python-version marker.
pennylane pennylane>=0.44.1,<0.46 Python 3.11 or 3.12; the dependency is guarded by a Python-version marker.

A resolvable dependency window is broader than the particular configuration tested here. Installing an adapter extra does not enable every feature of the external framework.

3. Optimize a Circuit and Verify Gradients

Optimization removes redundant work under the passes' supported rules. For example, two identical self-inverse gates can cancel, and adjacent rotations with the same axis can merge by adding their angles.

Optimization level Behavior in this release
0 Leave the submitted instruction sequence unchanged by optimization passes.
1 Local simplification, inverse cancellation, adjacent rotation merging, supported one-qubit folding, and removable zero-state resets.
2 Default; adds supported commuting transformations, two-qubit block simplification, and measurement-aware diagonal removal.
3 Reserved and rejected with CompilationError; it is not an alias for level 2.

These level numbers select FlagQuantum's pass sequences; they do not promise identical passes or results to another framework's levels. Level 2 runs the available target-independent passes, rather than a general search for the globally shortest circuit. Level selection and pass ordering are documented in the release compiler sources.

Check Mathematical Equivalence Alongside Compilation

Mathematical requirement Corresponding code / check Scope
$U_{\mathrm{optimized}}\lvert00\rangle=U_{\mathrm{original}}\lvert00\rangle$ torch.allclose(state(original), state(optimized), atol=1e-6) compile_optimize.py checks this chosen input, not an exhaustive unitary proof.
$R_Y(0)=I$ assert len(trainable.instructions) == 1 A numerical identity at one parameter value need not be removable during training.
$\langle X\rangle_{R_Y(\theta)\lvert0\rangle}=\sin\theta$ fq.expectation(fq.X(0)) The value is zero at theta=0.
$d\langle X\rangle/d\theta=\cos\theta=1\;\;(\theta=0)$ value.backward()
torch.allclose(theta.grad, torch.ones_like(theta))
Preserving the forward state alone would not catch an erased derivative.

Save as compile_optimize.py:

import torch
import flagquantum as fq
from flagquantum.compiler import optimize
from flagquantum.errors import CompilationError

options = fq.ExecutionOptions(mode='statevector', backend='pytorch', device='cpu')
circuit = fq.Circuit(2).h(0).h(0).ry(0, 0.2).ry(0, 0.3).cx(0, 1)
original = circuit.to_ir()
optimized = optimize(original, optimization_level=1)

def state(program):
    return fq.run(program, outputs=fq.probabilities(), options=options).statevector()

assert len(original.instructions) == 5
assert len(optimized.instructions) == 2
assert torch.allclose(state(original), state(optimized), atol=1e-6)
print('Original instructions:', len(original.instructions))
print('Optimized instructions:', len(optimized.instructions))
print('Optimized gates:', [gate.name for gate in optimized.instructions])
print('Statevector equivalence: True')
try:
    optimize(original, optimization_level=3)
except CompilationError:
    print('Reserved optimization level 3 rejected: True')
else:
    raise AssertionError('Expected reserved level to be rejected')

theta = torch.nn.Parameter(torch.tensor(0.0))
trainable = optimize(fq.Circuit(1).ry(0, theta), optimization_level=1)
assert len(trainable.instructions) == 1
value = fq.run(trainable, outputs=fq.expectation(fq.X(0)), options=options).expectation().sum()
value.backward()
assert theta.grad is not None and torch.allclose(theta.grad, torch.ones_like(theta))
print('Zero-angle trainable gate retained: True')
print(f'd<X0>/dtheta at zero: {theta.grad.item():.6f}')

Run python compile_optimize.py. Expected output:

Original instructions: 5
Optimized instructions: 2
Optimized gates: ['ry', 'cx']
Statevector equivalence: True
Reserved optimization level 3 rejected: True
Zero-angle trainable gate retained: True
d<X0>/dtheta at zero: 1.000000

The first example uses level 1 to make the reduction easy to inspect: the H pair cancels and RY(0.2) followed by RY(0.3) becomes one RY(0.5). The statevector assertion verifies this input's numerical result.

The second check uses a trainable zero-angle rotation. Although its current operator is the identity, deleting it would erase the nonzero derivative of <X0>. The assertion confirms that the gate and gradient survive this transformation. When training, compile a fresh forward computation as needed; do not cache a transformed autograd graph and reuse it across optimizer steps.

Some transformations preserve measurement semantics under explicit measurement context rather than an arbitrary unmeasured statevector. Choose your validation according to the program's contract: states for unitary examples, measurement mappings/distributions for measured programs, and gradients for differentiable workflows. A single zero-state comparison is useful smoke coverage, not a proof for every input state.

4. Compile for an Explicit Connectivity Map

Suppose qubits form a line 0 — 1 — 2. A direct CX(0, 2) does not use a permitted edge. Routing can insert SWAPs so two-qubit operations use the declared edges.

Save as compile_route.py:

import torch
import flagquantum as fq
from flagquantum.compiler import CouplingMap, compile, schedule_layers

options = fq.ExecutionOptions(mode='statevector', backend='pytorch', device='cpu')
original = fq.Circuit(3).h(0).cx(0, 2).to_ir()
line = CouplingMap(3, [(0, 1), (1, 2)])
routed = compile(original, coupling_map=line, routing_strategy='restore_after_each_gate', optimize=False)
allowed = {frozenset((0, 1)), frozenset((1, 2))}
for gate in routed.instructions:
    if len(gate.wires) == 2:
        assert frozenset(gate.wires) in allowed
before = fq.run(original, outputs=fq.probabilities(), options=options).statevector()
after = fq.run(routed, outputs=fq.probabilities(), options=options).statevector()
assert torch.allclose(before, after, atol=1e-6)
print('Original instructions:', len(original.instructions))
print('Routed instructions:', len(routed.instructions))
print('Routed gates:', [gate.name for gate in routed.instructions])
print('All two-qubit gates use line edges: True')
print('Logical output ordering restored: True')
print('Logical layers:', len(schedule_layers(routed)))
print('Statevector equivalence: True')

Run python compile_route.py. Expected output:

Original instructions: 2
Routed instructions: 4
Routed gates: ['h', 'swap', 'cx', 'swap']
All two-qubit gates use line edges: True
Logical output ordering restored: True
Logical layers: 4
Statevector equivalence: True

optimize=False keeps the routing mechanics visible. With optimization enabled, the expert compiler optimizes before routing and again afterward. CouplingMap supplies undirected connectivity in this example; it does not describe control direction, native gate availability, gate durations, or calibration quality. The legality assertion checks only two-qubit connectivity.

The demonstrated restore_after_each_gate strategy restores the logical layout after each routed gate. The release also offers persistent_layout, sabre, and sabre_layout strategies that restore logical order at program output. Its auto selection compares the cost estimates for restore_after_each_gate and persistent_layout; it does not search all strategies for a global optimum. See the compiler guide for their exact scope and layout metadata.

schedule_layers creates logical dependency layers. A layer count is not a pulse schedule or a device execution-time estimate.

What about native gates and directed hardware?

Direction legalization, native-gate rewriting, target constraints, placement, and artifact emission belong to additional target-aware compiler paths. Use an explicit supported target snapshot and the documented entry points; a CouplingMap alone does not provide these inputs.

Some synthesis functions are reached by module path and are not stable expert exports. Their supported basis, exactness, and refusal rules are narrower than general numerical matrix decomposition. In particular, a rewrite that agrees only up to a global phase may be refused when that phase cannot be represented by the relevant IR path. Do not infer arbitrary target support from the simple routing example.

The compiler operates in process over one program on CPU. These transformations do not establish distributed compilation, multi-device execution, pulse-level scheduling, or hardware fidelity. Named compiler plugins require their own installation and target contract; selecting a plugin does not remove its restrictions.

5. Exchange OpenQASM and Preserve Measurement Order

The module-path emit_openqasm function emits OpenQASM 2 or 3 text. The public fq.from_openqasm importer accepts the supported subset emitted by FlagQuantum; it is not a general parser for arbitrary third-party OpenQASM.

Save as compile_qasm.py:

import torch
import flagquantum as fq
from flagquantum.compiler.openqasm import emit_openqasm

options = fq.ExecutionOptions(mode='statevector', backend='pytorch', device='cpu')
original = fq.Circuit(2).x(0)
text = emit_openqasm(original, version=3.0, result_qubits=(1, 0))
imported = fq.from_openqasm(text)
assert tuple(imported.measurement_qubits) == (1, 0)
expected = fq.run(original, outputs=fq.probabilities(), options=options).probabilities
actual = fq.run(imported.to_circuit(), outputs=fq.probabilities(), options=options).probabilities
assert torch.allclose(expected, actual, atol=1e-6)
counts = fq.run(imported.to_circuit(), outputs=fq.counts(qubits=imported.measurement_qubits), shots=16, options=options).counts[0]
assert counts == {'01': 16}
print(text)
print('Imported measurement order:', tuple(imported.measurement_qubits))
print('Counts in that order:', counts)
print('Round-trip probabilities match: True')

Run python compile_qasm.py. Expected output:

OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
x q[0];
c[0] = measure q[1];
c[1] = measure q[0];
Imported measurement order: (1, 0)
Counts in that order: {'01': 16}
Round-trip probabilities match: True

x(0) prepares logical |10>. Reading qubits in order (1, 0) therefore yields "01". The counts are deterministic in this example, so an exact dictionary assertion is appropriate.

The importer returns a program wrapper with measurement_qubits, to_ir(), and to_circuit(). Use to_ir() when you need the terminal measurement mapping retained in IR. For the demonstrated sampling path, to_circuit() supplies the circuit and the explicit output request supplies the imported measurement order. Omitting that order would change how the count key is interpreted.

The emitted text includes terminal measurements; it does not serialize a training loop, optimizer, Python callbacks, device configuration, or an autograd graph. Bind supported numeric values before text interchange. Unsupported language constructs are rejected with OpenQASMImportError and an issue_code; check the release importer contract instead of assuming general OpenQASM 2/3 language coverage.

6. Convert Between FlagQuantum and Qiskit

Use export_qiskit and import_qiskit when you want conversion reports. Convenience functions to_qiskit and from_qiskit return only the external circuit and IR, respectively.

Save as compile_qiskit.py:

import torch
import flagquantum as fq
from qiskit.quantum_info import Statevector
from flagquantum.ecosystem.qiskit import (
    export_qiskit, import_qiskit, qiskit_statevector_to_flagquantum,
)

options = fq.ExecutionOptions(mode='statevector', backend='pytorch', device='cpu')
original = fq.Circuit(2).x(0)
exported = export_qiskit(original)
imported = import_qiskit(exported.circuit)
assert exported.report.lossless and imported.report.lossless
native = fq.run(original, outputs=fq.probabilities(), options=options).statevector()[0]
foreign = Statevector.from_instruction(exported.circuit).data
converted = qiskit_statevector_to_flagquantum(foreign, n_qubits=2)
returned = fq.run(imported.ir, outputs=fq.probabilities(), options=options).statevector()[0]
assert torch.allclose(native.to(converted.dtype), converted, atol=1e-6)
assert torch.allclose(native, returned, atol=1e-6)
print('Export lossless:', exported.report.lossless)
print('Import lossless:', imported.report.lossless)
print('Qiskit nonzero index:', int(abs(foreign).argmax()))
print('FlagQuantum nonzero index:', int(native.abs().argmax()))
print('Statevectors match after order conversion: True')
print('Round-trip statevector matches: True')

Run python compile_qiskit.py. Expected output:

Export lossless: True
Import lossless: True
Qiskit nonzero index: 1
FlagQuantum nonzero index: 2
Statevectors match after order conversion: True
Round-trip statevector matches: True

The asymmetric x(0) circuit deliberately exposes the ordering difference: Qiskit's raw statevector puts this amplitude at index 1, while FlagQuantum puts it at index 2. The adapter maps gate wires, and the explicit qiskit_statevector_to_flagquantum helper reorders a raw reference state for comparison. Do not manually reverse gate indices as well.

Inspect both import and export reports. “Lossless” describes the adapter's represented semantics under its supported mapping; it does not certify a hardware execution, a transpilation target, performance, or an autograd connection across frameworks. Qiskit supports additional objects and execution features outside this mapping.

Unsupported Operations and Partial Conversion

Save as compile_rejection.py:

from qiskit import QuantumCircuit
from flagquantum.ecosystem.qiskit import import_qiskit, QiskitConversionError

source = QuantumCircuit(1)
source.h(0)
with source.for_loop(range(2)):
    source.x(0)
try:
    import_qiskit(source)
except QiskitConversionError as error:
    print('Strict conversion rejected:', not error.report.lossless)
    print('Issue codes:', sorted({issue.code for issue in error.report.issues}))
else:
    raise AssertionError('Expected loop to be rejected')

# Inspect the partial result; do not treat it as an equivalent executable.
partial = import_qiskit(source, allow_lossy=True)
assert not partial.report.lossless
assert [gate.name for gate in partial.ir.instructions] == ['h']
print('Partial report lossless:', partial.report.lossless)
print('Partial gates:', [gate.name for gate in partial.ir.instructions])
print('Partial IR is not a control-flow-preserving replacement.')

Run python compile_rejection.py. Expected output:

Strict conversion rejected: True
Issue codes: ['unsupported_control_flow']
Partial report lossless: False
Partial gates: ['h']
Partial IR is not a control-flow-preserving replacement.

The Qiskit for_loop construct is refused by default. allow_lossy=True permits a deliberately partial IR plus an issue report; it is useful for inspection and migration, and does not make the missing semantics executable. Never treat the remaining instructions as an equivalent replacement for the original program.

Other boundaries include the supported symbolic arithmetic subset, measurement/classical-condition forms, and representability of execution requests. Qiskit custom-unitary conversion is bounded to at most three qubits in this release. Register flattening, barriers, and arbitrary circuit metadata can also produce conversion issues. Read each issue's code, severity, message, and operation index; do not suppress a report merely because a numerical smoke test passes.

7. Other Frameworks and Explicit Simulator Bridges

The adapters are optional translators at the CircuitIR boundary. They do not place external circuit objects inside FlagQuantum's native simulation or training model.

Framework Conversion entry points Important boundaries
Qiskit flagquantum.ecosystem.qiskit: export_qiskit, import_qiskit, to_qiskit, from_qiskit Supported gates, bounded custom unitaries, supported parameter expressions and classical constructs. General control flow is not supported. Raw statevector order differs.
Cirq flagquantum.ecosystem.cirq: export_cirq, import_cirq, to_cirq, from_cirq Static supported operations and contiguous LineQubit mapping; supported symbolic arithmetic and a restricted terminal-measurement contract. Arbitrary qudits, control flow, and measurement variants are outside that contract. Raw export can reject an idle-wire extent it cannot preserve.
PennyLane flagquantum.ecosystem.pennylane: export_pennylane, import_pennylane, to_pennylane, from_pennylane Converts QuantumScript, rather than an arbitrary QNode or Python quantum function. Bind symbolic parameters before export. Script measurement processes and finite shots are separate execution concerns. Raw export can reject idle-wire extent; wire-label flattening is reported.

Use each adapter's report-bearing functions for diagnostics and its release contract to check supported operations. Conversion APIs share a pattern, but their accepted programs and issue codes differ.

Conversion Versus Execution

Explicit local simulator bridges have their own run functions under the Qiskit, Cirq, and PennyLane ecosystem namespaces. Importing one of those functions selects that external execution path; ordinary fq.run continues to use its selected native runtime and does not automatically switch to an external simulator.

For example, the release's documented Qiskit Aer and PennyLane Lightning bridges support a fully bound, unbatched local CPU statevector/sampling/counts slice with ordered qubit selection. Their documented scope excludes gradients, noise, dynamic circuits, automatic routing, and fallback; the Lightning bridge also excludes QNodes and alternative devices. A dynamic Aer integration elsewhere in the package is a separately scoped workflow, not an extension of this simple bridge contract.

These bridges were not executed for this page. Follow their dedicated release guides and install their complete dependencies before use. A framework's own support for GPU execution or differentiation does not establish those features in a FlagQuantum bridge. Likewise, exporting a trainable PyTorch angle as an external numeric value does not transfer its autograd graph; use the native differentiable workflow for Training Parameterized Circuits.

8. Practical Checks and Troubleshooting

Problem Check or action
fq.compile rejects coupling_map= Import the expert compile from flagquantum.compiler for that signature.
A level is rejected Use implemented integer levels 0, 1, or 2; level 3 is reserved.
Compilation adds gates Routing introduces movement operations. Inspect connectivity and final layout, then measure the intended cost; fewer source gates need not mean fewer routed gates.
Native hardware rejects a routed circuit Connectivity alone does not legalize gate basis, direction, timing, or provider constraints. Use the supported target compilation workflow.
Converted states appear different Check logical wire mapping, endianness, selected output order, dtype, and any represented global phase before comparing.
Conversion raises a typed error Inspect error.report; reduce to the unsupported construct and check the adapter contract.
Optional import fails Install the matching extra in the active supported interpreter; inspect its Python marker and dependency window.
Gradients disappear after export Conversion is a program boundary, not a cross-framework autograd bridge. Keep the native training path or use an explicitly supported gradient integration.
An external circuit loses idle qubits or measurements Inspect the conversion report and separate circuit structure from the execution plan.

For meaningful validation, record the FlagQuantum and external-framework versions, source IR, compiler settings, topology/target, conversion reports, output convention, and the reference you compared. A smaller gate count or successful conversion alone is insufficient evidence of correctness or improved runtime.

Sources and Related Guides

All technical source links below are pinned to v0.3.1:

Clone this wiki locally