|
| 1 | +# ============================================================================ # |
| 2 | +# Copyright (c) 2025 NVIDIA Corporation & Affiliates. # |
| 3 | +# All rights reserved. # |
| 4 | +# # |
| 5 | +# This source code and the accompanying materials are made available under # |
| 6 | +# the terms of the Apache License 2.0 which accompanies this distribution. # |
| 7 | +# ============================================================================ # |
| 8 | +# [Begin Documentation] |
| 9 | + |
| 10 | +import numpy as np |
| 11 | +from scipy.sparse import csr_matrix |
| 12 | +import cudaq_qec as qec |
| 13 | +import json |
| 14 | +import time |
| 15 | + |
| 16 | +# For fetching data |
| 17 | +import requests |
| 18 | +import bz2 |
| 19 | +import os |
| 20 | + |
| 21 | +# Note: running this script will automatically download data if necessary. |
| 22 | + |
| 23 | +### Helper functions ### |
| 24 | + |
| 25 | + |
| 26 | +def parse_csr_mat(j, dims, mat_name): |
| 27 | + """ |
| 28 | + Parse a CSR-style matrix from a JSON file using SciPy's sparse matrix utilities. |
| 29 | + """ |
| 30 | + assert len(dims) == 2, "dims must be a tuple of two integers" |
| 31 | + |
| 32 | + # Extract indptr and indices from the JSON. |
| 33 | + indptr = np.array(j[f"{mat_name}_indptr"], dtype=int) |
| 34 | + indices = np.array(j[f"{mat_name}_indices"], dtype=int) |
| 35 | + |
| 36 | + # Check that the CSR structure is consistent. |
| 37 | + assert len(indptr) == dims[0] + 1, "indptr length must equal dims[0] + 1" |
| 38 | + assert np.all( |
| 39 | + indices < dims[1]), "All column indices must be less than dims[1]" |
| 40 | + |
| 41 | + # Create a data array of ones. |
| 42 | + data = np.ones(indptr[-1], dtype=np.uint8) |
| 43 | + |
| 44 | + # Build the CSR matrix and return it as a dense numpy array. |
| 45 | + csr = csr_matrix((data, indices, indptr), shape=dims, dtype=np.uint8) |
| 46 | + return csr.toarray() |
| 47 | + |
| 48 | + |
| 49 | +def parse_H_csr(j, dims): |
| 50 | + """ |
| 51 | + Parse a CSR-style parity check matrix from an input file in JSON format" |
| 52 | + """ |
| 53 | + return parse_csr_mat(j, dims, "H") |
| 54 | + |
| 55 | + |
| 56 | +def parse_obs_csr(j, dims): |
| 57 | + """ |
| 58 | + Parse a CSR-style observable matrix from an input file in JSON format" |
| 59 | + """ |
| 60 | + return parse_csr_mat(j, dims, "obs_mat") |
| 61 | + |
| 62 | + |
| 63 | +### Main decoder loop ### |
| 64 | + |
| 65 | + |
| 66 | +def run_decoder(filename, num_shots, run_as_batched): |
| 67 | + """ |
| 68 | + Load a JSON file and decode "num_shots" syndromes. |
| 69 | + """ |
| 70 | + t_load_begin = time.time() |
| 71 | + with open(filename, "r") as f: |
| 72 | + j = json.load(f) |
| 73 | + |
| 74 | + dims = j["shape"] |
| 75 | + assert len(dims) == 2 |
| 76 | + |
| 77 | + # Read the Parity Check Matrix |
| 78 | + H = parse_H_csr(j, dims) |
| 79 | + syndrome_length, block_length = dims |
| 80 | + t_load_end = time.time() |
| 81 | + |
| 82 | + print(f"{filename} parsed in {1e3 * (t_load_end-t_load_begin)} ms") |
| 83 | + |
| 84 | + error_rate_vec = np.array(j["error_rate_vec"]) |
| 85 | + assert len(error_rate_vec) == block_length |
| 86 | + obs_mat_dims = j["obs_mat_shape"] |
| 87 | + obs_mat = parse_obs_csr(j, obs_mat_dims) |
| 88 | + assert dims[1] == obs_mat_dims[0] |
| 89 | + file_num_trials = j["num_trials"] |
| 90 | + num_shots = min(num_shots, file_num_trials) |
| 91 | + print( |
| 92 | + f'Your JSON file has {file_num_trials} shots. Running {num_shots} now.') |
| 93 | + |
| 94 | + # osd_method: 0=Off, 1=OSD-0, 2=Exhaustive, 3=Combination Sweep |
| 95 | + osd_method = 1 |
| 96 | + |
| 97 | + # When osd_method is: |
| 98 | + # 2) there are 2^osd_order additional error mechanisms checked. |
| 99 | + # 3) there are an additional k + osd_order*(osd_order-1)/2 error |
| 100 | + # mechanisms checked. |
| 101 | + # Ref: https://arxiv.org/pdf/2005.07016 |
| 102 | + osd_order = 0 |
| 103 | + |
| 104 | + # Maximum number of BP iterations before attempting OSD (if necessary) |
| 105 | + max_iter = 50 |
| 106 | + |
| 107 | + nv_dec_args = { |
| 108 | + "max_iterations": max_iter, |
| 109 | + "error_rate_vec": error_rate_vec, |
| 110 | + "use_sparsity": True, |
| 111 | + "use_osd": osd_method > 0, |
| 112 | + "osd_order": osd_order, |
| 113 | + "osd_method": osd_method |
| 114 | + } |
| 115 | + |
| 116 | + if run_as_batched: |
| 117 | + # Perform BP processing for up to 1000 syndromes per batch. If there |
| 118 | + # are more than 1000 syndromes, the decoder will chunk them up and |
| 119 | + # process each batch sequentially under the hood. |
| 120 | + nv_dec_args['bp_batch_size'] = min(1000, num_shots) |
| 121 | + |
| 122 | + try: |
| 123 | + nv_dec_gpu_and_cpu = qec.get_decoder("nv-qldpc-decoder", H, |
| 124 | + **nv_dec_args) |
| 125 | + except Exception as e: |
| 126 | + print( |
| 127 | + 'The nv-qldpc-decoder is not available with your current CUDA-Q ' + |
| 128 | + 'QEC installation.') |
| 129 | + exit(0) |
| 130 | + decoding_time = 0 |
| 131 | + bp_converged_flags = [] |
| 132 | + num_logical_errors = 0 |
| 133 | + |
| 134 | + # Batched API |
| 135 | + if run_as_batched: |
| 136 | + syndrome_list = [] |
| 137 | + obs_truth_list = [] |
| 138 | + for i in range(num_shots): |
| 139 | + syndrome = j["trials"][i]["syndrome_truth"] |
| 140 | + obs_truth = j["trials"][i]["obs_truth"] |
| 141 | + syndrome_list.append(syndrome) |
| 142 | + obs_truth_list.append(obs_truth) |
| 143 | + t0 = time.time() |
| 144 | + results = nv_dec_gpu_and_cpu.decode_batch(syndrome_list) |
| 145 | + t1 = time.time() |
| 146 | + decoding_time += t1 - t0 |
| 147 | + for r, obs_truth in zip(results, obs_truth_list): |
| 148 | + bp_converged_flags.append(r.converged) |
| 149 | + dec_result = np.array(r.result, dtype=np.uint8) |
| 150 | + |
| 151 | + # See if this prediction flipped the observable |
| 152 | + predicted_observable = obs_mat.T @ dec_result % 2 |
| 153 | + print(f"predicted_observable: {predicted_observable}") |
| 154 | + |
| 155 | + # See if the observable was actually flipped according to the truth |
| 156 | + # data |
| 157 | + actual_observable = np.array(obs_truth, dtype=np.uint8) |
| 158 | + print(f"actual_observable: {actual_observable}") |
| 159 | + |
| 160 | + if np.sum(predicted_observable != actual_observable) > 0: |
| 161 | + num_logical_errors += 1 |
| 162 | + |
| 163 | + # Non-batched API |
| 164 | + else: |
| 165 | + for i in range(num_shots): |
| 166 | + syndrome = j["trials"][i]["syndrome_truth"] |
| 167 | + obs_truth = j["trials"][i]["obs_truth"] |
| 168 | + |
| 169 | + t0 = time.time() |
| 170 | + bp_converged, dec_result = nv_dec_gpu_and_cpu.decode(syndrome) |
| 171 | + t1 = time.time() |
| 172 | + trial_diff = t1 - t0 |
| 173 | + decoding_time += trial_diff |
| 174 | + |
| 175 | + dec_result = np.array(dec_result, dtype=np.uint8) |
| 176 | + bp_converged_flags.append(bp_converged) |
| 177 | + |
| 178 | + # See if this prediction flipped the observable |
| 179 | + predicted_observable = obs_mat.T @ dec_result % 2 |
| 180 | + print(f"predicted_observable: {predicted_observable}") |
| 181 | + |
| 182 | + # See if the observable was actually flipped according to the truth |
| 183 | + # data |
| 184 | + actual_observable = np.array(obs_truth, dtype=np.uint8) |
| 185 | + print(f"actual_observable: {actual_observable}") |
| 186 | + |
| 187 | + if np.sum(predicted_observable != actual_observable) > 0: |
| 188 | + num_logical_errors += 1 |
| 189 | + |
| 190 | + # Count how many shots the decoder failed to correct the errors |
| 191 | + print(f"{num_logical_errors} logical errors in {num_shots} shots") |
| 192 | + print( |
| 193 | + f"Number of shots that converged with BP processing: {np.sum(np.array(bp_converged_flags))}" |
| 194 | + ) |
| 195 | + print( |
| 196 | + f"Average decoding time for {num_shots} shots was {1e3 * decoding_time / num_shots} ms per shot" |
| 197 | + ) |
| 198 | + |
| 199 | + |
| 200 | +if __name__ == "__main__": |
| 201 | + # See other test data options in https://github.com/NVIDIA/cudaqx/releases/tag/0.2.0 |
| 202 | + filename = 'osd_1008_8785_0.001.json' |
| 203 | + bz2filename = filename + '.bz2' |
| 204 | + if not os.path.exists(filename): |
| 205 | + url = f"https://github.com/NVIDIA/cudaqx/releases/download/0.2.0/{bz2filename}" |
| 206 | + |
| 207 | + print(f'Downloading data from {url}') |
| 208 | + |
| 209 | + # Download the file |
| 210 | + response = requests.get(url, stream=True) |
| 211 | + response.raise_for_status() # Raise an error if download fails |
| 212 | + with open(bz2filename, "wb") as f: |
| 213 | + for chunk in response.iter_content(chunk_size=8192): |
| 214 | + f.write(chunk) |
| 215 | + |
| 216 | + print(f'Decompressing {bz2filename} into {filename}') |
| 217 | + |
| 218 | + # Decompress the file |
| 219 | + with bz2.BZ2File(bz2filename, "rb") as f_in, open(filename, |
| 220 | + "wb") as f_out: |
| 221 | + f_out.write(f_in.read()) |
| 222 | + |
| 223 | + print(f"Decompressed file saved as {filename}") |
| 224 | + |
| 225 | + num_shots = 100 |
| 226 | + run_as_batched = True |
| 227 | + run_decoder(filename, num_shots, run_as_batched) |
0 commit comments