|
7 | 7 | "source": [ |
8 | 8 | "# Submitting Cirq Circuits to Azure Quantum with the QDK\n", |
9 | 9 | "\n", |
10 | | - "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." |
| 10 | + "This notebook demonstrates how to run Cirq `Circuit` jobs on Azure Quantum using `AzureQuantumService` from the QDK." |
11 | 11 | ] |
12 | 12 | }, |
13 | 13 | { |
|
17 | 17 | "source": [ |
18 | 18 | "The workflow demonstrated here:\n", |
19 | 19 | "\n", |
20 | | - "1. Build (or load) a Cirq `Circuit`.\n", |
21 | | - "2. Convert it to OpenQASM 3 text via `circuit.to_qasm(version=\"3.0\")`.\n", |
22 | | - "3. Compile the OpenQASM 3 source to QIR with `qdk.openqasm.compile`.\n", |
23 | | - "4. Connect to (or create) an Azure Quantum workspace using `qdk.azure.Workspace`.\n", |
24 | | - "5. Pick a target (e.g., a simulator like `rigetti.sim.qvm`).\n", |
25 | | - "6. Submit the QIR payload and retrieve measurement results." |
| 20 | + "1. Build a Cirq `Circuit` with named measurement keys.\n", |
| 21 | + "2. Reference an existing Azure Quantum workspace with `AzureQuantumService`.\n", |
| 22 | + "3. Browse available targets via `service.targets()`.\n", |
| 23 | + "4. Call `service.create_job(program=circuit, repetitions=..., target=...)` and fetch results (`job.results()`)." |
26 | 24 | ] |
27 | 25 | }, |
28 | 26 | { |
|
32 | 30 | "source": [ |
33 | 31 | "## Prerequisites\n", |
34 | 32 | "\n", |
35 | | - "Ensure the `qdk` package is installed with `azure` and `cirq` extras. If not, install dependencies below." |
| 33 | + "This notebook assumes the `qdk` package with Azure Quantum and Cirq support is installed. You can install everything with:" |
36 | 34 | ] |
37 | 35 | }, |
38 | 36 | { |
|
50 | 48 | "id": "20b9ed32", |
51 | 49 | "metadata": {}, |
52 | 50 | "source": [ |
53 | | - "After installing, restart the kernel if necessary. Verify imports:" |
| 51 | + "This installs:\n", |
| 52 | + "- The base `qdk` package (compiler, OpenQASM/QIR tooling)\n", |
| 53 | + "- Azure Quantum client dependencies for submission\n", |
| 54 | + "- Cirq for circuit construction\n", |
| 55 | + "\n", |
| 56 | + "After installing, restart the notebook kernel if it was already running. You can verify installation with:" |
54 | 57 | ] |
55 | 58 | }, |
56 | 59 | { |
|
68 | 71 | "id": "e2b111db", |
69 | 72 | "metadata": {}, |
70 | 73 | "source": [ |
71 | | - "## Submitting a simple Cirq circuit\n", |
72 | | - "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." |
| 74 | + "## Build a simple Cirq circuit\n", |
| 75 | + "\n", |
| 76 | + "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." |
73 | 77 | ] |
74 | 78 | }, |
75 | 79 | { |
|
79 | 83 | "metadata": {}, |
80 | 84 | "outputs": [], |
81 | 85 | "source": [ |
82 | | - "# Build a simple circuit\n", |
| 86 | + "import cirq\n", |
| 87 | + "\n", |
83 | 88 | "q0, q1 = cirq.LineQubit.range(2)\n", |
84 | | - "simple_circuit = cirq.Circuit(\n", |
| 89 | + "circuit = cirq.Circuit(\n", |
85 | 90 | " cirq.H(q0),\n", |
86 | | - " cirq.measure(q0, key='m0'),\n", |
87 | | - " cirq.X(q1),\n", |
88 | | - " cirq.measure(q1, key='m1'),\n", |
| 91 | + " cirq.CNOT(q0, q1),\n", |
| 92 | + " cirq.measure(q0, key=\"q0\"),\n", |
| 93 | + " cirq.measure(q1, key=\"q1\"),\n", |
89 | 94 | ")\n", |
90 | | - "print(simple_circuit)" |
| 95 | + "print(circuit)" |
91 | 96 | ] |
92 | 97 | }, |
93 | 98 | { |
|
96 | 101 | "metadata": {}, |
97 | 102 | "source": [ |
98 | 103 | "## Configure Azure Quantum workspace connection\n", |
99 | | - "Replace the placeholder values below with your own subscription, resource group, workspace name, and location." |
| 104 | + "\n", |
| 105 | + "To connect to an Azure workspace replace the following variables with your own values." |
100 | 106 | ] |
101 | 107 | }, |
102 | 108 | { |
|
113 | 119 | ] |
114 | 120 | }, |
115 | 121 | { |
116 | | - "cell_type": "code", |
117 | | - "execution_count": null, |
118 | | - "id": "9f336702", |
| 122 | + "cell_type": "markdown", |
| 123 | + "id": "184b6e57", |
119 | 124 | "metadata": {}, |
120 | | - "outputs": [], |
121 | 125 | "source": [ |
122 | | - "from qdk.openqasm import compile\n", |
123 | | - "from qdk.azure import Workspace\n", |
124 | | - "from qdk import TargetProfile\n", |
125 | | - "\n", |
126 | | - "def submit_cirq_circuit_to_azure(circuit: cirq.Circuit, target_name: str, name: str, shots: int = 100):\n", |
127 | | - " # 1. Export to OpenQASM 3\n", |
128 | | - " qasm3_str = circuit.to_qasm(version='3.0')\n", |
129 | | - " # 2. Compile to QIR with a base target profile\n", |
130 | | - " qir = compile(qasm3_str, target_profile=TargetProfile.Base)\n", |
131 | | - " # 3. Connect workspace\n", |
132 | | - " workspace = Workspace(\n", |
133 | | - " subscription_id=subscription_id,\n", |
134 | | - " resource_group=resource_group,\n", |
135 | | - " name=workspace_name,\n", |
136 | | - " location=location,\n", |
137 | | - " )\n", |
138 | | - " # 4. Select target (string e.g. 'rigetti.sim.qvm' or other available target)\n", |
139 | | - " target = workspace.get_targets(target_name)\n", |
140 | | - " # 5. Submit QIR payload\n", |
141 | | - " job = target.submit(qir, name, shots=shots)\n", |
142 | | - " return job.get_results()" |
| 126 | + "## Submit the circuit to Azure Quantum\n", |
| 127 | + "\n", |
| 128 | + "`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", |
| 129 | + "\n", |
| 130 | + "- **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", |
| 131 | + "- **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", |
| 132 | + "\n", |
| 133 | + "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." |
143 | 134 | ] |
144 | 135 | }, |
145 | 136 | { |
146 | | - "cell_type": "markdown", |
147 | | - "id": "a0436202", |
| 137 | + "cell_type": "code", |
| 138 | + "execution_count": null, |
| 139 | + "id": "9f336702", |
148 | 140 | "metadata": {}, |
| 141 | + "outputs": [], |
149 | 142 | "source": [ |
150 | | - "### Submit the simple circuit\n", |
151 | | - "(Make sure you changed the workspace credentials above.)" |
| 143 | + "from qdk.azure import Workspace\n", |
| 144 | + "from azure.quantum.cirq import AzureQuantumService\n", |
| 145 | + "\n", |
| 146 | + "workspace = Workspace(\n", |
| 147 | + " subscription_id=subscription_id,\n", |
| 148 | + " resource_group=resource_group,\n", |
| 149 | + " name=workspace_name,\n", |
| 150 | + " location=location,\n", |
| 151 | + ")\n", |
| 152 | + "\n", |
| 153 | + "service = AzureQuantumService(workspace)\n", |
| 154 | + "\n", |
| 155 | + "# List available targets\n", |
| 156 | + "for target in service.targets():\n", |
| 157 | + " print(f\"{target.name:45s} {type(target).__name__}\")" |
152 | 158 | ] |
153 | 159 | }, |
154 | 160 | { |
|
158 | 164 | "metadata": {}, |
159 | 165 | "outputs": [], |
160 | 166 | "source": [ |
161 | | - "# Uncomment after setting workspace credentials\n", |
162 | | - "# results = submit_cirq_circuit_to_azure(simple_circuit, 'rigetti.sim.qvm', 'cirq-simple-job')\n", |
163 | | - "# print(results)" |
| 167 | + "from collections import Counter\n", |
| 168 | + "\n", |
| 169 | + "# Replace with any target name from the list above\n", |
| 170 | + "target_name = \"rigetti.sim.qvm\"\n", |
| 171 | + "\n", |
| 172 | + "job = service.create_job(\n", |
| 173 | + " program=circuit,\n", |
| 174 | + " repetitions=100,\n", |
| 175 | + " name=\"cirq-bell-job\",\n", |
| 176 | + " target=target_name,\n", |
| 177 | + ")\n", |
| 178 | + "print(f\"Job {job.job_id()} submitted — waiting for results...\")\n", |
| 179 | + "\n", |
| 180 | + "result = job.results()\n", |
| 181 | + "\n", |
| 182 | + "# Combine separate measurement keys into joint bitstrings\n", |
| 183 | + "keys = sorted(result.measurements.keys())\n", |
| 184 | + "joint = Counter(\n", |
| 185 | + " \"\".join(str(int(result.measurements[k][i][0])) for k in keys)\n", |
| 186 | + " for i in range(len(result.measurements[keys[0]]))\n", |
| 187 | + ")\n", |
| 188 | + "total = sum(joint.values())\n", |
| 189 | + "print(f\"\\nResults ({total} shots) [keys: {', '.join(keys)}]:\")\n", |
| 190 | + "for bitstring, count in sorted(joint.items()):\n", |
| 191 | + " print(f\" {bitstring}: {count:4d} ({count/total:.1%})\")" |
164 | 192 | ] |
165 | 193 | }, |
166 | 194 | { |
167 | 195 | "cell_type": "markdown", |
168 | 196 | "id": "1478e88a", |
169 | 197 | "metadata": {}, |
170 | 198 | "source": [ |
171 | | - "## Notes\n", |
172 | | - "- Ensure all measurement keys appear before any classical condition usage if you introduce classical controls.\n", |
173 | | - "- Multi-target or custom gates may need full decomposition before submission if they produce unsupported classical constructs." |
| 199 | + "## Handling qubit loss on noisy hardware\n", |
| 200 | + "\n", |
| 201 | + "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", |
| 202 | + "\n", |
| 203 | + "The `cirq.ResultDict` returned by `job.results()` exposes two ways to access shots:\n", |
| 204 | + "\n", |
| 205 | + "| Field | What it contains |\n", |
| 206 | + "|---|---|\n", |
| 207 | + "| **`result.measurements[key]`** | NumPy int8 array of accepted shots only (no `\"-\"`), shape `(accepted_shots, num_qubits)` |\n", |
| 208 | + "| **`result.raw_measurements()[key]`** | String array of all shots (loss shots have `\"-\"`), same key structure as `measurements` |\n", |
| 209 | + "| **`result.raw_shots`** | The original shot objects exactly as returned by the backend |\n", |
| 210 | + "\n", |
| 211 | + "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", |
| 212 | + "\n", |
| 213 | + "> **Tip**: A high loss rate may indicate hardware instability or a circuit that is too deep for the current calibration." |
174 | 214 | ] |
175 | 215 | } |
176 | 216 | ], |
|
0 commit comments