From dd7589cb8dc1cab9851c80ed379f585dabbe064b Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Wed, 11 Mar 2026 13:32:29 -0700 Subject: [PATCH 1/3] Add Cirq to the manual testing file --- azure_quantum_manual_tests.ipynb | 153 ++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 2 deletions(-) diff --git a/azure_quantum_manual_tests.ipynb b/azure_quantum_manual_tests.ipynb index aa13d8f9..979f5ba4 100644 --- a/azure_quantum_manual_tests.ipynb +++ b/azure_quantum_manual_tests.ipynb @@ -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", @@ -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." @@ -375,6 +376,154 @@ " print(f\" [{target_name}] Counts: {counts}\")\n", " validate_qiskit_ghz(target_name, counts)" ] + }, + { + "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", + " result = future.result()\n", + " validate_cirq_ghz(target_name, result, repetitions=cirq_repetitions)" + ] } ], "metadata": { From f5680e69b199c26978e89c9f4c845db8a6cfd9c7 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Wed, 11 Mar 2026 13:35:02 -0700 Subject: [PATCH 2/3] Add environment clearing cell to setup --- azure_quantum_manual_tests.ipynb | 46 +++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/azure_quantum_manual_tests.ipynb b/azure_quantum_manual_tests.ipynb index 979f5ba4..6812803c 100644 --- a/azure_quantum_manual_tests.ipynb +++ b/azure_quantum_manual_tests.ipynb @@ -64,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." ] }, { @@ -73,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)" ] }, { From 50950a09d9e1a44d8ceb0f9c3730e05be7a204cc Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Thu, 12 Mar 2026 10:48:16 -0700 Subject: [PATCH 3/3] fix some issues with quantinuum.py output translation --- .../azure/quantum/cirq/targets/quantinuum.py | 14 +++++---- azure_quantum_manual_tests.ipynb | 29 ++++++++++++++----- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/azure-quantum/azure/quantum/cirq/targets/quantinuum.py b/azure-quantum/azure/quantum/cirq/targets/quantinuum.py index 583d1767..d7e38c2f 100644 --- a/azure-quantum/azure/quantum/cirq/targets/quantinuum.py +++ b/azure-quantum/azure/quantum/cirq/targets/quantinuum.py @@ -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_") } @@ -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, @@ -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", {})) diff --git a/azure_quantum_manual_tests.ipynb b/azure_quantum_manual_tests.ipynb index 6812803c..9997af65 100644 --- a/azure_quantum_manual_tests.ipynb +++ b/azure_quantum_manual_tests.ipynb @@ -263,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}\")" ] }, { @@ -410,9 +413,12 @@ " 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}\")" ] }, { @@ -559,8 +565,15 @@ " 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", - " result = future.result()\n", - " validate_cirq_ghz(target_name, result, repetitions=cirq_repetitions)" + " 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}\")" ] } ],