Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions azure-quantum/azure/quantum/cirq/targets/quantinuum.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ def _translate_cirq_circuit(circuit) -> str:
@staticmethod
def _to_cirq_result(result: Dict[str, Any], param_resolver, **kwargs):
from cirq import ResultDict

measurements = {
key.lstrip("m_"): np.array([[int(_v)] for _v in value])
key.removeprefix("m_"): np.array([[int(bit) for bit in _v] for _v in value])
for key, value in result.items()
if key.startswith("m_")
}
Expand All @@ -73,12 +74,13 @@ def _to_cirq_job(self, azure_job: "AzureJob", program: "cirq.Circuit" = None):
def _measurement_dict(program) -> Dict[str, Sequence[int]]:
"""Returns a dictionary of measurement keys to target qubit index."""
from cirq import MeasurementGate

measurements = [
op for op in program.all_operations() if isinstance(op.gate, MeasurementGate)
op
for op in program.all_operations()
if isinstance(op.gate, MeasurementGate)
]
return {
meas.gate.key: [q.x for q in meas.qubits] for meas in measurements
}
return {meas.gate.key: [q.x for q in meas.qubits] for meas in measurements}

def submit(
self,
Expand All @@ -103,7 +105,7 @@ def submit(
metadata = {
"qubits": len(program.all_qubits()),
"repetitions": repetitions,
"measurement_dict": json.dumps(self._measurement_dict(program))
"measurement_dict": json.dumps(self._measurement_dict(program)),
}
# Override metadata with value from kwargs
metadata.update(kwargs.get("metadata", {}))
Expand Down
224 changes: 212 additions & 12 deletions azure_quantum_manual_tests.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
"id": "8b4edafb",
"metadata": {},
"source": [
"# Azure Quantum Manual Test Plan: Q# and Qiskit Job Submission\n",
"# Azure Quantum Manual Test Plan: Q#, Qiskit, and Cirq Job Submission\n",
"\n",
"This is a manual test plan notebook for verifying end-to-end job submission and result correctness against Azure Quantum.\n",
"\n",
"## What's Tested\n",
"- **Q# job submission** to three simulators via `target.submit()`\n",
"- **Qiskit job submission** to three simulators via `AzureQuantumProvider`\n",
"- **Cirq job submission** to three simulators via `AzureQuantumService` (including the generic Cirq→QIR submission path for targets without a dedicated Cirq target wrapper)\n",
"\n",
"## Simulators Under Test\n",
"\n",
Expand Down Expand Up @@ -50,7 +51,7 @@
"\n",
"3. **Install the local `azure-quantum` package** (run from the root of the `azure-quantum-python` repo):\n",
" ```\n",
" pip install \".\\azure-quantum\\.[qsharp,qiskit]\"\n",
" pip install \".\\azure-quantum[qsharp,qiskit,cirq]\"\n",
" ```\n",
"\n",
"4. **Set the kernel for this notebook** to `{env_name}` using the kernel picker in the top-right corner of the notebook editor."
Expand All @@ -63,7 +64,7 @@
"source": [
"## Workspace Configuration\n",
"\n",
"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."
"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."
]
},
{
Expand All @@ -72,12 +73,50 @@
"id": "ff8f9336",
"metadata": {},
"outputs": [],
"source": [
"resource_id = \"\""
]
},
{
"cell_type": "markdown",
"id": "0e0017de",
"metadata": {},
"source": [
"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)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b2341f06",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"for _var in (\"AZURE_CLIENT_ID\", \"AZURE_CLIENT_SECRET\",\n",
" \"AZURE_CLIENT_CERTIFICATE_PATH\", \"AZURE_CLIENT_SEND_CERTIFICATE_CHAIN\"):\n",
" os.environ.pop(_var, None)"
]
},
{
"cell_type": "markdown",
"id": "ecf95959",
"metadata": {},
"source": [
"Connect to the Azure Quantum workspace using the `resource_id` set above."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e597e2be",
"metadata": {},
"outputs": [],
"source": [
"from azure.quantum import Workspace\n",
"\n",
"workspace = Workspace(\n",
" resource_id=\"\"\n",
")"
"workspace = Workspace(resource_id=resource_id)"
]
},
{
Expand Down Expand Up @@ -224,9 +263,12 @@
" futures = {executor.submit(job.get_results): target_name for target_name, job in jobs.items()}\n",
" for future in concurrent.futures.as_completed(futures):\n",
" target_name = futures[future]\n",
" results = future.result()\n",
" print(f\" [{target_name}] Results: {results}\")\n",
" validate_qsharp_ghz(target_name, results)"
" try:\n",
" results = future.result()\n",
" print(f\" [{target_name}] Results: {results}\")\n",
" validate_qsharp_ghz(target_name, results)\n",
" except Exception as exc:\n",
" print(f\" [{target_name}] FAIL: {exc}\")"
]
},
{
Expand Down Expand Up @@ -371,9 +413,167 @@
" futures = {executor.submit(job.result): target_name for target_name, job in jobs.items()}\n",
" for future in concurrent.futures.as_completed(futures):\n",
" target_name = futures[future]\n",
" counts = future.result().get_counts()\n",
" print(f\" [{target_name}] Counts: {counts}\")\n",
" validate_qiskit_ghz(target_name, counts)"
" try:\n",
" counts = future.result().get_counts()\n",
" print(f\" [{target_name}] Counts: {counts}\")\n",
" validate_qiskit_ghz(target_name, counts)\n",
" except Exception as exc:\n",
" print(f\" [{target_name}] FAIL: {exc}\")"
]
},
{
"cell_type": "markdown",
"id": "c3c09949",
"metadata": {},
"source": [
"---\n",
"\n",
"## Cirq Job Tests\n",
"\n",
"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], ...]}`."
]
},
{
"cell_type": "markdown",
"id": "16f840a7",
"metadata": {},
"source": [
"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."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9fc5e031",
"metadata": {},
"outputs": [],
"source": [
"import cirq\n",
"from collections import Counter\n",
"\n",
"cirq_repetitions = 200\n",
"q0, q1, q2 = cirq.LineQubit.range(3)\n",
"\n",
"cirq_circuit = cirq.Circuit(\n",
" cirq.H(q0),\n",
" cirq.CNOT(q0, q1),\n",
" cirq.CNOT(q1, q2),\n",
" cirq.measure(q0, q1, q2, key=\"m\"),\n",
")\n",
"\n",
"print(cirq_circuit)\n",
"\n",
"\n",
"def validate_cirq_ghz(target_name, result, repetitions, tolerance=0.35):\n",
" \"\"\"\n",
" Validate results from a Cirq GHZ job submitted via AzureQuantumService.\n",
" Expects measurement key 'm' with ~50% '000' and ~50% '111' shots.\n",
" \"\"\"\n",
" meas = result.measurements.get(\"m\")\n",
" assert meas is not None, (\n",
" f\"[{target_name}] Missing measurement key 'm'. \"\n",
" f\"Available keys: {sorted(result.measurements.keys())}\"\n",
" )\n",
" counts = Counter(\"\".join(str(int(b)) for b in row) for row in meas)\n",
" total = sum(counts.values())\n",
" assert total == repetitions, (\n",
" f\"[{target_name}] Expected {repetitions} shots, got {total}. Counts: {counts}\"\n",
" )\n",
" unexpected = {k: v for k, v in counts.items() if k not in (\"000\", \"111\")}\n",
" if unexpected:\n",
" print(f\" [{target_name}] WARN: unexpected outcomes: {unexpected}\")\n",
" count_000 = counts.get(\"000\", 0)\n",
" count_111 = counts.get(\"111\", 0)\n",
" assert count_000 > 0, f\"[{target_name}] Expected '000' in results, got: {counts}\"\n",
" assert count_111 > 0, f\"[{target_name}] Expected '111' in results, got: {counts}\"\n",
" ratio_000 = count_000 / total\n",
" ratio_111 = count_111 / total\n",
" assert abs(ratio_000 - 0.5) < tolerance, (\n",
" f\"[{target_name}] '000' ratio {ratio_000:.2f} too far from 0.5 (tolerance {tolerance})\"\n",
" )\n",
" assert abs(ratio_111 - 0.5) < tolerance, (\n",
" f\"[{target_name}] '111' ratio {ratio_111:.2f} too far from 0.5 (tolerance {tolerance})\"\n",
" )\n",
" print(\n",
" f\" [{target_name}] PASS: '000'={count_000} ({ratio_000:.1%}), \"\n",
" f\"'111'={count_111} ({ratio_111:.1%}), total={total}\"\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "04535b5b",
"metadata": {},
"source": [
"Connect to the workspace via `AzureQuantumService` and list the available Cirq target wrappers to confirm the expected targets are accessible."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "86d9ac22",
"metadata": {},
"outputs": [],
"source": [
"from azure.quantum.cirq import AzureQuantumService\n",
"\n",
"cirq_service = AzureQuantumService(workspace)\n",
"\n",
"print(\"Cirq target wrappers available in workspace:\")\n",
"for t in cirq_service.targets():\n",
" print(f\" - {t.name}: {type(t).__name__}\")"
]
},
{
"cell_type": "markdown",
"id": "9a5e44a5",
"metadata": {},
"source": [
"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."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "05d88c03",
"metadata": {},
"outputs": [],
"source": [
"import concurrent.futures\n",
"\n",
"cirq_targets = [\n",
" \"ionq.simulator\",\n",
" \"quantinuum.sim.h2-1e\",\n",
" \"rigetti.sim.qvm\",\n",
"]\n",
"\n",
"# Submit all jobs first (non-blocking)\n",
"print(\"Submitting Cirq GHZ jobs...\")\n",
"cirq_jobs = {}\n",
"for target_name in cirq_targets:\n",
" cirq_jobs[target_name] = cirq_service.create_job(\n",
" program=cirq_circuit,\n",
" repetitions=cirq_repetitions,\n",
" target=target_name,\n",
" name=f\"ghz-cirq-{target_name}\",\n",
" )\n",
" print(f\" Submitted to {target_name} (id: {cirq_jobs[target_name].job_id()})\")\n",
"\n",
"# Wait for all results in parallel\n",
"print(\"\\nWaiting for results...\")\n",
"with concurrent.futures.ThreadPoolExecutor() as executor:\n",
" futures = {executor.submit(job.results): target_name for target_name, job in cirq_jobs.items()}\n",
" for future in concurrent.futures.as_completed(futures):\n",
" target_name = futures[future]\n",
" try:\n",
" result = future.result()\n",
" # The IonQ provider wrapper (cirq_ionq.Job) returns a SimulatorResult or\n",
" # QPUResult rather than a cirq.Result. Normalize by calling to_cirq_result().\n",
" if hasattr(result, \"to_cirq_result\"):\n",
" result = result.to_cirq_result()\n",
" validate_cirq_ghz(target_name, result, repetitions=cirq_repetitions)\n",
" except Exception as exc:\n",
" print(f\" [{target_name}] FAIL: {exc}\")"
]
}
],
Expand Down
Loading