|
5 | 5 | "id": "8b4edafb", |
6 | 6 | "metadata": {}, |
7 | 7 | "source": [ |
8 | | - "# Azure Quantum Manual Test Plan: Q# and Qiskit Job Submission\n", |
| 8 | + "# Azure Quantum Manual Test Plan: Q#, Qiskit, and Cirq Job Submission\n", |
9 | 9 | "\n", |
10 | 10 | "This is a manual test plan notebook for verifying end-to-end job submission and result correctness against Azure Quantum.\n", |
11 | 11 | "\n", |
12 | 12 | "## What's Tested\n", |
13 | 13 | "- **Q# job submission** to three simulators via `target.submit()`\n", |
14 | 14 | "- **Qiskit job submission** to three simulators via `AzureQuantumProvider`\n", |
| 15 | + "- **Cirq job submission** to three simulators via `AzureQuantumService` (including the generic Cirq→QIR submission path for targets without a dedicated Cirq target wrapper)\n", |
15 | 16 | "\n", |
16 | 17 | "## Simulators Under Test\n", |
17 | 18 | "\n", |
|
50 | 51 | "\n", |
51 | 52 | "3. **Install the local `azure-quantum` package** (run from the root of the `azure-quantum-python` repo):\n", |
52 | 53 | " ```\n", |
53 | | - " pip install \".\\azure-quantum\\.[qsharp,qiskit]\"\n", |
| 54 | + " pip install \".\\azure-quantum[qsharp,qiskit,cirq]\"\n", |
54 | 55 | " ```\n", |
55 | 56 | "\n", |
56 | 57 | "4. **Set the kernel for this notebook** to `{env_name}` using the kernel picker in the top-right corner of the notebook editor." |
|
63 | 64 | "source": [ |
64 | 65 | "## Workspace Configuration\n", |
65 | 66 | "\n", |
66 | | - "Set the `resource_id` below to point to your Azure Quantum workspace. This is the only cell you need to change when switching workspaces — all test sections below share this connection." |
| 67 | + "Set `resource_id` below to point to your Azure Quantum workspace. **This is the only cell you need to edit** — all test sections share this connection." |
67 | 68 | ] |
68 | 69 | }, |
69 | 70 | { |
|
72 | 73 | "id": "ff8f9336", |
73 | 74 | "metadata": {}, |
74 | 75 | "outputs": [], |
| 76 | + "source": [ |
| 77 | + "resource_id = \"\"" |
| 78 | + ] |
| 79 | + }, |
| 80 | + { |
| 81 | + "cell_type": "markdown", |
| 82 | + "id": "0e0017de", |
| 83 | + "metadata": {}, |
| 84 | + "source": [ |
| 85 | + "This repo's `.env` file injects service principal credentials that are blocked by Conditional Access for interactive use. Clear them so `DefaultAzureCredential` falls through to interactive login (VS Code / Azure CLI)." |
| 86 | + ] |
| 87 | + }, |
| 88 | + { |
| 89 | + "cell_type": "code", |
| 90 | + "execution_count": null, |
| 91 | + "id": "b2341f06", |
| 92 | + "metadata": {}, |
| 93 | + "outputs": [], |
| 94 | + "source": [ |
| 95 | + "import os\n", |
| 96 | + "\n", |
| 97 | + "for _var in (\"AZURE_CLIENT_ID\", \"AZURE_CLIENT_SECRET\",\n", |
| 98 | + " \"AZURE_CLIENT_CERTIFICATE_PATH\", \"AZURE_CLIENT_SEND_CERTIFICATE_CHAIN\"):\n", |
| 99 | + " os.environ.pop(_var, None)" |
| 100 | + ] |
| 101 | + }, |
| 102 | + { |
| 103 | + "cell_type": "markdown", |
| 104 | + "id": "ecf95959", |
| 105 | + "metadata": {}, |
| 106 | + "source": [ |
| 107 | + "Connect to the Azure Quantum workspace using the `resource_id` set above." |
| 108 | + ] |
| 109 | + }, |
| 110 | + { |
| 111 | + "cell_type": "code", |
| 112 | + "execution_count": null, |
| 113 | + "id": "e597e2be", |
| 114 | + "metadata": {}, |
| 115 | + "outputs": [], |
75 | 116 | "source": [ |
76 | 117 | "from azure.quantum import Workspace\n", |
77 | 118 | "\n", |
78 | | - "workspace = Workspace(\n", |
79 | | - " resource_id=\"\"\n", |
80 | | - ")" |
| 119 | + "workspace = Workspace(resource_id=resource_id)" |
81 | 120 | ] |
82 | 121 | }, |
83 | 122 | { |
|
224 | 263 | " futures = {executor.submit(job.get_results): target_name for target_name, job in jobs.items()}\n", |
225 | 264 | " for future in concurrent.futures.as_completed(futures):\n", |
226 | 265 | " target_name = futures[future]\n", |
227 | | - " results = future.result()\n", |
228 | | - " print(f\" [{target_name}] Results: {results}\")\n", |
229 | | - " validate_qsharp_ghz(target_name, results)" |
| 266 | + " try:\n", |
| 267 | + " results = future.result()\n", |
| 268 | + " print(f\" [{target_name}] Results: {results}\")\n", |
| 269 | + " validate_qsharp_ghz(target_name, results)\n", |
| 270 | + " except Exception as exc:\n", |
| 271 | + " print(f\" [{target_name}] FAIL: {exc}\")" |
230 | 272 | ] |
231 | 273 | }, |
232 | 274 | { |
|
371 | 413 | " futures = {executor.submit(job.result): target_name for target_name, job in jobs.items()}\n", |
372 | 414 | " for future in concurrent.futures.as_completed(futures):\n", |
373 | 415 | " target_name = futures[future]\n", |
374 | | - " counts = future.result().get_counts()\n", |
375 | | - " print(f\" [{target_name}] Counts: {counts}\")\n", |
376 | | - " validate_qiskit_ghz(target_name, counts)" |
| 416 | + " try:\n", |
| 417 | + " counts = future.result().get_counts()\n", |
| 418 | + " print(f\" [{target_name}] Counts: {counts}\")\n", |
| 419 | + " validate_qiskit_ghz(target_name, counts)\n", |
| 420 | + " except Exception as exc:\n", |
| 421 | + " print(f\" [{target_name}] FAIL: {exc}\")" |
| 422 | + ] |
| 423 | + }, |
| 424 | + { |
| 425 | + "cell_type": "markdown", |
| 426 | + "id": "c3c09949", |
| 427 | + "metadata": {}, |
| 428 | + "source": [ |
| 429 | + "---\n", |
| 430 | + "\n", |
| 431 | + "## Cirq Job Tests\n", |
| 432 | + "\n", |
| 433 | + "Submit a Cirq circuit to each simulator target via `AzureQuantumService`. Results come back as a `cirq.Result` with per-shot measurement arrays, e.g. `{'m': [[0,0,0], [1,1,1], ...]}`." |
| 434 | + ] |
| 435 | + }, |
| 436 | + { |
| 437 | + "cell_type": "markdown", |
| 438 | + "id": "16f840a7", |
| 439 | + "metadata": {}, |
| 440 | + "source": [ |
| 441 | + "Build the 3-qubit GHZ circuit in Cirq with measurement key `m`, which will be submitted to each target below. Also define `validate_cirq_ghz`, which asserts that both `000` and `111` outcomes are present and each accounts for roughly half of all shots." |
| 442 | + ] |
| 443 | + }, |
| 444 | + { |
| 445 | + "cell_type": "code", |
| 446 | + "execution_count": null, |
| 447 | + "id": "9fc5e031", |
| 448 | + "metadata": {}, |
| 449 | + "outputs": [], |
| 450 | + "source": [ |
| 451 | + "import cirq\n", |
| 452 | + "from collections import Counter\n", |
| 453 | + "\n", |
| 454 | + "cirq_repetitions = 200\n", |
| 455 | + "q0, q1, q2 = cirq.LineQubit.range(3)\n", |
| 456 | + "\n", |
| 457 | + "cirq_circuit = cirq.Circuit(\n", |
| 458 | + " cirq.H(q0),\n", |
| 459 | + " cirq.CNOT(q0, q1),\n", |
| 460 | + " cirq.CNOT(q1, q2),\n", |
| 461 | + " cirq.measure(q0, q1, q2, key=\"m\"),\n", |
| 462 | + ")\n", |
| 463 | + "\n", |
| 464 | + "print(cirq_circuit)\n", |
| 465 | + "\n", |
| 466 | + "\n", |
| 467 | + "def validate_cirq_ghz(target_name, result, repetitions, tolerance=0.35):\n", |
| 468 | + " \"\"\"\n", |
| 469 | + " Validate results from a Cirq GHZ job submitted via AzureQuantumService.\n", |
| 470 | + " Expects measurement key 'm' with ~50% '000' and ~50% '111' shots.\n", |
| 471 | + " \"\"\"\n", |
| 472 | + " meas = result.measurements.get(\"m\")\n", |
| 473 | + " assert meas is not None, (\n", |
| 474 | + " f\"[{target_name}] Missing measurement key 'm'. \"\n", |
| 475 | + " f\"Available keys: {sorted(result.measurements.keys())}\"\n", |
| 476 | + " )\n", |
| 477 | + " counts = Counter(\"\".join(str(int(b)) for b in row) for row in meas)\n", |
| 478 | + " total = sum(counts.values())\n", |
| 479 | + " assert total == repetitions, (\n", |
| 480 | + " f\"[{target_name}] Expected {repetitions} shots, got {total}. Counts: {counts}\"\n", |
| 481 | + " )\n", |
| 482 | + " unexpected = {k: v for k, v in counts.items() if k not in (\"000\", \"111\")}\n", |
| 483 | + " if unexpected:\n", |
| 484 | + " print(f\" [{target_name}] WARN: unexpected outcomes: {unexpected}\")\n", |
| 485 | + " count_000 = counts.get(\"000\", 0)\n", |
| 486 | + " count_111 = counts.get(\"111\", 0)\n", |
| 487 | + " assert count_000 > 0, f\"[{target_name}] Expected '000' in results, got: {counts}\"\n", |
| 488 | + " assert count_111 > 0, f\"[{target_name}] Expected '111' in results, got: {counts}\"\n", |
| 489 | + " ratio_000 = count_000 / total\n", |
| 490 | + " ratio_111 = count_111 / total\n", |
| 491 | + " assert abs(ratio_000 - 0.5) < tolerance, (\n", |
| 492 | + " f\"[{target_name}] '000' ratio {ratio_000:.2f} too far from 0.5 (tolerance {tolerance})\"\n", |
| 493 | + " )\n", |
| 494 | + " assert abs(ratio_111 - 0.5) < tolerance, (\n", |
| 495 | + " f\"[{target_name}] '111' ratio {ratio_111:.2f} too far from 0.5 (tolerance {tolerance})\"\n", |
| 496 | + " )\n", |
| 497 | + " print(\n", |
| 498 | + " f\" [{target_name}] PASS: '000'={count_000} ({ratio_000:.1%}), \"\n", |
| 499 | + " f\"'111'={count_111} ({ratio_111:.1%}), total={total}\"\n", |
| 500 | + " )" |
| 501 | + ] |
| 502 | + }, |
| 503 | + { |
| 504 | + "cell_type": "markdown", |
| 505 | + "id": "04535b5b", |
| 506 | + "metadata": {}, |
| 507 | + "source": [ |
| 508 | + "Connect to the workspace via `AzureQuantumService` and list the available Cirq target wrappers to confirm the expected targets are accessible." |
| 509 | + ] |
| 510 | + }, |
| 511 | + { |
| 512 | + "cell_type": "code", |
| 513 | + "execution_count": null, |
| 514 | + "id": "86d9ac22", |
| 515 | + "metadata": {}, |
| 516 | + "outputs": [], |
| 517 | + "source": [ |
| 518 | + "from azure.quantum.cirq import AzureQuantumService\n", |
| 519 | + "\n", |
| 520 | + "cirq_service = AzureQuantumService(workspace)\n", |
| 521 | + "\n", |
| 522 | + "print(\"Cirq target wrappers available in workspace:\")\n", |
| 523 | + "for t in cirq_service.targets():\n", |
| 524 | + " print(f\" - {t.name}: {type(t).__name__}\")" |
| 525 | + ] |
| 526 | + }, |
| 527 | + { |
| 528 | + "cell_type": "markdown", |
| 529 | + "id": "9a5e44a5", |
| 530 | + "metadata": {}, |
| 531 | + "source": [ |
| 532 | + "Submit the GHZ circuit to all three targets in a fast serial loop (submissions are non-blocking), then wait for all responses concurrently using a thread pool. Results are validated as they arrive." |
| 533 | + ] |
| 534 | + }, |
| 535 | + { |
| 536 | + "cell_type": "code", |
| 537 | + "execution_count": null, |
| 538 | + "id": "05d88c03", |
| 539 | + "metadata": {}, |
| 540 | + "outputs": [], |
| 541 | + "source": [ |
| 542 | + "import concurrent.futures\n", |
| 543 | + "\n", |
| 544 | + "cirq_targets = [\n", |
| 545 | + " \"ionq.simulator\",\n", |
| 546 | + " \"quantinuum.sim.h2-1e\",\n", |
| 547 | + " \"rigetti.sim.qvm\",\n", |
| 548 | + "]\n", |
| 549 | + "\n", |
| 550 | + "# Submit all jobs first (non-blocking)\n", |
| 551 | + "print(\"Submitting Cirq GHZ jobs...\")\n", |
| 552 | + "cirq_jobs = {}\n", |
| 553 | + "for target_name in cirq_targets:\n", |
| 554 | + " cirq_jobs[target_name] = cirq_service.create_job(\n", |
| 555 | + " program=cirq_circuit,\n", |
| 556 | + " repetitions=cirq_repetitions,\n", |
| 557 | + " target=target_name,\n", |
| 558 | + " name=f\"ghz-cirq-{target_name}\",\n", |
| 559 | + " )\n", |
| 560 | + " print(f\" Submitted to {target_name} (id: {cirq_jobs[target_name].job_id()})\")\n", |
| 561 | + "\n", |
| 562 | + "# Wait for all results in parallel\n", |
| 563 | + "print(\"\\nWaiting for results...\")\n", |
| 564 | + "with concurrent.futures.ThreadPoolExecutor() as executor:\n", |
| 565 | + " futures = {executor.submit(job.results): target_name for target_name, job in cirq_jobs.items()}\n", |
| 566 | + " for future in concurrent.futures.as_completed(futures):\n", |
| 567 | + " target_name = futures[future]\n", |
| 568 | + " try:\n", |
| 569 | + " result = future.result()\n", |
| 570 | + " # The IonQ provider wrapper (cirq_ionq.Job) returns a SimulatorResult or\n", |
| 571 | + " # QPUResult rather than a cirq.Result. Normalize by calling to_cirq_result().\n", |
| 572 | + " if hasattr(result, \"to_cirq_result\"):\n", |
| 573 | + " result = result.to_cirq_result()\n", |
| 574 | + " validate_cirq_ghz(target_name, result, repetitions=cirq_repetitions)\n", |
| 575 | + " except Exception as exc:\n", |
| 576 | + " print(f\" [{target_name}] FAIL: {exc}\")" |
377 | 577 | ] |
378 | 578 | } |
379 | 579 | ], |
|
0 commit comments