openquantum-sdk-cirq 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openquantum_sdk_cirq-0.5.0/MANIFEST.in +9 -0
- openquantum_sdk_cirq-0.5.0/PKG-INFO +145 -0
- openquantum_sdk_cirq-0.5.0/README.md +126 -0
- openquantum_sdk_cirq-0.5.0/cirq_openquantum/__init__.py +7 -0
- openquantum_sdk_cirq-0.5.0/cirq_openquantum/result.py +215 -0
- openquantum_sdk_cirq-0.5.0/cirq_openquantum/sampler.py +249 -0
- openquantum_sdk_cirq-0.5.0/openquantum_sdk_cirq.egg-info/PKG-INFO +145 -0
- openquantum_sdk_cirq-0.5.0/openquantum_sdk_cirq.egg-info/SOURCES.txt +15 -0
- openquantum_sdk_cirq-0.5.0/openquantum_sdk_cirq.egg-info/dependency_links.txt +1 -0
- openquantum_sdk_cirq-0.5.0/openquantum_sdk_cirq.egg-info/requires.txt +8 -0
- openquantum_sdk_cirq-0.5.0/openquantum_sdk_cirq.egg-info/top_level.txt +2 -0
- openquantum_sdk_cirq-0.5.0/pyproject.toml +41 -0
- openquantum_sdk_cirq-0.5.0/setup.cfg +4 -0
- openquantum_sdk_cirq-0.5.0/tests/__init__.py +0 -0
- openquantum_sdk_cirq-0.5.0/tests/conftest.py +46 -0
- openquantum_sdk_cirq-0.5.0/tests/test_integration.py +96 -0
- openquantum_sdk_cirq-0.5.0/tests/test_sampler.py +319 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Unit tests ship in the sdist. The wheel does not install them.
|
|
2
|
+
include README.md
|
|
3
|
+
include pyproject.toml
|
|
4
|
+
recursive-include cirq_openquantum *.py
|
|
5
|
+
recursive-include tests *.py
|
|
6
|
+
global-exclude *.pyc
|
|
7
|
+
global-exclude __pycache__
|
|
8
|
+
prune .venv
|
|
9
|
+
prune .pytest_cache
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openquantum-sdk-cirq
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Cirq Python SDK for Open Quantum
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Classifier: Development Status :: 4 - Beta
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: openquantum-sdk>=1.0.0
|
|
13
|
+
Requires-Dist: cirq-core>=1.4
|
|
14
|
+
Requires-Dist: numpy>=1.22
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-mock>=3.10; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# Cirq-Open Quantum Plugin
|
|
21
|
+
|
|
22
|
+
> **Beta.** This plugin is in beta; its API and defaults may still change. Feedback is welcome in the [Quantum Rings community](https://www.quantumrings.com/community).
|
|
23
|
+
|
|
24
|
+
A [Cirq](https://quantumai.google/cirq) sampler that submits circuits to the
|
|
25
|
+
[Open Quantum](https://www.openquantum.com) platform. Write circuits with Cirq
|
|
26
|
+
and run them on real quantum hardware.
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
Cirq exports OpenQASM natively (`cirq.Circuit.to_qasm()`), so this plugin does
|
|
31
|
+
not depend on the Qiskit plugin. `OpenQuantumSampler` is a thin
|
|
32
|
+
[`cirq.Sampler`](https://quantumai.google/reference/python/cirq/Sampler):
|
|
33
|
+
|
|
34
|
+
- `OpenQuantumClient.from_env` (Core SDK) handles authentication and backend
|
|
35
|
+
discovery.
|
|
36
|
+
- Each resolved circuit is serialized to OpenQASM and submitted through the
|
|
37
|
+
framework-agnostic `SchedulerClient`.
|
|
38
|
+
- Platform counts are reconstructed into `cirq.Result` objects.
|
|
39
|
+
|
|
40
|
+
Because it implements `run_sweep`, the standard `cirq.Sampler` methods —
|
|
41
|
+
`run`, `sample`, `run_sweep`, and `run_batch` — all work.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Plugin (and core SDK)
|
|
47
|
+
pip install openquantum-sdk-cirq
|
|
48
|
+
|
|
49
|
+
# or via extras:
|
|
50
|
+
pip install "openquantum-sdk[cirq]"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This pulls in `cirq-core` and `openquantum-sdk`.
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import cirq
|
|
59
|
+
from cirq_openquantum import OpenQuantumSampler
|
|
60
|
+
|
|
61
|
+
sampler = OpenQuantumSampler(
|
|
62
|
+
backend="ionq:forte-1",
|
|
63
|
+
client_id="your_client_id",
|
|
64
|
+
client_secret="your_client_secret",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
q = cirq.LineQubit.range(2)
|
|
68
|
+
circuit = cirq.Circuit([
|
|
69
|
+
cirq.H(q[0]),
|
|
70
|
+
cirq.CNOT(q[0], q[1]),
|
|
71
|
+
cirq.measure(*q, key="m"),
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
result = sampler.run(circuit, repetitions=1024)
|
|
75
|
+
print(result.histogram(key="m"))
|
|
76
|
+
# Counter({0: ~512, 3: ~512}) # 00 and 11
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Circuits must contain at least one `cirq.measure(...)`; the QASM exporter does
|
|
80
|
+
not add measurements for you.
|
|
81
|
+
|
|
82
|
+
## Authentication
|
|
83
|
+
|
|
84
|
+
Resolved in this order:
|
|
85
|
+
|
|
86
|
+
1. **Pre-built client** — pass `client=OpenQuantumClient(...)` for full control
|
|
87
|
+
(saved accounts, custom URLs, keyring).
|
|
88
|
+
2. **Bearer token** — pass `token=` or set `OPENQUANTUM_TOKEN`.
|
|
89
|
+
3. **Client credentials** — pass `client_id=` + `client_secret=` or set
|
|
90
|
+
`OPENQUANTUM_CLIENT_ID` + `OPENQUANTUM_CLIENT_SECRET`.
|
|
91
|
+
|
|
92
|
+
| Parameter | Environment Variable | Description |
|
|
93
|
+
|-----------|---------------------|-------------|
|
|
94
|
+
| `token` | `OPENQUANTUM_TOKEN` | Bearer token |
|
|
95
|
+
| `client_id` | `OPENQUANTUM_CLIENT_ID` | OAuth2 client ID |
|
|
96
|
+
| `client_secret` | `OPENQUANTUM_CLIENT_SECRET` | OAuth2 client secret |
|
|
97
|
+
| | `OPENQUANTUM_SDK_KEY` | Path to an SDK-key JSON file (`{client_id, client_secret}`) |
|
|
98
|
+
| `organization_id` | `OPENQUANTUM_ORGANIZATION_ID` | Organization UUID (optional if single org) |
|
|
99
|
+
| `scheduler_url` | `OPENQUANTUM_SCHEDULER_URL` | Scheduler API base, when the argument is omitted |
|
|
100
|
+
| `management_url` | `OPENQUANTUM_MANAGEMENT_URL` | Management API base, when the argument is omitted |
|
|
101
|
+
| `keycloak_base` | `OPENQUANTUM_KEYCLOAK_BASE` | Keycloak base, when the argument is omitted |
|
|
102
|
+
| `realm` | `OPENQUANTUM_KEYCLOAK_REALM` | Keycloak realm, when the argument is omitted |
|
|
103
|
+
|
|
104
|
+
`from_env()` raises `AuthenticationError` when no credential resolves. The older `OPENQUANTUM_*_BASE` URL names are not read here; those remain an MCP-only fallback.
|
|
105
|
+
|
|
106
|
+
## Configuration
|
|
107
|
+
|
|
108
|
+
| Parameter | Default | Description |
|
|
109
|
+
|-----------|---------|-------------|
|
|
110
|
+
| `backend` | required | Backend class ID or short_code (e.g. `"ionq:forte-1"`) |
|
|
111
|
+
| `job_subcategory_id` | `"oth:oth"` | Workload subcategory (set the most specific match) |
|
|
112
|
+
| `execution_plan` | `"auto"` | `"auto"`, `"public"`, `"private"`, an enum, or a plan UUID. `"auto"` picks the cheapest plan |
|
|
113
|
+
| `queue_priority` | `"auto"` | `"auto"`, `"standard"`, `"priority"`, `"instant"`, an enum, or a priority UUID. `"auto"` picks the cheapest priority |
|
|
114
|
+
| `export_format` | `"qasm3"` | `"qasm2"` or `"qasm3"` |
|
|
115
|
+
| `configuration_data` | `{}` | Backend-specific config passthrough (e.g. `{"engine": "amber_quantum_rings"}`) |
|
|
116
|
+
| `reverse_bits` | `True` | Reverse platform bitstrings to Cirq's big-endian order |
|
|
117
|
+
| `verbose` | `False` | Print submission progress and credit cost to stdout |
|
|
118
|
+
| `job_timeout_seconds` | `86400` | Max wait for job completion (1 day) |
|
|
119
|
+
|
|
120
|
+
Shots are set per run via `repetitions` and injected into `configuration_data`.
|
|
121
|
+
|
|
122
|
+
## Backend-specific configuration
|
|
123
|
+
|
|
124
|
+
Some backends require extra fields. The free Quantum Rings simulator
|
|
125
|
+
(`qr:gpu-gb10`) requires an `engine`:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
sampler = OpenQuantumSampler(
|
|
129
|
+
backend="qr:gpu-gb10",
|
|
130
|
+
configuration_data={"engine": "amber_quantum_rings"},
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Bit ordering
|
|
135
|
+
|
|
136
|
+
The platform returns counts little-endian (the first classical bit is the
|
|
137
|
+
rightmost character, Qiskit-style), while Cirq is big-endian (the first
|
|
138
|
+
measured qubit is the most-significant bit). The sampler reverses each
|
|
139
|
+
bitstring by default (`reverse_bits=True`) so the mapping is correct, verified
|
|
140
|
+
live against an asymmetric `|10>` state. Set `reverse_bits=False` only for a
|
|
141
|
+
backend that returns counts big-endian.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
Apache License 2.0
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Cirq-Open Quantum Plugin
|
|
2
|
+
|
|
3
|
+
> **Beta.** This plugin is in beta; its API and defaults may still change. Feedback is welcome in the [Quantum Rings community](https://www.quantumrings.com/community).
|
|
4
|
+
|
|
5
|
+
A [Cirq](https://quantumai.google/cirq) sampler that submits circuits to the
|
|
6
|
+
[Open Quantum](https://www.openquantum.com) platform. Write circuits with Cirq
|
|
7
|
+
and run them on real quantum hardware.
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
Cirq exports OpenQASM natively (`cirq.Circuit.to_qasm()`), so this plugin does
|
|
12
|
+
not depend on the Qiskit plugin. `OpenQuantumSampler` is a thin
|
|
13
|
+
[`cirq.Sampler`](https://quantumai.google/reference/python/cirq/Sampler):
|
|
14
|
+
|
|
15
|
+
- `OpenQuantumClient.from_env` (Core SDK) handles authentication and backend
|
|
16
|
+
discovery.
|
|
17
|
+
- Each resolved circuit is serialized to OpenQASM and submitted through the
|
|
18
|
+
framework-agnostic `SchedulerClient`.
|
|
19
|
+
- Platform counts are reconstructed into `cirq.Result` objects.
|
|
20
|
+
|
|
21
|
+
Because it implements `run_sweep`, the standard `cirq.Sampler` methods —
|
|
22
|
+
`run`, `sample`, `run_sweep`, and `run_batch` — all work.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Plugin (and core SDK)
|
|
28
|
+
pip install openquantum-sdk-cirq
|
|
29
|
+
|
|
30
|
+
# or via extras:
|
|
31
|
+
pip install "openquantum-sdk[cirq]"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This pulls in `cirq-core` and `openquantum-sdk`.
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import cirq
|
|
40
|
+
from cirq_openquantum import OpenQuantumSampler
|
|
41
|
+
|
|
42
|
+
sampler = OpenQuantumSampler(
|
|
43
|
+
backend="ionq:forte-1",
|
|
44
|
+
client_id="your_client_id",
|
|
45
|
+
client_secret="your_client_secret",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
q = cirq.LineQubit.range(2)
|
|
49
|
+
circuit = cirq.Circuit([
|
|
50
|
+
cirq.H(q[0]),
|
|
51
|
+
cirq.CNOT(q[0], q[1]),
|
|
52
|
+
cirq.measure(*q, key="m"),
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
result = sampler.run(circuit, repetitions=1024)
|
|
56
|
+
print(result.histogram(key="m"))
|
|
57
|
+
# Counter({0: ~512, 3: ~512}) # 00 and 11
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Circuits must contain at least one `cirq.measure(...)`; the QASM exporter does
|
|
61
|
+
not add measurements for you.
|
|
62
|
+
|
|
63
|
+
## Authentication
|
|
64
|
+
|
|
65
|
+
Resolved in this order:
|
|
66
|
+
|
|
67
|
+
1. **Pre-built client** — pass `client=OpenQuantumClient(...)` for full control
|
|
68
|
+
(saved accounts, custom URLs, keyring).
|
|
69
|
+
2. **Bearer token** — pass `token=` or set `OPENQUANTUM_TOKEN`.
|
|
70
|
+
3. **Client credentials** — pass `client_id=` + `client_secret=` or set
|
|
71
|
+
`OPENQUANTUM_CLIENT_ID` + `OPENQUANTUM_CLIENT_SECRET`.
|
|
72
|
+
|
|
73
|
+
| Parameter | Environment Variable | Description |
|
|
74
|
+
|-----------|---------------------|-------------|
|
|
75
|
+
| `token` | `OPENQUANTUM_TOKEN` | Bearer token |
|
|
76
|
+
| `client_id` | `OPENQUANTUM_CLIENT_ID` | OAuth2 client ID |
|
|
77
|
+
| `client_secret` | `OPENQUANTUM_CLIENT_SECRET` | OAuth2 client secret |
|
|
78
|
+
| | `OPENQUANTUM_SDK_KEY` | Path to an SDK-key JSON file (`{client_id, client_secret}`) |
|
|
79
|
+
| `organization_id` | `OPENQUANTUM_ORGANIZATION_ID` | Organization UUID (optional if single org) |
|
|
80
|
+
| `scheduler_url` | `OPENQUANTUM_SCHEDULER_URL` | Scheduler API base, when the argument is omitted |
|
|
81
|
+
| `management_url` | `OPENQUANTUM_MANAGEMENT_URL` | Management API base, when the argument is omitted |
|
|
82
|
+
| `keycloak_base` | `OPENQUANTUM_KEYCLOAK_BASE` | Keycloak base, when the argument is omitted |
|
|
83
|
+
| `realm` | `OPENQUANTUM_KEYCLOAK_REALM` | Keycloak realm, when the argument is omitted |
|
|
84
|
+
|
|
85
|
+
`from_env()` raises `AuthenticationError` when no credential resolves. The older `OPENQUANTUM_*_BASE` URL names are not read here; those remain an MCP-only fallback.
|
|
86
|
+
|
|
87
|
+
## Configuration
|
|
88
|
+
|
|
89
|
+
| Parameter | Default | Description |
|
|
90
|
+
|-----------|---------|-------------|
|
|
91
|
+
| `backend` | required | Backend class ID or short_code (e.g. `"ionq:forte-1"`) |
|
|
92
|
+
| `job_subcategory_id` | `"oth:oth"` | Workload subcategory (set the most specific match) |
|
|
93
|
+
| `execution_plan` | `"auto"` | `"auto"`, `"public"`, `"private"`, an enum, or a plan UUID. `"auto"` picks the cheapest plan |
|
|
94
|
+
| `queue_priority` | `"auto"` | `"auto"`, `"standard"`, `"priority"`, `"instant"`, an enum, or a priority UUID. `"auto"` picks the cheapest priority |
|
|
95
|
+
| `export_format` | `"qasm3"` | `"qasm2"` or `"qasm3"` |
|
|
96
|
+
| `configuration_data` | `{}` | Backend-specific config passthrough (e.g. `{"engine": "amber_quantum_rings"}`) |
|
|
97
|
+
| `reverse_bits` | `True` | Reverse platform bitstrings to Cirq's big-endian order |
|
|
98
|
+
| `verbose` | `False` | Print submission progress and credit cost to stdout |
|
|
99
|
+
| `job_timeout_seconds` | `86400` | Max wait for job completion (1 day) |
|
|
100
|
+
|
|
101
|
+
Shots are set per run via `repetitions` and injected into `configuration_data`.
|
|
102
|
+
|
|
103
|
+
## Backend-specific configuration
|
|
104
|
+
|
|
105
|
+
Some backends require extra fields. The free Quantum Rings simulator
|
|
106
|
+
(`qr:gpu-gb10`) requires an `engine`:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
sampler = OpenQuantumSampler(
|
|
110
|
+
backend="qr:gpu-gb10",
|
|
111
|
+
configuration_data={"engine": "amber_quantum_rings"},
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Bit ordering
|
|
116
|
+
|
|
117
|
+
The platform returns counts little-endian (the first classical bit is the
|
|
118
|
+
rightmost character, Qiskit-style), while Cirq is big-endian (the first
|
|
119
|
+
measured qubit is the most-significant bit). The sampler reverses each
|
|
120
|
+
bitstring by default (`reverse_bits=True`) so the mapping is correct, verified
|
|
121
|
+
live against an asymmetric `|10>` state. Set `reverse_bits=False` only for a
|
|
122
|
+
backend that returns counts big-endian.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
Apache License 2.0
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Build ``cirq.Result`` objects from Open Quantum platform counts.
|
|
2
|
+
|
|
3
|
+
The platform returns a ``{bitstring: count}`` histogram for each submitted
|
|
4
|
+
circuit. Cirq's result layer wants per-shot measurement records keyed by
|
|
5
|
+
measurement key, so this module expands the histogram back into the
|
|
6
|
+
``(repetitions, num_qubits)`` ``numpy`` arrays a :class:`cirq.ResultDict`
|
|
7
|
+
expects.
|
|
8
|
+
|
|
9
|
+
**Bitstring layout.** The platform indexes each returned bitstring by
|
|
10
|
+
*classical bit*, not by qubit. It parses the exported OpenQASM, allocates one
|
|
11
|
+
classical register (``creg``/``bit[]``) per measurement, and concatenates them
|
|
12
|
+
little-endian in declaration order (Qiskit convention): the first-declared
|
|
13
|
+
register occupies the least-significant (rightmost) bits, and the rightmost
|
|
14
|
+
character of the string is classical bit 0. ``OpenQuantumSampler`` reverses each
|
|
15
|
+
bitstring by default (``reverse_bits=True``) so the mapping to ascending
|
|
16
|
+
classical-bit index is correct.
|
|
17
|
+
|
|
18
|
+
Because the layout is classical-bit based, it is reconstructed from the exact
|
|
19
|
+
QASM that was submitted: each ``measure q[i] -> m_<key>[j]`` statement ties a
|
|
20
|
+
qubit to a classical bit, and the register declaration order fixes the global
|
|
21
|
+
bit offsets. Slicing by qubit index (as an earlier version did) only coincided
|
|
22
|
+
with the true layout when every qubit was measured once, in qubit order, under
|
|
23
|
+
a single key; reordered, multi-key, or subset measurements came back wrong.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import re
|
|
29
|
+
from typing import Dict, List, Mapping, Optional, Sequence, Tuple
|
|
30
|
+
|
|
31
|
+
import cirq
|
|
32
|
+
import numpy as np
|
|
33
|
+
|
|
34
|
+
from openquantum_sdk.exceptions import CircuitValidationError, JobError
|
|
35
|
+
|
|
36
|
+
# QASM 2.0: ``creg name[width];`` QASM 3.0: ``bit[width] name;`` / ``bit name;``
|
|
37
|
+
_DECL_CREG = re.compile(r"\bcreg\s+(\w+)\s*\[\s*(\d+)\s*\]")
|
|
38
|
+
_DECL_BIT_N = re.compile(r"\bbit\s*\[\s*(\d+)\s*\]\s*(\w+)")
|
|
39
|
+
_DECL_BIT_1 = re.compile(r"\bbit\s+(\w+)\s*;")
|
|
40
|
+
# QASM 2.0: ``measure q[i] -> name[j];`` QASM 3.0: ``name[j] = measure q[i];``
|
|
41
|
+
_MEAS_Q2 = re.compile(r"\bmeasure\s+q\s*\[\s*(\d+)\s*\]\s*->\s*(\w+)\s*\[\s*(\d+)\s*\]")
|
|
42
|
+
_MEAS_Q3 = re.compile(r"(\w+)\s*\[\s*(\d+)\s*\]\s*=\s*measure\s+q\s*\[\s*(\d+)\s*\]")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_qasm_registers(qasm: str):
|
|
46
|
+
"""Parse classical register declarations and measurements from QASM.
|
|
47
|
+
|
|
48
|
+
Returns ``(cregs, offsets, total, creg_qubits)`` where ``cregs`` is the
|
|
49
|
+
ordered ``[(name, width)]`` list of register declarations, ``offsets`` maps
|
|
50
|
+
each register name to its global classical-bit offset (first-declared
|
|
51
|
+
register at offset 0, i.e. least significant), ``total`` is the total
|
|
52
|
+
classical-bit width, and ``creg_qubits`` maps each register name to
|
|
53
|
+
``{creg_bit: qubit_index}`` recovered from the measure statements.
|
|
54
|
+
"""
|
|
55
|
+
cregs: List[Tuple[str, int]] = [] # (name, width) in declaration order
|
|
56
|
+
creg_qubits: Dict[str, Dict[int, int]] = {}
|
|
57
|
+
|
|
58
|
+
for raw in qasm.splitlines():
|
|
59
|
+
line = raw.split("//", 1)[0] # drop trailing comments
|
|
60
|
+
if not line.strip():
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
bit_n = _DECL_BIT_N.search(line)
|
|
64
|
+
creg = _DECL_CREG.search(line)
|
|
65
|
+
bit_1 = _DECL_BIT_1.search(line)
|
|
66
|
+
if bit_n:
|
|
67
|
+
cregs.append((bit_n.group(2), int(bit_n.group(1))))
|
|
68
|
+
elif creg:
|
|
69
|
+
cregs.append((creg.group(1), int(creg.group(2))))
|
|
70
|
+
elif bit_1:
|
|
71
|
+
cregs.append((bit_1.group(1), 1))
|
|
72
|
+
|
|
73
|
+
for mo in _MEAS_Q2.finditer(line):
|
|
74
|
+
qidx, name, cbit = int(mo.group(1)), mo.group(2), int(mo.group(3))
|
|
75
|
+
creg_qubits.setdefault(name, {})[cbit] = qidx
|
|
76
|
+
for mo in _MEAS_Q3.finditer(line):
|
|
77
|
+
name, cbit, qidx = mo.group(1), int(mo.group(2)), int(mo.group(3))
|
|
78
|
+
creg_qubits.setdefault(name, {})[cbit] = qidx
|
|
79
|
+
|
|
80
|
+
offsets: Dict[str, int] = {}
|
|
81
|
+
offset = 0
|
|
82
|
+
for name, width in cregs:
|
|
83
|
+
offsets[name] = offset
|
|
84
|
+
offset += width
|
|
85
|
+
return cregs, offsets, offset, creg_qubits
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _measurement_layout(
|
|
89
|
+
circuit, qasm: str
|
|
90
|
+
) -> Tuple[List[Tuple[str, List[int]]], int]:
|
|
91
|
+
"""Return ``([(key, global_classical_bits)], total_bits)`` for a circuit.
|
|
92
|
+
|
|
93
|
+
Each measurement key is matched to the classical register that captured it
|
|
94
|
+
by comparing the ordered tuple of measured qubit indices (recovered from the
|
|
95
|
+
QASM measure statements) rather than by register name, since Cirq sanitises
|
|
96
|
+
unsafe keys into names like ``m0``/``m1`` that do not echo the key. Column
|
|
97
|
+
``c`` of a key's result array maps to its register's classical bit ``c``.
|
|
98
|
+
"""
|
|
99
|
+
cregs, offsets, total, creg_qubits = _parse_qasm_registers(qasm)
|
|
100
|
+
|
|
101
|
+
ordered_qubits = cirq.QubitOrder.DEFAULT.order_for(circuit.all_qubits())
|
|
102
|
+
index_of = {q: i for i, q in enumerate(ordered_qubits)}
|
|
103
|
+
|
|
104
|
+
# Ordered qubit-index tuple each register captured, used for matching.
|
|
105
|
+
creg_tuple = {
|
|
106
|
+
name: tuple(creg_qubits.get(name, {}).get(c) for c in range(width))
|
|
107
|
+
for name, width in cregs
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
layout: List[Tuple[str, List[int]]] = []
|
|
111
|
+
used = set()
|
|
112
|
+
for op in circuit.all_operations():
|
|
113
|
+
gate = getattr(op, "gate", None)
|
|
114
|
+
if not isinstance(gate, cirq.MeasurementGate):
|
|
115
|
+
continue
|
|
116
|
+
key = cirq.measurement_key_name(op)
|
|
117
|
+
qtuple = tuple(index_of[q] for q in op.qubits)
|
|
118
|
+
|
|
119
|
+
match = None
|
|
120
|
+
for name, _width in cregs:
|
|
121
|
+
if name not in used and creg_tuple[name] == qtuple:
|
|
122
|
+
match = name
|
|
123
|
+
break
|
|
124
|
+
if match is None:
|
|
125
|
+
raise JobError(
|
|
126
|
+
f"Could not map measurement key {key!r} (qubits {qtuple}) to a "
|
|
127
|
+
f"classical register in the submitted QASM."
|
|
128
|
+
)
|
|
129
|
+
used.add(match)
|
|
130
|
+
base = offsets[match]
|
|
131
|
+
layout.append((key, [base + c for c in range(len(qtuple))]))
|
|
132
|
+
|
|
133
|
+
return layout, total
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def result_from_counts(
|
|
137
|
+
counts: Mapping[str, int],
|
|
138
|
+
circuit,
|
|
139
|
+
params=None,
|
|
140
|
+
*,
|
|
141
|
+
reverse_bits: bool = False,
|
|
142
|
+
qasm: Optional[str] = None,
|
|
143
|
+
):
|
|
144
|
+
"""Reconstruct a ``cirq.ResultDict`` from a platform counts histogram.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
counts: ``{bitstring: count}`` as returned by the scheduler.
|
|
148
|
+
circuit: The resolved ``cirq.Circuit`` that was submitted.
|
|
149
|
+
params: The ``cirq.ParamResolver`` used for this circuit (stored on the
|
|
150
|
+
result so sweeps round-trip their parameters).
|
|
151
|
+
reverse_bits: Reverse each platform bitstring before mapping. The
|
|
152
|
+
platform returns counts little-endian (classical bit 0 is the
|
|
153
|
+
rightmost character), so this must be ``True`` to map onto ascending
|
|
154
|
+
classical-bit index. The sampler passes ``True`` by default.
|
|
155
|
+
qasm: The exact OpenQASM submitted for this circuit. When omitted it is
|
|
156
|
+
regenerated with ``circuit.to_qasm()``; pass the submitted string to
|
|
157
|
+
guarantee the layout matches what the platform parsed.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
A ``cirq.ResultDict`` whose ``measurements[key]`` is a
|
|
161
|
+
``(repetitions, num_qubits_in_key)`` ``uint8`` array.
|
|
162
|
+
"""
|
|
163
|
+
if qasm is None:
|
|
164
|
+
qasm = circuit.to_qasm()
|
|
165
|
+
|
|
166
|
+
layout, total = _measurement_layout(circuit, qasm)
|
|
167
|
+
if not layout:
|
|
168
|
+
raise CircuitValidationError(
|
|
169
|
+
"Circuit has no measurement gates; add cirq.measure(...) before "
|
|
170
|
+
"sampling on the Open Quantum platform."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
per_key_rows: Dict[str, List[List[int]]] = {key: [] for key, _ in layout}
|
|
174
|
+
for bitstring, count in counts.items():
|
|
175
|
+
clean = bitstring.replace(" ", "")
|
|
176
|
+
if len(clean) < total:
|
|
177
|
+
# The platform may omit leading (most-significant) zero bits.
|
|
178
|
+
clean = clean.rjust(total, "0")
|
|
179
|
+
elif len(clean) > total:
|
|
180
|
+
raise JobError(
|
|
181
|
+
f"Counts bitstring {bitstring!r} has {len(clean)} bits but the "
|
|
182
|
+
f"circuit declares {total} classical bit(s)."
|
|
183
|
+
)
|
|
184
|
+
if reverse_bits:
|
|
185
|
+
clean = clean[::-1]
|
|
186
|
+
bits = [int(c) for c in clean]
|
|
187
|
+
for key, global_bits in layout:
|
|
188
|
+
segment = [bits[g] for g in global_bits]
|
|
189
|
+
per_key_rows[key].extend([segment] * int(count))
|
|
190
|
+
|
|
191
|
+
measurements = {
|
|
192
|
+
key: np.array(per_key_rows[key], dtype=np.uint8).reshape(
|
|
193
|
+
len(per_key_rows[key]), len(global_bits)
|
|
194
|
+
)
|
|
195
|
+
for key, global_bits in layout
|
|
196
|
+
}
|
|
197
|
+
resolver = params if params is not None else cirq.ParamResolver({})
|
|
198
|
+
return cirq.ResultDict(params=resolver, measurements=measurements)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def results_from_counts_list(
|
|
202
|
+
counts_list: Sequence[Mapping[str, int]],
|
|
203
|
+
circuit,
|
|
204
|
+
resolvers,
|
|
205
|
+
*,
|
|
206
|
+
reverse_bits: bool = False,
|
|
207
|
+
qasm: Optional[str] = None,
|
|
208
|
+
):
|
|
209
|
+
"""Build one ``cirq.ResultDict`` per resolver from a list of histograms."""
|
|
210
|
+
return [
|
|
211
|
+
result_from_counts(
|
|
212
|
+
counts, circuit, resolver, reverse_bits=reverse_bits, qasm=qasm
|
|
213
|
+
)
|
|
214
|
+
for counts, resolver in zip(counts_list, resolvers)
|
|
215
|
+
]
|