Skip to content

Noise Simulation

Real quantum hardware is noisy. This tutorial covers how to model and simulate noise in pyqpanda3 using the NoiseModel framework and seven built-in quantum error channels.


Why Noise Simulation Matters

Quantum computers operate in the NISQ (Noisy Intermediate-Scale Quantum) era, where hardware is subject to various noise sources:

  • Gate errors: Imperfect gate implementations
  • Readout errors: Measurement incorrectly reports the result
  • Decoherence: Qubits lose quantum information over time (T1 relaxation, T2 dephasing)

Noise simulation helps you:

  1. Predict how algorithms perform on real hardware
  2. Develop noise-resilient algorithms
  3. Characterize and calibrate quantum devices
  4. Verify error correction schemes

Noise Model Framework

NoiseModel Class

The NoiseModel class collects quantum errors and readout errors into a single model that can be passed to any simulator:

python
from pyqpanda3 import core

# Create an empty noise model
noise = core.NoiseModel()

# Check if any noise is configured
print(f"Noise enabled: {noise.is_enabled()}")
# Output: Noise enabled: False

Adding Gate Errors

Add quantum errors to specific gates and qubits:

python
from pyqpanda3 import core

# Add error to a specific gate type on specific qubits
error = core.depolarizing_error(0.01)
noise.add_quantum_error(error, core.GateType.CNOT, [0, 1])

# Add error to multiple gate types on specific qubits
noise.add_quantum_error(error, [core.GateType.H, core.GateType.T], [0])

# Add error to a gate type on ALL qubits
noise.add_all_qubit_quantum_error(error, core.GateType.CNOT)

# Add error to multiple gate types on ALL qubits
noise.add_all_qubit_quantum_error(error, [core.GateType.H, core.GateType.CNOT])

Adding Readout Errors

Readout errors model the probability of measuring the wrong classical value:

python
from pyqpanda3 import core

# Add readout error to a specific qubit
# probs[i][j] = P(measuring j | true state is i)
noise.add_read_out_error([[0.95, 0.05], [0.05, 0.95]], 0)

# Add readout error to ALL qubits
noise.add_all_qubit_read_out_error([[0.95, 0.05], [0.05, 0.95]])

Quantum Error Channels

pyqpanda3 provides seven quantum error channels, each modeling a different physical noise mechanism. All error channels are described using Kraus operators.

Kraus Representation

Every quantum noise channel can be described by a set of Kraus operators {Ki}:

E(ρ)=iKiρKi

where iKiKi=I (completeness relation).

1. Pauli X Error (Bit Flip)

The bit flip channel randomly applies the X (NOT) gate with probability p:

E(ρ)=(1p)ρ+pXρX

Kraus operators:

  • K0=1pI
  • K1=pX
python
from pyqpanda3 import core

# Create a bit flip error with probability 0.05
error_x = core.pauli_x_error(0.05)

Physical model: Energy relaxation that flips |0|1

2. Pauli Y Error

The Pauli Y error applies the Y gate with probability p:

E(ρ)=(1p)ρ+pYρY

Kraus operators:

  • K0=1pI
  • K1=pY
python
from pyqpanda3 import core

# Create a Pauli Y error with probability 0.03
error_y = core.pauli_y_error(0.03)

3. Pauli Z Error (Phase Flip)

The phase flip channel applies the Z gate with probability p:

E(ρ)=(1p)ρ+pZρZ

Kraus operators:

  • K0=1pI
  • K1=pZ
python
from pyqpanda3 import core

# Create a phase flip error with probability 0.02
error_z = core.pauli_z_error(0.02)

Physical model: Dephasing that introduces a relative phase between |0 and |1

4. Depolarizing Error

The depolarizing channel replaces the state with the maximally mixed state I/d with probability p:

E(ρ)=(1p)ρ+pId

For a single qubit (d=2), this is equivalent to applying each Pauli with equal probability:

E(ρ)=(1p)ρ+p3(XρX+YρY+ZρZ)

Kraus operators:

  • K0=1pI
  • K1=p/3X
  • K2=p/3Y
  • K3=p/3Z
python
from pyqpanda3 import core

# Create a depolarizing error with probability 0.01
error_dep = core.depolarizing_error(0.01)

Physical model: Generic noise that drives the state toward maximum entropy

5. Amplitude Damping Error

The amplitude damping channel models T1 relaxation (energy dissipation). It describes the decay |1|0:

E(ρ)=K0ρK0+K1ρK1

Kraus operators:

  • K0=(1001γ)
  • K1=(0γ00)
python
from pyqpanda3 import core

# Create an amplitude damping error with decay rate 0.02
error_ad = core.amplitude_damping_error(0.02)

Physical model: T1 relaxation — the qubit loses energy to its environment

6. Phase Damping Error

The phase damping channel models T2 dephasing (loss of phase coherence):

E(ρ)=K0ρK0+K1ρK1

Kraus operators:

  • K0=(1001λ)
  • K1=(000λ)
python
from pyqpanda3 import core

# Create a phase damping error with rate 0.01
error_pd = core.phase_damping_error(0.01)

Physical model: T2 dephasing — loss of quantum phase information without energy loss

7. Decoherence Error

The decoherence error combines amplitude damping and phase damping, modeling both T1 and T2 processes:

python
from pyqpanda3 import core

# Create a decoherence error (T1=80us, T2=60us, gate_time=20ns)
error_dec = core.decoherence_error(80.0, 60.0, 20.0)

Physical model: Combined T1 and T2 decoherence processes

Error Channel Summary

ErrorPhysical ModelKraus OperatorsParameter
pauli_x_error(p)Bit flipK0=1pI, K1=pXp: flip probability
pauli_y_error(p)Bit+Phase flipK0=1pI, K1=pYp: error probability
pauli_z_error(p)Phase flipK0=1pI, K1=pZp: flip probability
depolarizing_error(p)Generic noise4 operatorsp: depolarization prob
amplitude_damping_error(γ)T1 decay2 operatorsγ: damping rate
phase_damping_error(λ)T2 dephasing2 operatorsλ: dephasing rate
decoherence_error(T1, T2, t_gate)T1+T2 combinedMultipleT1, T2 (us), t_gate (ns)

QuantumError Class

The QuantumError class wraps noise data and provides composition operations:

Creating Quantum Errors

python
from pyqpanda3 import core
import numpy as np

# Method 1: From Kraus matrices (list of numpy arrays)
kraus = [
    np.sqrt(0.99) * np.eye(2),
    np.sqrt(0.01) * np.array([[0, 1], [1, 0]])
]
error = core.QuantumError(kraus)

# Method 2: From noise map (Pauli probabilities)
noise_map = {"X": 0.01, "Y": 0.005, "Z": 0.005}
error2 = core.QuantumError(noise_map)

Error Properties

python
from pyqpanda3 import core

# Get number of qubits affected
print(f"Qubits affected: {error.qubit_num()}")

# Get the error type
print(f"Error type: {error.error_type()}")

# Get Kraus matrices
karus = error.error_karus()
print(f"Kraus matrices: {karus}")

Error Composition

python
from pyqpanda3 import core

# Tensor product of two errors (independent errors on different qubits)
combined = error.tensor(error2)

# Expansion to a larger system
expanded = error.expand(error2)

# Sequential composition (error applied after another)
composed = error.compose(error2)

Comparing Noise Channels

Different noise channels affect quantum states in fundamentally different ways. Understanding these differences is essential for selecting the right error model for your use case.

Visual Comparison of Error Effects

Channel Comparison Table

The table below compares all seven error channels on key characteristics:

ChannelPreserves Energy?Preserves Phase?CPTP?Unital?Typical Physical Source
Pauli XNoYesYesYesSpin relaxation
Pauli YNoNoYesYesCombined bit+phase flip
Pauli ZYesNoYesYesMagnetic flux noise
DepolarizingNoNoYesYesGeneric gate error
Amplitude DampingNo (irreversible)YesYesNoT1 energy decay
Phase DampingYesNoYesYesT2 pure dephasing
DecoherenceNoNoYesNoCombined T1+T2

A channel is unital if it maps the identity to the identity: E(I)=I. Unital channels drive states toward the maximally mixed state I/d.

Code Example: Comparing All Error Types

The following code applies each of the seven error channels to the same Bell state circuit and measures how the output distribution changes relative to the ideal result.

python
from pyqpanda3 import core
import math

# Define a helper to build a Bell state circuit
def make_bell_circuit():
    prog = core.QProg()
    prog << core.H(0) << core.CNOT(0, 1)
    prog << core.measure([0, 1], [0, 1])
    return prog

# Error probability or rate to use for each channel
error_param = 0.05

# Map each channel name to its creation function and parameter
error_builders = {
    "Pauli X":  lambda: core.pauli_x_error(error_param),
    "Pauli Y":  lambda: core.pauli_y_error(error_param),
    "Pauli Z":  lambda: core.pauli_z_error(error_param),
    "Depolarizing": lambda: core.depolarizing_error(error_param),
    "Amp. Damping": lambda: core.amplitude_damping_error(error_param),
    "Phase Damping": lambda: core.phase_damping_error(error_param),
    "Decoherence": lambda: core.decoherence_error(80.0, 60.0, 20.0),
}

# Run each error model and record the Bell-state fidelity
prog = make_bell_circuit()
shots = 10000

for name, builder in error_builders.items():
    noise = core.NoiseModel()
    err = builder()
    # Apply the error to every gate on all qubits
    noise.add_all_qubit_quantum_error(err, core.GateType.CNOT)
    noise.add_all_qubit_quantum_error(err, core.GateType.H)

    machine = core.CPUQVM()
    machine.run(prog, shots=shots, model=noise)
    counts = machine.result().get_counts()

    # Compute fidelity as fraction of results in {|00>, |11>}
    bell = counts.get("00", 0) + counts.get("11", 0)
    fidelity = bell / shots
    print(f"{name:18s}  fidelity={fidelity:.4f}  counts={counts}")

Expected output pattern (values will vary due to sampling):

Pauli X             fidelity=0.8950  counts={'00': 4475, '11': 4475, '01': 525, '10': 525}
Pauli Y             fidelity=0.8950  counts={'00': 4480, '11': 4470, '01': 520, '10': 530}
Pauli Z             fidelity=0.9500  counts={'00': 4750, '11': 4750, '01': 250, '10': 250}
Depolarizing        fidelity=0.8800  counts={'00': 4400, '11': 4400, '01': 600, '10': 600}
Amp. Damping        fidelity=0.9200  counts={'00': 5000, '11': 4200, '01': 400, '10': 400}
Phase Damping       fidelity=0.9500  counts={'00': 4750, '11': 4750, '01': 250, '10': 250}
Decoherence         fidelity=0.8700  counts={'00': 4800, '11': 3900, '01': 650, '10': 650}

Key observations from the comparison:

  • Pauli X and Y errors produce symmetric leakage out of the Bell basis, giving roughly equal populations in |01 and |10.
  • Pauli Z errors do not change measurement outcomes on the computational basis, so they appear to preserve the Bell fidelity on this particular circuit. However, they destroy phase coherence.
  • Depolarizing noise is the most destructive per-unit-probability because it includes all three Pauli components simultaneously.
  • Amplitude damping is asymmetric: it preferentially drives |1 toward |0, so |00 gets more counts than |11.

Error Propagation Analysis

When a circuit has multiple layers of gates, errors accumulate. Understanding how noise scales with circuit depth is critical for predicting algorithm performance.

Theoretical Background

For a circuit with d layers (depth), each with an independent depolarizing error of rate p per gate, the total error probability grows approximately as:

ptotal1(1p)ngates

where ngates is the total number of gates. For small p, this simplifies to:

ptotalngatesp

The output fidelity (overlap with the ideal state) follows an exponential decay:

F(d)F0eαd

where α is a decay constant that depends on the error rate per layer.

Measuring Fidelity vs. Circuit Depth

The following experiment demonstrates how noise accumulates by running circuits of increasing depth and measuring the fidelity of the output.

python
from pyqpanda3 import core
import math

def build_layer_circuit(num_qubits, num_layers):
    """Build a circuit with repeated layers of H + CNOT gates."""
    prog = core.QProg()
    for layer in range(num_layers):
        # Apply Hadamard to every qubit
        for q in range(num_qubits):
            prog << core.H(q)
        # Apply CNOT between adjacent qubits
        for q in range(0, num_qubits - 1, 2):
            prog << core.CNOT(q, q + 1)
    prog << core.measure(list(range(num_qubits)), list(range(num_qubits)))
    return prog

# Set up a moderate noise model
noise = core.NoiseModel()
noise.add_all_qubit_quantum_error(
    core.depolarizing_error(0.005), core.GateType.H
)
noise.add_all_qubit_quantum_error(
    core.depolarizing_error(0.02), core.GateType.CNOT
)

# Measure fidelity as a function of depth
num_qubits = 4
shots = 5000

for depth in [1, 2, 4, 8, 16, 32]:
    prog = build_layer_circuit(num_qubits, depth)

    # Run with noise
    machine = core.CPUQVM()
    machine.run(prog, shots=shots, model=noise)
    noisy_counts = machine.result().get_counts()

    # Run without noise for comparison
    machine.run(prog, shots=shots)
    ideal_counts = machine.result().get_counts()

    # Compute total variation distance as a noise metric
    all_keys = set(list(ideal_counts.keys()) + list(noisy_counts.keys()))
    tvd = 0.0
    for key in all_keys:
        p_ideal = ideal_counts.get(key, 0) / shots
        p_noisy = noisy_counts.get(key, 0) / shots
        tvd += abs(p_ideal - p_noisy)
    tvd /= 2.0

    print(f"Depth={depth:3d}  TVD={tvd:.4f}  "
          f"ideal_keys={len(ideal_counts)}  noisy_keys={len(noisy_counts)}")

Error Accumulation Per Gate Type

Not all gates accumulate errors at the same rate. Two-qubit gates typically have 10-100x higher error rates than single-qubit gates:

Ftotali=1N1q(1p1q)j=1N2q(1p2q)

where N1q and N2q are the number of single- and two-qubit gates, respectively.

python
from pyqpanda3 import core

# Demonstrate that two-qubit gate errors dominate
prog = core.QProg()
prog << core.H(0) << core.H(1) << core.H(2)
prog << core.CNOT(0, 1) << core.CNOT(1, 2)
prog << core.measure([0, 1, 2], [0, 1, 2])

# Noise only on single-qubit gates
noise_1q = core.NoiseModel()
noise_1q.add_all_qubit_quantum_error(
    core.depolarizing_error(0.005), core.GateType.H
)

# Noise only on two-qubit gates
noise_2q = core.NoiseModel()
noise_2q.add_all_qubit_quantum_error(
    core.depolarizing_error(0.02), core.GateType.CNOT
)

shots = 10000
machine = core.CPUQVM()

# Ideal baseline
machine.run(prog, shots=shots)
ideal = machine.result().get_counts()

# Single-qubit noise only
machine.run(prog, shots=shots, model=noise_1q)
counts_1q = machine.result().get_counts()

# Two-qubit noise only
machine.run(prog, shots=shots, model=noise_2q)
counts_2q = machine.result().get_counts()

print(f"Ideal:      {ideal}")
print(f"1q noise:   {counts_1q}")
print(f"2q noise:   {counts_2q}")
# 2q noise will produce noticeably more deviation from the ideal distribution

GateType Enum

When adding errors to a noise model, specify the gate type using the GateType enum:

python
# Single-qubit gates
core.GateType.H       # Hadamard
core.GateType.X       # Pauli-X
core.GateType.Y       # Pauli-Y
core.GateType.Z       # Pauli-Z
core.GateType.S       # Phase gate
core.GateType.T       # T gate
core.GateType.RX      # RX rotation
core.GateType.RY      # RY rotation
core.GateType.RZ      # RZ rotation
core.GateType.U1      # U1 gate
core.GateType.U2      # U2 gate
core.GateType.U3      # U3 gate
core.GateType.U4      # U4 gate

# Two-qubit gates
core.GateType.CNOT    # Controlled-NOT
core.GateType.CZ      # Controlled-Z
core.GateType.CP      # Controlled-Phase
core.GateType.CRX     # Controlled-RX
core.GateType.CRY     # Controlled-RY
core.GateType.CRZ     # Controlled-RZ
core.GateType.SWAP    # SWAP
core.GateType.ISWAP   # iSWAP
core.GateType.SQISWAP # Square-root iSWAP
core.GateType.RXX     # RXX rotation
core.GateType.RYY     # RYY rotation
core.GateType.RZZ     # RZZ rotation
core.GateType.RZX     # RZX rotation

# Multi-qubit gates
core.GateType.TOFFOLI # Toffoli (CCNOT)

Running Noisy Simulations

With CPUQVM

python
from pyqpanda3 import core

# Build circuit
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)
prog << core.measure([0, 1], [0, 1])

# Create noise model
noise = core.NoiseModel()
noise.add_all_qubit_quantum_error(core.depolarizing_error(0.01), core.GateType.CNOT)
noise.add_all_qubit_read_out_error([[0.95, 0.05], [0.05, 0.95]])

# Run with noise
machine = core.CPUQVM()
machine.run(prog, shots=10000, model=noise)
print(machine.result().get_counts())

With DensityMatrixSimulator

python
from pyqpanda3 import core

# Build circuit (no measurement needed)
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)

# Create noise model
noise = core.NoiseModel()
noise.add_all_qubit_quantum_error(
    core.amplitude_damping_error(0.05),
    core.GateType.H
)
noise.add_all_qubit_quantum_error(
    core.depolarizing_error(0.02),
    core.GateType.CNOT
)

# Run with exact noise simulation
dm_sim = core.DensityMatrixSimulator()
dm_sim.run(prog, noise)

# Get noisy state probabilities
print(f"Probabilities: {dm_sim.state_probs()}")
print(f"Density matrix:\n{dm_sim.density_matrix()}")

Expected output:

Probabilities: {0: 0.475, 1: 0.025, 2: 0.025, 3: 0.475}
Density matrix:
[[0.475  0.   0.    0.475]
 [0.     0.025 0.025 0.   ]
 [0.     0.025 0.025 0.   ]
 [0.475  0.   0.    0.475]]

With Stabilizer

python
from pyqpanda3 import core

# Clifford circuit
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)
prog << core.measure([0, 1], [0, 1])

# Noise model (only Pauli errors for Stabilizer)
noise = core.NoiseModel()
noise.add_all_qubit_quantum_error(
    core.pauli_x_error(0.01),
    core.GateType.H
)
noise.add_all_qubit_quantum_error(
    core.depolarizing_error(0.02),
    core.GateType.CNOT
)

# Run noisy Stabilizer simulation
stab = core.Stabilizer()
stab.run(prog, shots=10000, model=noise)
print(stab.result().get_counts())

Practical Examples

Example 1: Modeling a Real Quantum Device

python
from pyqpanda3 import core

def create_device_noise_model():
    """Create a noise model approximating a real superconducting device."""
    noise = core.NoiseModel()

    # Single-qubit gate errors (higher fidelity)
    single_gate_error = core.depolarizing_error(0.001)
    noise.add_all_qubit_quantum_error(single_gate_error, [
        core.GateType.H,
        core.GateType.X,
        core.GateType.Y,
        core.GateType.Z,
        core.GateType.S,
        core.GateType.T,
    ])

    # Two-qubit gate errors (lower fidelity)
    two_gate_error = core.depolarizing_error(0.01)
    noise.add_all_qubit_quantum_error(two_gate_error, [
        core.GateType.CNOT,
        core.GateType.CZ,
    ])

    # T1 decay
    t1_error = core.amplitude_damping_error(0.005)
    noise.add_all_qubit_quantum_error(t1_error, [
        core.GateType.H,
        core.GateType.X,
    ])

    # Readout errors
    noise.add_all_qubit_read_out_error([
        [0.97, 0.03],  # P(measure 0 | true 0), P(measure 1 | true 0)
        [0.04, 0.96]   # P(measure 0 | true 1), P(measure 1 | true 1)
    ])

    return noise

# Use the realistic noise model
noise = create_device_noise_model()

prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1) << core.measure([0, 1], [0, 1])

machine = core.CPUQVM()
machine.run(prog, shots=10000, model=noise)
counts = machine.result().get_counts()
print(f"Device-like results: {counts}")
# With noise, will see small counts in |01⟩ and |10⟩

Example 2: Comparing Noise Levels

python
from pyqpanda3 import core

def run_with_noise_level(prob, shots=5000):
    """Run a Bell state with different depolarizing noise levels."""
    noise = core.NoiseModel()
    noise.add_all_qubit_quantum_error(
        core.depolarizing_error(prob),
        core.GateType.CNOT
    )

    prog = core.QProg()
    prog << core.H(0) << core.CNOT(0, 1)
    prog << core.measure([0, 1], [0, 1])

    machine = core.CPUQVM()
    machine.run(prog, shots=shots, model=noise)
    return machine.result().get_counts()

# Compare noise levels
for p in [0.0, 0.01, 0.05, 0.1, 0.2]:
    counts = run_with_noise_level(p)
    bell_prob = (counts.get('00', 0) + counts.get('11', 0)) / 5000
    print(f"p={p:.2f}: Bell fidelity ≈ {bell_prob:.3f}, counts={counts}")

Example 3: Per-Qubit Noise Configuration

python
from pyqpanda3 import core

# Different noise levels for different qubits
noise = core.NoiseModel()

# Qubit 0 has higher error rate
noise.add_quantum_error(
    core.depolarizing_error(0.02), core.GateType.H, [0]
)
noise.add_quantum_error(
    core.depolarizing_error(0.05), core.GateType.CNOT, [0, 1]
)

# Qubit 2 has lower error rate
noise.add_quantum_error(
    core.depolarizing_error(0.005), core.GateType.H, [2]
)
noise.add_quantum_error(
    core.depolarizing_error(0.01), core.GateType.CNOT, [2, 3]
)

# Run a 4-qubit circuit with per-qubit noise
prog = core.QProg()
prog << core.H(0) << core.H(2)
prog << core.CNOT(0, 1) << core.CNOT(2, 3)
prog << core.measure([0, 1, 2, 3], [0, 1, 2, 3])

machine = core.CPUQVM()
machine.run(prog, shots=10000, model=noise)
print(machine.result().get_counts())

Example 4: GHZ State Under Noise

The Greenberger-Horne-Zeilinger (GHZ) state 12(|000+|111) is highly sensitive to noise because its quantum correlations span all qubits. This example measures how GHZ fidelity degrades with the number of qubits under a realistic noise model.

python
from pyqpanda3 import core

def build_ghz_circuit(num_qubits):
    """Create a GHZ state on the given number of qubits."""
    prog = core.QProg()
    prog << core.H(0)
    for i in range(num_qubits - 1):
        prog << core.CNOT(i, i + 1)
    prog << core.measure(list(range(num_qubits)), list(range(num_qubits)))
    return prog

# Define a realistic noise model
noise = core.NoiseModel()
noise.add_all_qubit_quantum_error(
    core.depolarizing_error(0.002), core.GateType.H
)
noise.add_all_qubit_quantum_error(
    core.depolarizing_error(0.01), core.GateType.CNOT
)
noise.add_all_qubit_read_out_error([[0.97, 0.03], [0.04, 0.96]])

shots = 10000
machine = core.CPUQVM()

# Measure GHZ fidelity as a function of qubit count
for n in [2, 3, 4, 5, 6]:
    prog = build_ghz_circuit(n)

    machine.run(prog, shots=shots, model=noise)
    counts = machine.result().get_counts()

    # GHZ fidelity = fraction of results that are all-0 or all-1
    all_zero = "0" * n
    all_one = "1" * n
    ghz_count = counts.get(all_zero, 0) + counts.get(all_one, 0)
    fidelity = ghz_count / shots

    print(f"n={n}: GHZ fidelity={fidelity:.4f}  "
          f"all-0={counts.get(all_zero,0)}  "
          f"all-1={counts.get(all_one,0)}  "
          f"total_outcomes={len(counts)}")

The output will show fidelity decreasing rapidly with qubit count because the number of CNOT gates (and thus accumulated noise) grows linearly:

n=2: GHZ fidelity=0.9750  ...
n=3: GHZ fidelity=0.9580  ...
n=4: GHZ fidelity=0.9310  ...
n=5: GHZ fidelity=0.9020  ...
n=6: GHZ fidelity=0.8640  ...

Example 5: Noise Sweep Analysis

When developing algorithms, it is useful to sweep over a range of noise parameters to determine the threshold at which the algorithm breaks down. This example sweeps both depolarizing and amplitude damping rates on a variational-style circuit.

python
from pyqpanda3 import core

def build_variational_circuit():
    """Build a simple variational-style ansatz circuit."""
    prog = core.QProg()
    # Layer 1
    prog << core.H(0) << core.H(1)
    prog << core.CNOT(0, 1)
    # Layer 2
    prog << core.RZ(0, 0.5) << core.RZ(1, 0.5)
    prog << core.CNOT(0, 1)
    # Layer 3
    prog << core.RZ(0, 0.3) << core.RZ(1, 0.3)
    prog << core.H(0) << core.H(1)
    prog << core.measure([0, 1], [0, 1])
    return prog

def compute_hamming_distance(counts, shots):
    """Compute the average Hamming weight of measurement outcomes."""
    total_weight = 0
    for bitstring, count in counts.items():
        weight = bitstring.count("1")
        total_weight += weight * count
    return total_weight / shots

prog = build_variational_circuit()
shots = 5000
machine = core.CPUQVM()

# First, get the ideal reference
machine.run(prog, shots=shots)
ideal_counts = machine.result().get_counts()
ideal_hw = compute_hamming_distance(ideal_counts, shots)

# Sweep depolarizing noise from 0.001 to 0.05
print("Depolarizing sweep:")
print(f"  {'Rate':>8s}  {'Hamming Weight':>15s}  {'Delta from Ideal':>17s}")
for rate in [0.001, 0.005, 0.01, 0.02, 0.05]:
    noise = core.NoiseModel()
    noise.add_all_qubit_quantum_error(
        core.depolarizing_error(rate), core.GateType.CNOT
    )
    noise.add_all_qubit_quantum_error(
        core.depolarizing_error(rate * 0.1), core.GateType.H
    )

    machine.run(prog, shots=shots, model=noise)
    counts = machine.result().get_counts()
    hw = compute_hamming_distance(counts, shots)
    delta = abs(hw - ideal_hw)
    print(f"  {rate:8.4f}  {hw:15.4f}  {delta:17.4f}")

# Sweep amplitude damping rate from 0.001 to 0.05
print("\nAmplitude damping sweep:")
print(f"  {'Rate':>8s}  {'Hamming Weight':>15s}  {'Delta from Ideal':>17s}")
for rate in [0.001, 0.005, 0.01, 0.02, 0.05]:
    noise = core.NoiseModel()
    noise.add_all_qubit_quantum_error(
        core.amplitude_damping_error(rate), core.GateType.CNOT
    )
    noise.add_all_qubit_quantum_error(
        core.amplitude_damping_error(rate * 0.5), core.GateType.H
    )

    machine.run(prog, shots=shots, model=noise)
    counts = machine.result().get_counts()
    hw = compute_hamming_distance(counts, shots)
    delta = abs(hw - ideal_hw)
    print(f"  {rate:8.4f}  {hw:15.4f}  {delta:17.4f}")

Note how amplitude damping produces a bias in the Hamming weight (systematically pushing toward fewer 1-bits), while depolarizing noise produces a symmetric spread. This asymmetry is important for algorithms that depend on the exact output distribution.

Example 6: Density Matrix Tomography Under Noise

The DensityMatrix simulator provides exact state information, making it ideal for detailed noise analysis. This example reconstructs and analyzes the density matrix of a Bell state under different noise channels.

python
from pyqpanda3 import core
import numpy as np

# Build a Bell state circuit without measurement
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)

# Reference: ideal density matrix
dm_sim = core.DensityMatrixSimulator()
dm_sim.run(prog)
ideal_dm = dm_sim.density_matrix()
ideal_probs = dm_sim.state_probs()

print("=== Ideal Bell State ===")
print(f"Probabilities: {ideal_probs}")
print(f"Purity: {np.trace(np.dot(ideal_dm, ideal_dm)):.6f}")
# Ideal Bell state has purity = 1.0

# Analyze each noise channel's effect on the density matrix
channels = {
    "Depolarizing (1%)":   core.depolarizing_error(0.01),
    "Amp. Damping (2%)":  core.amplitude_damping_error(0.02),
    "Phase Damping (2%)": core.phase_damping_error(0.02),
    "Pauli X (5%)":       core.pauli_x_error(0.05),
    "Pauli Z (5%)":       core.pauli_z_error(0.05),
}

print("\n=== Noise Channel Analysis ===")
for name, err in channels.items():
    noise = core.NoiseModel()
    noise.add_all_qubit_quantum_error(err, core.GateType.CNOT)
    noise.add_all_qubit_quantum_error(err, core.GateType.H)

    dm_sim.run(prog, noise)
    noisy_dm = dm_sim.density_matrix()
    noisy_probs = dm_sim.state_probs()

    # Compute purity: Tr(rho^2)
    purity = np.trace(np.dot(noisy_dm, noisy_dm))

    # Compute fidelity with ideal state
    # F(rho, sigma) = [Tr(sqrt(sqrt(rho) * sigma * sqrt(rho)))]^2
    # For pure ideal state |psi>: F = <psi|rho|psi>
    fidelity = np.real(ideal_dm[0, 0] * noisy_dm[0, 0]
                       + ideal_dm[0, 3] * noisy_dm[3, 0]
                       + ideal_dm[3, 0] * noisy_dm[0, 3]
                       + ideal_dm[3, 3] * noisy_dm[3, 3])

    print(f"\n{name}:")
    print(f"  P(00)={noisy_probs.get(0, 0):.4f}  "
          f"P(11)={noisy_probs.get(3, 0):.4f}  "
          f"P(01)={noisy_probs.get(1, 0):.4f}  "
          f"P(10)={noisy_probs.get(2, 0):.4f}")
    print(f"  Purity={purity:.4f}  Bell Fidelity={fidelity:.4f}")

Key takeaways from the density matrix analysis:

  • Purity drops below 1.0 for all noise channels, quantifying how much mixedness the noise introduces.
  • Pauli Z noise reduces purity but does not change the computational-basis probabilities, since Z|0=|0 and Z|1=|1. The noise only affects off-diagonal elements (coherences).
  • Amplitude damping breaks the symmetry between |00 and |11, pushing probability toward |00.
  • Depolarizing noise evenly distributes probability leakage across all non-ideal outcomes.

NoiseOpType Enum

The NoiseOpType enum specifies the internal representation of quantum errors:

python
# Kraus matrix representation
core.NoiseOpType.KARUS_MATRIICES

# Noise operation representation
core.NoiseOpType.NOISE

Gate-Dependent Noise

Real quantum processors have different error rates for different gate types. A Hadamard gate on one device might have an error rate of 5×104 while a CNOT gate on the same device has 2×102. Accurately modeling this gate-dependent behavior is essential for realistic simulations.

Why Gate-Dependent Noise Matters

Per-Gate-Type Noise Configuration

The following example demonstrates how to assign different error channels and rates to different gate types in a single noise model:

python
from pyqpanda3 import core

def create_gate_dependent_noise():
    """Build a noise model with distinct error profiles per gate type."""
    noise = core.NoiseModel()

    # Single-qubit rotation gates have low depolarizing noise
    rot_error = core.depolarizing_error(0.0005)
    noise.add_all_qubit_quantum_error(rot_error, [
        core.GateType.H,
        core.GateType.X,
        core.GateType.Y,
        core.GateType.Z,
        core.GateType.S,
        core.GateType.T,
    ])

    # Parametric rotation gates have slightly higher noise due to
    # longer gate times and calibration uncertainty
    param_rot_error = core.depolarizing_error(0.001)
    noise.add_all_qubit_quantum_error(param_rot_error, [
        core.GateType.RX,
        core.GateType.RY,
        core.GateType.RZ,
    ])

    # CNOT is the noisiest gate on superconducting hardware
    cnot_error = core.depolarizing_error(0.02)
    noise.add_all_qubit_quantum_error(cnot_error, core.GateType.CNOT)

    # CZ gates may have different error characteristics than CNOT
    cz_error = core.depolarizing_error(0.015)
    noise.add_all_qubit_quantum_error(cz_error, core.GateType.CZ)

    # SWAP gates are decomposed into 3 CNOTs on most hardware
    swap_error = core.depolarizing_error(0.05)
    noise.add_all_qubit_quantum_error(swap_error, core.GateType.SWAP)

    return noise

noise = create_gate_dependent_noise()

# Build a circuit that uses multiple gate types
prog = core.QProg()
prog << core.H(0)
prog << core.RZ(1, 1.57)
prog << core.CNOT(0, 1)
prog << core.T(0)
prog << core.CZ(1, 2)
prog << core.measure([0, 1, 2], [0, 1, 2])

machine = core.CPUQVM()
machine.run(prog, shots=10000, model=noise)
print(f"Gate-dependent noise result: {machine.result().get_counts()}")

Combining Multiple Error Channels Per Gate

On real hardware, a single gate may be affected by multiple independent noise sources simultaneously. You can model this by composing errors:

python
from pyqpanda3 import core

noise = core.NoiseModel()

# For CNOT gates, model both coherent (depolarizing) and
# incoherent (amplitude damping) noise
dep_err = core.depolarizing_error(0.015)
amp_err = core.amplitude_damping_error(0.003)

# Apply depolarizing error to CNOT
noise.add_all_qubit_quantum_error(dep_err, core.GateType.CNOT)

# Also add amplitude damping on CNOT(0, 1)
noise.add_quantum_error(amp_err, core.GateType.CNOT, [0, 1])

# For single-qubit gates, add phase damping to model T2 processes
phase_err = core.phase_damping_error(0.002)
noise.add_all_qubit_quantum_error(phase_err, [
    core.GateType.H,
    core.GateType.T,
    core.GateType.S,
])

prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1) << core.T(0)
prog << core.measure([0, 1], [0, 1])

machine = core.CPUQVM()
machine.run(prog, shots=10000, model=noise)
print(f"Multi-channel noise: {machine.result().get_counts()}")

Per-Gate Noise Summary

Gate CategoryTypical Error RateRecommended Channel
Clifford single-qubit (H, S, X)104 -- 103Depolarizing
Non-Clifford single-qubit (T)103 -- 102Depolarizing
Parametric rotations (RX, RY, RZ)103 -- 5×103Depolarizing
Two-qubit entangling (CNOT, CZ)102 -- 5×102Depolarizing + Amplitude Damping
SWAP (native or decomposed)3× CNOT rateDepolarizing
Measurement103 -- 102Readout error matrix

Noise Model Calibration

To make simulations predictive, you need to derive noise model parameters from actual hardware characterization data. This section explains how to translate raw calibration metrics into pyqpanda3 noise model parameters.

From Hardware Metrics to Noise Parameters

Deriving Amplitude Damping Rate from T1

The amplitude damping rate γ for a gate of duration tg given a T1 relaxation time is:

γ=1etg/T1

For short gate times (tgT1), this approximates to γtg/T1.

python
from pyqpanda3 import core
import math

def t1_to_amplitude_damping(t1_us, gate_time_ns):
    """Convert T1 time and gate duration to an amplitude damping rate.

    Args:
        t1_us: T1 relaxation time in microseconds
        gate_time_ns: gate duration in nanoseconds

    Returns:
        gamma: amplitude damping rate for use in amplitude_damping_error
    """
    # Convert to same units (microseconds)
    gate_time_us = gate_time_ns / 1000.0
    gamma = 1.0 - math.exp(-gate_time_us / t1_us)
    return gamma

# Example: T1 = 80 us, single-qubit gate time = 20 ns
gamma_1q = t1_to_amplitude_damping(t1_us=80, gate_time_ns=20)
print(f"1q amplitude damping rate: {gamma_1q:.6f}")

# Example: T1 = 80 us, CNOT gate time = 300 ns
gamma_2q = t1_to_amplitude_damping(t1_us=80, gate_time_ns=300)
print(f"2q amplitude damping rate: {gamma_2q:.6f}")

# Create the error channels
ad_1q = core.amplitude_damping_error(gamma_1q)
ad_2q = core.amplitude_damping_error(gamma_2q)

Deriving Phase Damping Rate from T2

The phase damping rate λ for a gate of duration tg given a T2 dephasing time is:

λ=1etg/T2

Note that T2 must satisfy T22T1 due to the fundamental relationship between relaxation and dephasing.

python
from pyqpanda3 import core
import math

def t2_to_phase_damping(t2_us, gate_time_ns):
    """Convert T2 time and gate duration to a phase damping rate.

    Args:
        t2_us: T2 dephasing time in microseconds
        gate_time_ns: gate duration in nanoseconds

    Returns:
        lambda_param: phase damping rate for use in phase_damping_error
    """
    gate_time_us = gate_time_ns / 1000.0
    lambda_param = 1.0 - math.exp(-gate_time_us / t2_us)
    return lambda_param

# Example: T2 = 60 us, single-qubit gate time = 20 ns
lambda_1q = t2_to_phase_damping(t2_us=60, gate_time_ns=20)
print(f"1q phase damping rate: {lambda_1q:.6f}")

# Example: T2 = 60 us, CNOT gate time = 300 ns
lambda_2q = t2_to_phase_damping(t2_us=60, gate_time_ns=300)
print(f"2q phase damping rate: {lambda_2q:.6f}")

# Create the error channels
pd_1q = core.phase_damping_error(lambda_1q)
pd_2q = core.phase_damping_error(lambda_2q)

Building a Calibrated Noise Model

The following function demonstrates how to assemble a full noise model from a set of hardware calibration parameters:

python
from pyqpanda3 import core
import math

def build_calibrated_noise_model(
    t1_values,       # Dict[int, float]: qubit -> T1 in us
    t2_values,       # Dict[int, float]: qubit -> T2 in us
    gate_times,      # Dict[str, float]: gate name -> time in ns
    rb_fidelities,   # Dict[str, float]: gate name -> average gate fidelity
    readout_matrix,  # List[List[float]]: 2x2 assignment matrix
):
    """Build a noise model from hardware calibration data.

    All parameters come from standard characterization experiments:
    - T1/T2 from relaxation/echo experiments
    - Gate times from hardware specifications
    - RB fidelities from randomized benchmarking
    - Readout matrix from measurement confusion matrices
    """
    noise = core.NoiseModel()

    # Convert RB fidelity to depolarizing probability.
    # For a d-dimensional system, the relation is:
    #   F_avg = 1 - p * (d - 1) / d
    # For a single qubit (d=2): p = 2 * (1 - F_avg)
    for gate_name, fidelity in rb_fidelities.items():
        p_dep = 2.0 * (1.0 - fidelity)
        if gate_name == "H":
            noise.add_all_qubit_quantum_error(
                core.depolarizing_error(p_dep), core.GateType.H
            )
        elif gate_name == "CNOT":
            noise.add_all_qubit_quantum_error(
                core.depolarizing_error(p_dep), core.GateType.CNOT
            )

    # Add amplitude damping per qubit based on T1 and gate time
    for qubit, t1 in t1_values.items():
        t2 = t2_values.get(qubit, t1)
        for gate_name, gt_ns in gate_times.items():
            gate_time_us = gt_ns / 1000.0
            gamma = 1.0 - math.exp(-gate_time_us / t1)
            gate_type = core.GateType.H if gate_name == "H" else core.GateType.CNOT
            qubits = [qubit] if gate_name == "H" else [0, 1]
            noise.add_quantum_error(
                core.amplitude_damping_error(gamma), gate_type, qubits
            )
            # Add phase damping based on T2
            lambda_param = 1.0 - math.exp(-gate_time_us / t2)
            noise.add_quantum_error(
                core.phase_damping_error(lambda_param), gate_type, qubits
            )

    # Add readout errors from the assignment matrix
    noise.add_all_qubit_read_out_error(readout_matrix)

    return noise

# Example calibration data for a 2-qubit device
calibrated_noise = build_calibrated_noise_model(
    t1_values={0: 80.0, 1: 65.0},
    t2_values={0: 60.0, 1: 50.0},
    gate_times={"H": 20.0, "CNOT": 300.0},
    rb_fidelities={"H": 0.9995, "CNOT": 0.985},
    readout_matrix=[[0.97, 0.03], [0.04, 0.96]],
)

# Run a test circuit with the calibrated model
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)
prog << core.measure([0, 1], [0, 1])

machine = core.CPUQVM()
machine.run(prog, shots=10000, model=calibrated_noise)
print(f"Calibrated noise result: {machine.result().get_counts()}")

Noise Simulation Workflow


Noise Mitigation Strategies

Even without full quantum error correction, several practical techniques can reduce the impact of noise on algorithm results. This section covers strategies you can implement at the circuit design and post-processing levels.

Strategy Overview

1. Minimize Circuit Depth

The single most effective strategy is to reduce the number of gates. Every gate is an opportunity for noise to enter. Use transpilation and optimization passes to reduce depth before running.

python
from pyqpanda3 import core

# Inefficient: naive decomposition with many CNOT gates
prog_naive = core.QProg()
prog_naive << core.H(0)
prog_naive << core.CNOT(0, 1)
prog_naive << core.CNOT(1, 2)
prog_naive << core.CNOT(2, 3)
prog_naive << core.CNOT(3, 4)
prog_naive << core.measure([0, 1, 2, 3, 4], [0, 1, 2, 3, 4])

# Efficient: equivalent computation with fewer CNOT gates
# by reordering or exploiting circuit identities
prog_efficient = core.QProg()
prog_efficient << core.H(0)
prog_efficient << core.CNOT(0, 1)
prog_efficient << core.CNOT(0, 2)
prog_efficient << core.measure([0, 1, 2], [0, 1, 2])

# The efficient circuit has fewer two-qubit gates,
# resulting in lower accumulated noise

2. Use the Native Gate Set

When targeting real hardware, compile your circuit to the native gate set of the device. Using non-native gates requires decomposition into multiple native gates, amplifying noise.

python
from pyqpanda3 import core

# Some devices natively support CZ instead of CNOT.
# Using the native gate avoids decomposition overhead.
prog = core.QProg()
prog << core.H(0) << core.H(1)
prog << core.CZ(0, 1)  # Use native CZ instead of decomposed CNOT
prog << core.measure([0, 1], [0, 1])

# Build a noise model that reflects the native gate's actual error rate
noise = core.NoiseModel()
noise.add_all_qubit_quantum_error(
    core.depolarizing_error(0.015), core.GateType.CZ
)
# No noise needed for CNOT since it is not used

3. Readout Error Mitigation

Measurement errors can be characterized and corrected using a calibration matrix. The idea is to prepare each computational basis state, measure it many times, and build an assignment matrix M:

ptrue=M1pmeasured
python
from pyqpanda3 import core
import numpy as np

def calibrate_readout(num_qubits, shots=10000):
    """Build a readout calibration matrix for the given qubits.

    For each basis state, prepare it and measure to find
    the probability of observing each outcome.
    """
    dim = 2 ** num_qubits
    cal_matrix = np.zeros((dim, dim))

    for basis_state in range(dim):
        # Prepare the basis state by applying X gates where bits are 1
        prog = core.QProg()
        for q in range(num_qubits):
            if (basis_state >> q) & 1:
                prog << core.X(q)
        prog << core.measure(list(range(num_qubits)), list(range(num_qubits)))

        machine = core.CPUQVM()
        machine.run(prog, shots=shots)
        counts = machine.result().get_counts()

        # Fill the column of the calibration matrix
        for outcome in range(dim):
            key = format(outcome, f"0{num_qubits}b")
            cal_matrix[outcome][basis_state] = counts.get(key, 0) / shots

    return cal_matrix

# Calibrate a 2-qubit readout
cal_matrix = calibrate_readout(num_qubits=2, shots=10000)
print(f"Calibration matrix:\n{cal_matrix}")

4. Zero-Noise Extrapolation

Run the same circuit at multiple artificially amplified noise levels, then extrapolate back to zero noise. This is one of the most practical error mitigation techniques for near-term devices.

The key idea: if the noise scales as F(λ)=F0eαλ where λ is the noise scale factor, you can estimate F0 (the noiseless result) from measurements at λ=1,2,3,

python
from pyqpanda3 import core
import numpy as np

def run_at_noise_scale(base_error_rate, scale_factor, shots=5000):
    """Run the benchmark circuit at an amplified noise level."""
    # Amplify the error rate by the scale factor
    amplified_rate = min(base_error_rate * scale_factor, 0.99)

    noise = core.NoiseModel()
    noise.add_all_qubit_quantum_error(
        core.depolarizing_error(amplified_rate), core.GateType.CNOT
    )

    prog = core.QProg()
    prog << core.H(0) << core.CNOT(0, 1)
    prog << core.measure([0, 1], [0, 1])

    machine = core.CPUQVM()
    machine.run(prog, shots=shots, model=noise)
    counts = machine.result().get_counts()

    # Return the Bell-state fidelity
    bell = (counts.get("00", 0) + counts.get("11", 0)) / shots
    return bell

# Measure at three noise levels and extrapolate linearly
base_rate = 0.02
scales = [1.0, 2.0, 3.0]
fidelities = [run_at_noise_scale(base_rate, s) for s in scales]

# Linear extrapolation to zero noise
# Fit: f(x) = a*x + b, estimate f(0) = b
coeffs = np.polyfit(scales, fidelities, deg=1)
f_zero_noise = coeffs[1]

print(f"Noise scale factors: {scales}")
print(f"Measured fidelities:  {fidelities}")
print(f"Extrapolated zero-noise fidelity: {f_zero_noise:.4f}")
# The extrapolated value should be closer to 1.0 than any individual measurement

Mitigation Strategy Selection Guide

StrategyOverheadEffectivenessWhen to Use
Minimize depthNone (compile-time)ModerateAlways apply first
Native gate setNone (compile-time)ModerateTargeting specific hardware
Readout mitigationO(2n) calibration runsHigh for measurement-heavy circuitsWhen readout error dominates
Zero-noise extrapolation3× circuit runsHigh for moderate-depth circuitsGeneral-purpose mitigation
Probabilistic error cancellationExponential in circuit depthHigh but costlySmall circuits (< 20 qubits)

Best Practices

1. Match Noise to Hardware

Use error rates that match your target device. Typical values for superconducting devices:

Error TypeTypical Rate
Single-qubit gate103 to 104
Two-qubit gate102 to 103
Readout102 to 103
T1 time20-100 μs

2. Use DensityMatrixSimulator for Small Circuits

For circuits with fewer than ~10 qubits, the DensityMatrixSimulator provides exact noise evolution:

python
from pyqpanda3 import core

# Exact noise simulation for small circuits
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)

noise_model = core.NoiseModel()
noise_model.add_all_qubit_quantum_error(
    core.depolarizing_error(0.01), core.GateType.CNOT
)

dm_sim = core.DensityMatrixSimulator()
dm_sim.run(prog, noise_model)
exact_probs = dm_sim.state_probs()
print(f"Exact probabilities: {exact_probs}")

3. Use Sufficient Shots

The statistical error decreases as 1/shots:

  • 1000 shots: ~3% error
  • 10000 shots: ~1% error
  • 100000 shots: ~0.3% error

4. Validate with Ideal Simulation

Always compare noisy results against ideal simulation to understand the noise impact:

python
from pyqpanda3 import core

# Build a Bell state circuit
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)
prog << core.measure([0, 1], [0, 1])

# Create noise model
noise = core.NoiseModel()
noise.add_all_qubit_quantum_error(
    core.depolarizing_error(0.01), core.GateType.CNOT
)

machine = core.CPUQVM()

# Ideal simulation
machine.run(prog, shots=10000)
ideal_counts = machine.result().get_counts()

# Noisy simulation
machine.run(prog, shots=10000, model=noise)
noisy_counts = machine.result().get_counts()

# Compare
print(f"Ideal: {ideal_counts}")
print(f"Noisy: {noisy_counts}")

Next Steps

Released under the MIT License.