Skip to content

Commit 6698781

Browse files
Cirq Manual Testing (#732)
* Add Cirq to the manual testing file * Add environment clearing cell to setup * fix some issues with quantinuum.py output translation
1 parent 071a068 commit 6698781

2 files changed

Lines changed: 220 additions & 18 deletions

File tree

azure-quantum/azure/quantum/cirq/targets/quantinuum.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ def _translate_cirq_circuit(circuit) -> str:
5050
@staticmethod
5151
def _to_cirq_result(result: Dict[str, Any], param_resolver, **kwargs):
5252
from cirq import ResultDict
53+
5354
measurements = {
54-
key.lstrip("m_"): np.array([[int(_v)] for _v in value])
55+
key.removeprefix("m_"): np.array([[int(bit) for bit in _v] for _v in value])
5556
for key, value in result.items()
5657
if key.startswith("m_")
5758
}
@@ -73,12 +74,13 @@ def _to_cirq_job(self, azure_job: "AzureJob", program: "cirq.Circuit" = None):
7374
def _measurement_dict(program) -> Dict[str, Sequence[int]]:
7475
"""Returns a dictionary of measurement keys to target qubit index."""
7576
from cirq import MeasurementGate
77+
7678
measurements = [
77-
op for op in program.all_operations() if isinstance(op.gate, MeasurementGate)
79+
op
80+
for op in program.all_operations()
81+
if isinstance(op.gate, MeasurementGate)
7882
]
79-
return {
80-
meas.gate.key: [q.x for q in meas.qubits] for meas in measurements
81-
}
83+
return {meas.gate.key: [q.x for q in meas.qubits] for meas in measurements}
8284

8385
def submit(
8486
self,
@@ -103,7 +105,7 @@ def submit(
103105
metadata = {
104106
"qubits": len(program.all_qubits()),
105107
"repetitions": repetitions,
106-
"measurement_dict": json.dumps(self._measurement_dict(program))
108+
"measurement_dict": json.dumps(self._measurement_dict(program)),
107109
}
108110
# Override metadata with value from kwargs
109111
metadata.update(kwargs.get("metadata", {}))

azure_quantum_manual_tests.ipynb

Lines changed: 212 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
"id": "8b4edafb",
66
"metadata": {},
77
"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",
99
"\n",
1010
"This is a manual test plan notebook for verifying end-to-end job submission and result correctness against Azure Quantum.\n",
1111
"\n",
1212
"## What's Tested\n",
1313
"- **Q# job submission** to three simulators via `target.submit()`\n",
1414
"- **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",
1516
"\n",
1617
"## Simulators Under Test\n",
1718
"\n",
@@ -50,7 +51,7 @@
5051
"\n",
5152
"3. **Install the local `azure-quantum` package** (run from the root of the `azure-quantum-python` repo):\n",
5253
" ```\n",
53-
" pip install \".\\azure-quantum\\.[qsharp,qiskit]\"\n",
54+
" pip install \".\\azure-quantum[qsharp,qiskit,cirq]\"\n",
5455
" ```\n",
5556
"\n",
5657
"4. **Set the kernel for this notebook** to `{env_name}` using the kernel picker in the top-right corner of the notebook editor."
@@ -63,7 +64,7 @@
6364
"source": [
6465
"## Workspace Configuration\n",
6566
"\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."
6768
]
6869
},
6970
{
@@ -72,12 +73,50 @@
7273
"id": "ff8f9336",
7374
"metadata": {},
7475
"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": [],
75116
"source": [
76117
"from azure.quantum import Workspace\n",
77118
"\n",
78-
"workspace = Workspace(\n",
79-
" resource_id=\"\"\n",
80-
")"
119+
"workspace = Workspace(resource_id=resource_id)"
81120
]
82121
},
83122
{
@@ -224,9 +263,12 @@
224263
" futures = {executor.submit(job.get_results): target_name for target_name, job in jobs.items()}\n",
225264
" for future in concurrent.futures.as_completed(futures):\n",
226265
" 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}\")"
230272
]
231273
},
232274
{
@@ -371,9 +413,167 @@
371413
" futures = {executor.submit(job.result): target_name for target_name, job in jobs.items()}\n",
372414
" for future in concurrent.futures.as_completed(futures):\n",
373415
" 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}\")"
377577
]
378578
}
379579
],

0 commit comments

Comments
 (0)