microsoft/qdk
Publicmirrored from https://github.com/microsoft/qdkAvailable
source/pip/tests/test_adaptive_cpu_noise.py
412lines · modecode
| 1 | # Copyright (c) Microsoft Corporation. |
| 2 | # Licensed under the MIT License. |
| 3 | |
| 4 | """Noise tests for the adaptive CPU bytecode interpreter. |
| 5 | |
| 6 | Each test targets noise injection by supplying hand-written Adaptive Profile |
| 7 | QIR that exercises noise channels and encodes the expected result into a |
| 8 | measurement outcome. |
| 9 | |
| 10 | This is a CPU counterpart to ``test_adaptive_gpu_noise.py``. |
| 11 | """ |
| 12 | |
| 13 | from collections import Counter |
| 14 | from typing import Optional, List |
| 15 | import pytest |
| 16 | from qsharp._simulation import run_qir, NoiseConfig, Result |
| 17 | import qsharp.openqasm |
| 18 | from typing import Literal |
| 19 | |
| 20 | |
| 21 | # --------------------------------------------------------------------------- |
| 22 | # Helpers |
| 23 | # --------------------------------------------------------------------------- |
| 24 | |
| 25 | SHOTS = 100 |
| 26 | SIM_TYPES = ["cpu", "clifford"] |
| 27 | |
| 28 | |
| 29 | def map_result_list_to_str(results: List[Result]): |
| 30 | results_str = "" |
| 31 | for r in results: |
| 32 | match r: |
| 33 | case Result.Zero: |
| 34 | results_str += "0" |
| 35 | case Result.One: |
| 36 | results_str += "1" |
| 37 | case Result.Loss: |
| 38 | results_str += "L" |
| 39 | return results_str |
| 40 | |
| 41 | |
| 42 | def get_histogram( |
| 43 | qir_fragment: str, |
| 44 | *, |
| 45 | extra_decls: str = "", |
| 46 | num_qubits: int = 1, |
| 47 | num_results: int = 1, |
| 48 | noise: Optional[NoiseConfig] = None, |
| 49 | record: Optional[List[int]] = None, |
| 50 | shots=SHOTS, |
| 51 | sim_type: Literal["clifford", "cpu"] = "cpu", |
| 52 | ): |
| 53 | qir = format_qir( |
| 54 | qir_fragment, |
| 55 | extra_decls=extra_decls, |
| 56 | num_qubits=num_qubits, |
| 57 | num_results=num_results, |
| 58 | record=record, |
| 59 | ) |
| 60 | results = map( |
| 61 | map_result_list_to_str, run_qir(qir, shots, noise, seed=42, type=sim_type) |
| 62 | ) |
| 63 | return Counter(results) |
| 64 | |
| 65 | |
| 66 | def check_result( |
| 67 | qir_fragment: str, |
| 68 | expected: str, |
| 69 | *, |
| 70 | extra_decls: str = "", |
| 71 | num_qubits: int = 1, |
| 72 | num_results: int = 1, |
| 73 | noise: Optional[NoiseConfig] = None, |
| 74 | record: Optional[List[int]] = None, |
| 75 | sim_type: Literal["clifford", "cpu"] = "cpu", |
| 76 | ): |
| 77 | """Assert every shot produces *expected*.""" |
| 78 | counts = get_histogram( |
| 79 | qir_fragment, |
| 80 | extra_decls=extra_decls, |
| 81 | num_qubits=num_qubits, |
| 82 | num_results=num_results, |
| 83 | noise=noise, |
| 84 | record=record, |
| 85 | sim_type=sim_type, |
| 86 | ) |
| 87 | |
| 88 | assert counts == { |
| 89 | expected: SHOTS |
| 90 | }, f"Expected all {SHOTS} shots to be '{expected}', got {counts}" |
| 91 | |
| 92 | |
| 93 | _DECLS = """\ |
| 94 | declare void @__quantum__qis__x__body(%Qubit*) |
| 95 | declare void @__quantum__qis__h__body(%Qubit*) |
| 96 | declare void @__quantum__qis__mresetz__body(%Qubit*, %Result*) |
| 97 | declare void @__quantum__qis__mz__body(%Qubit*, %Result*) #1 |
| 98 | declare void @__quantum__qis__reset__body(%Qubit*) |
| 99 | declare void @__quantum__qis__cx__body(%Qubit*, %Qubit*) |
| 100 | declare void @__quantum__qis__z__body(%Qubit*) |
| 101 | declare void @__quantum__qis__s__body(%Qubit*) |
| 102 | declare void @__quantum__qis__t__body(%Qubit*) |
| 103 | declare void @__quantum__qis__cz__body(%Qubit*, %Qubit*) |
| 104 | declare void @__quantum__qis__rz__body(double, %Qubit*) |
| 105 | declare i1 @__quantum__qis__read_result__body(%Result*) |
| 106 | declare void @__quantum__rt__tuple_record_output(i64, i8*) |
| 107 | declare void @__quantum__rt__result_record_output(%Result*, i8*) |
| 108 | declare void @__quantum__rt__initialize(i8*) |
| 109 | """ |
| 110 | |
| 111 | |
| 112 | def format_qir( |
| 113 | body: str, |
| 114 | *, |
| 115 | extra_decls: str = "", |
| 116 | num_qubits: int = 1, |
| 117 | num_results: int = 1, |
| 118 | record=None, |
| 119 | ): |
| 120 | if record is None: |
| 121 | record = range(num_results) |
| 122 | output_recording = ( |
| 123 | f" call void @__quantum__rt__tuple_record_output(i64 {len(record)}, i8* null)" |
| 124 | ) |
| 125 | for result_id in record: |
| 126 | output_recording += f"\n call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 {result_id} to %Result*), i8* null)" |
| 127 | |
| 128 | return f"""\ |
| 129 | %Result = type opaque |
| 130 | %Qubit = type opaque |
| 131 | |
| 132 | define i64 @ENTRYPOINT__main() #0 {{ |
| 133 | {body} |
| 134 | {output_recording} |
| 135 | ret i64 0 |
| 136 | }} |
| 137 | |
| 138 | {_DECLS} |
| 139 | {extra_decls} |
| 140 | attributes #0 = {{ "entry_point" "qir_profiles"="adaptive_profile" "required_num_qubits"="{num_qubits}" "required_num_results"="{num_results}" }} |
| 141 | attributes #1 = {{ "irreversible" }} |
| 142 | """ |
| 143 | |
| 144 | |
| 145 | # The purpose of this test is to inject noise in an identity gate, and assert its behavior. |
| 146 | # Since QIS does not specify an identity gate, we use CNOT and inject noise in the target qubit. |
| 147 | I_QIR = """ |
| 148 | entry: |
| 149 | call void @__quantum__qis__cx__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Qubit* inttoptr (i64 0 to %Qubit*)) |
| 150 | call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) |
| 151 | """ |
| 152 | |
| 153 | H_I_H_QIR = """ |
| 154 | entry: |
| 155 | call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 0 to %Qubit*)) |
| 156 | call void @__quantum__qis__cx__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Qubit* inttoptr (i64 0 to %Qubit*)) |
| 157 | call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 0 to %Qubit*)) |
| 158 | call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) |
| 159 | """ |
| 160 | |
| 161 | |
| 162 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 163 | def test_no_noise_on_i_yields_0(sim_type): |
| 164 | check_result(I_QIR, "0", num_qubits=2, sim_type=sim_type) |
| 165 | |
| 166 | |
| 167 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 168 | def test_x_noise_on_i_yields_1(sim_type): |
| 169 | noise = NoiseConfig() |
| 170 | noise.cx.ix = 1.0 |
| 171 | check_result(I_QIR, "1", num_qubits=2, noise=noise, sim_type=sim_type) |
| 172 | |
| 173 | |
| 174 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 175 | def test_y_noise_on_i_yields_1(sim_type): |
| 176 | noise = NoiseConfig() |
| 177 | noise.cx.iy = 1.0 |
| 178 | check_result(I_QIR, "1", num_qubits=2, noise=noise, sim_type=sim_type) |
| 179 | |
| 180 | |
| 181 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 182 | def test_z_noise_on_i_yields_0(sim_type): |
| 183 | noise = NoiseConfig() |
| 184 | noise.cx.iz = 1.0 |
| 185 | check_result(I_QIR, "0", num_qubits=2, noise=noise, sim_type=sim_type) |
| 186 | |
| 187 | |
| 188 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 189 | def test_x_noise_on_h_i_h_yields_0(sim_type): |
| 190 | noise = NoiseConfig() |
| 191 | noise.cx.ix = 1.0 |
| 192 | check_result(H_I_H_QIR, "0", num_qubits=2, noise=noise, sim_type=sim_type) |
| 193 | |
| 194 | |
| 195 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 196 | def test_y_noise_on_h_i_h_yields_1(sim_type): |
| 197 | noise = NoiseConfig() |
| 198 | noise.cx.iy = 1.0 |
| 199 | check_result(H_I_H_QIR, "1", num_qubits=2, noise=noise, sim_type=sim_type) |
| 200 | |
| 201 | |
| 202 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 203 | def test_z_noise_on_h_i_h_yields_1(sim_type): |
| 204 | noise = NoiseConfig() |
| 205 | noise.cx.iz = 1.0 |
| 206 | check_result(H_I_H_QIR, "1", num_qubits=2, noise=noise, sim_type=sim_type) |
| 207 | |
| 208 | |
| 209 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 210 | def test_probabilistic_x_noise(sim_type): |
| 211 | noise = NoiseConfig() |
| 212 | noise.cx.ix = 0.5 |
| 213 | counts = get_histogram( |
| 214 | I_QIR, shots=1000, num_qubits=2, noise=noise, sim_type=sim_type |
| 215 | ) |
| 216 | |
| 217 | assert counts["0"] > 400, f"Expected ~500 '0' results, got {counts['0']}" |
| 218 | assert counts["1"] > 400, f"Expected ~500 '1' results, got {counts['1']}" |
| 219 | |
| 220 | |
| 221 | QASM_WITH_CORRELATED_NOISE = """ |
| 222 | OPENQASM 3.0; |
| 223 | include "stdgates.inc"; |
| 224 | |
| 225 | @qdk.qir.noise_intrinsic |
| 226 | gate test_noise_intrinsic q0, q1, q2 {} |
| 227 | |
| 228 | qubit[3] qs; |
| 229 | x qs[1]; |
| 230 | test_noise_intrinsic qs[0], qs[1], qs[2]; |
| 231 | bit[3] res = measure qs; |
| 232 | """ |
| 233 | |
| 234 | QIR_WITH_CORRELATED_NOISE = qsharp.openqasm.compile( |
| 235 | QASM_WITH_CORRELATED_NOISE, |
| 236 | output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, |
| 237 | target_profile=qsharp.TargetProfile.Adaptive_RIF, |
| 238 | ) |
| 239 | |
| 240 | |
| 241 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 242 | def test_noise_intrinsics_noiseless(sim_type): |
| 243 | output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=None, type=sim_type) |
| 244 | assert output == [[Result.Zero, Result.One, Result.Zero]] |
| 245 | |
| 246 | |
| 247 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 248 | def test_noise_intrinsics_noisy(sim_type): |
| 249 | noise = NoiseConfig() |
| 250 | table = noise.intrinsic("test_noise_intrinsic", 3) |
| 251 | table.yyy = 1.0 |
| 252 | output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=noise, type=sim_type) |
| 253 | assert output == [[Result.One, Result.Zero, Result.One]] |
| 254 | |
| 255 | |
| 256 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 257 | def test_noise_intrinsics_load_csv_dir(sim_type): |
| 258 | noise = NoiseConfig() |
| 259 | noise.load_csv_dir("./csv_dir_test") |
| 260 | output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=noise, type=sim_type) |
| 261 | assert output == [[Result.One, Result.Zero, Result.One]] |
| 262 | |
| 263 | |
| 264 | NOISE_INTRINSICS_WITH_REGISTERS_QIR = r""" |
| 265 | %Result = type opaque |
| 266 | %Qubit = type opaque |
| 267 | |
| 268 | @0 = internal constant [4 x i8] c"0_a\00" |
| 269 | @1 = internal constant [6 x i8] c"1_a0r\00" |
| 270 | @2 = internal constant [6 x i8] c"2_a1r\00" |
| 271 | @3 = internal constant [6 x i8] c"3_a2r\00" |
| 272 | |
| 273 | define i64 @ENTRYPOINT__main() #0 { |
| 274 | block_0: |
| 275 | %q1 = inttoptr i64 0 to %Qubit* |
| 276 | %q2 = inttoptr i64 1 to %Qubit* |
| 277 | %q3 = inttoptr i64 2 to %Qubit* |
| 278 | call void @__quantum__rt__initialize(i8* null) |
| 279 | call void @__quantum__qis__x__body(%Qubit* %q2) |
| 280 | call void @test_noise_intrinsic(%Qubit* %q1, %Qubit* %q2, %Qubit* %q3) |
| 281 | call void @__quantum__qis__m__body(%Qubit* %q1, %Result* inttoptr (i64 0 to %Result*)) |
| 282 | call void @__quantum__qis__m__body(%Qubit* %q2, %Result* inttoptr (i64 1 to %Result*)) |
| 283 | call void @__quantum__qis__m__body(%Qubit* %q3, %Result* inttoptr (i64 2 to %Result*)) |
| 284 | call void @__quantum__rt__array_record_output(i64 3, i8* getelementptr inbounds ([4 x i8], [4 x i8]* @0, i64 0, i64 0)) |
| 285 | call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* getelementptr inbounds ([6 x i8], [6 x i8]* @1, i64 0, i64 0)) |
| 286 | call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* getelementptr inbounds ([6 x i8], [6 x i8]* @2, i64 0, i64 0)) |
| 287 | call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 2 to %Result*), i8* getelementptr inbounds ([6 x i8], [6 x i8]* @3, i64 0, i64 0)) |
| 288 | ret i64 0 |
| 289 | } |
| 290 | |
| 291 | declare void @__quantum__rt__initialize(i8*) |
| 292 | declare void @__quantum__qis__x__body(%Qubit*) |
| 293 | declare void @test_noise_intrinsic(%Qubit*, %Qubit*, %Qubit*) #2 |
| 294 | declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1 |
| 295 | declare void @__quantum__rt__array_record_output(i64, i8*) |
| 296 | declare void @__quantum__rt__result_record_output(%Result*, i8*) |
| 297 | |
| 298 | attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="3" "required_num_results"="3" } |
| 299 | attributes #1 = { "irreversible" } |
| 300 | attributes #2 = { "qdk_noise" } |
| 301 | |
| 302 | !llvm.module.flags = !{!0, !1, !2, !3, !4, !5} |
| 303 | |
| 304 | !0 = !{i32 1, !"qir_major_version", i32 1} |
| 305 | !1 = !{i32 7, !"qir_minor_version", i32 0} |
| 306 | !2 = !{i32 1, !"dynamic_qubit_management", i1 false} |
| 307 | !3 = !{i32 1, !"dynamic_result_management", i1 false} |
| 308 | !4 = !{i32 5, !"int_computations", !{!"i64"}} |
| 309 | !5 = !{i32 5, !"float_computations", !{!"double"}} |
| 310 | """ |
| 311 | |
| 312 | |
| 313 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 314 | def test_noise_intrinsics_with_registers_noisy(sim_type): |
| 315 | noise = NoiseConfig() |
| 316 | table = noise.intrinsic("test_noise_intrinsic", 3) |
| 317 | table.yyy = 1.0 |
| 318 | output = run_qir( |
| 319 | NOISE_INTRINSICS_WITH_REGISTERS_QIR, shots=1, noise=noise, type=sim_type |
| 320 | ) |
| 321 | assert output == [[Result.One, Result.Zero, Result.One]] |
| 322 | |
| 323 | |
| 324 | # --- Tests for varied qubit counts (1, 2, 5) --- |
| 325 | |
| 326 | QASM_NOISE_1Q = """ |
| 327 | OPENQASM 3.0; |
| 328 | include "stdgates.inc"; |
| 329 | |
| 330 | @qdk.qir.noise_intrinsic |
| 331 | gate noise_1q q0 {} |
| 332 | |
| 333 | qubit q; |
| 334 | noise_1q q; |
| 335 | bit res = measure q; |
| 336 | """ |
| 337 | |
| 338 | QIR_NOISE_1Q = qsharp.openqasm.compile( |
| 339 | QASM_NOISE_1Q, |
| 340 | output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, |
| 341 | target_profile=qsharp.TargetProfile.Adaptive_RIF, |
| 342 | ) |
| 343 | |
| 344 | |
| 345 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 346 | def test_noise_intrinsic_1q_x_flip(sim_type): |
| 347 | noise = NoiseConfig() |
| 348 | table = noise.intrinsic("noise_1q", 1) |
| 349 | table.x = 1.0 |
| 350 | output = run_qir(QIR_NOISE_1Q, shots=1, noise=noise, type=sim_type) |
| 351 | assert output == [Result.One] |
| 352 | |
| 353 | |
| 354 | QASM_NOISE_2Q = """ |
| 355 | OPENQASM 3.0; |
| 356 | include "stdgates.inc"; |
| 357 | |
| 358 | @qdk.qir.noise_intrinsic |
| 359 | gate noise_2q q0, q1 {} |
| 360 | |
| 361 | qubit[2] qs; |
| 362 | x qs[0]; |
| 363 | noise_2q qs[0], qs[1]; |
| 364 | bit[2] res = measure qs; |
| 365 | """ |
| 366 | |
| 367 | QIR_NOISE_2Q = qsharp.openqasm.compile( |
| 368 | QASM_NOISE_2Q, |
| 369 | output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, |
| 370 | target_profile=qsharp.TargetProfile.Adaptive_RIF, |
| 371 | ) |
| 372 | |
| 373 | |
| 374 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 375 | def test_noise_intrinsic_2q_xx_flip(sim_type): |
| 376 | noise = NoiseConfig() |
| 377 | table = noise.intrinsic("noise_2q", 2) |
| 378 | table.xx = 1.0 |
| 379 | # qs[0] was |1>, qs[1] was |0> -> XX flips both -> qs[0]=|0>, qs[1]=|1> |
| 380 | output = run_qir(QIR_NOISE_2Q, shots=1, noise=noise, type=sim_type) |
| 381 | assert output == [[Result.Zero, Result.One]] |
| 382 | |
| 383 | |
| 384 | QASM_NOISE_5Q = """ |
| 385 | OPENQASM 3.0; |
| 386 | include "stdgates.inc"; |
| 387 | |
| 388 | @qdk.qir.noise_intrinsic |
| 389 | gate noise_5q q0, q1, q2, q3, q4 {} |
| 390 | |
| 391 | qubit[5] qs; |
| 392 | x qs[1]; |
| 393 | x qs[3]; |
| 394 | noise_5q qs[0], qs[1], qs[2], qs[3], qs[4]; |
| 395 | bit[5] res = measure qs; |
| 396 | """ |
| 397 | |
| 398 | QIR_NOISE_5Q = qsharp.openqasm.compile( |
| 399 | QASM_NOISE_5Q, |
| 400 | output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, |
| 401 | target_profile=qsharp.TargetProfile.Adaptive_RIF, |
| 402 | ) |
| 403 | |
| 404 | |
| 405 | @pytest.mark.parametrize("sim_type", SIM_TYPES) |
| 406 | def test_noise_intrinsic_5q_xxxxx_flip(sim_type): |
| 407 | noise = NoiseConfig() |
| 408 | table = noise.intrinsic("noise_5q", 5) |
| 409 | table.xxxxx = 1.0 |
| 410 | # Initial: |01010> -> XXXXX flips all -> |10101> |
| 411 | output = run_qir(QIR_NOISE_5Q, shots=1, noise=noise, type=sim_type) |
| 412 | assert output == [[Result.One, Result.Zero, Result.One, Result.Zero, Result.One]] |
| 413 | |