Skip to content

Commit e1ff321

Browse files
melody-renbmhowe23
andauthored
Add expressive noise model example (#123)
Adapted from Justin's example, using `apply_noise` instead of custom gates --------- Signed-off-by: Melody Ren <[email protected]> Signed-off-by: melody-ren <[email protected]> Co-authored-by: Ben Howe <[email protected]>
1 parent 2a6382c commit e1ff321

File tree

3 files changed

+222
-0
lines changed

3 files changed

+222
-0
lines changed

.github/workflows/pr_workflow.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ jobs:
1919
build-all: ${{ steps.filter.outputs.build-all }}
2020
build-qec: ${{ steps.filter.outputs.build-qec }}
2121
build-solvers: ${{ steps.filter.outputs.build-solvers }}
22+
build-examples-qec: ${{ steps.filter.outputs.build-examples-qec }}
23+
build-examples-solvers: ${{ steps.filter.outputs.build-examples-solvers }}
2224
steps:
2325
- name: Checkout repository
2426
uses: actions/checkout@v4
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import cudaq
2+
import cudaq_qec as qec
3+
import numpy as np
4+
5+
nRounds = 3
6+
nShots = 500
7+
# Physical error rate
8+
p_per_round = 0.01
9+
p_per_mz = 0.01
10+
11+
12+
# Construct the measurement error syndrome matrix based on the distance and number of rounds
13+
def construct_measurement_error_syndrome(distance, nRounds):
14+
num_stabilizers = distance - 1
15+
num_mea_q = num_stabilizers * nRounds
16+
17+
syndrome_rows = []
18+
19+
# In this scheme, need two rounds for each measurement syndrome
20+
for i in range(nRounds - 1):
21+
for j in range(num_stabilizers):
22+
syndrome = np.zeros((num_mea_q,), dtype=np.uint8)
23+
24+
# The error on ancilla (j) in round (i) affects stabilizer checks at two positions:
25+
# First occurrence in round i
26+
pos1 = i * num_stabilizers + j
27+
# Second occurrence in round i+1
28+
pos2 = (i + 1) * num_stabilizers + j
29+
30+
# Mark the syndrome
31+
syndrome[pos1] = 1
32+
syndrome[pos2] = 1
33+
34+
syndrome_rows.append(syndrome)
35+
36+
return np.array(syndrome_rows).T
37+
38+
39+
# Generate the parity check matrix for n-rounds by duplicating the input parity check matrix Hz
40+
# and appending the measurement error syndrome matrix.
41+
def get_circuit_level_pcm(distance, nRounds, Hz):
42+
if nRounds < 2:
43+
raise ValueError("nRounds must be greater than or equal to 2")
44+
if distance < 3:
45+
raise ValueError("distance must be greater than or equal to 3")
46+
47+
# Parity check matrix for a single round
48+
H = np.array(Hz)
49+
50+
# Extends H to nRounds
51+
rows, cols = H.shape
52+
H_nrounds = np.zeros((rows * nRounds, cols * nRounds), dtype=np.uint8)
53+
for i in range(nRounds):
54+
H_nrounds[i * rows:(i + 1) * rows, i * cols:(i + 1) * cols] = H
55+
print("H_nrounds\n", H_nrounds)
56+
57+
# Construct the measurement error syndrome matrix for Z errors
58+
H_mz = construct_measurement_error_syndrome(distance, nRounds)
59+
print("H_mz\n", H_mz)
60+
assert H_nrounds.shape[0] == H_mz.shape[
61+
0], "Dimensions of H_nrounds and H_mz do not match"
62+
63+
# Append columns for measurement errors to H
64+
H_pcm = np.concatenate((H_nrounds, H_mz), axis=1)
65+
print(f"H_pcm:\n{H_pcm}")
66+
67+
return H_pcm
68+
69+
70+
# Example of how to construct a repetition code with a distance of 3 and random
71+
# bit flip errors applied to the data qubits
72+
@cudaq.kernel
73+
def three_qubit_repetition_code():
74+
data_qubits = cudaq.qvector(3)
75+
ancilla_qubits = cudaq.qvector(2)
76+
77+
# Initialize the logical |1> state as |111>
78+
x(data_qubits)
79+
80+
for i in range(nRounds):
81+
# Random Bit Flip Errors
82+
for j in range(3):
83+
cudaq.apply_noise(cudaq.XError, p_per_round, data_qubits[j])
84+
85+
# Extract Syndromes
86+
h(ancilla_qubits)
87+
88+
# First Parity Check
89+
z.ctrl(ancilla_qubits[0], data_qubits[0])
90+
z.ctrl(ancilla_qubits[0], data_qubits[1])
91+
92+
# Second Parity Check
93+
z.ctrl(ancilla_qubits[1], data_qubits[1])
94+
z.ctrl(ancilla_qubits[1], data_qubits[2])
95+
96+
h(ancilla_qubits)
97+
98+
# Measure the ancilla qubits
99+
s0 = mz(ancilla_qubits[0])
100+
s1 = mz(ancilla_qubits[1])
101+
reset(ancilla_qubits[0])
102+
reset(ancilla_qubits[1])
103+
104+
# Final measurement to get the data qubits
105+
mz(data_qubits)
106+
107+
108+
# Create a noise model
109+
noise_model = cudaq.NoiseModel()
110+
# Add measurement noise
111+
noise_model.add_all_qubit_channel("mz", cudaq.BitFlipChannel(p_per_mz))
112+
113+
# Run the kernel and observe results
114+
# The percent of samples that are 000 corresponds to the logical error rate
115+
cudaq.set_target("stim")
116+
result = cudaq.sample(three_qubit_repetition_code,
117+
shots_count=nShots,
118+
noise_model=noise_model,
119+
explicit_measurements=True)
120+
121+
# The following section will demonstrate how to decode the results
122+
# Get the parity check matrix for n-rounds of the repetition code
123+
Hz = [[1, 1, 0], [0, 1, 1]] # Parity check matrix for 1 round
124+
H_pcm = get_circuit_level_pcm(3, nRounds, Hz)
125+
126+
# Get observables
127+
observables = np.array([1, 0, 0, 0, 0, 0], dtype=np.uint8)
128+
Lz = np.array([1, 0, 0], dtype=np.uint8)
129+
print(f"observables:\n{observables}")
130+
print(f"Lz:\n{Lz}")
131+
# Pad the observables to be the same dimension as the decoded observable
132+
Lz_nrounds = np.tile(Lz, nRounds)
133+
pad_size = max(0, H_pcm.shape[1] - Lz_nrounds.shape[0])
134+
Lz_nround_mz = np.pad(Lz_nrounds, (0, pad_size), mode='constant')
135+
print(f"Lz_nround_mz\n{Lz_nround_mz}")
136+
137+
# Get a decoder
138+
decoder = qec.get_decoder("single_error_lut", H_pcm)
139+
nLogicalErrors = 0
140+
141+
# initialize a Pauli frame to track logical flips
142+
# through the stabilizer rounds. Only need the Z component for the repetition code.
143+
pauli_frame = np.array([0, 0], dtype=np.uint8)
144+
expected_value = 1
145+
for shot, outcome in enumerate(result.get_sequential_data()):
146+
outcome_array = np.array([int(bit) for bit in outcome], dtype=np.uint8)
147+
syndrome = outcome_array[:len(outcome_array) - 3]
148+
data = outcome_array[len(outcome_array) - 3:]
149+
print("\nshot:", shot)
150+
print("syndrome:", syndrome)
151+
152+
# Decode the syndrome
153+
convergence, result = decoder.decode(syndrome)
154+
data_prediction = np.array(result, dtype=np.uint8)
155+
156+
# See if the decoded result anti-commutes with the observables
157+
print("decode result:", data_prediction)
158+
decoded_observables = (Lz_nround_mz @ data_prediction) % 2
159+
print("decoded_observables:", decoded_observables)
160+
161+
# update pauli frame
162+
pauli_frame[0] = (pauli_frame[0] + decoded_observables) % 2
163+
print("pauli frame:", pauli_frame)
164+
165+
logical_measurements = (Lz @ data.transpose()) % 2
166+
print("LMz:", logical_measurements)
167+
168+
corrected_mz = (logical_measurements + pauli_frame[0]) % 2
169+
print("Expected value:", expected_value)
170+
print("Corrected value:", corrected_mz)
171+
if (corrected_mz != expected_value):
172+
nLogicalErrors += 1
173+
174+
# Count how many shots the decoder failed to correct the errors
175+
print("\nNumber of logical errors:", nLogicalErrors)

docs/sphinx/examples_rst/qec/circuit_level_noise.rst

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,48 @@ Here's how to use CUDA-Q QEC to perform a circuit-level noise model experiment i
7575

7676
The CUDA-Q QEC library thus provides a platform for numerical QEC experiments. The `qec.code` can be used to analyze a variety of QEC codes (both library or user provided), with a variety of decoders (both library or user provided).
7777
The CUDA-Q QEC library also provides tools to speed up the automation of generating noisy data and syndromes.
78+
79+
Addtionally, here's how to use CUDA-Q QEC to construct a multi-round parity check matrix and a custom error correction code for the circuit-level noise model experiment in Python:
80+
81+
.. tab:: Python
82+
83+
.. literalinclude:: ../../examples/qec/python/repetition_code_fine_grain_noise.py
84+
:language: python
85+
86+
This example illustrates how to:
87+
88+
1. Construct a multi-round parity check matrix – Users can extend a single-round parity check matrix across multiple rounds,
89+
incorporating measurement errors to track syndrome evolution over time. This enables more accurate circuit-level noise modeling for decoders.
90+
91+
2. Define custom error correction circuits with precise noise injection – Using `cudaq.apply_noise`, users can introduce specific error channels
92+
at targeted locations within the QEC circuit. This fine-grained control allows for precise testing of how different noise sources affect logical error rates.
93+
94+
In the previous example, we demonstrated how to introduce random X errors into each data qubit using `cudaq.apply_noise` during each round of syndrome extraction.
95+
CUDA-Q allows users to inject a variety of error channels at different locations within their circuits, enabling fine-grained noise modeling. The example below showcases
96+
additional ways to introduce errors into a quantum kernel:
97+
98+
.. code-block:: python
99+
100+
@cudaq.kernel
101+
def inject_noise_example():
102+
q = cudaq.qvector(3)
103+
104+
# Apply depolarization noise to the first qubit
105+
cudaq.apply_noise(cudaq.DepolarizationChannel, 0.1, q[0])
106+
107+
# Perform gate operations
108+
h(q[1])
109+
x.ctrl(q[1], q[2])
110+
111+
# Inject a Y error into the second qubit
112+
cudaq.apply_noise(cudaq.YError, 0.1, q[1])
113+
114+
# Apply a general Pauli noise channel to the third qubit, where the 3 values indicate the probability of X, Y, and Z errors.
115+
cudaq.apply_noise(cudaq.Pauli1, 0.1, 0.1, 0.1, q[2])
116+
117+
# Define and apply a noise model
118+
noise = cudaq.NoiseModel()
119+
counts = cudaq.sample(inject_noise_example, noise_model=noise)
120+
121+
For a full list of supported noise models and their parameters, refer to the `CUDA-Q documentation <https://nvidia.github.io/cuda-quantum/latest/index.html>`_.
122+

0 commit comments

Comments
 (0)