Skip to content

pyqpanda3 v0.4.1: making fidelity actually drive transpilation decisions

pyqpanda3 v0.4.0 connected the hardware workflow end to end: cloud chip metadata, fidelity-aware layout, instruction export, and schedule visualization. v0.4.1 targets a layer closer to the "kernel" of that pipeline — now that the backend can give you fidelity data, does the transpiler actually use it to make decisions?

The answer is: yes, now it does. The theme of this release is letting fidelity data truly drive transpilation decisions, and filling two long-standing simulator gaps.

Strongly-typed transpilation options and metrics ​

In 0.4.0, transpile() used positional arguments to control the optimization level and layout strategy. 0.4.1 adds TranspilationOptions, which groups every tunable into a single object, and pairs it with TranspilationResult to return the transpiled program, compile time, and metrics in one call.

python
from pyqpanda3.core import QProg, H, CZ, measure
from pyqpanda3.transpilation import (
    ChipBackend,
    LayoutMethod,
    RoutingMethod,
    Transpiler,
    TranspilationOptions,
)

prog = QProg()
prog << H(0) << CZ(0, 1) << measure(0, 0) << measure(1, 1)

backend = ChipBackend()
backend.chip_topology_edges = [[0, 1], [1, 2]]
backend.basic_gates = ["RPhi", "CZ"]
backend.single_gate_fidelity = {0: 0.99, 1: 0.99, 2: 0.99}
backend.readout_fidelity = {0: 0.90, 1: 0.90, 2: 0.99}
backend.double_gate_fidelity = {(0, 1): 0.90, (1, 2): 0.99}

options = TranspilationOptions()
options.optimization_level = 2
options.layout_method = LayoutMethod.FIDELITY
options.routing_method = RoutingMethod.LIGHT_SABRE
options.approximation_degree = 0.99
options.execution.routing_trials = 8
options.execution.seed = 42

result = Transpiler().transpile(prog, backend, options)

print(type(result).__name__)
print(result.compile_time_ns > 0)
print(sorted(result.metrics.keys()))

Example output shape:

text
TranspilationResult
True
['depth', 'fidelity_exact', 'fidelity_fallback', 'fidelity_unknown', 'log_expected_fidelity', 'two_qubit_gate_count']

result.metrics has the same shape as the dict returned by the standalone function transpilation_metrics(prog, backend) — it includes the two-qubit gate count, depth, log-expected fidelity, and counts of gates whose fidelity was matched exactly / fell back / was unknown. Transpilation quality is finally quantifiable and comparable, not just "it ran."

LightSABRE routing ​

RoutingMethod.LIGHT_SABRE (the string "light_sabre") enables the new layered-lookahead router. It expands the interaction graph layer by layer while routing, reducing two-qubit gate insertion and swap depth — useful for backends with limited connectivity or deeper circuits.

python
from pyqpanda3.core import QProg, H, CZ, measure
from pyqpanda3.transpilation import ChipBackend, Transpiler

prog = QProg()
prog << H(0) << CZ(0, 1) << measure(0, 0) << measure(1, 1)

backend = ChipBackend()
backend.chip_topology_edges = [[0, 1], [1, 2]]
backend.basic_gates = ["RPhi", "CZ"]

transpiled = Transpiler().transpile(
    prog,
    backend,
    {},
    2,
    "fidelity",
    "light_sabre",
    "default",
    approximation_degree=0.99,
)

print(type(transpiled).__name__)
print(len(transpiled.operations()) > 0)

Output:

text
QProg
True

Heisenberg-picture expectation simulator ​

In many cases you don't need the full state vector — you only want the expectation value <H> of some Pauli Hamiltonian. SparseHeisenbergSimulator computes exactly that, in the Heisenberg picture, without sampling and without returning a state vector. For the "supported gate set + product initial state" class of problems it is far cheaper than full simulation.

python
from pyqpanda3.core import QProg, CZ, SparseHeisenbergSimulator

zero = (1 + 0j, 0 + 0j)  # (|0> amplitude, |1> amplitude) for each qubit

prog = QProg() << CZ(0, 1)  # |00> unchanged

sim = SparseHeisenbergSimulator(num_qubits=2)
sim.set_initial_product_state([zero, zero])
sim.add_observable("ZZ", 1.0)
sim.evolve_backward(prog)

print(round(sim.expectation(), 6))

Output:

text
1.0

<ZZ> on |00> is 1.0, matching the analytic value. When the number of operator terms grows, you can truncate with max_terms and truncation_norm="l1", and inspect what was dropped via stats().

Matrix-product-state (MPS) simulator ​

MPSQVM runs quantum programs on a matrix-product-state backend, keeping memory usage low for weakly-entangled circuits even at large qubit counts.

python
from pyqpanda3.core import H, MPSQVM, QProg, measure

prog = QProg() << H(0) << measure(0, 0) << measure(1, 1)

qvm = MPSQVM()
qvm.run(prog, 1000)
result = qvm.result()

print(type(result).__name__)
print(sum(result.get_counts().values()) == 1000)

Example output shape:

text
QResult
True

Also worth noting ​

  • state_fidelity() lands in quantum_info, supporting all four density-matrix / state-vector combinations — filling the gap of "directly comparing two quantum states."
  • Choi, SuperOp, and PTM all gain ndarray(), making them easy to bridge with the NumPy ecosystem.
  • QProg.get_measure_qubits_cbits() returns measurement pairs in program order, handy for verifying the measurement mapping before submitting to the cloud.
  • A batch of stability fixes: the VQCircuit.append(vqc) self-reference segfault, the VF2 partial-layout segfault, the ParamExpression empty-expression crash, dephasing-noise Kraus orientation, the get_matrix memory leak, and PartialAmplitudeQVM index bounds checking.

The second half of 0.4.1: initial states, Heisenberg, and task cancellation ​

The 0.4.1 features landed in two waves during the release cycle: the first half brought the fidelity-driven transpilation pipeline and the two simulators; the second half added initial-state control for the simulators, truncation statistics for the Heisenberg picture, and cloud task cancellation.

Initial state: CPUQVM.init_state ​

CPUQVM gains init_state(state) in 0.4.1: set the simulator to any initial state before running, or call it without arguments to restore the default |0...0>. For example, put both qubits in a superposition before measuring:

python
import numpy as np
from pyqpanda3.core import CPUQVM, QProg, measure

qvm = CPUQVM()
qvm.init_state([1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)])

prog = QProg() << measure([0, 1], [0, 1])
qvm.run(prog, 1000)
print(qvm.result().get_counts())

qvm.init_state()  # restore default |0...0>
qvm.run(prog, 1000)
print(qvm.result().get_counts())

Example output shape:

text
{'00': 524, '11': 476}
{'00': 1000}

Note: MPSQVM's C++ init_state is not exposed in the Python bindings (0.4.1); use CPUQVM.init_state on the Python side.

Truncation statistics in the Heisenberg picture ​

Earlier we mentioned that SparseHeisenbergSimulator can truncate with max_terms and truncation_norm. What does that look like in practice? RX rotations inflate the number of operator terms from 1 to 8; with max_terms=4 the surplus terms are dropped by L1 norm, and stats() reports what was discarded:

python
from pyqpanda3.core import QProg, H, CNOT, RX, SparseHeisenbergSimulator

zero = (1 + 0j, 0 + 0j)
prog = QProg() << H(0) << CNOT(0, 1) << RX(0, 0.7) << RX(1, 1.3) << RX(2, 2.1)

# No truncation: watch the term count inflate
full = SparseHeisenbergSimulator(num_qubits=3)
full.set_initial_product_state([zero, zero, zero])
full.add_observable("ZZZ", 1.0)
full.evolve_backward(prog)
print("no truncation terms:", full.stats().active_terms)

# Truncation: keep at most 4 terms, drop the rest by L1 norm
trunc = SparseHeisenbergSimulator(num_qubits=3, max_terms=4, truncation_norm="l1")
trunc.set_initial_product_state([zero, zero, zero])
trunc.add_observable("ZZZ", 1.0)
trunc.evolve_backward(prog)
s = trunc.stats()
print("expectation:", round(trunc.expectation(), 6))
print("active_terms:", s.active_terms)
print("truncated_terms:", s.truncated_terms)
print("truncated_l1:", round(s.truncated_l1, 6))
print("coefficient_l1:", round(s.coefficient_l1, 6))

Output:

text
no truncation terms: 8
expectation: 0.400378
active_terms: 4
truncated_terms: 4
truncated_l1: 0.51565
coefficient_l1: 2.373074

Cloud task cancellation ​

QCloudJob gains cancel() and the static method cancel_task(task_id), so you can actively cancel a submitted cloud task; True means the cloud service accepted the cancellation request. Cancelling requires cloud credentials and a real task, so it cannot be executed locally:

python
from pyqpanda3.qcloud import QCloudJob, QCloudService

# Requires cloud credentials and a real task; cannot run locally
service = QCloudService("your_api_key")
job = QCloudJob("your_job_id")

print(job.cancel())                          # cancel the current job
print(QCloudJob.cancel_task("your_task_id"))  # cancel by task ID (static)

See the v0.4.1 release notes for the full list of API changes.

Released under the MIT License.