Skip to content
Open
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
146 changes: 93 additions & 53 deletions samples/python_interop/cirq_submission_to_azure.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"source": [
"# Submitting Cirq Circuits to Azure Quantum with the QDK\n",
"\n",
"This notebook shows how to take a Cirq `Circuit`, export it to OpenQASM 3, compile that OpenQASM 3 source to QIR using the Quantum Development Kit (QDK) Python APIs, and submit it as a job to an Azure Quantum target."
"This notebook demonstrates how to run Cirq `Circuit` jobs on Azure Quantum using `AzureQuantumService` from the QDK."
]
},
{
Expand All @@ -17,12 +17,10 @@
"source": [
"The workflow demonstrated here:\n",
"\n",
"1. Build (or load) a Cirq `Circuit`.\n",
"2. Convert it to OpenQASM 3 text via `circuit.to_qasm(version=\"3.0\")`.\n",
"3. Compile the OpenQASM 3 source to QIR with `qdk.openqasm.compile`.\n",
"4. Connect to (or create) an Azure Quantum workspace using `qdk.azure.Workspace`.\n",
"5. Pick a target (e.g., a simulator like `rigetti.sim.qvm`).\n",
"6. Submit the QIR payload and retrieve measurement results."
"1. Build a Cirq `Circuit` with named measurement keys.\n",
"2. Reference an existing Azure Quantum workspace with `AzureQuantumService`.\n",
"3. Browse available targets via `service.targets()`.\n",
"4. Call `service.create_job(program=circuit, repetitions=..., target=...)` and fetch results (`job.results()`)."
]
},
{
Expand All @@ -32,7 +30,7 @@
"source": [
"## Prerequisites\n",
"\n",
"Ensure the `qdk` package is installed with `azure` and `cirq` extras. If not, install dependencies below."
"This notebook assumes the `qdk` package with Azure Quantum and Cirq support is installed. You can install everything with:"
]
},
{
Expand All @@ -50,7 +48,12 @@
"id": "20b9ed32",
"metadata": {},
"source": [
"After installing, restart the kernel if necessary. Verify imports:"
"This installs:\n",
"- The base `qdk` package (compiler, OpenQASM/QIR tooling)\n",
"- Azure Quantum client dependencies for submission\n",
"- Cirq for circuit construction\n",
"\n",
"After installing, restart the notebook kernel if it was already running. You can verify installation with:"
]
},
{
Expand All @@ -68,8 +71,9 @@
"id": "e2b111db",
"metadata": {},
"source": [
"## Submitting a simple Cirq circuit\n",
"We'll build a small circuit creating a superposition on one qubit and flipping another, then measuring both. Afterwards we submit it to an Azure Quantum target."
"## Build a simple Cirq circuit\n",
"\n",
"We start with a Bell-state circuit with two named measurement keys — one per qubit. Named keys are required so the result dictionary has clearly labeled registers."
]
},
{
Expand All @@ -79,15 +83,16 @@
"metadata": {},
"outputs": [],
"source": [
"# Build a simple circuit\n",
"import cirq\n",
"\n",
"q0, q1 = cirq.LineQubit.range(2)\n",
"simple_circuit = cirq.Circuit(\n",
"circuit = cirq.Circuit(\n",
" cirq.H(q0),\n",
" cirq.measure(q0, key='m0'),\n",
" cirq.X(q1),\n",
" cirq.measure(q1, key='m1'),\n",
" cirq.CNOT(q0, q1),\n",
" cirq.measure(q0, key=\"q0\"),\n",
" cirq.measure(q1, key=\"q1\"),\n",
")\n",
"print(simple_circuit)"
"print(circuit)"
]
},
{
Expand All @@ -96,7 +101,8 @@
"metadata": {},
"source": [
"## Configure Azure Quantum workspace connection\n",
"Replace the placeholder values below with your own subscription, resource group, workspace name, and location."
"\n",
"To connect to an Azure workspace replace the following variables with your own values."
]
},
{
Expand All @@ -113,42 +119,42 @@
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9f336702",
"cell_type": "markdown",
"id": "184b6e57",
"metadata": {},
"outputs": [],
"source": [
"from qdk.openqasm import compile\n",
"from qdk.azure import Workspace\n",
"from qdk import TargetProfile\n",
"\n",
"def submit_cirq_circuit_to_azure(circuit: cirq.Circuit, target_name: str, name: str, shots: int = 100):\n",
" # 1. Export to OpenQASM 3\n",
" qasm3_str = circuit.to_qasm(version='3.0')\n",
" # 2. Compile to QIR with a base target profile\n",
" qir = compile(qasm3_str, target_profile=TargetProfile.Base)\n",
" # 3. Connect workspace\n",
" workspace = Workspace(\n",
" subscription_id=subscription_id,\n",
" resource_group=resource_group,\n",
" name=workspace_name,\n",
" location=location,\n",
" )\n",
" # 4. Select target (string e.g. 'rigetti.sim.qvm' or other available target)\n",
" target = workspace.get_targets(target_name)\n",
" # 5. Submit QIR payload\n",
" job = target.submit(qir, name, shots=shots)\n",
" return job.get_results()"
"## Submit the circuit to Azure Quantum\n",
"\n",
"`AzureQuantumService` exposes Azure Quantum targets as Cirq-compatible target objects. When you call `service.targets()`, the SDK returns one of two kinds of target:\n",
"\n",
"- **Provider-specific targets** — Some hardware vendors (e.g. IonQ, Quantinuum) ship dedicated Cirq target classes with native integration for their APIs, handling gate translation and result parsing using hardware-specific logic.\n",
"- **Generic QIR targets** — For any other target that accepts QIR input, the SDK automatically wraps it as an `AzureGenericQirCirqTarget`. These compile the Cirq circuit to OpenQASM 3 and then to QIR internally, so you don't have to manage those steps manually.\n",
"\n",
"In practice `service.targets()` selects the right type for each target — you use the same `service.create_job()` call regardless of which target type is returned."
]
},
{
"cell_type": "markdown",
"id": "a0436202",
"cell_type": "code",
"execution_count": null,
"id": "9f336702",
"metadata": {},
"outputs": [],
"source": [
"### Submit the simple circuit\n",
"(Make sure you changed the workspace credentials above.)"
"from qdk.azure import Workspace\n",
"from azure.quantum.cirq import AzureQuantumService\n",
"\n",
"workspace = Workspace(\n",
" subscription_id=subscription_id,\n",
" resource_group=resource_group,\n",
" name=workspace_name,\n",
" location=location,\n",
")\n",
"\n",
"service = AzureQuantumService(workspace)\n",
"\n",
"# List available targets\n",
"for target in service.targets():\n",
" print(f\"{target.name:45s} {type(target).__name__}\")"
]
},
{
Expand All @@ -158,19 +164,53 @@
"metadata": {},
"outputs": [],
"source": [
"# Uncomment after setting workspace credentials\n",
"# results = submit_cirq_circuit_to_azure(simple_circuit, 'rigetti.sim.qvm', 'cirq-simple-job')\n",
"# print(results)"
"from collections import Counter\n",
"\n",
"# Replace with any target name from the list above\n",
"target_name = \"rigetti.sim.qvm\"\n",
"\n",
"job = service.create_job(\n",
" program=circuit,\n",
" repetitions=100,\n",
" name=\"cirq-bell-job\",\n",
" target=target_name,\n",
")\n",
"print(f\"Job {job.job_id()} submitted — waiting for results...\")\n",
"\n",
"result = job.results()\n",
"\n",
"# Combine separate measurement keys into joint bitstrings\n",
"keys = sorted(result.measurements.keys())\n",
"joint = Counter(\n",
" \"\".join(str(int(result.measurements[k][i][0])) for k in keys)\n",
" for i in range(len(result.measurements[keys[0]]))\n",
")\n",
"total = sum(joint.values())\n",
"print(f\"\\nResults ({total} shots) [keys: {', '.join(keys)}]:\")\n",
"for bitstring, count in sorted(joint.items()):\n",
" print(f\" {bitstring}: {count:4d} ({count/total:.1%})\")"
]
},
{
"cell_type": "markdown",
"id": "1478e88a",
"metadata": {},
"source": [
"## Notes\n",
"- Ensure all measurement keys appear before any classical condition usage if you introduce classical controls.\n",
"- Multi-target or custom gates may need full decomposition before submission if they produce unsupported classical constructs."
"## Handling qubit loss on noisy hardware\n",
"\n",
"On some hardware backends — particularly neutral-atom and trapped-ion devices — a qubit may be lost before measurement (e.g. an atom is ejected from the trap). When this happens, the backend records `\"-\"` in the bitstring position for that qubit rather than `\"0\"` or `\"1\"`. Because loss shots contain non-binary characters, they cannot be included in standard measurement arrays, which assume a fixed binary alphabet. The SDK therefore separates them automatically.\n",
"\n",
"The `cirq.ResultDict` returned by `job.results()` exposes two ways to access shots:\n",
"\n",
"| Field | What it contains |\n",
"|---|---|\n",
"| **`result.measurements[key]`** | NumPy int8 array of accepted shots only (no `\"-\"`), shape `(accepted_shots, num_qubits)` |\n",
"| **`result.raw_measurements()[key]`** | String array of all shots (loss shots have `\"-\"`), same key structure as `measurements` |\n",
"| **`result.raw_shots`** | The original shot objects exactly as returned by the backend |\n",
"\n",
"Use `result.measurements` for any downstream analysis that expects clean binary arrays. Use `result.raw_measurements()` and `result.raw_shots` to inspect loss patterns — for example, to calculate the overall loss rate or identify which qubit positions are being lost most frequently.\n",
"\n",
"> **Tip**: A high loss rate may indicate hardware instability or a circuit that is too deep for the current calibration."
]
}
],
Expand Down
Loading
Loading