openquantum-sdk-qrisp 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.
@@ -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 qrisp_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,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: openquantum-sdk-qrisp
3
+ Version: 0.5.0
4
+ Summary: Qrisp 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.11
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: openquantum-sdk>=1.0.0
13
+ Requires-Dist: openquantum-sdk-qiskit>=1.0.0
14
+ Requires-Dist: qrisp>=0.9
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
+ # Qrisp-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 backend for the [Qrisp](https://www.qrisp.eu/) quantum programming framework
25
+ that submits circuits to the [Open Quantum](https://www.openquantum.com)
26
+ platform. Write high-level Qrisp programs and run them on real quantum hardware.
27
+
28
+ ## How it works
29
+
30
+ Qrisp already depends on Qiskit and can convert any circuit to a Qiskit circuit
31
+ (`QuantumCircuit.to_qiskit()`). This backend uses that bridge to reuse the Open
32
+ Quantum Qiskit plugin:
33
+
34
+ - `OpenQuantumService` handles authentication and backend discovery.
35
+ - The capability-aware `OpenQuantumBackend` (a Qiskit `BackendV2`) validates each
36
+ circuit against the target device (gate set, coupling map, shot and qubit
37
+ limits).
38
+ - `export_circuit_to_qasm` serialises to OpenQASM.
39
+
40
+ Submission runs through the framework-agnostic `SchedulerClient` in the core
41
+ SDK. No separate QASM serializer or auth code is reimplemented here.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ # Plugin (and core SDK + Qiskit plugin)
47
+ pip install openquantum-sdk-qrisp
48
+
49
+ # or via extras:
50
+ pip install "openquantum-sdk[qrisp]"
51
+ ```
52
+
53
+ This pulls in `qrisp`, `openquantum-sdk`, and `openquantum-sdk-qiskit`.
54
+
55
+ ## Quick Start
56
+
57
+ ```python
58
+ from qrisp import QuantumVariable, h, cx
59
+ from qrisp_openquantum import OpenQuantumBackend
60
+
61
+ oq = OpenQuantumBackend(
62
+ backend="ionq:aria-1",
63
+ shots=1024,
64
+ client_id="your_client_id",
65
+ client_secret="your_client_secret",
66
+ )
67
+
68
+ qv = QuantumVariable(2)
69
+ h(qv[0])
70
+ cx(qv[0], qv[1])
71
+
72
+ print(qv.get_measurement(backend=oq, shots=1024))
73
+ # {'00': 0.5, '11': 0.5}
74
+ ```
75
+
76
+ `get_measurement` returns Qrisp's decoded outcomes as probabilities (its own
77
+ normalization, applied for every backend). To get integer shot counts, run a
78
+ raw `QuantumCircuit` directly:
79
+
80
+ ```python
81
+ from qrisp import QuantumCircuit
82
+
83
+ qc = QuantumCircuit(2)
84
+ qc.h(0)
85
+ qc.cx(0, 1)
86
+ qc.measure([0, 1])
87
+
88
+ counts = qc.run(backend=oq, shots=1024)
89
+ # e.g. {'00': 518, '11': 506}
90
+ ```
91
+
92
+ ## Authentication
93
+
94
+ Resolved in this order:
95
+
96
+ 1. **Pre-built service** — pass `service=OpenQuantumService(...)` for full control
97
+ (saved accounts, custom URLs, keyring).
98
+ 2. **Bearer token** — pass `token=` or set `OPENQUANTUM_TOKEN`.
99
+ 3. **Client credentials** — pass `client_id=` + `client_secret=` or set
100
+ `OPENQUANTUM_CLIENT_ID` + `OPENQUANTUM_CLIENT_SECRET`.
101
+
102
+ | Parameter | Environment Variable | Description |
103
+ |-----------|---------------------|-------------|
104
+ | `token` | `OPENQUANTUM_TOKEN` | Bearer token |
105
+ | `client_id` | `OPENQUANTUM_CLIENT_ID` | OAuth2 client ID |
106
+ | `client_secret` | `OPENQUANTUM_CLIENT_SECRET` | OAuth2 client secret |
107
+ | | `OPENQUANTUM_SDK_KEY` | Path to an SDK-key JSON file (`{client_id, client_secret}`) |
108
+ | `organization_id` | `OPENQUANTUM_ORGANIZATION_ID` | Organization UUID (optional if single org) |
109
+ | `scheduler_url` | `OPENQUANTUM_SCHEDULER_URL` | Scheduler API base, when the argument is omitted |
110
+ | `management_url` | `OPENQUANTUM_MANAGEMENT_URL` | Management API base, when the argument is omitted |
111
+ | `keycloak_base` | `OPENQUANTUM_KEYCLOAK_BASE` | Keycloak base, when the argument is omitted |
112
+ | `realm` | `OPENQUANTUM_KEYCLOAK_REALM` | Keycloak realm, when the argument is omitted |
113
+
114
+ `OpenQuantumService.from_env()` raises `AuthenticationError` when no credential resolves. Circuits are serialized with the public `export_circuit_to_qasm` helper from `openquantum_sdk_qiskit`. The older `OPENQUANTUM_*_BASE` URL names are not read here; those remain an MCP-only fallback.
115
+
116
+ ## Configuration
117
+
118
+ | Parameter | Default | Description |
119
+ |-----------|---------|-------------|
120
+ | `backend` | required | Backend class ID or short_code (e.g. `"ionq:aria-1"`) |
121
+ | `shots` | `1024` | Default shots when a Qrisp call omits them |
122
+ | `job_subcategory_id` | `"oth:oth"` | Workload subcategory (set the most specific match) |
123
+ | `execution_plan` | `"auto"` | `"auto"`, `"public"`, `"private"`, an enum, or a plan UUID. `"auto"` picks the cheapest plan |
124
+ | `queue_priority` | `"auto"` | `"auto"`, `"standard"`, `"priority"`, `"instant"`, an enum, or a priority UUID. `"auto"` picks the cheapest priority |
125
+ | `export_format` | `"qasm3"` | `"qasm2"` or `"qasm3"` |
126
+ | `configuration_data` | `{}` | Backend-specific config passthrough (e.g. `{"engine": "amber_quantum_rings"}`) |
127
+ | `reverse_bits` | `False` | Reverse returned bitstring order if a backend needs it |
128
+ | `verbose` | `False` | Print submission progress and credit cost to stdout |
129
+ | `job_timeout_seconds` | `86400` | Max wait for job completion (1 day) |
130
+
131
+ ## Backend-specific configuration
132
+
133
+ Some backends require extra fields. The free Quantum Rings simulator
134
+ (`qr:gpu-gb10`) requires an `engine`:
135
+
136
+ ```python
137
+ oq = OpenQuantumBackend(
138
+ backend="qr:gpu-gb10",
139
+ shots=1024,
140
+ configuration_data={"engine": "amber_quantum_rings"},
141
+ )
142
+ ```
143
+
144
+ ## Bit ordering
145
+
146
+ Qrisp reads the leftmost character of a bitstring as the most-significant bit.
147
+ The platform's counts are passed through unchanged by default. If a specific
148
+ backend returns counts in the opposite order, construct the backend with
149
+ `reverse_bits=True`.
150
+
151
+ ## License
152
+
153
+ Apache License 2.0
@@ -0,0 +1,134 @@
1
+ # Qrisp-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 backend for the [Qrisp](https://www.qrisp.eu/) quantum programming framework
6
+ that submits circuits to the [Open Quantum](https://www.openquantum.com)
7
+ platform. Write high-level Qrisp programs and run them on real quantum hardware.
8
+
9
+ ## How it works
10
+
11
+ Qrisp already depends on Qiskit and can convert any circuit to a Qiskit circuit
12
+ (`QuantumCircuit.to_qiskit()`). This backend uses that bridge to reuse the Open
13
+ Quantum Qiskit plugin:
14
+
15
+ - `OpenQuantumService` handles authentication and backend discovery.
16
+ - The capability-aware `OpenQuantumBackend` (a Qiskit `BackendV2`) validates each
17
+ circuit against the target device (gate set, coupling map, shot and qubit
18
+ limits).
19
+ - `export_circuit_to_qasm` serialises to OpenQASM.
20
+
21
+ Submission runs through the framework-agnostic `SchedulerClient` in the core
22
+ SDK. No separate QASM serializer or auth code is reimplemented here.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ # Plugin (and core SDK + Qiskit plugin)
28
+ pip install openquantum-sdk-qrisp
29
+
30
+ # or via extras:
31
+ pip install "openquantum-sdk[qrisp]"
32
+ ```
33
+
34
+ This pulls in `qrisp`, `openquantum-sdk`, and `openquantum-sdk-qiskit`.
35
+
36
+ ## Quick Start
37
+
38
+ ```python
39
+ from qrisp import QuantumVariable, h, cx
40
+ from qrisp_openquantum import OpenQuantumBackend
41
+
42
+ oq = OpenQuantumBackend(
43
+ backend="ionq:aria-1",
44
+ shots=1024,
45
+ client_id="your_client_id",
46
+ client_secret="your_client_secret",
47
+ )
48
+
49
+ qv = QuantumVariable(2)
50
+ h(qv[0])
51
+ cx(qv[0], qv[1])
52
+
53
+ print(qv.get_measurement(backend=oq, shots=1024))
54
+ # {'00': 0.5, '11': 0.5}
55
+ ```
56
+
57
+ `get_measurement` returns Qrisp's decoded outcomes as probabilities (its own
58
+ normalization, applied for every backend). To get integer shot counts, run a
59
+ raw `QuantumCircuit` directly:
60
+
61
+ ```python
62
+ from qrisp import QuantumCircuit
63
+
64
+ qc = QuantumCircuit(2)
65
+ qc.h(0)
66
+ qc.cx(0, 1)
67
+ qc.measure([0, 1])
68
+
69
+ counts = qc.run(backend=oq, shots=1024)
70
+ # e.g. {'00': 518, '11': 506}
71
+ ```
72
+
73
+ ## Authentication
74
+
75
+ Resolved in this order:
76
+
77
+ 1. **Pre-built service** — pass `service=OpenQuantumService(...)` for full control
78
+ (saved accounts, custom URLs, keyring).
79
+ 2. **Bearer token** — pass `token=` or set `OPENQUANTUM_TOKEN`.
80
+ 3. **Client credentials** — pass `client_id=` + `client_secret=` or set
81
+ `OPENQUANTUM_CLIENT_ID` + `OPENQUANTUM_CLIENT_SECRET`.
82
+
83
+ | Parameter | Environment Variable | Description |
84
+ |-----------|---------------------|-------------|
85
+ | `token` | `OPENQUANTUM_TOKEN` | Bearer token |
86
+ | `client_id` | `OPENQUANTUM_CLIENT_ID` | OAuth2 client ID |
87
+ | `client_secret` | `OPENQUANTUM_CLIENT_SECRET` | OAuth2 client secret |
88
+ | | `OPENQUANTUM_SDK_KEY` | Path to an SDK-key JSON file (`{client_id, client_secret}`) |
89
+ | `organization_id` | `OPENQUANTUM_ORGANIZATION_ID` | Organization UUID (optional if single org) |
90
+ | `scheduler_url` | `OPENQUANTUM_SCHEDULER_URL` | Scheduler API base, when the argument is omitted |
91
+ | `management_url` | `OPENQUANTUM_MANAGEMENT_URL` | Management API base, when the argument is omitted |
92
+ | `keycloak_base` | `OPENQUANTUM_KEYCLOAK_BASE` | Keycloak base, when the argument is omitted |
93
+ | `realm` | `OPENQUANTUM_KEYCLOAK_REALM` | Keycloak realm, when the argument is omitted |
94
+
95
+ `OpenQuantumService.from_env()` raises `AuthenticationError` when no credential resolves. Circuits are serialized with the public `export_circuit_to_qasm` helper from `openquantum_sdk_qiskit`. The older `OPENQUANTUM_*_BASE` URL names are not read here; those remain an MCP-only fallback.
96
+
97
+ ## Configuration
98
+
99
+ | Parameter | Default | Description |
100
+ |-----------|---------|-------------|
101
+ | `backend` | required | Backend class ID or short_code (e.g. `"ionq:aria-1"`) |
102
+ | `shots` | `1024` | Default shots when a Qrisp call omits them |
103
+ | `job_subcategory_id` | `"oth:oth"` | Workload subcategory (set the most specific match) |
104
+ | `execution_plan` | `"auto"` | `"auto"`, `"public"`, `"private"`, an enum, or a plan UUID. `"auto"` picks the cheapest plan |
105
+ | `queue_priority` | `"auto"` | `"auto"`, `"standard"`, `"priority"`, `"instant"`, an enum, or a priority UUID. `"auto"` picks the cheapest priority |
106
+ | `export_format` | `"qasm3"` | `"qasm2"` or `"qasm3"` |
107
+ | `configuration_data` | `{}` | Backend-specific config passthrough (e.g. `{"engine": "amber_quantum_rings"}`) |
108
+ | `reverse_bits` | `False` | Reverse returned bitstring order if a backend needs it |
109
+ | `verbose` | `False` | Print submission progress and credit cost to stdout |
110
+ | `job_timeout_seconds` | `86400` | Max wait for job completion (1 day) |
111
+
112
+ ## Backend-specific configuration
113
+
114
+ Some backends require extra fields. The free Quantum Rings simulator
115
+ (`qr:gpu-gb10`) requires an `engine`:
116
+
117
+ ```python
118
+ oq = OpenQuantumBackend(
119
+ backend="qr:gpu-gb10",
120
+ shots=1024,
121
+ configuration_data={"engine": "amber_quantum_rings"},
122
+ )
123
+ ```
124
+
125
+ ## Bit ordering
126
+
127
+ Qrisp reads the leftmost character of a bitstring as the most-significant bit.
128
+ The platform's counts are passed through unchanged by default. If a specific
129
+ backend returns counts in the opposite order, construct the backend with
130
+ `reverse_bits=True`.
131
+
132
+ ## License
133
+
134
+ Apache License 2.0
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: openquantum-sdk-qrisp
3
+ Version: 0.5.0
4
+ Summary: Qrisp 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.11
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: openquantum-sdk>=1.0.0
13
+ Requires-Dist: openquantum-sdk-qiskit>=1.0.0
14
+ Requires-Dist: qrisp>=0.9
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
+ # Qrisp-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 backend for the [Qrisp](https://www.qrisp.eu/) quantum programming framework
25
+ that submits circuits to the [Open Quantum](https://www.openquantum.com)
26
+ platform. Write high-level Qrisp programs and run them on real quantum hardware.
27
+
28
+ ## How it works
29
+
30
+ Qrisp already depends on Qiskit and can convert any circuit to a Qiskit circuit
31
+ (`QuantumCircuit.to_qiskit()`). This backend uses that bridge to reuse the Open
32
+ Quantum Qiskit plugin:
33
+
34
+ - `OpenQuantumService` handles authentication and backend discovery.
35
+ - The capability-aware `OpenQuantumBackend` (a Qiskit `BackendV2`) validates each
36
+ circuit against the target device (gate set, coupling map, shot and qubit
37
+ limits).
38
+ - `export_circuit_to_qasm` serialises to OpenQASM.
39
+
40
+ Submission runs through the framework-agnostic `SchedulerClient` in the core
41
+ SDK. No separate QASM serializer or auth code is reimplemented here.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ # Plugin (and core SDK + Qiskit plugin)
47
+ pip install openquantum-sdk-qrisp
48
+
49
+ # or via extras:
50
+ pip install "openquantum-sdk[qrisp]"
51
+ ```
52
+
53
+ This pulls in `qrisp`, `openquantum-sdk`, and `openquantum-sdk-qiskit`.
54
+
55
+ ## Quick Start
56
+
57
+ ```python
58
+ from qrisp import QuantumVariable, h, cx
59
+ from qrisp_openquantum import OpenQuantumBackend
60
+
61
+ oq = OpenQuantumBackend(
62
+ backend="ionq:aria-1",
63
+ shots=1024,
64
+ client_id="your_client_id",
65
+ client_secret="your_client_secret",
66
+ )
67
+
68
+ qv = QuantumVariable(2)
69
+ h(qv[0])
70
+ cx(qv[0], qv[1])
71
+
72
+ print(qv.get_measurement(backend=oq, shots=1024))
73
+ # {'00': 0.5, '11': 0.5}
74
+ ```
75
+
76
+ `get_measurement` returns Qrisp's decoded outcomes as probabilities (its own
77
+ normalization, applied for every backend). To get integer shot counts, run a
78
+ raw `QuantumCircuit` directly:
79
+
80
+ ```python
81
+ from qrisp import QuantumCircuit
82
+
83
+ qc = QuantumCircuit(2)
84
+ qc.h(0)
85
+ qc.cx(0, 1)
86
+ qc.measure([0, 1])
87
+
88
+ counts = qc.run(backend=oq, shots=1024)
89
+ # e.g. {'00': 518, '11': 506}
90
+ ```
91
+
92
+ ## Authentication
93
+
94
+ Resolved in this order:
95
+
96
+ 1. **Pre-built service** — pass `service=OpenQuantumService(...)` for full control
97
+ (saved accounts, custom URLs, keyring).
98
+ 2. **Bearer token** — pass `token=` or set `OPENQUANTUM_TOKEN`.
99
+ 3. **Client credentials** — pass `client_id=` + `client_secret=` or set
100
+ `OPENQUANTUM_CLIENT_ID` + `OPENQUANTUM_CLIENT_SECRET`.
101
+
102
+ | Parameter | Environment Variable | Description |
103
+ |-----------|---------------------|-------------|
104
+ | `token` | `OPENQUANTUM_TOKEN` | Bearer token |
105
+ | `client_id` | `OPENQUANTUM_CLIENT_ID` | OAuth2 client ID |
106
+ | `client_secret` | `OPENQUANTUM_CLIENT_SECRET` | OAuth2 client secret |
107
+ | | `OPENQUANTUM_SDK_KEY` | Path to an SDK-key JSON file (`{client_id, client_secret}`) |
108
+ | `organization_id` | `OPENQUANTUM_ORGANIZATION_ID` | Organization UUID (optional if single org) |
109
+ | `scheduler_url` | `OPENQUANTUM_SCHEDULER_URL` | Scheduler API base, when the argument is omitted |
110
+ | `management_url` | `OPENQUANTUM_MANAGEMENT_URL` | Management API base, when the argument is omitted |
111
+ | `keycloak_base` | `OPENQUANTUM_KEYCLOAK_BASE` | Keycloak base, when the argument is omitted |
112
+ | `realm` | `OPENQUANTUM_KEYCLOAK_REALM` | Keycloak realm, when the argument is omitted |
113
+
114
+ `OpenQuantumService.from_env()` raises `AuthenticationError` when no credential resolves. Circuits are serialized with the public `export_circuit_to_qasm` helper from `openquantum_sdk_qiskit`. The older `OPENQUANTUM_*_BASE` URL names are not read here; those remain an MCP-only fallback.
115
+
116
+ ## Configuration
117
+
118
+ | Parameter | Default | Description |
119
+ |-----------|---------|-------------|
120
+ | `backend` | required | Backend class ID or short_code (e.g. `"ionq:aria-1"`) |
121
+ | `shots` | `1024` | Default shots when a Qrisp call omits them |
122
+ | `job_subcategory_id` | `"oth:oth"` | Workload subcategory (set the most specific match) |
123
+ | `execution_plan` | `"auto"` | `"auto"`, `"public"`, `"private"`, an enum, or a plan UUID. `"auto"` picks the cheapest plan |
124
+ | `queue_priority` | `"auto"` | `"auto"`, `"standard"`, `"priority"`, `"instant"`, an enum, or a priority UUID. `"auto"` picks the cheapest priority |
125
+ | `export_format` | `"qasm3"` | `"qasm2"` or `"qasm3"` |
126
+ | `configuration_data` | `{}` | Backend-specific config passthrough (e.g. `{"engine": "amber_quantum_rings"}`) |
127
+ | `reverse_bits` | `False` | Reverse returned bitstring order if a backend needs it |
128
+ | `verbose` | `False` | Print submission progress and credit cost to stdout |
129
+ | `job_timeout_seconds` | `86400` | Max wait for job completion (1 day) |
130
+
131
+ ## Backend-specific configuration
132
+
133
+ Some backends require extra fields. The free Quantum Rings simulator
134
+ (`qr:gpu-gb10`) requires an `engine`:
135
+
136
+ ```python
137
+ oq = OpenQuantumBackend(
138
+ backend="qr:gpu-gb10",
139
+ shots=1024,
140
+ configuration_data={"engine": "amber_quantum_rings"},
141
+ )
142
+ ```
143
+
144
+ ## Bit ordering
145
+
146
+ Qrisp reads the leftmost character of a bitstring as the most-significant bit.
147
+ The platform's counts are passed through unchanged by default. If a specific
148
+ backend returns counts in the opposite order, construct the backend with
149
+ `reverse_bits=True`.
150
+
151
+ ## License
152
+
153
+ Apache License 2.0
@@ -0,0 +1,15 @@
1
+ MANIFEST.in
2
+ README.md
3
+ pyproject.toml
4
+ openquantum_sdk_qrisp.egg-info/PKG-INFO
5
+ openquantum_sdk_qrisp.egg-info/SOURCES.txt
6
+ openquantum_sdk_qrisp.egg-info/dependency_links.txt
7
+ openquantum_sdk_qrisp.egg-info/requires.txt
8
+ openquantum_sdk_qrisp.egg-info/top_level.txt
9
+ qrisp_openquantum/__init__.py
10
+ qrisp_openquantum/backend.py
11
+ qrisp_openquantum/job.py
12
+ tests/__init__.py
13
+ tests/conftest.py
14
+ tests/test_backend.py
15
+ tests/test_integration.py
@@ -0,0 +1,8 @@
1
+ openquantum-sdk>=1.0.0
2
+ openquantum-sdk-qiskit>=1.0.0
3
+ qrisp>=0.9
4
+
5
+ [dev]
6
+ pytest>=7.0
7
+ pytest-cov
8
+ pytest-mock>=3.10
@@ -0,0 +1,2 @@
1
+ dist
2
+ qrisp_openquantum
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "openquantum-sdk-qrisp"
7
+ version = "0.5.0"
8
+ description = "Qrisp Python SDK for Open Quantum"
9
+ readme = "README.md"
10
+ license = {text = "Apache-2.0"}
11
+ requires-python = ">=3.11"
12
+ dependencies = [
13
+ "openquantum-sdk>=1.0.0",
14
+ "openquantum-sdk-qiskit>=1.0.0",
15
+ "qrisp>=0.9",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Programming Language :: Python :: 3",
20
+ "License :: OSI Approved :: Apache Software License",
21
+ "Operating System :: OS Independent",
22
+ ]
23
+
24
+ # Qrisp has no entry-point/plugin discovery mechanism. Backends are plain
25
+ # objects passed at call time, e.g. ``qv.get_measurement(backend=...)``. The
26
+ # user imports OpenQuantumBackend and hands it to Qrisp directly.
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=7.0",
31
+ "pytest-cov",
32
+ "pytest-mock>=3.10",
33
+ ]
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["."]
37
+ exclude = ["tests*"]
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
41
+ pythonpath = [".", "../.."]
@@ -0,0 +1,7 @@
1
+ """Qrisp backend for the Open Quantum quantum computing platform."""
2
+
3
+ from qrisp_openquantum.backend import OpenQuantumBackend
4
+ from qrisp_openquantum.job import OpenQuantumJob
5
+
6
+ __version__ = "0.5.0"
7
+ __all__ = ["OpenQuantumBackend", "OpenQuantumJob"]
@@ -0,0 +1,310 @@
1
+ """Open Quantum backend for the Qrisp quantum programming framework.
2
+
3
+ Qrisp circuits are converted to Qiskit circuits via ``QuantumCircuit.to_qiskit()``
4
+ (Qrisp already depends on Qiskit), so this backend reuses the Open Quantum Qiskit
5
+ plugin for the Qiskit-specific work:
6
+
7
+ * ``OpenQuantumService`` for authentication and backend discovery,
8
+ * the capability-aware ``OpenQuantumBackend`` (a Qiskit ``BackendV2``) for
9
+ circuit validation, and
10
+ * ``export_circuit_to_qasm`` for serialisation.
11
+
12
+ Submission itself goes through the framework-agnostic
13
+ ``openquantum_sdk.clients.SchedulerClient``. The result is a ``{bitstring: count}``
14
+ dict per circuit, which is exactly the contract Qrisp's result layer expects.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ from typing import Any, Dict, List, Optional, Union
21
+
22
+ from qiskit import QuantumCircuit as _QiskitCircuit
23
+ from qrisp.interface import Backend as QrispBackend
24
+
25
+ from openquantum_sdk.clients import JobSubmissionConfig
26
+ from openquantum_sdk.exceptions import JobError
27
+ from openquantum_sdk_qiskit import OpenQuantumService, export_circuit_to_qasm
28
+
29
+ from qrisp_openquantum.job import OpenQuantumJob
30
+
31
+
32
+ class OpenQuantumBackend(QrispBackend):
33
+ """Qrisp backend that executes circuits on the Open Quantum platform.
34
+
35
+ Pass an instance to any Qrisp execution call, e.g.
36
+ ``qv.get_measurement(backend=oq, shots=1024)`` or
37
+ ``qc.run(backend=oq, shots=1024)``.
38
+
39
+ Authentication mirrors the other Open Quantum plugins and is resolved in
40
+ this order:
41
+
42
+ 1. A pre-built :class:`openquantum_sdk_qiskit.OpenQuantumService` passed as
43
+ ``service`` (full control over auth, URLs, saved accounts).
44
+ 2. A bearer ``token`` (falls back to ``OPENQUANTUM_TOKEN``).
45
+ 3. ``client_id`` + ``client_secret`` for the OAuth2 client-credentials flow
46
+ (fall back to ``OPENQUANTUM_CLIENT_ID`` / ``OPENQUANTUM_CLIENT_SECRET``).
47
+
48
+ Args:
49
+ backend: Backend class ID or short_code (e.g. ``"ionq:aria-1"``).
50
+ shots: Default shot count used when a Qrisp call does not specify one.
51
+ service: Pre-built ``OpenQuantumService`` instance.
52
+ token: Bearer token for the API. Falls back to ``OPENQUANTUM_TOKEN``.
53
+ client_id: OAuth2 client ID. Falls back to ``OPENQUANTUM_CLIENT_ID``.
54
+ client_secret: OAuth2 client secret. Falls back to
55
+ ``OPENQUANTUM_CLIENT_SECRET``.
56
+ organization_id: Organization UUID. Falls back to
57
+ ``OPENQUANTUM_ORGANIZATION_ID``.
58
+ scheduler_url: Scheduler API base URL.
59
+ management_url: Management API base URL.
60
+ keycloak_base: Keycloak base URL for OAuth2.
61
+ realm: Keycloak realm name.
62
+ job_subcategory_id: Job subcategory ID or short_code (default
63
+ ``"oth:oth"``).
64
+ execution_plan: ``"auto"`` (default), ``"public"``, or ``"private"``.
65
+ queue_priority: ``"auto"`` (default), ``"standard"``, ``"priority"``,
66
+ or ``"instant"``.
67
+ export_format: Circuit serialisation format, ``"qasm2"`` or ``"qasm3"``
68
+ (default).
69
+ configuration_data: Backend-specific configuration passthrough (e.g.
70
+ ``{"engine": "amber_quantum_rings"}`` for ``qr:gpu-gb10``). ``shots``
71
+ is injected automatically from the per-run shot count.
72
+ reverse_bits: Reverse the bit order of returned bitstrings. The default
73
+ (``False``) passes the platform's bit order straight through to
74
+ Qrisp, which reads the leftmost character as the most-significant
75
+ bit. Set ``True`` only if a backend's counts come back in the
76
+ opposite order.
77
+ name: Optional Qrisp backend name (defaults to ``"openquantum"``).
78
+ verbose: Print submission progress and the credit cost to stdout.
79
+ Defaults to ``False``, matching
80
+ ``JobSubmissionConfig`` and the other plugins. Set ``True``
81
+ for step-by-step progress in a notebook or script.
82
+ job_timeout_seconds: Maximum seconds to wait for a job to complete
83
+ (default ``86400``, one day).
84
+ """
85
+
86
+ def __init__(
87
+ self,
88
+ backend: str,
89
+ *,
90
+ shots: int = 1024,
91
+ service: Optional[Any] = None,
92
+ token: Optional[str] = None,
93
+ client_id: Optional[str] = None,
94
+ client_secret: Optional[str] = None,
95
+ organization_id: Optional[str] = None,
96
+ scheduler_url: Optional[str] = None,
97
+ management_url: Optional[str] = None,
98
+ keycloak_base: Optional[str] = None,
99
+ realm: Optional[str] = None,
100
+ job_subcategory_id: str = "oth:oth",
101
+ execution_plan: str = "auto",
102
+ queue_priority: str = "auto",
103
+ export_format: str = "qasm3",
104
+ configuration_data: Optional[Dict[str, Any]] = None,
105
+ reverse_bits: bool = False,
106
+ name: Optional[str] = None,
107
+ verbose: bool = False,
108
+ job_timeout_seconds: int = 86_400,
109
+ ) -> None:
110
+ if not backend:
111
+ raise ValueError(
112
+ "OpenQuantumBackend requires a backend "
113
+ "(e.g., backend='ionq:aria-1')."
114
+ )
115
+ fmt = (export_format or "qasm3").lower()
116
+ if fmt not in {"qasm2", "qasm3"}:
117
+ raise ValueError("export_format must be 'qasm2' or 'qasm3'")
118
+
119
+ super().__init__(name=name or "openquantum", options={"shots": int(shots)})
120
+
121
+ self._backend_class_id = backend
122
+ self._configuration_data = dict(configuration_data or {})
123
+ self._organization_id = organization_id or os.environ.get(
124
+ "OPENQUANTUM_ORGANIZATION_ID"
125
+ )
126
+ self._job_subcategory_id = job_subcategory_id
127
+ self._execution_plan = execution_plan
128
+ self._queue_priority = queue_priority
129
+ self._export_format = fmt
130
+ self._reverse_bits = reverse_bits
131
+ self._verbose = verbose
132
+ self._job_timeout_seconds = int(job_timeout_seconds)
133
+
134
+ self._service = service or OpenQuantumService.from_env(
135
+ token=token,
136
+ client_id=client_id,
137
+ client_secret=client_secret,
138
+ scheduler_url=scheduler_url,
139
+ management_url=management_url,
140
+ keycloak_base=keycloak_base,
141
+ realm=realm,
142
+ )
143
+ self._scheduler = self._service.scheduler
144
+ # A capability-aware Qiskit BackendV2 used purely for client-side
145
+ # validation (gate set, coupling map, shot/qubit limits). It fetches
146
+ # constraint_data from the platform at construction time.
147
+ self._oq_backend = self._service.return_backend(
148
+ backend, export_format=fmt
149
+ )
150
+
151
+ @classmethod
152
+ def _default_options(cls):
153
+ return {"shots": 1024}
154
+
155
+ @property
156
+ def backend_class_id(self) -> str:
157
+ """The Open Quantum backend class ID or short_code targeted by this backend."""
158
+ return self._backend_class_id
159
+
160
+ @property
161
+ def backend(self) -> str:
162
+ """Alias for :attr:`backend_class_id` (matches the other plugins)."""
163
+ return self._backend_class_id
164
+
165
+ def run_async(
166
+ self,
167
+ circuits,
168
+ shots: Union[int, List[int], None] = None,
169
+ ) -> OpenQuantumJob:
170
+ """Submit one or more Qrisp circuits and return a completed job.
171
+
172
+ Each circuit is converted to a Qiskit circuit, validated against the
173
+ backend's capabilities, serialised to QASM, and submitted to the
174
+ platform synchronously.
175
+
176
+ Args:
177
+ circuits: A single Qrisp ``QuantumCircuit`` or a sequence of them.
178
+ shots: Shot count. An ``int`` applies to every circuit; a
179
+ ``list[int]`` sets per-circuit shots; ``None`` uses the
180
+ backend's default.
181
+
182
+ Returns:
183
+ An :class:`OpenQuantumJob` already in the ``DONE`` state.
184
+ """
185
+ batch = isinstance(circuits, (list, tuple))
186
+ circ_list = list(circuits) if batch else [circuits]
187
+
188
+ shots_list = self._resolve_shots(shots, len(circ_list))
189
+
190
+ counts_list: List[Dict[str, int]] = []
191
+ last_job_id: Optional[str] = None
192
+ for circ, circ_shots in zip(circ_list, shots_list):
193
+ counts, job_id = self._run_single(circ, circ_shots)
194
+ counts_list.append(counts)
195
+ last_job_id = job_id
196
+
197
+ return OpenQuantumJob(self, counts_list, job_id=last_job_id)
198
+
199
+ def _resolve_shots(
200
+ self, shots: Union[int, List[int], None], n_circuits: int
201
+ ) -> List[int]:
202
+ """Normalise ``shots`` into one positive int per circuit."""
203
+ if shots is None:
204
+ shots = self.options.get("shots")
205
+ if isinstance(shots, (list, tuple)):
206
+ if len(shots) != n_circuits:
207
+ raise ValueError(
208
+ f"Received {len(shots)} shot values for {n_circuits} circuits."
209
+ )
210
+ return [int(s) for s in shots]
211
+ return [int(shots)] * n_circuits
212
+
213
+ @staticmethod
214
+ def _flatten_registers(qiskit_circuit):
215
+ """Rebuild a Qiskit circuit onto single quantum/classical registers.
216
+
217
+ Qrisp's ``to_qiskit()`` emits one register per qubit, but the platform
218
+ precompiler allows at most one qubit register. Composing onto a single
219
+ ``QuantumCircuit(n, m)`` remaps every qubit/clbit by position.
220
+ """
221
+ if len(qiskit_circuit.qregs) <= 1 and len(qiskit_circuit.cregs) <= 1:
222
+ return qiskit_circuit
223
+
224
+ flat = _QiskitCircuit(
225
+ qiskit_circuit.num_qubits,
226
+ qiskit_circuit.num_clbits,
227
+ name=qiskit_circuit.name,
228
+ )
229
+ flat.compose(
230
+ qiskit_circuit,
231
+ qubits=range(qiskit_circuit.num_qubits),
232
+ clbits=range(qiskit_circuit.num_clbits),
233
+ inplace=True,
234
+ )
235
+ return flat
236
+
237
+ def _transpile_for_backend(self, qiskit_circuit):
238
+ """Transpile a Qiskit circuit to the target device's native gate set.
239
+
240
+ Delegates to the Qiskit plugin backend's ``transpile`` wrapper (a thin
241
+ wrapper around ``qiskit.transpile`` bound to the device Target), which
242
+ carries the basis gates, coupling map, and qubit count. Transpilation
243
+ keeps each measurement writing to its original classical bit, so the
244
+ bitstring order returned by the platform (indexed by clbit) is
245
+ preserved; only the physical qubit placement changes.
246
+
247
+ Falls back to the untouched circuit when the backend does not expose a
248
+ transpile entry point (e.g. a minimal stub in tests).
249
+ """
250
+ transpile = getattr(self._oq_backend, "transpile", None)
251
+ if not callable(transpile):
252
+ return qiskit_circuit
253
+ # Use Qiskit's default optimization level, matching the Qiskit plugin.
254
+ # (An earlier optimization_level=1 pin worked around a VF2Layout crash;
255
+ # the real cause was a zero-qubit barrier in the device Target, now
256
+ # fixed in the Qiskit plugin, so the pin is no longer needed.)
257
+ return transpile(qiskit_circuit)
258
+
259
+ def _run_single(self, circ, shots: int):
260
+ """Serialise, submit, and collect counts for one circuit."""
261
+ qiskit_circuit = self._flatten_registers(circ.to_qiskit())
262
+
263
+ # Decompose to the device's native gate set before validation. Qrisp's
264
+ # high-level types emit composite gates (e.g. a 'QFT no swap_dg' from
265
+ # QuantumFloat arithmetic) that the capability-aware validator rejects
266
+ # on hardware-class backends. Transpiling against the Qiskit plugin's
267
+ # Target resolves basis translation, layout, and routing on the client,
268
+ # which is exactly where the platform expects that work to happen.
269
+ qiskit_circuit = self._transpile_for_backend(qiskit_circuit)
270
+
271
+ # Reuse the Qiskit plugin's capability-aware validation when available.
272
+ validate = getattr(self._oq_backend, "validate_circuit", None)
273
+ if callable(validate):
274
+ validate(qiskit_circuit, shots=shots)
275
+
276
+ payload = export_circuit_to_qasm(qiskit_circuit, self._export_format)
277
+
278
+ configuration_data = dict(self._configuration_data)
279
+ configuration_data["shots"] = int(shots)
280
+ config = JobSubmissionConfig(
281
+ backend_class_id=self._backend_class_id,
282
+ name="Qrisp Job",
283
+ job_subcategory_id=self._job_subcategory_id,
284
+ configuration_data=configuration_data,
285
+ organization_id=self._organization_id,
286
+ execution_plan=self._execution_plan,
287
+ queue_priority=self._queue_priority,
288
+ submitted_with="qrisp",
289
+ verbose=self._verbose,
290
+ job_timeout_seconds=self._job_timeout_seconds,
291
+ )
292
+ job = self._scheduler.submit_job(config, file_content=payload)
293
+ raw = self._scheduler.download_job_output(job)
294
+ counts = self._extract_counts(raw)
295
+ if self._reverse_bits:
296
+ counts = {k.replace(" ", "")[::-1]: v for k, v in counts.items()}
297
+ return counts, getattr(job, "id", None)
298
+
299
+ @staticmethod
300
+ def _extract_counts(raw: Any) -> Dict[str, int]:
301
+ """Pull the ``{bitstring: count}`` mapping out of a job output payload."""
302
+ if isinstance(raw, dict):
303
+ if "counts" in raw:
304
+ return raw["counts"]
305
+ if "measurement_counts" in raw:
306
+ return raw["measurement_counts"]
307
+ return raw
308
+ raise JobError(
309
+ f"Unexpected job output format: {type(raw).__name__}"
310
+ )
@@ -0,0 +1,52 @@
1
+ """Job handle returned by the Open Quantum Qrisp backend.
2
+
3
+ Open Quantum job submission is synchronous: :meth:`SchedulerClient.submit_job`
4
+ uploads the circuit, approves the quote, creates the job, and polls until it
5
+ reaches a terminal state before returning. The Qrisp backend therefore does
6
+ all of its work eagerly inside ``run_async`` and hands the finished counts to
7
+ this job, which is already in the ``DONE`` state by the time the caller sees
8
+ it.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Sequence
14
+
15
+ from qrisp.interface import Job, JobResult, JobStatus
16
+
17
+
18
+ class OpenQuantumJob(Job):
19
+ """A completed Open Quantum job wrapping one counts dict per circuit.
20
+
21
+ Args:
22
+ backend: The :class:`OpenQuantumBackend` that produced this job.
23
+ counts: One ``{bitstring: count}`` dict per submitted circuit, in
24
+ input order.
25
+ job_id: Optional platform job identifier (the id of the last
26
+ submitted circuit, for reference).
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ backend,
32
+ counts: Sequence[dict],
33
+ job_id: str | None = None,
34
+ **kwargs,
35
+ ) -> None:
36
+ super().__init__(backend, job_id=job_id, **kwargs)
37
+ self._counts = list(counts)
38
+ # Submission is synchronous, so the job is already finished.
39
+ self._last_known_status = JobStatus.DONE
40
+
41
+ def result(self, timeout: float | None = None) -> JobResult:
42
+ """Return the measurement results (job is already terminal)."""
43
+ self._raise_for_status()
44
+ return JobResult(self._counts)
45
+
46
+ def cancel(self) -> bool:
47
+ """Jobs are already terminal when returned, so there is nothing to cancel."""
48
+ return False
49
+
50
+ def status(self) -> JobStatus:
51
+ """Return the cached (terminal) status."""
52
+ return self._last_known_status
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,71 @@
1
+ """Shared test fixtures for the OpenQuantum Qrisp backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import MagicMock
6
+
7
+ import pytest
8
+
9
+ from openquantum_sdk.clients import SchedulerClient
10
+ from openquantum_sdk.models import JobRead
11
+
12
+
13
+ SAMPLE_COUNTS = {"00": 512, "11": 512}
14
+
15
+
16
+ @pytest.fixture
17
+ def mock_scheduler() -> MagicMock:
18
+ """A mock SchedulerClient whose submit/download pipeline is stubbed."""
19
+ scheduler = MagicMock(spec=SchedulerClient)
20
+
21
+ mock_job = MagicMock(spec=JobRead)
22
+ mock_job.id = "job-123"
23
+ mock_job.status = "Completed"
24
+ mock_job.output_data_url = "https://example.com/output.json"
25
+
26
+ scheduler.submit_job.return_value = mock_job
27
+ scheduler.download_job_output.return_value = {"counts": dict(SAMPLE_COUNTS)}
28
+ return scheduler
29
+
30
+
31
+ @pytest.fixture
32
+ def mock_oq_backend() -> MagicMock:
33
+ """A mock capability-aware Qiskit backend (validation no-op).
34
+
35
+ ``transpile`` is the identity so submission/result-mapping tests keep
36
+ seeing the real circuit built by the ``to_qiskit`` stub.
37
+ """
38
+ oq_backend = MagicMock()
39
+ oq_backend.validate_circuit.return_value = None
40
+ oq_backend.transpile.side_effect = lambda circuit, **kwargs: circuit
41
+ return oq_backend
42
+
43
+
44
+ @pytest.fixture
45
+ def mock_service(mock_scheduler, mock_oq_backend) -> MagicMock:
46
+ """A mock OpenQuantumService exposing the scheduler and a backend factory."""
47
+ service = MagicMock()
48
+ service.scheduler = mock_scheduler
49
+ service.return_backend.return_value = mock_oq_backend
50
+ return service
51
+
52
+
53
+ @pytest.fixture
54
+ def two_qubit_circuit():
55
+ """A Qrisp circuit stand-in whose ``to_qiskit`` yields a real Bell circuit.
56
+
57
+ Using a stub keeps these unit tests focused on the plugin's submission and
58
+ result-mapping logic rather than Qrisp's circuit-construction API; the
59
+ live round-trip is covered by the integration test.
60
+ """
61
+ from qiskit import QuantumCircuit as QiskitCircuit
62
+
63
+ qc = QiskitCircuit(2, 2)
64
+ qc.h(0)
65
+ qc.cx(0, 1)
66
+ qc.measure(0, 0)
67
+ qc.measure(1, 1)
68
+
69
+ circuit = MagicMock()
70
+ circuit.to_qiskit.return_value = qc
71
+ return circuit
@@ -0,0 +1,262 @@
1
+ """Unit tests for the OpenQuantum Qrisp backend (mocked platform)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import MagicMock
6
+
7
+ import pytest
8
+
9
+ from openquantum_sdk.exceptions import AuthenticationError, JobError
10
+
11
+ from qrisp_openquantum import OpenQuantumBackend, OpenQuantumJob
12
+
13
+
14
+ def _make_backend(mock_service, **kwargs):
15
+ return OpenQuantumBackend(backend="ionq:aria-1", service=mock_service, **kwargs)
16
+
17
+
18
+ def test_requires_backend(mock_service):
19
+ with pytest.raises(ValueError):
20
+ OpenQuantumBackend(backend="", service=mock_service)
21
+
22
+
23
+ def test_rejects_bad_export_format(mock_service):
24
+ with pytest.raises(ValueError):
25
+ _make_backend(mock_service, export_format="qasm9")
26
+
27
+
28
+ def test_run_async_returns_done_job(mock_service, two_qubit_circuit):
29
+ backend = _make_backend(mock_service)
30
+ job = backend.run_async([two_qubit_circuit], shots=1024)
31
+
32
+ assert isinstance(job, OpenQuantumJob)
33
+ assert job.done()
34
+ assert job.result().all_counts == [{"00": 512, "11": 512}]
35
+
36
+
37
+ def test_submission_metadata_and_shots(mock_service, two_qubit_circuit):
38
+ backend = _make_backend(
39
+ mock_service,
40
+ job_subcategory_id="ml:qcl",
41
+ execution_plan="private",
42
+ queue_priority="priority",
43
+ )
44
+ backend.run_async([two_qubit_circuit], shots=2048)
45
+
46
+ config = mock_service.scheduler.submit_job.call_args.args[0]
47
+ assert config.backend_class_id == "ionq:aria-1"
48
+ assert config.job_subcategory_id == "ml:qcl"
49
+ assert config.execution_plan == "private"
50
+ assert config.queue_priority == "priority"
51
+ assert config.submitted_with == "qrisp"
52
+ assert config.configuration_data["shots"] == 2048
53
+ assert config.verbose is False
54
+ assert config.job_timeout_seconds == 86_400
55
+
56
+
57
+ def test_default_shots_used_when_unspecified(mock_service, two_qubit_circuit):
58
+ backend = _make_backend(mock_service, shots=256)
59
+ backend.run_async([two_qubit_circuit])
60
+ config = mock_service.scheduler.submit_job.call_args.args[0]
61
+ assert config.configuration_data["shots"] == 256
62
+
63
+
64
+ def test_circuit_is_validated(mock_service, mock_oq_backend, two_qubit_circuit):
65
+ backend = _make_backend(mock_service)
66
+ backend.run_async([two_qubit_circuit], shots=100)
67
+ mock_oq_backend.validate_circuit.assert_called_once()
68
+
69
+
70
+ def test_composite_gate_transpiled_before_validation(mock_service, mock_oq_backend):
71
+ """Regression for #453.
72
+
73
+ Qrisp's high-level types emit composite gates (e.g. a 'QFT no swap_dg'
74
+ from QuantumFloat arithmetic). The backend must transpile to the device
75
+ basis before the capability validator runs, otherwise submission fails
76
+ client-side with "Custom gate ... is not supported".
77
+ """
78
+ from qiskit import QuantumCircuit as QiskitCircuit
79
+
80
+ raw = QiskitCircuit(2, 2)
81
+ block = QiskitCircuit(2, name="QFT no swap_dg")
82
+ block.h(0)
83
+ block.cp(0.5, 0, 1)
84
+ raw.append(block.to_gate(), [0, 1])
85
+ raw.measure([0, 1], [0, 1])
86
+
87
+ circuit = MagicMock()
88
+ circuit.to_qiskit.return_value = raw
89
+
90
+ # Emulate the Qiskit plugin: the validator rejects the composite gate,
91
+ # transpile decomposes it to in-basis ops.
92
+ def _reject_composite(circ, shots=None):
93
+ for instruction in circ.data:
94
+ if instruction.operation.name == "QFT no swap_dg":
95
+ raise ValueError(
96
+ "Custom gate 'QFT no swap_dg' is not supported by backend"
97
+ )
98
+
99
+ mock_oq_backend.validate_circuit.side_effect = _reject_composite
100
+ mock_oq_backend.transpile.side_effect = lambda circ, **kwargs: circ.decompose()
101
+
102
+ backend = _make_backend(mock_service)
103
+ job = backend.run_async([circuit], shots=100) # must not raise
104
+ assert job.done()
105
+
106
+ # Without the transpile step the same circuit is rejected, confirming the
107
+ # fix is what lets it through.
108
+ mock_oq_backend.transpile.side_effect = lambda circ, **kwargs: circ
109
+ with pytest.raises(ValueError, match="QFT no swap_dg"):
110
+ backend.run_async([circuit], shots=100)
111
+
112
+
113
+ def test_batch_submits_each_circuit(mock_service, two_qubit_circuit):
114
+ backend = _make_backend(mock_service)
115
+ job = backend.run_async([two_qubit_circuit, two_qubit_circuit], shots=100)
116
+ assert mock_service.scheduler.submit_job.call_count == 2
117
+ assert job.result().num_circuits == 2
118
+
119
+
120
+ def test_per_circuit_shots_length_mismatch(mock_service, two_qubit_circuit):
121
+ backend = _make_backend(mock_service)
122
+ with pytest.raises(ValueError):
123
+ backend.run_async([two_qubit_circuit], shots=[100, 200])
124
+
125
+
126
+ def test_reverse_bits(mock_service, two_qubit_circuit):
127
+ mock_service.scheduler.download_job_output.return_value = {"counts": {"01": 10}}
128
+ backend = _make_backend(mock_service, reverse_bits=True)
129
+ job = backend.run_async([two_qubit_circuit], shots=10)
130
+ assert job.result().get_counts() == {"10": 10}
131
+
132
+
133
+ def test_raw_counts_without_counts_key(mock_service, two_qubit_circuit):
134
+ mock_service.scheduler.download_job_output.return_value = {"00": 5, "11": 5}
135
+ backend = _make_backend(mock_service)
136
+ job = backend.run_async([two_qubit_circuit], shots=10)
137
+ assert job.result().get_counts() == {"00": 5, "11": 5}
138
+
139
+
140
+ def test_configuration_data_passthrough(mock_service, two_qubit_circuit):
141
+ backend = _make_backend(mock_service, configuration_data={"engine": "amber_quantum_rings"})
142
+ backend.run_async([two_qubit_circuit], shots=100)
143
+ config = mock_service.scheduler.submit_job.call_args.args[0]
144
+ assert config.configuration_data["engine"] == "amber_quantum_rings"
145
+ assert config.configuration_data["shots"] == 100
146
+
147
+
148
+ def test_flatten_registers_collapses_multiple_registers():
149
+ """Qrisp emits one register per qubit; the platform needs a single qreg."""
150
+ from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
151
+
152
+ qc = QuantumCircuit(QuantumRegister(1, "a"), QuantumRegister(1, "b"),
153
+ ClassicalRegister(1, "ca"), ClassicalRegister(1, "cb"))
154
+ qc.h(0)
155
+ qc.cx(0, 1)
156
+ qc.measure(0, 0)
157
+ qc.measure(1, 1)
158
+ assert len(qc.qregs) == 2
159
+
160
+ flat = OpenQuantumBackend._flatten_registers(qc)
161
+ assert len(flat.qregs) == 1
162
+ assert len(flat.cregs) == 1
163
+ assert flat.num_qubits == 2 and flat.num_clbits == 2
164
+
165
+
166
+ def test_flatten_registers_noop_for_single_register():
167
+ from qiskit import QuantumCircuit
168
+
169
+ qc = QuantumCircuit(2, 2)
170
+ qc.h(0)
171
+ assert OpenQuantumBackend._flatten_registers(qc) is qc
172
+
173
+
174
+ def test_measurement_counts_key(mock_service, two_qubit_circuit):
175
+ mock_service.scheduler.download_job_output.return_value = {
176
+ "measurement_counts": {"00": 7, "11": 3}
177
+ }
178
+ backend = _make_backend(mock_service)
179
+ job = backend.run_async([two_qubit_circuit], shots=10)
180
+ assert job.result().get_counts() == {"00": 7, "11": 3}
181
+
182
+
183
+ def test_unexpected_output_format_raises(mock_service, two_qubit_circuit):
184
+ mock_service.scheduler.download_job_output.return_value = ["not", "a", "dict"]
185
+ backend = _make_backend(mock_service)
186
+ with pytest.raises(JobError, match="Unexpected job output format"):
187
+ backend.run_async([two_qubit_circuit], shots=10)
188
+
189
+
190
+ def test_job_timeout_seconds_passthrough(mock_service, two_qubit_circuit):
191
+ backend = _make_backend(mock_service, job_timeout_seconds=600, verbose=True)
192
+ backend.run_async([two_qubit_circuit], shots=10)
193
+ config = mock_service.scheduler.submit_job.call_args.args[0]
194
+ assert config.job_timeout_seconds == 600
195
+ assert config.verbose is True
196
+
197
+
198
+ def test_backend_property_alias(mock_service):
199
+ backend = _make_backend(mock_service)
200
+ assert backend.backend == "ionq:aria-1"
201
+ assert backend.backend_class_id == "ionq:aria-1"
202
+
203
+
204
+ def test_auth_resolution_via_env(monkeypatch, two_qubit_circuit):
205
+ """With no service passed, the backend resolves creds through the core path."""
206
+ import qrisp_openquantum.backend as backend_mod
207
+
208
+ captured = {}
209
+ _Orig = backend_mod.OpenQuantumService
210
+
211
+ class _FakeService(_Orig):
212
+ def __init__(self, **kwargs):
213
+ captured.update(kwargs)
214
+ self.scheduler = _make_fake_scheduler()
215
+
216
+ def return_backend(self, *args, **kwargs):
217
+ oq = MagicMock()
218
+ oq.validate_circuit.return_value = None
219
+ oq.transpile.side_effect = lambda circuit, **kw: circuit
220
+ return oq
221
+
222
+ monkeypatch.setattr(backend_mod, "OpenQuantumService", _FakeService)
223
+ monkeypatch.delenv("OPENQUANTUM_TOKEN", raising=False)
224
+ monkeypatch.delenv("OPENQUANTUM_SDK_KEY", raising=False)
225
+ monkeypatch.setenv("OPENQUANTUM_CLIENT_ID", "cid")
226
+ monkeypatch.setenv("OPENQUANTUM_CLIENT_SECRET", "secret")
227
+
228
+ backend = OpenQuantumBackend(backend="ionq:aria-1")
229
+ job = backend.run_async([two_qubit_circuit], shots=10)
230
+ assert job.done()
231
+ assert captured["creds"].client_id == "cid"
232
+ assert captured["token"] is None
233
+
234
+
235
+ def test_auth_missing_raises(monkeypatch):
236
+ monkeypatch.delenv("OPENQUANTUM_TOKEN", raising=False)
237
+ monkeypatch.delenv("OPENQUANTUM_CLIENT_ID", raising=False)
238
+ monkeypatch.delenv("OPENQUANTUM_CLIENT_SECRET", raising=False)
239
+ monkeypatch.delenv("OPENQUANTUM_SDK_KEY", raising=False)
240
+ with pytest.raises(AuthenticationError):
241
+ OpenQuantumBackend(backend="ionq:aria-1")
242
+
243
+
244
+ def test_token_and_credentials_raises(monkeypatch):
245
+ monkeypatch.delenv("OPENQUANTUM_TOKEN", raising=False)
246
+ monkeypatch.delenv("OPENQUANTUM_CLIENT_ID", raising=False)
247
+ monkeypatch.delenv("OPENQUANTUM_CLIENT_SECRET", raising=False)
248
+ monkeypatch.delenv("OPENQUANTUM_SDK_KEY", raising=False)
249
+ with pytest.raises(ValueError, match="not both"):
250
+ OpenQuantumBackend(
251
+ backend="ionq:aria-1",
252
+ token="tok",
253
+ client_id="cid",
254
+ client_secret="secret",
255
+ )
256
+
257
+
258
+ def _make_fake_scheduler():
259
+ scheduler = MagicMock()
260
+ scheduler.submit_job.return_value = MagicMock(id="job-1")
261
+ scheduler.download_job_output.return_value = {"counts": {"00": 512, "11": 512}}
262
+ return scheduler
@@ -0,0 +1,100 @@
1
+ """Opt-in live integration test for the OpenQuantum Qrisp backend.
2
+
3
+ Runs real Qrisp programs against the platform. Skipped unless credentials and a
4
+ target backend are provided via environment variables:
5
+
6
+ OPENQUANTUM_TOKEN (or OPENQUANTUM_CLIENT_ID + OPENQUANTUM_CLIENT_SECRET)
7
+ OPENQUANTUM_TEST_BACKEND e.g. "qr:gpu-gb10"
8
+
9
+ Optional overrides for a non-production environment:
10
+
11
+ OPENQUANTUM_SCHEDULER_URL, OPENQUANTUM_MANAGEMENT_URL,
12
+ OPENQUANTUM_KEYCLOAK_BASE, OPENQUANTUM_ORGANIZATION_ID,
13
+ OPENQUANTUM_TEST_ENGINE (configuration_data engine, default
14
+ "amber_quantum_rings" for qr:gpu-gb10)
15
+
16
+ This exercises the full path, including Qrisp circuit construction,
17
+ ``to_qiskit()`` conversion, and bit-order handling against real counts.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+
24
+ import pytest
25
+
26
+ HAS_AUTH = bool(
27
+ os.environ.get("OPENQUANTUM_TOKEN")
28
+ or (
29
+ os.environ.get("OPENQUANTUM_CLIENT_ID")
30
+ and os.environ.get("OPENQUANTUM_CLIENT_SECRET")
31
+ )
32
+ )
33
+ TEST_BACKEND = os.environ.get("OPENQUANTUM_TEST_BACKEND")
34
+
35
+ pytestmark = pytest.mark.skipif(
36
+ not (HAS_AUTH and TEST_BACKEND),
37
+ reason="Set OPENQUANTUM_TOKEN (or client id/secret) and "
38
+ "OPENQUANTUM_TEST_BACKEND to run the live integration test.",
39
+ )
40
+
41
+
42
+ def _make_backend(**extra):
43
+ from qrisp_openquantum import OpenQuantumBackend
44
+
45
+ kwargs = dict(extra)
46
+ for env, arg in (
47
+ ("OPENQUANTUM_SCHEDULER_URL", "scheduler_url"),
48
+ ("OPENQUANTUM_MANAGEMENT_URL", "management_url"),
49
+ ("OPENQUANTUM_KEYCLOAK_BASE", "keycloak_base"),
50
+ ("OPENQUANTUM_ORGANIZATION_ID", "organization_id"),
51
+ ):
52
+ if os.environ.get(env):
53
+ kwargs[arg] = os.environ[env]
54
+ # Only the free Quantum Rings simulator requires an engine. Other
55
+ # backends reject unknown configuration keys, so do not inject one
56
+ # unless the caller asked for it or the target is qr:gpu-gb10.
57
+ engine = os.environ.get("OPENQUANTUM_TEST_ENGINE")
58
+ if engine is None and TEST_BACKEND == "qr:gpu-gb10":
59
+ engine = "amber_quantum_rings"
60
+ if engine:
61
+ kwargs.setdefault("configuration_data", {"engine": engine})
62
+ kwargs.setdefault("shots", 1024)
63
+ return OpenQuantumBackend(backend=TEST_BACKEND, **kwargs)
64
+
65
+
66
+ def test_bell_state_live():
67
+ from qrisp import QuantumVariable, h, cx
68
+
69
+ oq = _make_backend()
70
+
71
+ qv = QuantumVariable(2)
72
+ h(qv[0])
73
+ cx(qv[0], qv[1])
74
+
75
+ result = qv.get_measurement(backend=oq, shots=1024)
76
+
77
+ # Bell state: outcomes concentrate on "00" and "11".
78
+ assert isinstance(result, dict)
79
+ assert sum(result.values()) == pytest.approx(1.0, abs=1e-6)
80
+ dominant = {k for k, v in result.items() if v > 0.1}
81
+ assert dominant.issubset({"00", "11"})
82
+
83
+
84
+ def test_asymmetric_state_live():
85
+ """Prepare a QuantumFloat set to 6 and confirm Qrisp's decode end to end.
86
+
87
+ A palindromic Bell pair cannot reveal bit ordering. 6 is 110b; the
88
+ bit-reversed value is 3 (011b), so a wrong reverse_bits default would
89
+ decode as 3.
90
+ """
91
+ from qrisp import QuantumFloat
92
+
93
+ oq = _make_backend(shots=512)
94
+ a = QuantumFloat(3)
95
+ a[:] = 6
96
+ result = a.get_measurement(backend=oq, shots=512)
97
+
98
+ assert isinstance(result, dict)
99
+ top = max(result, key=result.get)
100
+ assert int(top) == 6