microsoft/qdk

Public

mirrored fromhttps://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
iadavis/pipeline-issue-debugging

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/pip/tests/test_gpu_simulator.py

515lines · modecode

1# Copyright (c) Microsoft Corporation.
2# Licensed under the MIT License.
3
4from collections import Counter
5from pathlib import Path
6from typing import Sequence, cast
7import math
8import os
9import random
10
11import pytest
12import sys
13
14from qsharp._native import Result
15
16# Skip all tests in this module if QDK_GPU_TESTS is not set
17if not os.environ.get("QDK_GPU_TESTS"):
18 pytest.skip("Skipping GPU tests (QDK_GPU_TESTS not set)", allow_module_level=True)
19
20SKIP_REASON = "GPU is not available"
21
22gpu_info = "Unknown"
23
24try:
25 from qsharp._native import try_create_gpu_adapter
26
27 gpu_info = try_create_gpu_adapter()
28 # Printing to stderr so that it is visible if CI run fails
29 print(f"*** USING GPU: {gpu_info}", file=sys.stderr)
30
31 GPU_AVAILABLE = True
32except OSError as e:
33 GPU_AVAILABLE = False
34 SKIP_REASON = str(e)
35
36
37import qsharp
38from qsharp import TargetProfile
39from qsharp import openqasm
40
41from qsharp._simulation import run_qir_gpu, NoiseConfig
42
43current_file_path = Path(__file__)
44# Get the directory of the current file
45current_dir = current_file_path.parent
46
47
48def read_file(file_name: str) -> str:
49 return Path(file_name).read_text(encoding="utf-8")
50
51
52def read_file_relative(file_name: str) -> str:
53 return Path(current_dir / file_name).read_text(encoding="utf-8")
54
55
56def result_array_to_string(results: Sequence[Result]) -> str:
57 chars = []
58 for value in results:
59 if value == Result.Zero:
60 chars.append("0")
61 elif value == Result.One:
62 chars.append("1")
63 else:
64 chars.append("-")
65 return "".join(chars)
66
67
68@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
69def test_gpu_seeding_no_noise():
70 qsharp.init(target_profile=TargetProfile.Base)
71 qsharp.eval(
72 """
73 operation BellTest() : Result[] {
74 use qs = Qubit[2];
75 H(qs[0]);
76 CNOT(qs[0], qs[1]);
77 MResetEachZ(qs)
78 }
79 """
80 )
81
82 qir = str(qsharp.compile("BellTest()"))
83
84 results = [run_qir_gpu(qir, 1, None, seed)[0] for seed in range(12)]
85 print(results)
86
87 # Results will be an array of 12 lists [Result, Result]
88 # Each result should be [Zero, Zero] or [One, One]
89 # As evident from a manual experiment running with the seeds of 0..11
90 # gives 6 results of each. Experiment should be repeatable for fixed seeds.
91
92 # Verify we have 6 of each result
93 count_00 = sum(1 for r in results if r == [Result.Zero, Result.Zero])
94 count_11 = sum(1 for r in results if r == [Result.One, Result.One])
95 assert count_00 == 6
96 assert count_11 == 6
97
98
99@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
100def test_gpu_no_noise():
101 """Simple test that GPU simulator works without noise."""
102 qsharp.init(target_profile=TargetProfile.Base)
103 qsharp.eval(read_file_relative("CliffordIsing.qs"))
104
105 input = qsharp.compile(
106 "IsingModel2DEvolution(5, 5, PI() / 2.0, PI() / 2.0, 10.0, 10)"
107 )
108
109 output = run_qir_gpu(str(input))
110 print(output)
111 # Expecting deterministic output, no randomization seed needed.
112 assert output == [[Result.Zero] * 25], "Expected result of 0s with pi/2 angles."
113
114
115@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
116def test_gpu_bitflip_noise():
117 """Bitflip noise for GPU simulator."""
118 qsharp.init(target_profile=TargetProfile.Base)
119 qsharp.eval(read_file_relative("CliffordIsing.qs"))
120
121 input = qsharp.compile(
122 "IsingModel2DEvolution(5, 5, PI() / 2.0, PI() / 2.0, 10.0, 10)"
123 )
124
125 p_noise = 0.005
126 noise = NoiseConfig()
127 noise.rx.set_bitflip(p_noise)
128 noise.rzz.set_pauli_noise("XX", p_noise)
129 noise.mresetz.set_bitflip(p_noise)
130
131 output = run_qir_gpu(str(input), shots=3, noise=noise, seed=17)
132 result = [result_array_to_string(cast(Sequence[Result], x)) for x in output]
133 print(result)
134 # Reasonable results obtained from manual run
135 assert result == [
136 "0000000000011100000000110",
137 "0001001100000000000100110",
138 "0000000000011000000000000",
139 ]
140
141
142@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
143def test_gpu_mixed_noise():
144 qsharp.init(target_profile=TargetProfile.Base)
145 qsharp.eval(read_file_relative("CliffordIsing.qs"))
146
147 input = qsharp.compile(
148 "IsingModel2DEvolution(5, 5, PI() / 2.0, PI() / 2.0, 4.0, 4)"
149 )
150
151 noise = NoiseConfig()
152 noise.rz.set_bitflip(0.005)
153 noise.rz.loss = 0.003
154 noise.rzz.set_depolarizing(0.005)
155 noise.rzz.loss = 0.003
156
157 output = run_qir_gpu(str(input), shots=3, noise=noise, seed=53)
158 result = [result_array_to_string(cast(Sequence[Result], x)) for x in output]
159 print(result)
160 # Reasonable results obtained from manual run
161 assert result == [
162 "00000-00000000-0000000000",
163 "00100001000-0000000000-00",
164 "000000010000000-000000000",
165 ]
166
167
168@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
169def test_gpu_isolated_loss():
170 qsharp.init(target_profile=TargetProfile.Base)
171 program = """
172import Std.Math.PI;
173operation Main() : Result[] {
174 use qs = Qubit[3];
175 X(qs[0]);
176 X(qs[1]);
177 CNOT(qs[0], qs[1]);
178 // When loss is configured for X gate, qubit 2 should be unaffected.
179 Rx(PI() / 2.0, qs[2]);
180 Rx(PI() / 2.0, qs[2]);
181 MeasureEachZ(qs)
182}
183 """
184 qsharp.eval(program)
185
186 input = qsharp.compile("Main()")
187
188 noise = NoiseConfig()
189 noise.x.loss = 0.1
190
191 output = run_qir_gpu(str(input), shots=1000, noise=noise)
192 result = [result_array_to_string(cast(Sequence[Result], x)) for x in output]
193 histogram = Counter(result)
194 total = sum(histogram.values())
195 allowed_percent = {
196 "101": 0.81,
197 "1-1": 0.09,
198 "-11": 0.09,
199 "--1": 0.01,
200 }
201 tolerance = 0.2 * total
202 for bitstring, actual_count in histogram.items():
203 assert (
204 bitstring in allowed_percent
205 ), f"Unexpected measurement string: '{bitstring}'."
206 expected_count = allowed_percent[bitstring] * total
207 assert abs(actual_count - expected_count) <= tolerance, (
208 f"Count for {bitstring} outside 20% tolerance. "
209 f"Actual={actual_count}, Expected≈{expected_count:.0f}, Shots={total}."
210 )
211 # We don't check for missing strings, as low-probability strings may not appear in finite shots.
212
213
214@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
215def test_gpu_isolated_loss_and_noise():
216 qsharp.init(target_profile=TargetProfile.Base)
217 program = """
218import Std.Math.PI;
219operation Main() : Result[] {
220 use qs = Qubit[5];
221 for _ in 1..100 {
222 X(qs[0]);
223 X(qs[1]);
224 CNOT(qs[0], qs[1]);
225 }
226 Rx(PI() / 2.0, qs[4]);
227 Rx(PI() / 2.0, qs[4]);
228 MeasureEachZ(qs)
229}
230 """
231 qsharp.eval(program)
232
233 input = qsharp.compile("Main()")
234
235 noise = NoiseConfig()
236 noise.x.set_bitflip(0.001)
237 noise.x.loss = 0.001
238
239 output = run_qir_gpu(str(input), shots=1000, noise=noise)
240 result = [result_array_to_string(cast(Sequence[Result], x)) for x in output]
241 histogram = Counter(result)
242 total = sum(histogram.values())
243 assert total > 0, "No measurement results recorded."
244 for bitstring in histogram:
245 assert bitstring.endswith("001"), f"Unexpected suffix in '{bitstring}'."
246 probability_00001 = histogram.get("00001", 0) / total
247 assert 0.5 < probability_00001 < 0.8, (
248 f"Probability of 00001 outside expected range. "
249 f"Actual={probability_00001:.2%}, Shots={total}."
250 )
251
252
253def build_x_chain_qir(n_instances: int, n_x: int) -> str:
254 # Construct multiple instances of x gate chains
255 prefix = f"""
256 OPENQASM 3.0;
257 include "stdgates.inc";
258 bit[{n_instances}] c;
259 qubit[{n_instances}] q;
260 """
261
262 infix = """
263 x q;
264 """
265
266 suffix = """
267 c = measure q;
268 """
269
270 src_parallel = prefix + infix * n_x + suffix
271
272 # Compile resulting program
273 qsharp.init(target_profile=TargetProfile.Base)
274 qir_parallel = openqasm.compile(src_parallel)
275 return str(qir_parallel)
276
277
278@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
279@pytest.mark.parametrize(
280 "p_noise, n_x, n_instances, n_shots, max_percent",
281 [
282 (0.0005, 500, 10, 8192, 1.0),
283 (0.005, 500, 10, 4096, 2.0),
284 (0.0005, 20, 20, 100, 4.0), # 100 shots is low, so higher error tolerated
285 ],
286)
287def test_gpu_x_chain(
288 p_noise: float, n_x: int, n_instances: int, n_shots: int, max_percent: float
289):
290 """
291 Simulate multi-instance X-chain with bitflip noise many times
292 Compare result frequencies with analytically computed probabilities
293 """
294 # Use the GPU simulator with noise
295 noise = NoiseConfig()
296 noise.x.set_bitflip(p_noise)
297
298 qir = build_x_chain_qir(n_instances, n_x)
299 output = run_qir_gpu(qir, shots=n_shots, noise=noise, seed=18)
300 histogram = [0 for _ in range(n_instances + 1)]
301 for shot in output:
302 shot_results = cast(Sequence[Result], shot)
303 count_1 = shot_results.count(Result.One)
304 histogram[count_1] += 1
305
306 # Probability of obtaining 0 and 1 at the end of the X chain.
307 p_0 = ((2.0 * p_noise - 1.0) ** n_x + 1.0) / 2.0
308 p_1 = 1.0 - p_0
309
310 # Number of results with k ones that should be there.
311 p_N = [
312 p_0 ** ((n_instances - k)) * (p_1**k) * math.comb(n_instances, k) * n_shots
313 for k in range(n_instances + 1)
314 ]
315
316 # Error % for deviation from analytical value
317 error_percent = [abs(a - b) * 100.0 / n_shots for (a, b) in zip(histogram, p_N)]
318 print(", ".join(f"{a} (Δ≈{b:.1f}%)" for (a, b) in zip(histogram, error_percent)))
319
320 # We tolerate configured percentage error.
321 assert all(
322 err < max_percent for err in error_percent
323 ), f"Error percent too high: {error_percent}"
324
325
326def build_cy_noise_qir(n_cy: int) -> str:
327 src = """
328 OPENQASM 3.0;
329 include "stdgates.inc";
330 bit[2] c;
331 qubit[2] q;
332 x q[0];
333 h q[1];
334 """
335 src += "cy q[0], q[1];\n" * n_cy
336 src += """
337 h q[1];
338 c = measure q;
339 """
340
341 qsharp.init(target_profile=TargetProfile.Base)
342 # OpenQasm output semantics preserves order of bits in the output register.
343 qir_program = openqasm.compile(
344 src, output_semantics=openqasm.OutputSemantics.OpenQasm
345 )
346 return str(qir_program)
347
348
349@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
350def test_gpu_cy_noise_distribution():
351 """
352 Apply CY with per-gate Z noise and validate the expected odd-parity flip rate.
353 """
354 n_cy = 10
355 p_z = 0.01
356 n_shots = 1000
357 expected_p1 = (1.0 - (1.0 - 2.0 * p_z) ** n_cy) / 2.0
358
359 noise = NoiseConfig()
360 noise.cy.set_pauli_noise("IZ", p_z)
361
362 qir = build_cy_noise_qir(n_cy)
363 output = run_qir_gpu(qir, shots=n_shots, noise=noise, seed=77)
364
365 count_target_one = 0
366 for shot in output:
367 shot_results = cast(Sequence[Result], shot)
368 if shot_results[1] == Result.One:
369 count_target_one += 1
370
371 actual_p1 = count_target_one / n_shots
372 tolerance = 0.05
373 print(
374 f"CY noise rate outside tolerance. Expected≈{expected_p1:.3f}, actual={actual_p1:.3f}, tol={tolerance:.3f}"
375 )
376 assert abs(actual_p1 - expected_p1) <= tolerance, "CY noise rate outside tolerance."
377
378
379def generate_op_sequence(
380 n_qubits: int, n_ops: int, n_rand: int
381) -> list[tuple[int, int]]:
382 """Return operation tuples and randomly swap neighboring pairs n_rand times."""
383 if n_qubits < 0 or n_ops < 0 or n_rand < 0:
384 raise ValueError("Tuple bounds must be non-negative")
385
386 ops = [(q, op) for op in range(n_ops) for q in range(n_qubits)]
387
388 if len(ops) < 2 or n_rand == 0:
389 return ops
390
391 max_index = len(ops) - 1
392 for _ in range(n_rand):
393 idx = random.randrange(max_index)
394 left, right = ops[idx], ops[idx + 1]
395 if left[0] != right[0]:
396 ops[idx], ops[idx + 1] = right, left
397
398 return ops
399
400
401@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
402@pytest.mark.parametrize("noisy_gate, noise_number", [(0, 2), (1, 1), (2, 2), (3, 2)])
403def test_gpu_permuted_rotations(noisy_gate: int, noise_number: int):
404 qsharp.init(target_profile=TargetProfile.Base)
405
406 n_shots = 2000
407 n_qubits = 15
408 seed = 2026
409 p_loss = 0.1
410 tolerance_percent = 2.0
411 assert n_qubits >= 2, "Need at least two qubits"
412
413 random.seed(seed)
414 i1, i2 = random.sample(range(n_qubits), 2)
415 prefix = f"""
416operation tiny_coeffs() : Result[] {{
417 use q = Qubit[{n_qubits}];
418 let i1 = {i1};
419 let i2 = {i2};
420"""
421
422 # The following sequence of rotations is equivalent to identity:
423 # 0. H <- could be any rotation
424 # 1. Rx(1.123456789)
425 # 2. Ry(1.212121212)
426 # 3. Rz(1.14856940153986)
427 # 4. Ry(-1.41836046203971)
428 # 5. Rz(-0.325946593598928)
429 # 6. H <- adjoint to step 0
430 # We will perform these rotations on every qubit, but randomly intermix sequences for different qubits.
431 # This should still result in identity on all qubits as gates on different qubits commute.
432 # noise_number = how many times noisy gate appears in sequence.
433
434 n_ops = 7
435 ops = generate_op_sequence(n_qubits, n_ops, n_qubits * n_ops * 100)
436 infix = ""
437 for qubit, op in ops:
438 match op:
439 case 0 | 6:
440 infix += f" H(q[{qubit}]);\n"
441 case 1:
442 infix += f" Rx(1.123456789, q[{qubit}]);\n"
443 case 2:
444 infix += f" Ry(1.212121212, q[{qubit}]);\n"
445 case 3:
446 infix += f" Rz(1.14856940153986, q[{qubit}]);\n"
447 case 4:
448 infix += f" Ry(-1.41836046203971, q[{qubit}]);\n"
449 case 5:
450 infix += f" Rz(-0.325946593598928, q[{qubit}]);\n"
451
452 suffix = """
453 let m1 = M(q[i1]);
454 let m2 = M(q[i2]);
455 ResetAll(q);
456 return [m1, m2];
457}
458"""
459
460 program = prefix + infix + suffix
461 qsharp.eval(program)
462 input = qsharp.compile("tiny_coeffs()")
463
464 noise = NoiseConfig()
465 p_combined_loss = 1.0 - ((1.0 - p_loss) ** noise_number)
466 match noisy_gate:
467 case 0:
468 noise.h.loss = p_loss
469 case 1:
470 noise.rx.loss = p_loss
471 case 2:
472 noise.ry.loss = p_loss
473 case 3:
474 noise.rz.loss = p_loss
475 case _:
476 raise ValueError("Invalid noisy_gate value")
477
478 output = run_qir_gpu(str(input), shots=n_shots, noise=noise, seed=seed)
479 result_strings = [
480 result_array_to_string(cast(Sequence[Result], shot)) for shot in output
481 ]
482 assert (
483 len(result_strings) == n_shots
484 ), f"Shot count mismatch. Actual={len(result_strings)}, Expected={n_shots}"
485
486 p_minus = p_combined_loss
487 p_0 = 1.0 - p_minus
488 allowed = [
489 ("00", n_shots * p_0 * p_0),
490 ("0-", n_shots * p_0 * p_minus),
491 ("-0", n_shots * p_minus * p_0),
492 ("--", n_shots * p_minus * p_minus),
493 ]
494
495 counts = {pattern: 0 for pattern, _ in allowed}
496 for entry in result_strings:
497 assert (
498 entry in counts
499 ), f"Unexpected measurement string: '{entry}'. Program={program}."
500 counts[entry] += 1
501
502 tolerance = tolerance_percent / 100.0 * n_shots
503 print(
504 f"Permuted rotations test: n_qubits={n_qubits}, n_shots={n_shots}, seed={seed}, noise#{noise_number}, Δ<={tolerance:.0f} i1={i1}, i2={i2}"
505 )
506 summary_msg = ", ".join(
507 f"'{pattern}': {counts[pattern]} (Δ={abs(counts[pattern] - expected_count):.0f})"
508 for pattern, expected_count in allowed
509 )
510 print(summary_msg)
511 for pattern, expected_count in allowed:
512 actual_count = counts[pattern]
513 assert (
514 abs(actual_count - expected_count) <= tolerance
515 ), f"Count for {pattern} off by more than {tolerance_percent:.1f}% of shots. Actual={actual_count}, Expected={expected_count:.0f}, noise#{noise_number}, Program={program}."
516