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_cpu_simulator.py

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