Skip to content

0.5.0 ​

pyqpanda3 0.5.0 is the performance release. The headline changes are precision-selectable simulation (every CPU-family simulator now accepts precision="float32" or "float64"), a portable SIMD dispatcher that accelerates the state-vector kernels at runtime, and a rewritten LightSABRE layout/routing engine that makes transpilation both faster and measurably better in output quality. Alongside these, 0.5.0 adds a native RESET operation, a simulator capability registry, MPS truncation controls with run statistics, explicitly stated noise-model conventions, and a bundled mimalloc allocator shipped inside platform-specific wheels.

1. Simulator precision selection ​

All CPU-family simulators now take a precision keyword at construction. float32 halves the memory footprint of the state and roughly doubles kernel throughput on SIMD-capable hosts; float64 remains the default for full-accuracy work. The choice is immutable per instance and readable back through the precision property, together with supported_precisions.

python
import numpy as np
from pyqpanda3.core import CPUQVM, MPSQVM, H, CNOT, QProg

seed = np.array([0.5, 0.5, 0.5, 0.5], dtype=np.complex64)
prog = QProg() << H(0) << CNOT(0, 1)

qvm32 = CPUQVM(precision="float32")
qvm32.init_state(seed)
state32 = qvm32.current_state()
print(state32.dtype, qvm32.precision, tuple(qvm32.supported_precisions))

qvm64 = CPUQVM()  # default: float64
qvm64.init_state(seed)
print(qvm64.current_state().dtype, qvm64.precision)

mps = MPSQVM(precision="float32")
mps.run(prog, 2)
print(mps.current_state().dtype, mps.current_state().shape)

Output:

text
complex64 float32 ('float32', 'float64')
complex128 float64
complex64 (4,)

current_state() is new on CPUQVM and MPSQVM: it returns the state as a NumPy array in the simulator's native precision (complex64 for float32, complex128 for float64). Note the two classes report different things — CPUQVM.current_state() mirrors the state installed by init_state(...) and is not populated by run() (read a run's state through result().get_state_vector()), whereas MPSQVM.current_state() is populated by run(). DensityMatrixSimulator gains the matching density_matrix_native().

The precision family covers CPUQVM, DensityMatrixSimulator, MPSQVM, PartialAmplitudeQVM, SingleAmplitudeQVM, and SparseHeisenbergSimulator (float32/float64 each). Stabilizer reports the exact precision="exact" and rejects arithmetic precisions. GPUQVM supports both float32 and float64 when the build is CUDA-enabled; a build without CUDA registers float64 only and marks the backend unavailable. Note that the registry's available flag describes how the wheel was built, not whether the host can actually run it: a CUDA-enabled build on a host whose driver predates the bundled CUDA runtime reports available=True and raises the CUDA runtime's own RuntimeError at construction.

Invalid requests fail loudly and early:

python
from pyqpanda3.core import CPUQVM, Stabilizer

try:
    CPUQVM(precision="float16")
except ValueError as e:  # InvalidPrecisionError, a ValueError subclass
    print(type(e).__name__, "-", e)

try:
    Stabilizer(precision="float32")
except ValueError as e:  # UnsupportedPrecisionError
    print(type(e).__name__, "-", e)

Output:

text
InvalidPrecisionError - unknown precision 'float16'
UnsupportedPrecisionError - backend 'Stabilizer' does not support precision 'float32'; supported precisions are: exact

New exception types: InvalidPrecisionError, UnsupportedPrecisionError, PrecisionConversionError (all ValueError subclasses), plus BackendUnavailableError (RuntimeError subclass) raised when constructing a simulator whose build is absent.

2. Simulator capability registry ​

simulator_capabilities() answers "what can run here" without constructing anything. Called with a name it returns one SimulatorCapabilities record; called without arguments it returns all of them.

python
from pyqpanda3.core import simulator_capabilities

for cap in simulator_capabilities():
    print(f"{cap.name:26s} {','.join(cap.supported_precisions):16s}"
          f" default={cap.default_precision:8s} available={str(cap.available):5s}"
          f" domain={cap.simd_domain}")

Output:

text
CPUQVM                     float32,float64  default=float64  available=True  domain=StateVector
DensityMatrixSimulator     float32,float64  default=float64  available=True  domain=Density
GPUQVM                     float64          default=float64  available=False domain=NotApplicable
MPSQVM                     float32,float64  default=float64  available=True  domain=Tensor
PartialAmplitudeQVM        float32,float64  default=float64  available=True  domain=Amplitude
SingleAmplitudeQVM         float32,float64  default=float64  available=True  domain=Amplitude
SparseHeisenbergSimulator  float32,float64  default=float64  available=True  domain=Sparse
Stabilizer                 exact            default=exact    available=True  domain=NotApplicable

Each record exposes name, supported_precisions, default_precision, available, and simd_domain (one of StateVector, Density, Tensor, Amplitude, Sparse, NotApplicable). The registry is host-dependent: on a build without CUDA the GPUQVM row reads available=False and constructing a GPUQVM raises BackendUnavailableError instead of failing obscurely at import time; a CUDA-enabled build reports float32,float64 there instead.

3. SIMD runtime dispatch ​

The state-vector kernels are now compiled for multiple SIMD ISAs and selected at process start by a runtime detector — x86_64 hosts pick between scalar, sse2, avx2, and avx512; ARM64 hosts between scalar, neon, sve, and sve2. The new pyqpanda3.core.simd submodule exposes the diagnostics and the preference override:

python
from pyqpanda3.core import simd

cap = simd.capabilities()
print(cap.architecture, list(cap.available_backends), cap.active_backend)

simd.set_preference("scalar")   # force scalar kernels
print(simd.active_backend())
simd.set_preference("auto")     # restore the detected default
print(simd.active_backend())

Output (on an AVX2-capable x86_64 host):

text
x86_64 ['scalar', 'sse2', 'avx2', 'avx512'] avx2
scalar
avx2

simd.capabilities() returns an immutable snapshot with architecture, compiled_backends, available_backends, default_backend, active_backend, and fallback_reason. simd.set_preference(backend, strict=False) never raises for an unavailable backend (it records a fallback reason instead); with strict=True it raises UnsupportedSIMDBackendError. On the release benchmark host the AVX2 kernels deliver a median 2.13x speedup over scalar across the representative random-circuit suite (~2.5x at float32, ~1.9x at float64).

4. MPS truncation control and run statistics ​

MPSQVM now accepts truncation_atol and max_bond_dimension at construction, and reports what the SVD truncation actually did through the new stats() method (an MPSRunStats with discarded_weight and max_bond_dimension_seen).

python
import numpy as np
from pyqpanda3.core import MPSQVM, QProg, RY, CZ

rng = np.random.default_rng(7)
prog = QProg()
for q in range(6):
    prog << RY(q, rng.uniform(0, np.pi))
for layer in range(4):
    pairs = [(0, 1), (2, 3), (4, 5)] if layer % 2 == 0 else [(1, 2), (3, 4)]
    for a, b in pairs:
        prog << CZ(a, b) << RY(a, rng.uniform(0, np.pi)) << RY(b, rng.uniform(0, np.pi))

capped = MPSQVM(precision="float64", max_bond_dimension=2)
capped.run(prog, 1)
print(capped.stats().discarded_weight, capped.stats().max_bond_dimension_seen)

exact = MPSQVM()
exact.run(prog, 1)
print(exact.stats().discarded_weight, exact.stats().max_bond_dimension_seen)

Output:

text
0.024353811677256556 2
1.8613363972962274e-34 4

The default truncation_atol is precision-dependent (1e-6 for float32, 1e-12 for float64); max_bond_dimension is unlimited by default. SingleAmplitudeQVM gains a matching stats() (a SingleAmplitudeStats with cache_hit_count, cache_miss_count, and peak_workspace_bytes).

5. Native RESET operation ​

RESET(qubit) collapses a qubit to |0> as a first-class, non-unitary operation — no more measure + conditional-X emulation in user code. It is supported exactly on the CPU state-vector and density-matrix simulators; other backends raise at execution time, and controlled RESET has no physical definition.

python
from pyqpanda3.core import CPUQVM, QProg, RESET, X, measure

prog = QProg() << X(0) << RESET(0) << measure(0, 0)
qvm = CPUQVM()
qvm.run(prog, 100)
print(qvm.result().get_counts())

Output:

text
{'0': 100}

The operation round-trips through OriginIR in both directions and is parsed from OpenQASM 2.0 reset q[i]; statements into the native gate. Note one boundary: exporting a program that contains RESET back to OpenQASM with convert_qprog_to_qasm() raises a RuntimeError in 0.5.0 — serialize through OriginIR when RESET is present.

6. LightSABRE transpiler engine rewrite ​

The layout and routing stack was rebuilt around a shared LightSABRE engine: a bidirectional SABRE layout search with a balanced layout-routing objective, VF2-matching completion for subgraph-embeddable circuits, ring-neighborhood layout candidates, and a work-budget circuit breaker that bounds the search on pathological instances. The Python entry points are unchanged — RoutingMethod.LIGHT_SABRE, LayoutMethod, and the TranspilationOptions.execution config from 0.4.1 — but the outputs are better and the compile times are substantially lower.

On the benchpress suite (1,044 paired test cases against Qiskit 2.5.2, both sides passing 1,044/1,044), pyqpanda3 0.5.0 is faster than Qiskit on 1,040 of 1,044 cases with a geometric-mean time ratio of 0.233x, while the total two-qubit gate count of the outputs stays slightly ahead (0.957x). Across the 892 abstract-compilation cases the geometric mean is 0.212x and the two-qubit depth total is 0.918x of Qiskit's. See the benchmark page for the full analysis.

Determinism is part of the contract: fixing TranspilationExecutionConfig.seed makes repeated transpilations of the same program produce identical output, and the config can now also be passed as the last positional argument of the positional-argument transpile() overload:

python
from pyqpanda3.core import CNOT, H, QProg
from pyqpanda3.transpilation import Transpiler, TranspilationExecutionConfig, generate_topology

prog = QProg() << H(0) << CNOT(0, 1)
topo = generate_topology(4, "square")

config = TranspilationExecutionConfig()
config.seed = 42

transpiler = Transpiler()
a = transpiler.transpile(prog, topo, {}, 2, ["RX", "RY", "RZ", "CZ"], config)
b = transpiler.transpile(prog, topo, {}, 2, ["RX", "RY", "RZ", "CZ"], config)
print(len(a.operations()) == len(b.operations()))

Output:

text
True

7. Noise model semantics stated explicitly ​

Noise-model conventions are now stated explicitly rather than left to inference: Kraus operator matrices are row-major, ReadoutError matrices are indexed M[true][measured], quantum errors apply after the instruction they are attached to, and local (per-qubit) errors override default errors rather than compose with them. NoiseModel also becomes properly copyable:

python
import copy
from pyqpanda3.core import NoiseModel

model = NoiseModel()
model.add_read_out_error([[0.9, 0.1], [0.2, 0.8]], 0)

duplicate = copy.deepcopy(model)
snapshot = model.copy()
print(duplicate is not model, snapshot is not model)

Output:

text
True True

8. Faster kernels, smaller wheels ​

The 0.4.1 and 0.5.0 wheels were also measured head-to-head on the same host and interpreter, on identical seeded workloads (random long-range 19-qubit circuit against three fixed coupling maps, QV100-scale QASM import, 5-controlled-X synthesis, a 4-qubit unitary-oracle loop; times are the minimum of five interleaved rounds). The kernel-level wins are direct: QASM import at 0.43x of the 0.4.1 time, MCX synthesis at 0.44x with the identical 248-CZ output, linear-topology transpilation at 0.77x; square- and heavy-hex-topology transpilation times are on par while the transpiled two-qubit gate counts drop by 15% / 15% / 11% (linear / square / heavy-hex) under a fixed seed. State-vector float64 evolution stays performance-comparable with 0.4.1 -- the simulation speedup comes from the new float32 path and the SIMD kernels (a median 2.13x AVX2-over-scalar on the release benchmark suite).

pyqpanda3 0.5.0 vs 0.4.1 paired workloads

  • Oracle kernel: the ORACLE (arbitrary large-matrix) gate simulation path uses zero-copy matrix handling, blocked GEMM, and hand-written AVX2 complex GEMV/GEMM kernels; qubit-order semantics for routed circuits were corrected (decompositions are now equivalent after layout remapping).
  • QASM import fast path: importing QV100-scale QASM files no longer materializes intermediate circuit copies; the QV100 import benchmark drops from 1.48s to 0.73s.
  • DAG and translation: DAG circuit construction gained move semantics and a per-node scan-head cache (end-to-end transpilation about 20% faster on the paired suite), the constant-folding optimizer converges by fixed-point threshold, and cancellation/star-sweep passes deallocate deterministically.
  • mimalloc: wheels bundle the mimalloc allocator as a private shared runtime and the Python extension allocates through it, giving more stable large-state performance; wheels are now platform-specific (cpXY-cpXY-<platform> tags), so pip resolves the right binary per interpreter and OS.

API changes ​

Released under the MIT License.